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);
+ }
+}