Created message validators for NewRound and RoundChange (#760)

Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>
pull/2/head
tmohay 6 years ago committed by GitHub
parent 8ca6181001
commit 49865063ee
  1. 2
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftBlockHeightManager.java
  2. 2
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/RoundChangeManager.java
  3. 22
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidatorFactory.java
  4. 162
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundMessageValidator.java
  5. 176
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundPayloadValidator.java
  6. 120
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeMessageValidator.java
  7. 135
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangePayloadValidator.java
  8. 6
      consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftBlockHeightManagerTest.java
  9. 16
      consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/statemachine/RoundChangeManagerTest.java
  10. 52
      consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundMessageValidatorTest.java
  11. 6
      consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundSignedDataValidatorTest.java
  12. 51
      consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeMessageValidatorTest.java
  13. 24
      consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeSignedDataValidatorTest.java

@ -245,7 +245,7 @@ public class IbftBlockHeightManager implements BlockHeightManager {
} }
LOG.info("Received NewRound Payload for {}", newRound.getRoundIdentifier()); LOG.info("Received NewRound Payload for {}", newRound.getRoundIdentifier());
if (newRoundMessageValidator.validateNewRoundMessage(newRound.getSignedPayload())) { if (newRoundMessageValidator.validateNewRoundMessage(newRound)) {
if (messageAge == FUTURE_ROUND) { if (messageAge == FUTURE_ROUND) {
startNewRound(newRound.getRoundIdentifier().getRoundNumber()); startNewRound(newRound.getRoundIdentifier().getRoundNumber());
} }

@ -108,7 +108,7 @@ public class RoundChangeManager {
} }
private boolean isMessageValid(final RoundChange msg) { private boolean isMessageValid(final RoundChange msg) {
return roundChangeMessageValidator.validateMessage(msg.getSignedPayload()); return roundChangeMessageValidator.validateRoundChange(msg);
} }
private RoundChangeStatus storeRoundChangeMessage(final RoundChange msg) { private RoundChangeStatus storeRoundChangeMessage(final RoundChange msg) {

@ -63,21 +63,23 @@ public class MessageValidatorFactory {
final Collection<Address> validators = final Collection<Address> validators =
protocolContext.getConsensusState().getVoteTally().getValidators(); protocolContext.getConsensusState().getVoteTally().getValidators();
return new RoundChangeMessageValidator( return new RoundChangeMessageValidator(
this::createSignedDataValidator, new RoundChangePayloadValidator(
validators, this::createSignedDataValidator,
prepareMessageCountForQuorum( validators,
IbftHelpers.calculateRequiredValidatorQuorum(validators.size())), prepareMessageCountForQuorum(
parentHeader.getNumber() + 1); IbftHelpers.calculateRequiredValidatorQuorum(validators.size())),
parentHeader.getNumber() + 1));
} }
public NewRoundMessageValidator createNewRoundValidator(final BlockHeader parentHeader) { public NewRoundMessageValidator createNewRoundValidator(final BlockHeader parentHeader) {
final Collection<Address> validators = final Collection<Address> validators =
protocolContext.getConsensusState().getVoteTally().getValidators(); protocolContext.getConsensusState().getVoteTally().getValidators();
return new NewRoundMessageValidator( return new NewRoundMessageValidator(
validators, new NewRoundPayloadValidator(
proposerSelector, validators,
this::createSignedDataValidator, proposerSelector,
IbftHelpers.calculateRequiredValidatorQuorum(validators.size()), this::createSignedDataValidator,
parentHeader.getNumber() + 1); IbftHelpers.calculateRequiredValidatorQuorum(validators.size()),
parentHeader.getNumber() + 1));
} }
} }

