mirror of https://github.com/hyperledger/besu
Ibft Integration Test - Future Rounds (#542)
parent
872b27491c
commit
07f8bb84a6
@ -0,0 +1,147 @@ |
|||||||
|
/* |
||||||
|
* 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.injectEmptyNewRound; |
||||||
|
|
||||||
|
import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; |
||||||
|
import tech.pegasys.pantheon.consensus.ibft.IbftHelpers; |
||||||
|
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.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.consensus.ibft.support.ValidatorPeer; |
||||||
|
import tech.pegasys.pantheon.crypto.SECP256K1; |
||||||
|
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; |
||||||
|
|
||||||
|
public class FutureRoundTest { |
||||||
|
|
||||||
|
private final long blockTimeStamp = 100; |
||||||
|
private final Clock fixedClock = |
||||||
|
Clock.fixed(Instant.ofEpochSecond(blockTimeStamp), ZoneId.systemDefault()); |
||||||
|
|
||||||
|
private final int NETWORK_SIZE = 5; |
||||||
|
|
||||||
|
// Local node is not the first proposer for the current round.
|
||||||
|
private final TestContext context = |
||||||
|
TestContextFactory.createTestEnvironment(NETWORK_SIZE, 0, fixedClock); |
||||||
|
|
||||||
|
private final ConsensusRoundIdentifier roundId = new ConsensusRoundIdentifier(1, 0); |
||||||
|
private final RoundSpecificNodeRoles roles = context.getRoundSpecificRoles(roundId); |
||||||
|
|
||||||
|
private final ConsensusRoundIdentifier futureRoundId = new ConsensusRoundIdentifier(1, 5); |
||||||
|
private final RoundSpecificNodeRoles futureRoles = context.getRoundSpecificRoles(futureRoundId); |
||||||
|
|
||||||
|
private final MessageFactory localNodeMessageFactory = context.getLocalNodeMessageFactory(); |
||||||
|
|
||||||
|
@Before |
||||||
|
public void setup() { |
||||||
|
context.getController().start(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void messagesForFutureRoundAreNotActionedUntilRoundIsActive() { |
||||||
|
final Block futureBlock = context.createBlockForProposal(futureRoundId.getRoundNumber(), 60); |
||||||
|
final int quorum = IbftHelpers.calculateRequiredValidatorQuorum(NETWORK_SIZE); |
||||||
|
final ConsensusRoundIdentifier subsequentRoundId = new ConsensusRoundIdentifier(1, 6); |
||||||
|
final RoundSpecificNodeRoles subsequentRoles = context.getRoundSpecificRoles(subsequentRoundId); |
||||||
|
|
||||||
|
// required remotely received Prepares = quorum-2
|
||||||
|
// required remote received commits = quorum-1
|
||||||
|
|
||||||
|
// Inject 1 too few Commit messages (but sufficient Prepare
|
||||||
|
for (int i = 0; i < quorum - 3; i++) { |
||||||
|
futureRoles.getNonProposingPeer(i).injectPrepare(futureRoundId, futureBlock.getHash()); |
||||||
|
} |
||||||
|
|
||||||
|
for (int i = 0; i < quorum - 2; i++) { |
||||||
|
futureRoles.getNonProposingPeer(i).injectCommit(futureRoundId, futureBlock.getHash()); |
||||||
|
} |
||||||
|
|
||||||
|
// inject a prepare and a commit from a subsequent round, and ensure no transmissions are
|
||||||
|
// created
|
||||||
|
subsequentRoles.getNonProposingPeer(1).injectPrepare(subsequentRoundId, futureBlock.getHash()); |
||||||
|
subsequentRoles.getNonProposingPeer(1).injectCommit(subsequentRoundId, futureBlock.getHash()); |
||||||
|
|
||||||
|
assertPeersReceivedNoMessages(roles.getAllPeers()); |
||||||
|
assertThat(context.getBlockchain().getChainHeadBlockNumber()).isEqualTo(0); |
||||||
|
|
||||||
|
// inject a newRound to move to 'futureRoundId', and ensure localnode sends prepare, commit
|
||||||
|
// and updates blockchain
|
||||||
|
injectEmptyNewRound( |
||||||
|
futureRoundId, futureRoles.getProposer(), futureRoles.getAllPeers(), futureBlock); |
||||||
|
|
||||||
|
final SignedData<PreparePayload> expectedPrepare = |
||||||
|
localNodeMessageFactory.createSignedPreparePayload(futureRoundId, futureBlock.getHash()); |
||||||
|
|
||||||
|
assertPeersReceivedExactly(futureRoles.getAllPeers(), expectedPrepare); |
||||||
|
|
||||||
|
// following 1 more prepare, a commit msg will be sent
|
||||||
|
futureRoles.getNonProposingPeer(quorum - 3).injectPrepare(futureRoundId, futureBlock.getHash()); |
||||||
|
|
||||||
|
final SignedData<CommitPayload> expectedCommit = |
||||||
|
localNodeMessageFactory.createSignedCommitPayload( |
||||||
|
futureRoundId, |
||||||
|
futureBlock.getHash(), |
||||||
|
SECP256K1.sign(futureBlock.getHash(), context.getLocalNodeParams().getNodeKeyPair())); |
||||||
|
assertPeersReceivedExactly(futureRoles.getAllPeers(), expectedCommit); |
||||||
|
|
||||||
|
// requires 1 more commit and the blockchain will progress
|
||||||
|
futureRoles.getNonProposingPeer(quorum - 2).injectCommit(futureRoundId, futureBlock.getHash()); |
||||||
|
|
||||||
|
assertThat(context.getBlockchain().getChainHeadBlockNumber()).isEqualTo(1); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void priorRoundsCannotBeCompletedAfterReceptionOfNewRound() { |
||||||
|
final Block initialBlock = context.createBlockForProposal(roundId.getRoundNumber(), 30); |
||||||
|
final Block futureBlock = context.createBlockForProposal(futureRoundId.getRoundNumber(), 60); |
||||||
|
|
||||||
|
roles.getProposer().injectProposal(roundId, initialBlock); |
||||||
|
for (final ValidatorPeer peer : roles.getNonProposingPeers()) { |
||||||
|
peer.injectPrepare(roundId, initialBlock.getHash()); |
||||||
|
} |
||||||
|
roles.getProposer().injectCommit(roundId, initialBlock.getHash()); |
||||||
|
// At this stage, the local node has 2 commit msgs (proposer and local) so has not committed
|
||||||
|
assertThat(context.getBlockchain().getChainHeadBlockNumber()).isEqualTo(0); |
||||||
|
|
||||||
|
for (final ValidatorPeer peer : roles.getAllPeers()) { |
||||||
|
peer.clearReceivedMessages(); |
||||||
|
} |
||||||
|
|
||||||
|
injectEmptyNewRound( |
||||||
|
futureRoundId, futureRoles.getProposer(), futureRoles.getAllPeers(), futureBlock); |
||||||
|
|
||||||
|
final SignedData<PreparePayload> expectedFuturePrepare = |
||||||
|
localNodeMessageFactory.createSignedPreparePayload(futureRoundId, futureBlock.getHash()); |
||||||
|
assertPeersReceivedExactly(roles.getAllPeers(), expectedFuturePrepare); |
||||||
|
|
||||||
|
// attempt to complete the previous round
|
||||||
|
roles.getNonProposingPeers().get(0).injectCommit(roundId, initialBlock.getHash()); |
||||||
|
assertPeersReceivedNoMessages(roles.getAllPeers()); |
||||||
|
assertThat(context.getBlockchain().getChainHeadBlockNumber()).isEqualTo(0); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,231 @@ |
|||||||
|
/* |
||||||
|
* 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 java.util.Optional.empty; |
||||||
|
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.createValidPreparedCertificate; |
||||||
|
|
||||||
|
import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; |
||||||
|
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.RoundChangeCertificate; |
||||||
|
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.RoundChangePayload; |
||||||
|
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.consensus.ibft.support.TestHelpers; |
||||||
|
import tech.pegasys.pantheon.consensus.ibft.support.ValidatorPeer; |
||||||
|
import tech.pegasys.pantheon.ethereum.core.Block; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
import java.util.Optional; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
import org.assertj.core.util.Lists; |
||||||
|
import org.junit.Before; |
||||||
|
import org.junit.Test; |
||||||
|
|
||||||
|
/** Ensure the Ibft component responds appropriately when a NewRound message is received. */ |
||||||
|
public class ReceivedNewRoundTest { |
||||||
|
|
||||||
|
private final TestContext context = TestContextFactory.createTestEnvWithArbitraryClock(5, 0); |
||||||
|
private final ConsensusRoundIdentifier roundId = new ConsensusRoundIdentifier(1, 0); |
||||||
|
private final RoundSpecificNodeRoles roles = context.getRoundSpecificRoles(roundId); |
||||||
|
|
||||||
|
private final MessageFactory localNodeMessageFactory = context.getLocalNodeMessageFactory(); |
||||||
|
|
||||||
|
@Before |
||||||
|
public void setup() { |
||||||
|
context.getController().start(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void newRoundMessageWithEmptyPrepareCertificatesOfferNewBlock() { |
||||||
|
final ConsensusRoundIdentifier nextRoundId = new ConsensusRoundIdentifier(1, 1); |
||||||
|
final Block blockToPropose = context.createBlockForProposal(nextRoundId.getRoundNumber(), 15); |
||||||
|
final ConsensusRoundIdentifier targetRound = new ConsensusRoundIdentifier(1, 1); |
||||||
|
|
||||||
|
final List<SignedData<RoundChangePayload>> roundChanges = |
||||||
|
roles |
||||||
|
.getAllPeers() |
||||||
|
.stream() |
||||||
|
.map(p -> p.getMessageFactory().createSignedRoundChangePayload(targetRound, empty())) |
||||||
|
.collect(Collectors.toList()); |
||||||
|
|
||||||
|
final ValidatorPeer nextProposer = context.getRoundSpecificRoles(nextRoundId).getProposer(); |
||||||
|
|
||||||
|
nextProposer.injectNewRound( |
||||||
|
targetRound, |
||||||
|
new RoundChangeCertificate(roundChanges), |
||||||
|
nextProposer.getMessageFactory().createSignedProposalPayload(targetRound, blockToPropose)); |
||||||
|
|
||||||
|
final SignedData<PreparePayload> expectedPrepare = |
||||||
|
localNodeMessageFactory.createSignedPreparePayload(targetRound, blockToPropose.getHash()); |
||||||
|
|
||||||
|
assertPeersReceivedExactly(roles.getAllPeers(), expectedPrepare); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void newRoundMessageFromIllegalSenderIsDiscardedAndNoPrepareForNewRoundIsSent() { |
||||||
|
final ConsensusRoundIdentifier nextRoundId = new ConsensusRoundIdentifier(1, 1); |
||||||
|
final Block blockToPropose = context.createBlockForProposal(nextRoundId.getRoundNumber(), 15); |
||||||
|
|
||||||
|
final List<SignedData<RoundChangePayload>> roundChanges = |
||||||
|
roles |
||||||
|
.getAllPeers() |
||||||
|
.stream() |
||||||
|
.map(p -> p.getMessageFactory().createSignedRoundChangePayload(nextRoundId, empty())) |
||||||
|
.collect(Collectors.toList()); |
||||||
|
|
||||||
|
final ValidatorPeer illegalProposer = |
||||||
|
context.getRoundSpecificRoles(nextRoundId).getNonProposingPeer(0); |
||||||
|
|
||||||
|
illegalProposer.injectNewRound( |
||||||
|
nextRoundId, |
||||||
|
new RoundChangeCertificate(roundChanges), |
||||||
|
illegalProposer |
||||||
|
.getMessageFactory() |
||||||
|
.createSignedProposalPayload(nextRoundId, blockToPropose)); |
||||||
|
|
||||||
|
assertPeersReceivedNoMessages(roles.getAllPeers()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void newRoundWithPrepareCertificateResultsInNewRoundStartingWithExpectedBlock() { |
||||||
|
final Block initialBlock = context.createBlockForProposal(0, 15); |
||||||
|
final Block reproposedBlock = context.createBlockForProposal(1, 15); |
||||||
|
final ConsensusRoundIdentifier nextRoundId = new ConsensusRoundIdentifier(1, 1); |
||||||
|
|
||||||
|
final PreparedCertificate preparedCertificate = |
||||||
|
createValidPreparedCertificate(context, roundId, initialBlock); |
||||||
|
|
||||||
|
final List<SignedData<RoundChangePayload>> roundChanges = |
||||||
|
roles |
||||||
|
.getAllPeers() |
||||||
|
.stream() |
||||||
|
.map( |
||||||
|
p -> |
||||||
|
p.getMessageFactory() |
||||||
|
.createSignedRoundChangePayload( |
||||||
|
nextRoundId, Optional.of(preparedCertificate))) |
||||||
|
.collect(Collectors.toList()); |
||||||
|
|
||||||
|
final ValidatorPeer nextProposer = context.getRoundSpecificRoles(nextRoundId).getProposer(); |
||||||
|
|
||||||
|
nextProposer.injectNewRound( |
||||||
|
nextRoundId, |
||||||
|
new RoundChangeCertificate(roundChanges), |
||||||
|
roles |
||||||
|
.getNonProposingPeer(0) |
||||||
|
.getMessageFactory() |
||||||
|
.createSignedProposalPayload(nextRoundId, reproposedBlock)); |
||||||
|
|
||||||
|
assertPeersReceivedExactly( |
||||||
|
roles.getAllPeers(), |
||||||
|
localNodeMessageFactory.createSignedPreparePayload(nextRoundId, reproposedBlock.getHash())); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void newRoundMessageForPriorRoundIsNotActioned() { |
||||||
|
// first move to a future round, then inject a newRound for a prior round, local node
|
||||||
|
// should send no messages.
|
||||||
|
final ConsensusRoundIdentifier futureRound = new ConsensusRoundIdentifier(1, 2); |
||||||
|
for (final ValidatorPeer peer : roles.getAllPeers()) { |
||||||
|
peer.injectRoundChange(futureRound, empty()); |
||||||
|
} |
||||||
|
|
||||||
|
final ConsensusRoundIdentifier interimRound = new ConsensusRoundIdentifier(1, 1); |
||||||
|
final List<SignedData<RoundChangePayload>> roundChangePayloads = Lists.newArrayList(); |
||||||
|
for (final ValidatorPeer peer : roles.getAllPeers()) { |
||||||
|
roundChangePayloads.add( |
||||||
|
peer.getMessageFactory().createSignedRoundChangePayload(interimRound, empty())); |
||||||
|
} |
||||||
|
|
||||||
|
final ValidatorPeer interimRoundProposer = |
||||||
|
context.getRoundSpecificRoles(interimRound).getProposer(); |
||||||
|
|
||||||
|
final SignedData<ProposalPayload> proposal = |
||||||
|
interimRoundProposer |
||||||
|
.getMessageFactory() |
||||||
|
.createSignedProposalPayload(interimRound, context.createBlockForProposal(1, 30)); |
||||||
|
|
||||||
|
interimRoundProposer.injectNewRound( |
||||||
|
interimRound, new RoundChangeCertificate(roundChangePayloads), proposal); |
||||||
|
|
||||||
|
assertPeersReceivedNoMessages(roles.getAllPeers()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void receiveRoundStateIsNotLostIfASecondNewRoundMessageIsReceivedForCurrentRound() { |
||||||
|
final Block initialBlock = context.createBlockForProposal(0, 15); |
||||||
|
final Block reproposedBlock = context.createBlockForProposal(1, 15); |
||||||
|
final ConsensusRoundIdentifier nextRoundId = new ConsensusRoundIdentifier(1, 1); |
||||||
|
|
||||||
|
final PreparedCertificate preparedCertificate = |
||||||
|
createValidPreparedCertificate(context, roundId, initialBlock); |
||||||
|
|
||||||
|
final List<SignedData<RoundChangePayload>> roundChanges = |
||||||
|
roles |
||||||
|
.getAllPeers() |
||||||
|
.stream() |
||||||
|
.map( |
||||||
|
p -> |
||||||
|
p.getMessageFactory() |
||||||
|
.createSignedRoundChangePayload( |
||||||
|
nextRoundId, Optional.of(preparedCertificate))) |
||||||
|
.collect(Collectors.toList()); |
||||||
|
|
||||||
|
final RoundSpecificNodeRoles nextRoles = context.getRoundSpecificRoles(nextRoundId); |
||||||
|
final ValidatorPeer nextProposer = nextRoles.getProposer(); |
||||||
|
|
||||||
|
nextProposer.injectNewRound( |
||||||
|
nextRoundId, |
||||||
|
new RoundChangeCertificate(roundChanges), |
||||||
|
roles |
||||||
|
.getNonProposingPeer(0) |
||||||
|
.getMessageFactory() |
||||||
|
.createSignedProposalPayload(nextRoundId, reproposedBlock)); |
||||||
|
|
||||||
|
assertPeersReceivedExactly( |
||||||
|
roles.getAllPeers(), |
||||||
|
localNodeMessageFactory.createSignedPreparePayload(nextRoundId, reproposedBlock.getHash())); |
||||||
|
|
||||||
|
// Inject a prepare, then re-inject the newRound - then ensure only a single prepare is enough
|
||||||
|
// to trigger a Commit transmission from the local node
|
||||||
|
nextRoles.getNonProposingPeer(0).injectPrepare(nextRoundId, reproposedBlock.getHash()); |
||||||
|
|
||||||
|
nextProposer.injectNewRound( |
||||||
|
nextRoundId, |
||||||
|
new RoundChangeCertificate(roundChanges), |
||||||
|
roles |
||||||
|
.getNonProposingPeer(0) |
||||||
|
.getMessageFactory() |
||||||
|
.createSignedProposalPayload(nextRoundId, reproposedBlock)); |
||||||
|
|
||||||
|
assertPeersReceivedNoMessages(roles.getAllPeers()); |
||||||
|
|
||||||
|
nextRoles.getNonProposingPeer(1).injectPrepare(nextRoundId, reproposedBlock.getHash()); |
||||||
|
|
||||||
|
final SignedData<CommitPayload> expectedCommit = |
||||||
|
TestHelpers.createSignedCommentPayload( |
||||||
|
reproposedBlock, context.getLocalNodeParams().getNodeKeyPair(), nextRoundId); |
||||||
|
|
||||||
|
assertPeersReceivedExactly(nextRoles.getAllPeers(), expectedCommit); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue