From ce56ea8cb4ad981137092b575dd25ee65c8ee9ea Mon Sep 17 00:00:00 2001 From: Trent Mohay <37158202+rain-on@users.noreply.github.com> Date: Fri, 15 Feb 2019 13:55:36 +1100 Subject: [PATCH] Break out RoundChangeCertificate validation (#864) With upcoming changes to remove the NewRound message, and place the RoundChangeCertificate in the Proposal, it was decided to break out the RoundChangeCertiifcate validation into a separate file to minimise changes during message restructuring. --- .../validation/MessageValidatorFactory.java | 12 +- .../validation/NewRoundMessageValidator.java | 59 +---- .../validation/NewRoundPayloadValidator.java | 56 +---- .../RoundChangeCertificateValidator.java | 125 +++++++++++ .../NewRoundMessageValidatorTest.java | 108 ++-------- .../NewRoundSignedDataValidatorTest.java | 63 ++---- .../RoundChangeCertificateValidatorTest.java | 201 ++++++++++++++++++ 7 files changed, 378 insertions(+), 246 deletions(-) create mode 100644 consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeCertificateValidator.java create mode 100644 consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeCertificateValidatorTest.java diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidatorFactory.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidatorFactory.java index 3877166d2f..1e41c038ea 100644 --- a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidatorFactory.java +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidatorFactory.java @@ -81,15 +81,19 @@ public NewRoundMessageValidator createNewRoundValidator(final long chainHeight) final BlockValidator blockValidator = protocolSchedule.getByBlockNumber(chainHeight).getBlockValidator(); + final RoundChangeCertificateValidator roundChangeCertificateValidator = + new RoundChangeCertificateValidator( + validators, this::createSignedDataValidator, chainHeight); + return new NewRoundMessageValidator( new NewRoundPayloadValidator( - validators, proposerSelector, this::createSignedDataValidator, - IbftHelpers.calculateRequiredValidatorQuorum(validators.size()), - chainHeight), + chainHeight, + roundChangeCertificateValidator), new ProposalBlockConsistencyValidator(), blockValidator, - protocolContext); + protocolContext, + roundChangeCertificateValidator); } } diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundMessageValidator.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundMessageValidator.java index 06cd05bb47..dbdedb6aa1 100644 --- a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundMessageValidator.java +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundMessageValidator.java @@ -12,24 +12,14 @@ */ package tech.pegasys.pantheon.consensus.ibft.validation; -import static tech.pegasys.pantheon.consensus.ibft.IbftHelpers.findLatestPreparedCertificate; - -import tech.pegasys.pantheon.consensus.ibft.IbftBlockHashing; -import tech.pegasys.pantheon.consensus.ibft.IbftBlockInterface; import tech.pegasys.pantheon.consensus.ibft.IbftContext; import tech.pegasys.pantheon.consensus.ibft.messagewrappers.NewRound; -import tech.pegasys.pantheon.consensus.ibft.payload.NewRoundPayload; -import tech.pegasys.pantheon.consensus.ibft.payload.PreparedCertificate; -import tech.pegasys.pantheon.consensus.ibft.payload.RoundChangeCertificate; -import tech.pegasys.pantheon.consensus.ibft.payload.RoundChangePayload; -import tech.pegasys.pantheon.consensus.ibft.payload.SignedData; import tech.pegasys.pantheon.ethereum.BlockValidator; import tech.pegasys.pantheon.ethereum.BlockValidator.BlockProcessingOutputs; import tech.pegasys.pantheon.ethereum.ProtocolContext; import tech.pegasys.pantheon.ethereum.core.Block; import tech.pegasys.pantheon.ethereum.mainnet.HeaderValidationMode; -import java.util.Collection; import java.util.Optional; import org.apache.logging.log4j.LogManager; @@ -43,16 +33,19 @@ public class NewRoundMessageValidator { private final ProposalBlockConsistencyValidator proposalConsistencyValidator; private final BlockValidator blockValidator; private final ProtocolContext protocolContext; + private final RoundChangeCertificateValidator roundChangeCertificateValidator; public NewRoundMessageValidator( final NewRoundPayloadValidator payloadValidator, final ProposalBlockConsistencyValidator proposalConsistencyValidator, final BlockValidator blockValidator, - final ProtocolContext protocolContext) { + final ProtocolContext protocolContext, + final RoundChangeCertificateValidator roundChangeCertificateValidator) { this.payloadValidator = payloadValidator; this.proposalConsistencyValidator = proposalConsistencyValidator; this.blockValidator = blockValidator; this.protocolContext = protocolContext; + this.roundChangeCertificateValidator = roundChangeCertificateValidator; } public boolean validateNewRoundMessage(final NewRound msg) { @@ -61,8 +54,8 @@ public boolean validateNewRoundMessage(final NewRound msg) { return false; } - if (!validateProposalMessageMatchesLatestPrepareCertificate( - msg.getSignedPayload().getPayload(), msg.getBlock())) { + if (!roundChangeCertificateValidator.validateProposalMessageMatchesLatestPrepareCertificate( + msg.getRoundChangeCertificate(), msg.getBlock())) { LOG.debug( "Illegal NewRound message, piggybacked block does not match latest PrepareCertificate"); return false; @@ -88,44 +81,4 @@ private boolean validateBlock(final Block block) { return true; } - - private boolean validateProposalMessageMatchesLatestPrepareCertificate( - final NewRoundPayload payload, final Block proposedBlock) { - - final RoundChangeCertificate roundChangeCert = payload.getRoundChangeCertificate(); - final Collection> roundChangePayloads = - roundChangeCert.getRoundChangePayloads(); - - final Optional latestPreparedCertificate = - findLatestPreparedCertificate(roundChangePayloads); - - if (!latestPreparedCertificate.isPresent()) { - LOG.trace( - "No round change messages have a preparedCertificate, any valid block may be proposed."); - return true; - } - - // Need to check that if we substitute the LatestPrepareCert round number into the supplied - // block that we get the SAME hash as PreparedCert. - final Block currentBlockWithOldRound = - IbftBlockInterface.replaceRoundInBlock( - proposedBlock, - latestPreparedCertificate - .get() - .getProposalPayload() - .getPayload() - .getRoundIdentifier() - .getRoundNumber(), - IbftBlockHashing::calculateDataHashForCommittedSeal); - - if (!currentBlockWithOldRound - .getHash() - .equals(latestPreparedCertificate.get().getProposalPayload().getPayload().getDigest())) { - LOG.info( - "Invalid NewRound message, block in latest RoundChange does not match proposed block."); - return false; - } - - return true; - } } diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundPayloadValidator.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundPayloadValidator.java index 1f5ffac9c6..60993a6414 100644 --- a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundPayloadValidator.java +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundPayloadValidator.java @@ -12,20 +12,15 @@ */ package tech.pegasys.pantheon.consensus.ibft.validation; -import static tech.pegasys.pantheon.consensus.ibft.IbftHelpers.prepareMessageCountForQuorum; - import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; import tech.pegasys.pantheon.consensus.ibft.blockcreation.ProposerSelector; import tech.pegasys.pantheon.consensus.ibft.payload.NewRoundPayload; import tech.pegasys.pantheon.consensus.ibft.payload.ProposalPayload; import tech.pegasys.pantheon.consensus.ibft.payload.RoundChangeCertificate; -import tech.pegasys.pantheon.consensus.ibft.payload.RoundChangePayload; import tech.pegasys.pantheon.consensus.ibft.payload.SignedData; import tech.pegasys.pantheon.consensus.ibft.validation.RoundChangePayloadValidator.MessageValidatorForHeightFactory; import tech.pegasys.pantheon.ethereum.core.Address; -import java.util.Collection; - import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -33,23 +28,20 @@ public class NewRoundPayloadValidator { private static final Logger LOG = LogManager.getLogger(); - private final Collection
validators; private final ProposerSelector proposerSelector; private final MessageValidatorForHeightFactory messageValidatorFactory; - private final long quorum; private final long chainHeight; + private final RoundChangeCertificateValidator roundChangeCertificateValidator; public NewRoundPayloadValidator( - final Collection
validators, final ProposerSelector proposerSelector, final MessageValidatorForHeightFactory messageValidatorFactory, - final long quorum, - final long chainHeight) { - this.validators = validators; + final long chainHeight, + final RoundChangeCertificateValidator roundChangeCertificateValidator) { this.proposerSelector = proposerSelector; this.messageValidatorFactory = messageValidatorFactory; - this.quorum = quorum; this.chainHeight = chainHeight; + this.roundChangeCertificateValidator = roundChangeCertificateValidator; } public boolean validateNewRoundMessage(final SignedData msg) { @@ -82,49 +74,11 @@ public boolean validateNewRoundMessage(final SignedData msg) { return false; } - if (!validateRoundChangeMessagesAndEnsureTargetRoundMatchesRoot( + if (!roundChangeCertificateValidator.validateRoundChangeMessagesAndEnsureTargetRoundMatchesRoot( rootRoundIdentifier, roundChangeCert)) { return false; } return true; } - - private boolean validateRoundChangeMessagesAndEnsureTargetRoundMatchesRoot( - final ConsensusRoundIdentifier expectedRound, final RoundChangeCertificate roundChangeCert) { - - final Collection> roundChangeMsgs = - roundChangeCert.getRoundChangePayloads(); - - if (roundChangeMsgs.size() < quorum) { - LOG.info( - "Invalid NewRound message, RoundChange certificate has insufficient " - + "RoundChange messages."); - return false; - } - - if (!roundChangeCert.getRoundChangePayloads().stream() - .allMatch(p -> p.getPayload().getRoundIdentifier().equals(expectedRound))) { - LOG.info( - "Invalid NewRound message, not all embedded RoundChange messages have a " - + "matching target round."); - return false; - } - - for (final SignedData roundChangeMsg : - roundChangeCert.getRoundChangePayloads()) { - final RoundChangePayloadValidator roundChangeValidator = - new RoundChangePayloadValidator( - messageValidatorFactory, - validators, - prepareMessageCountForQuorum(quorum), - chainHeight); - - if (!roundChangeValidator.validateRoundChange(roundChangeMsg)) { - LOG.info("Invalid NewRound message, embedded RoundChange message failed validation."); - return false; - } - } - return true; - } } diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeCertificateValidator.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeCertificateValidator.java new file mode 100644 index 0000000000..e7ea6badff --- /dev/null +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeCertificateValidator.java @@ -0,0 +1,125 @@ +/* + * 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.validation; + +import static tech.pegasys.pantheon.consensus.ibft.IbftHelpers.findLatestPreparedCertificate; +import static tech.pegasys.pantheon.consensus.ibft.IbftHelpers.prepareMessageCountForQuorum; + +import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; +import tech.pegasys.pantheon.consensus.ibft.IbftBlockHashing; +import tech.pegasys.pantheon.consensus.ibft.IbftBlockInterface; +import tech.pegasys.pantheon.consensus.ibft.IbftHelpers; +import tech.pegasys.pantheon.consensus.ibft.payload.PreparedCertificate; +import tech.pegasys.pantheon.consensus.ibft.payload.RoundChangeCertificate; +import tech.pegasys.pantheon.consensus.ibft.payload.RoundChangePayload; +import tech.pegasys.pantheon.consensus.ibft.payload.SignedData; +import tech.pegasys.pantheon.consensus.ibft.validation.RoundChangePayloadValidator.MessageValidatorForHeightFactory; +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.Block; + +import java.util.Collection; +import java.util.Optional; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class RoundChangeCertificateValidator { + + private static final Logger LOG = LogManager.getLogger(); + + private final Collection
validators; + private final MessageValidatorForHeightFactory messageValidatorFactory; + private final long quorum; + private final long chainHeight; + + public RoundChangeCertificateValidator( + final Collection
validators, + final MessageValidatorForHeightFactory messageValidatorFactory, + final long chainHeight) { + this.validators = validators; + this.messageValidatorFactory = messageValidatorFactory; + this.quorum = IbftHelpers.calculateRequiredValidatorQuorum(validators.size()); + this.chainHeight = chainHeight; + } + + public boolean validateRoundChangeMessagesAndEnsureTargetRoundMatchesRoot( + final ConsensusRoundIdentifier expectedRound, final RoundChangeCertificate roundChangeCert) { + + final Collection> roundChangeMsgs = + roundChangeCert.getRoundChangePayloads(); + + if (roundChangeMsgs.size() < quorum) { + LOG.info("Invalid RoundChangeCertificate, insufficient RoundChange messages."); + return false; + } + + if (!roundChangeCert.getRoundChangePayloads().stream() + .allMatch(p -> p.getPayload().getRoundIdentifier().equals(expectedRound))) { + LOG.info( + "Invalid RoundChangeCertificate, not all embedded RoundChange messages have a " + + "matching target round."); + return false; + } + + final RoundChangePayloadValidator roundChangeValidator = + new RoundChangePayloadValidator( + messageValidatorFactory, validators, prepareMessageCountForQuorum(quorum), chainHeight); + + if (!roundChangeCert.getRoundChangePayloads().stream() + .allMatch(roundChangeValidator::validateRoundChange)) { + LOG.info("Invalid NewRound message, embedded RoundChange message failed validation."); + return false; + } + + return true; + } + + public boolean validateProposalMessageMatchesLatestPrepareCertificate( + final RoundChangeCertificate roundChangeCert, final Block proposedBlock) { + + final Collection> roundChangePayloads = + roundChangeCert.getRoundChangePayloads(); + + final Optional latestPreparedCertificate = + findLatestPreparedCertificate(roundChangePayloads); + + if (!latestPreparedCertificate.isPresent()) { + LOG.trace( + "No round change messages have a preparedCertificate, any valid block may be proposed."); + return true; + } + + // Need to check that if we substitute the LatestPrepareCert round number into the supplied + // block that we get the SAME hash as PreparedCert. + final Block currentBlockWithOldRound = + IbftBlockInterface.replaceRoundInBlock( + proposedBlock, + latestPreparedCertificate + .get() + .getProposalPayload() + .getPayload() + .getRoundIdentifier() + .getRoundNumber(), + IbftBlockHashing::calculateDataHashForCommittedSeal); + + if (!currentBlockWithOldRound + .getHash() + .equals(latestPreparedCertificate.get().getProposalPayload().getPayload().getDigest())) { + LOG.info( + "Invalid NewRound message, block in latest RoundChange does not match proposed block."); + return false; + } + + return true; + } +} diff --git a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundMessageValidatorTest.java b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundMessageValidatorTest.java index 95ecb63030..2ae60932ef 100644 --- a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundMessageValidatorTest.java +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundMessageValidatorTest.java @@ -13,7 +13,6 @@ package tech.pegasys.pantheon.consensus.ibft.validation; import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -28,7 +27,6 @@ import tech.pegasys.pantheon.consensus.ibft.messagewrappers.Proposal; import tech.pegasys.pantheon.consensus.ibft.payload.MessageFactory; import tech.pegasys.pantheon.consensus.ibft.payload.RoundChangeCertificate; -import tech.pegasys.pantheon.consensus.ibft.statemachine.PreparedRoundArtifacts; import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; import tech.pegasys.pantheon.ethereum.BlockValidator; import tech.pegasys.pantheon.ethereum.BlockValidator.BlockProcessingOutputs; @@ -67,6 +65,8 @@ public class NewRoundMessageValidatorTest { private ProposalBlockConsistencyValidator proposalBlockConsistencyValidator = mock(ProposalBlockConsistencyValidator.class); + private final RoundChangeCertificateValidator roundChangeCertificateValidator = + mock(RoundChangeCertificateValidator.class); @Mock private BlockValidator blockValidator; private ProtocolContext protocolContext; @@ -79,13 +79,21 @@ public void setup() { when(blockValidator.validateAndProcessBlock(any(), any(), any(), any())) .thenReturn(Optional.of(new BlockProcessingOutputs(null, null))); + when(roundChangeCertificateValidator.validateProposalMessageMatchesLatestPrepareCertificate( + any(), any())) + .thenReturn(true); + protocolContext = new ProtocolContext<>( mock(MutableBlockchain.class), mock(WorldStateArchive.class), mock(IbftContext.class)); validator = new NewRoundMessageValidator( - payloadValidator, proposalBlockConsistencyValidator, blockValidator, protocolContext); + payloadValidator, + proposalBlockConsistencyValidator, + blockValidator, + protocolContext, + roundChangeCertificateValidator); when(proposalBlockConsistencyValidator.validateProposalMatchesBlock(any(), any())) .thenReturn(true); @@ -156,93 +164,19 @@ public void validationFailsIfUnderlyingSignedDataValidatorFails() { } @Test - public void newRoundWithProposalNotMatchingLatestRoundChangeFails() { - final ConsensusRoundIdentifier preparedRound = TestHelpers.createFrom(roundIdentifier, 0, -1); - // The previous proposedBlock has been constructed with less validators, so is thus not - // identical - // to the proposedBlock in the new proposal (so should fail). - final Block prevProposedBlock = - TestHelpers.createProposalBlock(validators.subList(0, 1), preparedRound); - - final PreparedRoundArtifacts mismatchedRoundArtefacts = - new PreparedRoundArtifacts( - proposerMessageFactory.createProposal(preparedRound, prevProposedBlock), - singletonList( - validatorMessageFactory.createPrepare(preparedRound, prevProposedBlock.getHash()))); - - final NewRound invalidMsg = - proposerMessageFactory.createNewRound( - roundIdentifier, - new RoundChangeCertificate( - singletonList( - validatorMessageFactory - .createRoundChange(roundIdentifier, Optional.of(mismatchedRoundArtefacts)) - .getSignedPayload())), - proposerMessageFactory - .createProposal(roundIdentifier, proposedBlock) - .getSignedPayload(), - proposedBlock); - - assertThat(validator.validateNewRoundMessage(invalidMsg)).isFalse(); - } + public void roundChangeCertificateDoesntContainSuppliedBlockFails() { + when(roundChangeCertificateValidator.validateProposalMessageMatchesLatestPrepareCertificate( + any(), any())) + .thenReturn(false); - @Test - public void lastestPreparedCertificateMatchesNewRoundProposalIsSuccessful() { - final ConsensusRoundIdentifier latterPrepareRound = - TestHelpers.createFrom(roundIdentifier, 0, -1); - final Block latterBlock = TestHelpers.createProposalBlock(validators, latterPrepareRound); - final Proposal latterProposal = - proposerMessageFactory.createProposal(latterPrepareRound, latterBlock); - final Optional latterTerminatedRoundArtefacts = - Optional.of( - new PreparedRoundArtifacts( - latterProposal, - Lists.newArrayList( - validatorMessageFactory.createPrepare( - latterPrepareRound, proposedBlock.getHash())))); - - // An earlier PrepareCert is added to ensure the path to find the latest PrepareCert - // is correctly followed. - final ConsensusRoundIdentifier earlierPreparedRound = - new ConsensusRoundIdentifier( - roundIdentifier.getSequenceNumber(), roundIdentifier.getRoundNumber() - 2); - final Block earlierBlock = - TestHelpers.createProposalBlock(validators.subList(0, 1), earlierPreparedRound); - final Proposal earlierProposal = - proposerMessageFactory.createProposal(earlierPreparedRound, earlierBlock); - final Optional earlierTerminatedRoundArtefacts = - Optional.of( - new PreparedRoundArtifacts( - earlierProposal, - Lists.newArrayList( - validatorMessageFactory.createPrepare( - earlierPreparedRound, earlierBlock.getHash())))); - - final RoundChangeCertificate roundChangeCert = - new RoundChangeCertificate( - Lists.newArrayList( - proposerMessageFactory - .createRoundChange(roundIdentifier, earlierTerminatedRoundArtefacts) - .getSignedPayload(), - validatorMessageFactory - .createRoundChange(roundIdentifier, latterTerminatedRoundArtefacts) - .getSignedPayload())); - - // Ensure a message containing the earlier proposal fails - final NewRound newRoundWithEarlierProposal = + final Proposal proposal = proposerMessageFactory.createProposal(roundIdentifier, proposedBlock); + final NewRound message = proposerMessageFactory.createNewRound( roundIdentifier, - roundChangeCert, - earlierProposal.getSignedPayload(), - earlierProposal.getBlock()); - assertThat(validator.validateNewRoundMessage(newRoundWithEarlierProposal)).isFalse(); + new RoundChangeCertificate(emptyList()), + proposal.getSignedPayload(), + proposal.getBlock()); - final NewRound newRoundWithLatterProposal = - proposerMessageFactory.createNewRound( - roundIdentifier, - roundChangeCert, - latterProposal.getSignedPayload(), - latterProposal.getBlock()); - assertThat(validator.validateNewRoundMessage(newRoundWithLatterProposal)).isTrue(); + assertThat(validator.validateNewRoundMessage(message)).isFalse(); } } diff --git a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundSignedDataValidatorTest.java b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundSignedDataValidatorTest.java index 637df0384c..fe8839c54e 100644 --- a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundSignedDataValidatorTest.java +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundSignedDataValidatorTest.java @@ -25,14 +25,12 @@ import tech.pegasys.pantheon.consensus.ibft.payload.MessageFactory; import tech.pegasys.pantheon.consensus.ibft.payload.NewRoundPayload; import tech.pegasys.pantheon.consensus.ibft.payload.RoundChangeCertificate; -import tech.pegasys.pantheon.consensus.ibft.statemachine.PreparedRoundArtifacts; import tech.pegasys.pantheon.consensus.ibft.validation.RoundChangePayloadValidator.MessageValidatorForHeightFactory; import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; import tech.pegasys.pantheon.ethereum.core.Address; import tech.pegasys.pantheon.ethereum.core.Block; import tech.pegasys.pantheon.ethereum.core.Util; -import java.util.Collections; import java.util.List; import java.util.Optional; @@ -58,6 +56,8 @@ public class NewRoundSignedDataValidatorTest { private final MessageValidatorForHeightFactory validatorFactory = mock(MessageValidatorForHeightFactory.class); private final SignedDataValidator signedDataValidator = mock(SignedDataValidator.class); + private final RoundChangeCertificateValidator roundChangeCertificateValidator = + mock(RoundChangeCertificateValidator.class); private Block proposedBlock; private NewRound validMsg; @@ -81,9 +81,13 @@ public void setup() { when(signedDataValidator.validateProposal(any())).thenReturn(true); when(signedDataValidator.validatePrepare(any())).thenReturn(true); + when(roundChangeCertificateValidator.validateRoundChangeMessagesAndEnsureTargetRoundMatchesRoot( + any(), any())) + .thenReturn(true); + validator = new NewRoundPayloadValidator( - validators, proposerSelector, validatorFactory, 1, chainHeight); + proposerSelector, validatorFactory, chainHeight, roundChangeCertificateValidator); } /* NOTE: All test herein assume that the Proposer is the expected transmitter of the NewRound @@ -149,54 +153,11 @@ public void newRoundTargetingDifferentSequenceNumberFails() { } @Test - public void newRoundWithEmptyRoundChangeCertificateFails() { - msgBuilder.setRoundChangeCertificate(new RoundChangeCertificate(Collections.emptyList())); - - final NewRound inValidMsg = signPayload(msgBuilder.build(), proposerKey, proposedBlock); - - assertThat(validator.validateNewRoundMessage(inValidMsg.getSignedPayload())).isFalse(); - } - - @Test - public void roundChangeMessagesDoNotAllTargetRoundOfNewRoundMsgFails() { - final ConsensusRoundIdentifier prevRound = TestHelpers.createFrom(roundIdentifier, 0, -1); - - final RoundChangeCertificate.Builder roundChangeBuilder = new RoundChangeCertificate.Builder(); - roundChangeBuilder.appendRoundChangeMessage( - proposerMessageFactory.createRoundChange(roundIdentifier, Optional.empty())); - roundChangeBuilder.appendRoundChangeMessage( - proposerMessageFactory.createRoundChange(prevRound, Optional.empty())); - - msgBuilder.setRoundChangeCertificate(roundChangeBuilder.buildCertificate()); - - final NewRound msg = signPayload(msgBuilder.build(), proposerKey, proposedBlock); - - assertThat(validator.validateNewRoundMessage(msg.getSignedPayload())).isFalse(); - } - - @Test - public void invalidEmbeddedRoundChangeMessageFails() { - final ConsensusRoundIdentifier prevRound = TestHelpers.createFrom(roundIdentifier, 0, -1); - - final RoundChangeCertificate.Builder roundChangeBuilder = new RoundChangeCertificate.Builder(); - roundChangeBuilder.appendRoundChangeMessage( - proposerMessageFactory.createRoundChange( - roundIdentifier, - Optional.of( - new PreparedRoundArtifacts( - proposerMessageFactory.createProposal(prevRound, proposedBlock), - Lists.newArrayList( - validatorMessageFactory.createPrepare( - prevRound, proposedBlock.getHash())))))); - - msgBuilder.setRoundChangeCertificate(roundChangeBuilder.buildCertificate()); - - // The prepare Message in the RoundChange Cert will be deemed illegal. - when(signedDataValidator.validatePrepare(any())).thenReturn(false); - - final NewRound msg = signPayload(msgBuilder.build(), proposerKey, proposedBlock); - - assertThat(validator.validateNewRoundMessage(msg.getSignedPayload())).isFalse(); + public void roundChangeCertificateFailsValidation() { + when(roundChangeCertificateValidator.validateRoundChangeMessagesAndEnsureTargetRoundMatchesRoot( + any(), any())) + .thenReturn(false); + assertThat(validator.validateNewRoundMessage(validMsg.getSignedPayload())).isFalse(); } @Test diff --git a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeCertificateValidatorTest.java b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeCertificateValidatorTest.java new file mode 100644 index 0000000000..a503de8c01 --- /dev/null +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeCertificateValidatorTest.java @@ -0,0 +1,201 @@ +/* + * 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.validation; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; +import tech.pegasys.pantheon.consensus.ibft.TestHelpers; +import tech.pegasys.pantheon.consensus.ibft.messagewrappers.Proposal; +import tech.pegasys.pantheon.consensus.ibft.payload.MessageFactory; +import tech.pegasys.pantheon.consensus.ibft.payload.RoundChangeCertificate; +import tech.pegasys.pantheon.consensus.ibft.statemachine.PreparedRoundArtifacts; +import tech.pegasys.pantheon.consensus.ibft.validation.RoundChangePayloadValidator.MessageValidatorForHeightFactory; +import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.Block; +import tech.pegasys.pantheon.ethereum.core.Util; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import com.google.common.collect.Lists; +import org.junit.Before; +import org.junit.Test; + +public class RoundChangeCertificateValidatorTest { + + private final KeyPair proposerKey = KeyPair.generate(); + private final KeyPair validatorKey = KeyPair.generate(); + private final KeyPair otherValidatorKey = KeyPair.generate(); + private final MessageFactory proposerMessageFactory = new MessageFactory(proposerKey); + private final MessageFactory validatorMessageFactory = new MessageFactory(validatorKey); + private final List
validators = Lists.newArrayList(); + private final long chainHeight = 2; + private final ConsensusRoundIdentifier roundIdentifier = + new ConsensusRoundIdentifier(chainHeight, 4); + private RoundChangeCertificateValidator validator; + + private final MessageValidatorForHeightFactory validatorFactory = + mock(MessageValidatorForHeightFactory.class); + private final SignedDataValidator signedDataValidator = mock(SignedDataValidator.class); + + private Block proposedBlock; + + @Before + public void setup() { + validators.add(Util.publicKeyToAddress(proposerKey.getPublicKey())); + validators.add(Util.publicKeyToAddress(validatorKey.getPublicKey())); + validators.add(Util.publicKeyToAddress(otherValidatorKey.getPublicKey())); + + proposedBlock = TestHelpers.createProposalBlock(validators, roundIdentifier); + + validator = new RoundChangeCertificateValidator(validators, validatorFactory, 5); + } + + @Test + public void newRoundWithEmptyRoundChangeCertificateFails() { + final RoundChangeCertificate cert = new RoundChangeCertificate(Collections.emptyList()); + + assertThat( + validator.validateRoundChangeMessagesAndEnsureTargetRoundMatchesRoot( + roundIdentifier, cert)) + .isFalse(); + } + + @Test + public void roundChangeMessagesDoNotAllTargetRoundFails() { + final ConsensusRoundIdentifier prevRound = TestHelpers.createFrom(roundIdentifier, 0, -1); + + final RoundChangeCertificate.Builder roundChangeBuilder = new RoundChangeCertificate.Builder(); + roundChangeBuilder.appendRoundChangeMessage( + proposerMessageFactory.createRoundChange(roundIdentifier, Optional.empty())); + roundChangeBuilder.appendRoundChangeMessage( + proposerMessageFactory.createRoundChange(prevRound, Optional.empty())); + + assertThat( + validator.validateRoundChangeMessagesAndEnsureTargetRoundMatchesRoot( + roundIdentifier, roundChangeBuilder.buildCertificate())) + .isFalse(); + } + + @Test + public void invalidPrepareMessageInOnePrepareCertificateFails() { + final ConsensusRoundIdentifier prevRound = TestHelpers.createFrom(roundIdentifier, 0, -1); + + final RoundChangeCertificate.Builder roundChangeBuilder = new RoundChangeCertificate.Builder(); + roundChangeBuilder.appendRoundChangeMessage( + proposerMessageFactory.createRoundChange( + roundIdentifier, + Optional.of( + new PreparedRoundArtifacts( + proposerMessageFactory.createProposal(prevRound, proposedBlock), + Lists.newArrayList( + validatorMessageFactory.createPrepare( + prevRound, proposedBlock.getHash())))))); + + // The prepare Message in the RoundChange Cert will be deemed illegal. + when(signedDataValidator.validatePrepare(any())).thenReturn(false); + + assertThat( + validator.validateRoundChangeMessagesAndEnsureTargetRoundMatchesRoot( + roundIdentifier, roundChangeBuilder.buildCertificate())) + .isFalse(); + } + + @Test + public void detectsTheSuppliedBlockIsNotInLatestPrepareCertificate() { + final ConsensusRoundIdentifier preparedRound = TestHelpers.createFrom(roundIdentifier, 0, -1); + // The previous proposedBlock has been constructed with less validators, so is thus not + // identical + // to the proposedBlock in the new proposal (so should fail). + final Block prevProposedBlock = + TestHelpers.createProposalBlock(validators.subList(0, 1), preparedRound); + + final PreparedRoundArtifacts mismatchedRoundArtefacts = + new PreparedRoundArtifacts( + proposerMessageFactory.createProposal(preparedRound, prevProposedBlock), + singletonList( + validatorMessageFactory.createPrepare(preparedRound, prevProposedBlock.getHash()))); + + final RoundChangeCertificate roundChangeCert = + new RoundChangeCertificate( + singletonList( + validatorMessageFactory + .createRoundChange(roundIdentifier, Optional.of(mismatchedRoundArtefacts)) + .getSignedPayload())); + + assertThat( + validator.validateProposalMessageMatchesLatestPrepareCertificate( + roundChangeCert, proposedBlock)) + .isFalse(); + } + + @Test + public void correctlyMatchesBlockAgainstLatestInRoundChangeCertificate() { + final ConsensusRoundIdentifier latterPrepareRound = + TestHelpers.createFrom(roundIdentifier, 0, -1); + final Block latterBlock = TestHelpers.createProposalBlock(validators, latterPrepareRound); + final Proposal latterProposal = + proposerMessageFactory.createProposal(latterPrepareRound, latterBlock); + final Optional latterTerminatedRoundArtefacts = + Optional.of( + new PreparedRoundArtifacts( + latterProposal, + org.assertj.core.util.Lists.newArrayList( + validatorMessageFactory.createPrepare( + latterPrepareRound, proposedBlock.getHash())))); + + // An earlier PrepareCert is added to ensure the path to find the latest PrepareCert + // is correctly followed. + final ConsensusRoundIdentifier earlierPreparedRound = + new ConsensusRoundIdentifier( + roundIdentifier.getSequenceNumber(), roundIdentifier.getRoundNumber() - 2); + final Block earlierBlock = + TestHelpers.createProposalBlock(validators.subList(0, 1), earlierPreparedRound); + final Proposal earlierProposal = + proposerMessageFactory.createProposal(earlierPreparedRound, earlierBlock); + final Optional earlierTerminatedRoundArtefacts = + Optional.of( + new PreparedRoundArtifacts( + earlierProposal, + org.assertj.core.util.Lists.newArrayList( + validatorMessageFactory.createPrepare( + earlierPreparedRound, earlierBlock.getHash())))); + + final RoundChangeCertificate roundChangeCert = + new RoundChangeCertificate( + org.assertj.core.util.Lists.newArrayList( + proposerMessageFactory + .createRoundChange(roundIdentifier, earlierTerminatedRoundArtefacts) + .getSignedPayload(), + validatorMessageFactory + .createRoundChange(roundIdentifier, latterTerminatedRoundArtefacts) + .getSignedPayload())); + + assertThat( + validator.validateProposalMessageMatchesLatestPrepareCertificate( + roundChangeCert, earlierBlock)) + .isFalse(); + + assertThat( + validator.validateProposalMessageMatchesLatestPrepareCertificate( + roundChangeCert, latterBlock)) + .isTrue(); + } +}