@ -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 * 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 * the License. You may obtain a copy of the License at
@ -12,165 +12,17 @@
*/ */
package tech.pegasys.pantheon.consensus.ibft.validation; package tech.pegasys.pantheon.consensus.ibft.validation;
import static tech.pegasys.pantheon.consensus.ibft.IbftHelpers.findLatestPreparedCertificate; import tech.pegasys.pantheon.consensus.ibft.messagewrappers.NewRound;
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;
public class NewRoundMessageValidator { public class NewRoundMessageValidator {
private static final Logger LOG = LogManager.getLogger(); private final NewRoundPayloadValidator payloadValidator;
private final Collection<Address> validators;
private final ProposerSelector proposerSelector;
private final MessageValidatorForHeightFactory messageValidatorFactory;
private final long quorum;
private final long chainHeight;
public NewRoundMessageValidator(
final Collection<Address> 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<NewRoundPayload> msg) {
final NewRoundPayload payload = msg.getPayload(); public NewRoundMessageValidator(final NewRoundPayloadValidator payloadValidator) {
final ConsensusRoundIdentifier rootRoundIdentifier = payload.getRoundIdentifier(); this.payloadValidator = payloadValidator;
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> 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( public boolean validateNewRoundMessage(final NewRound msg) {
final ConsensusRoundIdentifier expectedRound, final RoundChangeCertificate roundChangeCert) { return payloadValidator.validateNewRoundMessage(msg.getSignedPayload());
final Collection<SignedData<RoundChangePayload>> 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<RoundChangePayload> 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<SignedData<RoundChangePayload>> roundChangeMsgs =
roundChangeCert.getRoundChangePayloads();
final Optional<PreparedCertificate> 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;
} }
} }

@ -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<Address> validators;
private final ProposerSelector proposerSelector;
private final MessageValidatorForHeightFactory messageValidatorFactory;
private final long quorum;
private final long chainHeight;
public NewRoundPayloadValidator(
final Collection<Address> 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<NewRoundPayload> 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> 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<SignedData<RoundChangePayload>> 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<RoundChangePayload> 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<SignedData<RoundChangePayload>> roundChangeMsgs =
roundChangeCert.getRoundChangePayloads();
final Optional<PreparedCertificate> 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;
}
}

@ -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 * 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 * the License. You may obtain a copy of the License at
@ -12,124 +12,18 @@
*/ */
package tech.pegasys.pantheon.consensus.ibft.validation; package tech.pegasys.pantheon.consensus.ibft.validation;
import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; import tech.pegasys.pantheon.consensus.ibft.messagewrappers.RoundChange;
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 RoundChangeMessageValidator { public class RoundChangeMessageValidator {
private static final Logger LOG = LogManager.getLogger(); private final RoundChangePayloadValidator roundChangePayloadValidator;
private final MessageValidatorForHeightFactory messageValidatorFactory;
private final Collection<Address> validators;
private final long minimumPrepareMessages;
private final long chainHeight;
public RoundChangeMessageValidator( public RoundChangeMessageValidator(
final MessageValidatorForHeightFactory messageValidatorFactory, final RoundChangePayloadValidator roundChangePayloadValidator) {
final Collection<Address> validators, this.roundChangePayloadValidator = roundChangePayloadValidator;
final long minimumPrepareMessages,
final long chainHeight) {
this.messageValidatorFactory = messageValidatorFactory;
this.validators = validators;
this.minimumPrepareMessages = minimumPrepareMessages;
this.chainHeight = chainHeight;
}
public boolean validateMessage(final SignedData<RoundChangePayload> 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<ProposalPayload> 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<PreparePayload> 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 boolean validateRoundChange(final RoundChange msg) {
public interface MessageValidatorForHeightFactory { return roundChangePayloadValidator.validateRoundChange(msg.getSignedPayload());
SignedDataValidator createAt(final ConsensusRoundIdentifier roundIdentifier);
} }
} }

@ -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<Address> validators;
private final long minimumPrepareMessages;
private final long chainHeight;
public RoundChangePayloadValidator(
final MessageValidatorForHeightFactory messageValidatorFactory,
final Collection<Address> validators,
final long minimumPrepareMessages,
final long chainHeight) {
this.messageValidatorFactory = messageValidatorFactory;
this.validators = validators;
this.minimumPrepareMessages = minimumPrepareMessages;
this.chainHeight = chainHeight;
}
public boolean validateRoundChange(final SignedData<RoundChangePayload> 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<ProposalPayload> 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<PreparePayload> 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);
}
}

@ -90,7 +90,7 @@ public class IbftBlockHeightManagerTest {
@Mock private BlockImporter<IbftContext> blockImporter; @Mock private BlockImporter<IbftContext> blockImporter;
@Mock private BlockTimer blockTimer; @Mock private BlockTimer blockTimer;
@Mock private RoundTimer roundTimer; @Mock private RoundTimer roundTimer;
@Mock private NewRoundMessageValidator newRoundMessageValidator; @Mock private NewRoundMessageValidator newRoundPayloadValidator;
@Captor private ArgumentCaptor<Optional<TerminatedRoundArtefacts>> terminatedRoundArtefactsCaptor; @Captor private ArgumentCaptor<Optional<TerminatedRoundArtefacts>> terminatedRoundArtefactsCaptor;
@ -134,9 +134,9 @@ public class IbftBlockHeightManagerTest {
when(finalState.getQuorum()).thenReturn(3); when(finalState.getQuorum()).thenReturn(3);
when(finalState.getMessageFactory()).thenReturn(messageFactory); when(finalState.getMessageFactory()).thenReturn(messageFactory);
when(blockCreator.createBlock(anyLong())).thenReturn(createdBlock); when(blockCreator.createBlock(anyLong())).thenReturn(createdBlock);
when(newRoundMessageValidator.validateNewRoundMessage(any())).thenReturn(true); when(newRoundPayloadValidator.validateNewRoundMessage(any())).thenReturn(true);
when(messageValidatorFactory.createNewRoundValidator(any())) when(messageValidatorFactory.createNewRoundValidator(any()))
.thenReturn(newRoundMessageValidator); .thenReturn(newRoundPayloadValidator);
when(messageValidatorFactory.createMessageValidator(any())).thenReturn(messageValidator); when(messageValidatorFactory.createMessageValidator(any())).thenReturn(messageValidator);
protocolContext = protocolContext =

@ -26,6 +26,7 @@ import tech.pegasys.pantheon.consensus.ibft.messagewrappers.Proposal;
import tech.pegasys.pantheon.consensus.ibft.messagewrappers.RoundChange; import tech.pegasys.pantheon.consensus.ibft.messagewrappers.RoundChange;
import tech.pegasys.pantheon.consensus.ibft.payload.MessageFactory; import tech.pegasys.pantheon.consensus.ibft.payload.MessageFactory;
import tech.pegasys.pantheon.consensus.ibft.validation.RoundChangeMessageValidator; 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.consensus.ibft.validation.SignedDataValidator;
import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair;
import tech.pegasys.pantheon.ethereum.BlockValidator; import tech.pegasys.pantheon.ethereum.BlockValidator;
@ -80,8 +81,8 @@ public class RoundChangeManagerTest {
.thenReturn(Optional.of(new BlockProcessingOutputs(null, null))); .thenReturn(Optional.of(new BlockProcessingOutputs(null, null)));
BlockHeader parentHeader = mock(BlockHeader.class); BlockHeader parentHeader = mock(BlockHeader.class);
RoundChangeMessageValidator.MessageValidatorForHeightFactory messageValidatorFactory = RoundChangePayloadValidator.MessageValidatorForHeightFactory messageValidatorFactory =
mock(RoundChangeMessageValidator.MessageValidatorForHeightFactory.class); mock(RoundChangePayloadValidator.MessageValidatorForHeightFactory.class);
when(messageValidatorFactory.createAt(ri1)) when(messageValidatorFactory.createAt(ri1))
.thenAnswer( .thenAnswer(
@ -113,11 +114,12 @@ public class RoundChangeManagerTest {
final RoundChangeMessageValidator roundChangeMessageValidator = final RoundChangeMessageValidator roundChangeMessageValidator =
new RoundChangeMessageValidator( new RoundChangeMessageValidator(
messageValidatorFactory, new RoundChangePayloadValidator(
validators, messageValidatorFactory,
IbftHelpers.calculateRequiredValidatorQuorum( validators,
IbftHelpers.calculateRequiredValidatorQuorum(validators.size())), IbftHelpers.calculateRequiredValidatorQuorum(
2); IbftHelpers.calculateRequiredValidatorQuorum(validators.size())),
2));
manager = new RoundChangeManager(2, roundChangeMessageValidator); manager = new RoundChangeManager(2, roundChangeMessageValidator);
} }

@ -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());
}
}

@ -27,7 +27,7 @@ import tech.pegasys.pantheon.consensus.ibft.payload.MessageFactory;
import tech.pegasys.pantheon.consensus.ibft.payload.NewRoundPayload; import tech.pegasys.pantheon.consensus.ibft.payload.NewRoundPayload;
import tech.pegasys.pantheon.consensus.ibft.payload.RoundChangeCertificate; import tech.pegasys.pantheon.consensus.ibft.payload.RoundChangeCertificate;
import tech.pegasys.pantheon.consensus.ibft.statemachine.TerminatedRoundArtefacts; 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.crypto.SECP256K1.KeyPair;
import tech.pegasys.pantheon.ethereum.core.Address; import tech.pegasys.pantheon.ethereum.core.Address;
import tech.pegasys.pantheon.ethereum.core.Block; import tech.pegasys.pantheon.ethereum.core.Block;
@ -53,7 +53,7 @@ public class NewRoundSignedDataValidatorTest {
private final long chainHeight = 2; private final long chainHeight = 2;
private final ConsensusRoundIdentifier roundIdentifier = private final ConsensusRoundIdentifier roundIdentifier =
new ConsensusRoundIdentifier(chainHeight, 4); new ConsensusRoundIdentifier(chainHeight, 4);
private NewRoundMessageValidator validator; private NewRoundPayloadValidator validator;
private final ProposerSelector proposerSelector = mock(ProposerSelector.class); private final ProposerSelector proposerSelector = mock(ProposerSelector.class);
private final MessageValidatorForHeightFactory validatorFactory = private final MessageValidatorForHeightFactory validatorFactory =
@ -83,7 +83,7 @@ public class NewRoundSignedDataValidatorTest {
when(signedDataValidator.validatePrepareMessage(any())).thenReturn(true); when(signedDataValidator.validatePrepareMessage(any())).thenReturn(true);
validator = validator =
new NewRoundMessageValidator( new NewRoundPayloadValidator(
validators, proposerSelector, validatorFactory, 1, chainHeight); validators, proposerSelector, validatorFactory, 1, chainHeight);
} }

@ -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());
}
}

