Allow IBFT Round to be created using PreparedCert (#429)

When the local node is to be the proposer for a new round
(due to round change), the new Ibft Round is to be created using the
latest received PreparedCertificate, and a NewRound message multicast
to all validators.

It should be noted, the block in the NewRound Proposal must be
modified from that received in the PreparedCertificate such that it
contains the executing round number.
tmohay 6 years ago committed by GitHub
parent e62d68e877
commit 5425a92c0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 28
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/IbftHelpers.java
  2. 47
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftRound.java
  3. 15
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftRoundFactory.java
  4. 26
      consensus/ibft/src/main/java/tech/pegasys/pantheon/consensus/ibft/validation/NewRoundMessageValidator.java
  5. 83
      consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/IbftHelpersTest.java
  6. 72
      consensus/ibft/src/test/java/tech/pegasys/pantheon/consensus/ibft/statemachine/IbftRoundTest.java

@ -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;
@ -20,6 +23,7 @@ import tech.pegasys.pantheon.ethereum.core.Hash;
import tech.pegasys.pantheon.ethereum.core.Util;
import java.util.Collection;
import java.util.Optional;
public class IbftHelpers {
@ -51,4 +55,28 @@ public class IbftHelpers {
return new Block(sealedHeader, block.getBody());
}
public static 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 = 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;
}
}

@ -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;
@ -23,6 +25,7 @@ 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.RoundChangeCertificate;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.SignedData;
import tech.pegasys.pantheon.crypto.SECP256K1;
import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair;
@ -31,6 +34,7 @@ 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.BlockHeaderBuilder;
import tech.pegasys.pantheon.ethereum.core.BlockImporter;
import tech.pegasys.pantheon.ethereum.core.Hash;
import tech.pegasys.pantheon.ethereum.mainnet.HeaderValidationMode;
@ -86,6 +90,49 @@ public class IbftRound {
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();

@ -48,7 +48,7 @@ public class IbftRoundFactory {
new ConsensusRoundIdentifier(nextBlockHeight, round);
final IbftBlockCreator blockCreator = blockCreatorFactory.create(parentHeader, round);
final RoundState roundContext =
final RoundState roundState =
new RoundState(
roundIdentifier,
finalState.getQuorumSize(),
@ -60,11 +60,20 @@ public class IbftRoundFactory {
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(),

@ -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;
@ -160,28 +162,4 @@ public class NewRoundMessageValidator {
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;
}
}

@ -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 {
@ -63,4 +76,74 @@ public class IbftHelpersTest {
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();
}
}

@ -15,6 +15,7 @@ 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.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@ -28,6 +29,10 @@ 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.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;
@ -53,6 +58,7 @@ import org.junit.Before;
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;
@ -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;
@ -248,4 +256,68 @@ public class IbftRoundTest {
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());
}
}

Loading…
Cancel
Save