From c2e98152e0220df7700e931af296d801d143748c Mon Sep 17 00:00:00 2001 From: tmohay <37158202+rain-on@users.noreply.github.com> Date: Fri, 16 Nov 2018 13:19:13 +1100 Subject: [PATCH] Separating Validator voting from block serialisation values (#263) The consensus-mechanism specific block values have been separated from the VoteTallyUpdater such that there is a single updater rather than one per consensus mechanism. This has necessitated the creation of a custom serialiser/ deserialiser for each mechanism instead. This change will ultimatley bleed through to the proposed votes and their insertion to mined blocks. --- .../clique/CliqueVoteTallyUpdater.java | 76 ------- .../clique/CliqueVotingBlockInterface.java | 70 +++++++ .../consensus/clique/VoteTallyCache.java | 5 +- .../CoinbaseHeaderValidationRule.java | 4 +- .../jsonrpc/CliqueJsonRpcMethodsFactory.java | 8 +- .../CliqueVotingBlockInterfaceTest.java | 126 +++++++++++ .../consensus/clique/VoteTallyCacheTest.java | 7 +- consensus/common/build.gradle | 1 + .../pantheon/consensus/common/CastVote.java | 67 ++++++ .../common/ValidatorVotePolarity.java | 32 +++ .../consensus/common/VoteBlockInterface.java | 30 +++ .../pantheon/consensus/common/VoteTally.java | 33 ++- .../consensus/common/VoteTallyUpdater.java} | 30 +-- .../consensus/common/VoteProposerTest.java | 12 +- .../consensus/common/VoteTallyTest.java | 152 +++++++------- .../common/VoteTallyUpdaterTest.java} | 92 +++----- .../consensus/ibft/IbftBlockImporter.java | 1 + .../consensus/ibft/VoteTallyUpdater.java | 35 ---- .../consensus/ibft/IbftBlockImporterTest.java | 1 + .../IbftLegacyVotingBlockInterface.java | 70 +++++++ .../ibftlegacy/IbftProtocolSchedule.java | 3 +- .../IbftLegacyVotingBlockInterfaceTest.java | 127 +++++++++++ .../ibftlegacy/IbftVoteTallyUpdaterTest.java | 198 ------------------ .../consensus/ibftlegacy/TestHelpers.java | 55 +++++ .../controller/CliquePantheonController.java | 7 +- .../controller/IbftPantheonController.java | 6 +- 26 files changed, 738 insertions(+), 510 deletions(-) delete mode 100644 consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/CliqueVoteTallyUpdater.java create mode 100644 consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/CliqueVotingBlockInterface.java create mode 100644 consensus/clique/src/test/java/tech/pegasys/pantheon/consensus/clique/CliqueVotingBlockInterfaceTest.java create mode 100644 consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/CastVote.java create mode 100644 consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/ValidatorVotePolarity.java create mode 100644 consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/VoteBlockInterface.java rename consensus/{ibftlegacy/src/main/java/tech/pegasys/pantheon/consensus/ibftlegacy/IbftVoteTallyUpdater.java => common/src/main/java/tech/pegasys/pantheon/consensus/common/VoteTallyUpdater.java} (69%) rename consensus/{clique/src/test/java/tech/pegasys/pantheon/consensus/clique/CliqueVoteTallyUpdaterTest.java => common/src/test/java/tech/pegasys/pantheon/consensus/common/VoteTallyUpdaterTest.java} (60%) delete mode 100644 consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/VoteTallyUpdater.java create mode 100644 consensus/ibftlegacy/src/main/java/tech/pegasys/pantheon/consensus/ibftlegacy/IbftLegacyVotingBlockInterface.java create mode 100644 consensus/ibftlegacy/src/test/java/tech/pegasys/pantheon/consensus/ibftlegacy/IbftLegacyVotingBlockInterfaceTest.java delete mode 100644 consensus/ibftlegacy/src/test/java/tech/pegasys/pantheon/consensus/ibftlegacy/IbftVoteTallyUpdaterTest.java create mode 100644 consensus/ibftlegacy/src/test/java/tech/pegasys/pantheon/consensus/ibftlegacy/TestHelpers.java diff --git a/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/CliqueVoteTallyUpdater.java b/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/CliqueVoteTallyUpdater.java deleted file mode 100644 index dd7ce79698..0000000000 --- a/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/CliqueVoteTallyUpdater.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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.clique; - -import static org.apache.logging.log4j.LogManager.getLogger; - -import tech.pegasys.pantheon.consensus.common.EpochManager; -import tech.pegasys.pantheon.consensus.common.VoteTally; -import tech.pegasys.pantheon.consensus.common.VoteType; -import tech.pegasys.pantheon.ethereum.chain.Blockchain; -import tech.pegasys.pantheon.ethereum.core.Address; -import tech.pegasys.pantheon.ethereum.core.BlockHeader; -import tech.pegasys.pantheon.util.bytes.BytesValue; - -import java.util.List; - -import org.apache.logging.log4j.Logger; - -public class CliqueVoteTallyUpdater { - - private static final Logger LOG = getLogger(); - public static final Address NO_VOTE_SUBJECT = Address.wrap(BytesValue.wrap(new byte[20])); - - private final EpochManager epochManager; - - public CliqueVoteTallyUpdater(final EpochManager epochManager) { - this.epochManager = epochManager; - } - - public VoteTally buildVoteTallyFromBlockchain(final Blockchain blockchain) { - final long chainHeadBlockNumber = blockchain.getChainHeadBlockNumber(); - final long epochBlockNumber = epochManager.getLastEpochBlock(chainHeadBlockNumber); - LOG.debug("Loading validator voting state starting from block {}", epochBlockNumber); - final BlockHeader epochBlock = blockchain.getBlockHeader(epochBlockNumber).get(); - final List
initialValidators = - CliqueExtraData.decode(epochBlock.getExtraData()).getValidators(); - final VoteTally voteTally = new VoteTally(initialValidators); - for (long blockNumber = epochBlockNumber + 1; - blockNumber <= chainHeadBlockNumber; - blockNumber++) { - updateForBlock(blockchain.getBlockHeader(blockNumber).get(), voteTally); - } - return voteTally; - } - - /** - * Update the vote tally to reflect changes caused by appending a new block to the chain. - * - * @param header the header of the block being added - * @param voteTally the vote tally to update - */ - public void updateForBlock(final BlockHeader header, final VoteTally voteTally) { - final Address candidate = header.getCoinbase(); - if (epochManager.isEpochBlock(header.getNumber())) { - // epoch blocks are not allowed to include a vote - voteTally.discardOutstandingVotes(); - return; - } - - if (!candidate.equals(NO_VOTE_SUBJECT)) { - final CliqueExtraData extraData = CliqueExtraData.decode(header.getExtraData()); - final Address proposer = CliqueBlockHashing.recoverProposerAddress(header, extraData); - voteTally.addVote(proposer, candidate, VoteType.fromNonce(header.getNonce()).get()); - } - } -} diff --git a/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/CliqueVotingBlockInterface.java b/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/CliqueVotingBlockInterface.java new file mode 100644 index 0000000000..5efff4cbc0 --- /dev/null +++ b/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/CliqueVotingBlockInterface.java @@ -0,0 +1,70 @@ +/* + * 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.clique; + +import tech.pegasys.pantheon.consensus.common.CastVote; +import tech.pegasys.pantheon.consensus.common.ValidatorVotePolarity; +import tech.pegasys.pantheon.consensus.common.VoteBlockInterface; +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.util.bytes.BytesValue; + +import java.util.List; +import java.util.Optional; + +import com.google.common.collect.ImmutableBiMap; + +public class CliqueVotingBlockInterface implements VoteBlockInterface { + + public static final Address NO_VOTE_SUBJECT = + Address.wrap(BytesValue.wrap(new byte[Address.SIZE])); + + private static final ImmutableBiMap voteToValue = + ImmutableBiMap.of( + ValidatorVotePolarity.ADD, 0xFFFFFFFFFFFFFFFFL, + ValidatorVotePolarity.DROP, 0x0L); + + @Override + public Optional extractVoteFromHeader(final BlockHeader header) { + final Address candidate = header.getCoinbase(); + if (!candidate.equals(NO_VOTE_SUBJECT)) { + final CliqueExtraData cliqueExtraData = CliqueExtraData.decode(header.getExtraData()); + final Address proposer = CliqueBlockHashing.recoverProposerAddress(header, cliqueExtraData); + final ValidatorVotePolarity votePolarity = voteToValue.inverse().get(header.getNonce()); + final Address recipient = header.getCoinbase(); + + return Optional.of(new CastVote(votePolarity, proposer, recipient)); + } + return Optional.empty(); + } + + @Override + public BlockHeaderBuilder insertVoteToHeaderBuilder( + final BlockHeaderBuilder builder, final Optional vote) { + if (vote.isPresent()) { + final CastVote voteToCast = vote.get(); + builder.nonce(voteToValue.get(voteToCast.getVotePolarity())); + builder.coinbase(voteToCast.getRecipient()); + } else { + builder.nonce(voteToValue.get(ValidatorVotePolarity.DROP)); + builder.coinbase(NO_VOTE_SUBJECT); + } + return builder; + } + + @Override + public List
validatorsInBlock(final BlockHeader header) { + return CliqueExtraData.decode(header.getExtraData()).getValidators(); + } +} diff --git a/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/VoteTallyCache.java b/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/VoteTallyCache.java index cb1d5794c5..02ad1012ea 100644 --- a/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/VoteTallyCache.java +++ b/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/VoteTallyCache.java @@ -16,6 +16,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import tech.pegasys.pantheon.consensus.common.EpochManager; import tech.pegasys.pantheon.consensus.common.VoteTally; +import tech.pegasys.pantheon.consensus.common.VoteTallyUpdater; import tech.pegasys.pantheon.ethereum.chain.Blockchain; import tech.pegasys.pantheon.ethereum.core.BlockHeader; import tech.pegasys.pantheon.ethereum.core.Hash; @@ -32,14 +33,14 @@ public class VoteTallyCache { private final Blockchain blockchain; private final EpochManager epochManager; - private final CliqueVoteTallyUpdater voteTallyUpdater; + private final VoteTallyUpdater voteTallyUpdater; private final Cache voteTallyCache = CacheBuilder.newBuilder().maximumSize(100).build(); public VoteTallyCache( final Blockchain blockchain, - final CliqueVoteTallyUpdater voteTallyUpdater, + final VoteTallyUpdater voteTallyUpdater, final EpochManager epochManager) { checkNotNull(blockchain); checkNotNull(voteTallyUpdater); diff --git a/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/headervalidationrules/CoinbaseHeaderValidationRule.java b/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/headervalidationrules/CoinbaseHeaderValidationRule.java index 5b271aa40b..d055de2912 100644 --- a/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/headervalidationrules/CoinbaseHeaderValidationRule.java +++ b/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/headervalidationrules/CoinbaseHeaderValidationRule.java @@ -12,7 +12,7 @@ */ package tech.pegasys.pantheon.consensus.clique.headervalidationrules; -import tech.pegasys.pantheon.consensus.clique.CliqueVoteTallyUpdater; +import tech.pegasys.pantheon.consensus.clique.CliqueVotingBlockInterface; import tech.pegasys.pantheon.consensus.common.EpochManager; import tech.pegasys.pantheon.ethereum.core.BlockHeader; import tech.pegasys.pantheon.ethereum.mainnet.DetachedBlockHeaderValidationRule; @@ -30,7 +30,7 @@ public class CoinbaseHeaderValidationRule implements DetachedBlockHeaderValidati // are allowed to be cast on epoch blocks public boolean validate(final BlockHeader header, final BlockHeader parent) { if (epochManager.isEpochBlock(header.getNumber())) { - return header.getCoinbase().equals(CliqueVoteTallyUpdater.NO_VOTE_SUBJECT); + return header.getCoinbase().equals(CliqueVotingBlockInterface.NO_VOTE_SUBJECT); } return true; } diff --git a/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/jsonrpc/CliqueJsonRpcMethodsFactory.java b/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/jsonrpc/CliqueJsonRpcMethodsFactory.java index e1967cde00..c5d07e4b51 100644 --- a/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/jsonrpc/CliqueJsonRpcMethodsFactory.java +++ b/consensus/clique/src/main/java/tech/pegasys/pantheon/consensus/clique/jsonrpc/CliqueJsonRpcMethodsFactory.java @@ -13,7 +13,7 @@ package tech.pegasys.pantheon.consensus.clique.jsonrpc; import tech.pegasys.pantheon.consensus.clique.CliqueContext; -import tech.pegasys.pantheon.consensus.clique.CliqueVoteTallyUpdater; +import tech.pegasys.pantheon.consensus.clique.CliqueVotingBlockInterface; import tech.pegasys.pantheon.consensus.clique.VoteTallyCache; import tech.pegasys.pantheon.consensus.clique.jsonrpc.methods.CliqueGetSigners; import tech.pegasys.pantheon.consensus.clique.jsonrpc.methods.CliqueGetSignersAtHash; @@ -22,6 +22,7 @@ import tech.pegasys.pantheon.consensus.clique.jsonrpc.methods.Discard; import tech.pegasys.pantheon.consensus.clique.jsonrpc.methods.Propose; import tech.pegasys.pantheon.consensus.common.EpochManager; import tech.pegasys.pantheon.consensus.common.VoteProposer; +import tech.pegasys.pantheon.consensus.common.VoteTallyUpdater; import tech.pegasys.pantheon.ethereum.ProtocolContext; import tech.pegasys.pantheon.ethereum.chain.MutableBlockchain; import tech.pegasys.pantheon.ethereum.db.WorldStateArchive; @@ -70,7 +71,8 @@ public class CliqueJsonRpcMethodsFactory { private VoteTallyCache createVoteTallyCache( final ProtocolContext context, final MutableBlockchain blockchain) { final EpochManager epochManager = context.getConsensusState().getEpochManager(); - final CliqueVoteTallyUpdater cliqueVoteTallyUpdater = new CliqueVoteTallyUpdater(epochManager); - return new VoteTallyCache(blockchain, cliqueVoteTallyUpdater, epochManager); + final VoteTallyUpdater voteTallyUpdater = + new VoteTallyUpdater(epochManager, new CliqueVotingBlockInterface()); + return new VoteTallyCache(blockchain, voteTallyUpdater, epochManager); } } diff --git a/consensus/clique/src/test/java/tech/pegasys/pantheon/consensus/clique/CliqueVotingBlockInterfaceTest.java b/consensus/clique/src/test/java/tech/pegasys/pantheon/consensus/clique/CliqueVotingBlockInterfaceTest.java new file mode 100644 index 0000000000..8f9c92e1e9 --- /dev/null +++ b/consensus/clique/src/test/java/tech/pegasys/pantheon/consensus/clique/CliqueVotingBlockInterfaceTest.java @@ -0,0 +1,126 @@ +/* + * 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.clique; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static tech.pegasys.pantheon.consensus.common.ValidatorVotePolarity.ADD; +import static tech.pegasys.pantheon.consensus.common.ValidatorVotePolarity.DROP; + +import tech.pegasys.pantheon.consensus.common.CastVote; +import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.AddressHelpers; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.BlockHeaderBuilder; +import tech.pegasys.pantheon.ethereum.core.BlockHeaderTestFixture; +import tech.pegasys.pantheon.ethereum.core.Util; +import tech.pegasys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; + +import java.util.List; +import java.util.Optional; + +import org.junit.Test; + +public class CliqueVotingBlockInterfaceTest { + + private static final KeyPair proposerKeys = KeyPair.generate(); + private static final Address proposerAddress = + Util.publicKeyToAddress(proposerKeys.getPublicKey()); + private static final List
validatorList = singletonList(proposerAddress); + + private final CliqueVotingBlockInterface blockInterface = new CliqueVotingBlockInterface(); + + private final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + + private final BlockHeader header = + TestHelpers.createCliqueSignedBlockHeader(headerBuilder, proposerKeys, validatorList); + + private final BlockHeaderBuilder builder = + BlockHeaderBuilder.fromHeader(headerBuilder.buildHeader()) + .blockHashFunction(MainnetBlockHashFunction::createHash); + + @Test + public void headerWithZeroCoinbaseReturnsAnEmptyVote() { + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + headerBuilder.coinbase(AddressHelpers.ofValue(0)); + + assertThat(blockInterface.extractVoteFromHeader(headerBuilder.buildHeader())).isEmpty(); + } + + @Test + public void headerWithNonceOfZeroReportsDropVote() { + headerBuilder.coinbase(AddressHelpers.ofValue(1)).nonce(0L); + + final Optional extractedVote = blockInterface.extractVoteFromHeader(header); + + assertThat(extractedVote).contains(new CastVote(DROP, proposerAddress, header.getCoinbase())); + } + + @Test + public void headerWithNonceOfMaxLongReportsAddVote() { + headerBuilder.coinbase(AddressHelpers.ofValue(2)).nonce(0xFFFFFFFFFFFFFFFFL); + + final BlockHeader header = + TestHelpers.createCliqueSignedBlockHeader(headerBuilder, proposerKeys, validatorList); + final Optional extractedVote = blockInterface.extractVoteFromHeader(header); + + assertThat(extractedVote).contains(new CastVote(ADD, proposerAddress, header.getCoinbase())); + } + + @Test + public void blendingAddVoteToHeaderResultsInHeaderWithNonceOfMaxLong() { + + final CastVote vote = new CastVote(ADD, AddressHelpers.ofValue(1), AddressHelpers.ofValue(2)); + final BlockHeaderBuilder builderWithVote = + blockInterface.insertVoteToHeaderBuilder(builder, Optional.of(vote)); + + final BlockHeader header = builderWithVote.buildBlockHeader(); + + assertThat(header.getCoinbase()).isEqualTo(vote.getRecipient()); + assertThat(header.getNonce()).isEqualTo(0xFFFFFFFFFFFFFFFFL); + } + + @Test + public void blendingDropVoteToHeaderResultsInHeaderWithNonceOfZero() { + + final CastVote vote = new CastVote(DROP, AddressHelpers.ofValue(1), AddressHelpers.ofValue(2)); + final BlockHeaderBuilder builderWithVote = + blockInterface.insertVoteToHeaderBuilder(builder, Optional.of(vote)); + + final BlockHeader header = builderWithVote.buildBlockHeader(); + + assertThat(header.getCoinbase()).isEqualTo(vote.getRecipient()); + assertThat(header.getNonce()).isEqualTo(0x0L); + } + + @Test + public void nonVoteBlendedIntoHeaderResultsInACoinbaseOfZero() { + final BlockHeaderBuilder builderWithVote = + blockInterface.insertVoteToHeaderBuilder(builder, Optional.empty()); + + final BlockHeader header = builderWithVote.buildBlockHeader(); + + assertThat(header.getCoinbase()).isEqualTo(AddressHelpers.ofValue(0)); + assertThat(header.getNonce()).isEqualTo(0x0L); + } + + @Test + public void extractsValidatorsFromHeader() { + final BlockHeader header = + TestHelpers.createCliqueSignedBlockHeader(headerBuilder, proposerKeys, validatorList); + final List
extractedValidators = blockInterface.validatorsInBlock(header); + + assertThat(extractedValidators).isEqualTo(validatorList); + } +} diff --git a/consensus/clique/src/test/java/tech/pegasys/pantheon/consensus/clique/VoteTallyCacheTest.java b/consensus/clique/src/test/java/tech/pegasys/pantheon/consensus/clique/VoteTallyCacheTest.java index cee3be4686..d767d25239 100644 --- a/consensus/clique/src/test/java/tech/pegasys/pantheon/consensus/clique/VoteTallyCacheTest.java +++ b/consensus/clique/src/test/java/tech/pegasys/pantheon/consensus/clique/VoteTallyCacheTest.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import tech.pegasys.pantheon.consensus.common.EpochManager; +import tech.pegasys.pantheon.consensus.common.VoteTallyUpdater; import tech.pegasys.pantheon.crypto.SECP256K1.Signature; import tech.pegasys.pantheon.ethereum.chain.MutableBlockchain; import tech.pegasys.pantheon.ethereum.core.AddressHelpers; @@ -79,7 +80,7 @@ public class VoteTallyCacheTest { @Test public void parentBlockVoteTallysAreCachedWhenChildVoteTallyRequested() { - final CliqueVoteTallyUpdater tallyUpdater = mock(CliqueVoteTallyUpdater.class); + final VoteTallyUpdater tallyUpdater = mock(VoteTallyUpdater.class); final VoteTallyCache cache = new VoteTallyCache(blockChain, tallyUpdater, new EpochManager(30_000)); @@ -105,7 +106,7 @@ public class VoteTallyCacheTest { @Test public void exceptionThrownIfNoParentBlockExists() { - final CliqueVoteTallyUpdater tallyUpdater = mock(CliqueVoteTallyUpdater.class); + final VoteTallyUpdater tallyUpdater = mock(VoteTallyUpdater.class); final VoteTallyCache cache = new VoteTallyCache(blockChain, tallyUpdater, new EpochManager(30_000)); @@ -119,7 +120,7 @@ public class VoteTallyCacheTest { @Test public void walkBackStopsWhenACachedVoteTallyIsFound() { - final CliqueVoteTallyUpdater tallyUpdater = mock(CliqueVoteTallyUpdater.class); + final VoteTallyUpdater tallyUpdater = mock(VoteTallyUpdater.class); final VoteTallyCache cache = new VoteTallyCache(blockChain, tallyUpdater, new EpochManager(30_000)); diff --git a/consensus/common/build.gradle b/consensus/common/build.gradle index 4a297ed24c..0fdf5ec215 100644 --- a/consensus/common/build.gradle +++ b/consensus/common/build.gradle @@ -30,6 +30,7 @@ dependencies { implementation project(':util') implementation 'com.google.guava:guava' + testImplementation project(':crypto') testImplementation project( path: ':ethereum:core', configuration: 'testSupportArtifacts') testImplementation 'junit:junit' testImplementation "org.assertj:assertj-core" diff --git a/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/CastVote.java b/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/CastVote.java new file mode 100644 index 0000000000..6d58017559 --- /dev/null +++ b/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/CastVote.java @@ -0,0 +1,67 @@ +/* + * 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; + +import tech.pegasys.pantheon.ethereum.core.Address; + +import java.util.Objects; + +import com.google.common.base.Preconditions; + +public class CastVote { + + private final ValidatorVotePolarity votePolarity; + private final Address proposer; + private final Address recipient; + + public CastVote( + final ValidatorVotePolarity votePolarity, final Address proposer, final Address recipient) { + Preconditions.checkNotNull(votePolarity); + Preconditions.checkNotNull(proposer); + Preconditions.checkNotNull(recipient); + this.votePolarity = votePolarity; + this.proposer = proposer; + this.recipient = recipient; + } + + public ValidatorVotePolarity getVotePolarity() { + return votePolarity; + } + + public Address getProposer() { + return proposer; + } + + public Address getRecipient() { + return recipient; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CastVote castVote = (CastVote) o; + return votePolarity == castVote.votePolarity + && Objects.equals(proposer, castVote.proposer) + && Objects.equals(recipient, castVote.recipient); + } + + @Override + public int hashCode() { + return Objects.hash(votePolarity, proposer, recipient); + } +} diff --git a/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/ValidatorVotePolarity.java b/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/ValidatorVotePolarity.java new file mode 100644 index 0000000000..ce2cb5d9d0 --- /dev/null +++ b/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/ValidatorVotePolarity.java @@ -0,0 +1,32 @@ +/* + * 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; + +/** + * Determines if a validator vote is indicating that they should be added, or removed. This does not + * attempt to determine how said vote should be serialised/deserialised. + */ +public enum ValidatorVotePolarity implements ValidatorVote { + ADD, + DROP; + + @Override + public boolean isAddVote() { + return this.equals(ADD); + } + + @Override + public boolean isDropVote() { + return this.equals(DROP); + } +} diff --git a/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/VoteBlockInterface.java b/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/VoteBlockInterface.java new file mode 100644 index 0000000000..b8e7050027 --- /dev/null +++ b/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/VoteBlockInterface.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 tech.pegasys.pantheon.consensus.common; + +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.BlockHeaderBuilder; + +import java.util.List; +import java.util.Optional; + +public interface VoteBlockInterface { + + Optional extractVoteFromHeader(final BlockHeader header); + + BlockHeaderBuilder insertVoteToHeaderBuilder( + final BlockHeaderBuilder builder, final Optional vote); + + List
validatorsInBlock(final BlockHeader header); +} 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 d86d57c8d4..2f861e1885 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 @@ -52,35 +52,32 @@ public class VoteTally implements ValidatorProvider { * Add a vote to the current tally. The current validator list will be updated if this vote takes * the tally past the required votes to approve the change. * - * @param proposer the address of the validator casting the vote via block proposal - * @param subject the validator the vote is about - * @param validatorVote the type of vote, either add or drop + * @param castVote The vote which was cast in a block header. */ - public void addVote( - final Address proposer, final Address subject, final ValidatorVote validatorVote) { + public void addVote(final CastVote castVote) { final Set
addVotesForSubject = - addVotesBySubject.computeIfAbsent(subject, target -> new HashSet<>()); + addVotesBySubject.computeIfAbsent(castVote.getRecipient(), target -> new HashSet<>()); final Set
removeVotesForSubject = - removeVotesBySubject.computeIfAbsent(subject, target -> new HashSet<>()); + removeVotesBySubject.computeIfAbsent(castVote.getRecipient(), target -> new HashSet<>()); - if (validatorVote.isAddVote()) { - addVotesForSubject.add(proposer); - removeVotesForSubject.remove(proposer); + if (castVote.getVotePolarity().isAddVote()) { + addVotesForSubject.add(castVote.getProposer()); + removeVotesForSubject.remove(castVote.getProposer()); } else { - removeVotesForSubject.add(proposer); - addVotesForSubject.remove(proposer); + removeVotesForSubject.add(castVote.getProposer()); + addVotesForSubject.remove(castVote.getProposer()); } final int validatorLimit = validatorLimit(); if (addVotesForSubject.size() >= validatorLimit) { - currentValidators.add(subject); - discardOutstandingVotesFor(subject); + currentValidators.add(castVote.getRecipient()); + discardOutstandingVotesFor(castVote.getRecipient()); } if (removeVotesForSubject.size() >= validatorLimit) { - currentValidators.remove(subject); - discardOutstandingVotesFor(subject); - addVotesBySubject.values().forEach(votes -> votes.remove(subject)); - removeVotesBySubject.values().forEach(votes -> votes.remove(subject)); + currentValidators.remove(castVote.getRecipient()); + discardOutstandingVotesFor(castVote.getRecipient()); + addVotesBySubject.values().forEach(votes -> votes.remove(castVote.getRecipient())); + removeVotesBySubject.values().forEach(votes -> votes.remove(castVote.getRecipient())); } } diff --git a/consensus/ibftlegacy/src/main/java/tech/pegasys/pantheon/consensus/ibftlegacy/IbftVoteTallyUpdater.java b/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/VoteTallyUpdater.java similarity index 69% rename from consensus/ibftlegacy/src/main/java/tech/pegasys/pantheon/consensus/ibftlegacy/IbftVoteTallyUpdater.java rename to consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/VoteTallyUpdater.java index 928c26284b..393a20fbf3 100644 --- a/consensus/ibftlegacy/src/main/java/tech/pegasys/pantheon/consensus/ibftlegacy/IbftVoteTallyUpdater.java +++ b/consensus/common/src/main/java/tech/pegasys/pantheon/consensus/common/VoteTallyUpdater.java @@ -10,18 +10,14 @@ * 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.ibftlegacy; +package tech.pegasys.pantheon.consensus.common; -import tech.pegasys.pantheon.consensus.common.EpochManager; -import tech.pegasys.pantheon.consensus.common.VoteTally; -import tech.pegasys.pantheon.consensus.common.VoteType; -import tech.pegasys.pantheon.consensus.ibft.VoteTallyUpdater; import tech.pegasys.pantheon.ethereum.chain.Blockchain; import tech.pegasys.pantheon.ethereum.core.Address; import tech.pegasys.pantheon.ethereum.core.BlockHeader; -import tech.pegasys.pantheon.util.bytes.BytesValue; import java.util.List; +import java.util.Optional; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -30,15 +26,17 @@ import org.apache.logging.log4j.Logger; * Provides the logic to extract vote tally state from the blockchain and update it as blocks are * added. */ -public class IbftVoteTallyUpdater implements VoteTallyUpdater { +public class VoteTallyUpdater { private static final Logger LOG = LogManager.getLogger(); - private static final Address NO_VOTE_SUBJECT = Address.wrap(BytesValue.wrap(new byte[20])); private final EpochManager epochManager; + private final VoteBlockInterface blockInterface; - public IbftVoteTallyUpdater(final EpochManager epochManager) { + public VoteTallyUpdater( + final EpochManager epochManager, final VoteBlockInterface blockInterface) { this.epochManager = epochManager; + this.blockInterface = blockInterface; } /** @@ -47,14 +45,12 @@ public class IbftVoteTallyUpdater implements VoteTallyUpdater { * @param blockchain the blockchain to load the current state from * @return a VoteTally reflecting the state of the blockchain head */ - @Override public VoteTally buildVoteTallyFromBlockchain(final Blockchain blockchain) { final long chainHeadBlockNumber = blockchain.getChainHeadBlockNumber(); final long epochBlockNumber = epochManager.getLastEpochBlock(chainHeadBlockNumber); LOG.info("Loading validator voting state starting from block {}", epochBlockNumber); final BlockHeader epochBlock = blockchain.getBlockHeader(epochBlockNumber).get(); - final List
initialValidators = - IbftExtraData.decode(epochBlock.getExtraData()).getValidators(); + final List
initialValidators = blockInterface.validatorsInBlock(epochBlock); final VoteTally voteTally = new VoteTally(initialValidators); for (long blockNumber = epochBlockNumber + 1; blockNumber <= chainHeadBlockNumber; @@ -70,18 +66,12 @@ public class IbftVoteTallyUpdater implements VoteTallyUpdater { * @param header the header of the block being added * @param voteTally the vote tally to update */ - @Override public void updateForBlock(final BlockHeader header, final VoteTally voteTally) { - final Address candidate = header.getCoinbase(); if (epochManager.isEpochBlock(header.getNumber())) { voteTally.discardOutstandingVotes(); return; } - - if (!candidate.equals(NO_VOTE_SUBJECT)) { - final IbftExtraData ibftExtraData = IbftExtraData.decode(header.getExtraData()); - final Address proposer = IbftBlockHashing.recoverProposerAddress(header, ibftExtraData); - voteTally.addVote(proposer, candidate, VoteType.fromNonce(header.getNonce()).get()); - } + final Optional vote = blockInterface.extractVoteFromHeader(header); + vote.ifPresent(voteTally::addVote); } } diff --git a/consensus/common/src/test/java/tech/pegasys/pantheon/consensus/common/VoteProposerTest.java b/consensus/common/src/test/java/tech/pegasys/pantheon/consensus/common/VoteProposerTest.java index c31eb027f5..723cb2e94a 100644 --- a/consensus/common/src/test/java/tech/pegasys/pantheon/consensus/common/VoteProposerTest.java +++ b/consensus/common/src/test/java/tech/pegasys/pantheon/consensus/common/VoteProposerTest.java @@ -13,8 +13,8 @@ package tech.pegasys.pantheon.consensus.common; import static org.assertj.core.api.Assertions.assertThat; -import static tech.pegasys.pantheon.consensus.common.VoteType.ADD; -import static tech.pegasys.pantheon.consensus.common.VoteType.DROP; +import static tech.pegasys.pantheon.consensus.common.ValidatorVotePolarity.ADD; +import static tech.pegasys.pantheon.consensus.common.ValidatorVotePolarity.DROP; import tech.pegasys.pantheon.ethereum.core.Address; @@ -158,7 +158,7 @@ public class VoteProposerTest { proposer.drop(a1); final VoteTally tally = new VoteTally(Arrays.asList(a2, a3)); - tally.addVote(localAddress, a1, ADD); + tally.addVote(new CastVote(ADD, localAddress, a1)); assertThat(proposer.getVote(localAddress, tally)) .isEqualTo(Optional.of(new AbstractMap.SimpleEntry<>(a1, VoteType.DROP))); @@ -174,7 +174,7 @@ public class VoteProposerTest { proposer.auth(a1); final VoteTally tally = new VoteTally(Arrays.asList(a2, a3)); - tally.addVote(localAddress, a1, DROP); + tally.addVote(new CastVote(DROP, localAddress, a1)); assertThat(proposer.getVote(localAddress, tally)) .isEqualTo(Optional.of(new AbstractMap.SimpleEntry<>(a1, VoteType.ADD))); @@ -188,7 +188,7 @@ public class VoteProposerTest { final Address a3 = Address.fromHexString("3"); final VoteTally tally = new VoteTally(Arrays.asList(a1, a2, a3)); - tally.addVote(localAddress, a1, ADD); + tally.addVote(new CastVote(ADD, localAddress, a1)); proposer.drop(a1); @@ -204,7 +204,7 @@ public class VoteProposerTest { final Address a3 = Address.fromHexString("3"); final VoteTally tally = new VoteTally(Arrays.asList(a1, a2, a3)); - tally.addVote(localAddress, a1, DROP); + tally.addVote(new CastVote(DROP, localAddress, a1)); proposer.auth(a1); diff --git a/consensus/common/src/test/java/tech/pegasys/pantheon/consensus/common/VoteTallyTest.java b/consensus/common/src/test/java/tech/pegasys/pantheon/consensus/common/VoteTallyTest.java index 459975b010..65db65ab1f 100644 --- a/consensus/common/src/test/java/tech/pegasys/pantheon/consensus/common/VoteTallyTest.java +++ b/consensus/common/src/test/java/tech/pegasys/pantheon/consensus/common/VoteTallyTest.java @@ -15,6 +15,8 @@ package tech.pegasys.pantheon.consensus.common; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; +import static tech.pegasys.pantheon.consensus.common.ValidatorVotePolarity.ADD; +import static tech.pegasys.pantheon.consensus.common.ValidatorVotePolarity.DROP; import tech.pegasys.pantheon.ethereum.core.Address; @@ -36,8 +38,8 @@ public class VoteTallyTest { @Test public void validatorsAreNotAddedBeforeRequiredVoteCountReached() { final VoteTally voteTally = fourValidators(); - voteTally.addVote(validator1, validator5, VoteType.ADD); - voteTally.addVote(validator2, validator5, VoteType.ADD); + voteTally.addVote(new CastVote(ADD, validator1, validator5)); + voteTally.addVote(new CastVote(ADD, validator2, validator5)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3, validator4); @@ -46,9 +48,9 @@ public class VoteTallyTest { @Test public void validatorAddedToListWhenMoreThanHalfOfProposersVoteToAdd() { final VoteTally voteTally = fourValidators(); - voteTally.addVote(validator1, validator5, VoteType.ADD); - voteTally.addVote(validator2, validator5, VoteType.ADD); - voteTally.addVote(validator3, validator5, VoteType.ADD); + voteTally.addVote(new CastVote(ADD, validator1, validator5)); + voteTally.addVote(new CastVote(ADD, validator2, validator5)); + voteTally.addVote(new CastVote(ADD, validator3, validator5)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3, validator4, validator5); @@ -58,9 +60,9 @@ public class VoteTallyTest { public void validatorsAreAddedInCorrectOrder() { final VoteTally voteTally = new VoteTally(asList(validator1, validator2, validator3, validator5)); - voteTally.addVote(validator1, validator4, VoteType.ADD); - voteTally.addVote(validator2, validator4, VoteType.ADD); - voteTally.addVote(validator3, validator4, VoteType.ADD); + voteTally.addVote(new CastVote(ADD, validator1, validator4)); + voteTally.addVote(new CastVote(ADD, validator2, validator4)); + voteTally.addVote(new CastVote(ADD, validator3, validator4)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3, validator4, validator5); @@ -69,9 +71,9 @@ public class VoteTallyTest { @Test public void duplicateVotesFromSameProposerAreIgnored() { final VoteTally voteTally = fourValidators(); - voteTally.addVote(validator1, validator5, VoteType.ADD); - voteTally.addVote(validator2, validator5, VoteType.ADD); - voteTally.addVote(validator2, validator5, VoteType.ADD); + voteTally.addVote(new CastVote(ADD, validator1, validator5)); + voteTally.addVote(new CastVote(ADD, validator2, validator5)); + voteTally.addVote(new CastVote(ADD, validator2, validator5)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3, validator4); @@ -80,10 +82,10 @@ public class VoteTallyTest { @Test public void proposerChangingAddVoteToDropBeforeLimitReachedDiscardsAddVote() { final VoteTally voteTally = fourValidators(); - voteTally.addVote(validator1, validator5, VoteType.ADD); - voteTally.addVote(validator1, validator5, VoteType.DROP); - voteTally.addVote(validator2, validator5, VoteType.ADD); - voteTally.addVote(validator3, validator5, VoteType.ADD); + voteTally.addVote(new CastVote(ADD, validator1, validator5)); + voteTally.addVote(new CastVote(DROP, validator1, validator5)); + voteTally.addVote(new CastVote(ADD, validator2, validator5)); + voteTally.addVote(new CastVote(ADD, validator3, validator5)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3, validator4); @@ -92,10 +94,10 @@ public class VoteTallyTest { @Test public void proposerChangingAddVoteToDropAfterLimitReachedPreservesAddVote() { final VoteTally voteTally = fourValidators(); - voteTally.addVote(validator1, validator5, VoteType.ADD); - voteTally.addVote(validator2, validator5, VoteType.ADD); - voteTally.addVote(validator3, validator5, VoteType.ADD); - voteTally.addVote(validator1, validator5, VoteType.DROP); + voteTally.addVote(new CastVote(ADD, validator1, validator5)); + voteTally.addVote(new CastVote(ADD, validator2, validator5)); + voteTally.addVote(new CastVote(ADD, validator3, validator5)); + voteTally.addVote(new CastVote(DROP, validator1, validator5)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3, validator4, validator5); @@ -105,23 +107,23 @@ public class VoteTallyTest { public void clearVotesAboutAValidatorWhenItIsAdded() { final VoteTally voteTally = fourValidators(); // Vote to add validator5 - voteTally.addVote(validator1, validator5, VoteType.ADD); - voteTally.addVote(validator2, validator5, VoteType.ADD); - voteTally.addVote(validator3, validator5, VoteType.ADD); + voteTally.addVote(new CastVote(ADD, validator1, validator5)); + voteTally.addVote(new CastVote(ADD, validator2, validator5)); + voteTally.addVote(new CastVote(ADD, validator3, validator5)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3, validator4, validator5); // Then vote it back out - voteTally.addVote(validator2, validator5, VoteType.DROP); - voteTally.addVote(validator3, validator5, VoteType.DROP); - voteTally.addVote(validator4, validator5, VoteType.DROP); + voteTally.addVote(new CastVote(DROP, validator2, validator5)); + voteTally.addVote(new CastVote(DROP, validator3, validator5)); + voteTally.addVote(new CastVote(DROP, validator4, validator5)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3, validator4); // And then start voting to add it back in, but validator1's vote should have been discarded - voteTally.addVote(validator2, validator5, VoteType.ADD); - voteTally.addVote(validator3, validator5, VoteType.ADD); + voteTally.addVote(new CastVote(ADD, validator2, validator5)); + voteTally.addVote(new CastVote(ADD, validator3, validator5)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3, validator4); } @@ -129,7 +131,7 @@ public class VoteTallyTest { @Test public void requiresASingleVoteWhenThereIsOnlyOneValidator() { final VoteTally voteTally = new VoteTally(singletonList(validator1)); - voteTally.addVote(validator1, validator2, VoteType.ADD); + voteTally.addVote(new CastVote(ADD, validator1, validator2)); assertThat(voteTally.getCurrentValidators()).containsExactly(validator1, validator2); } @@ -137,11 +139,11 @@ public class VoteTallyTest { @Test public void requiresTwoVotesWhenThereAreTwoValidators() { final VoteTally voteTally = new VoteTally(asList(validator1, validator2)); - voteTally.addVote(validator1, validator3, VoteType.ADD); + voteTally.addVote(new CastVote(ADD, validator1, validator3)); assertThat(voteTally.getCurrentValidators()).containsExactly(validator1, validator2); - voteTally.addVote(validator2, validator3, VoteType.ADD); + voteTally.addVote(new CastVote(ADD, validator2, validator3)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3); } @@ -149,10 +151,10 @@ public class VoteTallyTest { @Test public void resetVotes() { final VoteTally voteTally = fourValidators(); - voteTally.addVote(validator1, validator5, VoteType.ADD); - voteTally.addVote(validator2, validator5, VoteType.ADD); + voteTally.addVote(new CastVote(ADD, validator1, validator5)); + voteTally.addVote(new CastVote(ADD, validator2, validator5)); voteTally.discardOutstandingVotes(); - voteTally.addVote(validator3, validator5, VoteType.ADD); + voteTally.addVote(new CastVote(ADD, validator3, validator5)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3, validator4); @@ -161,8 +163,8 @@ public class VoteTallyTest { @Test public void validatorsAreNotRemovedBeforeRequiredVoteCountReached() { final VoteTally voteTally = fourValidators(); - voteTally.addVote(validator1, validator4, VoteType.DROP); - voteTally.addVote(validator2, validator4, VoteType.DROP); + voteTally.addVote(new CastVote(DROP, validator1, validator4)); + voteTally.addVote(new CastVote(DROP, validator2, validator4)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3, validator4); @@ -171,9 +173,9 @@ public class VoteTallyTest { @Test public void validatorRemovedFromListWhenMoreThanHalfOfProposersVoteToDrop() { final VoteTally voteTally = fourValidators(); - voteTally.addVote(validator1, validator4, VoteType.DROP); - voteTally.addVote(validator2, validator4, VoteType.DROP); - voteTally.addVote(validator3, validator4, VoteType.DROP); + voteTally.addVote(new CastVote(DROP, validator1, validator4)); + voteTally.addVote(new CastVote(DROP, validator2, validator4)); + voteTally.addVote(new CastVote(DROP, validator3, validator4)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3); @@ -182,9 +184,9 @@ public class VoteTallyTest { @Test public void validatorsAreInCorrectOrderAfterRemoval() { final VoteTally voteTally = new VoteTally(asList(validator1, validator2, validator4)); - voteTally.addVote(validator1, validator3, VoteType.DROP); - voteTally.addVote(validator2, validator3, VoteType.DROP); - voteTally.addVote(validator4, validator3, VoteType.DROP); + voteTally.addVote(new CastVote(DROP, validator1, validator3)); + voteTally.addVote(new CastVote(DROP, validator2, validator3)); + voteTally.addVote(new CastVote(DROP, validator4, validator3)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator4); @@ -193,9 +195,9 @@ public class VoteTallyTest { @Test public void duplicateDropVotesFromSameProposerAreIgnored() { final VoteTally voteTally = fourValidators(); - voteTally.addVote(validator1, validator4, VoteType.DROP); - voteTally.addVote(validator2, validator4, VoteType.DROP); - voteTally.addVote(validator2, validator4, VoteType.DROP); + voteTally.addVote(new CastVote(DROP, validator1, validator4)); + voteTally.addVote(new CastVote(DROP, validator2, validator4)); + voteTally.addVote(new CastVote(DROP, validator2, validator4)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3, validator4); @@ -204,10 +206,10 @@ public class VoteTallyTest { @Test public void proposerChangingDropVoteToAddBeforeLimitReachedDiscardsDropVote() { final VoteTally voteTally = fourValidators(); - voteTally.addVote(validator1, validator4, VoteType.DROP); - voteTally.addVote(validator1, validator4, VoteType.ADD); - voteTally.addVote(validator2, validator4, VoteType.DROP); - voteTally.addVote(validator3, validator4, VoteType.DROP); + voteTally.addVote(new CastVote(DROP, validator1, validator4)); + voteTally.addVote(new CastVote(ADD, validator1, validator4)); + voteTally.addVote(new CastVote(DROP, validator2, validator4)); + voteTally.addVote(new CastVote(DROP, validator3, validator4)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3, validator4); @@ -216,10 +218,10 @@ public class VoteTallyTest { @Test public void proposerChangingDropVoteToAddAfterLimitReachedPreservesDropVote() { final VoteTally voteTally = fourValidators(); - voteTally.addVote(validator1, validator4, VoteType.DROP); - voteTally.addVote(validator2, validator4, VoteType.DROP); - voteTally.addVote(validator3, validator4, VoteType.DROP); - voteTally.addVote(validator1, validator4, VoteType.ADD); + voteTally.addVote(new CastVote(DROP, validator1, validator4)); + voteTally.addVote(new CastVote(DROP, validator2, validator4)); + voteTally.addVote(new CastVote(DROP, validator3, validator4)); + voteTally.addVote(new CastVote(ADD, validator1, validator4)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3); @@ -228,19 +230,19 @@ public class VoteTallyTest { @Test public void removedValidatorsVotesAreDiscarded() { final VoteTally voteTally = fourValidators(); - voteTally.addVote(validator4, validator5, VoteType.ADD); - voteTally.addVote(validator4, validator3, VoteType.DROP); + voteTally.addVote(new CastVote(ADD, validator4, validator5)); + voteTally.addVote(new CastVote(DROP, validator4, validator3)); - voteTally.addVote(validator1, validator4, VoteType.DROP); - voteTally.addVote(validator2, validator4, VoteType.DROP); - voteTally.addVote(validator3, validator4, VoteType.DROP); + voteTally.addVote(new CastVote(DROP, validator1, validator4)); + voteTally.addVote(new CastVote(DROP, validator2, validator4)); + voteTally.addVote(new CastVote(DROP, validator3, validator4)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3); // Now adding only requires 2 votes (>50% of the 3 remaining validators) // but validator4's vote no longer counts - voteTally.addVote(validator1, validator5, VoteType.ADD); - voteTally.addVote(validator1, validator3, VoteType.DROP); + voteTally.addVote(new CastVote(ADD, validator1, validator5)); + voteTally.addVote(new CastVote(DROP, validator1, validator3)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3); @@ -251,23 +253,23 @@ public class VoteTallyTest { final VoteTally voteTally = new VoteTally(asList(validator1, validator2, validator3, validator4, validator5)); // Vote to remove validator5 - voteTally.addVote(validator1, validator5, VoteType.DROP); - voteTally.addVote(validator2, validator5, VoteType.DROP); - voteTally.addVote(validator3, validator5, VoteType.DROP); + voteTally.addVote(new CastVote(DROP, validator1, validator5)); + voteTally.addVote(new CastVote(DROP, validator2, validator5)); + voteTally.addVote(new CastVote(DROP, validator3, validator5)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3, validator4); // Then vote it back in - voteTally.addVote(validator2, validator5, VoteType.ADD); - voteTally.addVote(validator3, validator5, VoteType.ADD); - voteTally.addVote(validator4, validator5, VoteType.ADD); + voteTally.addVote(new CastVote(ADD, validator2, validator5)); + voteTally.addVote(new CastVote(ADD, validator3, validator5)); + voteTally.addVote(new CastVote(ADD, validator4, validator5)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3, validator4, validator5); // And then start voting to drop it again, but validator1's vote should have been discarded - voteTally.addVote(validator2, validator5, VoteType.DROP); - voteTally.addVote(validator3, validator5, VoteType.DROP); + voteTally.addVote(new CastVote(DROP, validator2, validator5)); + voteTally.addVote(new CastVote(DROP, validator3, validator5)); assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3, validator4, validator5); } @@ -275,25 +277,25 @@ public class VoteTallyTest { @Test public void trackMultipleOngoingVotesIndependently() { final VoteTally voteTally = fourValidators(); - voteTally.addVote(validator1, validator5, VoteType.ADD); - voteTally.addVote(validator1, validator3, VoteType.DROP); + voteTally.addVote(new CastVote(ADD, validator1, validator5)); + voteTally.addVote(new CastVote(DROP, validator1, validator3)); - voteTally.addVote(validator2, validator5, VoteType.ADD); - voteTally.addVote(validator2, validator1, VoteType.DROP); + voteTally.addVote(new CastVote(ADD, validator2, validator5)); + voteTally.addVote(new CastVote(DROP, validator2, validator1)); // Neither vote has enough votes to complete. assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3, validator4); - voteTally.addVote(validator3, validator5, VoteType.ADD); - voteTally.addVote(validator3, validator1, VoteType.DROP); + voteTally.addVote(new CastVote(ADD, validator3, validator5)); + voteTally.addVote(new CastVote(DROP, validator3, validator1)); // Validator 5 now has 3 votes and is added assertThat(voteTally.getCurrentValidators()) .containsExactly(validator1, validator2, validator3, validator4, validator5); - voteTally.addVote(validator4, validator5, VoteType.ADD); - voteTally.addVote(validator4, validator1, VoteType.DROP); + voteTally.addVote(new CastVote(ADD, validator4, validator5)); + voteTally.addVote(new CastVote(DROP, validator4, validator1)); // Validator 1 now gets dropped. assertThat(voteTally.getCurrentValidators()) diff --git a/consensus/clique/src/test/java/tech/pegasys/pantheon/consensus/clique/CliqueVoteTallyUpdaterTest.java b/consensus/common/src/test/java/tech/pegasys/pantheon/consensus/common/VoteTallyUpdaterTest.java similarity index 60% rename from consensus/clique/src/test/java/tech/pegasys/pantheon/consensus/clique/CliqueVoteTallyUpdaterTest.java rename to consensus/common/src/test/java/tech/pegasys/pantheon/consensus/common/VoteTallyUpdaterTest.java index 7c36dc5a91..8cf8bffc8f 100644 --- a/consensus/clique/src/test/java/tech/pegasys/pantheon/consensus/clique/CliqueVoteTallyUpdaterTest.java +++ b/consensus/common/src/test/java/tech/pegasys/pantheon/consensus/common/VoteTallyUpdaterTest.java @@ -10,41 +10,32 @@ * 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.clique; +package tech.pegasys.pantheon.consensus.common; import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; +import static tech.pegasys.pantheon.consensus.common.ValidatorVotePolarity.ADD; -import tech.pegasys.pantheon.consensus.common.EpochManager; -import tech.pegasys.pantheon.consensus.common.VoteTally; -import tech.pegasys.pantheon.consensus.common.VoteType; -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.chain.MutableBlockchain; 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.util.bytes.BytesValue; -import java.math.BigInteger; -import java.util.List; import java.util.Optional; import org.junit.Test; -public class CliqueVoteTallyUpdaterTest { +public class VoteTallyUpdaterTest { private static final long EPOCH_LENGTH = 30_000; - public static final Signature INVALID_SEAL = - Signature.create(BigInteger.ONE, BigInteger.ONE, (byte) 0); private final VoteTally voteTally = mock(VoteTally.class); private final MutableBlockchain blockchain = mock(MutableBlockchain.class); private final KeyPair proposerKeyPair = KeyPair.generate(); @@ -54,30 +45,31 @@ public class CliqueVoteTallyUpdaterTest { private final Address validator1 = Address.fromHexString("00dae27b350bae20c5652124af5d8b5cba001ec1"); - private final CliqueVoteTallyUpdater updater = - new CliqueVoteTallyUpdater(new EpochManager(EPOCH_LENGTH)); + private final VoteBlockInterface serialiser = mock(VoteBlockInterface.class); + + private final VoteTallyUpdater updater = + new VoteTallyUpdater(new EpochManager(EPOCH_LENGTH), serialiser); @Test - public void voteTallyUpdatedWithVoteFromBlock() { + public void voteTallyUpdatedWithAddVote() { + when(serialiser.extractVoteFromHeader(any())) + .thenReturn(Optional.of(new CastVote(ADD, proposerAddress, subject))); + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); - headerBuilder.number(1); - headerBuilder.nonce(VoteType.ADD.getNonceValue()); - headerBuilder.coinbase(subject); - addProposer(headerBuilder); + headerBuilder.number(EPOCH_LENGTH - 1); final BlockHeader header = headerBuilder.buildHeader(); updater.updateForBlock(header, voteTally); - verify(voteTally).addVote(proposerAddress, subject, VoteType.ADD); + verify(voteTally).addVote(new CastVote(ADD, proposerAddress, subject)); } @Test public void voteTallyNotUpdatedWhenBlockHasNoVoteSubject() { + when(serialiser.extractVoteFromHeader(any())).thenReturn(Optional.empty()); + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); - headerBuilder.number(1); - headerBuilder.nonce(VoteType.ADD.getNonceValue()); - headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000")); - addProposer(headerBuilder); + headerBuilder.number(EPOCH_LENGTH - 1); final BlockHeader header = headerBuilder.buildHeader(); updater.updateForBlock(header, voteTally); @@ -87,11 +79,10 @@ public class CliqueVoteTallyUpdaterTest { @Test public void outstandingVotesDiscardedWhenEpochReached() { + when(serialiser.extractVoteFromHeader(any())).thenReturn(Optional.empty()); + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); headerBuilder.number(EPOCH_LENGTH); - headerBuilder.nonce(VoteType.ADD.getNonceValue()); - headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000")); - addProposer(headerBuilder); final BlockHeader header = headerBuilder.buildHeader(); updater.updateForBlock(header, voteTally); @@ -102,11 +93,9 @@ public class CliqueVoteTallyUpdaterTest { @Test public void buildVoteTallyByExtractingValidatorsFromGenesisBlock() { + when(serialiser.validatorsInBlock(any())).thenReturn(asList(subject, validator1)); final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); headerBuilder.number(0); - headerBuilder.nonce(VoteType.ADD.getNonceValue()); - headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000")); - addProposer(headerBuilder, asList(subject, validator1)); final BlockHeader header = headerBuilder.buildHeader(); when(blockchain.getChainHeadBlockNumber()).thenReturn(EPOCH_LENGTH); @@ -118,11 +107,8 @@ public class CliqueVoteTallyUpdaterTest { @Test public void buildVoteTallyByExtractingValidatorsFromEpochBlock() { + when(serialiser.validatorsInBlock(any())).thenReturn(asList(subject, validator1)); final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); - headerBuilder.number(EPOCH_LENGTH); - headerBuilder.nonce(VoteType.ADD.getNonceValue()); - headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000")); - addProposer(headerBuilder, asList(subject, validator1)); final BlockHeader header = headerBuilder.buildHeader(); when(blockchain.getChainHeadBlockNumber()).thenReturn(EPOCH_LENGTH); @@ -134,15 +120,15 @@ public class CliqueVoteTallyUpdaterTest { @Test public void addVotesFromBlocksAfterMostRecentEpoch() { + when(serialiser.validatorsInBlock(any())).thenReturn(asList(validator1)); + when(serialiser.extractVoteFromHeader(any())) + .thenReturn(Optional.of(new CastVote(ADD, proposerAddress, subject))); + final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); headerBuilder.number(EPOCH_LENGTH); - headerBuilder.nonce(VoteType.ADD.getNonceValue()); - headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000")); - addProposer(headerBuilder, singletonList(validator1)); final BlockHeader epochHeader = headerBuilder.buildHeader(); headerBuilder.number(EPOCH_LENGTH + 1); - headerBuilder.coinbase(subject); final BlockHeader voteBlockHeader = headerBuilder.buildHeader(); when(blockchain.getChainHeadBlockNumber()).thenReturn(EPOCH_LENGTH + 1); @@ -152,32 +138,4 @@ public class CliqueVoteTallyUpdaterTest { final VoteTally voteTally = updater.buildVoteTallyFromBlockchain(blockchain); assertThat(voteTally.getCurrentValidators()).containsExactly(subject, validator1); } - - private void addProposer(final BlockHeaderTestFixture builder) { - addProposer(builder, singletonList(proposerAddress)); - } - - private void addProposer(final BlockHeaderTestFixture builder, final List
validators) { - - final CliqueExtraData initialIbftExtraData = - new CliqueExtraData( - BytesValue.wrap(new byte[CliqueExtraData.EXTRA_VANITY_LENGTH]), - INVALID_SEAL, - validators); - - builder.extraData(initialIbftExtraData.encode()); - final BlockHeader header = builder.buildHeader(); - final Hash proposerSealHash = - CliqueBlockHashing.calculateDataHashForProposerSeal(header, initialIbftExtraData); - - final Signature proposerSignature = SECP256K1.sign(proposerSealHash, proposerKeyPair); - - final CliqueExtraData sealedData = - new CliqueExtraData( - BytesValue.wrap(new byte[CliqueExtraData.EXTRA_VANITY_LENGTH]), - proposerSignature, - validators); - - builder.extraData(sealedData.encode()); - } } diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockImporter.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockImporter.java index d535c09349..125d721d30 100644 --- a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockImporter.java +++ b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockImporter.java @@ -12,6 +12,7 @@ */ package tech.pegasys.pantheon.consensus.ibft; +import tech.pegasys.pantheon.consensus.common.VoteTallyUpdater; import tech.pegasys.pantheon.ethereum.ProtocolContext; import tech.pegasys.pantheon.ethereum.core.Block; import tech.pegasys.pantheon.ethereum.core.BlockHeader; diff --git a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/VoteTallyUpdater.java b/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/VoteTallyUpdater.java deleted file mode 100644 index 3c1b38485a..0000000000 --- a/consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/VoteTallyUpdater.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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.common.VoteTally; -import tech.pegasys.pantheon.ethereum.chain.Blockchain; -import tech.pegasys.pantheon.ethereum.core.BlockHeader; - -public interface VoteTallyUpdater { - /** - * Create a new VoteTally based on the current blockchain state. - * - * @param blockchain the blockchain to load the current state from - * @return a VoteTally reflecting the state of the blockchain head - */ - VoteTally buildVoteTallyFromBlockchain(final Blockchain blockchain); - - /** - * Update the vote tally to reflect changes caused by appending a new block to the chain. - * - * @param header the header of the block being added - * @param voteTally the vote tally to update - */ - void updateForBlock(final BlockHeader header, final VoteTally voteTally); -} diff --git a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockImporterTest.java b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockImporterTest.java index 9eefe49f97..ccb5354ead 100644 --- a/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockImporterTest.java +++ b/consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/IbftBlockImporterTest.java @@ -20,6 +20,7 @@ import static org.mockito.Mockito.when; import tech.pegasys.pantheon.consensus.common.VoteProposer; import tech.pegasys.pantheon.consensus.common.VoteTally; +import tech.pegasys.pantheon.consensus.common.VoteTallyUpdater; import tech.pegasys.pantheon.ethereum.ProtocolContext; import tech.pegasys.pantheon.ethereum.chain.MutableBlockchain; import tech.pegasys.pantheon.ethereum.core.Block; diff --git a/consensus/ibftlegacy/src/main/java/tech/pegasys/pantheon/consensus/ibftlegacy/IbftLegacyVotingBlockInterface.java b/consensus/ibftlegacy/src/main/java/tech/pegasys/pantheon/consensus/ibftlegacy/IbftLegacyVotingBlockInterface.java new file mode 100644 index 0000000000..e771bf65eb --- /dev/null +++ b/consensus/ibftlegacy/src/main/java/tech/pegasys/pantheon/consensus/ibftlegacy/IbftLegacyVotingBlockInterface.java @@ -0,0 +1,70 @@ +/* + * 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.ibftlegacy; + +import tech.pegasys.pantheon.consensus.common.CastVote; +import tech.pegasys.pantheon.consensus.common.ValidatorVotePolarity; +import tech.pegasys.pantheon.consensus.common.VoteBlockInterface; +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.util.bytes.BytesValue; + +import java.util.List; +import java.util.Optional; + +import com.google.common.collect.ImmutableBiMap; + +public class IbftLegacyVotingBlockInterface implements VoteBlockInterface { + + private static final Address NO_VOTE_SUBJECT = + Address.wrap(BytesValue.wrap(new byte[Address.SIZE])); + + private static final ImmutableBiMap voteToValue = + ImmutableBiMap.of( + ValidatorVotePolarity.ADD, 0xFFFFFFFFFFFFFFFFL, + ValidatorVotePolarity.DROP, 0x0L); + + @Override + public Optional extractVoteFromHeader(final BlockHeader header) { + final Address candidate = header.getCoinbase(); + if (!candidate.equals(NO_VOTE_SUBJECT)) { + final IbftExtraData ibftExtraData = IbftExtraData.decode(header.getExtraData()); + final Address proposer = IbftBlockHashing.recoverProposerAddress(header, ibftExtraData); + final ValidatorVotePolarity votePolarity = voteToValue.inverse().get(header.getNonce()); + final Address recipient = header.getCoinbase(); + + return Optional.of(new CastVote(votePolarity, proposer, recipient)); + } + return Optional.empty(); + } + + @Override + public BlockHeaderBuilder insertVoteToHeaderBuilder( + final BlockHeaderBuilder builder, final Optional vote) { + if (vote.isPresent()) { + final CastVote voteToCast = vote.get(); + builder.nonce(voteToValue.get(voteToCast.getVotePolarity())); + builder.coinbase(voteToCast.getRecipient()); + } else { + builder.nonce(voteToValue.get(ValidatorVotePolarity.DROP)); + builder.coinbase(NO_VOTE_SUBJECT); + } + return builder; + } + + @Override + public List
validatorsInBlock(final BlockHeader header) { + return IbftExtraData.decode(header.getExtraData()).getValidators(); + } +} diff --git a/consensus/ibftlegacy/src/main/java/tech/pegasys/pantheon/consensus/ibftlegacy/IbftProtocolSchedule.java b/consensus/ibftlegacy/src/main/java/tech/pegasys/pantheon/consensus/ibftlegacy/IbftProtocolSchedule.java index f828a4b1d9..4e06499425 100644 --- a/consensus/ibftlegacy/src/main/java/tech/pegasys/pantheon/consensus/ibftlegacy/IbftProtocolSchedule.java +++ b/consensus/ibftlegacy/src/main/java/tech/pegasys/pantheon/consensus/ibftlegacy/IbftProtocolSchedule.java @@ -17,6 +17,7 @@ import static tech.pegasys.pantheon.consensus.ibftlegacy.IbftBlockHeaderValidati import tech.pegasys.pantheon.config.GenesisConfigOptions; import tech.pegasys.pantheon.config.IbftConfigOptions; import tech.pegasys.pantheon.consensus.common.EpochManager; +import tech.pegasys.pantheon.consensus.common.VoteTallyUpdater; import tech.pegasys.pantheon.consensus.ibft.IbftBlockImporter; import tech.pegasys.pantheon.consensus.ibft.IbftContext; import tech.pegasys.pantheon.ethereum.core.Wei; @@ -59,7 +60,7 @@ public class IbftProtocolSchedule { new IbftBlockImporter( new MainnetBlockImporter<>( blockHeaderValidator, blockBodyValidator, blockProcessor), - new IbftVoteTallyUpdater(epochManager)), + new VoteTallyUpdater(epochManager, new IbftLegacyVotingBlockInterface())), (time, parent, protocolContext) -> BigInteger.ONE) .blockReward(Wei.ZERO) .blockHashFunction(IbftBlockHashing::calculateHashOfIbftBlockOnChain); diff --git a/consensus/ibftlegacy/src/test/java/tech/pegasys/pantheon/consensus/ibftlegacy/IbftLegacyVotingBlockInterfaceTest.java b/consensus/ibftlegacy/src/test/java/tech/pegasys/pantheon/consensus/ibftlegacy/IbftLegacyVotingBlockInterfaceTest.java new file mode 100644 index 0000000000..779e7bfa33 --- /dev/null +++ b/consensus/ibftlegacy/src/test/java/tech/pegasys/pantheon/consensus/ibftlegacy/IbftLegacyVotingBlockInterfaceTest.java @@ -0,0 +1,127 @@ +/* + * 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.ibftlegacy; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static tech.pegasys.pantheon.consensus.common.ValidatorVotePolarity.ADD; +import static tech.pegasys.pantheon.consensus.common.ValidatorVotePolarity.DROP; + +import tech.pegasys.pantheon.consensus.common.CastVote; +import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.AddressHelpers; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.BlockHeaderBuilder; +import tech.pegasys.pantheon.ethereum.core.BlockHeaderTestFixture; +import tech.pegasys.pantheon.ethereum.core.Util; +import tech.pegasys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; + +import java.util.List; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; + +public class IbftLegacyVotingBlockInterfaceTest { + + private static final KeyPair proposerKeys = KeyPair.generate(); + private static final Address proposerAddress = + Util.publicKeyToAddress(proposerKeys.getPublicKey()); + private static final List
validatorList = singletonList(proposerAddress); + + private final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); + private final IbftLegacyVotingBlockInterface blockInterface = + new IbftLegacyVotingBlockInterface(); + private final BlockHeaderBuilder builder = + BlockHeaderBuilder.fromHeader(headerBuilder.buildHeader()) + .blockHashFunction(MainnetBlockHashFunction::createHash); + + @Before + public void setup() { + // must set "number" to ensure extradata is correctly deserialised during hashing. + headerBuilder.coinbase(AddressHelpers.ofValue(0)).number(1); + } + + @Test + public void headerWithZeroCoinbaseReturnsAnEmptyVote() { + assertThat(blockInterface.extractVoteFromHeader(headerBuilder.buildHeader())).isEmpty(); + } + + @Test + public void headerWithNonceOfZeroReportsDropVote() { + headerBuilder.nonce(0x0L).coinbase(AddressHelpers.ofValue(2)); + final BlockHeader header = + TestHelpers.createIbftSignedBlockHeader(headerBuilder, proposerKeys, validatorList); + final Optional extractedVote = blockInterface.extractVoteFromHeader(header); + + assertThat(extractedVote).contains(new CastVote(DROP, proposerAddress, header.getCoinbase())); + } + + @Test + public void headerWithNonceOfMaxLongReportsAddVote() { + headerBuilder.nonce(0xFFFFFFFFFFFFFFFFL).coinbase(AddressHelpers.ofValue(2)); + + final BlockHeader header = + TestHelpers.createIbftSignedBlockHeader(headerBuilder, proposerKeys, validatorList); + final Optional extractedVote = blockInterface.extractVoteFromHeader(header); + + assertThat(extractedVote).contains(new CastVote(ADD, proposerAddress, header.getCoinbase())); + } + + @Test + public void blendingAddVoteToHeaderResultsInHeaderWithNonceOfMaxLong() { + final CastVote vote = new CastVote(ADD, AddressHelpers.ofValue(1), AddressHelpers.ofValue(2)); + final BlockHeaderBuilder builderWithVote = + blockInterface.insertVoteToHeaderBuilder(builder, Optional.of(vote)); + + final BlockHeader header = builderWithVote.buildBlockHeader(); + + assertThat(header.getCoinbase()).isEqualTo(vote.getRecipient()); + assertThat(header.getNonce()).isEqualTo(0xFFFFFFFFFFFFFFFFL); + } + + @Test + public void blendingDropVoteToHeaderResultsInHeaderWithNonceOfZero() { + final CastVote vote = new CastVote(DROP, AddressHelpers.ofValue(1), AddressHelpers.ofValue(2)); + final BlockHeaderBuilder builderWithVote = + blockInterface.insertVoteToHeaderBuilder(builder, Optional.of(vote)); + + final BlockHeader header = builderWithVote.buildBlockHeader(); + + assertThat(header.getCoinbase()).isEqualTo(vote.getRecipient()); + assertThat(header.getNonce()).isEqualTo(0x0L); + } + + @Test + public void nonVoteBlendedIntoHeaderResultsInACoinbaseOfZero() { + final BlockHeaderBuilder builderWithVote = + blockInterface.insertVoteToHeaderBuilder(builder, Optional.empty()); + + final BlockHeader header = builderWithVote.buildBlockHeader(); + + assertThat(header.getCoinbase()).isEqualTo(AddressHelpers.ofValue(0)); + assertThat(header.getNonce()).isEqualTo(0x0L); + } + + @Test + public void extractsValidatorsFromHeader() { + final BlockHeader header = + TestHelpers.createIbftSignedBlockHeader(headerBuilder, proposerKeys, validatorList); + + final IbftLegacyVotingBlockInterface serDeser = new IbftLegacyVotingBlockInterface(); + final List
extractedValidators = serDeser.validatorsInBlock(header); + + assertThat(extractedValidators).isEqualTo(validatorList); + } +} diff --git a/consensus/ibftlegacy/src/test/java/tech/pegasys/pantheon/consensus/ibftlegacy/IbftVoteTallyUpdaterTest.java b/consensus/ibftlegacy/src/test/java/tech/pegasys/pantheon/consensus/ibftlegacy/IbftVoteTallyUpdaterTest.java deleted file mode 100644 index ab05a7d7c3..0000000000 --- a/consensus/ibftlegacy/src/test/java/tech/pegasys/pantheon/consensus/ibftlegacy/IbftVoteTallyUpdaterTest.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * 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.ibftlegacy; - -import static java.util.Arrays.asList; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; - -import tech.pegasys.pantheon.consensus.common.EpochManager; -import tech.pegasys.pantheon.consensus.common.VoteTally; -import tech.pegasys.pantheon.consensus.common.VoteType; -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.chain.MutableBlockchain; -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.util.bytes.BytesValue; - -import java.math.BigInteger; -import java.util.List; -import java.util.Optional; - -import org.junit.Test; - -public class IbftVoteTallyUpdaterTest { - - private static final long EPOCH_LENGTH = 30_000; - public static final Signature INVALID_SEAL = - Signature.create(BigInteger.ONE, BigInteger.ONE, (byte) 0); - private final VoteTally voteTally = mock(VoteTally.class); - private final MutableBlockchain blockchain = mock(MutableBlockchain.class); - private final KeyPair proposerKeyPair = KeyPair.generate(); - private final Address proposerAddress = - Address.extract(Hash.hash(proposerKeyPair.getPublicKey().getEncodedBytes())); - private final Address subject = Address.fromHexString("007f4a23ca00cd043d25c2888c1aa5688f81a344"); - private final Address validator1 = - Address.fromHexString("00dae27b350bae20c5652124af5d8b5cba001ec1"); - - private final IbftVoteTallyUpdater updater = - new IbftVoteTallyUpdater(new EpochManager(EPOCH_LENGTH)); - - @Test - public void voteTallyUpdatedWithVoteFromBlock() { - final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); - headerBuilder.number(1); - headerBuilder.nonce(VoteType.ADD.getNonceValue()); - headerBuilder.coinbase(subject); - addProposer(headerBuilder); - final BlockHeader header = headerBuilder.buildHeader(); - - updater.updateForBlock(header, voteTally); - - verify(voteTally).addVote(proposerAddress, subject, VoteType.ADD); - } - - @Test - public void voteTallyNotUpdatedWhenBlockHasNoVoteSubject() { - final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); - headerBuilder.number(1); - headerBuilder.nonce(VoteType.ADD.getNonceValue()); - headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000")); - addProposer(headerBuilder); - final BlockHeader header = headerBuilder.buildHeader(); - - updater.updateForBlock(header, voteTally); - - verifyZeroInteractions(voteTally); - } - - @Test - public void outstandingVotesDiscardedWhenEpochReached() { - final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); - headerBuilder.number(EPOCH_LENGTH); - headerBuilder.nonce(VoteType.ADD.getNonceValue()); - headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000")); - addProposer(headerBuilder); - final BlockHeader header = headerBuilder.buildHeader(); - - updater.updateForBlock(header, voteTally); - - verify(voteTally).discardOutstandingVotes(); - verifyNoMoreInteractions(voteTally); - } - - @Test - public void buildVoteTallyByExtractingValidatorsFromGenesisBlock() { - final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); - headerBuilder.number(0); - headerBuilder.nonce(VoteType.ADD.getNonceValue()); - headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000")); - addProposer(headerBuilder, asList(subject, validator1)); - final BlockHeader header = headerBuilder.buildHeader(); - - when(blockchain.getChainHeadBlockNumber()).thenReturn(EPOCH_LENGTH); - when(blockchain.getBlockHeader(EPOCH_LENGTH)).thenReturn(Optional.of(header)); - - final VoteTally voteTally = updater.buildVoteTallyFromBlockchain(blockchain); - assertThat(voteTally.getCurrentValidators()).containsExactly(subject, validator1); - } - - @Test - public void buildVoteTallyByExtractingValidatorsFromEpochBlock() { - final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); - headerBuilder.number(EPOCH_LENGTH); - headerBuilder.nonce(VoteType.ADD.getNonceValue()); - headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000")); - addProposer(headerBuilder, asList(subject, validator1)); - final BlockHeader header = headerBuilder.buildHeader(); - - when(blockchain.getChainHeadBlockNumber()).thenReturn(EPOCH_LENGTH); - when(blockchain.getBlockHeader(EPOCH_LENGTH)).thenReturn(Optional.of(header)); - - final VoteTally voteTally = updater.buildVoteTallyFromBlockchain(blockchain); - assertThat(voteTally.getCurrentValidators()).containsExactly(subject, validator1); - } - - @Test - public void addVotesFromBlocksAfterMostRecentEpoch() { - final BlockHeaderTestFixture headerBuilder = new BlockHeaderTestFixture(); - headerBuilder.number(EPOCH_LENGTH); - headerBuilder.nonce(VoteType.ADD.getNonceValue()); - headerBuilder.coinbase(Address.fromHexString("0000000000000000000000000000000000000000")); - addProposer(headerBuilder, singletonList(validator1)); - final BlockHeader epochHeader = headerBuilder.buildHeader(); - - headerBuilder.number(EPOCH_LENGTH + 1); - headerBuilder.coinbase(subject); - final BlockHeader voteBlockHeader = headerBuilder.buildHeader(); - - when(blockchain.getChainHeadBlockNumber()).thenReturn(EPOCH_LENGTH + 1); - when(blockchain.getBlockHeader(EPOCH_LENGTH)).thenReturn(Optional.of(epochHeader)); - when(blockchain.getBlockHeader(EPOCH_LENGTH + 1)).thenReturn(Optional.of(voteBlockHeader)); - - final VoteTally voteTally = updater.buildVoteTallyFromBlockchain(blockchain); - assertThat(voteTally.getCurrentValidators()).containsExactly(subject, validator1); - } - - private void addProposer(final BlockHeaderTestFixture builder) { - addProposer(builder, singletonList(proposerAddress)); - } - - private void addProposer(final BlockHeaderTestFixture builder, final List
validators) { - - final IbftExtraData initialIbftExtraData = - new IbftExtraData( - BytesValue.wrap(new byte[IbftExtraData.EXTRA_VANITY_LENGTH]), - emptyList(), - INVALID_SEAL, - validators); - - builder.extraData(initialIbftExtraData.encode()); - final BlockHeader header = builder.buildHeader(); - final Hash proposerSealHash = - IbftBlockHashing.calculateDataHashForProposerSeal(header, initialIbftExtraData); - - final Signature proposerSignature = SECP256K1.sign(proposerSealHash, proposerKeyPair); - - final IbftExtraData proposedData = - new IbftExtraData( - BytesValue.wrap(new byte[IbftExtraData.EXTRA_VANITY_LENGTH]), - singletonList(proposerSignature), - proposerSignature, - validators); - - final Hash headerHashForCommitters = - IbftBlockHashing.calculateDataHashForCommittedSeal(header, proposedData); - final Signature proposerAsCommitterSignature = - SECP256K1.sign(headerHashForCommitters, proposerKeyPair); - - final IbftExtraData sealedData = - new IbftExtraData( - BytesValue.wrap(new byte[IbftExtraData.EXTRA_VANITY_LENGTH]), - singletonList(proposerAsCommitterSignature), - proposerSignature, - validators); - - builder.extraData(sealedData.encode()); - } -} diff --git a/consensus/ibftlegacy/src/test/java/tech/pegasys/pantheon/consensus/ibftlegacy/TestHelpers.java b/consensus/ibftlegacy/src/test/java/tech/pegasys/pantheon/consensus/ibftlegacy/TestHelpers.java new file mode 100644 index 0000000000..266c502aaa --- /dev/null +++ b/consensus/ibftlegacy/src/test/java/tech/pegasys/pantheon/consensus/ibftlegacy/TestHelpers.java @@ -0,0 +1,55 @@ +/* + * 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.ibftlegacy; + +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.BlockHeaderTestFixture; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.util.Collections; +import java.util.List; + +public class TestHelpers { + + public static BlockHeader createIbftSignedBlockHeader( + final BlockHeaderTestFixture blockHeaderBuilder, + final KeyPair signer, + final List
validators) { + + final IbftExtraData unsignedExtraData = + new IbftExtraData(BytesValue.wrap(new byte[32]), Collections.emptyList(), null, validators); + blockHeaderBuilder.extraData(unsignedExtraData.encode()); + + final Hash signingHash = + IbftBlockHashing.calculateDataHashForProposerSeal( + blockHeaderBuilder.buildHeader(), unsignedExtraData); + + final Signature proposerSignature = SECP256K1.sign(signingHash, signer); + + final IbftExtraData signedExtraData = + new IbftExtraData( + unsignedExtraData.getVanityData(), + unsignedExtraData.getSeals(), + proposerSignature, + unsignedExtraData.getValidators()); + + blockHeaderBuilder.extraData(signedExtraData.encode()); + + return blockHeaderBuilder.buildHeader(); + } +} diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/controller/CliquePantheonController.java b/pantheon/src/main/java/tech/pegasys/pantheon/controller/CliquePantheonController.java index 63debc2eb1..40fd3e4294 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/controller/CliquePantheonController.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/controller/CliquePantheonController.java @@ -19,13 +19,14 @@ import tech.pegasys.pantheon.config.GenesisConfigFile; import tech.pegasys.pantheon.config.GenesisConfigOptions; import tech.pegasys.pantheon.consensus.clique.CliqueContext; import tech.pegasys.pantheon.consensus.clique.CliqueProtocolSchedule; -import tech.pegasys.pantheon.consensus.clique.CliqueVoteTallyUpdater; +import tech.pegasys.pantheon.consensus.clique.CliqueVotingBlockInterface; import tech.pegasys.pantheon.consensus.clique.VoteTallyCache; import tech.pegasys.pantheon.consensus.clique.blockcreation.CliqueBlockScheduler; import tech.pegasys.pantheon.consensus.clique.blockcreation.CliqueMinerExecutor; import tech.pegasys.pantheon.consensus.clique.blockcreation.CliqueMiningCoordinator; import tech.pegasys.pantheon.consensus.common.EpochManager; import tech.pegasys.pantheon.consensus.common.VoteProposer; +import tech.pegasys.pantheon.consensus.common.VoteTallyUpdater; import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; import tech.pegasys.pantheon.ethereum.ProtocolContext; import tech.pegasys.pantheon.ethereum.blockcreation.MiningCoordinator; @@ -138,7 +139,9 @@ public class CliquePantheonController implements PantheonController { final EpochManager epochManager = new EpochManager(ibftConfig.getEpochLength()); final VoteTally voteTally = - new IbftVoteTallyUpdater(epochManager).buildVoteTallyFromBlockchain(blockchain); + new VoteTallyUpdater(epochManager, new IbftLegacyVotingBlockInterface()) + .buildVoteTallyFromBlockchain(blockchain); final VoteProposer voteProposer = new VoteProposer();