@ -26,7 +26,7 @@ import tech.pegasys.pantheon.consensus.ibft.messagewrappers.RoundChange;
import tech.pegasys.pantheon.consensus.ibft.payload.MessageFactory; import tech.pegasys.pantheon.consensus.ibft.payload.MessageFactory;
import tech.pegasys.pantheon.consensus.ibft.payload.PreparedCertificate; import tech.pegasys.pantheon.consensus.ibft.payload.PreparedCertificate;
import tech.pegasys.pantheon.consensus.ibft.statemachine.TerminatedRoundArtefacts; 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.crypto.SECP256K1.KeyPair;
import tech.pegasys.pantheon.ethereum.core.Address; import tech.pegasys.pantheon.ethereum.core.Address;
import tech.pegasys.pantheon.ethereum.core.Block; import tech.pegasys.pantheon.ethereum.core.Block;
@ -62,8 +62,8 @@ public class RoundChangeSignedDataValidatorTest {
private final MessageValidatorForHeightFactory validatorFactory = private final MessageValidatorForHeightFactory validatorFactory =
mock(MessageValidatorForHeightFactory.class); mock(MessageValidatorForHeightFactory.class);
private final RoundChangeMessageValidator validator = private final RoundChangePayloadValidator validator =
new RoundChangeMessageValidator(validatorFactory, validators, 1, chainHeight); new RoundChangePayloadValidator(validatorFactory, validators, 1, chainHeight);
@Before @Before
public void setup() { public void setup() {
@ -74,7 +74,7 @@ public class RoundChangeSignedDataValidatorTest {
when(validatorFactory.createAt(any())).thenReturn(basicValidator); when(validatorFactory.createAt(any())).thenReturn(basicValidator);
// By default, have all basic messages being valid thus any failures are attributed to logic // 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.addSignedProposalPayload(any())).thenReturn(true);
when(basicValidator.validatePrepareMessage(any())).thenReturn(true); when(basicValidator.validatePrepareMessage(any())).thenReturn(true);
} }
@ -83,7 +83,7 @@ public class RoundChangeSignedDataValidatorTest {
public void roundChangeSentByNonValidatorFails() { public void roundChangeSentByNonValidatorFails() {
final RoundChange msg = final RoundChange msg =
nonValidatorMessageFactory.createSignedRoundChangePayload(targetRound, Optional.empty()); nonValidatorMessageFactory.createSignedRoundChangePayload(targetRound, Optional.empty());
assertThat(validator.validateMessage(msg.getSignedPayload())).isFalse(); assertThat(validator.validateRoundChange(msg.getSignedPayload())).isFalse();
} }
@Test @Test
@ -91,7 +91,7 @@ public class RoundChangeSignedDataValidatorTest {
final RoundChange msg = final RoundChange msg =
proposerMessageFactory.createSignedRoundChangePayload(targetRound, Optional.empty()); proposerMessageFactory.createSignedRoundChangePayload(targetRound, Optional.empty());
assertThat(validator.validateMessage(msg.getSignedPayload())).isTrue(); assertThat(validator.validateRoundChange(msg.getSignedPayload())).isTrue();
} }
@Test @Test
@ -110,7 +110,7 @@ public class RoundChangeSignedDataValidatorTest {
when(basicValidator.addSignedProposalPayload(any())).thenReturn(false); when(basicValidator.addSignedProposalPayload(any())).thenReturn(false);
assertThat(validator.validateMessage(msg.getSignedPayload())).isFalse(); assertThat(validator.validateRoundChange(msg.getSignedPayload())).isFalse();
verify(validatorFactory, times(1)) verify(validatorFactory, times(1))
.createAt(prepareCertificate.getProposalPayload().getPayload().getRoundIdentifier()); .createAt(prepareCertificate.getProposalPayload().getPayload().getRoundIdentifier());
verify(basicValidator, times(1)) verify(basicValidator, times(1))
@ -131,7 +131,7 @@ public class RoundChangeSignedDataValidatorTest {
targetRound, Optional.of(terminatedRoundArtefacts)); targetRound, Optional.of(terminatedRoundArtefacts));
when(basicValidator.addSignedProposalPayload(any())).thenReturn(true); when(basicValidator.addSignedProposalPayload(any())).thenReturn(true);
assertThat(validator.validateMessage(msg.getSignedPayload())).isFalse(); assertThat(validator.validateRoundChange(msg.getSignedPayload())).isFalse();
} }
@Test @Test
@ -150,7 +150,7 @@ public class RoundChangeSignedDataValidatorTest {
proposerMessageFactory.createSignedRoundChangePayload( proposerMessageFactory.createSignedRoundChangePayload(
targetRound, Optional.of(terminatedRoundArtefacts)); 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, times(1)).validatePrepareMessage(prepareMsg.getSignedPayload());
verify(basicValidator, never()).validateCommmitMessage(any()); verify(basicValidator, never()).validateCommmitMessage(any());
@ -165,7 +165,7 @@ public class RoundChangeSignedDataValidatorTest {
proposerMessageFactory.createSignedRoundChangePayload( proposerMessageFactory.createSignedRoundChangePayload(
latterRoundIdentifier, Optional.empty()); latterRoundIdentifier, Optional.empty());
assertThat(validator.validateMessage(msg.getSignedPayload())).isFalse(); assertThat(validator.validateRoundChange(msg.getSignedPayload())).isFalse();
verify(basicValidator, never()).validatePrepareMessage(any()); verify(basicValidator, never()).validatePrepareMessage(any());
} }
@ -186,7 +186,7 @@ public class RoundChangeSignedDataValidatorTest {
proposerMessageFactory.createSignedRoundChangePayload( proposerMessageFactory.createSignedRoundChangePayload(
targetRound, Optional.of(terminatedRoundArtefacts)); targetRound, Optional.of(terminatedRoundArtefacts));
assertThat(validator.validateMessage(msg.getSignedPayload())).isFalse(); assertThat(validator.validateRoundChange(msg.getSignedPayload())).isFalse();
verify(validatorFactory, never()).createAt(any()); verify(validatorFactory, never()).createAt(any());
verify(basicValidator, never()).validatePrepareMessage(prepareMsg.getSignedPayload()); verify(basicValidator, never()).validatePrepareMessage(prepareMsg.getSignedPayload());
verify(basicValidator, never()).validateCommmitMessage(any()); verify(basicValidator, never()).validateCommmitMessage(any());
@ -212,7 +212,7 @@ public class RoundChangeSignedDataValidatorTest {
.thenReturn(true); .thenReturn(true);
when(basicValidator.validatePrepareMessage(prepareMsg.getSignedPayload())).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)) verify(validatorFactory, times(1))
.createAt(prepareCertificate.getProposalPayload().getPayload().getRoundIdentifier()); .createAt(prepareCertificate.getProposalPayload().getPayload().getRoundIdentifier());
verify(basicValidator, times(1)) verify(basicValidator, times(1))

Loading…
Cancel
Save