From 75424e6ff8c74921931b4fc43c1d3395abd563c5 Mon Sep 17 00:00:00 2001 From: tmohay Date: Mon, 4 Feb 2019 18:18:20 +1100 Subject: [PATCH] Created message validators for NewRound and RoundChange --- .../statemachine/IbftBlockHeightManager.java | 2 +- .../ibft/statemachine/RoundChangeManager.java | 2 +- .../validation/MessageValidatorFactory.java | 22 ++- .../validation/NewRoundMessageValidator.java | 162 +--------------- .../validation/NewRoundPayloadValidator.java | 176 ++++++++++++++++++ .../RoundChangeMessageValidator.java | 120 +----------- .../RoundChangePayloadValidator.java | 135 ++++++++++++++ .../IbftBlockHeightManagerTest.java | 6 +- .../statemachine/RoundChangeManagerTest.java | 16 +- .../NewRoundMessageValidatorTest.java | 52 ++++++ .../NewRoundSignedDataValidatorTest.java | 6 +- .../RoundChangeMessageValidatorTest.java | 51 +++++ .../RoundChangeSignedDataValidatorTest.java | 24 +-- 13 files changed, 469 insertions(+), 305 deletions(-) create mode 100644 consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundPayloadValidator.java create mode 100644 consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangePayloadValidator.java create mode 100644 consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundMessageValidatorTest.java create mode 100644 consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeMessageValidatorTest.java 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 4d531c6fb6..1f52cdaf8c 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 @@ -245,7 +245,7 @@ public void handleNewRoundPayload(final NewRound newRound) { } LOG.info("Received NewRound Payload for {}", newRound.getRoundIdentifier()); - if (newRoundMessageValidator.validateNewRoundMessage(newRound.getSignedPayload())) { + if (newRoundMessageValidator.validateNewRoundMessage(newRound)) { if (messageAge == FUTURE_ROUND) { startNewRound(newRound.getRoundIdentifier().getRoundNumber()); } diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/RoundChangeManager.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/RoundChangeManager.java index a260a7bb47..3f263604ee 100644 --- a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/RoundChangeManager.java +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/RoundChangeManager.java @@ -108,7 +108,7 @@ public Optional> appendRoundChangeMessage(final RoundCha } private boolean isMessageValid(final RoundChange msg) { - return roundChangeMessageValidator.validateMessage(msg.getSignedPayload()); + return roundChangeMessageValidator.validateRoundChange(msg); } private RoundChangeStatus storeRoundChangeMessage(final RoundChange msg) { 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 1c557185b1..945d8abcad 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 @@ -63,21 +63,23 @@ public RoundChangeMessageValidator createRoundChangeMessageValidator( final Collection
validators = protocolContext.getConsensusState().getVoteTally().getValidators(); return new RoundChangeMessageValidator( - this::createSignedDataValidator, - validators, - prepareMessageCountForQuorum( - IbftHelpers.calculateRequiredValidatorQuorum(validators.size())), - parentHeader.getNumber() + 1); + new RoundChangePayloadValidator( + this::createSignedDataValidator, + validators, + prepareMessageCountForQuorum( + IbftHelpers.calculateRequiredValidatorQuorum(validators.size())), + parentHeader.getNumber() + 1)); } public NewRoundMessageValidator createNewRoundValidator(final BlockHeader parentHeader) { final Collection
validators = protocolContext.getConsensusState().getVoteTally().getValidators(); return new NewRoundMessageValidator( - validators, - proposerSelector, - this::createSignedDataValidator, - IbftHelpers.calculateRequiredValidatorQuorum(validators.size()), - parentHeader.getNumber() + 1); + new NewRoundPayloadValidator( + validators, + proposerSelector, + 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 1e9b88ebac..f852e17279 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 @@ -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,165 +12,17 @@ */ 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.blockcreation.ProposerSelector; -import tech.pegasys.pantheon.consensus.ibft.payload.NewRoundPayload; -import tech.pegasys.pantheon.consensus.ibft.payload.PreparedCertificate; -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.RoundChangeMessageValidator.MessageValidatorForHeightFactory; -import tech.pegasys.pantheon.ethereum.core.Address; -import tech.pegasys.pantheon.ethereum.core.Hash; - -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.NewRound; public class NewRoundMessageValidator { - 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; - - public NewRoundMessageValidator( - final Collection
validators, - final ProposerSelector proposerSelector, - final MessageValidatorForHeightFactory messageValidatorFactory, - final long quorum, - final long chainHeight) { - this.validators = validators; - this.proposerSelector = proposerSelector; - this.messageValidatorFactory = messageValidatorFactory; - this.quorum = quorum; - this.chainHeight = chainHeight; - } - - public boolean validateNewRoundMessage(final SignedData msg) { + private final NewRoundPayloadValidator payloadValidator; - final NewRoundPayload payload = msg.getPayload(); - final ConsensusRoundIdentifier rootRoundIdentifier = payload.getRoundIdentifier(); - final Address expectedProposer = proposerSelector.selectProposerForRound(rootRoundIdentifier); - final RoundChangeCertificate roundChangeCert = payload.getRoundChangeCertificate(); - - if (!expectedProposer.equals(msg.getAuthor())) { - LOG.info("Invalid NewRound message, did not originate from expected proposer."); - return false; - } - - if (msg.getPayload().getRoundIdentifier().getSequenceNumber() != chainHeight) { - LOG.info("Invalid NewRound message, not valid for local chain height."); - return false; - } - - if (msg.getPayload().getRoundIdentifier().getRoundNumber() == 0) { - LOG.info("Invalid NewRound message, illegally targets a new round of 0."); - return false; - } - - final SignedData proposalPayload = payload.getProposalPayload(); - final SignedDataValidator proposalValidator = - messageValidatorFactory.createAt(rootRoundIdentifier); - if (!proposalValidator.addSignedProposalPayload(proposalPayload)) { - LOG.info("Invalid NewRound message, embedded proposal failed validation"); - return false; - } - - if (!validateRoundChangeMessagesAndEnsureTargetRoundMatchesRoot( - rootRoundIdentifier, roundChangeCert)) { - return false; - } - - return validateProposalMessageMatchesLatestPrepareCertificate(payload); + public NewRoundMessageValidator(final NewRoundPayloadValidator payloadValidator) { + this.payloadValidator = payloadValidator; } - 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 RoundChangeMessageValidator roundChangeValidator = - new RoundChangeMessageValidator( - messageValidatorFactory, - validators, - prepareMessageCountForQuorum(quorum), - chainHeight); - - if (!roundChangeValidator.validateMessage(roundChangeMsg)) { - LOG.info("Invalid NewRound message, embedded RoundChange message failed validation."); - return false; - } - } - return true; - } - - private boolean validateProposalMessageMatchesLatestPrepareCertificate( - final NewRoundPayload payload) { - - final RoundChangeCertificate roundChangeCert = payload.getRoundChangeCertificate(); - final Collection> roundChangeMsgs = - roundChangeCert.getRoundChangePayloads(); - - final Optional latestPreparedCertificate = - findLatestPreparedCertificate(roundChangeMsgs); - - if (!latestPreparedCertificate.isPresent()) { - LOG.info( - "No round change messages have a preparedCertificate, any valid block may be proposed."); - return true; - } - - // Get the hash of the block in latest prepareCert, not including the Round field. - final Hash roundAgnosticBlockHashPreparedCert = - IbftBlockHashing.calculateHashOfIbftBlockOnChain( - latestPreparedCertificate - .get() - .getProposalPayload() - .getPayload() - .getBlock() - .getHeader()); - - final Hash roundAgnosticBlockHashProposal = - IbftBlockHashing.calculateHashOfIbftBlockOnChain( - payload.getProposalPayload().getPayload().getBlock().getHeader()); - - if (!roundAgnosticBlockHashPreparedCert.equals(roundAgnosticBlockHashProposal)) { - LOG.info( - "Invalid NewRound message, block in latest RoundChange does not match proposed block."); - return false; - } - - return true; + public boolean validateNewRoundMessage(final NewRound msg) { + return payloadValidator.validateNewRoundMessage(msg.getSignedPayload()); } } 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 new file mode 100644 index 0000000000..9093eb6cf6 --- /dev/null +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundPayloadValidator.java @@ -0,0 +1,176 @@ +/* + * 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 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.blockcreation.ProposerSelector; +import tech.pegasys.pantheon.consensus.ibft.payload.NewRoundPayload; +import tech.pegasys.pantheon.consensus.ibft.payload.PreparedCertificate; +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 tech.pegasys.pantheon.ethereum.core.Hash; + +import java.util.Collection; +import java.util.Optional; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +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; + + public NewRoundPayloadValidator( + final Collection
validators, + final ProposerSelector proposerSelector, + final MessageValidatorForHeightFactory messageValidatorFactory, + final long quorum, + final long chainHeight) { + this.validators = validators; + this.proposerSelector = proposerSelector; + this.messageValidatorFactory = messageValidatorFactory; + this.quorum = quorum; + this.chainHeight = chainHeight; + } + + public boolean validateNewRoundMessage(final SignedData msg) { + + final NewRoundPayload payload = msg.getPayload(); + final ConsensusRoundIdentifier rootRoundIdentifier = payload.getRoundIdentifier(); + final Address expectedProposer = proposerSelector.selectProposerForRound(rootRoundIdentifier); + final RoundChangeCertificate roundChangeCert = payload.getRoundChangeCertificate(); + + if (!expectedProposer.equals(msg.getAuthor())) { + LOG.info("Invalid NewRound message, did not originate from expected proposer."); + return false; + } + + if (msg.getPayload().getRoundIdentifier().getSequenceNumber() != chainHeight) { + LOG.info("Invalid NewRound message, not valid for local chain height."); + return false; + } + + if (msg.getPayload().getRoundIdentifier().getRoundNumber() == 0) { + LOG.info("Invalid NewRound message, illegally targets a new round of 0."); + return false; + } + + final SignedData proposalPayload = payload.getProposalPayload(); + final SignedDataValidator proposalValidator = + messageValidatorFactory.createAt(rootRoundIdentifier); + if (!proposalValidator.addSignedProposalPayload(proposalPayload)) { + LOG.info("Invalid NewRound message, embedded proposal failed validation"); + return false; + } + + if (!validateRoundChangeMessagesAndEnsureTargetRoundMatchesRoot( + rootRoundIdentifier, roundChangeCert)) { + return false; + } + + return validateProposalMessageMatchesLatestPrepareCertificate(payload); + } + + 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; + } + + private boolean validateProposalMessageMatchesLatestPrepareCertificate( + final NewRoundPayload payload) { + + final RoundChangeCertificate roundChangeCert = payload.getRoundChangeCertificate(); + final Collection> roundChangeMsgs = + roundChangeCert.getRoundChangePayloads(); + + final Optional latestPreparedCertificate = + findLatestPreparedCertificate(roundChangeMsgs); + + if (!latestPreparedCertificate.isPresent()) { + LOG.info( + "No round change messages have a preparedCertificate, any valid block may be proposed."); + return true; + } + + // Get the hash of the block in latest prepareCert, not including the Round field. + final Hash roundAgnosticBlockHashPreparedCert = + IbftBlockHashing.calculateHashOfIbftBlockOnChain( + latestPreparedCertificate + .get() + .getProposalPayload() + .getPayload() + .getBlock() + .getHeader()); + + final Hash roundAgnosticBlockHashProposal = + IbftBlockHashing.calculateHashOfIbftBlockOnChain( + payload.getProposalPayload().getPayload().getBlock().getHeader()); + + if (!roundAgnosticBlockHashPreparedCert.equals(roundAgnosticBlockHashProposal)) { + 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/RoundChangeMessageValidator.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeMessageValidator.java index bd0ed139c9..f96d3c9e64 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 @@ -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,124 +12,18 @@ */ package tech.pegasys.pantheon.consensus.ibft.validation; -import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; -import tech.pegasys.pantheon.consensus.ibft.payload.PreparePayload; -import tech.pegasys.pantheon.consensus.ibft.payload.PreparedCertificate; -import tech.pegasys.pantheon.consensus.ibft.payload.ProposalPayload; -import tech.pegasys.pantheon.consensus.ibft.payload.RoundChangePayload; -import tech.pegasys.pantheon.consensus.ibft.payload.SignedData; -import tech.pegasys.pantheon.ethereum.core.Address; - -import java.util.Collection; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import tech.pegasys.pantheon.consensus.ibft.messagewrappers.RoundChange; public class RoundChangeMessageValidator { - private static final Logger LOG = LogManager.getLogger(); - - private final MessageValidatorForHeightFactory messageValidatorFactory; - private final Collection
validators; - private final long minimumPrepareMessages; - private final long chainHeight; + private final RoundChangePayloadValidator roundChangePayloadValidator; public RoundChangeMessageValidator( - final MessageValidatorForHeightFactory messageValidatorFactory, - final Collection
validators, - final long minimumPrepareMessages, - final long chainHeight) { - this.messageValidatorFactory = messageValidatorFactory; - this.validators = validators; - this.minimumPrepareMessages = minimumPrepareMessages; - this.chainHeight = chainHeight; - } - - public boolean validateMessage(final SignedData msg) { - - if (!validators.contains(msg.getAuthor())) { - LOG.info( - "Invalid RoundChange message, was not transmitted by a validator for the associated" - + " round."); - return false; - } - - final ConsensusRoundIdentifier targetRound = msg.getPayload().getRoundIdentifier(); - - if (targetRound.getSequenceNumber() != chainHeight) { - LOG.info("Invalid RoundChange message, not valid for local chain height."); - return false; - } - - if (msg.getPayload().getPreparedCertificate().isPresent()) { - final PreparedCertificate certificate = msg.getPayload().getPreparedCertificate().get(); - - return validatePrepareCertificate(certificate, targetRound); - } - - return true; - } - - private boolean validatePrepareCertificate( - final PreparedCertificate certificate, final ConsensusRoundIdentifier roundChangeTarget) { - final SignedData proposalMessage = certificate.getProposalPayload(); - - final ConsensusRoundIdentifier proposalRoundIdentifier = - proposalMessage.getPayload().getRoundIdentifier(); - - if (!validatePreparedCertificateRound(proposalRoundIdentifier, roundChangeTarget)) { - return false; - } - - final SignedDataValidator signedDataValidator = - messageValidatorFactory.createAt(proposalRoundIdentifier); - return validateConsistencyOfPrepareCertificateMessages(certificate, signedDataValidator); - } - - private boolean validateConsistencyOfPrepareCertificateMessages( - final PreparedCertificate certificate, final SignedDataValidator signedDataValidator) { - - if (!signedDataValidator.addSignedProposalPayload(certificate.getProposalPayload())) { - LOG.info("Invalid RoundChange message, embedded Proposal message failed validation."); - return false; - } - - if (certificate.getPreparePayloads().size() < minimumPrepareMessages) { - LOG.info( - "Invalid RoundChange message, insufficient Prepare messages exist to justify " - + "prepare certificate."); - return false; - } - - for (final SignedData prepareMsg : certificate.getPreparePayloads()) { - if (!signedDataValidator.validatePrepareMessage(prepareMsg)) { - LOG.info("Invalid RoundChange message, embedded Prepare message failed validation."); - return false; - } - } - - return true; - } - - private boolean validatePreparedCertificateRound( - final ConsensusRoundIdentifier prepareCertRound, - final ConsensusRoundIdentifier roundChangeTarget) { - - if (prepareCertRound.getSequenceNumber() != roundChangeTarget.getSequenceNumber()) { - LOG.info("Invalid RoundChange message, PreparedCertificate is not for local chain height."); - return false; - } - - if (prepareCertRound.getRoundNumber() >= roundChangeTarget.getRoundNumber()) { - LOG.info( - "Invalid RoundChange message, PreparedCertificate not older than RoundChange target."); - return false; - } - return true; + final RoundChangePayloadValidator roundChangePayloadValidator) { + this.roundChangePayloadValidator = roundChangePayloadValidator; } - @FunctionalInterface - public interface MessageValidatorForHeightFactory { - SignedDataValidator createAt(final ConsensusRoundIdentifier roundIdentifier); + public boolean validateRoundChange(final RoundChange msg) { + return roundChangePayloadValidator.validateRoundChange(msg.getSignedPayload()); } } diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangePayloadValidator.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangePayloadValidator.java new file mode 100644 index 0000000000..64fb19ceb8 --- /dev/null +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangePayloadValidator.java @@ -0,0 +1,135 @@ +/* + * 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.payload.PreparePayload; +import tech.pegasys.pantheon.consensus.ibft.payload.PreparedCertificate; +import tech.pegasys.pantheon.consensus.ibft.payload.ProposalPayload; +import tech.pegasys.pantheon.consensus.ibft.payload.RoundChangePayload; +import tech.pegasys.pantheon.consensus.ibft.payload.SignedData; +import tech.pegasys.pantheon.ethereum.core.Address; + +import java.util.Collection; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class RoundChangePayloadValidator { + + private static final Logger LOG = LogManager.getLogger(); + + private final MessageValidatorForHeightFactory messageValidatorFactory; + private final Collection
validators; + private final long minimumPrepareMessages; + private final long chainHeight; + + public RoundChangePayloadValidator( + final MessageValidatorForHeightFactory messageValidatorFactory, + final Collection
validators, + final long minimumPrepareMessages, + final long chainHeight) { + this.messageValidatorFactory = messageValidatorFactory; + this.validators = validators; + this.minimumPrepareMessages = minimumPrepareMessages; + this.chainHeight = chainHeight; + } + + public boolean validateRoundChange(final SignedData msg) { + + if (!validators.contains(msg.getAuthor())) { + LOG.info( + "Invalid RoundChange message, was not transmitted by a validator for the associated" + + " round."); + return false; + } + + final ConsensusRoundIdentifier targetRound = msg.getPayload().getRoundIdentifier(); + + if (targetRound.getSequenceNumber() != chainHeight) { + LOG.info("Invalid RoundChange message, not valid for local chain height."); + return false; + } + + if (msg.getPayload().getPreparedCertificate().isPresent()) { + final PreparedCertificate certificate = msg.getPayload().getPreparedCertificate().get(); + + return validatePrepareCertificate(certificate, targetRound); + } + + return true; + } + + private boolean validatePrepareCertificate( + final PreparedCertificate certificate, final ConsensusRoundIdentifier roundChangeTarget) { + final SignedData proposalMessage = certificate.getProposalPayload(); + + final ConsensusRoundIdentifier proposalRoundIdentifier = + proposalMessage.getPayload().getRoundIdentifier(); + + if (!validatePreparedCertificateRound(proposalRoundIdentifier, roundChangeTarget)) { + return false; + } + + final SignedDataValidator signedDataValidator = + messageValidatorFactory.createAt(proposalRoundIdentifier); + return validateConsistencyOfPrepareCertificateMessages(certificate, signedDataValidator); + } + + private boolean validateConsistencyOfPrepareCertificateMessages( + final PreparedCertificate certificate, final SignedDataValidator signedDataValidator) { + + if (!signedDataValidator.addSignedProposalPayload(certificate.getProposalPayload())) { + LOG.info("Invalid RoundChange message, embedded Proposal message failed validation."); + return false; + } + + if (certificate.getPreparePayloads().size() < minimumPrepareMessages) { + LOG.info( + "Invalid RoundChange message, insufficient Prepare messages exist to justify " + + "prepare certificate."); + return false; + } + + for (final SignedData prepareMsg : certificate.getPreparePayloads()) { + if (!signedDataValidator.validatePrepareMessage(prepareMsg)) { + LOG.info("Invalid RoundChange message, embedded Prepare message failed validation."); + return false; + } + } + + return true; + } + + private boolean validatePreparedCertificateRound( + final ConsensusRoundIdentifier prepareCertRound, + final ConsensusRoundIdentifier roundChangeTarget) { + + if (prepareCertRound.getSequenceNumber() != roundChangeTarget.getSequenceNumber()) { + LOG.info("Invalid RoundChange message, PreparedCertificate is not for local chain height."); + return false; + } + + if (prepareCertRound.getRoundNumber() >= roundChangeTarget.getRoundNumber()) { + LOG.info( + "Invalid RoundChange message, PreparedCertificate not older than RoundChange target."); + return false; + } + return true; + } + + @FunctionalInterface + public interface MessageValidatorForHeightFactory { + SignedDataValidator createAt(final ConsensusRoundIdentifier roundIdentifier); + } +} 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 da68a47ff4..e519145edb 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 @@ -90,7 +90,7 @@ public class IbftBlockHeightManagerTest { @Mock private BlockImporter blockImporter; @Mock private BlockTimer blockTimer; @Mock private RoundTimer roundTimer; - @Mock private NewRoundMessageValidator newRoundMessageValidator; + @Mock private NewRoundMessageValidator newRoundPayloadValidator; @Captor private ArgumentCaptor> terminatedRoundArtefactsCaptor; @@ -134,9 +134,9 @@ public void setup() { when(finalState.getQuorum()).thenReturn(3); when(finalState.getMessageFactory()).thenReturn(messageFactory); when(blockCreator.createBlock(anyLong())).thenReturn(createdBlock); - when(newRoundMessageValidator.validateNewRoundMessage(any())).thenReturn(true); + when(newRoundPayloadValidator.validateNewRoundMessage(any())).thenReturn(true); when(messageValidatorFactory.createNewRoundValidator(any())) - .thenReturn(newRoundMessageValidator); + .thenReturn(newRoundPayloadValidator); when(messageValidatorFactory.createMessageValidator(any())).thenReturn(messageValidator); protocolContext = 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 b2e5a5e68d..4c6301e1fb 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 @@ -26,6 +26,7 @@ import tech.pegasys.pantheon.consensus.ibft.messagewrappers.RoundChange; import tech.pegasys.pantheon.consensus.ibft.payload.MessageFactory; import tech.pegasys.pantheon.consensus.ibft.validation.RoundChangeMessageValidator; +import tech.pegasys.pantheon.consensus.ibft.validation.RoundChangePayloadValidator; import tech.pegasys.pantheon.consensus.ibft.validation.SignedDataValidator; import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; import tech.pegasys.pantheon.ethereum.BlockValidator; @@ -80,8 +81,8 @@ public void setup() { .thenReturn(Optional.of(new BlockProcessingOutputs(null, null))); BlockHeader parentHeader = mock(BlockHeader.class); - RoundChangeMessageValidator.MessageValidatorForHeightFactory messageValidatorFactory = - mock(RoundChangeMessageValidator.MessageValidatorForHeightFactory.class); + RoundChangePayloadValidator.MessageValidatorForHeightFactory messageValidatorFactory = + mock(RoundChangePayloadValidator.MessageValidatorForHeightFactory.class); when(messageValidatorFactory.createAt(ri1)) .thenAnswer( @@ -113,11 +114,12 @@ public void setup() { final RoundChangeMessageValidator roundChangeMessageValidator = new RoundChangeMessageValidator( - messageValidatorFactory, - validators, - IbftHelpers.calculateRequiredValidatorQuorum( - IbftHelpers.calculateRequiredValidatorQuorum(validators.size())), - 2); + new RoundChangePayloadValidator( + messageValidatorFactory, + validators, + IbftHelpers.calculateRequiredValidatorQuorum( + IbftHelpers.calculateRequiredValidatorQuorum(validators.size())), + 2)); manager = new RoundChangeManager(2, roundChangeMessageValidator); } 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 new file mode 100644 index 0000000000..858f7168a9 --- /dev/null +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundMessageValidatorTest.java @@ -0,0 +1,52 @@ +/* + * 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.emptyList; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; +import tech.pegasys.pantheon.consensus.ibft.TestHelpers; +import tech.pegasys.pantheon.consensus.ibft.messagewrappers.NewRound; +import tech.pegasys.pantheon.consensus.ibft.payload.MessageFactory; +import tech.pegasys.pantheon.consensus.ibft.payload.RoundChangeCertificate; +import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; +import tech.pegasys.pantheon.ethereum.core.Block; + +import org.junit.Test; + +public class NewRoundMessageValidatorTest { + + private final NewRoundPayloadValidator payloadValidator = mock(NewRoundPayloadValidator.class); + private final KeyPair keyPair = KeyPair.generate(); + private final MessageFactory messageFactory = new MessageFactory(keyPair); + private final ConsensusRoundIdentifier roundIdentifier = new ConsensusRoundIdentifier(1, 1); + private final Block block = + TestHelpers.createProposalBlock(emptyList(), roundIdentifier.getRoundNumber()); + + private final NewRoundMessageValidator validator = new NewRoundMessageValidator(payloadValidator); + + @Test + public void underlyingPayloadValidatorIsInvokedWithCorrectParameters() { + final NewRound message = + messageFactory.createSignedNewRoundPayload( + roundIdentifier, + new RoundChangeCertificate(emptyList()), + messageFactory.createSignedProposalPayload(roundIdentifier, block).getSignedPayload()); + + validator.validateNewRoundMessage(message); + verify(payloadValidator, times(1)).validateNewRoundMessage(message.getSignedPayload()); + } +} 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 8de87c97ca..f79ea3d4d8 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 @@ -27,7 +27,7 @@ import tech.pegasys.pantheon.consensus.ibft.payload.NewRoundPayload; import tech.pegasys.pantheon.consensus.ibft.payload.RoundChangeCertificate; import tech.pegasys.pantheon.consensus.ibft.statemachine.TerminatedRoundArtefacts; -import tech.pegasys.pantheon.consensus.ibft.validation.RoundChangeMessageValidator.MessageValidatorForHeightFactory; +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; @@ -53,7 +53,7 @@ public class NewRoundSignedDataValidatorTest { private final long chainHeight = 2; private final ConsensusRoundIdentifier roundIdentifier = new ConsensusRoundIdentifier(chainHeight, 4); - private NewRoundMessageValidator validator; + private NewRoundPayloadValidator validator; private final ProposerSelector proposerSelector = mock(ProposerSelector.class); private final MessageValidatorForHeightFactory validatorFactory = @@ -83,7 +83,7 @@ public void setup() { when(signedDataValidator.validatePrepareMessage(any())).thenReturn(true); validator = - new NewRoundMessageValidator( + new NewRoundPayloadValidator( validators, proposerSelector, validatorFactory, 1, chainHeight); } 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/RoundChangeMessageValidatorTest.java new file mode 100644 index 0000000000..daf9c78ae3 --- /dev/null +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeMessageValidatorTest.java @@ -0,0 +1,51 @@ +/* + * 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.emptyList; +import static java.util.Optional.empty; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; +import tech.pegasys.pantheon.consensus.ibft.TestHelpers; +import tech.pegasys.pantheon.consensus.ibft.messagewrappers.RoundChange; +import tech.pegasys.pantheon.consensus.ibft.payload.MessageFactory; +import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; +import tech.pegasys.pantheon.ethereum.core.Block; + +import org.junit.Test; + +public class RoundChangeMessageValidatorTest { + + private final RoundChangePayloadValidator payloadValidator = + mock(RoundChangePayloadValidator.class); + private final KeyPair keyPair = KeyPair.generate(); + private final MessageFactory messageFactory = new MessageFactory(keyPair); + private final ConsensusRoundIdentifier roundIdentifier = new ConsensusRoundIdentifier(1, 1); + private final Block block = + TestHelpers.createProposalBlock(emptyList(), roundIdentifier.getRoundNumber()); + + private final RoundChangeMessageValidator validator = + new RoundChangeMessageValidator(payloadValidator); + + @Test + public void underlyingPayloadValidatorIsInvokedWithCorrectParameters() { + final RoundChange message = + messageFactory.createSignedRoundChangePayload(roundIdentifier, empty()); + + validator.validateRoundChange(message); + verify(payloadValidator, times(1)).validateRoundChange(message.getSignedPayload()); + } +} diff --git a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeSignedDataValidatorTest.java b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeSignedDataValidatorTest.java index ba8411a162..f67b874aa0 100644 --- a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeSignedDataValidatorTest.java +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeSignedDataValidatorTest.java @@ -26,7 +26,7 @@ import tech.pegasys.pantheon.consensus.ibft.payload.MessageFactory; import tech.pegasys.pantheon.consensus.ibft.payload.PreparedCertificate; import tech.pegasys.pantheon.consensus.ibft.statemachine.TerminatedRoundArtefacts; -import tech.pegasys.pantheon.consensus.ibft.validation.RoundChangeMessageValidator.MessageValidatorForHeightFactory; +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; @@ -62,8 +62,8 @@ public class RoundChangeSignedDataValidatorTest { private final MessageValidatorForHeightFactory validatorFactory = mock(MessageValidatorForHeightFactory.class); - private final RoundChangeMessageValidator validator = - new RoundChangeMessageValidator(validatorFactory, validators, 1, chainHeight); + private final RoundChangePayloadValidator validator = + new RoundChangePayloadValidator(validatorFactory, validators, 1, chainHeight); @Before public void setup() { @@ -74,7 +74,7 @@ public void setup() { when(validatorFactory.createAt(any())).thenReturn(basicValidator); // By default, have all basic messages being valid thus any failures are attributed to logic - // in the RoundChangeMessageValidator + // in the RoundChangePayloadValidator when(basicValidator.addSignedProposalPayload(any())).thenReturn(true); when(basicValidator.validatePrepareMessage(any())).thenReturn(true); } @@ -83,7 +83,7 @@ public void setup() { public void roundChangeSentByNonValidatorFails() { final RoundChange msg = nonValidatorMessageFactory.createSignedRoundChangePayload(targetRound, Optional.empty()); - assertThat(validator.validateMessage(msg.getSignedPayload())).isFalse(); + assertThat(validator.validateRoundChange(msg.getSignedPayload())).isFalse(); } @Test @@ -91,7 +91,7 @@ public void roundChangeContainingNoCertificateIsSuccessful() { final RoundChange msg = proposerMessageFactory.createSignedRoundChangePayload(targetRound, Optional.empty()); - assertThat(validator.validateMessage(msg.getSignedPayload())).isTrue(); + assertThat(validator.validateRoundChange(msg.getSignedPayload())).isTrue(); } @Test @@ -110,7 +110,7 @@ public void roundChangeContainingInvalidProposalFails() { when(basicValidator.addSignedProposalPayload(any())).thenReturn(false); - assertThat(validator.validateMessage(msg.getSignedPayload())).isFalse(); + assertThat(validator.validateRoundChange(msg.getSignedPayload())).isFalse(); verify(validatorFactory, times(1)) .createAt(prepareCertificate.getProposalPayload().getPayload().getRoundIdentifier()); verify(basicValidator, times(1)) @@ -131,7 +131,7 @@ public void roundChangeContainingValidProposalButNoPrepareMessagesFails() { targetRound, Optional.of(terminatedRoundArtefacts)); when(basicValidator.addSignedProposalPayload(any())).thenReturn(true); - assertThat(validator.validateMessage(msg.getSignedPayload())).isFalse(); + assertThat(validator.validateRoundChange(msg.getSignedPayload())).isFalse(); } @Test @@ -150,7 +150,7 @@ public void roundChangeInvalidPrepareMessageFromProposerFails() { proposerMessageFactory.createSignedRoundChangePayload( targetRound, Optional.of(terminatedRoundArtefacts)); - assertThat(validator.validateMessage(msg.getSignedPayload())).isFalse(); + assertThat(validator.validateRoundChange(msg.getSignedPayload())).isFalse(); verify(basicValidator, times(1)).validatePrepareMessage(prepareMsg.getSignedPayload()); verify(basicValidator, never()).validateCommmitMessage(any()); @@ -165,7 +165,7 @@ public void roundChangeWithDifferentSequenceNumberFails() { proposerMessageFactory.createSignedRoundChangePayload( latterRoundIdentifier, Optional.empty()); - assertThat(validator.validateMessage(msg.getSignedPayload())).isFalse(); + assertThat(validator.validateRoundChange(msg.getSignedPayload())).isFalse(); verify(basicValidator, never()).validatePrepareMessage(any()); } @@ -186,7 +186,7 @@ public void roundChangeWithProposalFromARoundAheadOfRoundChangeTargetFails() { proposerMessageFactory.createSignedRoundChangePayload( targetRound, Optional.of(terminatedRoundArtefacts)); - assertThat(validator.validateMessage(msg.getSignedPayload())).isFalse(); + assertThat(validator.validateRoundChange(msg.getSignedPayload())).isFalse(); verify(validatorFactory, never()).createAt(any()); verify(basicValidator, never()).validatePrepareMessage(prepareMsg.getSignedPayload()); verify(basicValidator, never()).validateCommmitMessage(any()); @@ -212,7 +212,7 @@ public void roundChangeWithPastProposalForCurrentHeightIsSuccessful() { .thenReturn(true); when(basicValidator.validatePrepareMessage(prepareMsg.getSignedPayload())).thenReturn(true); - assertThat(validator.validateMessage(msg.getSignedPayload())).isTrue(); + assertThat(validator.validateRoundChange(msg.getSignedPayload())).isTrue(); verify(validatorFactory, times(1)) .createAt(prepareCertificate.getProposalPayload().getPayload().getRoundIdentifier()); verify(basicValidator, times(1))