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

Ibft RoundState #392

Merged
merged 2 commits into from
Dec 11, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Ibft RoundState
The IBFT RoundState is responsible for determining if sufficient
valid messages have been received to deem the node "Prepared" or
"Committed".
  • Loading branch information
tmohay committed Dec 11, 2018
commit e0b8a4a03d122bedeedb4b75461a06804ba8ce94
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* 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.ibftmessagedata.CommitPayload;
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.consensus.ibft.validation.MessageValidator;
import tech.pegasys.pantheon.crypto.SECP256K1.Signature;
import tech.pegasys.pantheon.ethereum.core.Block;

import java.util.Collection;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import com.google.common.collect.Sets;

// Data items used to define how a round will operate
public class RoundState {

private final ConsensusRoundIdentifier roundIdentifier;
private final MessageValidator validator;
private final long quorumSize;

private Optional<SignedData<ProposalPayload>> proposalMessage = Optional.empty();

// Must track the actual Prepare message, not just the sender, as these may need to be reused
// to send out in a PrepareCertificate.
private final Set<SignedData<PreparePayload>> preparePayloads = Sets.newHashSet();
private final Set<SignedData<CommitPayload>> commitPayloads = Sets.newHashSet();

private boolean prepared = false;
private boolean committed = false;

public RoundState(
final ConsensusRoundIdentifier roundIdentifier,
final int quorumSize,
final MessageValidator validator) {
this.roundIdentifier = roundIdentifier;
this.quorumSize = quorumSize;
this.validator = validator;
}

public ConsensusRoundIdentifier getRoundIdentifier() {
return roundIdentifier;
}

public boolean setProposedBlock(final SignedData<ProposalPayload> msg) {
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;
}

public void addPreparedPeer(final SignedData<PreparePayload> prepareMsg) {
if (!proposalMessage.isPresent()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar to below comment. This is simpler as (!proposalMessage.isPresent()) || (validator.validatePrepareMessage(prepareMsg))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The suggested change relies on the || operator precedence, in that the validator will fail the commit message if the proposal is not available.
Would prefer to be explicit rather than implicit.

Appreciate the actual functionality is identical.

preparePayloads.add(prepareMsg);
} else if (validator.validatePrepareMessage(prepareMsg)) {
preparePayloads.add(prepareMsg);
}
updateState();
}

public void addCommitSeal(final SignedData<CommitPayload> commitPayload) {
if (!proposalMessage.isPresent()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

simplify to (!proposalMessage.isPresent() || validator.validateCommmitMessage(commitPayload))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Less lines of code - but potentially harder read - where do other reviewers fall?

commitPayloads.add(commitPayload);
} else if (validator.validateCommmitMessage(commitPayload)) {
commitPayloads.add(commitPayload);
}

updateState();
}

private void updateState() {
// NOTE: The quorumSize for Prepare messages is 1 less than the quorum size as the proposer
// does not supply a prepare message
prepared = (preparePayloads.size() >= (quorumSize - 1)) && proposalMessage.isPresent();
committed = (commitPayloads.size() >= quorumSize) && proposalMessage.isPresent();
}

public Optional<Block> getProposedBlock() {
return proposalMessage.map(p -> p.getPayload().getBlock());
}

public boolean isPrepared() {
return prepared;
}

public boolean isCommitted() {
return committed;
}

public Collection<Signature> getCommitSeals() {
return commitPayloads
.stream()
.map(cp -> cp.getPayload().getCommitSeal())
.collect(Collectors.toList());
}

public Optional<PreparedCertificate> constructPreparedCertificate() {
if (isPrepared()) {
return Optional.of(new PreparedCertificate(proposalMessage.get(), preparePayloads));
}
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
/*
* 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.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import tech.pegasys.pantheon.consensus.ibft.ConsensusRoundIdentifier;
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.ProposalPayload;
import tech.pegasys.pantheon.consensus.ibft.ibftmessagedata.SignedData;
import tech.pegasys.pantheon.consensus.ibft.validation.MessageValidator;
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.Block;
import tech.pegasys.pantheon.ethereum.core.Hash;
import tech.pegasys.pantheon.ethereum.core.Util;

import java.math.BigInteger;
import java.util.List;

import com.google.common.collect.Lists;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class RoundStateTest {

private final List<KeyPair> validatorKeys = Lists.newArrayList();
private final List<MessageFactory> validatorMessageFactories = Lists.newArrayList();
private final ConsensusRoundIdentifier roundIdentifier = new ConsensusRoundIdentifier(1, 1);

private final List<Address> validators = Lists.newArrayList();

@Mock private MessageValidator messageValidator;

@Mock private Block block;

@Before
public void setup() {
for (int i = 0; i < 3; i++) {
final KeyPair newKeyPair = KeyPair.generate();
validatorKeys.add(newKeyPair);
validators.add(Util.publicKeyToAddress(newKeyPair.getPublicKey()));
validatorMessageFactories.add(new MessageFactory(newKeyPair));
}
when(block.getHash()).thenReturn(Hash.fromHexStringLenient("1"));
}

@Test
public void defaultRoundIsNotPreparedOrCommittedAndHasNoPreparedCertificate() {
final RoundState roundState = new RoundState(roundIdentifier, 1, messageValidator);

assertThat(roundState.isPrepared()).isFalse();
assertThat(roundState.isCommitted()).isFalse();
assertThat(roundState.constructPreparedCertificate()).isEmpty();
}

@Test
public void ifProposalMessageFailsValidationMethodReturnsFalse() {
when(messageValidator.addSignedProposalPayload(any())).thenReturn(false);
final RoundState roundState = new RoundState(roundIdentifier, 1, messageValidator);

final SignedData<ProposalPayload> proposal =
validatorMessageFactories.get(0).createSignedProposalPayload(roundIdentifier, block);

assertThat(roundState.setProposedBlock(proposal)).isFalse();
assertThat(roundState.isPrepared()).isFalse();
assertThat(roundState.isCommitted()).isFalse();
assertThat(roundState.constructPreparedCertificate()).isEmpty();
}

@Test
public void singleVlidatorIsPreparedWithJustProposal() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Vlidator

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done.

when(messageValidator.addSignedProposalPayload(any())).thenReturn(true);
final RoundState roundState = new RoundState(roundIdentifier, 1, messageValidator);

final SignedData<ProposalPayload> proposal =
validatorMessageFactories.get(0).createSignedProposalPayload(roundIdentifier, block);

assertThat(roundState.setProposedBlock(proposal)).isTrue();
assertThat(roundState.isPrepared()).isTrue();
assertThat(roundState.isCommitted()).isFalse();
assertThat(roundState.constructPreparedCertificate()).isNotEmpty();
assertThat(roundState.constructPreparedCertificate().get().getProposalPayload())
.isEqualTo(proposal);
}

@Test
public void singleValidatorRequiresCommitMessageToBeCommitted() {
when(messageValidator.addSignedProposalPayload(any())).thenReturn(true);
when(messageValidator.validateCommmitMessage(any())).thenReturn(true);

final RoundState roundState = new RoundState(roundIdentifier, 1, messageValidator);

final SignedData<ProposalPayload> proposal =
validatorMessageFactories.get(0).createSignedProposalPayload(roundIdentifier, block);

assertThat(roundState.setProposedBlock(proposal)).isTrue();
assertThat(roundState.isPrepared()).isTrue();
assertThat(roundState.isCommitted()).isFalse();

final SignedData<CommitPayload> commit =
validatorMessageFactories
.get(0)
.createSignedCommitPayload(
roundIdentifier,
block.getHash(),
Signature.create(BigInteger.ONE, BigInteger.ONE, (byte) 1));

roundState.addCommitSeal(commit);
assertThat(roundState.isPrepared()).isTrue();
assertThat(roundState.isCommitted()).isTrue();
assertThat(roundState.constructPreparedCertificate()).isNotEmpty();
}

@Test
public void prepareMessagesCanBeReceivedPriorToProposal() {
when(messageValidator.addSignedProposalPayload(any())).thenReturn(true);
when(messageValidator.validatePrepareMessage(any())).thenReturn(true);

final RoundState roundState = new RoundState(roundIdentifier, 3, messageValidator);

final SignedData<PreparePayload> firstPrepare =
validatorMessageFactories
.get(1)
.createSignedPreparePayload(roundIdentifier, block.getHash());

final SignedData<PreparePayload> secondPrepare =
validatorMessageFactories
.get(2)
.createSignedPreparePayload(roundIdentifier, block.getHash());

roundState.addPreparedPeer(firstPrepare);
assertThat(roundState.isPrepared()).isFalse();
assertThat(roundState.isCommitted()).isFalse();
assertThat(roundState.constructPreparedCertificate()).isEmpty();

roundState.addPreparedPeer(secondPrepare);
assertThat(roundState.isPrepared()).isFalse();
assertThat(roundState.isCommitted()).isFalse();
assertThat(roundState.constructPreparedCertificate()).isEmpty();

final SignedData<ProposalPayload> proposal =
validatorMessageFactories.get(0).createSignedProposalPayload(roundIdentifier, block);
assertThat(roundState.setProposedBlock(proposal)).isTrue();
assertThat(roundState.isPrepared()).isTrue();
assertThat(roundState.isCommitted()).isFalse();
assertThat(roundState.constructPreparedCertificate()).isNotEmpty();
}

@Test
public void invalidPriorPrepareMessagesAreDiscardedUponSubsequentProposal() {
final SignedData<PreparePayload> firstPrepare =
validatorMessageFactories
.get(1)
.createSignedPreparePayload(roundIdentifier, block.getHash());

final SignedData<PreparePayload> secondPrepare =
validatorMessageFactories
.get(2)
.createSignedPreparePayload(roundIdentifier, block.getHash());

// RoundState has a quorum size of 3, meaning 1 proposal and 2 prepare are required to be
// 'prepared'.
final RoundState roundState = new RoundState(roundIdentifier, 3, messageValidator);

when(messageValidator.addSignedProposalPayload(any())).thenReturn(true);
when(messageValidator.validatePrepareMessage(firstPrepare)).thenReturn(true);
when(messageValidator.validatePrepareMessage(secondPrepare)).thenReturn(false);

roundState.addPreparedPeer(firstPrepare);
roundState.addPreparedPeer(secondPrepare);
verify(messageValidator, never()).validatePrepareMessage(any());

final SignedData<ProposalPayload> proposal =
validatorMessageFactories.get(0).createSignedProposalPayload(roundIdentifier, block);

assertThat(roundState.setProposedBlock(proposal)).isTrue();
assertThat(roundState.isPrepared()).isFalse();
assertThat(roundState.isCommitted()).isFalse();
assertThat(roundState.constructPreparedCertificate()).isEmpty();
}

@Test
public void prepareMessageIsValidatedAgainstExitingProposal() {
final RoundState roundState = new RoundState(roundIdentifier, 2, messageValidator);

final SignedData<PreparePayload> firstPrepare =
validatorMessageFactories
.get(1)
.createSignedPreparePayload(roundIdentifier, block.getHash());

final SignedData<PreparePayload> secondPrepare =
validatorMessageFactories
.get(2)
.createSignedPreparePayload(roundIdentifier, block.getHash());

final SignedData<ProposalPayload> proposal =
validatorMessageFactories.get(0).createSignedProposalPayload(roundIdentifier, block);

when(messageValidator.addSignedProposalPayload(any())).thenReturn(true);
when(messageValidator.validatePrepareMessage(firstPrepare)).thenReturn(false);
when(messageValidator.validatePrepareMessage(secondPrepare)).thenReturn(true);

roundState.setProposedBlock(proposal);
assertThat(roundState.isPrepared()).isFalse();

roundState.addPreparedPeer(firstPrepare);
assertThat(roundState.isPrepared()).isFalse();

roundState.addPreparedPeer(secondPrepare);
assertThat(roundState.isPrepared()).isTrue();
}

@Test
public void commitSealsAreExtractedFromReceivedMessages() {
when(messageValidator.addSignedProposalPayload(any())).thenReturn(true);
when(messageValidator.validateCommmitMessage(any())).thenReturn(true);

final RoundState roundState = new RoundState(roundIdentifier, 2, messageValidator);

final SignedData<CommitPayload> firstCommit =
validatorMessageFactories
.get(1)
.createSignedCommitPayload(
roundIdentifier,
block.getHash(),
Signature.create(BigInteger.ONE, BigInteger.TEN, (byte) 1));

final SignedData<CommitPayload> secondCommit =
validatorMessageFactories
.get(2)
.createSignedCommitPayload(
roundIdentifier,
block.getHash(),
Signature.create(BigInteger.TEN, BigInteger.TEN, (byte) 1));

final SignedData<ProposalPayload> proposal =
validatorMessageFactories.get(0).createSignedProposalPayload(roundIdentifier, block);

roundState.setProposedBlock(proposal);
roundState.addCommitSeal(firstCommit);
assertThat(roundState.isCommitted()).isFalse();
roundState.addCommitSeal(secondCommit);
assertThat(roundState.isCommitted()).isTrue();

assertThat(roundState.getCommitSeals())
.containsOnly(
firstCommit.getPayload().getCommitSeal(), secondCommit.getPayload().getCommitSeal());
}
}