Basic Ibft message validators (#314)

tmohay 6 years ago committed by GitHub
parent 14957899d6
commit a8b52e19f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockHashing.java
  2. 3
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/ibftmessagedata/IbftUnsignedPrePrepareMessageData.java
  3. 204
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidator.java
  4. 274
      consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidatorTest.java

@ -41,6 +41,11 @@ public class IbftBlockHashing {
return Hash.hash(serializeHeader(header, ibftExtraData::encodeWithoutCommitSeals)); 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 * 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). * (as these change on each of the potentially circulated blocks at the current chain height).

@ -36,7 +36,8 @@ public class IbftUnsignedPrePrepareMessageData extends AbstractIbftUnsignedInRou
rlpInput.enterList(); rlpInput.enterList();
final ConsensusRoundIdentifier roundIdentifier = ConsensusRoundIdentifier.readFrom(rlpInput); final ConsensusRoundIdentifier roundIdentifier = ConsensusRoundIdentifier.readFrom(rlpInput);
final Block block = Block.readFrom(rlpInput, IbftBlockHashing::calculateHashOfIbftBlockOnChain); final Block block =
Block.readFrom(rlpInput, IbftBlockHashing::calculateDataHashForCommittedSeal);
rlpInput.leaveList(); rlpInput.leaveList();
return new IbftUnsignedPrePrepareMessageData(roundIdentifier, block); return new IbftUnsignedPrePrepareMessageData(roundIdentifier, block);

@ -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<Address> validators;
private final Address expectedProposer;
private final ConsensusRoundIdentifier roundIdentifier;
private final BlockHeaderValidator<IbftContext> headerValidator;
private final ProtocolContext<IbftContext> protocolContext;
private final BlockHeader parentHeader;
private Optional<IbftSignedMessageData<IbftUnsignedPrePrepareMessageData>> preprepareMessage =
Optional.empty();
public MessageValidator(
final Collection<Address> validators,
final Address expectedProposer,
final ConsensusRoundIdentifier roundIdentifier,
final BlockHeaderValidator<IbftContext> headerValidator,
final ProtocolContext<IbftContext> 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<IbftUnsignedPrePrepareMessageData> 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<IbftUnsignedPrePrepareMessageData> 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<IbftUnsignedPrePrepareMessageData> existingMsg,
final IbftSignedMessageData<IbftUnsignedPrePrepareMessageData> 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<IbftUnsignedPrepareMessageData> 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<IbftUnsignedCommitMessageData> 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<? extends AbstractIbftUnsignedInRoundMessageData> 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);
}
}

@ -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<Address> validators = Lists.newArrayList();
@Mock private BlockHeaderValidator<IbftContext> 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<IbftContext> 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<IbftUnsignedPrepareMessageData> prepareMsg =
proposerMessageFactory.createIbftSignedPrepareMessageData(roundIdentifier, Hash.ZERO);
assertThat(validator.validatePrepareMessage(prepareMsg)).isFalse();
}
@Test
public void receivingACommitMessageBeforePreprepareFails() {
final IbftSignedMessageData<IbftUnsignedCommitMessageData> commitMsg =
proposerMessageFactory.createIbftSignedCommitMessageData(
roundIdentifier, Hash.ZERO, SECP256K1.sign(Hash.ZERO, proposerKey));
assertThat(validator.validateCommmitMessage(commitMsg)).isFalse();
}
@Test
public void receivingPreprepareMessageFromNonProposerFails() {
final IbftSignedMessageData<IbftUnsignedPrePrepareMessageData> 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<IbftUnsignedPrePrepareMessageData> 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<IbftUnsignedPrePrepareMessageData> preprepareMsg =
proposerMessageFactory.createIbftSignedPrePrepareMessageData(roundIdentifier, block);
final IbftSignedMessageData<IbftUnsignedPrepareMessageData> 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<IbftUnsignedPrePrepareMessageData> preprepareMsg =
proposerMessageFactory.createIbftSignedPrePrepareMessageData(roundIdentifier, block);
final IbftSignedMessageData<IbftUnsignedPrepareMessageData> 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<IbftUnsignedPrePrepareMessageData> preprepareMsg =
proposerMessageFactory.createIbftSignedPrePrepareMessageData(roundIdentifier, block);
final ConsensusRoundIdentifier invalidRoundIdentifier =
new ConsensusRoundIdentifier(
roundIdentifier.getSequenceNumber(), roundIdentifier.getRoundNumber() + 1);
final IbftSignedMessageData<IbftUnsignedPrepareMessageData> prepareMsg =
validatorMessageFactory.createIbftSignedPrepareMessageData(
invalidRoundIdentifier, block.getHash());
final IbftSignedMessageData<IbftUnsignedCommitMessageData> 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<IbftUnsignedPrePrepareMessageData> preprepareMsg =
proposerMessageFactory.createIbftSignedPrePrepareMessageData(roundIdentifier, block);
final IbftSignedMessageData<IbftUnsignedPrepareMessageData> 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<IbftUnsignedPrePrepareMessageData> preprepareMsg =
proposerMessageFactory.createIbftSignedPrePrepareMessageData(roundIdentifier, block);
final IbftSignedMessageData<IbftUnsignedCommitMessageData> 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<IbftUnsignedPrePrepareMessageData> preprepareMsg =
proposerMessageFactory.createIbftSignedPrePrepareMessageData(roundIdentifier, block);
final IbftSignedMessageData<IbftUnsignedCommitMessageData> proposerCommitMsg =
proposerMessageFactory.createIbftSignedCommitMessageData(
roundIdentifier, block.getHash(), SECP256K1.sign(block.getHash(), proposerKey));
final IbftSignedMessageData<IbftUnsignedCommitMessageData> 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<IbftUnsignedPrePrepareMessageData> preprepareMsg =
proposerMessageFactory.createIbftSignedPrePrepareMessageData(roundIdentifier, block);
assertThat(validator.addPreprepareMessage(preprepareMsg)).isTrue();
final IbftSignedMessageData<IbftUnsignedPrePrepareMessageData> 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<IbftUnsignedPrePrepareMessageData> preprepareMsg =
proposerMessageFactory.createIbftSignedPrePrepareMessageData(roundIdentifier, block);
assertThat(validator.addPreprepareMessage(preprepareMsg)).isTrue();
final ConsensusRoundIdentifier newRoundIdentifier = new ConsensusRoundIdentifier(3, 0);
final IbftSignedMessageData<IbftUnsignedPrePrepareMessageData> 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<IbftUnsignedPrePrepareMessageData> preprepareMsg =
proposerMessageFactory.createIbftSignedPrePrepareMessageData(roundIdentifier, block);
assertThat(validator.addPreprepareMessage(preprepareMsg)).isTrue();
final IbftSignedMessageData<IbftUnsignedPrePrepareMessageData> secondPreprepareMsg =
proposerMessageFactory.createIbftSignedPrePrepareMessageData(roundIdentifier, block);
assertThat(validator.addPreprepareMessage(secondPreprepareMsg)).isTrue();
}
}
Loading…
Cancel
Save