mirror of https://github.com/hyperledger/besu
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. Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>pull/2/head
parent
b43ff0f30e
commit
9952b658ac
@ -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<Address> 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()); |
||||
} |
||||
} |
||||
} |
@ -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<ValidatorVotePolarity, Long> voteToValue = |
||||
ImmutableBiMap.of( |
||||
ValidatorVotePolarity.ADD, 0xFFFFFFFFFFFFFFFFL, |
||||
ValidatorVotePolarity.DROP, 0x0L); |
||||
|
||||
@Override |
||||
public Optional<CastVote> 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<CastVote> 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<Address> validatorsInBlock(final BlockHeader header) { |
||||
return CliqueExtraData.decode(header.getExtraData()).getValidators(); |
||||
} |
||||
} |
@ -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<Address> 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<CastVote> 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<CastVote> 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<Address> extractedValidators = blockInterface.validatorsInBlock(header); |
||||
|
||||
assertThat(extractedValidators).isEqualTo(validatorList); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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<CastVote> extractVoteFromHeader(final BlockHeader header); |
||||
|
||||
BlockHeaderBuilder insertVoteToHeaderBuilder( |
||||
final BlockHeaderBuilder builder, final Optional<CastVote> vote); |
||||
|
||||
List<Address> validatorsInBlock(final BlockHeader header); |
||||
} |
@ -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); |
||||
} |
@ -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<ValidatorVotePolarity, Long> voteToValue = |
||||
ImmutableBiMap.of( |
||||
ValidatorVotePolarity.ADD, 0xFFFFFFFFFFFFFFFFL, |
||||
ValidatorVotePolarity.DROP, 0x0L); |
||||
|
||||
@Override |
||||
public Optional<CastVote> 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<CastVote> 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<Address> validatorsInBlock(final BlockHeader header) { |
||||
return IbftExtraData.decode(header.getExtraData()).getValidators(); |
||||
} |
||||
} |
@ -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<Address> 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<CastVote> 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<CastVote> 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<Address> extractedValidators = serDeser.validatorsInBlock(header); |
||||
|
||||
assertThat(extractedValidators).isEqualTo(validatorList); |
||||
} |
||||
} |
@ -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<Address> 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()); |
||||
} |
||||
} |
@ -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<Address> 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(); |
||||
} |
||||
} |
Loading…
Reference in new issue