diff --git a/consensus/ibft/build.gradle b/consensus/ibft/build.gradle index 32929858e7..6d6a764cda 100644 --- a/consensus/ibft/build.gradle +++ b/consensus/ibft/build.gradle @@ -41,6 +41,7 @@ dependencies { implementation 'com.google.guava:guava' testImplementation project(path: ':ethereum:core', configuration: 'testSupportArtifacts') + testImplementation project(path: ':config:', configuration:'testSupportArtifacts') testImplementation 'junit:junit' testImplementation 'org.awaitility:awaitility' diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftExtraData.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftExtraData.java index ce693158d9..748637a717 100644 --- a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftExtraData.java +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftExtraData.java @@ -22,6 +22,7 @@ import tech.pegasys.pantheon.ethereum.rlp.BytesValueRLPOutput; import tech.pegasys.pantheon.ethereum.rlp.RLPInput; import tech.pegasys.pantheon.util.bytes.BytesValue; +import java.util.Collection; import java.util.List; import java.util.Optional; @@ -34,14 +35,14 @@ public class IbftExtraData { public static final int EXTRA_VANITY_LENGTH = 32; private final BytesValue vanityData; - private final List seals; + private final Collection seals; private final Optional vote; private final int round; private final List
validators; public IbftExtraData( final BytesValue vanityData, - final List seals, + final Collection seals, final Optional vote, final int round, final List
validators) { @@ -127,7 +128,7 @@ public class IbftExtraData { return vanityData; } - public List getSeals() { + public Collection getSeals() { return seals; } diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftHelpers.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftHelpers.java index 6b1c132c60..9e402c2284 100644 --- a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftHelpers.java +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftHelpers.java @@ -12,9 +12,15 @@ */ package tech.pegasys.pantheon.consensus.ibft; +import tech.pegasys.pantheon.crypto.SECP256K1.Signature; +import tech.pegasys.pantheon.ethereum.core.Block; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.BlockHeaderBuilder; import tech.pegasys.pantheon.ethereum.core.Hash; import tech.pegasys.pantheon.ethereum.core.Util; +import java.util.Collection; + public class IbftHelpers { public static final Hash EXPECTED_MIX_HASH = @@ -23,4 +29,26 @@ public class IbftHelpers { public static int calculateRequiredValidatorQuorum(final int validatorCount) { return Util.fastDivCeiling(2 * validatorCount, 3); } + + public static Block createSealedBlock( + final Block block, final Collection commitSeals) { + final BlockHeader initialHeader = block.getHeader(); + final IbftExtraData initialExtraData = IbftExtraData.decode(initialHeader.getExtraData()); + + final IbftExtraData sealedExtraData = + new IbftExtraData( + initialExtraData.getVanityData(), + commitSeals, + initialExtraData.getVote(), + initialExtraData.getRound(), + initialExtraData.getValidators()); + + final BlockHeader sealedHeader = + BlockHeaderBuilder.fromHeader(initialHeader) + .extraData(sealedExtraData.encode()) + .blockHashFunction(IbftBlockHashing::calculateHashOfIbftBlockOnChain) + .buildBlockHeader(); + + return new Block(sealedHeader, block.getBody()); + } } diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftFinalState.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftFinalState.java index 0fbd7a2820..f509366e58 100644 --- a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftFinalState.java +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftFinalState.java @@ -41,6 +41,7 @@ public class IbftFinalState { private final IbftBlockCreatorFactory blockCreatorFactory; private final MessageFactory messageFactory; private final BlockHeaderValidator ibftContextBlockHeaderValidator; + private final IbftMessageTransmitter messageTransmitter; public IbftFinalState( final ValidatorProvider validatorProvider, @@ -63,6 +64,7 @@ public class IbftFinalState { this.blockCreatorFactory = blockCreatorFactory; this.messageFactory = messageFactory; this.ibftContextBlockHeaderValidator = ibftContextBlockHeaderValidator; + this.messageTransmitter = new IbftMessageTransmitter(messageFactory, peers); } public int getQuorumSize() { @@ -116,4 +118,8 @@ public class IbftFinalState { public BlockHeaderValidator getBlockHeaderValidator() { return ibftContextBlockHeaderValidator; } + + public IbftMessageTransmitter getTransmitter() { + return messageTransmitter; + } } diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftMessageTransmitter.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftMessageTransmitter.java new file mode 100644 index 0000000000..d91aad559a --- /dev/null +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftMessageTransmitter.java @@ -0,0 +1,103 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.consensus.ibft.statemachine; + +import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; +import tech.pegasys.pantheon.consensus.ibft.ibftmessage.CommitMessage; +import tech.pegasys.pantheon.consensus.ibft.ibftmessage.NewRoundMessage; +import tech.pegasys.pantheon.consensus.ibft.ibftmessage.PrepareMessage; +import tech.pegasys.pantheon.consensus.ibft.ibftmessage.ProposalMessage; +import tech.pegasys.pantheon.consensus.ibft.ibftmessage.RoundChangeMessage; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.CommitPayload; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.MessageFactory; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.NewRoundPayload; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.PreparePayload; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.PreparedCertificate; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.ProposalPayload; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.RoundChangeCertificate; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.RoundChangePayload; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.SignedData; +import tech.pegasys.pantheon.consensus.ibft.network.IbftNetworkPeers; +import tech.pegasys.pantheon.crypto.SECP256K1.Signature; +import tech.pegasys.pantheon.ethereum.core.Block; +import tech.pegasys.pantheon.ethereum.core.Hash; + +import java.util.Optional; + +public class IbftMessageTransmitter { + + private final MessageFactory messageFactory; + private final IbftNetworkPeers networkPeers; + + public IbftMessageTransmitter( + final MessageFactory messageFactory, final IbftNetworkPeers networkPeers) { + this.messageFactory = messageFactory; + this.networkPeers = networkPeers; + } + + public void multicastProposal(final ConsensusRoundIdentifier roundIdentifier, final Block block) { + final SignedData signedPayload = + messageFactory.createSignedProposalPayload(roundIdentifier, block); + + final ProposalMessage message = ProposalMessage.create(signedPayload); + + networkPeers.multicastToValidators(message); + } + + public void multicastPrepare(final ConsensusRoundIdentifier roundIdentifier, final Hash digest) { + final SignedData signedPayload = + messageFactory.createSignedPreparePayload(roundIdentifier, digest); + + final PrepareMessage message = PrepareMessage.create(signedPayload); + + networkPeers.multicastToValidators(message); + } + + public void multicastCommit( + final ConsensusRoundIdentifier roundIdentifier, + final Hash digest, + final Signature commitSeal) { + final SignedData signedPayload = + messageFactory.createSignedCommitPayload(roundIdentifier, digest, commitSeal); + + final CommitMessage message = CommitMessage.create(signedPayload); + + networkPeers.multicastToValidators(message); + } + + public void multicastRoundChange( + final ConsensusRoundIdentifier roundIdentifier, + final Optional preparedCertificate) { + + final SignedData signedPayload = + messageFactory.createSignedRoundChangePayload(roundIdentifier, preparedCertificate); + + final RoundChangeMessage message = RoundChangeMessage.create(signedPayload); + + networkPeers.multicastToValidators(message); + } + + public void multicastNewRound( + final ConsensusRoundIdentifier roundIdentifier, + final RoundChangeCertificate roundChangeCertificate, + final SignedData proposalPayload) { + + final SignedData signedPayload = + messageFactory.createSignedNewRoundPayload( + roundIdentifier, roundChangeCertificate, proposalPayload); + + final NewRoundMessage message = NewRoundMessage.create(signedPayload); + + networkPeers.multicastToValidators(message); + } +} diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftRound.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftRound.java new file mode 100644 index 0000000000..b18f862935 --- /dev/null +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftRound.java @@ -0,0 +1,187 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.consensus.ibft.statemachine; + +import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; +import tech.pegasys.pantheon.consensus.ibft.IbftBlockHashing; +import tech.pegasys.pantheon.consensus.ibft.IbftContext; +import tech.pegasys.pantheon.consensus.ibft.IbftExtraData; +import tech.pegasys.pantheon.consensus.ibft.IbftHelpers; +import tech.pegasys.pantheon.consensus.ibft.blockcreation.IbftBlockCreator; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.CommitPayload; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.MessageFactory; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.PreparePayload; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.PreparedCertificate; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.ProposalPayload; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.SignedData; +import tech.pegasys.pantheon.crypto.SECP256K1; +import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; +import tech.pegasys.pantheon.crypto.SECP256K1.Signature; +import tech.pegasys.pantheon.ethereum.ProtocolContext; +import tech.pegasys.pantheon.ethereum.chain.MinedBlockObserver; +import tech.pegasys.pantheon.ethereum.core.Block; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.BlockImporter; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.mainnet.HeaderValidationMode; +import tech.pegasys.pantheon.util.Subscribers; + +import java.util.Optional; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class IbftRound { + + private static final Logger LOG = LogManager.getLogger(); + + private final Subscribers observers; + private final RoundState roundState; + private final IbftBlockCreator blockCreator; + private final ProtocolContext protocolContext; + private final BlockImporter blockImporter; + private final KeyPair nodeKeys; + private final MessageFactory messageFactory; // used only to create stored local msgs + private final IbftMessageTransmitter transmitter; + + public IbftRound( + final RoundState roundState, + final IbftBlockCreator blockCreator, + final ProtocolContext protocolContext, + final BlockImporter blockImporter, + final Subscribers observers, + final KeyPair nodeKeys, + final MessageFactory messageFactory, + final IbftMessageTransmitter transmitter) { + this.roundState = roundState; + this.blockCreator = blockCreator; + this.protocolContext = protocolContext; + this.blockImporter = blockImporter; + this.observers = observers; + this.nodeKeys = nodeKeys; + this.messageFactory = messageFactory; + this.transmitter = transmitter; + } + + public ConsensusRoundIdentifier getRoundIdentifier() { + return roundState.getRoundIdentifier(); + } + + public void createAndSendProposalMessage(final long headerTimeStampSeconds) { + LOG.info("Creating proposed block."); + final Block block = blockCreator.createBlock(headerTimeStampSeconds); + transmitter.multicastProposal(roundState.getRoundIdentifier(), block); + + updateStateWithProposedBlock( + messageFactory.createSignedProposalPayload(roundState.getRoundIdentifier(), block)); + } + + public void handleProposalMessage(final SignedData msg) { + LOG.info("Received a Proposal message."); + final Block block = msg.getPayload().getBlock(); + final boolean wasCommitted = roundState.isCommitted(); + + if (updateStateWithProposedBlock(msg)) { + LOG.info("Sending prepare message."); + transmitter.multicastPrepare(getRoundIdentifier(), block.getHash()); + final SignedData localPrepareMessage = + messageFactory.createSignedPreparePayload( + roundState.getRoundIdentifier(), block.getHash()); + peerIsPrepared(localPrepareMessage); + } + + if (wasCommitted != roundState.isCommitted()) { + importBlockToChain(); + } + } + + public void handlePrepareMessage(final SignedData msg) { + LOG.info("Received a prepare message."); + peerIsPrepared(msg); + } + + public void handleCommitMessage(final SignedData msg) { + LOG.info("Received a commit message."); + peerIsCommitted(msg); + } + + public Optional createPrepareCertificate() { + return roundState.constructPreparedCertificate(); + } + + private boolean updateStateWithProposedBlock(final SignedData msg) { + final boolean wasPrepared = roundState.isPrepared(); + final boolean blockAccepted = roundState.setProposedBlock(msg); + if (blockAccepted) { + // There are times handling a proposed block is enough to enter prepared. + if (wasPrepared != roundState.isPrepared()) { + LOG.info("Sending commit message."); + final Block block = roundState.getProposedBlock().get(); + transmitter.multicastCommit(getRoundIdentifier(), block.getHash(), createCommitSeal(block)); + } + final SignedData localCommitMessage = + messageFactory.createSignedCommitPayload( + roundState.getRoundIdentifier(), + msg.getPayload().getBlock().getHash(), + createCommitSeal(roundState.getProposedBlock().get())); + peerIsCommitted(localCommitMessage); + } + + return blockAccepted; + } + + private void peerIsPrepared(final SignedData msg) { + final boolean wasPrepared = roundState.isPrepared(); + roundState.addPrepareMessage(msg); + if (wasPrepared != roundState.isPrepared()) { + LOG.info("Sending commit message."); + final Block block = roundState.getProposedBlock().get(); + transmitter.multicastCommit(getRoundIdentifier(), block.getHash(), createCommitSeal(block)); + } + } + + private void peerIsCommitted(final SignedData msg) { + final boolean wasCommitted = roundState.isCommitted(); + roundState.addCommitMessage(msg); + if (wasCommitted != roundState.isCommitted()) { + importBlockToChain(); + } + } + + private void importBlockToChain() { + final Block blockToImport = + IbftHelpers.createSealedBlock( + roundState.getProposedBlock().get(), roundState.getCommitSeals()); + + LOG.info("Importing block to chain."); + boolean result = + blockImporter.importBlock(protocolContext, blockToImport, HeaderValidationMode.FULL); + if (!result) { + LOG.info("Failed to import block to chain."); + } else { + notifyNewBlockListeners(blockToImport); + } + } + + private Signature createCommitSeal(final Block block) { + final BlockHeader proposedHeader = block.getHeader(); + final IbftExtraData extraData = IbftExtraData.decode(proposedHeader.getExtraData()); + final Hash commitHash = + IbftBlockHashing.calculateDataHashForCommittedSeal(proposedHeader, extraData); + return SECP256K1.sign(commitHash, nodeKeys); + } + + private void notifyNewBlockListeners(final Block block) { + observers.forEach(obs -> obs.blockMined(block)); + } +} diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftRoundFactory.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftRoundFactory.java new file mode 100644 index 0000000000..342d73698e --- /dev/null +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftRoundFactory.java @@ -0,0 +1,73 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.consensus.ibft.statemachine; + +import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; +import tech.pegasys.pantheon.consensus.ibft.IbftContext; +import tech.pegasys.pantheon.consensus.ibft.blockcreation.IbftBlockCreator; +import tech.pegasys.pantheon.consensus.ibft.blockcreation.IbftBlockCreatorFactory; +import tech.pegasys.pantheon.consensus.ibft.validation.MessageValidator; +import tech.pegasys.pantheon.ethereum.ProtocolContext; +import tech.pegasys.pantheon.ethereum.chain.MinedBlockObserver; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSchedule; +import tech.pegasys.pantheon.util.Subscribers; + +public class IbftRoundFactory { + + private final IbftFinalState finalState; + + private final IbftBlockCreatorFactory blockCreatorFactory; + private final ProtocolContext protocolContext; + private final ProtocolSchedule protocolSchedule; + private final Subscribers minedBlockObservers = new Subscribers<>(); + + public IbftRoundFactory( + final IbftFinalState finalState, + final ProtocolContext protocolContext, + final ProtocolSchedule protocolSchedule) { + this.finalState = finalState; + this.blockCreatorFactory = finalState.getBlockCreatorFactory(); + this.protocolContext = protocolContext; + this.protocolSchedule = protocolSchedule; + } + + public IbftRound createNewRound(final BlockHeader parentHeader, final int round) { + long nextBlockHeight = parentHeader.getNumber() + 1; + final ConsensusRoundIdentifier roundIdentifier = + new ConsensusRoundIdentifier(nextBlockHeight, round); + final IbftBlockCreator blockCreator = blockCreatorFactory.create(parentHeader, round); + + final RoundState roundContext = + new RoundState( + roundIdentifier, + finalState.getQuorumSize(), + new MessageValidator( + finalState.getValidators(), + finalState.getProposerForRound(roundIdentifier), + roundIdentifier, + finalState.getBlockHeaderValidator(), + protocolContext, + parentHeader)); + + return new IbftRound( + roundContext, + blockCreator, + protocolContext, + protocolSchedule.getByBlockNumber(nextBlockHeight).getBlockImporter(), + minedBlockObservers, + finalState.getNodeKeys(), + finalState.getMessageFactory(), + finalState.getTransmitter()); + } +} diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/RoundState.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/RoundState.java index 92a72b429e..0cacc207e1 100644 --- a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/RoundState.java +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/RoundState.java @@ -60,27 +60,30 @@ public class RoundState { } public boolean setProposedBlock(final SignedData msg) { - if (validator.addSignedProposalPayload(msg)) { - proposalMessage = Optional.of(msg); - } else { - return false; + + if (!proposalMessage.isPresent()) { + if (validator.addSignedProposalPayload(msg)) { + proposalMessage = Optional.of(msg); + preparePayloads.removeIf(p -> !validator.validatePrepareMessage(p)); + commitPayloads.removeIf(p -> !validator.validateCommmitMessage(p)); + updateState(); + return true; + } } - preparePayloads.removeIf(p -> !validator.validatePrepareMessage(p)); - commitPayloads.removeIf(p -> !validator.validateCommmitMessage(p)); - updateState(); - return true; + + return false; } - public void addPreparedPeer(final SignedData prepareMsg) { - if (!proposalMessage.isPresent() || validator.validatePrepareMessage(prepareMsg)) { - preparePayloads.add(prepareMsg); + public void addPrepareMessage(final SignedData msg) { + if (!proposalMessage.isPresent() || validator.validatePrepareMessage(msg)) { + preparePayloads.add(msg); } updateState(); } - public void addCommitSeal(final SignedData commitPayload) { - if (!proposalMessage.isPresent() || validator.validateCommmitMessage(commitPayload)) { - commitPayloads.add(commitPayload); + public void addCommitMessage(final SignedData msg) { + if (!proposalMessage.isPresent() || validator.validateCommmitMessage(msg)) { + commitPayloads.add(msg); } updateState(); diff --git a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftRoundTest.java b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftRoundTest.java new file mode 100644 index 0000000000..38f5786b08 --- /dev/null +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftRoundTest.java @@ -0,0 +1,251 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.consensus.ibft.statemachine; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import tech.pegasys.pantheon.consensus.common.VoteProposer; +import tech.pegasys.pantheon.consensus.common.VoteTally; +import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; +import tech.pegasys.pantheon.consensus.ibft.IbftBlockHashing; +import tech.pegasys.pantheon.consensus.ibft.IbftContext; +import tech.pegasys.pantheon.consensus.ibft.IbftExtraData; +import tech.pegasys.pantheon.consensus.ibft.blockcreation.IbftBlockCreator; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.MessageFactory; +import tech.pegasys.pantheon.consensus.ibft.validation.MessageValidator; +import tech.pegasys.pantheon.crypto.SECP256K1; +import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; +import tech.pegasys.pantheon.crypto.SECP256K1.Signature; +import tech.pegasys.pantheon.ethereum.ProtocolContext; +import tech.pegasys.pantheon.ethereum.chain.MinedBlockObserver; +import tech.pegasys.pantheon.ethereum.chain.MutableBlockchain; +import tech.pegasys.pantheon.ethereum.core.Block; +import tech.pegasys.pantheon.ethereum.core.BlockBody; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.BlockHeaderTestFixture; +import tech.pegasys.pantheon.ethereum.core.BlockImporter; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.db.WorldStateArchive; +import tech.pegasys.pantheon.util.Subscribers; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.math.BigInteger; +import java.util.Collections; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class IbftRoundTest { + + private final KeyPair localNodeKeys = KeyPair.generate(); + private final ConsensusRoundIdentifier roundIdentifier = new ConsensusRoundIdentifier(1, 1); + private final MessageFactory messageFactory = new MessageFactory(localNodeKeys); + private ProtocolContext protocolContext; + + @Mock private MutableBlockchain blockChain; + @Mock private WorldStateArchive worldStateArchive; + @Mock private BlockImporter blockImporter; + @Mock private Subscribers subscribers; + @Mock private IbftMessageTransmitter transmitter; + + @Mock private IbftBlockCreator blockCreator; + @Mock private MessageValidator messageValidator; + + private Block proposedBlock; + private IbftExtraData proposedExtraData; + + final Signature remoteCommitSeal = Signature.create(BigInteger.ONE, BigInteger.ONE, (byte) 1); + + @Before + public void setup() { + protocolContext = + new ProtocolContext<>( + blockChain, + worldStateArchive, + new IbftContext(new VoteTally(Collections.emptyList()), new VoteProposer())); + + when(messageValidator.addSignedProposalPayload(any())).thenReturn(true); + when(messageValidator.validatePrepareMessage(any())).thenReturn(true); + when(messageValidator.validateCommmitMessage(any())).thenReturn(true); + + proposedExtraData = + new IbftExtraData( + BytesValue.wrap(new byte[32]), + Collections.emptyList(), + Optional.empty(), + 0, + Collections.emptyList()); + BlockHeaderTestFixture headerTestFixture = new BlockHeaderTestFixture(); + headerTestFixture.extraData(proposedExtraData.encode()); + headerTestFixture.number(1); + + BlockHeader header = headerTestFixture.buildHeader(); + proposedBlock = + new Block(header, new BlockBody(Collections.emptyList(), Collections.emptyList())); + + when(blockCreator.createBlock(anyLong())).thenReturn(proposedBlock); + + when(blockImporter.importBlock(any(), any(), any())).thenReturn(true); + } + + @Test + public void onReceptionOfValidProposalSendsAPrepareToNetworkPeers() { + final RoundState roundState = new RoundState(roundIdentifier, 3, messageValidator); + final IbftRound round = + new IbftRound( + roundState, + blockCreator, + protocolContext, + blockImporter, + subscribers, + localNodeKeys, + messageFactory, + transmitter); + + round.handleProposalMessage( + messageFactory.createSignedProposalPayload(roundIdentifier, proposedBlock)); + verify(transmitter, times(1)).multicastPrepare(roundIdentifier, proposedBlock.getHash()); + verify(transmitter, never()).multicastCommit(any(), any(), any()); + } + + @Test + public void sendsAProposalWhenRequested() { + final RoundState roundState = new RoundState(roundIdentifier, 3, messageValidator); + final IbftRound round = + new IbftRound( + roundState, + blockCreator, + protocolContext, + blockImporter, + subscribers, + localNodeKeys, + messageFactory, + transmitter); + + round.createAndSendProposalMessage(15); + verify(transmitter, times(1)).multicastProposal(roundIdentifier, proposedBlock); + verify(transmitter, never()).multicastPrepare(any(), any()); + verify(transmitter, never()).multicastCommit(any(), any(), any()); + } + + @Test + public void singleValidatorImportBlocksImmediatelyOnProposalCreation() { + final RoundState roundState = new RoundState(roundIdentifier, 1, messageValidator); + final IbftRound round = + new IbftRound( + roundState, + blockCreator, + protocolContext, + blockImporter, + subscribers, + localNodeKeys, + messageFactory, + transmitter); + round.createAndSendProposalMessage(15); + verify(transmitter, times(1)).multicastProposal(roundIdentifier, proposedBlock); + verify(transmitter, never()).multicastPrepare(any(), any()); + verify(transmitter, times(1)).multicastCommit(any(), any(), any()); + verify(blockImporter, times(1)).importBlock(any(), any(), any()); + } + + @Test + public void twoValidatorNetworkSendsPrepareOnProposalReceptionThenSendsCommitOnCommitReceive() { + final RoundState roundState = new RoundState(roundIdentifier, 2, messageValidator); + final IbftRound round = + new IbftRound( + roundState, + blockCreator, + protocolContext, + blockImporter, + subscribers, + localNodeKeys, + messageFactory, + transmitter); + + final Hash commitSealHash = + IbftBlockHashing.calculateDataHashForCommittedSeal( + proposedBlock.getHeader(), proposedExtraData); + final Signature localCommitSeal = SECP256K1.sign(commitSealHash, localNodeKeys); + + // Receive Proposal Message + round.handleProposalMessage( + messageFactory.createSignedProposalPayload(roundIdentifier, proposedBlock)); + verify(transmitter, times(1)).multicastPrepare(roundIdentifier, proposedBlock.getHash()); + verify(transmitter, times(1)) + .multicastCommit(roundIdentifier, proposedBlock.getHash(), localCommitSeal); + verify(blockImporter, never()).importBlock(any(), any(), any()); + + // Receive Commit Message + + round.handleCommitMessage( + messageFactory.createSignedCommitPayload( + roundIdentifier, proposedBlock.getHash(), remoteCommitSeal)); + + // Should import block when both commit seals are available. + ArgumentCaptor capturedBlock = ArgumentCaptor.forClass(Block.class); + verify(blockImporter, times(1)).importBlock(any(), capturedBlock.capture(), any()); + + // Ensure imported block contains both commit seals. + IbftExtraData importedExtraData = + IbftExtraData.decode(capturedBlock.getValue().getHeader().getExtraData()); + assertThat(importedExtraData.getSeals()).containsOnly(remoteCommitSeal, localCommitSeal); + } + + @Test + public void localNodeProposesToNetworkOfTwoValidatorsImportsOnReceptionOfCommitFromPeer() { + final RoundState roundState = new RoundState(roundIdentifier, 2, messageValidator); + final IbftRound round = + new IbftRound( + roundState, + blockCreator, + protocolContext, + blockImporter, + subscribers, + localNodeKeys, + messageFactory, + transmitter); + + final Hash commitSealHash = + IbftBlockHashing.calculateDataHashForCommittedSeal( + proposedBlock.getHeader(), proposedExtraData); + final Signature localCommitSeal = SECP256K1.sign(commitSealHash, localNodeKeys); + + round.createAndSendProposalMessage(15); + verify(transmitter, never()).multicastCommit(any(), any(), any()); + verify(blockImporter, never()).importBlock(any(), any(), any()); + + round.handlePrepareMessage( + messageFactory.createSignedPreparePayload(roundIdentifier, proposedBlock.getHash())); + + verify(transmitter, times(1)) + .multicastCommit(roundIdentifier, proposedBlock.getHash(), localCommitSeal); + verify(blockImporter, never()).importBlock(any(), any(), any()); + + round.handleCommitMessage( + messageFactory.createSignedCommitPayload( + roundIdentifier, proposedBlock.getHash(), remoteCommitSeal)); + verify(blockImporter, times(1)).importBlock(any(), any(), any()); + } +} diff --git a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/statemachine/RoundStateTest.java b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/statemachine/RoundStateTest.java index 65e090351e..1cfef6ea71 100644 --- a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/statemachine/RoundStateTest.java +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/statemachine/RoundStateTest.java @@ -127,7 +127,7 @@ public class RoundStateTest { block.getHash(), Signature.create(BigInteger.ONE, BigInteger.ONE, (byte) 1)); - roundState.addCommitSeal(commit); + roundState.addCommitMessage(commit); assertThat(roundState.isPrepared()).isTrue(); assertThat(roundState.isCommitted()).isTrue(); assertThat(roundState.constructPreparedCertificate()).isNotEmpty(); @@ -150,12 +150,12 @@ public class RoundStateTest { .get(2) .createSignedPreparePayload(roundIdentifier, block.getHash()); - roundState.addPreparedPeer(firstPrepare); + roundState.addPrepareMessage(firstPrepare); assertThat(roundState.isPrepared()).isFalse(); assertThat(roundState.isCommitted()).isFalse(); assertThat(roundState.constructPreparedCertificate()).isEmpty(); - roundState.addPreparedPeer(secondPrepare); + roundState.addPrepareMessage(secondPrepare); assertThat(roundState.isPrepared()).isFalse(); assertThat(roundState.isCommitted()).isFalse(); assertThat(roundState.constructPreparedCertificate()).isEmpty(); @@ -188,8 +188,8 @@ public class RoundStateTest { when(messageValidator.validatePrepareMessage(firstPrepare)).thenReturn(true); when(messageValidator.validatePrepareMessage(secondPrepare)).thenReturn(false); - roundState.addPreparedPeer(firstPrepare); - roundState.addPreparedPeer(secondPrepare); + roundState.addPrepareMessage(firstPrepare); + roundState.addPrepareMessage(secondPrepare); verify(messageValidator, never()).validatePrepareMessage(any()); final SignedData proposal = @@ -225,10 +225,10 @@ public class RoundStateTest { roundState.setProposedBlock(proposal); assertThat(roundState.isPrepared()).isFalse(); - roundState.addPreparedPeer(firstPrepare); + roundState.addPrepareMessage(firstPrepare); assertThat(roundState.isPrepared()).isFalse(); - roundState.addPreparedPeer(secondPrepare); + roundState.addPrepareMessage(secondPrepare); assertThat(roundState.isPrepared()).isTrue(); } @@ -259,9 +259,9 @@ public class RoundStateTest { validatorMessageFactories.get(0).createSignedProposalPayload(roundIdentifier, block); roundState.setProposedBlock(proposal); - roundState.addCommitSeal(firstCommit); + roundState.addCommitMessage(firstCommit); assertThat(roundState.isCommitted()).isFalse(); - roundState.addCommitSeal(secondCommit); + roundState.addCommitMessage(secondCommit); assertThat(roundState.isCommitted()).isTrue(); assertThat(roundState.getCommitSeals())