diff --git a/consensus/ibft/src/integration-test/java/tech/pegasys/pantheon/consensus/ibft/support/RoundSpecificNodeRoles.java b/consensus/ibft/src/integration-test/java/tech/pegasys/pantheon/consensus/ibft/support/RoundSpecificNodeRoles.java index b514367c8e..6a7bb30ac9 100644 --- a/consensus/ibft/src/integration-test/java/tech/pegasys/pantheon/consensus/ibft/support/RoundSpecificNodeRoles.java +++ b/consensus/ibft/src/integration-test/java/tech/pegasys/pantheon/consensus/ibft/support/RoundSpecificNodeRoles.java @@ -41,4 +41,8 @@ public class RoundSpecificNodeRoles { public List getNonProposingPeers() { return nonProposingPeers; } + + public ValidatorPeer getNonProposingPeer(final int index) { + return nonProposingPeers.get(index); + } } diff --git a/consensus/ibft/src/integration-test/java/tech/pegasys/pantheon/consensus/ibft/support/TestHelpers.java b/consensus/ibft/src/integration-test/java/tech/pegasys/pantheon/consensus/ibft/support/TestHelpers.java new file mode 100644 index 0000000000..942a659c44 --- /dev/null +++ b/consensus/ibft/src/integration-test/java/tech/pegasys/pantheon/consensus/ibft/support/TestHelpers.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019 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.support; + +import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; +import tech.pegasys.pantheon.consensus.ibft.IbftBlockHashing; +import tech.pegasys.pantheon.consensus.ibft.IbftExtraData; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.CommitPayload; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.MessageFactory; +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.core.Block; + +public class TestHelpers { + + public static SignedData createSignedCommentPayload( + final Block block, final KeyPair signingKeyPair, final ConsensusRoundIdentifier roundId) { + + final IbftExtraData extraData = IbftExtraData.decode(block.getHeader().getExtraData()); + + final Signature commitSeal = + SECP256K1.sign( + IbftBlockHashing.calculateDataHashForCommittedSeal(block.getHeader(), extraData), + signingKeyPair); + + final MessageFactory messageFactory = new MessageFactory(signingKeyPair); + + return messageFactory.createSignedCommitPayload(roundId, block.getHash(), commitSeal); + } +} diff --git a/consensus/ibft/src/integration-test/java/tech/pegasys/pantheon/consensus/ibft/tests/LocalNodeIsProposerTest.java b/consensus/ibft/src/integration-test/java/tech/pegasys/pantheon/consensus/ibft/tests/LocalNodeIsProposerTest.java new file mode 100644 index 0000000000..49296d9268 --- /dev/null +++ b/consensus/ibft/src/integration-test/java/tech/pegasys/pantheon/consensus/ibft/tests/LocalNodeIsProposerTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2019 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.tests; + +import static org.assertj.core.api.Assertions.assertThat; +import static tech.pegasys.pantheon.consensus.ibft.support.MessageReceptionHelpers.assertPeersReceivedExactly; +import static tech.pegasys.pantheon.consensus.ibft.support.MessageReceptionHelpers.assertPeersReceivedNoMessages; +import static tech.pegasys.pantheon.consensus.ibft.support.TestHelpers.createSignedCommentPayload; + +import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; +import tech.pegasys.pantheon.consensus.ibft.ibftevent.BlockTimerExpiry; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.CommitPayload; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.MessageFactory; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.ProposalPayload; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.SignedData; +import tech.pegasys.pantheon.consensus.ibft.support.RoundSpecificNodeRoles; +import tech.pegasys.pantheon.consensus.ibft.support.TestContext; +import tech.pegasys.pantheon.consensus.ibft.support.TestContextFactory; +import tech.pegasys.pantheon.ethereum.core.Block; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; + +import org.junit.Before; +import org.junit.Test; + +/** + * These tests assume the basic function of the Ibft Round State Machine has been proven via the + * LocalNodeIsNotProposerTest. + */ +public class LocalNodeIsProposerTest { + + final long blockTimeStamp = 100; + private final Clock fixedClock = + Clock.fixed(Instant.ofEpochSecond(blockTimeStamp), ZoneId.systemDefault()); + + // Local node will propose the first block + private final TestContext context = TestContextFactory.createTestEnvironment(4, 1, fixedClock); + private final ConsensusRoundIdentifier roundId = new ConsensusRoundIdentifier(1, 0); + private final RoundSpecificNodeRoles roles = context.getRoundSpecificRoles(roundId); + + private final MessageFactory localNodeMessageFactory = context.getLocalNodeMessageFactory(); + + private Block expectedProposedBlock; + private SignedData expectedTxProposal; + private SignedData expectedTxCommit; + + @Before + public void setup() { + expectedProposedBlock = context.createBlockForProposal(0, blockTimeStamp); + expectedTxProposal = + localNodeMessageFactory.createSignedProposalPayload(roundId, expectedProposedBlock); + + expectedTxCommit = + createSignedCommentPayload( + expectedProposedBlock, context.getLocalNodeParams().getNodeKeyPair(), roundId); + + // Start the Controller, and trigger "block timer" to send proposal. + context.getController().start(); + context.getController().handleBlockTimerExpiry(new BlockTimerExpiry(roundId)); + } + + @Test + public void basicCase() { + assertPeersReceivedExactly(roles.getAllPeers(), expectedTxProposal); + + // NOTE: In these test roles.getProposer() will return NULL. + roles.getNonProposingPeer(0).injectPrepare(roundId, expectedProposedBlock.getHash()); + assertPeersReceivedNoMessages(roles.getAllPeers()); + + roles.getNonProposingPeer(1).injectPrepare(roundId, expectedProposedBlock.getHash()); + assertPeersReceivedExactly(roles.getAllPeers(), expectedTxCommit); + + roles.getNonProposingPeer(1).injectCommit(roundId, expectedProposedBlock.getHash()); + assertThat(context.getBlockchain().getChainHeadBlockNumber()).isEqualTo(0); + assertPeersReceivedNoMessages(roles.getAllPeers()); + + roles.getNonProposingPeer(2).injectCommit(roundId, expectedProposedBlock.getHash()); + assertThat(context.getBlockchain().getChainHeadBlockNumber()).isEqualTo(1); + assertPeersReceivedNoMessages(roles.getAllPeers()); + } + + @Test + public void importsToChainWithoutReceivingPrepareMessages() { + assertPeersReceivedExactly(roles.getAllPeers(), expectedTxProposal); + + roles.getNonProposingPeer(1).injectCommit(roundId, expectedProposedBlock.getHash()); + assertThat(context.getBlockchain().getChainHeadBlockNumber()).isEqualTo(0); + assertPeersReceivedNoMessages(roles.getAllPeers()); + + roles.getNonProposingPeer(2).injectCommit(roundId, expectedProposedBlock.getHash()); + assertThat(context.getBlockchain().getChainHeadBlockNumber()).isEqualTo(1); + assertPeersReceivedNoMessages(roles.getAllPeers()); + } +} diff --git a/consensus/ibft/src/integration-test/java/tech/pegasys/pantheon/consensus/ibft/tests/LocalNodeNotProposerTest.java b/consensus/ibft/src/integration-test/java/tech/pegasys/pantheon/consensus/ibft/tests/LocalNodeNotProposerTest.java index 3241f94a45..efbd256ce5 100644 --- a/consensus/ibft/src/integration-test/java/tech/pegasys/pantheon/consensus/ibft/tests/LocalNodeNotProposerTest.java +++ b/consensus/ibft/src/integration-test/java/tech/pegasys/pantheon/consensus/ibft/tests/LocalNodeNotProposerTest.java @@ -74,23 +74,23 @@ public class LocalNodeNotProposerTest { assertPeersReceivedExactly(roles.getAllPeers(), expectedTxPrepare); - roles.getNonProposingPeers().get(0).injectPrepare(roundId, blockToPropose.getHash()); + roles.getNonProposingPeer(0).injectPrepare(roundId, blockToPropose.getHash()); assertPeersReceivedExactly(roles.getAllPeers(), expectedTxCommit); // Ensure the local blockchain has NOT incremented yet. assertThat(context.getCurrentChainHeight()).isEqualTo(0); // NO further messages should be transmitted when another Prepare is received. - roles.getNonProposingPeers().get(1).injectPrepare(roundId, blockToPropose.getHash()); + roles.getNonProposingPeer(1).injectPrepare(roundId, blockToPropose.getHash()); assertPeersReceivedNoMessages(roles.getAllPeers()); // Inject a commit, ensure blockChain is not updated, and no message are sent (not quorum yet) - roles.getNonProposingPeers().get(0).injectCommit(roundId, blockToPropose.getHash()); + roles.getNonProposingPeer(0).injectCommit(roundId, blockToPropose.getHash()); assertPeersReceivedNoMessages(roles.getAllPeers()); assertThat(context.getCurrentChainHeight()).isEqualTo(0); // A second commit message means quorum is reached, and blockchain should be updated. - roles.getNonProposingPeers().get(1).injectCommit(roundId, blockToPropose.getHash()); + roles.getNonProposingPeer(1).injectCommit(roundId, blockToPropose.getHash()); assertPeersReceivedNoMessages(roles.getAllPeers()); assertThat(context.getCurrentChainHeight()).isEqualTo(1); @@ -110,16 +110,16 @@ public class LocalNodeNotProposerTest { assertPeersReceivedNoMessages(roles.getAllPeers()); assertThat(context.getCurrentChainHeight()).isEqualTo(0); - roles.getNonProposingPeers().get(1).injectPrepare(roundId, blockToPropose.getHash()); + roles.getNonProposingPeer(1).injectPrepare(roundId, blockToPropose.getHash()); assertPeersReceivedExactly(roles.getAllPeers(), expectedTxCommit); // Inject a commit, ensure blockChain is not updated, and no message are sent (not quorum yet) - roles.getNonProposingPeers().get(0).injectCommit(roundId, blockToPropose.getHash()); + roles.getNonProposingPeer(0).injectCommit(roundId, blockToPropose.getHash()); assertPeersReceivedNoMessages(roles.getAllPeers()); assertThat(context.getCurrentChainHeight()).isEqualTo(0); // A second commit message means quorum is reached, and blockchain should be updated. - roles.getNonProposingPeers().get(1).injectCommit(roundId, blockToPropose.getHash()); + roles.getNonProposingPeer(1).injectCommit(roundId, blockToPropose.getHash()); assertPeersReceivedNoMessages(roles.getAllPeers()); assertThat(context.getCurrentChainHeight()).isEqualTo(1); } @@ -160,7 +160,7 @@ public class LocalNodeNotProposerTest { assertPeersReceivedExactly(roles.getAllPeers(), expectedTxPrepare); assertThat(context.getCurrentChainHeight()).isEqualTo(1); - roles.getNonProposingPeers().get(0).injectPrepare(roundId, blockToPropose.getHash()); + roles.getNonProposingPeer(0).injectPrepare(roundId, blockToPropose.getHash()); assertPeersReceivedExactly(roles.getAllPeers(), expectedTxCommit); assertThat(context.getCurrentChainHeight()).isEqualTo(1); } diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftBlockHeightManagerFactory.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftBlockHeightManagerFactory.java index 1b51e27b9c..bcee816bca 100644 --- a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftBlockHeightManagerFactory.java +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftBlockHeightManagerFactory.java @@ -17,8 +17,6 @@ import tech.pegasys.pantheon.consensus.ibft.validation.MessageValidatorFactory; import tech.pegasys.pantheon.ethereum.ProtocolContext; import tech.pegasys.pantheon.ethereum.core.BlockHeader; -import java.time.Clock; - public class IbftBlockHeightManagerFactory { private final IbftRoundFactory roundFactory; @@ -48,7 +46,7 @@ public class IbftBlockHeightManagerFactory { (roundIdentifier) -> messageValidatorFactory.createMessageValidator(roundIdentifier, parentHeader)), roundFactory, - Clock.systemUTC(), + finalState.getClock(), messageValidatorFactory); } }