Skip to content
This repository has been archived by the owner on Sep 26, 2019. It is now read-only.

Allow IBFT Round to be created using PreparedCert #429

Merged
merged 6 commits into from
Dec 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
*/
package tech.pegasys.pantheon.consensus.ibft;

import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.PreparedCertificate;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.RoundChangePayload;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.SignedData;
import tech.pegasys.pantheon.crypto.SECP256K1.Signature;
import tech.pegasys.pantheon.ethereum.core.Block;
import tech.pegasys.pantheon.ethereum.core.BlockHeader;
Expand All @@ -20,6 +23,7 @@
import tech.pegasys.pantheon.ethereum.core.Util;

import java.util.Collection;
import java.util.Optional;

public class IbftHelpers {

Expand Down Expand Up @@ -51,4 +55,28 @@ public static Block createSealedBlock(

return new Block(sealedHeader, block.getBody());
}

public static Optional<PreparedCertificate> findLatestPreparedCertificate(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't look there is a test for this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was previously fully tested as part of NewRoundMessageValidator (but was then moved out here) - have created additional tests based of the previously existing tests.

final Collection<SignedData<RoundChangePayload>> msgs) {

Optional<PreparedCertificate> result = Optional.empty();

for (SignedData<RoundChangePayload> roundChangeMsg : msgs) {
final RoundChangePayload payload = roundChangeMsg.getPayload();
if (payload.getPreparedCertificate().isPresent()) {
if (!result.isPresent()) {
result = payload.getPreparedCertificate();
} else {
final PreparedCertificate currentLatest = result.get();
final PreparedCertificate nextCert = payload.getPreparedCertificate().get();

if (currentLatest.getProposalPayload().getPayload().getRoundIdentifier().getRoundNumber()
< nextCert.getProposalPayload().getPayload().getRoundIdentifier().getRoundNumber()) {
result = Optional.of(nextCert);
}
}
}
}
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
*/
package tech.pegasys.pantheon.consensus.ibft.statemachine;

import static tech.pegasys.pantheon.consensus.ibft.IbftHelpers.findLatestPreparedCertificate;

import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier;
import tech.pegasys.pantheon.consensus.ibft.IbftBlockHashing;
import tech.pegasys.pantheon.consensus.ibft.IbftContext;
Expand All @@ -23,6 +25,7 @@
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.SignedData;
import tech.pegasys.pantheon.crypto.SECP256K1;
import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair;
Expand All @@ -31,6 +34,7 @@
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.BlockHeaderBuilder;
import tech.pegasys.pantheon.ethereum.core.BlockImporter;
import tech.pegasys.pantheon.ethereum.core.Hash;
import tech.pegasys.pantheon.ethereum.mainnet.HeaderValidationMode;
Expand Down Expand Up @@ -86,6 +90,49 @@ public void createAndSendProposalMessage(final long headerTimeStampSeconds) {
messageFactory.createSignedProposalPayload(roundState.getRoundIdentifier(), block));
}

public void startRoundWith(
final RoundChangeCertificate roundChangeCertificate, final long headerTimestamp) {
final Optional<PreparedCertificate> latestCertificate =
findLatestPreparedCertificate(roundChangeCertificate.getRoundChangePayloads());
if (!latestCertificate.isPresent()) {
final Block block = blockCreator.createBlock(headerTimestamp);
transmitter.multicastNewRound(
getRoundIdentifier(),
roundChangeCertificate,
messageFactory.createSignedProposalPayload(getRoundIdentifier(), block));
} else {
final SignedData<ProposalPayload> proposal =
createProposalFromPreparedCertificate(latestCertificate.get());
transmitter.multicastNewRound(getRoundIdentifier(), roundChangeCertificate, proposal);
updateStateWithProposedBlock(proposal);
}
}

private SignedData<ProposalPayload> createProposalFromPreparedCertificate(
final PreparedCertificate preparedCertificate) {
final Block block = preparedCertificate.getProposalPayload().getPayload().getBlock();

final IbftExtraData prevExtraData = IbftExtraData.decode(block.getHeader().getExtraData());
final IbftExtraData extraDataToPublish =
new IbftExtraData(
prevExtraData.getVanityData(),
prevExtraData.getSeals(),
prevExtraData.getVote(),
getRoundIdentifier().getRoundNumber(),
prevExtraData.getValidators());

final BlockHeaderBuilder headerBuilder = BlockHeaderBuilder.fromHeader(block.getHeader());
headerBuilder
.extraData(extraDataToPublish.encode())
.blockHashFunction(
blockHeader ->
IbftBlockHashing.calculateDataHashForCommittedSeal(
blockHeader, extraDataToPublish));
final BlockHeader newHeader = headerBuilder.buildBlockHeader();
final Block newBlock = new Block(newHeader, block.getBody());
return messageFactory.createSignedProposalPayload(getRoundIdentifier(), newBlock);
}

public void handleProposalMessage(final SignedData<ProposalPayload> msg) {
LOG.info("Received a Proposal message.");
final Block block = msg.getPayload().getBlock();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public IbftRound createNewRound(final BlockHeader parentHeader, final int round)
new ConsensusRoundIdentifier(nextBlockHeight, round);
final IbftBlockCreator blockCreator = blockCreatorFactory.create(parentHeader, round);

final RoundState roundContext =
final RoundState roundState =
new RoundState(
roundIdentifier,
finalState.getQuorumSize(),
Expand All @@ -60,11 +60,20 @@ public IbftRound createNewRound(final BlockHeader parentHeader, final int round)
protocolContext,
parentHeader));

return createNewRoundWithState(parentHeader, roundState);
}

