mirror of https://github.com/hyperledger/besu
Created message validators for NewRound and RoundChange (#760)
Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>pull/2/head
parent
8ca6181001
commit
49865063ee
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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()); |
||||||
|
} |
||||||
|
} |
@ -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()); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue