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 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
@@ -0,0 +1,121 @@
/*
* 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() || validator.validatePrepareMessage(prepareMsg)) {
preparePayloads.add(prepareMsg);
}
updateState();
}

public void addCommitSeal(final SignedData<CommitPayload> commitPayload) {
if (!proposalMessage.isPresent() || 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 singleValidatorIsPreparedWithJustProposal() {
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());
}
}