diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftBlockHeightManager.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftBlockHeightManager.java index 280e8c5371..0693a15c66 100644 --- a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftBlockHeightManager.java +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftBlockHeightManager.java @@ -98,7 +98,7 @@ public class IbftBlockHeightManager implements BlockHeightManager { new RoundState( roundIdentifier, finalState.getQuorum(), - messageValidatorFactory.createMessageValidator(roundIdentifier, parentHeader)); + messageValidatorFactory.createMessageValidator(roundIdentifier)); } @Override 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 index 8e46a8db41..8431bc2c54 100644 --- 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 @@ -17,6 +17,7 @@ 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.consensus.ibft.validation.SignedDataValidator; import tech.pegasys.pantheon.ethereum.BlockValidator; import tech.pegasys.pantheon.ethereum.ProtocolContext; import tech.pegasys.pantheon.ethereum.chain.MinedBlockObserver; @@ -56,12 +57,12 @@ public class IbftRoundFactory { roundIdentifier, finalState.getQuorum(), new MessageValidator( - finalState.getValidators(), - finalState.getProposerForRound(roundIdentifier), - roundIdentifier, - blockValidator, - protocolContext, - parentHeader)); + new SignedDataValidator( + finalState.getValidators(), + finalState.getProposerForRound(roundIdentifier), + roundIdentifier, + blockValidator, + protocolContext))); return createNewRoundWithState(parentHeader, roundState); } 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 655182a917..7846384b9d 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 @@ -66,10 +66,10 @@ public class RoundState { public boolean setProposedBlock(final Proposal msg) { if (!proposalMessage.isPresent()) { - if (validator.addSignedProposalPayload(msg.getSignedPayload())) { + if (validator.addSignedProposalPayload(msg)) { proposalMessage = Optional.of(msg); - preparePayloads.removeIf(p -> !validator.validatePrepareMessage(p.getSignedPayload())); - commitPayloads.removeIf(p -> !validator.validateCommmitMessage(p.getSignedPayload())); + preparePayloads.removeIf(p -> !validator.validatePrepareMessage(p)); + commitPayloads.removeIf(p -> !validator.validateCommitMessage(p)); updateState(); return true; } @@ -79,7 +79,7 @@ public class RoundState { } public void addPrepareMessage(final Prepare msg) { - if (!proposalMessage.isPresent() || validator.validatePrepareMessage(msg.getSignedPayload())) { + if (!proposalMessage.isPresent() || validator.validatePrepareMessage(msg)) { preparePayloads.add(msg); LOG.debug("Round state added prepare message prepare={}", msg); } @@ -87,7 +87,7 @@ public class RoundState { } public void addCommitMessage(final Commit msg) { - if (!proposalMessage.isPresent() || validator.validateCommmitMessage(msg.getSignedPayload())) { + if (!proposalMessage.isPresent() || validator.validateCommitMessage(msg)) { commitPayloads.add(msg); LOG.debug("Round state added commit message commit={}", msg); } diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidator.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidator.java index ba894df327..999cc8f4fd 100644 --- a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidator.java +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidator.java @@ -12,205 +12,27 @@ */ package tech.pegasys.pantheon.consensus.ibft.validation; -import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; -import tech.pegasys.pantheon.consensus.ibft.IbftContext; -import tech.pegasys.pantheon.consensus.ibft.IbftExtraData; -import tech.pegasys.pantheon.consensus.ibft.payload.CommitPayload; -import tech.pegasys.pantheon.consensus.ibft.payload.Payload; -import tech.pegasys.pantheon.consensus.ibft.payload.PreparePayload; -import tech.pegasys.pantheon.consensus.ibft.payload.ProposalPayload; -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.Address; -import tech.pegasys.pantheon.ethereum.core.Block; -import tech.pegasys.pantheon.ethereum.core.BlockHeader; -import tech.pegasys.pantheon.ethereum.core.Hash; -import tech.pegasys.pantheon.ethereum.core.Util; -import tech.pegasys.pantheon.ethereum.mainnet.HeaderValidationMode; - -import java.util.Collection; -import java.util.Optional; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import tech.pegasys.pantheon.consensus.ibft.messagewrappers.Commit; +import tech.pegasys.pantheon.consensus.ibft.messagewrappers.Prepare; +import tech.pegasys.pantheon.consensus.ibft.messagewrappers.Proposal; public class MessageValidator { - private static final Logger LOG = LogManager.getLogger(); - - private final Collection
validators; - private final Address expectedProposer; - private final ConsensusRoundIdentifier roundIdentifier; - private final BlockValidator blockValidator; - private final ProtocolContext protocolContext; - private final BlockHeader parentHeader; - - private Optional> proposal = Optional.empty(); - - public MessageValidator( - final Collection
validators, - final Address expectedProposer, - final ConsensusRoundIdentifier roundIdentifier, - final BlockValidator blockValidator, - final ProtocolContext protocolContext, - final BlockHeader parentHeader) { - this.validators = validators; - this.expectedProposer = expectedProposer; - this.roundIdentifier = roundIdentifier; - this.blockValidator = blockValidator; - this.protocolContext = protocolContext; - this.parentHeader = parentHeader; - } - - public boolean addSignedProposalPayload(final SignedData msg) { - - if (proposal.isPresent()) { - return handleSubsequentProposal(proposal.get(), msg); - } - - if (!validateSignedProposalPayload(msg)) { - return false; - } - - if (!validateBlockMatchesProposalRound(msg.getPayload())) { - return false; - } - - proposal = Optional.of(msg); - return true; - } - - private boolean validateSignedProposalPayload(final SignedData msg) { - - if (!msg.getPayload().getRoundIdentifier().equals(roundIdentifier)) { - LOG.info("Invalid Proposal message, does not match current round."); - return false; - } - - if (!msg.getAuthor().equals(expectedProposer)) { - LOG.info( - "Invalid Proposal message, was not created by the proposer expected for the " - + "associated round."); - return false; - } - - final Block proposedBlock = msg.getPayload().getBlock(); - - final Optional validationResult = - blockValidator.validateAndProcessBlock( - protocolContext, proposedBlock, HeaderValidationMode.LIGHT, HeaderValidationMode.FULL); - - if (!validationResult.isPresent()) { - LOG.info("Invalid Proposal message, block did not pass validation."); - return false; - } - - return true; - } - - private boolean handleSubsequentProposal( - final SignedData existingMsg, final SignedData newMsg) { - if (!existingMsg.getAuthor().equals(newMsg.getAuthor())) { - LOG.debug("Received subsequent invalid Proposal message; sender differs from original."); - return false; - } - - final ProposalPayload existingData = existingMsg.getPayload(); - final ProposalPayload newData = newMsg.getPayload(); - - if (!proposalMessagesAreIdentical(existingData, newData)) { - LOG.debug("Received subsequent invalid Proposal message; content differs from original."); - return false; - } - - return true; - } - - public boolean validatePrepareMessage(final SignedData msg) { - final String msgType = "Prepare"; - - if (!isMessageForCurrentRoundFromValidatorAndProposalAvailable(msg, msgType)) { - return false; - } - - if (msg.getAuthor().equals(expectedProposer)) { - LOG.info("Illegal Prepare message; was sent by the round's proposer."); - return false; - } - - return validateDigestMatchesProposal(msg.getPayload().getDigest(), msgType); - } - - public boolean validateCommmitMessage(final SignedData msg) { - final String msgType = "Commit"; - - if (!isMessageForCurrentRoundFromValidatorAndProposalAvailable(msg, msgType)) { - return false; - } - - final Block proposedBlock = proposal.get().getPayload().getBlock(); - final Address commitSealCreator = - Util.signatureToAddress(msg.getPayload().getCommitSeal(), proposedBlock.getHash()); - - if (!commitSealCreator.equals(msg.getAuthor())) { - LOG.info("Invalid Commit message. Seal was not created by the message transmitter."); - return false; - } - - return validateDigestMatchesProposal(msg.getPayload().getDigest(), msgType); - } - - private boolean isMessageForCurrentRoundFromValidatorAndProposalAvailable( - final SignedData msg, final String msgType) { - - if (!msg.getPayload().getRoundIdentifier().equals(roundIdentifier)) { - LOG.info("Invalid {} message, does not match current round.", msgType); - return false; - } - - if (!validators.contains(msg.getAuthor())) { - LOG.info( - "Invalid {} message, was not transmitted by a validator for the " + "associated round.", - msgType); - return false; - } + private final SignedDataValidator signedDataValidator; - if (!proposal.isPresent()) { - LOG.info( - "Unable to validate {} message. No Proposal exists against which to validate " - + "block digest.", - msgType); - return false; - } - return true; + public MessageValidator(final SignedDataValidator signedDataValidator) { + this.signedDataValidator = signedDataValidator; } - private boolean validateDigestMatchesProposal(final Hash digest, final String msgType) { - final Block proposedBlock = proposal.get().getPayload().getBlock(); - if (!digest.equals(proposedBlock.getHash())) { - LOG.info( - "Illegal {} message, digest does not match the block in the Prepare Message.", msgType); - return false; - } - return true; + public boolean addSignedProposalPayload(final Proposal msg) { + return signedDataValidator.addSignedProposalPayload(msg.getSignedPayload()); } - private boolean proposalMessagesAreIdentical( - final ProposalPayload right, final ProposalPayload left) { - return right.getBlock().getHash().equals(left.getBlock().getHash()) - && right.getRoundIdentifier().equals(left.getRoundIdentifier()); + public boolean validatePrepareMessage(final Prepare msg) { + return signedDataValidator.validatePrepareMessage(msg.getSignedPayload()); } - private boolean validateBlockMatchesProposalRound(final ProposalPayload payload) { - final ConsensusRoundIdentifier msgRound = payload.getRoundIdentifier(); - final IbftExtraData extraData = - IbftExtraData.decode(payload.getBlock().getHeader().getExtraData()); - if (extraData.getRound() != msgRound.getRoundNumber()) { - LOG.info("Invalid Proposal message, round number in block does not match that in message."); - return false; - } - return true; + public boolean validateCommitMessage(final Commit msg) { + return signedDataValidator.validateCommmitMessage(msg.getSignedPayload()); } } 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 0b05f8e84c..1c557185b1 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 @@ -41,18 +41,21 @@ public class MessageValidatorFactory { this.protocolContext = protocolContext; } - public MessageValidator createMessageValidator( - final ConsensusRoundIdentifier roundIdentifier, final BlockHeader parentHeader) { + private SignedDataValidator createSignedDataValidator( + final ConsensusRoundIdentifier roundIdentifier) { final BlockValidator blockValidator = protocolSchedule.getByBlockNumber(roundIdentifier.getSequenceNumber()).getBlockValidator(); - return new MessageValidator( + return new SignedDataValidator( protocolContext.getConsensusState().getVoteTally().getValidators(), proposerSelector.selectProposerForRound(roundIdentifier), roundIdentifier, blockValidator, - protocolContext, - parentHeader); + protocolContext); + } + + public MessageValidator createMessageValidator(final ConsensusRoundIdentifier roundIdentifier) { + return new MessageValidator(createSignedDataValidator(roundIdentifier)); } public RoundChangeMessageValidator createRoundChangeMessageValidator( @@ -60,7 +63,7 @@ public class MessageValidatorFactory { final Collection
validators = protocolContext.getConsensusState().getVoteTally().getValidators(); return new RoundChangeMessageValidator( - roundIdentifier -> createMessageValidator(roundIdentifier, parentHeader), + this::createSignedDataValidator, validators, prepareMessageCountForQuorum( IbftHelpers.calculateRequiredValidatorQuorum(validators.size())), @@ -73,7 +76,7 @@ public class MessageValidatorFactory { return new NewRoundMessageValidator( validators, proposerSelector, - roundIdentifier -> createMessageValidator(roundIdentifier, parentHeader), + this::createSignedDataValidator, IbftHelpers.calculateRequiredValidatorQuorum(validators.size()), parentHeader.getNumber() + 1); } 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 d15c4880b8..1e9b88ebac 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 @@ -80,7 +80,7 @@ public class NewRoundMessageValidator { } final SignedData proposalPayload = payload.getProposalPayload(); - final MessageValidator proposalValidator = + final SignedDataValidator proposalValidator = messageValidatorFactory.createAt(rootRoundIdentifier); if (!proposalValidator.addSignedProposalPayload(proposalPayload)) { LOG.info("Invalid NewRound message, embedded proposal failed validation"); diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeMessageValidator.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeMessageValidator.java index 9a647c0c9d..bd0ed139c9 100644 --- a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeMessageValidator.java +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeMessageValidator.java @@ -81,15 +81,15 @@ public class RoundChangeMessageValidator { return false; } - final MessageValidator messageValidator = + final SignedDataValidator signedDataValidator = messageValidatorFactory.createAt(proposalRoundIdentifier); - return validateConsistencyOfPrepareCertificateMessages(certificate, messageValidator); + return validateConsistencyOfPrepareCertificateMessages(certificate, signedDataValidator); } private boolean validateConsistencyOfPrepareCertificateMessages( - final PreparedCertificate certificate, final MessageValidator messageValidator) { + final PreparedCertificate certificate, final SignedDataValidator signedDataValidator) { - if (!messageValidator.addSignedProposalPayload(certificate.getProposalPayload())) { + if (!signedDataValidator.addSignedProposalPayload(certificate.getProposalPayload())) { LOG.info("Invalid RoundChange message, embedded Proposal message failed validation."); return false; } @@ -102,7 +102,7 @@ public class RoundChangeMessageValidator { } for (final SignedData prepareMsg : certificate.getPreparePayloads()) { - if (!messageValidator.validatePrepareMessage(prepareMsg)) { + if (!signedDataValidator.validatePrepareMessage(prepareMsg)) { LOG.info("Invalid RoundChange message, embedded Prepare message failed validation."); return false; } @@ -130,6 +130,6 @@ public class RoundChangeMessageValidator { @FunctionalInterface public interface MessageValidatorForHeightFactory { - MessageValidator createAt(final ConsensusRoundIdentifier roundIdentifier); + SignedDataValidator createAt(final ConsensusRoundIdentifier roundIdentifier); } } diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/SignedDataValidator.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/SignedDataValidator.java new file mode 100644 index 0000000000..0be260a5c8 --- /dev/null +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/SignedDataValidator.java @@ -0,0 +1,212 @@ +/* + * 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.validation; + +import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; +import tech.pegasys.pantheon.consensus.ibft.IbftContext; +import tech.pegasys.pantheon.consensus.ibft.IbftExtraData; +import tech.pegasys.pantheon.consensus.ibft.payload.CommitPayload; +import tech.pegasys.pantheon.consensus.ibft.payload.Payload; +import tech.pegasys.pantheon.consensus.ibft.payload.PreparePayload; +import tech.pegasys.pantheon.consensus.ibft.payload.ProposalPayload; +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.Address; +import tech.pegasys.pantheon.ethereum.core.Block; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.Util; +import tech.pegasys.pantheon.ethereum.mainnet.HeaderValidationMode; + +import java.util.Collection; +import java.util.Optional; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class SignedDataValidator { + + private static final Logger LOG = LogManager.getLogger(); + + private final Collection
validators; + private final Address expectedProposer; + private final ConsensusRoundIdentifier roundIdentifier; + private final BlockValidator blockValidator; + private final ProtocolContext protocolContext; + + private Optional> proposal = Optional.empty(); + + public SignedDataValidator( + final Collection
validators, + final Address expectedProposer, + final ConsensusRoundIdentifier roundIdentifier, + final BlockValidator blockValidator, + final ProtocolContext protocolContext) { + this.validators = validators; + this.expectedProposer = expectedProposer; + this.roundIdentifier = roundIdentifier; + this.blockValidator = blockValidator; + this.protocolContext = protocolContext; + } + + public boolean addSignedProposalPayload(final SignedData msg) { + + if (proposal.isPresent()) { + return handleSubsequentProposal(proposal.get(), msg); + } + + if (!validateSignedProposalPayload(msg)) { + return false; + } + + if (!validateBlockMatchesProposalRound(msg.getPayload())) { + return false; + } + + proposal = Optional.of(msg); + return true; + } + + private boolean validateSignedProposalPayload(final SignedData msg) { + + if (!msg.getPayload().getRoundIdentifier().equals(roundIdentifier)) { + LOG.info("Invalid Proposal message, does not match current round."); + return false; + } + + if (!msg.getAuthor().equals(expectedProposer)) { + LOG.info( + "Invalid Proposal message, was not created by the proposer expected for the " + + "associated round."); + return false; + } + + final Block proposedBlock = msg.getPayload().getBlock(); + + final Optional validationResult = + blockValidator.validateAndProcessBlock( + protocolContext, proposedBlock, HeaderValidationMode.LIGHT, HeaderValidationMode.FULL); + + if (!validationResult.isPresent()) { + LOG.info("Invalid Proposal message, block did not pass validation."); + return false; + } + + return true; + } + + private boolean handleSubsequentProposal( + final SignedData existingMsg, final SignedData newMsg) { + if (!existingMsg.getAuthor().equals(newMsg.getAuthor())) { + LOG.debug("Received subsequent invalid Proposal message; sender differs from original."); + return false; + } + + final ProposalPayload existingData = existingMsg.getPayload(); + final ProposalPayload newData = newMsg.getPayload(); + + if (!proposalMessagesAreIdentical(existingData, newData)) { + LOG.debug("Received subsequent invalid Proposal message; content differs from original."); + return false; + } + + return true; + } + + public boolean validatePrepareMessage(final SignedData msg) { + final String msgType = "Prepare"; + + if (!isMessageForCurrentRoundFromValidatorAndProposalAvailable(msg, msgType)) { + return false; + } + + if (msg.getAuthor().equals(expectedProposer)) { + LOG.info("Illegal Prepare message; was sent by the round's proposer."); + return false; + } + + return validateDigestMatchesProposal(msg.getPayload().getDigest(), msgType); + } + + public boolean validateCommmitMessage(final SignedData msg) { + final String msgType = "Commit"; + + if (!isMessageForCurrentRoundFromValidatorAndProposalAvailable(msg, msgType)) { + return false; + } + + final Block proposedBlock = proposal.get().getPayload().getBlock(); + final Address commitSealCreator = + Util.signatureToAddress(msg.getPayload().getCommitSeal(), proposedBlock.getHash()); + + if (!commitSealCreator.equals(msg.getAuthor())) { + LOG.info("Invalid Commit message. Seal was not created by the message transmitter."); + return false; + } + + return validateDigestMatchesProposal(msg.getPayload().getDigest(), msgType); + } + + private boolean isMessageForCurrentRoundFromValidatorAndProposalAvailable( + final SignedData msg, final String msgType) { + + if (!msg.getPayload().getRoundIdentifier().equals(roundIdentifier)) { + LOG.info("Invalid {} message, does not match current round.", msgType); + return false; + } + + if (!validators.contains(msg.getAuthor())) { + LOG.info( + "Invalid {} message, was not transmitted by a validator for the " + "associated round.", + msgType); + return false; + } + + if (!proposal.isPresent()) { + LOG.info( + "Unable to validate {} message. No Proposal exists against which to validate " + + "block digest.", + msgType); + return false; + } + return true; + } + + private boolean validateDigestMatchesProposal(final Hash digest, final String msgType) { + final Block proposedBlock = proposal.get().getPayload().getBlock(); + if (!digest.equals(proposedBlock.getHash())) { + LOG.info( + "Illegal {} message, digest does not match the block in the Prepare Message.", msgType); + return false; + } + return true; + } + + private boolean proposalMessagesAreIdentical( + final ProposalPayload right, final ProposalPayload left) { + return right.getBlock().getHash().equals(left.getBlock().getHash()) + && right.getRoundIdentifier().equals(left.getRoundIdentifier()); + } + + private boolean validateBlockMatchesProposalRound(final ProposalPayload payload) { + final ConsensusRoundIdentifier msgRound = payload.getRoundIdentifier(); + final IbftExtraData extraData = + IbftExtraData.decode(payload.getBlock().getHeader().getExtraData()); + if (extraData.getRound() != msgRound.getRoundNumber()) { + LOG.info("Invalid Proposal message, round number in block does not match that in message."); + return false; + } + return true; + } +} diff --git a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftBlockHeightManagerTest.java b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftBlockHeightManagerTest.java index c22388c455..5cd12cab8d 100644 --- a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftBlockHeightManagerTest.java +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftBlockHeightManagerTest.java @@ -127,7 +127,7 @@ public class IbftBlockHeightManagerTest { final MessageValidator messageValidator = mock(MessageValidator.class); when(messageValidator.addSignedProposalPayload(any())).thenReturn(true); - when(messageValidator.validateCommmitMessage(any())).thenReturn(true); + when(messageValidator.validateCommitMessage(any())).thenReturn(true); when(messageValidator.validatePrepareMessage(any())).thenReturn(true); when(finalState.getTransmitter()).thenReturn(messageTransmitter); when(finalState.getBlockTimer()).thenReturn(blockTimer); @@ -138,7 +138,7 @@ public class IbftBlockHeightManagerTest { when(newRoundMessageValidator.validateNewRoundMessage(any())).thenReturn(true); when(messageValidatorFactory.createNewRoundValidator(any())) .thenReturn(newRoundMessageValidator); - when(messageValidatorFactory.createMessageValidator(any(), any())).thenReturn(messageValidator); + when(messageValidatorFactory.createMessageValidator(any())).thenReturn(messageValidator); protocolContext = new ProtocolContext<>(null, null, new IbftContext(new VoteTally(validators), null)); 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 index 5b17ef8069..dc00243d6e 100644 --- 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 @@ -98,7 +98,7 @@ public class IbftRoundTest { when(messageValidator.addSignedProposalPayload(any())).thenReturn(true); when(messageValidator.validatePrepareMessage(any())).thenReturn(true); - when(messageValidator.validateCommmitMessage(any())).thenReturn(true); + when(messageValidator.validateCommitMessage(any())).thenReturn(true); proposedExtraData = new IbftExtraData( diff --git a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/statemachine/RoundChangeManagerTest.java b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/statemachine/RoundChangeManagerTest.java index c2e19f9632..d67cb7954c 100644 --- a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/statemachine/RoundChangeManagerTest.java +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/statemachine/RoundChangeManagerTest.java @@ -27,8 +27,8 @@ import tech.pegasys.pantheon.consensus.ibft.payload.MessageFactory; import tech.pegasys.pantheon.consensus.ibft.payload.PreparePayload; import tech.pegasys.pantheon.consensus.ibft.payload.PreparedCertificate; import tech.pegasys.pantheon.consensus.ibft.payload.SignedData; -import tech.pegasys.pantheon.consensus.ibft.validation.MessageValidator; import tech.pegasys.pantheon.consensus.ibft.validation.RoundChangeMessageValidator; +import tech.pegasys.pantheon.consensus.ibft.validation.SignedDataValidator; import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; import tech.pegasys.pantheon.ethereum.BlockValidator; import tech.pegasys.pantheon.ethereum.BlockValidator.BlockProcessingOutputs; @@ -88,33 +88,30 @@ public class RoundChangeManagerTest { when(messageValidatorFactory.createAt(ri1)) .thenAnswer( invocation -> - new MessageValidator( + new SignedDataValidator( validators, Util.publicKeyToAddress(proposerKey.getPublicKey()), ri1, blockValidator, - protocolContext, - parentHeader)); + protocolContext)); when(messageValidatorFactory.createAt(ri2)) .thenAnswer( invocation -> - new MessageValidator( + new SignedDataValidator( validators, Util.publicKeyToAddress(validator1Key.getPublicKey()), ri2, blockValidator, - protocolContext, - parentHeader)); + protocolContext)); when(messageValidatorFactory.createAt(ri3)) .thenAnswer( invocation -> - new MessageValidator( + new SignedDataValidator( validators, Util.publicKeyToAddress(validator2Key.getPublicKey()), ri3, blockValidator, - protocolContext, - parentHeader)); + protocolContext)); final RoundChangeMessageValidator roundChangeMessageValidator = new RoundChangeMessageValidator( 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 5735e0bc1b..25faa298fc 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 @@ -107,7 +107,7 @@ public class RoundStateTest { @Test public void singleValidatorRequiresCommitMessageToBeCommitted() { when(messageValidator.addSignedProposalPayload(any())).thenReturn(true); - when(messageValidator.validateCommmitMessage(any())).thenReturn(true); + when(messageValidator.validateCommitMessage(any())).thenReturn(true); final RoundState roundState = new RoundState(roundIdentifier, 1, messageValidator); @@ -184,9 +184,8 @@ public class RoundStateTest { final RoundState roundState = new RoundState(roundIdentifier, 3, messageValidator); when(messageValidator.addSignedProposalPayload(any())).thenReturn(true); - when(messageValidator.validatePrepareMessage(firstPrepare.getSignedPayload())).thenReturn(true); - when(messageValidator.validatePrepareMessage(secondPrepare.getSignedPayload())) - .thenReturn(false); + when(messageValidator.validatePrepareMessage(firstPrepare)).thenReturn(true); + when(messageValidator.validatePrepareMessage(secondPrepare)).thenReturn(false); roundState.addPrepareMessage(firstPrepare); roundState.addPrepareMessage(secondPrepare); @@ -219,10 +218,8 @@ public class RoundStateTest { validatorMessageFactories.get(0).createSignedProposalPayload(roundIdentifier, block); when(messageValidator.addSignedProposalPayload(any())).thenReturn(true); - when(messageValidator.validatePrepareMessage(firstPrepare.getSignedPayload())) - .thenReturn(false); - when(messageValidator.validatePrepareMessage(secondPrepare.getSignedPayload())) - .thenReturn(true); + when(messageValidator.validatePrepareMessage(firstPrepare)).thenReturn(false); + when(messageValidator.validatePrepareMessage(secondPrepare)).thenReturn(true); roundState.setProposedBlock(proposal); assertThat(roundState.isPrepared()).isFalse(); @@ -237,7 +234,7 @@ public class RoundStateTest { @Test public void commitSealsAreExtractedFromReceivedMessages() { when(messageValidator.addSignedProposalPayload(any())).thenReturn(true); - when(messageValidator.validateCommmitMessage(any())).thenReturn(true); + when(messageValidator.validateCommitMessage(any())).thenReturn(true); final RoundState roundState = new RoundState(roundIdentifier, 2, messageValidator); diff --git a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidatorTest.java b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidatorTest.java index eb0a17fec3..058dbae2d1 100644 --- a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidatorTest.java +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidatorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 ConsenSys AG. + * 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 @@ -12,256 +12,75 @@ */ package tech.pegasys.pantheon.consensus.ibft.validation; -import static java.util.Optional.empty; -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.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; -import tech.pegasys.pantheon.consensus.ibft.IbftContext; -import tech.pegasys.pantheon.consensus.ibft.IbftExtraData; +import tech.pegasys.pantheon.consensus.ibft.TestHelpers; import tech.pegasys.pantheon.consensus.ibft.messagewrappers.Commit; import tech.pegasys.pantheon.consensus.ibft.messagewrappers.Prepare; import tech.pegasys.pantheon.consensus.ibft.messagewrappers.Proposal; import tech.pegasys.pantheon.consensus.ibft.payload.MessageFactory; import tech.pegasys.pantheon.crypto.SECP256K1; import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; -import tech.pegasys.pantheon.ethereum.BlockValidator; -import tech.pegasys.pantheon.ethereum.BlockValidator.BlockProcessingOutputs; -import tech.pegasys.pantheon.ethereum.ProtocolContext; -import tech.pegasys.pantheon.ethereum.chain.MutableBlockchain; import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.AddressHelpers; import tech.pegasys.pantheon.ethereum.core.Block; -import tech.pegasys.pantheon.ethereum.core.BlockHeader; -import tech.pegasys.pantheon.ethereum.core.Hash; -import tech.pegasys.pantheon.ethereum.core.Util; -import tech.pegasys.pantheon.ethereum.worldstate.WorldStateArchive; -import tech.pegasys.pantheon.util.bytes.BytesValue; -import java.util.Collections; import java.util.List; -import java.util.Optional; -import com.google.common.collect.Lists; +import org.assertj.core.util.Lists; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -@RunWith(MockitoJUnitRunner.class) public class MessageValidatorTest { - private final KeyPair proposerKey = KeyPair.generate(); - private final KeyPair validatorKey = KeyPair.generate(); - private final KeyPair nonValidatorKey = KeyPair.generate(); - private final MessageFactory proposerMessageFactory = new MessageFactory(proposerKey); - private final MessageFactory validatorMessageFactory = new MessageFactory(validatorKey); - private final MessageFactory nonValidatorMessageFactory = new MessageFactory(nonValidatorKey); + private KeyPair keyPair = KeyPair.generate(); + private MessageFactory messageFactory = new MessageFactory(keyPair); + private ConsensusRoundIdentifier roundIdentifier = new ConsensusRoundIdentifier(1, 1); - private final List
validators = Lists.newArrayList(); + private SignedDataValidator signedDataValidator = mock(SignedDataValidator.class); - @Mock private BlockValidator blockValidator; - private final BlockHeader parentHeader = mock(BlockHeader.class); - private final ConsensusRoundIdentifier roundIdentifier = new ConsensusRoundIdentifier(2, 0); - private MessageValidator validator; + private MessageValidator messageValidator = new MessageValidator(signedDataValidator); - private final Block block = mock(Block.class); + private final List
validators = + Lists.newArrayList( + AddressHelpers.ofValue(0), + AddressHelpers.ofValue(1), + AddressHelpers.ofValue(2), + AddressHelpers.ofValue(3)); + + private final Block block = + TestHelpers.createProposalBlock(validators, roundIdentifier.getRoundNumber()); @Before public void setup() { - validators.add(Util.publicKeyToAddress(proposerKey.getPublicKey())); - validators.add(Util.publicKeyToAddress(validatorKey.getPublicKey())); - - final ProtocolContext protocolContext = - new ProtocolContext<>( - mock(MutableBlockchain.class), mock(WorldStateArchive.class), mock(IbftContext.class)); - - validator = - new MessageValidator( - validators, - Util.publicKeyToAddress(proposerKey.getPublicKey()), - roundIdentifier, - blockValidator, - protocolContext, - parentHeader); - - when(block.getHash()).thenReturn(Hash.fromHexStringLenient("1")); - when(blockValidator.validateAndProcessBlock(any(), any(), any(), any())) - .thenReturn(Optional.of(new BlockProcessingOutputs(null, null))); - insertRoundToBlockHeader(0); - } - - private void insertRoundToBlockHeader(final int round) { - final IbftExtraData extraData = - new IbftExtraData( - BytesValue.wrap(new byte[32]), Collections.emptyList(), empty(), round, validators); - final BlockHeader header = mock(BlockHeader.class); - when(header.getExtraData()).thenReturn(extraData.encode()); - when(block.getHeader()).thenReturn(header); - } - - @Test - public void receivingAPrepareMessageBeforeProposalFails() { - final Prepare prepareMsg = - proposerMessageFactory.createSignedPreparePayload(roundIdentifier, Hash.ZERO); - - assertThat(validator.validatePrepareMessage(prepareMsg.getSignedPayload())).isFalse(); - } - - @Test - public void receivingACommitMessageBeforeProposalFails() { - final Commit commitMsg = - proposerMessageFactory.createSignedCommitPayload( - roundIdentifier, Hash.ZERO, SECP256K1.sign(Hash.ZERO, proposerKey)); - - assertThat(validator.validateCommmitMessage(commitMsg.getSignedPayload())).isFalse(); + when(signedDataValidator.addSignedProposalPayload(any())).thenReturn(true); + when(signedDataValidator.validatePrepareMessage(any())).thenReturn(true); + when(signedDataValidator.validateCommmitMessage(any())).thenReturn(true); } @Test - public void receivingProposalMessageFromNonProposerFails() { - final Proposal proposalMsg = - validatorMessageFactory.createSignedProposalPayload(roundIdentifier, mock(Block.class)); - - assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isFalse(); - } + public void messageValidatorDefersToUnderlyingSignedDataValidator() { + final Proposal proposal = messageFactory.createSignedProposalPayload(roundIdentifier, block); - @Test - public void receivingProposalMessageWithIllegalBlockFails() { - when(blockValidator.validateAndProcessBlock(any(), any(), any(), any())).thenReturn(empty()); - final Proposal proposalMsg = - proposerMessageFactory.createSignedProposalPayload(roundIdentifier, mock(Block.class)); - - assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isFalse(); - } - - @Test - public void receivingPrepareFromProposerFails() { - final Proposal proposalMsg = - proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); - - final Prepare prepareMsg = - proposerMessageFactory.createSignedPreparePayload(roundIdentifier, block.getHash()); - - assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); - assertThat(validator.validatePrepareMessage(prepareMsg.getSignedPayload())).isFalse(); - } - - @Test - public void receivingPrepareFromNonValidatorFails() { - final Proposal proposalMsg = - proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); - - final Prepare prepareMsg = - nonValidatorMessageFactory.createSignedPreparePayload(roundIdentifier, block.getHash()); - - assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); - assertThat(validator.validatePrepareMessage(prepareMsg.getSignedPayload())).isFalse(); - } - - @Test - public void receivingMessagesWithDifferentRoundIdFromProposalFails() { - final Proposal proposalMsg = - proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); + final Prepare prepare = + messageFactory.createSignedPreparePayload(roundIdentifier, block.getHash()); - final ConsensusRoundIdentifier invalidRoundIdentifier = - new ConsensusRoundIdentifier( - roundIdentifier.getSequenceNumber(), roundIdentifier.getRoundNumber() + 1); - final Prepare prepareMsg = - validatorMessageFactory.createSignedPreparePayload(invalidRoundIdentifier, block.getHash()); - final Commit commitMsg = - validatorMessageFactory.createSignedCommitPayload( - invalidRoundIdentifier, block.getHash(), SECP256K1.sign(block.getHash(), proposerKey)); + final Commit commit = + messageFactory.createSignedCommitPayload( + roundIdentifier, block.getHash(), SECP256K1.sign(block.getHash(), keyPair)); - assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); - assertThat(validator.validatePrepareMessage(prepareMsg.getSignedPayload())).isFalse(); - assertThat(validator.validateCommmitMessage(commitMsg.getSignedPayload())).isFalse(); - } - - @Test - public void receivingPrepareNonProposerValidatorWithCorrectRoundIsSuccessful() { - final Proposal proposalMsg = - proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); - final Prepare prepareMsg = - validatorMessageFactory.createSignedPreparePayload(roundIdentifier, block.getHash()); - - assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); - assertThat(validator.validatePrepareMessage(prepareMsg.getSignedPayload())).isTrue(); - } - - @Test - public void receivingACommitMessageWithAnInvalidCommitSealFails() { - final Proposal proposalMsg = - proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); - - final Commit commitMsg = - proposerMessageFactory.createSignedCommitPayload( - roundIdentifier, block.getHash(), SECP256K1.sign(block.getHash(), nonValidatorKey)); - - assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); - assertThat(validator.validateCommmitMessage(commitMsg.getSignedPayload())).isFalse(); - } - - @Test - public void commitMessageContainingValidSealFromValidatorIsSuccessful() { - final Proposal proposalMsg = - proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); - - final Commit proposerCommitMsg = - proposerMessageFactory.createSignedCommitPayload( - roundIdentifier, block.getHash(), SECP256K1.sign(block.getHash(), proposerKey)); - - final Commit validatorCommitMsg = - validatorMessageFactory.createSignedCommitPayload( - roundIdentifier, block.getHash(), SECP256K1.sign(block.getHash(), validatorKey)); - - assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); - assertThat(validator.validateCommmitMessage(proposerCommitMsg.getSignedPayload())).isTrue(); - assertThat(validator.validateCommmitMessage(validatorCommitMsg.getSignedPayload())).isTrue(); - } - - @Test - public void subsequentProposalHasDifferentSenderFails() { - final Proposal proposalMsg = - proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); - assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); - - final Proposal secondProposalMsg = - validatorMessageFactory.createSignedProposalPayload(roundIdentifier, block); - assertThat(validator.addSignedProposalPayload(secondProposalMsg.getSignedPayload())).isFalse(); - } - - @Test - public void subsequentProposalHasDifferentContentFails() { - final Proposal proposalMsg = - proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); - assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); - - final ConsensusRoundIdentifier newRoundIdentifier = new ConsensusRoundIdentifier(3, 0); - final Proposal secondProposalMsg = - proposerMessageFactory.createSignedProposalPayload(newRoundIdentifier, block); - assertThat(validator.addSignedProposalPayload(secondProposalMsg.getSignedPayload())).isFalse(); - } - - @Test - public void subsequentProposalHasIdenticalSenderAndContentIsSuccessful() { - final Proposal proposalMsg = - proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); - assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); - - final Proposal secondProposalMsg = - proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); - assertThat(validator.addSignedProposalPayload(secondProposalMsg.getSignedPayload())).isTrue(); - } - - @Test - public void blockRoundMisMatchWithMessageRoundFails() { - insertRoundToBlockHeader(roundIdentifier.getRoundNumber() + 1); + messageValidator.addSignedProposalPayload(proposal); + verify(signedDataValidator, times(1)).addSignedProposalPayload(proposal.getSignedPayload()); - final Proposal proposalMsg = - proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); + messageValidator.validatePrepareMessage(prepare); + verify(signedDataValidator, times(1)).validatePrepareMessage(prepare.getSignedPayload()); - assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isFalse(); + messageValidator.validateCommitMessage(commit); + verify(signedDataValidator, times(1)).validateCommmitMessage(commit.getSignedPayload()); } } 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/NewRoundSignedDataValidatorTest.java similarity index 96% rename from consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundMessageValidatorTest.java rename to consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundSignedDataValidatorTest.java index b420e62a8e..d5f02bf04a 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/NewRoundSignedDataValidatorTest.java @@ -41,7 +41,7 @@ import com.google.common.collect.Lists; import org.junit.Before; import org.junit.Test; -public class NewRoundMessageValidatorTest { +public class NewRoundSignedDataValidatorTest { private final KeyPair proposerKey = KeyPair.generate(); private final KeyPair validatorKey = KeyPair.generate(); @@ -58,7 +58,7 @@ public class NewRoundMessageValidatorTest { private final ProposerSelector proposerSelector = mock(ProposerSelector.class); private final MessageValidatorForHeightFactory validatorFactory = mock(MessageValidatorForHeightFactory.class); - private final MessageValidator messageValidator = mock(MessageValidator.class); + private final SignedDataValidator signedDataValidator = mock(SignedDataValidator.class); private Block proposedBlock; private NewRound validMsg; @@ -78,9 +78,9 @@ public class NewRoundMessageValidatorTest { when(proposerSelector.selectProposerForRound(any())).thenReturn(proposerAddress); - when(validatorFactory.createAt(any())).thenReturn(messageValidator); - when(messageValidator.addSignedProposalPayload(any())).thenReturn(true); - when(messageValidator.validatePrepareMessage(any())).thenReturn(true); + when(validatorFactory.createAt(any())).thenReturn(signedDataValidator); + when(signedDataValidator.addSignedProposalPayload(any())).thenReturn(true); + when(signedDataValidator.validatePrepareMessage(any())).thenReturn(true); validator = new NewRoundMessageValidator( @@ -226,7 +226,7 @@ public class NewRoundMessageValidatorTest { msgBuilder.setRoundChangeCertificate(roundChangeBuilder.buildCertificate()); // The prepare Message in the RoundChange Cert will be deemed illegal. - when(messageValidator.validatePrepareMessage(any())).thenReturn(false); + when(signedDataValidator.validatePrepareMessage(any())).thenReturn(false); final NewRound msg = signPayload(msgBuilder.build(), proposerKey); @@ -293,7 +293,7 @@ public class NewRoundMessageValidatorTest { @Test public void embeddedProposalFailsValidation() { - when(messageValidator.addSignedProposalPayload(any())).thenReturn(false, true); + when(signedDataValidator.addSignedProposalPayload(any())).thenReturn(false, true); final Proposal proposal = proposerMessageFactory.createSignedProposalPayload(roundIdentifier, proposedBlock); diff --git a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeMessageValidatorTest.java b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeSignedDataValidatorTest.java similarity index 98% rename from consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeMessageValidatorTest.java rename to consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeSignedDataValidatorTest.java index e62a3429e6..3560e1d956 100644 --- a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeMessageValidatorTest.java +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeSignedDataValidatorTest.java @@ -40,7 +40,7 @@ import com.google.common.collect.Lists; import org.junit.Before; import org.junit.Test; -public class RoundChangeMessageValidatorTest { +public class RoundChangeSignedDataValidatorTest { private final KeyPair proposerKey = KeyPair.generate(); private final KeyPair validatorKey = KeyPair.generate(); @@ -56,7 +56,7 @@ public class RoundChangeMessageValidatorTest { private final Block block = mock(Block.class); - private final MessageValidator basicValidator = mock(MessageValidator.class); + private final SignedDataValidator basicValidator = mock(SignedDataValidator.class); private final List
validators = Lists.newArrayList(); private final MessageValidatorForHeightFactory validatorFactory = diff --git a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/SignedDataValidatorTest.java b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/SignedDataValidatorTest.java new file mode 100644 index 0000000000..622947752d --- /dev/null +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/SignedDataValidatorTest.java @@ -0,0 +1,266 @@ +/* + * 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.validation; + +import static java.util.Optional.empty; +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.IbftContext; +import tech.pegasys.pantheon.consensus.ibft.IbftExtraData; +import tech.pegasys.pantheon.consensus.ibft.messagewrappers.Commit; +import tech.pegasys.pantheon.consensus.ibft.messagewrappers.Prepare; +import tech.pegasys.pantheon.consensus.ibft.messagewrappers.Proposal; +import tech.pegasys.pantheon.consensus.ibft.payload.MessageFactory; +import tech.pegasys.pantheon.crypto.SECP256K1; +import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; +import tech.pegasys.pantheon.ethereum.BlockValidator; +import tech.pegasys.pantheon.ethereum.BlockValidator.BlockProcessingOutputs; +import tech.pegasys.pantheon.ethereum.ProtocolContext; +import tech.pegasys.pantheon.ethereum.chain.MutableBlockchain; +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.Block; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.Util; +import tech.pegasys.pantheon.ethereum.worldstate.WorldStateArchive; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +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; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class SignedDataValidatorTest { + + private final KeyPair proposerKey = KeyPair.generate(); + private final KeyPair validatorKey = KeyPair.generate(); + private final KeyPair nonValidatorKey = KeyPair.generate(); + private final MessageFactory proposerMessageFactory = new MessageFactory(proposerKey); + private final MessageFactory validatorMessageFactory = new MessageFactory(validatorKey); + private final MessageFactory nonValidatorMessageFactory = new MessageFactory(nonValidatorKey); + + private final List
validators = Lists.newArrayList(); + + @Mock private BlockValidator blockValidator; + private final BlockHeader parentHeader = mock(BlockHeader.class); + private final ConsensusRoundIdentifier roundIdentifier = new ConsensusRoundIdentifier(2, 0); + private SignedDataValidator validator; + + private final Block block = mock(Block.class); + + @Before + public void setup() { + validators.add(Util.publicKeyToAddress(proposerKey.getPublicKey())); + validators.add(Util.publicKeyToAddress(validatorKey.getPublicKey())); + + final ProtocolContext protocolContext = + new ProtocolContext<>( + mock(MutableBlockchain.class), mock(WorldStateArchive.class), mock(IbftContext.class)); + + validator = + new SignedDataValidator( + validators, + Util.publicKeyToAddress(proposerKey.getPublicKey()), + roundIdentifier, + blockValidator, + protocolContext); + + when(block.getHash()).thenReturn(Hash.fromHexStringLenient("1")); + when(blockValidator.validateAndProcessBlock(any(), any(), any(), any())) + .thenReturn(Optional.of(new BlockProcessingOutputs(null, null))); + insertRoundToBlockHeader(0); + } + + private void insertRoundToBlockHeader(final int round) { + final IbftExtraData extraData = + new IbftExtraData( + BytesValue.wrap(new byte[32]), Collections.emptyList(), empty(), round, validators); + final BlockHeader header = mock(BlockHeader.class); + when(header.getExtraData()).thenReturn(extraData.encode()); + when(block.getHeader()).thenReturn(header); + } + + @Test + public void receivingAPrepareMessageBeforeProposalFails() { + final Prepare prepareMsg = + proposerMessageFactory.createSignedPreparePayload(roundIdentifier, Hash.ZERO); + + assertThat(validator.validatePrepareMessage(prepareMsg.getSignedPayload())).isFalse(); + } + + @Test + public void receivingACommitMessageBeforeProposalFails() { + final Commit commitMsg = + proposerMessageFactory.createSignedCommitPayload( + roundIdentifier, Hash.ZERO, SECP256K1.sign(block.getHash(), proposerKey)); + + assertThat(validator.validateCommmitMessage(commitMsg.getSignedPayload())).isFalse(); + } + + @Test + public void receivingProposalMessageFromNonProposerFails() { + final Proposal proposalMsg = + validatorMessageFactory.createSignedProposalPayload(roundIdentifier, mock(Block.class)); + + assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isFalse(); + } + + @Test + public void receivingProposalMessageWithIllegalBlockFails() { + when(blockValidator.validateAndProcessBlock(any(), any(), any(), any())).thenReturn(empty()); + final Proposal proposalMsg = + proposerMessageFactory.createSignedProposalPayload(roundIdentifier, mock(Block.class)); + + assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isFalse(); + } + + @Test + public void receivingPrepareFromProposerFails() { + final Proposal proposalMsg = + proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); + + final Prepare prepareMsg = + proposerMessageFactory.createSignedPreparePayload(roundIdentifier, block.getHash()); + + assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); + assertThat(validator.validatePrepareMessage(prepareMsg.getSignedPayload())).isFalse(); + } + + @Test + public void receivingPrepareFromNonValidatorFails() { + final Proposal proposalMsg = + proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); + + final Prepare prepareMsg = + nonValidatorMessageFactory.createSignedPreparePayload(roundIdentifier, block.getHash()); + + assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); + assertThat(validator.validatePrepareMessage(prepareMsg.getSignedPayload())).isFalse(); + } + + @Test + public void receivingMessagesWithDifferentRoundIdFromProposalFails() { + final Proposal proposalMsg = + proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); + + final ConsensusRoundIdentifier invalidRoundIdentifier = + new ConsensusRoundIdentifier( + roundIdentifier.getSequenceNumber(), roundIdentifier.getRoundNumber() + 1); + final Prepare prepareMsg = + validatorMessageFactory.createSignedPreparePayload(invalidRoundIdentifier, block.getHash()); + final Commit commitMsg = + validatorMessageFactory.createSignedCommitPayload( + invalidRoundIdentifier, block.getHash(), SECP256K1.sign(block.getHash(), proposerKey)); + + assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); + assertThat(validator.validatePrepareMessage(prepareMsg.getSignedPayload())).isFalse(); + assertThat(validator.validateCommmitMessage(commitMsg.getSignedPayload())).isFalse(); + } + + @Test + public void receivingPrepareNonProposerValidatorWithCorrectRoundIsSuccessful() { + final Proposal proposalMsg = + proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); + final Prepare prepareMsg = + validatorMessageFactory.createSignedPreparePayload(roundIdentifier, block.getHash()); + + assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); + assertThat(validator.validatePrepareMessage(prepareMsg.getSignedPayload())).isTrue(); + } + + @Test + public void receivingACommitMessageWithAnInvalidCommitSealFails() { + final Proposal proposalMsg = + proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); + + final Commit commitMsg = + proposerMessageFactory.createSignedCommitPayload( + roundIdentifier, block.getHash(), SECP256K1.sign(block.getHash(), nonValidatorKey)); + + assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); + assertThat(validator.validateCommmitMessage(commitMsg.getSignedPayload())).isFalse(); + } + + @Test + public void commitMessageContainingValidSealFromValidatorIsSuccessful() { + final Proposal proposalMsg = + proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); + + final Commit proposerCommitMsg = + proposerMessageFactory.createSignedCommitPayload( + roundIdentifier, block.getHash(), SECP256K1.sign(block.getHash(), proposerKey)); + + final Commit validatorCommitMsg = + validatorMessageFactory.createSignedCommitPayload( + roundIdentifier, block.getHash(), SECP256K1.sign(block.getHash(), validatorKey)); + + assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); + assertThat(validator.validateCommmitMessage(proposerCommitMsg.getSignedPayload())).isTrue(); + assertThat(validator.validateCommmitMessage(validatorCommitMsg.getSignedPayload())).isTrue(); + } + + @Test + public void subsequentProposalHasDifferentSenderFails() { + final Proposal proposalMsg = + proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); + assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); + + final Proposal secondProposalMsg = + validatorMessageFactory.createSignedProposalPayload(roundIdentifier, block); + assertThat(validator.addSignedProposalPayload(secondProposalMsg.getSignedPayload())).isFalse(); + } + + @Test + public void subsequentProposalHasDifferentContentFails() { + final Proposal proposalMsg = + proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); + assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); + + final ConsensusRoundIdentifier newRoundIdentifier = new ConsensusRoundIdentifier(3, 0); + final Proposal secondProposalMsg = + proposerMessageFactory.createSignedProposalPayload(newRoundIdentifier, block); + assertThat(validator.addSignedProposalPayload(secondProposalMsg.getSignedPayload())).isFalse(); + } + + @Test + public void subsequentProposalHasIdenticalSenderAndContentIsSuccessful() { + final Proposal proposalMsg = + proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); + assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); + + final Proposal secondProposalMsg = + proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); + assertThat(validator.addSignedProposalPayload(secondProposalMsg.getSignedPayload())).isTrue(); + } + + @Test + public void blockRoundMisMatchWithMessageRoundFails() { + insertRoundToBlockHeader(roundIdentifier.getRoundNumber() + 1); + + final Proposal proposalMsg = + proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); + + assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isFalse(); + } +}