Round change validation (#315)

RoundChangeValidator has been added which is responsible for
validating the content of a RoundChange message by using the
underlying MessageValidator capabilities.
tmohay 6 years ago committed by GitHub
parent e60b784b7a
commit 24b8d730e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 19
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/ConsensusRoundIdentifier.java
  2. 2
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidator.java
  3. 140
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeMessageValidator.java
  4. 4
      consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/MessageValidatorTest.java
  5. 224
      consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/validation/RoundChangeMessageValidatorTest.java

@ -15,6 +15,8 @@ package tech.pegasys.pantheon.consensus.ibft;
import tech.pegasys.pantheon.ethereum.rlp.RLPInput;
import tech.pegasys.pantheon.ethereum.rlp.RLPOutput;
import java.util.Objects;
import com.google.common.base.MoreObjects;
/**
@ -89,4 +91,21 @@ public class ConsensusRoundIdentifier implements Comparable<ConsensusRoundIdenti
.add("Round", round)
.toString();
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ConsensusRoundIdentifier that = (ConsensusRoundIdentifier) o;
return sequence == that.sequence && round == that.round;
}
@Override
public int hashCode() {
return Objects.hash(sequence, round);
}
}

@ -199,6 +199,6 @@ public class MessageValidator {
private boolean preprepareMessagesAreIdentical(
final IbftUnsignedPrePrepareMessageData right, final IbftUnsignedPrePrepareMessageData left) {
return right.getBlock().getHash().equals(left.getBlock().getHash())
&& (right.getRoundIdentifier().compareTo(left.getRoundIdentifier()) == 0);
&& right.getRoundIdentifier().equals(left.getRoundIdentifier());
}
}

@ -0,0 +1,140 @@
/*
* 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.ibftmessagedata.IbftPreparedCertificate;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.IbftSignedMessageData;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.IbftUnsignedPrePrepareMessageData;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.IbftUnsignedPrepareMessageData;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.IbftUnsignedRoundChangeMessageData;
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 RoundChangeMessageValidator {
private static final Logger LOG = LogManager.getLogger();
private final MessageValidatorFactory messageValidatorFactory;
private final Collection<Address> validators;
private final long minimumPrepareMessages;
private final ConsensusRoundIdentifier currentRound;
public RoundChangeMessageValidator(
final MessageValidatorFactory messageValidatorFactory,
final Collection<Address> validators,
final long minimumPrepareMessages,
final ConsensusRoundIdentifier currentRound) {
this.messageValidatorFactory = messageValidatorFactory;
this.validators = validators;
this.minimumPrepareMessages = minimumPrepareMessages;
this.currentRound = currentRound;
}
public boolean validateMessage(
final IbftSignedMessageData<IbftUnsignedRoundChangeMessageData> msg) {
if (!validators.contains(msg.getSender())) {
LOG.info(
"Invalid RoundChange message, was not transmitted by a validator for the associated"
+ " round.");
return false;
}
final ConsensusRoundIdentifier roundChangeTarget =
msg.getUnsignedMessageData().getRoundChangeIdentifier();
if (roundChangeTarget.getSequenceNumber() != currentRound.getSequenceNumber()) {
LOG.info("Invalid RoundChange message, not valid for local chain height.");
return false;
}
if (msg.getUnsignedMessageData().getPreparedCertificate().isPresent()) {
final IbftPreparedCertificate certificate =
msg.getUnsignedMessageData().getPreparedCertificate().get();
return validatePrepareCertificate(certificate, roundChangeTarget);
}
return true;
}
private boolean validatePrepareCertificate(
final IbftPreparedCertificate certificate, final ConsensusRoundIdentifier roundChangeTarget) {
final IbftSignedMessageData<IbftUnsignedPrePrepareMessageData> preprepareMessage =
certificate.getIbftPrePrepareMessage();
final ConsensusRoundIdentifier prepareCertRound =
preprepareMessage.getUnsignedMessageData().getRoundIdentifier();
if (!validatePreprepareCertificateRound(prepareCertRound, roundChangeTarget)) {
return false;
}
final MessageValidator messageValidator = messageValidatorFactory.createAt(prepareCertRound);
return validateConsistencyOfPrepareCertificateMessages(certificate, messageValidator);
}
private boolean validateConsistencyOfPrepareCertificateMessages(
final IbftPreparedCertificate certificate, final MessageValidator messageValidator) {
if (!messageValidator.addPreprepareMessage(certificate.getIbftPrePrepareMessage())) {
LOG.info("Invalid RoundChange message, embedded Preprepare message failed validation.");
return false;
}
if (certificate.getIbftPrepareMessages().size() < minimumPrepareMessages) {
LOG.info(
"Invalid RoundChange message, insufficient prepare messages exist to justify "
+ "prepare certificate.");
return false;
}
for (final IbftSignedMessageData<IbftUnsignedPrepareMessageData> prepareMsg :
certificate.getIbftPrepareMessages()) {
if (!messageValidator.validatePrepareMessage(prepareMsg)) {
LOG.info("Invalid RoundChange message, embedded Prepare message failed validation.");
return false;
}
}
return true;
}
private boolean validatePreprepareCertificateRound(
final ConsensusRoundIdentifier prepareCertRound,
final ConsensusRoundIdentifier roundChangeTarget) {
if (prepareCertRound.getSequenceNumber() != roundChangeTarget.getSequenceNumber()) {
LOG.info("Invalid RoundChange message, PreprepareCertificate is not for local chain height.");
return false;
}
if (prepareCertRound.getRoundNumber() >= roundChangeTarget.getRoundNumber()) {
LOG.info(
"Invalid RoundChange message, PreprepareCertificate is newer than RoundChange target.");
return false;
}
return true;
}
@FunctionalInterface
public interface MessageValidatorFactory {
MessageValidator createAt(final ConsensusRoundIdentifier roundIdentifier);
}
}

@ -56,10 +56,10 @@ public class MessageValidatorTest {
private final IbftMessageFactory nonValidatorMessageFactory =
new IbftMessageFactory(nonValidatorKey);
private List<Address> validators = Lists.newArrayList();
private final List<Address> validators = Lists.newArrayList();
@Mock private BlockHeaderValidator<IbftContext> headerValidator;
private BlockHeader parentHeader = mock(BlockHeader.class);
private final BlockHeader parentHeader = mock(BlockHeader.class);
private final ConsensusRoundIdentifier roundIdentifier = new ConsensusRoundIdentifier(2, 0);
private MessageValidator validator;

@ -0,0 +1,224 @@
/*
* 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.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.IbftMessageFactory;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.IbftPreparedCertificate;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.IbftSignedMessageData;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.IbftUnsignedPrepareMessageData;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.IbftUnsignedRoundChangeMessageData;
import tech.pegasys.pantheon.consensus.ibft.validation.RoundChangeMessageValidator.MessageValidatorFactory;
import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair;
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 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;
public class RoundChangeMessageValidatorTest {
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 final ConsensusRoundIdentifier currentRound = new ConsensusRoundIdentifier(2, 3);
private final ConsensusRoundIdentifier targetRound = new ConsensusRoundIdentifier(2, 4);
private final Block block = mock(Block.class);
private final MessageValidator basicValidator = mock(MessageValidator.class);
private final List<Address> validators = Lists.newArrayList();
private final MessageValidatorFactory validatorFactory = mock(MessageValidatorFactory.class);
private final RoundChangeMessageValidator validator =
new RoundChangeMessageValidator(validatorFactory, validators, 1, currentRound);
@Before
public void setup() {
validators.add(Util.publicKeyToAddress(proposerKey.getPublicKey()));
validators.add(Util.publicKeyToAddress(validatorKey.getPublicKey()));
when(block.getHash()).thenReturn(Hash.fromHexStringLenient("1"));
when(validatorFactory.createAt(any())).thenReturn(basicValidator);
// By default, have all basic messages being valid thus any failures are attributed to logic
// in the RoundChangeMessageValidator
when(basicValidator.addPreprepareMessage(any())).thenReturn(true);
when(basicValidator.validatePrepareMessage(any())).thenReturn(true);
}
@Test
public void roundChangeSentByNonValidatorFails() {
final IbftSignedMessageData<IbftUnsignedRoundChangeMessageData> msg =
nonValidatorMessageFactory.createIbftSignedRoundChangeMessageData(
targetRound, Optional.empty());
assertThat(validator.validateMessage(msg)).isFalse();
}
@Test
public void roundChangeContainingNoCertificateIsSuccessful() {
final IbftSignedMessageData<IbftUnsignedRoundChangeMessageData> msg =
proposerMessageFactory.createIbftSignedRoundChangeMessageData(
targetRound, Optional.empty());
assertThat(validator.validateMessage(msg)).isTrue();
}
@Test
public void roundChangeContainingInvalidPreprepareFails() {
final IbftPreparedCertificate prepareCertificate =
new IbftPreparedCertificate(
proposerMessageFactory.createIbftSignedPrePrepareMessageData(currentRound, block),
Collections.emptyList());
final IbftSignedMessageData<IbftUnsignedRoundChangeMessageData> msg =
proposerMessageFactory.createIbftSignedRoundChangeMessageData(
targetRound, Optional.of(prepareCertificate));
when(basicValidator.addPreprepareMessage(any())).thenReturn(false);
assertThat(validator.validateMessage(msg)).isFalse();
verify(validatorFactory, times(1))
.createAt(
prepareCertificate
.getIbftPrePrepareMessage()
.getUnsignedMessageData()
.getRoundIdentifier());
verify(basicValidator, times(1))
.addPreprepareMessage(prepareCertificate.getIbftPrePrepareMessage());
verify(basicValidator, never()).validatePrepareMessage(any());
verify(basicValidator, never()).validateCommmitMessage(any());
}
@Test
public void roundChangeContainingValidPreprepareButNoPrepareMessagesFails() {
final IbftPreparedCertificate prepareCertificate =
new IbftPreparedCertificate(
proposerMessageFactory.createIbftSignedPrePrepareMessageData(currentRound, block),
Collections.emptyList());
final IbftSignedMessageData<IbftUnsignedRoundChangeMessageData> msg =
proposerMessageFactory.createIbftSignedRoundChangeMessageData(
targetRound, Optional.of(prepareCertificate));
when(basicValidator.addPreprepareMessage(any())).thenReturn(true);
assertThat(validator.validateMessage(msg)).isFalse();
}
@Test
public void roundChangeInvalidPrepareMessageFromProposerFails() {
final IbftSignedMessageData<IbftUnsignedPrepareMessageData> prepareMsg =
validatorMessageFactory.createIbftSignedPrepareMessageData(currentRound, block.getHash());
final IbftPreparedCertificate prepareCertificate =
new IbftPreparedCertificate(
proposerMessageFactory.createIbftSignedPrePrepareMessageData(currentRound, block),
Lists.newArrayList(prepareMsg));
when(basicValidator.addPreprepareMessage(any())).thenReturn(true);
when(basicValidator.validatePrepareMessage(any())).thenReturn(false);
final IbftSignedMessageData<IbftUnsignedRoundChangeMessageData> msg =
proposerMessageFactory.createIbftSignedRoundChangeMessageData(
targetRound, Optional.of(prepareCertificate));
assertThat(validator.validateMessage(msg)).isFalse();
verify(basicValidator, times(1)).validatePrepareMessage(prepareMsg);
verify(basicValidator, never()).validateCommmitMessage(any());
}
@Test
public void roundChangeWithDifferentSequenceNumberFails() {
final ConsensusRoundIdentifier latterRoundIdentifier =
new ConsensusRoundIdentifier(currentRound.getSequenceNumber() + 1, 1);
final IbftSignedMessageData<IbftUnsignedRoundChangeMessageData> msg =
proposerMessageFactory.createIbftSignedRoundChangeMessageData(
latterRoundIdentifier, Optional.empty());
assertThat(validator.validateMessage(msg)).isFalse();
verify(basicValidator, never()).validatePrepareMessage(any());
}
@Test
public void roundChangeWithPreprepareFromARoundAheadOfRoundChangeTargetFails() {
final ConsensusRoundIdentifier futureRound =
new ConsensusRoundIdentifier(
currentRound.getSequenceNumber(), currentRound.getRoundNumber() + 2);
final IbftSignedMessageData<IbftUnsignedPrepareMessageData> prepareMsg =
validatorMessageFactory.createIbftSignedPrepareMessageData(futureRound, block.getHash());
final IbftPreparedCertificate prepareCertificate =
new IbftPreparedCertificate(
proposerMessageFactory.createIbftSignedPrePrepareMessageData(futureRound, block),
Lists.newArrayList(prepareMsg));
final IbftSignedMessageData<IbftUnsignedRoundChangeMessageData> msg =
proposerMessageFactory.createIbftSignedRoundChangeMessageData(
targetRound, Optional.of(prepareCertificate));
assertThat(validator.validateMessage(msg)).isFalse();
verify(validatorFactory, never()).createAt(any());
verify(basicValidator, never()).validatePrepareMessage(prepareMsg);
verify(basicValidator, never()).validateCommmitMessage(any());
}
@Test
public void roudnChangeWithPastPreprepareForCurrentHeightIsSuccessful() {
final IbftSignedMessageData<IbftUnsignedPrepareMessageData> prepareMsg =
validatorMessageFactory.createIbftSignedPrepareMessageData(currentRound, block.getHash());
final IbftPreparedCertificate prepareCertificate =
new IbftPreparedCertificate(
proposerMessageFactory.createIbftSignedPrePrepareMessageData(currentRound, block),
Lists.newArrayList(prepareMsg));
final IbftSignedMessageData<IbftUnsignedRoundChangeMessageData> msg =
proposerMessageFactory.createIbftSignedRoundChangeMessageData(
targetRound, Optional.of(prepareCertificate));
when(basicValidator.addPreprepareMessage(prepareCertificate.getIbftPrePrepareMessage()))
.thenReturn(true);
when(basicValidator.validatePrepareMessage(prepareMsg)).thenReturn(true);
assertThat(validator.validateMessage(msg)).isTrue();
verify(validatorFactory, times(1))
.createAt(
prepareCertificate
.getIbftPrePrepareMessage()
.getUnsignedMessageData()
.getRoundIdentifier());
verify(basicValidator, times(1))
.addPreprepareMessage(prepareCertificate.getIbftPrePrepareMessage());
verify(basicValidator, times(1)).validatePrepareMessage(prepareMsg);
}
}
Loading…
Cancel
Save