mirror of https://github.com/hyperledger/besu
Spilt Ibft MessageValidator into components (#752)
Moving to IBFT2.1 requires that validation be conducted on the signeddata aspects of a message separately from the 'piggybacked' block. Move Validators to using Messages Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>pull/2/head
parent
562251638e
commit
4a7b9823ad
@ -0,0 +1,212 @@ |
||||
/* |
||||
* Copyright 2018 ConsenSys AG. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
*/ |
||||
package tech.pegasys.pantheon.consensus.ibft.validation; |
||||
|
||||
import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; |
||||
import tech.pegasys.pantheon.consensus.ibft.IbftContext; |
||||
import tech.pegasys.pantheon.consensus.ibft.IbftExtraData; |
||||
import tech.pegasys.pantheon.consensus.ibft.payload.CommitPayload; |
||||
import tech.pegasys.pantheon.consensus.ibft.payload.Payload; |
||||
import tech.pegasys.pantheon.consensus.ibft.payload.PreparePayload; |
||||
import tech.pegasys.pantheon.consensus.ibft.payload.ProposalPayload; |
||||
import tech.pegasys.pantheon.consensus.ibft.payload.SignedData; |
||||
import tech.pegasys.pantheon.ethereum.BlockValidator; |
||||
import tech.pegasys.pantheon.ethereum.BlockValidator.BlockProcessingOutputs; |
||||
import tech.pegasys.pantheon.ethereum.ProtocolContext; |
||||
import tech.pegasys.pantheon.ethereum.core.Address; |
||||
import tech.pegasys.pantheon.ethereum.core.Block; |
||||
import tech.pegasys.pantheon.ethereum.core.Hash; |
||||
import tech.pegasys.pantheon.ethereum.core.Util; |
||||
import tech.pegasys.pantheon.ethereum.mainnet.HeaderValidationMode; |
||||
|
||||
import java.util.Collection; |
||||
import java.util.Optional; |
||||
|
||||
import org.apache.logging.log4j.LogManager; |
||||
import org.apache.logging.log4j.Logger; |
||||
|
||||
public class SignedDataValidator { |
||||
|
||||
private static final Logger LOG = LogManager.getLogger(); |
||||
|
||||
private final Collection<Address> validators; |
||||
private final Address expectedProposer; |
||||
private final ConsensusRoundIdentifier roundIdentifier; |
||||
private final BlockValidator<IbftContext> blockValidator; |
||||
private final ProtocolContext<IbftContext> protocolContext; |
||||
|
||||
private Optional<SignedData<ProposalPayload>> proposal = Optional.empty(); |
||||
|
||||
public SignedDataValidator( |
||||
final Collection<Address> validators, |
||||
final Address expectedProposer, |
||||
final ConsensusRoundIdentifier roundIdentifier, |
||||
final BlockValidator<IbftContext> blockValidator, |
||||
final ProtocolContext<IbftContext> protocolContext) { |
||||
this.validators = validators; |
||||
this.expectedProposer = expectedProposer; |
||||
this.roundIdentifier = roundIdentifier; |
||||
this.blockValidator = blockValidator; |
||||
this.protocolContext = protocolContext; |
||||
} |
||||
|
||||
public boolean addSignedProposalPayload(final SignedData<ProposalPayload> msg) { |
||||
|
||||
if (proposal.isPresent()) { |
||||
return handleSubsequentProposal(proposal.get(), msg); |
||||
} |
||||
|
||||
if (!validateSignedProposalPayload(msg)) { |
||||
return false; |
||||
} |
||||
|
||||
if (!validateBlockMatchesProposalRound(msg.getPayload())) { |
||||
return false; |
||||
} |
||||
|
||||
proposal = Optional.of(msg); |
||||
return true; |
||||
} |
||||
|
||||
private boolean validateSignedProposalPayload(final SignedData<ProposalPayload> msg) { |
||||
|
||||
if (!msg.getPayload().getRoundIdentifier().equals(roundIdentifier)) { |
||||
LOG.info("Invalid Proposal message, does not match current round."); |
||||
return false; |
||||
} |
||||
|
||||
if (!msg.getAuthor().equals(expectedProposer)) { |
||||
LOG.info( |
||||
"Invalid Proposal message, was not created by the proposer expected for the " |
||||
+ "associated round."); |
||||
return false; |
||||
} |
||||
|
||||
final Block proposedBlock = msg.getPayload().getBlock(); |
||||
|
||||
final Optional<BlockProcessingOutputs> validationResult = |
||||
blockValidator.validateAndProcessBlock( |
||||
protocolContext, proposedBlock, HeaderValidationMode.LIGHT, HeaderValidationMode.FULL); |
||||
|
||||
if (!validationResult.isPresent()) { |
||||
LOG.info("Invalid Proposal message, block did not pass validation."); |
||||
return false; |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
private boolean handleSubsequentProposal( |
||||
final SignedData<ProposalPayload> existingMsg, final SignedData<ProposalPayload> newMsg) { |
||||
if (!existingMsg.getAuthor().equals(newMsg.getAuthor())) { |
||||
LOG.debug("Received subsequent invalid Proposal message; sender differs from original."); |
||||
return false; |
||||
} |
||||
|
||||
final ProposalPayload existingData = existingMsg.getPayload(); |
||||
final ProposalPayload newData = newMsg.getPayload(); |
||||
|
||||
if (!proposalMessagesAreIdentical(existingData, newData)) { |
||||
LOG.debug("Received subsequent invalid Proposal message; content differs from original."); |
||||
return false; |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
public boolean validatePrepareMessage(final SignedData<PreparePayload> msg) { |
||||
final String msgType = "Prepare"; |
||||
|
||||
if (!isMessageForCurrentRoundFromValidatorAndProposalAvailable(msg, msgType)) { |
||||
return false; |
||||
} |
||||
|
||||
if (msg.getAuthor().equals(expectedProposer)) { |
||||
LOG.info("Illegal Prepare message; was sent by the round's proposer."); |
||||
return false; |
||||
} |
||||
|
||||
return validateDigestMatchesProposal(msg.getPayload().getDigest(), msgType); |
||||
} |
||||
|
||||
public boolean validateCommmitMessage(final SignedData<CommitPayload> msg) { |
||||
final String msgType = "Commit"; |
||||
|
||||
if (!isMessageForCurrentRoundFromValidatorAndProposalAvailable(msg, msgType)) { |
||||
return false; |
||||
} |
||||
|
||||
final Block proposedBlock = proposal.get().getPayload().getBlock(); |
||||
final Address commitSealCreator = |
||||
Util.signatureToAddress(msg.getPayload().getCommitSeal(), proposedBlock.getHash()); |
||||
|
||||
if (!commitSealCreator.equals(msg.getAuthor())) { |
||||
LOG.info("Invalid Commit message. Seal was not created by the message transmitter."); |
||||
return false; |
||||
} |
||||
|
||||
return validateDigestMatchesProposal(msg.getPayload().getDigest(), msgType); |
||||
} |
||||
|
||||
private boolean isMessageForCurrentRoundFromValidatorAndProposalAvailable( |
||||
final SignedData<? extends Payload> msg, final String msgType) { |
||||
|
||||
if (!msg.getPayload().getRoundIdentifier().equals(roundIdentifier)) { |
||||
LOG.info("Invalid {} message, does not match current round.", msgType); |
||||
return false; |
||||
} |
||||
|
||||
if (!validators.contains(msg.getAuthor())) { |
||||
LOG.info( |
||||
"Invalid {} message, was not transmitted by a validator for the " + "associated round.", |
||||
msgType); |
||||
return false; |
||||
} |
||||
|
||||
if (!proposal.isPresent()) { |
||||
LOG.info( |
||||
"Unable to validate {} message. No Proposal exists against which to validate " |
||||
+ "block digest.", |
||||
msgType); |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
private boolean validateDigestMatchesProposal(final Hash digest, final String msgType) { |
||||
final Block proposedBlock = proposal.get().getPayload().getBlock(); |
||||
if (!digest.equals(proposedBlock.getHash())) { |
||||
LOG.info( |
||||
"Illegal {} message, digest does not match the block in the Prepare Message.", msgType); |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
private boolean proposalMessagesAreIdentical( |
||||
final ProposalPayload right, final ProposalPayload left) { |
||||
return right.getBlock().getHash().equals(left.getBlock().getHash()) |
||||
&& right.getRoundIdentifier().equals(left.getRoundIdentifier()); |
||||
} |
||||
|
||||
private boolean validateBlockMatchesProposalRound(final ProposalPayload payload) { |
||||
final ConsensusRoundIdentifier msgRound = payload.getRoundIdentifier(); |
||||
final IbftExtraData extraData = |
||||
IbftExtraData.decode(payload.getBlock().getHeader().getExtraData()); |
||||
if (extraData.getRound() != msgRound.getRoundNumber()) { |
||||
LOG.info("Invalid Proposal message, round number in block does not match that in message."); |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
} |
@ -0,0 +1,266 @@ |
||||
/* |
||||
* Copyright 2018 ConsenSys AG. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with |
||||
* the License. You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on |
||||
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the |
||||
* specific language governing permissions and limitations under the License. |
||||
*/ |
||||
package tech.pegasys.pantheon.consensus.ibft.validation; |
||||
|
||||
import static java.util.Optional.empty; |
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.ArgumentMatchers.any; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.when; |
||||
|
||||
import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; |
||||
import tech.pegasys.pantheon.consensus.ibft.IbftContext; |
||||
import tech.pegasys.pantheon.consensus.ibft.IbftExtraData; |
||||
import tech.pegasys.pantheon.consensus.ibft.messagewrappers.Commit; |
||||
import tech.pegasys.pantheon.consensus.ibft.messagewrappers.Prepare; |
||||
import tech.pegasys.pantheon.consensus.ibft.messagewrappers.Proposal; |
||||
import tech.pegasys.pantheon.consensus.ibft.payload.MessageFactory; |
||||
import tech.pegasys.pantheon.crypto.SECP256K1; |
||||
import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; |
||||
import tech.pegasys.pantheon.ethereum.BlockValidator; |
||||
import tech.pegasys.pantheon.ethereum.BlockValidator.BlockProcessingOutputs; |
||||
import tech.pegasys.pantheon.ethereum.ProtocolContext; |
||||
import tech.pegasys.pantheon.ethereum.chain.MutableBlockchain; |
||||
import tech.pegasys.pantheon.ethereum.core.Address; |
||||
import tech.pegasys.pantheon.ethereum.core.Block; |
||||
import tech.pegasys.pantheon.ethereum.core.BlockHeader; |
||||
import tech.pegasys.pantheon.ethereum.core.Hash; |
||||
import tech.pegasys.pantheon.ethereum.core.Util; |
||||
import tech.pegasys.pantheon.ethereum.worldstate.WorldStateArchive; |
||||
import tech.pegasys.pantheon.util.bytes.BytesValue; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.Optional; |
||||
|
||||
import com.google.common.collect.Lists; |
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import org.junit.runner.RunWith; |
||||
import org.mockito.Mock; |
||||
import org.mockito.junit.MockitoJUnitRunner; |
||||
|
||||
@RunWith(MockitoJUnitRunner.class) |
||||
public class SignedDataValidatorTest { |
||||
|
||||
private final KeyPair proposerKey = KeyPair.generate(); |
||||
private final KeyPair validatorKey = KeyPair.generate(); |
||||
private final KeyPair nonValidatorKey = KeyPair.generate(); |
||||
private final MessageFactory proposerMessageFactory = new MessageFactory(proposerKey); |
||||
private final MessageFactory validatorMessageFactory = new MessageFactory(validatorKey); |
||||
private final MessageFactory nonValidatorMessageFactory = new MessageFactory(nonValidatorKey); |
||||
|
||||
private final List<Address> validators = Lists.newArrayList(); |
||||
|
||||
@Mock private BlockValidator<IbftContext> blockValidator; |
||||
private final BlockHeader parentHeader = mock(BlockHeader.class); |
||||
private final ConsensusRoundIdentifier roundIdentifier = new ConsensusRoundIdentifier(2, 0); |
||||
private SignedDataValidator validator; |
||||
|
||||
private final Block block = mock(Block.class); |
||||
|
||||
@Before |
||||
public void setup() { |
||||
validators.add(Util.publicKeyToAddress(proposerKey.getPublicKey())); |
||||
validators.add(Util.publicKeyToAddress(validatorKey.getPublicKey())); |
||||
|
||||
final ProtocolContext<IbftContext> protocolContext = |
||||
new ProtocolContext<>( |
||||
mock(MutableBlockchain.class), mock(WorldStateArchive.class), mock(IbftContext.class)); |
||||
|
||||
validator = |
||||
new SignedDataValidator( |
||||
validators, |
||||
Util.publicKeyToAddress(proposerKey.getPublicKey()), |
||||
roundIdentifier, |
||||
blockValidator, |
||||
protocolContext); |
||||
|
||||
when(block.getHash()).thenReturn(Hash.fromHexStringLenient("1")); |
||||
when(blockValidator.validateAndProcessBlock(any(), any(), any(), any())) |
||||
.thenReturn(Optional.of(new BlockProcessingOutputs(null, null))); |
||||
insertRoundToBlockHeader(0); |
||||
} |
||||
|
||||
private void insertRoundToBlockHeader(final int round) { |
||||
final IbftExtraData extraData = |
||||
new IbftExtraData( |
||||
BytesValue.wrap(new byte[32]), Collections.emptyList(), empty(), round, validators); |
||||
final BlockHeader header = mock(BlockHeader.class); |
||||
when(header.getExtraData()).thenReturn(extraData.encode()); |
||||
when(block.getHeader()).thenReturn(header); |
||||
} |
||||
|
||||
@Test |
||||
public void receivingAPrepareMessageBeforeProposalFails() { |
||||
final Prepare prepareMsg = |
||||
proposerMessageFactory.createSignedPreparePayload(roundIdentifier, Hash.ZERO); |
||||
|
||||
assertThat(validator.validatePrepareMessage(prepareMsg.getSignedPayload())).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void receivingACommitMessageBeforeProposalFails() { |
||||
final Commit commitMsg = |
||||
proposerMessageFactory.createSignedCommitPayload( |
||||
roundIdentifier, Hash.ZERO, SECP256K1.sign(block.getHash(), proposerKey)); |
||||
|
||||
assertThat(validator.validateCommmitMessage(commitMsg.getSignedPayload())).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void receivingProposalMessageFromNonProposerFails() { |
||||
final Proposal proposalMsg = |
||||
validatorMessageFactory.createSignedProposalPayload(roundIdentifier, mock(Block.class)); |
||||
|
||||
assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void receivingProposalMessageWithIllegalBlockFails() { |
||||
when(blockValidator.validateAndProcessBlock(any(), any(), any(), any())).thenReturn(empty()); |
||||
final Proposal proposalMsg = |
||||
proposerMessageFactory.createSignedProposalPayload(roundIdentifier, mock(Block.class)); |
||||
|
||||
assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void receivingPrepareFromProposerFails() { |
||||
final Proposal proposalMsg = |
||||
proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); |
||||
|
||||
final Prepare prepareMsg = |
||||
proposerMessageFactory.createSignedPreparePayload(roundIdentifier, block.getHash()); |
||||
|
||||
assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); |
||||
assertThat(validator.validatePrepareMessage(prepareMsg.getSignedPayload())).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void receivingPrepareFromNonValidatorFails() { |
||||
final Proposal proposalMsg = |
||||
proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); |
||||
|
||||
final Prepare prepareMsg = |
||||
nonValidatorMessageFactory.createSignedPreparePayload(roundIdentifier, block.getHash()); |
||||
|
||||
assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); |
||||
assertThat(validator.validatePrepareMessage(prepareMsg.getSignedPayload())).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void receivingMessagesWithDifferentRoundIdFromProposalFails() { |
||||
final Proposal proposalMsg = |
||||
proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); |
||||
|
||||
final ConsensusRoundIdentifier invalidRoundIdentifier = |
||||
new ConsensusRoundIdentifier( |
||||
roundIdentifier.getSequenceNumber(), roundIdentifier.getRoundNumber() + 1); |
||||
final Prepare prepareMsg = |
||||
validatorMessageFactory.createSignedPreparePayload(invalidRoundIdentifier, block.getHash()); |
||||
final Commit commitMsg = |
||||
validatorMessageFactory.createSignedCommitPayload( |
||||
invalidRoundIdentifier, block.getHash(), SECP256K1.sign(block.getHash(), proposerKey)); |
||||
|
||||
assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); |
||||
assertThat(validator.validatePrepareMessage(prepareMsg.getSignedPayload())).isFalse(); |
||||
assertThat(validator.validateCommmitMessage(commitMsg.getSignedPayload())).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void receivingPrepareNonProposerValidatorWithCorrectRoundIsSuccessful() { |
||||
final Proposal proposalMsg = |
||||
proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); |
||||
final Prepare prepareMsg = |
||||
validatorMessageFactory.createSignedPreparePayload(roundIdentifier, block.getHash()); |
||||
|
||||
assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); |
||||
assertThat(validator.validatePrepareMessage(prepareMsg.getSignedPayload())).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void receivingACommitMessageWithAnInvalidCommitSealFails() { |
||||
final Proposal proposalMsg = |
||||
proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); |
||||
|
||||
final Commit commitMsg = |
||||
proposerMessageFactory.createSignedCommitPayload( |
||||
roundIdentifier, block.getHash(), SECP256K1.sign(block.getHash(), nonValidatorKey)); |
||||
|
||||
assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); |
||||
assertThat(validator.validateCommmitMessage(commitMsg.getSignedPayload())).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void commitMessageContainingValidSealFromValidatorIsSuccessful() { |
||||
final Proposal proposalMsg = |
||||
proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); |
||||
|
||||
final Commit proposerCommitMsg = |
||||
proposerMessageFactory.createSignedCommitPayload( |
||||
roundIdentifier, block.getHash(), SECP256K1.sign(block.getHash(), proposerKey)); |
||||
|
||||
final Commit validatorCommitMsg = |
||||
validatorMessageFactory.createSignedCommitPayload( |
||||
roundIdentifier, block.getHash(), SECP256K1.sign(block.getHash(), validatorKey)); |
||||
|
||||
assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); |
||||
assertThat(validator.validateCommmitMessage(proposerCommitMsg.getSignedPayload())).isTrue(); |
||||
assertThat(validator.validateCommmitMessage(validatorCommitMsg.getSignedPayload())).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void subsequentProposalHasDifferentSenderFails() { |
||||
final Proposal proposalMsg = |
||||
proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); |
||||
assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); |
||||
|
||||
final Proposal secondProposalMsg = |
||||
validatorMessageFactory.createSignedProposalPayload(roundIdentifier, block); |
||||
assertThat(validator.addSignedProposalPayload(secondProposalMsg.getSignedPayload())).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void subsequentProposalHasDifferentContentFails() { |
||||
final Proposal proposalMsg = |
||||
proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); |
||||
assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); |
||||
|
||||
final ConsensusRoundIdentifier newRoundIdentifier = new ConsensusRoundIdentifier(3, 0); |
||||
final Proposal secondProposalMsg = |
||||
proposerMessageFactory.createSignedProposalPayload(newRoundIdentifier, block); |
||||
assertThat(validator.addSignedProposalPayload(secondProposalMsg.getSignedPayload())).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void subsequentProposalHasIdenticalSenderAndContentIsSuccessful() { |
||||
final Proposal proposalMsg = |
||||
proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); |
||||
assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isTrue(); |
||||
|
||||
final Proposal secondProposalMsg = |
||||
proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); |
||||
assertThat(validator.addSignedProposalPayload(secondProposalMsg.getSignedPayload())).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void blockRoundMisMatchWithMessageRoundFails() { |
||||
insertRoundToBlockHeader(roundIdentifier.getRoundNumber() + 1); |
||||
|
||||
final Proposal proposalMsg = |
||||
proposerMessageFactory.createSignedProposalPayload(roundIdentifier, block); |
||||
|
||||
assertThat(validator.addSignedProposalPayload(proposalMsg.getSignedPayload())).isFalse(); |
||||
} |
||||
} |
Loading…
Reference in new issue