diff --git a/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/ValidatorVote.java b/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/ValidatorVote.java new file mode 100644 index 0000000000..d02ca36e20 --- /dev/null +++ b/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/ValidatorVote.java @@ -0,0 +1,19 @@ +/* + * 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.common; + +public interface ValidatorVote { + boolean isAddVote(); + + boolean isDropVote(); +} diff --git a/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/VoteTally.java b/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/VoteTally.java index 397659d130..d86d57c8d4 100644 --- a/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/VoteTally.java +++ b/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/VoteTally.java @@ -54,15 +54,16 @@ public class VoteTally implements ValidatorProvider { * * @param proposer the address of the validator casting the vote via block proposal * @param subject the validator the vote is about - * @param voteType the type of vote, either add or drop + * @param validatorVote the type of vote, either add or drop */ - public void addVote(final Address proposer, final Address subject, final VoteType voteType) { + public void addVote( + final Address proposer, final Address subject, final ValidatorVote validatorVote) { final Set
addVotesForSubject = addVotesBySubject.computeIfAbsent(subject, target -> new HashSet<>()); final Set
removeVotesForSubject = removeVotesBySubject.computeIfAbsent(subject, target -> new HashSet<>()); - if (voteType == VoteType.ADD) { + if (validatorVote.isAddVote()) { addVotesForSubject.add(proposer); removeVotesForSubject.remove(proposer); } else { diff --git a/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/VoteType.java b/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/VoteType.java index 4571d49359..f7af42855b 100644 --- a/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/VoteType.java +++ b/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/VoteType.java @@ -14,7 +14,7 @@ package tech.pegasys.pantheon.consensus.common; import java.util.Optional; -public enum VoteType { +public enum VoteType implements ValidatorVote { ADD(0xFFFFFFFFFFFFFFFFL), DROP(0x0L); @@ -36,4 +36,14 @@ public enum VoteType { } return Optional.empty(); } + + @Override + public boolean isAddVote() { + return this.equals(ADD); + } + + @Override + public boolean isDropVote() { + return this.equals(DROP); + } } diff --git a/consensus/common/src/test/java/net/consensys/pantheon/consensus/common/VoteTypeTest.java b/consensus/common/src/test/java/net/consensys/pantheon/consensus/common/VoteTypeTest.java new file mode 100644 index 0000000000..89311e36cf --- /dev/null +++ b/consensus/common/src/test/java/net/consensys/pantheon/consensus/common/VoteTypeTest.java @@ -0,0 +1,30 @@ +/* + * 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 net.consensys.pantheon.consensus.common; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +import tech.pegasys.pantheon.consensus.common.VoteType; + +import org.junit.Test; + +public class VoteTypeTest { + @Test + public void testValidatorVoteMethodImplementation() { + assertThat(VoteType.ADD.isAddVote()).isTrue(); + assertThat(VoteType.ADD.isDropVote()).isFalse(); + + assertThat(VoteType.DROP.isAddVote()).isFalse(); + assertThat(VoteType.DROP.isDropVote()).isTrue(); + } +} 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 new file mode 100644 index 0000000000..69f9541a61 --- /dev/null +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockHashing.java @@ -0,0 +1,95 @@ +/* + * 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; + +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.BlockHeaderBuilder; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.Util; +import tech.pegasys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class IbftBlockHashing { + + /** + * Constructs a hash of the block header suitable for signing as a committed seal. The extra data + * in the hash uses an empty list for the committed seals. + * + * @param header The header for which a proposer seal is to be calculated (with or without extra + * data) + * @param ibftExtraData The extra data block which is to be inserted to the header once seal is + * calculated + * @return the hash of the header including the validator and proposer seal in the extra data + */ + public static Hash calculateDataHashForCommittedSeal( + final BlockHeader header, final IbftExtraData ibftExtraData) { + 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). + * + * @param header The header for which a block hash is to be calculated + * @return the hash of the header to be used when referencing the header on the blockchain + */ + public static Hash calculateHashOfIbftBlockOnChain(final BlockHeader header) { + final IbftExtraData ibftExtraData = IbftExtraData.decode(header.getExtraData()); + return Hash.hash( + serializeHeader(header, ibftExtraData::encodeWithoutCommitSealsAndRoundNumber)); + } + + /** + * Recovers the {@link Address} for each validator that contributed a committed seal to the block. + * + * @param header the block header that was signed by the committed seals + * @param ibftExtraData the parsed {@link IbftExtraData} from the header + * @return the addresses of validators that provided a committed seal + */ + public static List
recoverCommitterAddresses( + final BlockHeader header, final IbftExtraData ibftExtraData) { + final Hash committerHash = + IbftBlockHashing.calculateDataHashForCommittedSeal(header, ibftExtraData); + + return ibftExtraData + .getSeals() + .stream() + .map(p -> Util.signatureToAddress(p, committerHash)) + .collect(Collectors.toList()); + } + + private static BytesValue serializeHeader( + final BlockHeader header, final Supplier extraDataSerializer) { + + // create a block header which is a copy of the header supplied as parameter except of the + // extraData field + BlockHeaderBuilder builder = BlockHeaderBuilder.fromHeader(header); + builder.blockHashFunction(IbftBlockHashing::calculateHashOfIbftBlockOnChain); + + // set the extraData field using the supplied extraDataSerializer if the block height is not 0 + if (header.getNumber() == 0) { + builder.extraData(header.getExtraData()); + } else { + builder.extraData(extraDataSerializer.get()); + } + + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + builder.buildBlockHeader().writeTo(out); + return out.encoded(); + } +} diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockHeaderValidationRulesetFactory.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockHeaderValidationRulesetFactory.java new file mode 100644 index 0000000000..ac8d943ecf --- /dev/null +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockHeaderValidationRulesetFactory.java @@ -0,0 +1,76 @@ +/* + * 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; + +import tech.pegasys.pantheon.consensus.ibft.headervalidationrules.IbftCoinbaseValidationRule; +import tech.pegasys.pantheon.consensus.ibft.headervalidationrules.IbftExtraDataValidationRule; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.mainnet.BlockHeaderValidator; +import tech.pegasys.pantheon.ethereum.mainnet.headervalidationrules.AncestryValidationRule; +import tech.pegasys.pantheon.ethereum.mainnet.headervalidationrules.ConstantFieldValidationRule; +import tech.pegasys.pantheon.ethereum.mainnet.headervalidationrules.GasLimitRangeAndDeltaValidationRule; +import tech.pegasys.pantheon.ethereum.mainnet.headervalidationrules.GasUsageValidationRule; +import tech.pegasys.pantheon.ethereum.mainnet.headervalidationrules.TimestampBoundedByFutureParameter; +import tech.pegasys.pantheon.ethereum.mainnet.headervalidationrules.TimestampMoreRecentThanParent; +import tech.pegasys.pantheon.util.uint.UInt256; + +public class IbftBlockHeaderValidationRulesetFactory { + + /** + * Produces a BlockHeaderValidator configured for assessing ibft block headers which are to form + * part of the BlockChain (i.e. not proposed blocks, which do not contain commit seals) + * + * @param secondsBetweenBlocks the minimum number of seconds which must elapse between blocks. + * @return BlockHeaderValidator configured for assessing ibft block headers + */ + public static BlockHeaderValidator ibftBlockHeaderValidator( + final long secondsBetweenBlocks) { + return createValidator(secondsBetweenBlocks, true); + } + + /** + * Produces a BlockHeaderValidator configured for assessing IBFT proposed blocks (i.e. blocks + * which need to be vetted by the validators, and do not contain commit seals). + * + * @param secondsBetweenBlocks the minimum number of seconds which must elapse between blocks. + * @return BlockHeaderValidator configured for assessing ibft block headers + */ + public static BlockHeaderValidator ibftProposedBlockValidator( + final long secondsBetweenBlocks) { + return createValidator(secondsBetweenBlocks, false); + } + + private static BlockHeaderValidator createValidator( + final long secondsBetweenBlocks, final boolean validateCommitSeals) { + return new BlockHeaderValidator.Builder() + .addRule(new AncestryValidationRule()) + .addRule(new GasUsageValidationRule()) + .addRule(new GasLimitRangeAndDeltaValidationRule(5000, 0x7fffffffffffffffL)) + .addRule(new TimestampBoundedByFutureParameter(1)) + .addRule(new TimestampMoreRecentThanParent(secondsBetweenBlocks)) + .addRule( + new ConstantFieldValidationRule<>( + "MixHash", BlockHeader::getMixHash, IbftHelpers.EXPECTED_MIX_HASH)) + .addRule( + new ConstantFieldValidationRule<>( + "OmmersHash", BlockHeader::getOmmersHash, Hash.EMPTY_LIST_HASH)) + .addRule( + new ConstantFieldValidationRule<>( + "Difficulty", BlockHeader::getDifficulty, UInt256.ONE)) + .addRule(new ConstantFieldValidationRule<>("Nonce", BlockHeader::getNonce, 0L)) + .addRule(new IbftExtraDataValidationRule(validateCommitSeals)) + .addRule(new IbftCoinbaseValidationRule()) + .build(); + } +} diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/Ibft2ExtraData.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftExtraData.java similarity index 78% rename from consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/Ibft2ExtraData.java rename to consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftExtraData.java index e80891b75e..ce693158d9 100644 --- a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/Ibft2ExtraData.java +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftExtraData.java @@ -29,7 +29,7 @@ import java.util.Optional; * Represents the data structure stored in the extraData field of the BlockHeader used when * operating under an IBFT 2.0 consensus mechanism. */ -public class Ibft2ExtraData { +public class IbftExtraData { public static final int EXTRA_VANITY_LENGTH = 32; @@ -39,7 +39,7 @@ public class Ibft2ExtraData { private final int round; private final List
validators; - public Ibft2ExtraData( + public IbftExtraData( final BytesValue vanityData, final List seals, final Optional vote, @@ -57,7 +57,7 @@ public class Ibft2ExtraData { this.vote = vote; } - public static Ibft2ExtraData decode(final BytesValue input) { + public static IbftExtraData decode(final BytesValue input) { checkArgument( input.size() > EXTRA_VANITY_LENGTH, "Invalid BytesValue supplied - too short to produce a valid IBFT Extra Data object."); @@ -78,10 +78,29 @@ public class Ibft2ExtraData { final List seals = rlpInput.readList(rlp -> Signature.decode(rlp.readBytesValue())); rlpInput.leaveList(); - return new Ibft2ExtraData(vanityData, seals, vote, round, validators); + return new IbftExtraData(vanityData, seals, vote, round, validators); } public BytesValue encode() { + return encode(EncodingType.ALL); + } + + public BytesValue encodeWithoutCommitSeals() { + return encode(EncodingType.EXCLUDE_COMMIT_SEALS); + } + + public BytesValue encodeWithoutCommitSealsAndRoundNumber() { + return encode(EncodingType.EXCLUDE_COMMIT_SEALS_AND_ROUND_NUMBER); + } + + private enum EncodingType { + ALL, + EXCLUDE_COMMIT_SEALS, + EXCLUDE_COMMIT_SEALS_AND_ROUND_NUMBER + } + + private BytesValue encode(final EncodingType encodingType) { + final BytesValueRLPOutput encoder = new BytesValueRLPOutput(); encoder.startList(); encoder.writeBytesValue(vanityData); @@ -91,8 +110,13 @@ public class Ibft2ExtraData { } else { encoder.writeNull(); } - encoder.writeInt(round); - encoder.writeList(seals, (committer, rlp) -> rlp.writeBytesValue(committer.encodedBytes())); + + if (encodingType != EncodingType.EXCLUDE_COMMIT_SEALS_AND_ROUND_NUMBER) { + encoder.writeInt(round); + if (encodingType != EncodingType.EXCLUDE_COMMIT_SEALS) { + encoder.writeList(seals, (committer, rlp) -> rlp.writeBytesValue(committer.encodedBytes())); + } + } encoder.endList(); return encoder.encoded(); diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftHelpers.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftHelpers.java new file mode 100644 index 0000000000..6b1c132c60 --- /dev/null +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftHelpers.java @@ -0,0 +1,26 @@ +/* + * 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; + +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.Util; + +public class IbftHelpers { + + public static final Hash EXPECTED_MIX_HASH = + Hash.fromHexString("0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365"); + + public static int calculateRequiredValidatorQuorum(final int validatorCount) { + return Util.fastDivCeiling(2 * validatorCount, 3); + } +} diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftVoteType.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftVoteType.java index 13e4b75e13..f048f43b7c 100644 --- a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftVoteType.java +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftVoteType.java @@ -12,11 +12,12 @@ */ package tech.pegasys.pantheon.consensus.ibft; +import tech.pegasys.pantheon.consensus.common.ValidatorVote; import tech.pegasys.pantheon.ethereum.rlp.RLPException; import tech.pegasys.pantheon.ethereum.rlp.RLPInput; import tech.pegasys.pantheon.ethereum.rlp.RLPOutput; -public enum IbftVoteType { +public enum IbftVoteType implements ValidatorVote { ADD((byte) 0xFF), DROP((byte) 0x00); @@ -44,4 +45,14 @@ public enum IbftVoteType { public void writeTo(final RLPOutput rlpOutput) { rlpOutput.writeByte(voteValue); } + + @Override + public boolean isAddVote() { + return this.equals(ADD); + } + + @Override + public boolean isDropVote() { + return this.equals(DROP); + } } diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/headervalidationrules/IbftCoinbaseValidationRule.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/headervalidationrules/IbftCoinbaseValidationRule.java new file mode 100644 index 0000000000..f2ac4d5f3b --- /dev/null +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/headervalidationrules/IbftCoinbaseValidationRule.java @@ -0,0 +1,53 @@ +/* + * 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.headervalidationrules; + +import tech.pegasys.pantheon.consensus.common.ValidatorProvider; +import tech.pegasys.pantheon.consensus.ibft.IbftContext; +import tech.pegasys.pantheon.ethereum.ProtocolContext; +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.mainnet.AttachedBlockHeaderValidationRule; + +import java.util.Collection; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Ensures that the coinbase (which corresponds to the block proposer) is included in the list of + * validators + */ +public class IbftCoinbaseValidationRule implements AttachedBlockHeaderValidationRule { + + private static final Logger LOGGER = LogManager.getLogger(IbftCoinbaseValidationRule.class); + + @Override + public boolean validate( + final BlockHeader header, + final BlockHeader parent, + final ProtocolContext context) { + + final ValidatorProvider validatorProvider = context.getConsensusState().getVoteTally(); + Address proposer = header.getCoinbase(); + + final Collection
storedValidators = validatorProvider.getCurrentValidators(); + + if (!storedValidators.contains(proposer)) { + LOGGER.trace("Block proposer is not a member of the validators."); + return false; + } + + return true; + } +} diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/headervalidationrules/IbftExtraDataValidationRule.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/headervalidationrules/IbftExtraDataValidationRule.java new file mode 100644 index 0000000000..a8ee59317a --- /dev/null +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/headervalidationrules/IbftExtraDataValidationRule.java @@ -0,0 +1,124 @@ +/* + * 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.headervalidationrules; + +import static tech.pegasys.pantheon.consensus.ibft.IbftHelpers.calculateRequiredValidatorQuorum; + +import tech.pegasys.pantheon.consensus.common.ValidatorProvider; +import tech.pegasys.pantheon.consensus.ibft.IbftBlockHashing; +import tech.pegasys.pantheon.consensus.ibft.IbftContext; +import tech.pegasys.pantheon.consensus.ibft.IbftExtraData; +import tech.pegasys.pantheon.ethereum.ProtocolContext; +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.mainnet.AttachedBlockHeaderValidationRule; +import tech.pegasys.pantheon.ethereum.rlp.RLPException; + +import java.util.Collection; +import java.util.List; + +import com.google.common.collect.Iterables; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Ensures the byte content of the extraData field can be deserialised into an appropriate + * structure, and that the structure created contains data matching expectations from preceding + * blocks. + */ +public class IbftExtraDataValidationRule implements AttachedBlockHeaderValidationRule { + + private static final Logger LOGGER = LogManager.getLogger(IbftExtraDataValidationRule.class); + + private final boolean validateCommitSeals; + + public IbftExtraDataValidationRule(final boolean validateCommitSeals) { + this.validateCommitSeals = validateCommitSeals; + } + + @Override + public boolean validate( + final BlockHeader header, + final BlockHeader parent, + final ProtocolContext context) { + return validateExtraData(header, context); + } + + /** + * Responsible for determining the validity of the extra data field. Ensures: + * + *
    + *
  • Bytes in the extra data field can be decoded as per IBFT specification + *
  • Proposer (derived from the proposerSeal) is a member of the validators + *
  • Committers (derived from committerSeals) are all members of the validators + *
+ * + * @param header the block header containing the extraData to be validated. + * @return True if the extraData successfully produces an IstanbulExtraData object, false + * otherwise + */ + private boolean validateExtraData( + final BlockHeader header, final ProtocolContext context) { + try { + final ValidatorProvider validatorProvider = context.getConsensusState().getVoteTally(); + final IbftExtraData ibftExtraData = IbftExtraData.decode(header.getExtraData()); + + final Collection
storedValidators = validatorProvider.getCurrentValidators(); + + if (validateCommitSeals) { + final List
committers = + IbftBlockHashing.recoverCommitterAddresses(header, ibftExtraData); + if (!validateCommitters(committers, storedValidators)) { + return false; + } + } + + if (!Iterables.elementsEqual(ibftExtraData.getValidators(), storedValidators)) { + LOGGER.trace( + "Incorrect validators. Expected {} but got {}.", + storedValidators, + ibftExtraData.getValidators()); + return false; + } + + } catch (final RLPException ex) { + LOGGER.trace("ExtraData field was unable to be deserialised into an IBFT Struct.", ex); + return false; + } catch (final IllegalArgumentException ex) { + LOGGER.trace("Failed to verify extra data", ex); + return false; + } + + return true; + } + + private boolean validateCommitters( + final Collection
committers, final Collection
storedValidators) { + + final int minimumSealsRequired = calculateRequiredValidatorQuorum(storedValidators.size()); + if (committers.size() < minimumSealsRequired) { + LOGGER.trace( + "Insufficient committers to seal block. (Required {}, received {})", + minimumSealsRequired, + committers.size()); + return false; + } + + if (!storedValidators.containsAll(committers)) { + LOGGER.trace("Not all committers are in the locally maintained validator list."); + return false; + } + + return true; + } +} diff --git a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockHashingTest.java b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockHashingTest.java new file mode 100644 index 0000000000..90b7cd90ad --- /dev/null +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockHashingTest.java @@ -0,0 +1,180 @@ +/* + * 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; + +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Java6Assertions.assertThat; + +import tech.pegasys.pantheon.crypto.SECP256K1; +import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; +import tech.pegasys.pantheon.crypto.SECP256K1.PrivateKey; +import tech.pegasys.pantheon.crypto.SECP256K1.Signature; +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.BlockHeaderBuilder; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.LogsBloomFilter; +import tech.pegasys.pantheon.ethereum.core.Util; +import tech.pegasys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import tech.pegasys.pantheon.util.bytes.BytesValue; +import tech.pegasys.pantheon.util.uint.UInt256; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.junit.Test; + +public class IbftBlockHashingTest { + + private static final List COMMITTERS_KEY_PAIRS = committersKeyPairs(); + private static final List
VALIDATORS = + Arrays.asList(Address.fromHexString("1"), Address.fromHexString("2")); + private static final Optional VOTE = Optional.of(Vote.authVote(Address.fromHexString("3"))); + private static final int ROUND = 0x00FEDCBA; + private static final BytesValue VANITY_DATA = vanityBytes(); + + private static final BlockHeader HEADER_TO_BE_HASHED = headerToBeHashed(); + private static final Hash EXPECTED_HEADER_HASH = expectedHeaderHash(); + + @Test + public void testCalculateHashOfIbft2BlockOnChain() { + Hash actualHeaderHash = IbftBlockHashing.calculateHashOfIbftBlockOnChain(HEADER_TO_BE_HASHED); + assertThat(actualHeaderHash).isEqualTo(EXPECTED_HEADER_HASH); + } + + @Test + public void testRecoverCommitterAddresses() { + List
actualCommitterAddresses = + IbftBlockHashing.recoverCommitterAddresses( + HEADER_TO_BE_HASHED, IbftExtraData.decode(HEADER_TO_BE_HASHED.getExtraData())); + + List
expectedCommitterAddresses = + COMMITTERS_KEY_PAIRS + .stream() + .map(keyPair -> Util.publicKeyToAddress(keyPair.getPublicKey())) + .collect(Collectors.toList()); + + assertThat(actualCommitterAddresses).isEqualTo(expectedCommitterAddresses); + } + + @Test + public void testCalculateDataHashForCommittedSeal() { + Hash dataHahsForCommittedSeal = + IbftBlockHashing.calculateDataHashForCommittedSeal( + HEADER_TO_BE_HASHED, IbftExtraData.decode(HEADER_TO_BE_HASHED.getExtraData())); + + BlockHeaderBuilder builder = setHeaderFieldsExceptForExtraData(); + + List commitSeals = + COMMITTERS_KEY_PAIRS + .stream() + .map(keyPair -> SECP256K1.sign(dataHahsForCommittedSeal, keyPair)) + .collect(Collectors.toList()); + + IbftExtraData extraDataWithCommitSeals = + new IbftExtraData(VANITY_DATA, commitSeals, VOTE, ROUND, VALIDATORS); + + builder.extraData(extraDataWithCommitSeals.encode()); + BlockHeader actualHeader = builder.buildBlockHeader(); + assertThat(actualHeader).isEqualTo(HEADER_TO_BE_HASHED); + } + + private static List committersKeyPairs() { + return IntStream.rangeClosed(1, 4) + .mapToObj(i -> KeyPair.create(PrivateKey.create(UInt256.of(i).getBytes()))) + .collect(Collectors.toList()); + } + + private static BlockHeaderBuilder setHeaderFieldsExceptForExtraData() { + final BlockHeaderBuilder builder = new BlockHeaderBuilder(); + builder.parentHash( + Hash.fromHexString("0xa7762d3307dbf2ae6a1ae1b09cf61c7603722b2379731b6b90409cdb8c8288a0")); + builder.ommersHash( + Hash.fromHexString("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347")); + builder.coinbase(Address.fromHexString("0x0000000000000000000000000000000000000000")); + builder.stateRoot( + Hash.fromHexString("0xca07595b82f908822971b7e848398e3395e59ee52565c7ef3603df1a1fa7bc80")); + builder.transactionsRoot( + Hash.fromHexString("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")); + builder.receiptsRoot( + Hash.fromHexString("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")); + builder.logsBloom( + LogsBloomFilter.fromHexString( + "0x000000000000000000000000000000000000000000000000" + + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "0000")); + builder.difficulty(UInt256.ONE); + builder.number(1); + builder.gasLimit(4704588); + builder.gasUsed(0); + builder.timestamp(1530674616); + builder.mixHash( + Hash.fromHexString("0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365")); + builder.nonce(0); + builder.blockHashFunction(IbftBlockHashing::calculateHashOfIbftBlockOnChain); + return builder; + } + + private static BytesValue vanityBytes() { + final byte[] vanity_bytes = new byte[32]; + for (int i = 0; i < vanity_bytes.length; i++) { + vanity_bytes[i] = (byte) i; + } + return BytesValue.wrap(vanity_bytes); + } + + private static BlockHeader headerToBeHashed() { + BlockHeaderBuilder builder = setHeaderFieldsExceptForExtraData(); + + builder.extraData( + new IbftExtraData(VANITY_DATA, emptyList(), VOTE, ROUND, VALIDATORS) + .encodeWithoutCommitSeals()); + + BytesValueRLPOutput rlpForHeaderFroCommittersSigning = new BytesValueRLPOutput(); + builder.buildBlockHeader().writeTo(rlpForHeaderFroCommittersSigning); + + List commitSeals = + COMMITTERS_KEY_PAIRS + .stream() + .map( + keyPair -> + SECP256K1.sign(Hash.hash(rlpForHeaderFroCommittersSigning.encoded()), keyPair)) + .collect(Collectors.toList()); + + IbftExtraData extraDataWithCommitSeals = + new IbftExtraData(VANITY_DATA, commitSeals, VOTE, ROUND, VALIDATORS); + + builder.extraData(extraDataWithCommitSeals.encode()); + return builder.buildBlockHeader(); + } + + private static Hash expectedHeaderHash() { + BlockHeaderBuilder builder = setHeaderFieldsExceptForExtraData(); + + builder.extraData( + new IbftExtraData(VANITY_DATA, emptyList(), VOTE, 0, VALIDATORS) + .encodeWithoutCommitSealsAndRoundNumber()); + + BytesValueRLPOutput rlpOutput = new BytesValueRLPOutput(); + builder.buildBlockHeader().writeTo(rlpOutput); + + return Hash.hash(rlpOutput.encoded()); + } +} diff --git a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockHeaderValidationRulesetFactoryTest.java b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockHeaderValidationRulesetFactoryTest.java new file mode 100644 index 0000000000..99c7c07241 --- /dev/null +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockHeaderValidationRulesetFactoryTest.java @@ -0,0 +1,318 @@ +/* + * 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; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static tech.pegasys.pantheon.consensus.ibft.IbftProtocolContextFixture.protocolContext; + +import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.BlockHeaderTestFixture; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.Util; +import tech.pegasys.pantheon.ethereum.mainnet.BlockHeaderValidator; +import tech.pegasys.pantheon.ethereum.mainnet.HeaderValidationMode; +import tech.pegasys.pantheon.util.bytes.BytesValue; +import tech.pegasys.pantheon.util.uint.UInt256; + +import java.util.List; +import java.util.Optional; + +import org.junit.Test; + +public class IbftBlockHeaderValidationRulesetFactoryTest { + + @Test + public void ibftValidateHeaderPasses() { + final KeyPair proposerKeyPair = KeyPair.generate(); + final Address proposerAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + + final List
validators = singletonList(proposerAddress); + + final BlockHeader parentHeader = + getPresetHeaderBuilder(1, proposerKeyPair, validators, null).buildHeader(); + final BlockHeader blockHeader = + getPresetHeaderBuilder(2, proposerKeyPair, validators, parentHeader).buildHeader(); + + final BlockHeaderValidator validator = + IbftBlockHeaderValidationRulesetFactory.ibftBlockHeaderValidator(5); + + assertThat( + validator.validateHeader( + blockHeader, parentHeader, protocolContext(validators), HeaderValidationMode.FULL)) + .isTrue(); + } + + @Test + public void ibftValidateHeaderFailsOnExtraData() { + final KeyPair proposerKeyPair = KeyPair.generate(); + final Address proposerAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + + final List
validators = singletonList(proposerAddress); + + final BlockHeader parentHeader = + getPresetHeaderBuilder(1, proposerKeyPair, validators, null).buildHeader(); + final BlockHeader blockHeader = + getPresetHeaderBuilder(2, proposerKeyPair, emptyList(), parentHeader).buildHeader(); + + final BlockHeaderValidator validator = + IbftBlockHeaderValidationRulesetFactory.ibftBlockHeaderValidator(5); + + assertThat( + validator.validateHeader( + blockHeader, parentHeader, protocolContext(validators), HeaderValidationMode.FULL)) + .isFalse(); + } + + @Test + public void ibftValidateHeaderFailsOnCoinbaseData() { + final KeyPair proposerKeyPair = KeyPair.generate(); + final Address proposerAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + + final Address nonProposerAddress = Util.publicKeyToAddress(KeyPair.generate().getPublicKey()); + + final List
validators = singletonList(proposerAddress); + + final BlockHeader parentHeader = + getPresetHeaderBuilder(1, proposerKeyPair, validators, null).buildHeader(); + final BlockHeader blockHeader = + getPresetHeaderBuilder(2, proposerKeyPair, validators, parentHeader) + .coinbase(nonProposerAddress) + .buildHeader(); + + final BlockHeaderValidator validator = + IbftBlockHeaderValidationRulesetFactory.ibftBlockHeaderValidator(5); + + assertThat( + validator.validateHeader( + blockHeader, parentHeader, protocolContext(validators), HeaderValidationMode.FULL)) + .isFalse(); + } + + @Test + public void ibftValidateHeaderFailsOnNonce() { + final KeyPair proposerKeyPair = KeyPair.generate(); + final Address proposerAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + + final List
validators = singletonList(proposerAddress); + + final BlockHeader parentHeader = + getPresetHeaderBuilder(1, proposerKeyPair, validators, null).buildHeader(); + final BlockHeader blockHeader = + getPresetHeaderBuilder(2, proposerKeyPair, validators, parentHeader).nonce(3).buildHeader(); + + final BlockHeaderValidator validator = + IbftBlockHeaderValidationRulesetFactory.ibftBlockHeaderValidator(5); + + assertThat( + validator.validateHeader( + blockHeader, parentHeader, protocolContext(validators), HeaderValidationMode.FULL)) + .isFalse(); + } + + @Test + public void ibftValidateHeaderFailsOnTimestamp() { + final KeyPair proposerKeyPair = KeyPair.generate(); + final Address proposerAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + + final List
validators = singletonList(proposerAddress); + + final BlockHeader parentHeader = + getPresetHeaderBuilder(1, proposerKeyPair, validators, null).buildHeader(); + final BlockHeader blockHeader = + getPresetHeaderBuilder(2, proposerKeyPair, validators, parentHeader) + .timestamp(100) + .buildHeader(); + + final BlockHeaderValidator validator = + IbftBlockHeaderValidationRulesetFactory.ibftBlockHeaderValidator(5); + + assertThat( + validator.validateHeader( + blockHeader, parentHeader, protocolContext(validators), HeaderValidationMode.FULL)) + .isFalse(); + } + + @Test + public void ibftValidateHeaderFailsOnMixHash() { + final KeyPair proposerKeyPair = KeyPair.generate(); + final Address proposerAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + + final List
validators = singletonList(proposerAddress); + + final BlockHeader parentHeader = + getPresetHeaderBuilder(1, proposerKeyPair, validators, null).buildHeader(); + final BlockHeader blockHeader = + getPresetHeaderBuilder(2, proposerKeyPair, validators, parentHeader) + .mixHash(Hash.EMPTY_TRIE_HASH) + .buildHeader(); + + final BlockHeaderValidator validator = + IbftBlockHeaderValidationRulesetFactory.ibftBlockHeaderValidator(5); + + assertThat( + validator.validateHeader( + blockHeader, parentHeader, protocolContext(validators), HeaderValidationMode.FULL)) + .isFalse(); + } + + @Test + public void ibftValidateHeaderFailsOnOmmers() { + final KeyPair proposerKeyPair = KeyPair.generate(); + final Address proposerAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + + final List
validators = singletonList(proposerAddress); + + final BlockHeader parentHeader = + getPresetHeaderBuilder(1, proposerKeyPair, validators, null).buildHeader(); + final BlockHeader blockHeader = + getPresetHeaderBuilder(2, proposerKeyPair, validators, parentHeader) + .ommersHash(Hash.EMPTY_TRIE_HASH) + .buildHeader(); + + final BlockHeaderValidator validator = + IbftBlockHeaderValidationRulesetFactory.ibftBlockHeaderValidator(5); + + assertThat( + validator.validateHeader( + blockHeader, parentHeader, protocolContext(validators), HeaderValidationMode.FULL)) + .isFalse(); + } + + @Test + public void ibftValidateHeaderFailsOnDifficulty() { + final KeyPair proposerKeyPair = KeyPair.generate(); + final Address proposerAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + + final List
validators = singletonList(proposerAddress); + + final BlockHeader parentHeader = + getPresetHeaderBuilder(1, proposerKeyPair, validators, null).buildHeader(); + final BlockHeader blockHeader = + getPresetHeaderBuilder(2, proposerKeyPair, validators, parentHeader) + .difficulty(UInt256.of(5)) + .buildHeader(); + + final BlockHeaderValidator validator = + IbftBlockHeaderValidationRulesetFactory.ibftBlockHeaderValidator(5); + + assertThat( + validator.validateHeader( + blockHeader, parentHeader, protocolContext(validators), HeaderValidationMode.FULL)) + .isFalse(); + } + + @Test + public void ibftValidateHeaderFailsOnAncestor() { + final KeyPair proposerKeyPair = KeyPair.generate(); + final Address proposerAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + + final List
validators = singletonList(proposerAddress); + + final BlockHeader parentHeader = + getPresetHeaderBuilder(1, proposerKeyPair, validators, null).buildHeader(); + final BlockHeader blockHeader = + getPresetHeaderBuilder(2, proposerKeyPair, validators, null).buildHeader(); + + final BlockHeaderValidator validator = + IbftBlockHeaderValidationRulesetFactory.ibftBlockHeaderValidator(5); + + assertThat( + validator.validateHeader( + blockHeader, parentHeader, protocolContext(validators), HeaderValidationMode.FULL)) + .isFalse(); + } + + @Test + public void ibftValidateHeaderFailsOnGasUsage() { + final KeyPair proposerKeyPair = KeyPair.generate(); + final Address proposerAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + + final List
validators = singletonList(proposerAddress); + + final BlockHeader parentHeader = + getPresetHeaderBuilder(1, proposerKeyPair, validators, null).buildHeader(); + final BlockHeader blockHeader = + getPresetHeaderBuilder(2, proposerKeyPair, validators, parentHeader) + .gasLimit(5_000) + .gasUsed(6_000) + .buildHeader(); + + final BlockHeaderValidator validator = + IbftBlockHeaderValidationRulesetFactory.ibftBlockHeaderValidator(5); + + assertThat( + validator.validateHeader( + blockHeader, parentHeader, protocolContext(validators), HeaderValidationMode.FULL)) + .isFalse(); + } + + @Test + public void ibftValidateHeaderFailsOnGasLimitRange() { + final KeyPair proposerKeyPair = KeyPair.generate(); + final Address proposerAddress = Util.publicKeyToAddress(proposerKeyPair.getPublicKey()); + + final List
validators = singletonList(proposerAddress); + + final BlockHeader parentHeader = + getPresetHeaderBuilder(1, proposerKeyPair, validators, null).buildHeader(); + final BlockHeader blockHeader = + getPresetHeaderBuilder(2, proposerKeyPair, validators, parentHeader) + .gasLimit(4999) + .buildHeader(); + + final BlockHeaderValidator validator = + IbftBlockHeaderValidationRulesetFactory.ibftBlockHeaderValidator(5); + + assertThat( + validator.validateHeader( + blockHeader, parentHeader, protocolContext(validators), HeaderValidationMode.FULL)) + .isFalse(); + } + + private BlockHeaderTestFixture getPresetHeaderBuilder( + final long number, + final KeyPair proposerKeyPair, + final List
validators, + final BlockHeader parent) { + final BlockHeaderTestFixture builder = new BlockHeaderTestFixture(); + + if (parent != null) { + builder.parentHash(parent.getHash()); + } + builder.number(number); + builder.gasLimit(5000); + builder.timestamp(6000 * number); + builder.mixHash( + Hash.fromHexString("0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365")); + builder.ommersHash(Hash.EMPTY_LIST_HASH); + builder.nonce(0); + builder.difficulty(UInt256.ONE); + builder.coinbase(Util.publicKeyToAddress(proposerKeyPair.getPublicKey())); + + final IbftExtraData ibftExtraData = + IbftExtraDataFixture.createExtraData( + builder.buildHeader(), + BytesValue.wrap(new byte[IbftExtraData.EXTRA_VANITY_LENGTH]), + Optional.of(Vote.authVote(Address.fromHexString("1"))), + validators, + singletonList(proposerKeyPair), + 0xDEADBEEF); + + builder.extraData(ibftExtraData.encode()); + return builder; + } +} diff --git a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/IbftExtraDataFixture.java b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/IbftExtraDataFixture.java new file mode 100644 index 0000000000..9bad5630cc --- /dev/null +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/IbftExtraDataFixture.java @@ -0,0 +1,100 @@ +/* + * 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; + +import static java.util.Collections.emptyList; + +import tech.pegasys.pantheon.crypto.SECP256K1; +import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; +import tech.pegasys.pantheon.crypto.SECP256K1.Signature; +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class IbftExtraDataFixture { + + public static IbftExtraData createExtraData( + final BlockHeader header, + final BytesValue vanityData, + final Optional vote, + final List
validators, + final List committerKeyPairs) { + + return createExtraData(header, vanityData, vote, validators, committerKeyPairs, 0); + } + + public static IbftExtraData createExtraData( + final BlockHeader header, + final BytesValue vanityData, + final Optional vote, + final List
validators, + final List committerKeyPairs, + final int roundNumber) { + + return createExtraData( + header, vanityData, vote, validators, committerKeyPairs, roundNumber, false); + } + + public static IbftExtraData createExtraData( + final BlockHeader header, + final BytesValue vanityData, + final Optional vote, + final List
validators, + final List committerKeyPairs, + final int baseRoundNumber, + final boolean useDifferentRoundNumbersForCommittedSeals) { + + final IbftExtraData ibftExtraDataNoCommittedSeals = + new IbftExtraData(vanityData, emptyList(), vote, baseRoundNumber, validators); + + // if useDifferentRoundNumbersForCommittedSeals is true then each committed seal will be + // calculated for an extraData field with a different round number + List commitSeals = + IntStream.range(0, committerKeyPairs.size()) + .mapToObj( + i -> { + final int round = + useDifferentRoundNumbersForCommittedSeals + ? ibftExtraDataNoCommittedSeals.getRound() + i + : ibftExtraDataNoCommittedSeals.getRound(); + + IbftExtraData extraDataForCommittedSealCalculation = + new IbftExtraData( + ibftExtraDataNoCommittedSeals.getVanityData(), + emptyList(), + ibftExtraDataNoCommittedSeals.getVote(), + round, + ibftExtraDataNoCommittedSeals.getValidators()); + + final Hash headerHashForCommitters = + IbftBlockHashing.calculateDataHashForCommittedSeal( + header, extraDataForCommittedSealCalculation); + + return SECP256K1.sign(headerHashForCommitters, committerKeyPairs.get(i)); + }) + .collect(Collectors.toList()); + + return new IbftExtraData( + ibftExtraDataNoCommittedSeals.getVanityData(), + commitSeals, + ibftExtraDataNoCommittedSeals.getVote(), + ibftExtraDataNoCommittedSeals.getRound(), + ibftExtraDataNoCommittedSeals.getValidators()); + } +} diff --git a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/Ibft2ExtraDataTest.java b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/IbftExtraDataTest.java similarity index 76% rename from consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/Ibft2ExtraDataTest.java rename to consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/IbftExtraDataTest.java index 4fecb8cf93..65de6a6776 100644 --- a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/Ibft2ExtraDataTest.java +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/IbftExtraDataTest.java @@ -31,7 +31,7 @@ import java.util.Random; import com.google.common.collect.Lists; import org.junit.Test; -public class Ibft2ExtraDataTest { +public class IbftExtraDataTest { private final String RAW_HEX_ENCODING_STRING = "f8f1a00102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20ea9400000000000000000000000000000000000" + "00001940000000000000000000000000000000000000002d794000000000000000000000000000000000000000181ff8400fedc" @@ -39,10 +39,10 @@ public class Ibft2ExtraDataTest { + "0000000000000000000000000000000000a00b84100000000000000000000000000000000000000000000000000000000000000" + "0a000000000000000000000000000000000000000000000000000000000000000100"; - private final Ibft2ExtraData DECODED_EXTRA_DATA_FOR_RAW_HEX_ENCODING_STRING = + private final IbftExtraData DECODED_EXTRA_DATA_FOR_RAW_HEX_ENCODING_STRING = getDecodedExtraDataForRawHexEncodingString(); - private static Ibft2ExtraData getDecodedExtraDataForRawHexEncodingString() { + private static IbftExtraData getDecodedExtraDataForRawHexEncodingString() { final List
validators = Arrays.asList(Address.fromHexString("1"), Address.fromHexString("2")); final Optional vote = Optional.of(Vote.authVote(Address.fromHexString("1"))); @@ -56,7 +56,7 @@ public class Ibft2ExtraDataTest { final byte[] vanity_bytes = createNonEmptyVanityData(); final BytesValue vanity_data = BytesValue.wrap(vanity_bytes); - return new Ibft2ExtraData(vanity_data, committerSeals, vote, round, validators); + return new IbftExtraData(vanity_data, committerSeals, vote, round, validators); } @Test @@ -90,7 +90,7 @@ public class Ibft2ExtraDataTest { final BytesValue bufferToInject = encoder.encoded(); - final Ibft2ExtraData extraData = Ibft2ExtraData.decode(bufferToInject); + final IbftExtraData extraData = IbftExtraData.decode(bufferToInject); assertThat(extraData.getVanityData()).isEqualTo(vanity_data); assertThat(extraData.getVote()).isEqualTo(vote); @@ -100,7 +100,7 @@ public class Ibft2ExtraDataTest { } /** - * This test specifically verifies that {@link Ibft2ExtraData#decode(BytesValue)} uses {@link + * This test specifically verifies that {@link IbftExtraData#decode(BytesValue)} uses {@link * RLPInput#readInt()} rather than {@link RLPInput#readIntScalar()} to decode the round number */ @Test @@ -134,8 +134,7 @@ public class Ibft2ExtraDataTest { final BytesValue bufferToInject = encoder.encoded(); - assertThatThrownBy(() -> Ibft2ExtraData.decode(bufferToInject)) - .isInstanceOf(RLPException.class); + assertThatThrownBy(() -> IbftExtraData.decode(bufferToInject)).isInstanceOf(RLPException.class); } @Test @@ -163,7 +162,7 @@ public class Ibft2ExtraDataTest { final BytesValue bufferToInject = encoder.encoded(); - final Ibft2ExtraData extraData = Ibft2ExtraData.decode(bufferToInject); + final IbftExtraData extraData = IbftExtraData.decode(bufferToInject); assertThat(extraData.getVanityData()).isEqualTo(vanity_data); assertThat(extraData.getVote().isPresent()).isEqualTo(false); @@ -183,10 +182,10 @@ public class Ibft2ExtraDataTest { final byte[] vanity_bytes = new byte[32]; final BytesValue vanity_data = BytesValue.wrap(vanity_bytes); - final Ibft2ExtraData expectedExtraData = - new Ibft2ExtraData(vanity_data, committerSeals, vote, round, validators); + IbftExtraData expectedExtraData = + new IbftExtraData(vanity_data, committerSeals, vote, round, validators); - final Ibft2ExtraData actualExtraData = Ibft2ExtraData.decode(expectedExtraData.encode()); + IbftExtraData actualExtraData = IbftExtraData.decode(expectedExtraData.encode()); assertThat(actualExtraData).isEqualToComparingFieldByField(expectedExtraData); } @@ -220,7 +219,7 @@ public class Ibft2ExtraDataTest { final BytesValue bufferToInject = encoder.encoded(); - final Ibft2ExtraData extraData = Ibft2ExtraData.decode(bufferToInject); + final IbftExtraData extraData = IbftExtraData.decode(bufferToInject); assertThat(extraData.getVanityData()).isEqualTo(vanity_data); assertThat(extraData.getVote()).isEqualTo(vote); @@ -240,10 +239,10 @@ public class Ibft2ExtraDataTest { final byte[] vanity_bytes = new byte[32]; final BytesValue vanity_data = BytesValue.wrap(vanity_bytes); - final Ibft2ExtraData expectedExtraData = - new Ibft2ExtraData(vanity_data, committerSeals, vote, round, validators); + IbftExtraData expectedExtraData = + new IbftExtraData(vanity_data, committerSeals, vote, round, validators); - final Ibft2ExtraData actualExtraData = Ibft2ExtraData.decode(expectedExtraData.encode()); + IbftExtraData actualExtraData = IbftExtraData.decode(expectedExtraData.encode()); assertThat(actualExtraData).isEqualToComparingFieldByField(expectedExtraData); } @@ -282,7 +281,7 @@ public class Ibft2ExtraDataTest { final BytesValue bufferToInject = encoder.encoded(); - final Ibft2ExtraData extraData = Ibft2ExtraData.decode(bufferToInject); + final IbftExtraData extraData = IbftExtraData.decode(bufferToInject); assertThat(extraData.getVanityData()).isEqualTo(vanity_data); assertThat(extraData.getVote()).isEqualTo(vote); @@ -306,10 +305,10 @@ public class Ibft2ExtraDataTest { final byte[] vanity_bytes = createNonEmptyVanityData(); final BytesValue vanity_data = BytesValue.wrap(vanity_bytes); - final Ibft2ExtraData expectedExtraData = - new Ibft2ExtraData(vanity_data, committerSeals, vote, round, validators); + IbftExtraData expectedExtraData = + new IbftExtraData(vanity_data, committerSeals, vote, round, validators); - final Ibft2ExtraData actualExtraData = Ibft2ExtraData.decode(expectedExtraData.encode()); + IbftExtraData actualExtraData = IbftExtraData.decode(expectedExtraData.encode()); assertThat(actualExtraData).isEqualToComparingFieldByField(expectedExtraData); } @@ -324,14 +323,87 @@ public class Ibft2ExtraDataTest { @Test public void decodingOfKnownRawHexStringMatchesKnowExtraDataObject() { - final Ibft2ExtraData expectedExtraData = DECODED_EXTRA_DATA_FOR_RAW_HEX_ENCODING_STRING; + final IbftExtraData expectedExtraData = DECODED_EXTRA_DATA_FOR_RAW_HEX_ENCODING_STRING; - final BytesValue rawDecoding = BytesValue.fromHexString(RAW_HEX_ENCODING_STRING); - final Ibft2ExtraData actualExtraData = Ibft2ExtraData.decode(rawDecoding); + BytesValue rawDecoding = BytesValue.fromHexString(RAW_HEX_ENCODING_STRING); + IbftExtraData actualExtraData = IbftExtraData.decode(rawDecoding); assertThat(actualExtraData).isEqualToComparingFieldByField(expectedExtraData); } + @Test + public void testEncodeWithoutCommitSeals() { + final List
validators = + Arrays.asList(Address.fromHexString("1"), Address.fromHexString("2")); + final Optional vote = Optional.of(Vote.authVote(Address.fromHexString("1"))); + final int round = 0x00FEDCBA; + final List committerSeals = + Arrays.asList( + Signature.create(BigInteger.ONE, BigInteger.TEN, (byte) 0), + Signature.create(BigInteger.TEN, BigInteger.ONE, (byte) 0)); + + // Create a byte buffer with no data. + final byte[] vanity_bytes = createNonEmptyVanityData(); + final BytesValue vanity_data = BytesValue.wrap(vanity_bytes); + + final BytesValueRLPOutput encoder = new BytesValueRLPOutput(); + encoder.startList(); + encoder.writeBytesValue(vanity_data); + encoder.writeList(validators, (validator, rlp) -> rlp.writeBytesValue(validator)); + + // encoded vote + encoder.startList(); + encoder.writeBytesValue(vote.get().getRecipient()); + vote.get().getVoteType().writeTo(encoder); + encoder.endList(); + encoder.writeInt(round); + encoder.endList(); + + BytesValue expectedEncoding = encoder.encoded(); + + BytesValue actualEncoding = + new IbftExtraData(vanity_data, committerSeals, vote, round, validators) + .encodeWithoutCommitSeals(); + + assertThat(actualEncoding).isEqualTo(expectedEncoding); + } + + @Test + public void testEncodeWithoutCommitSealsAndRoundNumber() { + final List
validators = + Arrays.asList(Address.fromHexString("1"), Address.fromHexString("2")); + final Optional vote = Optional.of(Vote.authVote(Address.fromHexString("1"))); + final int round = 0x00FEDCBA; + final List committerSeals = + Arrays.asList( + Signature.create(BigInteger.ONE, BigInteger.TEN, (byte) 0), + Signature.create(BigInteger.TEN, BigInteger.ONE, (byte) 0)); + + // Create a byte buffer with no data. + final byte[] vanity_bytes = createNonEmptyVanityData(); + final BytesValue vanity_data = BytesValue.wrap(vanity_bytes); + + final BytesValueRLPOutput encoder = new BytesValueRLPOutput(); + encoder.startList(); + encoder.writeBytesValue(vanity_data); + encoder.writeList(validators, (validator, rlp) -> rlp.writeBytesValue(validator)); + + // encoded vote + encoder.startList(); + encoder.writeBytesValue(vote.get().getRecipient()); + vote.get().getVoteType().writeTo(encoder); + encoder.endList(); + encoder.endList(); + + BytesValue expectedEncoding = encoder.encoded(); + + BytesValue actualEncoding = + new IbftExtraData(vanity_data, committerSeals, vote, round, validators) + .encodeWithoutCommitSealsAndRoundNumber(); + + assertThat(actualEncoding).isEqualTo(expectedEncoding); + } + @Test public void incorrectlyStructuredRlpThrowsException() { final List
validators = Lists.newArrayList(); @@ -362,8 +434,7 @@ public class Ibft2ExtraDataTest { final BytesValue bufferToInject = encoder.encoded(); - assertThatThrownBy(() -> Ibft2ExtraData.decode(bufferToInject)) - .isInstanceOf(RLPException.class); + assertThatThrownBy(() -> IbftExtraData.decode(bufferToInject)).isInstanceOf(RLPException.class); } @Test @@ -400,8 +471,7 @@ public class Ibft2ExtraDataTest { final BytesValue bufferToInject = encoder.encoded(); - assertThatThrownBy(() -> Ibft2ExtraData.decode(bufferToInject)) - .isInstanceOf(RLPException.class); + assertThatThrownBy(() -> IbftExtraData.decode(bufferToInject)).isInstanceOf(RLPException.class); } private static byte[] createNonEmptyVanityData() { @@ -409,7 +479,6 @@ public class Ibft2ExtraDataTest { for (int i = 0; i < vanity_bytes.length; i++) { vanity_bytes[i] = (byte) (i + 1); } - return vanity_bytes; } } diff --git a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/IbftVoteTypeTest.java b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/IbftVoteTypeTest.java new file mode 100644 index 0000000000..78db1c4d8a --- /dev/null +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/IbftVoteTypeTest.java @@ -0,0 +1,29 @@ +/* + * 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; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +import org.junit.Test; + +public class IbftVoteTypeTest { + + @Test + public void testValidatorVoteMethodImplementation() { + assertThat(IbftVoteType.ADD.isAddVote()).isTrue(); + assertThat(IbftVoteType.ADD.isDropVote()).isFalse(); + + assertThat(IbftVoteType.DROP.isAddVote()).isFalse(); + assertThat(IbftVoteType.DROP.isDropVote()).isTrue(); + } +} diff --git a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/headervalidationrules/IbftCoinbaseValidationRuleTest.java b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/headervalidationrules/IbftCoinbaseValidationRuleTest.java new file mode 100644 index 0000000000..7abeb7cc84 --- /dev/null +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/headervalidationrules/IbftCoinbaseValidationRuleTest.java @@ -0,0 +1,104 @@ +/* + * 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.headervalidationrules; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.pantheon.consensus.common.VoteTally; +import tech.pegasys.pantheon.consensus.ibft.IbftContext; +import tech.pegasys.pantheon.consensus.ibft.IbftExtraData; +import tech.pegasys.pantheon.consensus.ibft.IbftExtraDataFixture; +import tech.pegasys.pantheon.consensus.ibft.Vote; +import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; +import tech.pegasys.pantheon.ethereum.ProtocolContext; +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.BlockHeaderTestFixture; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.Util; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.util.List; +import java.util.Optional; + +import com.google.common.collect.Lists; +import org.junit.Test; + +public class IbftCoinbaseValidationRuleTest { + + public static BlockHeader createProposedBlockHeader( + final KeyPair proposerKeyPair, + final List
validators, + final List committerKeyPairs) { + + final BlockHeaderTestFixture builder = new BlockHeaderTestFixture(); + builder.number(1); // must NOT be block 0, as that should not contain seals at all + builder.coinbase(Util.publicKeyToAddress(proposerKeyPair.getPublicKey())); + final BlockHeader header = builder.buildHeader(); + + final IbftExtraData ibftExtraData = + IbftExtraDataFixture.createExtraData( + header, + BytesValue.wrap(new byte[IbftExtraData.EXTRA_VANITY_LENGTH]), + Optional.of(Vote.authVote(Address.fromHexString("1"))), + validators, + committerKeyPairs); + + builder.extraData(ibftExtraData.encode()); + return builder.buildHeader(); + } + + @Test + public void proposerInValidatorListPassesValidation() { + final KeyPair proposerKeyPair = KeyPair.generate(); + final Address proposerAddress = + Address.extract(Hash.hash(proposerKeyPair.getPublicKey().getEncodedBytes())); + + final List
validators = Lists.newArrayList(proposerAddress); + + final List committers = Lists.newArrayList(proposerKeyPair); + + final VoteTally voteTally = new VoteTally(validators); + final ProtocolContext context = + new ProtocolContext<>(null, null, new IbftContext(voteTally, null)); + + final IbftCoinbaseValidationRule coinbaseValidationRule = new IbftCoinbaseValidationRule(); + + BlockHeader header = createProposedBlockHeader(proposerKeyPair, validators, committers); + + assertThat(coinbaseValidationRule.validate(header, null, context)).isTrue(); + } + + @Test + public void proposerNotInValidatorListFailsValidation() { + final KeyPair proposerKeyPair = KeyPair.generate(); + + final KeyPair otherValidatorKeyPair = KeyPair.generate(); + final Address otherValidatorNodeAddress = + Address.extract(Hash.hash(otherValidatorKeyPair.getPublicKey().getEncodedBytes())); + + final List
validators = Lists.newArrayList(otherValidatorNodeAddress); + + final List committers = Lists.newArrayList(otherValidatorKeyPair); + + final VoteTally voteTally = new VoteTally(validators); + final ProtocolContext context = + new ProtocolContext<>(null, null, new IbftContext(voteTally, null)); + + final IbftCoinbaseValidationRule coinbaseValidationRule = new IbftCoinbaseValidationRule(); + + BlockHeader header = createProposedBlockHeader(proposerKeyPair, validators, committers); + + assertThat(coinbaseValidationRule.validate(header, null, context)).isFalse(); + } +} diff --git a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/headervalidationrules/IbftExtraDataValidationRuleTest.java b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/headervalidationrules/IbftExtraDataValidationRuleTest.java new file mode 100644 index 0000000000..75173a1e91 --- /dev/null +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/headervalidationrules/IbftExtraDataValidationRuleTest.java @@ -0,0 +1,253 @@ +/* + * 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.headervalidationrules; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.pantheon.consensus.common.VoteTally; +import tech.pegasys.pantheon.consensus.ibft.IbftContext; +import tech.pegasys.pantheon.consensus.ibft.IbftExtraData; +import tech.pegasys.pantheon.consensus.ibft.IbftExtraDataFixture; +import tech.pegasys.pantheon.consensus.ibft.Vote; +import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; +import tech.pegasys.pantheon.ethereum.ProtocolContext; +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.BlockHeaderTestFixture; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.Util; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import com.google.common.collect.Lists; +import org.junit.Test; + +public class IbftExtraDataValidationRuleTest { + + public static BlockHeader createProposedBlockHeader( + final List
validators, + final List committerKeyPairs, + final boolean useDifferentRoundNumbersForCommittedSeals) { + final int BASE_ROUND_NUMBER = 5; + final BlockHeaderTestFixture builder = new BlockHeaderTestFixture(); + builder.number(1); // must NOT be block 0, as that should not contain seals at all + + final BlockHeader header = builder.buildHeader(); + + final IbftExtraData ibftExtraData = + IbftExtraDataFixture.createExtraData( + header, + BytesValue.wrap(new byte[IbftExtraData.EXTRA_VANITY_LENGTH]), + Optional.of(Vote.authVote(Address.fromHexString("1"))), + validators, + committerKeyPairs, + BASE_ROUND_NUMBER, + useDifferentRoundNumbersForCommittedSeals); + + builder.extraData(ibftExtraData.encode()); + return builder.buildHeader(); + } + + @Test + public void correctlyConstructedHeaderPassesValidation() { + final List committerKeyPairs = + IntStream.range(0, 2).mapToObj(i -> KeyPair.generate()).collect(Collectors.toList()); + + final List
committerAddresses = + committerKeyPairs + .stream() + .map(keyPair -> Util.publicKeyToAddress(keyPair.getPublicKey())) + .sorted() + .collect(Collectors.toList()); + + final VoteTally voteTally = new VoteTally(committerAddresses); + final ProtocolContext context = + new ProtocolContext<>(null, null, new IbftContext(voteTally, null)); + + final IbftExtraDataValidationRule extraDataValidationRule = + new IbftExtraDataValidationRule(true); + + BlockHeader header = createProposedBlockHeader(committerAddresses, committerKeyPairs, false); + + assertThat(extraDataValidationRule.validate(header, null, context)).isTrue(); + } + + @Test + public void insufficientCommitSealsFailsValidation() { + final KeyPair committerKeyPair = KeyPair.generate(); + final Address committerAddress = + Address.extract(Hash.hash(committerKeyPair.getPublicKey().getEncodedBytes())); + + final List
validators = singletonList(committerAddress); + final VoteTally voteTally = new VoteTally(validators); + final ProtocolContext context = + new ProtocolContext<>(null, null, new IbftContext(voteTally, null)); + + final IbftExtraDataValidationRule extraDataValidationRule = + new IbftExtraDataValidationRule(true); + + final BlockHeader header = createProposedBlockHeader(validators, emptyList(), false); + + // Note that no committer seals are in the header's IBFT extra data. + final IbftExtraData headerExtraData = IbftExtraData.decode(header.getExtraData()); + assertThat(headerExtraData.getSeals().size()).isEqualTo(0); + + assertThat(extraDataValidationRule.validate(header, null, context)).isFalse(); + } + + @Test + public void outOfOrderValidatorListFailsValidation() { + final List committerKeyPairs = + IntStream.range(0, 2).mapToObj(i -> KeyPair.generate()).collect(Collectors.toList()); + + final List
committerAddresses = + committerKeyPairs + .stream() + .map(keyPair -> Util.publicKeyToAddress(keyPair.getPublicKey())) + .sorted() + .collect(Collectors.toList()); + + final List
validators = Lists.reverse(committerAddresses); + + final VoteTally voteTally = new VoteTally(validators); + final ProtocolContext context = + new ProtocolContext<>(null, null, new IbftContext(voteTally, null)); + + final IbftExtraDataValidationRule extraDataValidationRule = + new IbftExtraDataValidationRule(true); + + BlockHeader header = createProposedBlockHeader(validators, committerKeyPairs, false); + + assertThat(extraDataValidationRule.validate(header, null, context)).isFalse(); + } + + @Test + public void mismatchingReportedValidatorsVsLocallyStoredListFailsValidation() { + final List committerKeyPairs = + IntStream.range(0, 2).mapToObj(i -> KeyPair.generate()).collect(Collectors.toList()); + + final List
validators = + IntStream.range(0, 2) + .mapToObj(i -> Util.publicKeyToAddress(KeyPair.generate().getPublicKey())) + .collect(Collectors.toList()); + + final VoteTally voteTally = new VoteTally(validators); + final ProtocolContext context = + new ProtocolContext<>(null, null, new IbftContext(voteTally, null)); + + final IbftExtraDataValidationRule extraDataValidationRule = + new IbftExtraDataValidationRule(true); + + BlockHeader header = createProposedBlockHeader(validators, committerKeyPairs, false); + + assertThat(extraDataValidationRule.validate(header, null, context)).isFalse(); + } + + @Test + public void committerNotInValidatorListFailsValidation() { + final KeyPair committerKeyPair = KeyPair.generate(); + final Address committerAddress = Util.publicKeyToAddress(committerKeyPair.getPublicKey()); + + final List
validators = singletonList(committerAddress); + final VoteTally voteTally = new VoteTally(validators); + + // Insert an extraData block with committer seals. + final KeyPair nonValidatorKeyPair = KeyPair.generate(); + + BlockHeader header = + createProposedBlockHeader(validators, singletonList(nonValidatorKeyPair), false); + + final ProtocolContext context = + new ProtocolContext<>(null, null, new IbftContext(voteTally, null)); + final IbftExtraDataValidationRule extraDataValidationRule = + new IbftExtraDataValidationRule(true); + + assertThat(extraDataValidationRule.validate(header, null, context)).isFalse(); + } + + @Test + public void ratioOfCommittersToValidatorsAffectValidation() { + assertThat(subExecution(4, 4, false)).isEqualTo(true); + assertThat(subExecution(4, 3, false)).isEqualTo(true); + assertThat(subExecution(4, 2, false)).isEqualTo(false); + + assertThat(subExecution(5, 4, false)).isEqualTo(true); + assertThat(subExecution(5, 3, false)).isEqualTo(false); + assertThat(subExecution(5, 2, false)).isEqualTo(false); + + assertThat(subExecution(6, 4, false)).isEqualTo(true); + assertThat(subExecution(6, 3, false)).isEqualTo(false); + assertThat(subExecution(6, 2, false)).isEqualTo(false); + + assertThat(subExecution(7, 5, false)).isEqualTo(true); + assertThat(subExecution(7, 4, false)).isEqualTo(false); + + assertThat(subExecution(8, 6, false)).isEqualTo(true); + assertThat(subExecution(8, 5, false)).isEqualTo(false); + assertThat(subExecution(8, 4, false)).isEqualTo(false); + + assertThat(subExecution(9, 6, false)).isEqualTo(true); + assertThat(subExecution(9, 5, false)).isEqualTo(false); + assertThat(subExecution(9, 4, false)).isEqualTo(false); + + assertThat(subExecution(10, 7, false)).isEqualTo(true); + assertThat(subExecution(10, 6, false)).isEqualTo(false); + + assertThat(subExecution(12, 8, false)).isEqualTo(true); + assertThat(subExecution(12, 7, false)).isEqualTo(false); + assertThat(subExecution(12, 6, false)).isEqualTo(false); + } + + @Test + public void validationFailsIfCommittedSealsAreForDifferentRounds() { + assertThat(subExecution(2, 2, true)).isEqualTo(false); + assertThat(subExecution(4, 4, true)).isEqualTo(false); + } + + private boolean subExecution( + final int validatorCount, + final int committerCount, + final boolean useDifferentRoundNumbersForCommittedSeals) { + + final List
validators = Lists.newArrayList(); + final List committerKeys = Lists.newArrayList(); + + for (int i = 0; i < validatorCount; i++) { // need -1 to account for proposer + final KeyPair committerKeyPair = KeyPair.generate(); + committerKeys.add(committerKeyPair); + validators.add(Address.extract(Hash.hash(committerKeyPair.getPublicKey().getEncodedBytes()))); + } + + Collections.sort(validators); + final VoteTally voteTally = new VoteTally(validators); + BlockHeader header = + createProposedBlockHeader( + validators, + committerKeys.subList(0, committerCount), + useDifferentRoundNumbersForCommittedSeals); + + final ProtocolContext context = + new ProtocolContext<>(null, null, new IbftContext(voteTally, null)); + final IbftExtraDataValidationRule extraDataValidationRule = + new IbftExtraDataValidationRule(true); + + return extraDataValidationRule.validate(header, null, context); + } +} diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/core/Util.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/core/Util.java index 6d6ae70353..4ab9578c71 100644 --- a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/core/Util.java +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/core/Util.java @@ -34,4 +34,12 @@ public class Util { public static Address publicKeyToAddress(final PublicKey publicKey) { return Address.extract(Hash.hash(publicKey.getEncodedBytes())); } + + /** + * Implements a fast version of ceiling(numerator/denominator) that does not require using + * floating point math + */ + public static int fastDivCeiling(final int numerator, final int denominator) { + return ((numerator - 1) / denominator) + 1; + } } diff --git a/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/core/UtilTest.java b/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/core/UtilTest.java new file mode 100644 index 0000000000..ca2f6ad056 --- /dev/null +++ b/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/core/UtilTest.java @@ -0,0 +1,33 @@ +/* + * 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.ethereum.core; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +import org.junit.Test; + +public class UtilTest { + @Test + public void testFastDivCeil() { + assertThat(Util.fastDivCeiling(0, 3)).isEqualTo(1); + assertThat(Util.fastDivCeiling(1, 3)).isEqualTo(1); + assertThat(Util.fastDivCeiling(2, 3)).isEqualTo(1); + assertThat(Util.fastDivCeiling(3, 3)).isEqualTo(1); + + assertThat(Util.fastDivCeiling(4, 3)).isEqualTo(2); + assertThat(Util.fastDivCeiling(5, 3)).isEqualTo(2); + assertThat(Util.fastDivCeiling(6, 3)).isEqualTo(2); + + assertThat(Util.fastDivCeiling(7, 3)).isEqualTo(3); + } +}