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