Ibft Consensus Round Classes (#405)

The IbftRound is responsible for sequencing the transmission of
network packets based on received data.
tmohay 6 years ago committed by GitHub
parent 24aac90efb
commit f99457bb3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      consensus/ibft/build.gradle
  2. 7
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftExtraData.java
  3. 28
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftHelpers.java
  4. 6
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftFinalState.java
  5. 103
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftMessageTransmitter.java
  6. 187
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftRound.java
  7. 73
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftRoundFactory.java
  8. 21
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/RoundState.java
  9. 251
      consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftRoundTest.java
  10. 18
      consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/statemachine/RoundStateTest.java

@ -41,6 +41,7 @@ dependencies {
implementation 'com.google.guava:guava'
testImplementation project(path: ':ethereum:core', configuration: 'testSupportArtifacts')
testImplementation project(path: ':config:', configuration:'testSupportArtifacts')
testImplementation 'junit:junit'
testImplementation 'org.awaitility:awaitility'

@ -22,6 +22,7 @@ import tech.pegasys.pantheon.ethereum.rlp.BytesValueRLPOutput;
import tech.pegasys.pantheon.ethereum.rlp.RLPInput;
import tech.pegasys.pantheon.util.bytes.BytesValue;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
@ -34,14 +35,14 @@ public class IbftExtraData {
public static final int EXTRA_VANITY_LENGTH = 32;
private final BytesValue vanityData;
private final List<Signature> seals;
private final Collection<Signature> seals;
private final Optional<Vote> vote;
private final int round;
private final List<Address> validators;
public IbftExtraData(
final BytesValue vanityData,
final List<Signature> seals,
final Collection<Signature> seals,
final Optional<Vote> vote,
final int round,
final List<Address> validators) {
@ -127,7 +128,7 @@ public class IbftExtraData {
return vanityData;
}
public List<Signature> getSeals() {
public Collection<Signature> getSeals() {
return seals;
}

@ -12,9 +12,15 @@
*/
package tech.pegasys.pantheon.consensus.ibft;
import tech.pegasys.pantheon.crypto.SECP256K1.Signature;
import tech.pegasys.pantheon.ethereum.core.Block;
import tech.pegasys.pantheon.ethereum.core.BlockHeader;
import tech.pegasys.pantheon.ethereum.core.BlockHeaderBuilder;
import tech.pegasys.pantheon.ethereum.core.Hash;
import tech.pegasys.pantheon.ethereum.core.Util;
import java.util.Collection;
public class IbftHelpers {
public static final Hash EXPECTED_MIX_HASH =
@ -23,4 +29,26 @@ public class IbftHelpers {
public static int calculateRequiredValidatorQuorum(final int validatorCount) {
return Util.fastDivCeiling(2 * validatorCount, 3);
}
public static Block createSealedBlock(
final Block block, final Collection<Signature> commitSeals) {
final BlockHeader initialHeader = block.getHeader();
final IbftExtraData initialExtraData = IbftExtraData.decode(initialHeader.getExtraData());
final IbftExtraData sealedExtraData =
new IbftExtraData(
initialExtraData.getVanityData(),
commitSeals,
initialExtraData.getVote(),
initialExtraData.getRound(),
initialExtraData.getValidators());
final BlockHeader sealedHeader =
BlockHeaderBuilder.fromHeader(initialHeader)
.extraData(sealedExtraData.encode())
.blockHashFunction(IbftBlockHashing::calculateHashOfIbftBlockOnChain)
.buildBlockHeader();
return new Block(sealedHeader, block.getBody());
}
}

@ -41,6 +41,7 @@ public class IbftFinalState {
private final IbftBlockCreatorFactory blockCreatorFactory;
private final MessageFactory messageFactory;
private final BlockHeaderValidator<IbftContext> ibftContextBlockHeaderValidator;
private final IbftMessageTransmitter messageTransmitter;
public IbftFinalState(
final ValidatorProvider validatorProvider,
@ -63,6 +64,7 @@ public class IbftFinalState {
this.blockCreatorFactory = blockCreatorFactory;
this.messageFactory = messageFactory;
this.ibftContextBlockHeaderValidator = ibftContextBlockHeaderValidator;
this.messageTransmitter = new IbftMessageTransmitter(messageFactory, peers);
}
public int getQuorumSize() {
@ -116,4 +118,8 @@ public class IbftFinalState {
public BlockHeaderValidator<IbftContext> getBlockHeaderValidator() {
return ibftContextBlockHeaderValidator;
}
public IbftMessageTransmitter getTransmitter() {
return messageTransmitter;
}
}

@ -0,0 +1,103 @@
/*
* 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.statemachine;
import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier;
import tech.pegasys.pantheon.consensus.ibft.ibftmessage.CommitMessage;
import tech.pegasys.pantheon.consensus.ibft.ibftmessage.NewRoundMessage;
import tech.pegasys.pantheon.consensus.ibft.ibftmessage.PrepareMessage;
import tech.pegasys.pantheon.consensus.ibft.ibftmessage.ProposalMessage;
import tech.pegasys.pantheon.consensus.ibft.ibftmessage.RoundChangeMessage;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.CommitPayload;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.MessageFactory;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.NewRoundPayload;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.PreparePayload;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.PreparedCertificate;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.ProposalPayload;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.RoundChangeCertificate;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.RoundChangePayload;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.SignedData;
import tech.pegasys.pantheon.consensus.ibft.network.IbftNetworkPeers;
import tech.pegasys.pantheon.crypto.SECP256K1.Signature;
import tech.pegasys.pantheon.ethereum.core.Block;
import tech.pegasys.pantheon.ethereum.core.Hash;
import java.util.Optional;
public class IbftMessageTransmitter {
private final MessageFactory messageFactory;
private final IbftNetworkPeers networkPeers;
public IbftMessageTransmitter(
final MessageFactory messageFactory, final IbftNetworkPeers networkPeers) {
this.messageFactory = messageFactory;
this.networkPeers = networkPeers;
}
public void multicastProposal(final ConsensusRoundIdentifier roundIdentifier, final Block block) {
final SignedData<ProposalPayload> signedPayload =
messageFactory.createSignedProposalPayload(roundIdentifier, block);
final ProposalMessage message = ProposalMessage.create(signedPayload);
networkPeers.multicastToValidators(message);
}
public void multicastPrepare(final ConsensusRoundIdentifier roundIdentifier, final Hash digest) {
final SignedData<PreparePayload> signedPayload =
messageFactory.createSignedPreparePayload(roundIdentifier, digest);
final PrepareMessage message = PrepareMessage.create(signedPayload);
networkPeers.multicastToValidators(message);
}
public void multicastCommit(
final ConsensusRoundIdentifier roundIdentifier,
final Hash digest,
final Signature commitSeal) {
final SignedData<CommitPayload> signedPayload =
messageFactory.createSignedCommitPayload(roundIdentifier, digest, commitSeal);
final CommitMessage message = CommitMessage.create(signedPayload);
networkPeers.multicastToValidators(message);
}
public void multicastRoundChange(
final ConsensusRoundIdentifier roundIdentifier,
final Optional<PreparedCertificate> preparedCertificate) {
final SignedData<RoundChangePayload> signedPayload =
messageFactory.createSignedRoundChangePayload(roundIdentifier, preparedCertificate);
final RoundChangeMessage message = RoundChangeMessage.create(signedPayload);
networkPeers.multicastToValidators(message);
}
public void multicastNewRound(
final ConsensusRoundIdentifier roundIdentifier,
final RoundChangeCertificate roundChangeCertificate,
final SignedData<ProposalPayload> proposalPayload) {
final SignedData<NewRoundPayload> signedPayload =
messageFactory.createSignedNewRoundPayload(
roundIdentifier, roundChangeCertificate, proposalPayload);
final NewRoundMessage message = NewRoundMessage.create(signedPayload);
networkPeers.multicastToValidators(message);
}
}

@ -0,0 +1,187 @@
/*
* 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.statemachine;
import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier;
import tech.pegasys.pantheon.consensus.ibft.IbftBlockHashing;
import tech.pegasys.pantheon.consensus.ibft.IbftContext;
import tech.pegasys.pantheon.consensus.ibft.IbftExtraData;
import tech.pegasys.pantheon.consensus.ibft.IbftHelpers;
import tech.pegasys.pantheon.consensus.ibft.blockcreation.IbftBlockCreator;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.CommitPayload;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.MessageFactory;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.PreparePayload;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.PreparedCertificate;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.ProposalPayload;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.SignedData;
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.ProtocolContext;
import tech.pegasys.pantheon.ethereum.chain.MinedBlockObserver;
import tech.pegasys.pantheon.ethereum.core.Block;
import tech.pegasys.pantheon.ethereum.core.BlockHeader;
import tech.pegasys.pantheon.ethereum.core.BlockImporter;
import tech.pegasys.pantheon.ethereum.core.Hash;
import tech.pegasys.pantheon.ethereum.mainnet.HeaderValidationMode;
import tech.pegasys.pantheon.util.Subscribers;
import java.util.Optional;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class IbftRound {
private static final Logger LOG = LogManager.getLogger();
private final Subscribers<MinedBlockObserver> observers;
private final RoundState roundState;
private final IbftBlockCreator blockCreator;
private final ProtocolContext<IbftContext> protocolContext;
private final BlockImporter<IbftContext> blockImporter;
private final KeyPair nodeKeys;
private final MessageFactory messageFactory; // used only to create stored local msgs
private final IbftMessageTransmitter transmitter;
public IbftRound(
final RoundState roundState,
final IbftBlockCreator blockCreator,
final ProtocolContext<IbftContext> protocolContext,
final BlockImporter<IbftContext> blockImporter,
final Subscribers<MinedBlockObserver> observers,
final KeyPair nodeKeys,
final MessageFactory messageFactory,
final IbftMessageTransmitter transmitter) {
this.roundState = roundState;
this.blockCreator = blockCreator;
this.protocolContext = protocolContext;
this.blockImporter = blockImporter;
this.observers = observers;
this.nodeKeys = nodeKeys;
this.messageFactory = messageFactory;
this.transmitter = transmitter;
}
public ConsensusRoundIdentifier getRoundIdentifier() {
return roundState.getRoundIdentifier();
}
public void createAndSendProposalMessage(final long headerTimeStampSeconds) {
LOG.info("Creating proposed block.");
final Block block = blockCreator.createBlock(headerTimeStampSeconds);
transmitter.multicastProposal(roundState.getRoundIdentifier(), block);
updateStateWithProposedBlock(
messageFactory.createSignedProposalPayload(roundState.getRoundIdentifier(), block));
}
public void handleProposalMessage(final SignedData<ProposalPayload> msg) {
LOG.info("Received a Proposal message.");
final Block block = msg.getPayload().getBlock();
final boolean wasCommitted = roundState.isCommitted();
if (updateStateWithProposedBlock(msg)) {
LOG.info("Sending prepare message.");
transmitter.multicastPrepare(getRoundIdentifier(), block.getHash());
final SignedData<PreparePayload> localPrepareMessage =
messageFactory.createSignedPreparePayload(
roundState.getRoundIdentifier(), block.getHash());
peerIsPrepared(localPrepareMessage);
}
if (wasCommitted != roundState.isCommitted()) {
importBlockToChain();
}
}
public void handlePrepareMessage(final SignedData<PreparePayload> msg) {
LOG.info("Received a prepare message.");
peerIsPrepared(msg);
}
public void handleCommitMessage(final SignedData<CommitPayload> msg) {
LOG.info("Received a commit message.");
peerIsCommitted(msg);
}
public Optional<PreparedCertificate> createPrepareCertificate() {
return roundState.constructPreparedCertificate();
}
private boolean updateStateWithProposedBlock(final SignedData<ProposalPayload> msg) {
final boolean wasPrepared = roundState.isPrepared();
final boolean blockAccepted = roundState.setProposedBlock(msg);
if (blockAccepted) {
// There are times handling a proposed block is enough to enter prepared.
if (wasPrepared != roundState.isPrepared()) {
LOG.info("Sending commit message.");
final Block block = roundState.getProposedBlock().get();
transmitter.multicastCommit(getRoundIdentifier(), block.getHash(), createCommitSeal(block));
}
final SignedData<CommitPayload> localCommitMessage =
messageFactory.createSignedCommitPayload(
roundState.getRoundIdentifier(),
msg.getPayload().getBlock().getHash(),
createCommitSeal(roundState.getProposedBlock().get()));
peerIsCommitted(localCommitMessage);
}
return blockAccepted;
}
private void peerIsPrepared(final SignedData<PreparePayload> msg) {
final boolean wasPrepared = roundState.isPrepared();
roundState.addPrepareMessage(msg);
if (wasPrepared != roundState.isPrepared()) {
LOG.info("Sending commit message.");
final Block block = roundState.getProposedBlock().get();
transmitter.multicastCommit(getRoundIdentifier(), block.getHash(), createCommitSeal(block));
}
}
private void peerIsCommitted(final SignedData<CommitPayload> msg) {
final boolean wasCommitted = roundState.isCommitted();
roundState.addCommitMessage(msg);
if (wasCommitted != roundState.isCommitted()) {
importBlockToChain();
}
}
private void importBlockToChain() {
final Block blockToImport =
IbftHelpers.createSealedBlock(
roundState.getProposedBlock().get(), roundState.getCommitSeals());
LOG.info("Importing block to chain.");
boolean result =
blockImporter.importBlock(protocolContext, blockToImport, HeaderValidationMode.FULL);
if (!result) {
LOG.info("Failed to import block to chain.");
} else {
notifyNewBlockListeners(blockToImport);
}
}
private Signature createCommitSeal(final Block block) {
final BlockHeader proposedHeader = block.getHeader();
final IbftExtraData extraData = IbftExtraData.decode(proposedHeader.getExtraData());
final Hash commitHash =
IbftBlockHashing.calculateDataHashForCommittedSeal(proposedHeader, extraData);
return SECP256K1.sign(commitHash, nodeKeys);
}
private void notifyNewBlockListeners(final Block block) {
observers.forEach(obs -> obs.blockMined(block));
}
}

@ -0,0 +1,73 @@
/*
* 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.statemachine;
import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier;
import tech.pegasys.pantheon.consensus.ibft.IbftContext;
import tech.pegasys.pantheon.consensus.ibft.blockcreation.IbftBlockCreator;
import tech.pegasys.pantheon.consensus.ibft.blockcreation.IbftBlockCreatorFactory;
import tech.pegasys.pantheon.consensus.ibft.validation.MessageValidator;
import tech.pegasys.pantheon.ethereum.ProtocolContext;
import tech.pegasys.pantheon.ethereum.chain.MinedBlockObserver;
import tech.pegasys.pantheon.ethereum.core.BlockHeader;
import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSchedule;
import tech.pegasys.pantheon.util.Subscribers;
public class IbftRoundFactory {
private final IbftFinalState finalState;
private final IbftBlockCreatorFactory blockCreatorFactory;
private final ProtocolContext<IbftContext> protocolContext;
private final ProtocolSchedule<IbftContext> protocolSchedule;
private final Subscribers<MinedBlockObserver> minedBlockObservers = new Subscribers<>();
public IbftRoundFactory(
final IbftFinalState finalState,
final ProtocolContext<IbftContext> protocolContext,
final ProtocolSchedule<IbftContext> protocolSchedule) {
this.finalState = finalState;
this.blockCreatorFactory = finalState.getBlockCreatorFactory();
this.protocolContext = protocolContext;
this.protocolSchedule = protocolSchedule;
}
public IbftRound createNewRound(final BlockHeader parentHeader, final int round) {
long nextBlockHeight = parentHeader.getNumber() + 1;
final ConsensusRoundIdentifier roundIdentifier =
new ConsensusRoundIdentifier(nextBlockHeight, round);
final IbftBlockCreator blockCreator = blockCreatorFactory.create(parentHeader, round);
final RoundState roundContext =
new RoundState(
roundIdentifier,
finalState.getQuorumSize(),
new MessageValidator(
finalState.getValidators(),
finalState.getProposerForRound(roundIdentifier),
roundIdentifier,
finalState.getBlockHeaderValidator(),
protocolContext,
parentHeader));
return new IbftRound(
roundContext,
blockCreator,
protocolContext,
protocolSchedule.getByBlockNumber(nextBlockHeight).getBlockImporter(),
minedBlockObservers,
finalState.getNodeKeys(),
finalState.getMessageFactory(),
finalState.getTransmitter());
}
}

@ -60,27 +60,30 @@ public class RoundState {
}
public boolean setProposedBlock(final SignedData<ProposalPayload> msg) {
if (!proposalMessage.isPresent()) {
if (validator.addSignedProposalPayload(msg)) {
proposalMessage = Optional.of(msg);
} else {
return false;
}
preparePayloads.removeIf(p -> !validator.validatePrepareMessage(p));
commitPayloads.removeIf(p -> !validator.validateCommmitMessage(p));
updateState();
return true;
}
}
return false;
}
public void addPreparedPeer(final SignedData<PreparePayload> prepareMsg) {
if (!proposalMessage.isPresent() || validator.validatePrepareMessage(prepareMsg)) {
preparePayloads.add(prepareMsg);
public void addPrepareMessage(final SignedData<PreparePayload> msg) {
if (!proposalMessage.isPresent() || validator.validatePrepareMessage(msg)) {
preparePayloads.add(msg);
}
updateState();
}
public void addCommitSeal(final SignedData<CommitPayload> commitPayload) {
if (!proposalMessage.isPresent() || validator.validateCommmitMessage(commitPayload)) {
commitPayloads.add(commitPayload);
public void addCommitMessage(final SignedData<CommitPayload> msg) {
if (!proposalMessage.isPresent() || validator.validateCommmitMessage(msg)) {
commitPayloads.add(msg);
}
updateState();

@ -0,0 +1,251 @@
/*
* 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.statemachine;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import tech.pegasys.pantheon.consensus.common.VoteProposer;
import tech.pegasys.pantheon.consensus.common.VoteTally;
import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier;
import tech.pegasys.pantheon.consensus.ibft.IbftBlockHashing;
import tech.pegasys.pantheon.consensus.ibft.IbftContext;
import tech.pegasys.pantheon.consensus.ibft.IbftExtraData;
import tech.pegasys.pantheon.consensus.ibft.blockcreation.IbftBlockCreator;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.MessageFactory;
import tech.pegasys.pantheon.consensus.ibft.validation.MessageValidator;
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.ProtocolContext;
import tech.pegasys.pantheon.ethereum.chain.MinedBlockObserver;
import tech.pegasys.pantheon.ethereum.chain.MutableBlockchain;
import tech.pegasys.pantheon.ethereum.core.Block;
import tech.pegasys.pantheon.ethereum.core.BlockBody;
import tech.pegasys.pantheon.ethereum.core.BlockHeader;
import tech.pegasys.pantheon.ethereum.core.BlockHeaderTestFixture;
import tech.pegasys.pantheon.ethereum.core.BlockImporter;
import tech.pegasys.pantheon.ethereum.core.Hash;
import tech.pegasys.pantheon.ethereum.db.WorldStateArchive;
import tech.pegasys.pantheon.util.Subscribers;
import tech.pegasys.pantheon.util.bytes.BytesValue;
import java.math.BigInteger;
import java.util.Collections;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class IbftRoundTest {
private final KeyPair localNodeKeys = KeyPair.generate();
private final ConsensusRoundIdentifier roundIdentifier = new ConsensusRoundIdentifier(1, 1);
private final MessageFactory messageFactory = new MessageFactory(localNodeKeys);
private ProtocolContext<IbftContext> protocolContext;
@Mock private MutableBlockchain blockChain;
@Mock private WorldStateArchive worldStateArchive;
@Mock private BlockImporter<IbftContext> blockImporter;
@Mock private Subscribers<MinedBlockObserver> subscribers;
@Mock private IbftMessageTransmitter transmitter;
@Mock private IbftBlockCreator blockCreator;
@Mock private MessageValidator messageValidator;
private Block proposedBlock;
private IbftExtraData proposedExtraData;
final Signature remoteCommitSeal = Signature.create(BigInteger.ONE, BigInteger.ONE, (byte) 1);
@Before
public void setup() {
protocolContext =
new ProtocolContext<>(
blockChain,
worldStateArchive,
new IbftContext(new VoteTally(Collections.emptyList()), new VoteProposer()));
when(messageValidator.addSignedProposalPayload(any())).thenReturn(true);
when(messageValidator.validatePrepareMessage(any())).thenReturn(true);
when(messageValidator.validateCommmitMessage(any())).thenReturn(true);
proposedExtraData =
new IbftExtraData(
BytesValue.wrap(new byte[32]),
Collections.emptyList(),
Optional.empty(),
0,
Collections.emptyList());
BlockHeaderTestFixture headerTestFixture = new BlockHeaderTestFixture();
headerTestFixture.extraData(proposedExtraData.encode());
headerTestFixture.number(1);
BlockHeader header = headerTestFixture.buildHeader();
proposedBlock =
new Block(header, new BlockBody(Collections.emptyList(), Collections.emptyList()));
when(blockCreator.createBlock(anyLong())).thenReturn(proposedBlock);
when(blockImporter.importBlock(any(), any(), any())).thenReturn(true);
}
@Test
public void onReceptionOfValidProposalSendsAPrepareToNetworkPeers() {
final RoundState roundState = new RoundState(roundIdentifier, 3, messageValidator);
final IbftRound round =
new IbftRound(
roundState,
blockCreator,
protocolContext,
blockImporter,
subscribers,
localNodeKeys,
messageFactory,
transmitter);
round.handleProposalMessage(
messageFactory.createSignedProposalPayload(roundIdentifier, proposedBlock));
verify(transmitter, times(1)).multicastPrepare(roundIdentifier, proposedBlock.getHash());
verify(transmitter, never()).multicastCommit(any(), any(), any());
}
@Test
public void sendsAProposalWhenRequested() {
final RoundState roundState = new RoundState(roundIdentifier, 3, messageValidator);
final IbftRound round =
new IbftRound(
roundState,
blockCreator,
protocolContext,
blockImporter,
subscribers,
localNodeKeys,
messageFactory,
transmitter);
round.createAndSendProposalMessage(15);
verify(transmitter, times(1)).multicastProposal(roundIdentifier, proposedBlock);
verify(transmitter, never()).multicastPrepare(any(), any());
verify(transmitter, never()).multicastCommit(any(), any(), any());
}
@Test
public void singleValidatorImportBlocksImmediatelyOnProposalCreation() {
final RoundState roundState = new RoundState(roundIdentifier, 1, messageValidator);
final IbftRound round =
new IbftRound(
roundState,
blockCreator,
protocolContext,
blockImporter,
subscribers,
localNodeKeys,
messageFactory,
transmitter);
round.createAndSendProposalMessage(15);
verify(transmitter, times(1)).multicastProposal(roundIdentifier, proposedBlock);
verify(transmitter, never()).multicastPrepare(any(), any());
verify(transmitter, times(1)).multicastCommit(any(), any(), any());
verify(blockImporter, times(1)).importBlock(any(), any(), any());
}
@Test
public void twoValidatorNetworkSendsPrepareOnProposalReceptionThenSendsCommitOnCommitReceive() {
final RoundState roundState = new RoundState(roundIdentifier, 2, messageValidator);
final IbftRound round =
new IbftRound(
roundState,
blockCreator,
protocolContext,
blockImporter,
subscribers,
localNodeKeys,
messageFactory,
transmitter);
final Hash commitSealHash =
IbftBlockHashing.calculateDataHashForCommittedSeal(
proposedBlock.getHeader(), proposedExtraData);
final Signature localCommitSeal = SECP256K1.sign(commitSealHash, localNodeKeys);
// Receive Proposal Message
round.handleProposalMessage(
messageFactory.createSignedProposalPayload(roundIdentifier, proposedBlock));
verify(transmitter, times(1)).multicastPrepare(roundIdentifier, proposedBlock.getHash());
verify(transmitter, times(1))
.multicastCommit(roundIdentifier, proposedBlock.getHash(), localCommitSeal);
verify(blockImporter, never()).importBlock(any(), any(), any());
// Receive Commit Message
round.handleCommitMessage(
messageFactory.createSignedCommitPayload(
roundIdentifier, proposedBlock.getHash(), remoteCommitSeal));
// Should import block when both commit seals are available.
ArgumentCaptor<Block> capturedBlock = ArgumentCaptor.forClass(Block.class);
verify(blockImporter, times(1)).importBlock(any(), capturedBlock.capture(), any());
// Ensure imported block contains both commit seals.
IbftExtraData importedExtraData =
IbftExtraData.decode(capturedBlock.getValue().getHeader().getExtraData());
assertThat(importedExtraData.getSeals()).containsOnly(remoteCommitSeal, localCommitSeal);
}
@Test
public void localNodeProposesToNetworkOfTwoValidatorsImportsOnReceptionOfCommitFromPeer() {
final RoundState roundState = new RoundState(roundIdentifier, 2, messageValidator);
final IbftRound round =
new IbftRound(
roundState,
blockCreator,
protocolContext,
blockImporter,
subscribers,
localNodeKeys,
messageFactory,
transmitter);
final Hash commitSealHash =
IbftBlockHashing.calculateDataHashForCommittedSeal(
proposedBlock.getHeader(), proposedExtraData);
final Signature localCommitSeal = SECP256K1.sign(commitSealHash, localNodeKeys);
round.createAndSendProposalMessage(15);
verify(transmitter, never()).multicastCommit(any(), any(), any());
verify(blockImporter, never()).importBlock(any(), any(), any());
round.handlePrepareMessage(
messageFactory.createSignedPreparePayload(roundIdentifier, proposedBlock.getHash()));
verify(transmitter, times(1))
.multicastCommit(roundIdentifier, proposedBlock.getHash(), localCommitSeal);
verify(blockImporter, never()).importBlock(any(), any(), any());
round.handleCommitMessage(
messageFactory.createSignedCommitPayload(
roundIdentifier, proposedBlock.getHash(), remoteCommitSeal));
verify(blockImporter, times(1)).importBlock(any(), any(), any());
}
}

@ -127,7 +127,7 @@ public class RoundStateTest {
block.getHash(),
Signature.create(BigInteger.ONE, BigInteger.ONE, (byte) 1));
roundState.addCommitSeal(commit);
roundState.addCommitMessage(commit);
assertThat(roundState.isPrepared()).isTrue();
assertThat(roundState.isCommitted()).isTrue();
assertThat(roundState.constructPreparedCertificate()).isNotEmpty();
@ -150,12 +150,12 @@ public class RoundStateTest {
.get(2)
.createSignedPreparePayload(roundIdentifier, block.getHash());
roundState.addPreparedPeer(firstPrepare);
roundState.addPrepareMessage(firstPrepare);
assertThat(roundState.isPrepared()).isFalse();
assertThat(roundState.isCommitted()).isFalse();
assertThat(roundState.constructPreparedCertificate()).isEmpty();
roundState.addPreparedPeer(secondPrepare);
roundState.addPrepareMessage(secondPrepare);
assertThat(roundState.isPrepared()).isFalse();
assertThat(roundState.isCommitted()).isFalse();
assertThat(roundState.constructPreparedCertificate()).isEmpty();
@ -188,8 +188,8 @@ public class RoundStateTest {
when(messageValidator.validatePrepareMessage(firstPrepare)).thenReturn(true);
when(messageValidator.validatePrepareMessage(secondPrepare)).thenReturn(false);
roundState.addPreparedPeer(firstPrepare);
roundState.addPreparedPeer(secondPrepare);
roundState.addPrepareMessage(firstPrepare);
roundState.addPrepareMessage(secondPrepare);
verify(messageValidator, never()).validatePrepareMessage(any());
final SignedData<ProposalPayload> proposal =
@ -225,10 +225,10 @@ public class RoundStateTest {
roundState.setProposedBlock(proposal);
assertThat(roundState.isPrepared()).isFalse();
roundState.addPreparedPeer(firstPrepare);
roundState.addPrepareMessage(firstPrepare);
assertThat(roundState.isPrepared()).isFalse();
roundState.addPreparedPeer(secondPrepare);
roundState.addPrepareMessage(secondPrepare);
assertThat(roundState.isPrepared()).isTrue();
}
@ -259,9 +259,9 @@ public class RoundStateTest {
validatorMessageFactories.get(0).createSignedProposalPayload(roundIdentifier, block);
roundState.setProposedBlock(proposal);
roundState.addCommitSeal(firstCommit);
roundState.addCommitMessage(firstCommit);
assertThat(roundState.isCommitted()).isFalse();
roundState.addCommitSeal(secondCommit);
roundState.addCommitMessage(secondCommit);
assertThat(roundState.isCommitted()).isTrue();
assertThat(roundState.getCommitSeals())

Loading…
Cancel
Save