public IbftRound createNewRoundWithState(
final BlockHeader parentHeader, final RoundState roundState) {
final ConsensusRoundIdentifier roundIdentifier = roundState.getRoundIdentifier();
final IbftBlockCreator blockCreator =
blockCreatorFactory.create(parentHeader, roundIdentifier.getRoundNumber());

return new IbftRound(
roundContext,
roundState,
blockCreator,
protocolContext,
protocolSchedule.getByBlockNumber(nextBlockHeight).getBlockImporter(),
protocolSchedule.getByBlockNumber(roundIdentifier.getSequenceNumber()).getBlockImporter(),
minedBlockObservers,
finalState.getNodeKeys(),
finalState.getMessageFactory(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
*/
package tech.pegasys.pantheon.consensus.ibft.validation;

import static tech.pegasys.pantheon.consensus.ibft.IbftHelpers.findLatestPreparedCertificate;

import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier;
import tech.pegasys.pantheon.consensus.ibft.blockcreation.ProposerSelector;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.NewRoundPayload;
Expand Down Expand Up @@ -160,28 +162,4 @@ private boolean validateProposalMessageMatchesLatestPrepareCertificate(

return true;
}

private Optional<PreparedCertificate> findLatestPreparedCertificate(
final Collection<SignedData<RoundChangePayload>> msgs) {

Optional<PreparedCertificate> result = Optional.empty();

for (SignedData<RoundChangePayload> roundChangeMsg : msgs) {
final RoundChangePayload payload = roundChangeMsg.getPayload();
if (payload.getPreparedCertificate().isPresent()) {
if (!result.isPresent()) {
result = Optional.of(payload.getPreparedCertificate().get());
} else {
final PreparedCertificate currentLatest = result.get();
final PreparedCertificate nextCert = payload.getPreparedCertificate().get();

if (currentLatest.getProposalPayload().getPayload().getRoundIdentifier().getRoundNumber()
< nextCert.getProposalPayload().getPayload().getRoundIdentifier().getRoundNumber()) {
result = Optional.of(nextCert);
}
}
}
}
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,21 @@
package tech.pegasys.pantheon.consensus.ibft;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static tech.pegasys.pantheon.consensus.ibft.IbftHelpers.calculateRequiredValidatorQuorum;

import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.MessageFactory;
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.KeyPair;
import tech.pegasys.pantheon.ethereum.core.Block;
import tech.pegasys.pantheon.ethereum.core.Hash;

import java.util.Optional;

import com.google.common.collect.Lists;
import org.junit.Test;

public class IbftHelpersTest {
Expand Down Expand Up @@ -63,4 +76,74 @@ public void calculateRequiredValidatorQuorum15Validator() {
public void calculateRequiredValidatorQuorum20Validator() {
assertThat(calculateRequiredValidatorQuorum(20)).isEqualTo(14);
}

@Test
public void latestPreparedCertificateIsExtractedFromRoundChangeCertificate() {
// NOTE: This function does not validate that all RoundCHanges/Prepares etc. come from valid
// sources, it is only responsible for determine which of the list or RoundChange messages
// contains the newest
// NOTE: This capability is tested as part of the NewRoundMessageValidationTests.
final KeyPair proposerKey = KeyPair.generate();
final MessageFactory proposerMessageFactory = new MessageFactory(proposerKey);
final Block proposedBlock = mock(Block.class);
when(proposedBlock.getHash()).thenReturn(Hash.fromHexStringLenient("1"));
final ConsensusRoundIdentifier roundIdentifier = new ConsensusRoundIdentifier(1, 4);

final ConsensusRoundIdentifier preparedRound = TestHelpers.createFrom(roundIdentifier, 0, -1);
final SignedData<ProposalPayload> differentProposal =
proposerMessageFactory.createSignedProposalPayload(preparedRound, proposedBlock);

final Optional<PreparedCertificate> latterPreparedCert =
Optional.of(
new PreparedCertificate(
differentProposal,
Lists.newArrayList(
proposerMessageFactory.createSignedPreparePayload(
roundIdentifier, proposedBlock.getHash()),
proposerMessageFactory.createSignedPreparePayload(
roundIdentifier, proposedBlock.getHash()))));

// An earlier PrepareCert is added to ensure the path to find the latest PrepareCert
// is correctly followed.
final ConsensusRoundIdentifier earlierPreparedRound =
TestHelpers.createFrom(roundIdentifier, 0, -2);
final SignedData<ProposalPayload> earlierProposal =
proposerMessageFactory.createSignedProposalPayload(earlierPreparedRound, proposedBlock);
final Optional<PreparedCertificate> earlierPreparedCert =
Optional.of(
new PreparedCertificate(
earlierProposal,
Lists.newArrayList(
proposerMessageFactory.createSignedPreparePayload(
earlierPreparedRound, proposedBlock.getHash()),
proposerMessageFactory.createSignedPreparePayload(
earlierPreparedRound, proposedBlock.getHash()))));

final Optional<PreparedCertificate> newestCert =
IbftHelpers.findLatestPreparedCertificate(
Lists.newArrayList(
proposerMessageFactory.createSignedRoundChangePayload(
roundIdentifier, earlierPreparedCert),
proposerMessageFactory.createSignedRoundChangePayload(
roundIdentifier, latterPreparedCert)));

assertThat(newestCert).isEqualTo(latterPreparedCert);
}

@Test
public void allRoundChangeHaveNoPreparedReturnsEmptyOptional() {
final KeyPair proposerKey = KeyPair.generate();
final MessageFactory proposerMessageFactory = new MessageFactory(proposerKey);
final ConsensusRoundIdentifier roundIdentifier = new ConsensusRoundIdentifier(1, 4);

final Optional<PreparedCertificate> newestCert =
IbftHelpers.findLatestPreparedCertificate(
Lists.newArrayList(
proposerMessageFactory.createSignedRoundChangePayload(
roundIdentifier, Optional.empty()),
proposerMessageFactory.createSignedRoundChangePayload(
roundIdentifier, Optional.empty())));

assertThat(newestCert).isEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
Expand All @@ -28,6 +29,10 @@
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.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.SignedData;
import tech.pegasys.pantheon.consensus.ibft.validation.MessageValidator;
import tech.pegasys.pantheon.crypto.SECP256K1;
import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair;
Expand All @@ -53,6 +58,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

Expand All @@ -73,6 +79,8 @@ public class IbftRoundTest {
@Mock private IbftBlockCreator blockCreator;
@Mock private MessageValidator messageValidator;

@Captor private ArgumentCaptor<SignedData<ProposalPayload>> payloadArgCaptor;

private Block proposedBlock;
private IbftExtraData proposedExtraData;

Expand Down Expand Up @@ -248,4 +256,68 @@ public void localNodeProposesToNetworkOfTwoValidatorsImportsOnReceptionOfCommitF
roundIdentifier, proposedBlock.getHash(), remoteCommitSeal));
verify(blockImporter, times(1)).importBlock(any(), any(), any());
}

@Test
public void aNewRoundMessageWithAnewBlockIsSentUponReceptionOfARoundChangeWithNoCertificate() {
final RoundState roundState = new RoundState(roundIdentifier, 2, messageValidator);
final IbftRound round =
new IbftRound(
roundState,
blockCreator,
protocolContext,
blockImporter,
subscribers,
localNodeKeys,
messageFactory,
transmitter);

final RoundChangeCertificate roundChangeCertificate =
new RoundChangeCertificate(Collections.emptyList());

round.startRoundWith(roundChangeCertificate, 15);
verify(transmitter, times(1))
.multicastNewRound(eq(roundIdentifier), eq(roundChangeCertificate), any());
}

@Test
public void aNewRoundMessageWithTheSameBlockIsSentUponReceptionOfARoundChangeWithCertificate() {
SignedData<ProposalPayload> mockedSentMessage =
messageFactory.createSignedProposalPayload(roundIdentifier, proposedBlock);

final ConsensusRoundIdentifier priorRoundChange = new ConsensusRoundIdentifier(1, 0);
final RoundState roundState = new RoundState(roundIdentifier, 2, messageValidator);
final IbftRound round =
new IbftRound(
roundState,
blockCreator,
protocolContext,
blockImporter,
subscribers,
localNodeKeys,
messageFactory,
transmitter);

final RoundChangeCertificate roundChangeCertificate =
new RoundChangeCertificate(
Collections.singletonList(
messageFactory.createSignedRoundChangePayload(
roundIdentifier,
Optional.of(
new PreparedCertificate(
messageFactory.createSignedProposalPayload(
priorRoundChange, proposedBlock),
Collections
.emptyList()))))); // NOTE: IbftRound assumes the prepare's are
// valid

round.startRoundWith(roundChangeCertificate, 15);
verify(transmitter, times(1))
.multicastNewRound(
eq(roundIdentifier), eq(roundChangeCertificate), payloadArgCaptor.capture());

final IbftExtraData proposedExtraData =
IbftExtraData.decode(
payloadArgCaptor.getValue().getPayload().getBlock().getHeader().getExtraData());
assertThat(proposedExtraData.getRound()).isEqualTo(roundIdentifier.getRoundNumber());
}
}