From a8b52e19f553f077e891e96f215ca2763c706b97 Mon Sep 17 00:00:00 2001 From: tmohay <37158202+rain-on@users.noreply.github.com> Date: Wed, 28 Nov 2018 13:35:27 +1100 Subject: [PATCH] Basic Ibft message validators (#314) --- .../consensus/ibft/IbftBlockHashing.java | 5 + .../IbftUnsignedPrePrepareMessageData.java | 3 +- .../ibft/validation/MessageValidator.java | 204 +++++++++++++ .../ibft/validation/MessageValidatorTest.java | 274 ++++++++++++++++++ 4 files changed, 485 insertions(+), 1 deletion(-) create mode 100644 consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidator.java create mode 100644 consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidatorTest.java diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockHashing.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockHashing.java index 69f9541a61..4e8d880c1b 100644 --- a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockHashing.java +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockHashing.java @@ -41,6 +41,11 @@ public class IbftBlockHashing { return Hash.hash(serializeHeader(header, ibftExtraData::encodeWithoutCommitSeals)); } + public static Hash calculateDataHashForCommittedSeal(final BlockHeader header) { + final IbftExtraData ibftExtraData = IbftExtraData.decode(header.getExtraData()); + return Hash.hash(serializeHeader(header, ibftExtraData::encodeWithoutCommitSeals)); + } + /** * Constructs a hash of the block header, but omits the committerSeals and sets round number to 0 * (as these change on each of the potentially circulated blocks at the current chain height). diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/ibftmessagedata/IbftUnsignedPrePrepareMessageData.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/ibftmessagedata/IbftUnsignedPrePrepareMessageData.java index 4378b46880..11c2541132 100644 --- a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/ibftmessagedata/IbftUnsignedPrePrepareMessageData.java +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/ibftmessagedata/IbftUnsignedPrePrepareMessageData.java @@ -36,7 +36,8 @@ public class IbftUnsignedPrePrepareMessageData extends AbstractIbftUnsignedInRou rlpInput.enterList(); final ConsensusRoundIdentifier roundIdentifier = ConsensusRoundIdentifier.readFrom(rlpInput); - final Block block = Block.readFrom(rlpInput, IbftBlockHashing::calculateHashOfIbftBlockOnChain); + final Block block = + Block.readFrom(rlpInput, IbftBlockHashing::calculateDataHashForCommittedSeal); rlpInput.leaveList(); return new IbftUnsignedPrePrepareMessageData(roundIdentifier, block); diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidator.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidator.java new file mode 100644 index 0000000000..1a643429a0 --- /dev/null +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidator.java @@ -0,0 +1,204 @@ +/* + * 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.ethereum.mainnet.HeaderValidationMode.FULL; + +import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier; +import tech.pegasys.pantheon.consensus.ibft.IbftContext; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.AbstractIbftUnsignedInRoundMessageData; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.IbftSignedMessageData; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.IbftUnsignedCommitMessageData; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.IbftUnsignedPrePrepareMessageData; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.IbftUnsignedPrepareMessageData; +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.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.Util; +import tech.pegasys.pantheon.ethereum.mainnet.BlockHeaderValidator; + +import java.util.Collection; +import java.util.Optional; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class MessageValidator { + + private static final Logger LOG = LogManager.getLogger(); + + private final Collection
validators; + private final Address expectedProposer; + private final ConsensusRoundIdentifier roundIdentifier; + private final BlockHeaderValidator headerValidator; + private final ProtocolContext protocolContext; + private final BlockHeader parentHeader; + + private Optional> preprepareMessage = + Optional.empty(); + + public MessageValidator( + final Collection
validators, + final Address expectedProposer, + final ConsensusRoundIdentifier roundIdentifier, + final BlockHeaderValidator headerValidator, + final ProtocolContext protocolContext, + final BlockHeader parentHeader) { + this.validators = validators; + this.expectedProposer = expectedProposer; + this.roundIdentifier = roundIdentifier; + this.headerValidator = headerValidator; + this.protocolContext = protocolContext; + this.parentHeader = parentHeader; + } + + public boolean addPreprepareMessage( + final IbftSignedMessageData msg) { + + if (preprepareMessage.isPresent()) { + return handleSubsequentPreprepareMessage(preprepareMessage.get(), msg); + } + + if (!validatePreprepareMessage(msg)) { + return false; + } + + preprepareMessage = Optional.of(msg); + return true; + } + + private boolean validatePreprepareMessage( + final IbftSignedMessageData msg) { + + if (!msg.getUnsignedMessageData().getRoundIdentifier().equals(roundIdentifier)) { + LOG.info("Invalid Preprepare message, does not match current round."); + return false; + } + + if (!msg.getSender().equals(expectedProposer)) { + LOG.info( + "Invalid Preprepare message, was not created by the proposer expected for the " + + "associated round."); + return false; + } + + final Block proposedBlock = msg.getUnsignedMessageData().getBlock(); + if (!headerValidator.validateHeader( + proposedBlock.getHeader(), parentHeader, protocolContext, FULL)) { + LOG.info("Invalid Prepare message, block did not pass header validation."); + return false; + } + + return true; + } + + private boolean handleSubsequentPreprepareMessage( + final IbftSignedMessageData existingMsg, + final IbftSignedMessageData newMsg) { + if (!existingMsg.getSender().equals(newMsg.getSender())) { + LOG.debug("Received subsequent invalid Preprepare message; sender differs from original."); + return false; + } + + final IbftUnsignedPrePrepareMessageData existingData = existingMsg.getUnsignedMessageData(); + final IbftUnsignedPrePrepareMessageData newData = newMsg.getUnsignedMessageData(); + + if (!preprepareMessagesAreIdentical(existingData, newData)) { + LOG.debug("Received subsequent invalid Preprepare message; content differs from original."); + return false; + } + + return true; + } + + public boolean validatePrepareMessage( + final IbftSignedMessageData msg) { + final String msgType = "Prepare"; + + if (!isMessageForCurrentRoundFromValidatorAndPreprareMessageAvailable(msg, msgType)) { + return false; + } + + if (msg.getSender().equals(expectedProposer)) { + LOG.info("Illegal Prepare message; was sent by the round's proposer."); + return false; + } + + return validateDigestMatchesPreprepareBlock(msg.getUnsignedMessageData().getDigest(), msgType); + } + + public boolean validateCommmitMessage( + final IbftSignedMessageData msg) { + final String msgType = "Commit"; + + if (!isMessageForCurrentRoundFromValidatorAndPreprareMessageAvailable(msg, msgType)) { + return false; + } + + final Block proposedBlock = preprepareMessage.get().getUnsignedMessageData().getBlock(); + final Address commitSealCreator = + Util.signatureToAddress( + msg.getUnsignedMessageData().getCommitSeal(), proposedBlock.getHash()); + + if (!commitSealCreator.equals(msg.getSender())) { + LOG.info("Invalid Commit message. Seal was not created by the message transmitter."); + return false; + } + + return validateDigestMatchesPreprepareBlock(msg.getUnsignedMessageData().getDigest(), msgType); + } + + private boolean isMessageForCurrentRoundFromValidatorAndPreprareMessageAvailable( + final IbftSignedMessageData msg, + final String msgType) { + + if (!msg.getUnsignedMessageData().getRoundIdentifier().equals(roundIdentifier)) { + LOG.info("Invalid {} message, does not match current round.", msgType); + return false; + } + + if (!validators.contains(msg.getSender())) { + LOG.info( + "Invalid {} message, was not transmitted by a validator for the " + "associated round.", + msgType); + return false; + } + + if (!preprepareMessage.isPresent()) { + LOG.info( + "Unable to validate {} message. No Preprepare message exists against " + + "which to validate block digest.", + msgType); + return false; + } + return true; + } + + private boolean validateDigestMatchesPreprepareBlock(final Hash digest, final String msgType) { + final Block proposedBlock = preprepareMessage.get().getUnsignedMessageData().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 preprepareMessagesAreIdentical( + final IbftUnsignedPrePrepareMessageData right, final IbftUnsignedPrePrepareMessageData left) { + return right.getBlock().getHash().equals(left.getBlock().getHash()) + && (right.getRoundIdentifier().compareTo(left.getRoundIdentifier()) == 0); + } +} diff --git a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidatorTest.java b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidatorTest.java new file mode 100644 index 0000000000..f6b7a65f0f --- /dev/null +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidatorTest.java @@ -0,0 +1,274 @@ +/* + * 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 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.ibftmessagedata.IbftMessageFactory; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.IbftSignedMessageData; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.IbftUnsignedCommitMessageData; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.IbftUnsignedPrePrepareMessageData; +import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.IbftUnsignedPrepareMessageData; +import tech.pegasys.pantheon.crypto.SECP256K1; +import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; +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.db.WorldStateArchive; +import tech.pegasys.pantheon.ethereum.mainnet.BlockHeaderValidator; + +import java.util.List; + +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 MessageValidatorTest { + + private final KeyPair proposerKey = KeyPair.generate(); + private final KeyPair validatorKey = KeyPair.generate(); + private final KeyPair nonValidatorKey = KeyPair.generate(); + private final IbftMessageFactory proposerMessageFactory = new IbftMessageFactory(proposerKey); + private final IbftMessageFactory validatorMessageFactory = new IbftMessageFactory(validatorKey); + private final IbftMessageFactory nonValidatorMessageFactory = + new IbftMessageFactory(nonValidatorKey); + + private List
validators = Lists.newArrayList(); + + @Mock private BlockHeaderValidator headerValidator; + private BlockHeader parentHeader = mock(BlockHeader.class); + private final ConsensusRoundIdentifier roundIdentifier = new ConsensusRoundIdentifier(2, 0); + private MessageValidator 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 protocolContext = + new ProtocolContext<>( + mock(MutableBlockchain.class), mock(WorldStateArchive.class), mock(IbftContext.class)); + + validator = + new MessageValidator( + validators, + Util.publicKeyToAddress(proposerKey.getPublicKey()), + roundIdentifier, + headerValidator, + protocolContext, + parentHeader); + + when(block.getHash()).thenReturn(Hash.fromHexStringLenient("1")); + } + + @Test + public void receivingAPrepareMessageBeforePrePrepareFails() { + final IbftSignedMessageData prepareMsg = + proposerMessageFactory.createIbftSignedPrepareMessageData(roundIdentifier, Hash.ZERO); + + assertThat(validator.validatePrepareMessage(prepareMsg)).isFalse(); + } + + @Test + public void receivingACommitMessageBeforePreprepareFails() { + final IbftSignedMessageData commitMsg = + proposerMessageFactory.createIbftSignedCommitMessageData( + roundIdentifier, Hash.ZERO, SECP256K1.sign(Hash.ZERO, proposerKey)); + + assertThat(validator.validateCommmitMessage(commitMsg)).isFalse(); + } + + @Test + public void receivingPreprepareMessageFromNonProposerFails() { + final IbftSignedMessageData preprepareMsg = + validatorMessageFactory.createIbftSignedPrePrepareMessageData( + roundIdentifier, mock(Block.class)); + + assertThat(validator.addPreprepareMessage(preprepareMsg)).isFalse(); + } + + @Test + public void receivingPreprepareMessageWithIllegalBlockFails() { + when(headerValidator.validateHeader(any(), any(), any(), any())).thenReturn(false); + final IbftSignedMessageData preprepareMsg = + proposerMessageFactory.createIbftSignedPrePrepareMessageData( + roundIdentifier, mock(Block.class)); + + assertThat(validator.addPreprepareMessage(preprepareMsg)).isFalse(); + } + + @Test + public void receivingPrepareFromProposerFails() { + when(headerValidator.validateHeader(any(), any(), any(), any())).thenReturn(true); + + final IbftSignedMessageData preprepareMsg = + proposerMessageFactory.createIbftSignedPrePrepareMessageData(roundIdentifier, block); + + final IbftSignedMessageData prepareMsg = + proposerMessageFactory.createIbftSignedPrepareMessageData(roundIdentifier, block.getHash()); + + assertThat(validator.addPreprepareMessage(preprepareMsg)).isTrue(); + assertThat(validator.validatePrepareMessage(prepareMsg)).isFalse(); + } + + @Test + public void receivingPrepareFromNonValidatorFails() { + when(headerValidator.validateHeader(any(), any(), any(), any())).thenReturn(true); + final Block block = mock(Block.class); + final BlockHeader header = mock(BlockHeader.class); + when(header.getHash()).thenReturn(Hash.fromHexStringLenient("1")); // arbitrary hash value. + when(block.getHeader()).thenReturn(header); + + final IbftSignedMessageData preprepareMsg = + proposerMessageFactory.createIbftSignedPrePrepareMessageData(roundIdentifier, block); + + final IbftSignedMessageData prepareMsg = + nonValidatorMessageFactory.createIbftSignedPrepareMessageData( + roundIdentifier, header.getHash()); + + assertThat(validator.addPreprepareMessage(preprepareMsg)).isTrue(); + assertThat(validator.validatePrepareMessage(prepareMsg)).isFalse(); + } + + @Test + public void receivingMessagesWithDifferentRoundIdFromPreprepareFails() { + when(headerValidator.validateHeader(any(), any(), any(), any())).thenReturn(true); + + final IbftSignedMessageData preprepareMsg = + proposerMessageFactory.createIbftSignedPrePrepareMessageData(roundIdentifier, block); + + final ConsensusRoundIdentifier invalidRoundIdentifier = + new ConsensusRoundIdentifier( + roundIdentifier.getSequenceNumber(), roundIdentifier.getRoundNumber() + 1); + final IbftSignedMessageData prepareMsg = + validatorMessageFactory.createIbftSignedPrepareMessageData( + invalidRoundIdentifier, block.getHash()); + final IbftSignedMessageData commitMsg = + validatorMessageFactory.createIbftSignedCommitMessageData( + invalidRoundIdentifier, block.getHash(), SECP256K1.sign(block.getHash(), proposerKey)); + + assertThat(validator.addPreprepareMessage(preprepareMsg)).isTrue(); + assertThat(validator.validatePrepareMessage(prepareMsg)).isFalse(); + assertThat(validator.validateCommmitMessage(commitMsg)).isFalse(); + } + + @Test + public void receivingPrepareNonProposerValidatorWithCorrectRoundIsSuccessful() { + when(headerValidator.validateHeader(any(), any(), any(), any())).thenReturn(true); + final Block block = mock(Block.class); + final BlockHeader header = mock(BlockHeader.class); + final Hash blockHash = Hash.fromHexStringLenient("1"); + when(header.getHash()).thenReturn(blockHash); // arbitrary hash value. + when(block.getHeader()).thenReturn(header); + when(block.getHash()).thenReturn(blockHash); + + final IbftSignedMessageData preprepareMsg = + proposerMessageFactory.createIbftSignedPrePrepareMessageData(roundIdentifier, block); + final IbftSignedMessageData prepareMsg = + validatorMessageFactory.createIbftSignedPrepareMessageData( + roundIdentifier, header.getHash()); + + assertThat(validator.addPreprepareMessage(preprepareMsg)).isTrue(); + assertThat(validator.validatePrepareMessage(prepareMsg)).isTrue(); + } + + @Test + public void receivingACommitMessageWithAnInvalidCommitSealFails() { + when(headerValidator.validateHeader(any(), any(), any(), any())).thenReturn(true); + + final IbftSignedMessageData preprepareMsg = + proposerMessageFactory.createIbftSignedPrePrepareMessageData(roundIdentifier, block); + + final IbftSignedMessageData commitMsg = + proposerMessageFactory.createIbftSignedCommitMessageData( + roundIdentifier, block.getHash(), SECP256K1.sign(block.getHash(), nonValidatorKey)); + + assertThat(validator.addPreprepareMessage(preprepareMsg)).isTrue(); + assertThat(validator.validateCommmitMessage(commitMsg)).isFalse(); + } + + @Test + public void commitMessageContainingValidSealFromValidatorIsSuccessful() { + when(headerValidator.validateHeader(any(), any(), any(), any())).thenReturn(true); + + final IbftSignedMessageData preprepareMsg = + proposerMessageFactory.createIbftSignedPrePrepareMessageData(roundIdentifier, block); + + final IbftSignedMessageData proposerCommitMsg = + proposerMessageFactory.createIbftSignedCommitMessageData( + roundIdentifier, block.getHash(), SECP256K1.sign(block.getHash(), proposerKey)); + + final IbftSignedMessageData validatorCommitMsg = + validatorMessageFactory.createIbftSignedCommitMessageData( + roundIdentifier, block.getHash(), SECP256K1.sign(block.getHash(), validatorKey)); + + assertThat(validator.addPreprepareMessage(preprepareMsg)).isTrue(); + assertThat(validator.validateCommmitMessage(proposerCommitMsg)).isTrue(); + assertThat(validator.validateCommmitMessage(validatorCommitMsg)).isTrue(); + } + + @Test + public void subsequentPreprepareHasDifferentSenderFails() { + when(headerValidator.validateHeader(any(), any(), any(), any())).thenReturn(true); + + final IbftSignedMessageData preprepareMsg = + proposerMessageFactory.createIbftSignedPrePrepareMessageData(roundIdentifier, block); + assertThat(validator.addPreprepareMessage(preprepareMsg)).isTrue(); + + final IbftSignedMessageData secondPreprepareMsg = + validatorMessageFactory.createIbftSignedPrePrepareMessageData(roundIdentifier, block); + assertThat(validator.addPreprepareMessage(secondPreprepareMsg)).isFalse(); + } + + @Test + public void subsequentPreprepareHasDifferentContentFails() { + when(headerValidator.validateHeader(any(), any(), any(), any())).thenReturn(true); + + final IbftSignedMessageData preprepareMsg = + proposerMessageFactory.createIbftSignedPrePrepareMessageData(roundIdentifier, block); + assertThat(validator.addPreprepareMessage(preprepareMsg)).isTrue(); + + final ConsensusRoundIdentifier newRoundIdentifier = new ConsensusRoundIdentifier(3, 0); + final IbftSignedMessageData secondPreprepareMsg = + proposerMessageFactory.createIbftSignedPrePrepareMessageData(newRoundIdentifier, block); + assertThat(validator.addPreprepareMessage(secondPreprepareMsg)).isFalse(); + } + + @Test + public void subsequentPreprepareHasIdenticalSenderAndContentIsSuccessful() { + when(headerValidator.validateHeader(any(), any(), any(), any())).thenReturn(true); + + final IbftSignedMessageData preprepareMsg = + proposerMessageFactory.createIbftSignedPrePrepareMessageData(roundIdentifier, block); + assertThat(validator.addPreprepareMessage(preprepareMsg)).isTrue(); + + final IbftSignedMessageData secondPreprepareMsg = + proposerMessageFactory.createIbftSignedPrePrepareMessageData(roundIdentifier, block); + assertThat(validator.addPreprepareMessage(secondPreprepareMsg)).isTrue(); + } +}