From 5ee3079e1f27d92cac4f9f843ff87fb0a51a3ac7 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 6 Dec 2023 12:34:57 +0100 Subject: [PATCH 1/9] feat: Store all received proposals (#91) * wip: Store proposals within driver * Less cloning * Store a list of proposal for value * Less cloning * Cleanup * Drop proposal with polka previous if we are not in Propose step * Fall through instead of dropping the proposal * Rename variable in votekeeper for less ambiguity * Add new test, add comments and fix driver_steps_polka_previous_invalid_proposal_with_locked * Add test summary and cleanup test names * Formatting * Remove commented out code --------- Co-authored-by: Anca Zamfir --- Code/driver/src/driver.rs | 53 +++++--- Code/driver/src/lib.rs | 1 + Code/driver/src/proposals.rs | 48 +++++++ Code/round/src/state.rs | 5 - Code/round/src/state_machine.rs | 20 +-- Code/test/src/utils.rs | 67 +++++----- Code/test/tests/driver.rs | 63 ++------- Code/test/tests/driver_extra.rs | 229 +++++++++++++++++++++++++------- Code/vote/src/keeper.rs | 26 ++-- 9 files changed, 330 insertions(+), 182 deletions(-) create mode 100644 Code/driver/src/proposals.rs diff --git a/Code/driver/src/driver.rs b/Code/driver/src/driver.rs index 110780383..137bd2bb7 100644 --- a/Code/driver/src/driver.rs +++ b/Code/driver/src/driver.rs @@ -16,6 +16,7 @@ use malachite_vote::ThresholdParams; use crate::input::Input; use crate::output::Output; +use crate::proposals::Proposals; use crate::Error; use crate::ProposerSelector; use crate::Validity; @@ -33,6 +34,7 @@ where pub votes: VoteKeeper, pub round_state: RoundState, + pub proposals: Proposals, } impl Driver @@ -57,6 +59,7 @@ where validator_set, votes, round_state: RoundState::default(), + proposals: Proposals::new(), } } @@ -162,11 +165,14 @@ where return Ok(None); } + self.proposals.insert(proposal.clone()); + let polka_for_pol = self.votes.is_threshold_met( &proposal.pol_round(), VoteType::Prevote, Threshold::Value(proposal.value().id()), ); + let polka_previous = proposal.pol_round().is_defined() && polka_for_pol && proposal.pol_round() < self.round_state.round; @@ -181,7 +187,7 @@ where // L32 return self.apply_input( proposal.round(), - RoundInput::InvalidProposalAndPolkaPrevious(proposal.clone()), + RoundInput::InvalidProposalAndPolkaPrevious(proposal), ); } else { return Ok(None); @@ -201,13 +207,12 @@ where ) { return self.apply_input( proposal.round(), - RoundInput::ProposalAndPrecommitValue(proposal.clone()), + RoundInput::ProposalAndPrecommitValue(proposal), ); } - // If the proposal is for a different round drop the proposal - // TODO - this check is also done in the round state machine, decide where to do it - if self.round_state.round != proposal.round() { + // If the proposal is for a different round, drop the proposal + if self.round() != proposal.round() { return Ok(None); } @@ -216,26 +221,28 @@ where VoteType::Prevote, Threshold::Value(proposal.value().id()), ); + let polka_current = polka_for_current && self.round_state.step >= Step::Prevote; // L36 if polka_current { return self.apply_input( proposal.round(), - RoundInput::ProposalAndPolkaCurrent(proposal.clone()), + RoundInput::ProposalAndPolkaCurrent(proposal), ); } // L28 - if polka_previous { + if self.round_state.step == Step::Propose && polka_previous { + // TODO: Check proposal vr is equal to threshold vr return self.apply_input( proposal.round(), - RoundInput::ProposalAndPolkaPrevious(proposal.clone()), + RoundInput::ProposalAndPolkaPrevious(proposal), ); } // TODO - Caller needs to store the proposal (valid or not) as the quorum (polka or commits) may be met later - self.apply_input(proposal.round(), RoundInput::Proposal(proposal.clone())) + self.apply_input(proposal.round(), RoundInput::Proposal(proposal)) } fn apply_vote( @@ -300,20 +307,29 @@ where let data = Info::new(input_round, &self.address, proposer.address()); - // Multiplex the input with the round state. + // Multiplex the event with the round state. let mux_input = match input { - RoundInput::PolkaValue(value_id) => match round_state.proposal { - Some(ref proposal) if proposal.value().id() == value_id => { + RoundInput::PolkaValue(value_id) => { + let proposal = self.proposals.find(&value_id, |p| p.round() == input_round); + + if let Some(proposal) = proposal { + assert_eq!(proposal.value().id(), value_id); RoundInput::ProposalAndPolkaCurrent(proposal.clone()) + } else { + RoundInput::PolkaAny } - _ => RoundInput::PolkaAny, - }, - RoundInput::PrecommitValue(value_id) => match round_state.proposal { - Some(ref proposal) if proposal.value().id() == value_id => { + } + + RoundInput::PrecommitValue(value_id) => { + let proposal = self.proposals.find(&value_id, |p| p.round() == input_round); + + if let Some(proposal) = proposal { + assert_eq!(proposal.value().id(), value_id); RoundInput::ProposalAndPrecommitValue(proposal.clone()) + } else { + RoundInput::PrecommitAny } - _ => RoundInput::PrecommitAny, - }, + } _ => input, }; @@ -339,6 +355,7 @@ where .field("address", &self.address) .field("validator_set", &self.validator_set) .field("votes", &self.votes) + .field("proposals", &self.proposals.proposals) .field("round_state", &self.round_state) .finish() } diff --git a/Code/driver/src/lib.rs b/Code/driver/src/lib.rs index 95c0f560f..fd77260a5 100644 --- a/Code/driver/src/lib.rs +++ b/Code/driver/src/lib.rs @@ -18,6 +18,7 @@ mod driver; mod error; mod input; mod output; +mod proposals; mod proposer; mod util; diff --git a/Code/driver/src/proposals.rs b/Code/driver/src/proposals.rs new file mode 100644 index 000000000..cf0173efe --- /dev/null +++ b/Code/driver/src/proposals.rs @@ -0,0 +1,48 @@ +use alloc::collections::BTreeMap; +use alloc::vec::Vec; + +use malachite_common::ValueId; +use malachite_common::{Context, Proposal, Value}; + +/// Stores proposals at each round, indexed by their value id. +pub struct Proposals +where + Ctx: Context, +{ + pub(crate) proposals: BTreeMap, Vec>, +} + +impl Proposals +where + Ctx: Context, +{ + pub fn new() -> Self { + Self { + proposals: BTreeMap::new(), + } + } + + pub fn insert(&mut self, proposal: Ctx::Proposal) { + let value_id = proposal.value().id(); + self.proposals.entry(value_id).or_default().push(proposal); + } + + pub fn find( + &self, + value_id: &ValueId, + p: impl Fn(&Ctx::Proposal) -> bool, + ) -> Option<&Ctx::Proposal> { + self.proposals + .get(value_id) + .and_then(|proposals| proposals.iter().find(|proposal| p(proposal))) + } +} + +impl Default for Proposals +where + Ctx: Context, +{ + fn default() -> Self { + Self::new() + } +} diff --git a/Code/round/src/state.rs b/Code/round/src/state.rs index 09697a8d8..c15d1e091 100644 --- a/Code/round/src/state.rs +++ b/Code/round/src/state.rs @@ -38,7 +38,6 @@ where pub round: Round, pub step: Step, - pub proposal: Option, pub locked: Option>, pub valid: Option>, } @@ -52,7 +51,6 @@ where height, round, step: Step::NewRound, - proposal: None, locked: None, valid: None, } @@ -108,7 +106,6 @@ where height: self.height.clone(), round: self.round, step: self.step, - proposal: self.proposal.clone(), locked: self.locked.clone(), valid: self.valid.clone(), } @@ -125,7 +122,6 @@ where .field("height", &self.round) .field("round", &self.round) .field("step", &self.step) - .field("proposal", &self.proposal) .field("locked", &self.locked) .field("valid", &self.valid) .finish() @@ -141,7 +137,6 @@ where self.height == other.height && self.round == other.round && self.step == other.step - && self.proposal == other.proposal && self.locked == other.locked && self.valid == other.valid } diff --git a/Code/round/src/state_machine.rs b/Code/round/src/state_machine.rs index 093feb57b..82fbd1ded 100644 --- a/Code/round/src/state_machine.rs +++ b/Code/round/src/state_machine.rs @@ -249,7 +249,7 @@ where /// /// Ref: L22/L28 pub fn prevote( - mut state: State, + state: State, address: &Ctx::Address, proposal: &Ctx::Proposal, ) -> Transition @@ -266,7 +266,6 @@ where }; let output = Output::prevote(state.height.clone(), state.round, value, address.clone()); - state.proposal = Some(proposal.clone()); Transition::to(state.with_step(Step::Prevote)).with_output(output) } @@ -292,7 +291,7 @@ where /// NOTE: Only one of this and set_valid_value should be called once in a round /// How do we enforce this? pub fn precommit( - mut state: State, + state: State, address: &Ctx::Address, proposal: Ctx::Proposal, ) -> Transition @@ -311,16 +310,6 @@ where address.clone(), ); - let current_value = match state.proposal { - Some(ref proposal) => proposal.value().clone(), - None => { - state.proposal = Some(proposal.clone()); - proposal.value().clone() - } - }; - - debug_assert_eq!(current_value.id(), value.id()); - let next = state .set_locked(value.clone()) .set_valid(value.clone()) @@ -394,12 +383,11 @@ where /// Ref: L36/L42 /// /// NOTE: only one of this and precommit should be called once in a round -pub fn set_valid_value(mut state: State, proposal: &Ctx::Proposal) -> Transition +pub fn set_valid_value(state: State, proposal: &Ctx::Proposal) -> Transition where Ctx: Context, { - state.proposal = Some(proposal.clone()); - Transition::to(state.clone().set_valid(proposal.value().clone())) + Transition::to(state.set_valid(proposal.value().clone())) } //--------------------------------------------------------------------- diff --git a/Code/test/src/utils.rs b/Code/test/src/utils.rs index d4450f206..4dfbe5092 100644 --- a/Code/test/src/utils.rs +++ b/Code/test/src/utils.rs @@ -174,7 +174,6 @@ pub fn propose_state(round: Round) -> State { height: Height::new(1), round, step: Step::Propose, - proposal: None, locked: None, valid: None, } @@ -194,9 +193,8 @@ pub fn propose_state_with_proposal_and_valid( height: Height::new(1), round: state_round, step: Step::Propose, - proposal: Some(proposal.clone()), valid: Some(RoundValue { - value: proposal.clone().value, + value: proposal.value, round: valid_round, }), locked: None, @@ -211,13 +209,12 @@ pub fn propose_state_with_proposal_and_locked_and_valid( height: Height::new(1), round, step: Step::Propose, - proposal: Some(proposal.clone()), valid: Some(RoundValue { - value: proposal.clone().value, + value: proposal.value, round: Round::new(0), }), locked: Some(RoundValue { - value: proposal.clone().value, + value: proposal.value, round: Round::new(0), }), } @@ -228,23 +225,11 @@ pub fn prevote_state(round: Round) -> State { height: Height::new(1), round, step: Step::Prevote, - proposal: None, locked: None, valid: None, } } -pub fn prevote_state_with_proposal(round: Round, proposal: Proposal) -> State { - State { - height: Height::new(1), - round, - step: Step::Prevote, - proposal: Some(proposal.clone()), - valid: None, - locked: None, - } -} - pub fn prevote_state_with_proposal_and_valid( state_round: Round, valid_round: Round, @@ -254,9 +239,8 @@ pub fn prevote_state_with_proposal_and_valid( height: Height::new(1), round: state_round, step: Step::Prevote, - proposal: Some(proposal.clone()), valid: Some(RoundValue { - value: proposal.clone().value, + value: proposal.value, round: valid_round, }), locked: None, @@ -271,13 +255,12 @@ pub fn prevote_state_with_proposal_and_locked_and_valid( height: Height::new(1), round, step: Step::Prevote, - proposal: Some(proposal.clone()), valid: Some(RoundValue { - value: proposal.clone().value, + value: proposal.value, round: Round::new(0), }), locked: Some(RoundValue { - value: proposal.clone().value, + value: proposal.value, round: Round::new(0), }), } @@ -291,13 +274,12 @@ pub fn precommit_state_with_proposal_and_locked_and_valid( height: Height::new(1), round, step: Step::Precommit, - proposal: Some(proposal.clone()), valid: Some(RoundValue { - value: proposal.clone().value, + value: proposal.value, round: Round::new(0), }), locked: Some(RoundValue { - value: proposal.clone().value, + value: proposal.value, round: Round::new(0), }), } @@ -308,7 +290,6 @@ pub fn precommit_state(round: Round) -> State { height: Height::new(1), round, step: Step::Precommit, - proposal: None, locked: None, valid: None, } @@ -323,9 +304,8 @@ pub fn precommit_state_with_proposal_and_valid( height: Height::new(1), round: state_round, step: Step::Precommit, - proposal: Some(proposal.clone()), valid: Some(RoundValue { - value: proposal.clone().value, + value: proposal.value, round: valid_round, }), locked: None, @@ -337,7 +317,6 @@ pub fn new_round(round: Round) -> State { height: Height::new(1), round, step: Step::NewRound, - proposal: None, valid: None, locked: None, } @@ -348,9 +327,8 @@ pub fn new_round_with_proposal_and_valid(round: Round, proposal: Proposal) -> St height: Height::new(1), round, step: Step::NewRound, - proposal: Some(proposal.clone()), valid: Some(RoundValue { - value: proposal.clone().value, + value: proposal.value, round: Round::new(0), }), locked: None, @@ -365,13 +343,12 @@ pub fn new_round_with_proposal_and_locked_and_valid( height: Height::new(1), round, step: Step::NewRound, - proposal: Some(proposal.clone()), valid: Some(RoundValue { - value: proposal.clone().value, + value: proposal.value, round: Round::new(0), }), locked: Some(RoundValue { - value: proposal.clone().value, + value: proposal.value, round: Round::new(0), }), } @@ -383,8 +360,26 @@ pub fn decided_state(round: Round, _value: Value) -> State { height: Height::new(1), round, step: Step::Commit, - proposal: None, valid: None, locked: None, } } + +pub fn decided_state_with_proposal_and_locked_and_valid( + round: Round, + proposal: Proposal, +) -> State { + State { + height: Height::new(1), + round, + step: Step::Commit, + valid: Some(RoundValue { + value: proposal.value, + round: Round::new(0), + }), + locked: Some(RoundValue { + value: proposal.value, + round: Round::new(0), + }), + } +} diff --git a/Code/test/tests/driver.rs b/Code/test/tests/driver.rs index 62d4ff454..beec2c586 100644 --- a/Code/test/tests/driver.rs +++ b/Code/test/tests/driver.rs @@ -54,7 +54,6 @@ fn driver_steps_proposer() { height: Height::new(1), round: Round::new(0), step: Step::Propose, - proposal: None, locked: None, valid: None, }, @@ -68,7 +67,6 @@ fn driver_steps_proposer() { height: Height::new(1), round: Round::new(0), step: Step::Propose, - proposal: None, locked: None, valid: None, }, @@ -85,7 +83,7 @@ fn driver_steps_proposer() { height: Height::new(1), round: Round::new(0), step: Step::Prevote, - proposal: Some(proposal.clone()), + // proposal: Some(proposal.clone()), locked: None, valid: None, }, @@ -99,7 +97,7 @@ fn driver_steps_proposer() { height: Height::new(1), round: Round::new(0), step: Step::Prevote, - proposal: Some(proposal.clone()), + // proposal: Some(proposal.clone()), locked: None, valid: None, }, @@ -116,7 +114,7 @@ fn driver_steps_proposer() { height: Height::new(1), round: Round::new(0), step: Step::Prevote, - proposal: Some(proposal.clone()), + // proposal: Some(proposal.clone()), locked: None, valid: None, }, @@ -136,7 +134,7 @@ fn driver_steps_proposer() { height: Height::new(1), round: Round::new(0), step: Step::Precommit, - proposal: Some(proposal.clone()), + // proposal: Some(proposal.clone()), locked: Some(RoundValue { value, round: Round::new(0), @@ -156,7 +154,7 @@ fn driver_steps_proposer() { height: Height::new(1), round: Round::new(0), step: Step::Precommit, - proposal: Some(proposal.clone()), + // proposal: Some(proposal.clone()), locked: Some(RoundValue { value, round: Round::new(0), @@ -179,7 +177,7 @@ fn driver_steps_proposer() { height: Height::new(1), round: Round::new(0), step: Step::Precommit, - proposal: Some(proposal.clone()), + // proposal: Some(proposal.clone()), locked: Some(RoundValue { value, round: Round::new(0), @@ -202,7 +200,7 @@ fn driver_steps_proposer() { height: Height::new(1), round: Round::new(0), step: Step::Commit, - proposal: Some(proposal.clone()), + // proposal: Some(proposal.clone()), locked: Some(RoundValue { value, round: Round::new(0), @@ -262,7 +260,6 @@ fn driver_steps_proposer_timeout_get_value() { height: Height::new(1), round: Round::new(0), step: Step::Propose, - proposal: None, locked: None, valid: None, }, @@ -278,7 +275,6 @@ fn driver_steps_proposer_timeout_get_value() { round: Round::new(0), height: Height::new(1), step: Step::Prevote, - proposal: None, locked: None, valid: None, }, @@ -335,7 +331,6 @@ fn driver_steps_not_proposer_valid() { height: Height::new(1), round: Round::new(0), step: Step::Propose, - proposal: None, locked: None, valid: None, }, @@ -352,7 +347,7 @@ fn driver_steps_not_proposer_valid() { height: Height::new(1), round: Round::new(0), step: Step::Prevote, - proposal: Some(proposal.clone()), + // proposal: Some(proposal.clone()), locked: None, valid: None, }, @@ -366,7 +361,7 @@ fn driver_steps_not_proposer_valid() { height: Height::new(1), round: Round::new(0), step: Step::Prevote, - proposal: Some(proposal.clone()), + // proposal: Some(proposal.clone()), locked: None, valid: None, }, @@ -383,7 +378,7 @@ fn driver_steps_not_proposer_valid() { height: Height::new(1), round: Round::new(0), step: Step::Prevote, - proposal: Some(proposal.clone()), + // proposal: Some(proposal.clone()), locked: None, valid: None, }, @@ -403,7 +398,7 @@ fn driver_steps_not_proposer_valid() { height: Height::new(1), round: Round::new(0), step: Step::Precommit, - proposal: Some(proposal.clone()), + // proposal: Some(proposal.clone()), locked: Some(RoundValue { value, round: Round::new(0), @@ -423,7 +418,7 @@ fn driver_steps_not_proposer_valid() { height: Height::new(1), round: Round::new(0), step: Step::Precommit, - proposal: Some(proposal.clone()), + // proposal: Some(proposal.clone()), locked: Some(RoundValue { value, round: Round::new(0), @@ -446,7 +441,7 @@ fn driver_steps_not_proposer_valid() { height: Height::new(1), round: Round::new(0), step: Step::Precommit, - proposal: Some(proposal.clone()), + // proposal: Some(proposal.clone()), locked: Some(RoundValue { value, round: Round::new(0), @@ -469,7 +464,7 @@ fn driver_steps_not_proposer_valid() { height: Height::new(1), round: Round::new(0), step: Step::Commit, - proposal: Some(proposal.clone()), + // proposal: Some(proposal.clone()), locked: Some(RoundValue { value, round: Round::new(0), @@ -532,7 +527,6 @@ fn driver_steps_not_proposer_invalid() { height: Height::new(1), round: Round::new(0), step: Step::Propose, - proposal: None, locked: None, valid: None, }, @@ -548,7 +542,6 @@ fn driver_steps_not_proposer_invalid() { height: Height::new(1), round: Round::new(0), step: Step::Prevote, - proposal: None, locked: None, valid: None, }, @@ -562,7 +555,6 @@ fn driver_steps_not_proposer_invalid() { height: Height::new(1), round: Round::new(0), step: Step::Prevote, - proposal: None, locked: None, valid: None, }, @@ -578,7 +570,6 @@ fn driver_steps_not_proposer_invalid() { height: Height::new(1), round: Round::new(0), step: Step::Prevote, - proposal: None, locked: None, valid: None, }, @@ -594,7 +585,6 @@ fn driver_steps_not_proposer_invalid() { height: Height::new(1), round: Round::new(0), step: Step::Prevote, - proposal: None, locked: None, valid: None, }, @@ -610,7 +600,6 @@ fn driver_steps_not_proposer_invalid() { height: Height::new(1), round: Round::new(0), step: Step::Precommit, - proposal: None, locked: None, valid: None, }, @@ -668,7 +657,6 @@ fn driver_steps_not_proposer_other_height() { height: Height::new(1), round: Round::new(0), step: Step::Propose, - proposal: None, locked: None, valid: None, }, @@ -682,7 +670,6 @@ fn driver_steps_not_proposer_other_height() { height: Height::new(1), round: Round::new(0), step: Step::Propose, - proposal: None, locked: None, valid: None, }, @@ -740,7 +727,6 @@ fn driver_steps_not_proposer_other_round() { height: Height::new(1), round: Round::new(0), step: Step::Propose, - proposal: None, locked: None, valid: None, }, @@ -754,7 +740,6 @@ fn driver_steps_not_proposer_other_round() { height: Height::new(1), round: Round::new(0), step: Step::Propose, - proposal: None, locked: None, valid: None, }, @@ -810,7 +795,6 @@ fn driver_steps_not_proposer_timeout_multiple_rounds() { height: Height::new(1), round: Round::new(0), step: Step::Propose, - proposal: None, locked: None, valid: None, }, @@ -827,7 +811,6 @@ fn driver_steps_not_proposer_timeout_multiple_rounds() { round: Round::new(0), height: Height::new(1), step: Step::Prevote, - proposal: None, locked: None, valid: None, }, @@ -842,7 +825,6 @@ fn driver_steps_not_proposer_timeout_multiple_rounds() { height: Height::new(1), round: Round::new(0), step: Step::Prevote, - proposal: None, locked: None, valid: None, }, @@ -860,7 +842,6 @@ fn driver_steps_not_proposer_timeout_multiple_rounds() { height: Height::new(1), round: Round::new(0), step: Step::Prevote, - proposal: None, locked: None, valid: None, }, @@ -879,7 +860,6 @@ fn driver_steps_not_proposer_timeout_multiple_rounds() { height: Height::new(1), round: Round::new(0), step: Step::Precommit, - proposal: None, locked: None, valid: None, }, @@ -894,7 +874,6 @@ fn driver_steps_not_proposer_timeout_multiple_rounds() { height: Height::new(1), round: Round::new(0), step: Step::Precommit, - proposal: None, locked: None, valid: None, }, @@ -912,7 +891,6 @@ fn driver_steps_not_proposer_timeout_multiple_rounds() { height: Height::new(1), round: Round::new(0), step: Step::Precommit, - proposal: None, locked: None, valid: None, }, @@ -929,7 +907,6 @@ fn driver_steps_not_proposer_timeout_multiple_rounds() { height: Height::new(1), round: Round::new(0), step: Step::Precommit, - proposal: None, locked: None, valid: None, }, @@ -944,7 +921,6 @@ fn driver_steps_not_proposer_timeout_multiple_rounds() { height: Height::new(1), round: Round::new(1), step: Step::NewRound, - proposal: None, locked: None, valid: None, }, @@ -958,7 +934,6 @@ fn driver_steps_not_proposer_timeout_multiple_rounds() { height: Height::new(1), round: Round::new(1), step: Step::Propose, - proposal: None, locked: None, valid: None, }, @@ -1109,7 +1084,6 @@ fn driver_steps_skip_round_skip_threshold() { height, round: Round::new(0), step: Step::Propose, - proposal: None, locked: None, valid: None, }, @@ -1126,7 +1100,6 @@ fn driver_steps_skip_round_skip_threshold() { height, round: Round::new(0), step: Step::Prevote, - proposal: None, locked: None, valid: None, }, @@ -1141,7 +1114,6 @@ fn driver_steps_skip_round_skip_threshold() { height, round: Round::new(0), step: Step::Prevote, - proposal: None, locked: None, valid: None, }, @@ -1158,7 +1130,6 @@ fn driver_steps_skip_round_skip_threshold() { height, round: Round::new(0), step: Step::Prevote, - proposal: None, locked: None, valid: None, }, @@ -1175,7 +1146,6 @@ fn driver_steps_skip_round_skip_threshold() { height, round: Round::new(1), step: Step::NewRound, - proposal: None, locked: None, valid: None, }, @@ -1229,7 +1199,6 @@ fn driver_steps_skip_round_quorum_threshold() { height, round: Round::new(0), step: Step::Propose, - proposal: None, locked: None, valid: None, }, @@ -1246,7 +1215,6 @@ fn driver_steps_skip_round_quorum_threshold() { height, round: Round::new(0), step: Step::Prevote, - proposal: None, locked: None, valid: None, }, @@ -1261,7 +1229,6 @@ fn driver_steps_skip_round_quorum_threshold() { height, round: Round::new(0), step: Step::Prevote, - proposal: None, locked: None, valid: None, }, @@ -1278,7 +1245,6 @@ fn driver_steps_skip_round_quorum_threshold() { height, round: Round::new(0), step: Step::Prevote, - proposal: None, locked: None, valid: None, }, @@ -1295,7 +1261,6 @@ fn driver_steps_skip_round_quorum_threshold() { height, round: Round::new(1), step: Step::NewRound, - proposal: None, locked: None, valid: None, }, diff --git a/Code/test/tests/driver_extra.rs b/Code/test/tests/driver_extra.rs index ff9a3b9a7..7397c6569 100644 --- a/Code/test/tests/driver_extra.rs +++ b/Code/test/tests/driver_extra.rs @@ -7,7 +7,25 @@ use malachite_round::state::State; use malachite_test::{Height, Proposal, TestContext, ValidatorSet, Value}; use malachite_test::utils::*; - +// The following tests are performed: +// - L49 with commits from current rounds, no locked value, no valid value: +// `driver_steps_decide_current_with_no_locked_no_valid()` +// +// - L49 with commits from previous rounds, no locked value, no valid value: +// `driver_steps_decide_previous_with_no_locked_no_valid()` +// +// - L49 with commits from previous round, with locked and valid values +// `driver_steps_decide_previous_with_locked_and_valid()` +// +// - L28 in round 1, via L36 in round 0, with locked invalid value v. +// `driver_steps_polka_previous_invalid_proposal()`' +// +// - L28 in round 1 with no locked value, via L36 in round 0 with step precommit. +// `driver_steps_polka_previous_with_no_locked()` +// +// - L28 in round 1 with locked value, via L36 in round 0 with step prevote. +// `driver_steps_polka_previous_with_locked() +// // TODO - move all below to utils? struct TestStep { desc: &'static str, @@ -192,6 +210,124 @@ fn driver_steps_decide_previous_with_no_locked_no_valid() { run_steps(&mut driver, steps); } +// Arrive at L49 with commits from previous round, with locked and valid values +// +// Ev: NewRound(0) Timeout(propose) Proposal +// State: NewRound ------------> Propose ----------------> Prevote ------------> Prevote ---------------> Precommit --> +// Msg: propose_timer Prevote(nil) prevote_timer Precommit(value) +// Alg: L21 L57 L34 L40 +// +// Ev: NewRound(1) +// State: --> Precommit ---------------------------> NewRound -----------> Propose --------------------> Commit +// Msg: new_round(1) propose_timer decide(v, round=1) +// Alg: L56 L21 L49-L54 +// +// v1=2, v2=3, v3=2, we are v3 +// L21 - start round 0, we, v3, are not the proposer, start timeout propose (step propose) +// L57 - timeout propose, prevote for nil (v2) (step prevote) +// L34 - v1 and v2 prevote for same proposal, we get +2/3 prevotes, start prevote timer (step prevote) +// L37-L43 - v3 receives the proposal, sets locked and value (step precommit) +// L55 - v2 precommits for value in round 1, i.e. vr receives f+1 vote for round 1 from v2 (step new_round) +// L11 - v3 starts new round, not proposer, start timeout propose (step propose) +// L49 - v3 gets +2/3 precommits (from v1 and v2) for round 0, it has the proposal, decide +// +#[test] +fn driver_steps_decide_previous_with_locked_and_valid() { + let value = Value::new(9999); + + let [(v1, sk1), (v2, sk2), (v3, sk3)] = make_validators([2, 3, 2]); + let (my_sk, my_addr) = (sk3.clone(), v3.address); + + let ctx = TestContext::new(my_sk.clone()); + let sel = RotateProposer; + let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); + + let mut driver = Driver::new(ctx, sel, vs, my_addr); + + let steps = vec![ + TestStep { + desc: "Start round 0, we, v3, are not the proposer, start timeout propose", + input: new_round_input(Round::new(0)), + expected_output: start_propose_timer_output(Round::new(0)), + expected_round: Round::new(0), + new_state: propose_state(Round::new(0)), + }, + TestStep { + desc: "Timeout propopse, prevote for nil (v3)", + input: timeout_propose_input(Round::new(0)), + expected_output: prevote_nil_output(Round::new(0), &my_addr, &my_sk), + expected_round: Round::new(0), + new_state: prevote_state(Round::new(0)), + }, + TestStep { + desc: "v1 prevotes a proposal", + input: prevote_input(&v1.address, &sk1), + expected_output: None, + expected_round: Round::new(0), + new_state: prevote_state(Round::new(0)), + }, + TestStep { + desc: "v2 prevotes for same proposal, we get +2/3 prevotes, start prevote timer", + input: prevote_input(&v2.address, &sk2), + expected_output: start_prevote_timer_output(Round::new(0)), + expected_round: Round::new(0), + new_state: prevote_state(Round::new(0)), + }, + TestStep { + desc: "Receive proposal, L37-L43", + input: proposal_input(Round::new(0), value, Round::Nil, Validity::Valid), + expected_output: precommit_output(Round::new(0), Value::new(9999), &v3.address, &sk3), + expected_round: Round::new(0), + new_state: precommit_state_with_proposal_and_locked_and_valid( + Round::new(0), + Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + ), + }, + TestStep { + desc: "v2 precommits for value in round 1, i.e. f+1 vote for round 1 from v2", + input: precommit_input(Round::new(1), Value::new(9999), &v2.address, &sk2), + expected_output: new_round_output(Round::new(1)), + expected_round: Round::new(1), + new_state: new_round_with_proposal_and_locked_and_valid( + Round::new(1), + Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + ), + }, + TestStep { + desc: "Start round 1, we, v3, are not the proposer, start timeout propose", + input: new_round_input(Round::new(1)), + expected_output: start_propose_timer_output(Round::new(1)), + expected_round: Round::new(1), + new_state: propose_state_with_proposal_and_locked_and_valid( + Round::new(1), + Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + ), + }, + TestStep { + desc: "v1 precommits for round 0 and same proposal", + input: precommit_input(Round::new(0), value, &v1.address, &sk1), + expected_output: None, + expected_round: Round::new(1), + new_state: propose_state_with_proposal_and_locked_and_valid( + Round::new(1), + Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + ), + }, + TestStep { + desc: "v2 precommits for round 0 and same proposal, we get +2/3 precommit, decide", + input: precommit_input(Round::new(0), value, &v2.address, &sk2), + expected_output: decide_output(Round::new(0), value), + expected_round: Round::new(1), + new_state: decided_state_with_proposal_and_locked_and_valid( + Round::new(1), + Proposal::new(Height::new(1), Round::new(1), value, Round::Nil), + ), + }, + ]; + + run_steps(&mut driver, steps); +} + // Arrive at L36 in round 0, with step prevote and then L28 in round 1, with locked value v. // // Ev: NewRound(0) Proposal @@ -241,9 +377,9 @@ fn driver_steps_polka_previous_with_locked() { input: proposal_input(Round::new(0), value, Round::Nil, Validity::Valid), expected_output: prevote_output(Round::new(0), &my_addr, &my_sk), expected_round: Round::new(0), - new_state: prevote_state_with_proposal( + new_state: prevote_state( Round::new(0), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + // Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), ), }, TestStep { @@ -251,9 +387,9 @@ fn driver_steps_polka_previous_with_locked() { input: prevote_input(&v3.address, &sk3), expected_output: None, expected_round: Round::new(0), - new_state: prevote_state_with_proposal( + new_state: prevote_state( Round::new(0), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + // Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), ), }, TestStep { @@ -301,12 +437,33 @@ fn driver_steps_polka_previous_with_locked() { run_steps(&mut driver, steps) } +// Arrive at L36 in round 0, with step precommit and then L28 in round 1 with invalid value. +// +// Ev: NewRound(0) Timeout(propose) +// State: NewRound ------------> Propose --------------> Prevote -------------> Prevote ---------------------------> NewRound --> +// Msg: propose_timer prevote(nil) prevote_timer new_round(1) +// Alg: L21 L59 L35 L56 +// +// Ev: NewRound(1) InvalidProposal(round=0) +// State: --> NewRound ---------------> Propose -------------------------> Prevote +// Msg: propose_timer prevote(nil,round=1) +// Alg: L21 L28-L32 +// +// v1=2, v2=3, v3=2, we are v3 +// L21 - v3 is not proposer starts timeout propose (step propose) +// L57 - propose timeout, prevote nil (step prevote) +// L37 - v1 and v2 prevote for v, v3 gets +2/3 prevotes, start timeout prevote (step prevote) +// L55 - Receive f+1 vote for round 1 from v2, start new round (step new_round) +// L21 - v3 is not proposer starts timeout propose (step propose) +// L28 - v2 receives invalid proposal and has 2f+1 prevotes from round 0 and: +// L29 - with invalid proposal +// L32 - v2 sends prevote(nil, round=1) (step prevote) #[test] -fn driver_steps_polka_previous_invalid_proposal_with_locked() { +fn driver_steps_polka_previous_invalid_proposal() { let value = Value::new(9999); - let [(v1, sk1), (v2, sk2), (v3, sk3)] = make_validators([2, 2, 3]); - let (my_sk, my_addr) = (sk2, v2.address); + let [(v1, sk1), (v2, sk2), (v3, sk3)] = make_validators([2, 3, 2]); + let (my_sk, my_addr) = (sk3, v3.address); let ctx = TestContext::new(my_sk.clone()); let sel = RotateProposer; @@ -316,71 +473,53 @@ fn driver_steps_polka_previous_invalid_proposal_with_locked() { let steps = vec![ TestStep { - desc: "Start round 0, we, v2, are not the proposer, start timeout propose", + desc: "Start round 0, we, v3, are not the proposer, start timeout propose", input: new_round_input(Round::new(0)), expected_output: start_propose_timer_output(Round::new(0)), expected_round: Round::new(0), new_state: propose_state(Round::new(0)), }, TestStep { - desc: "receive a proposal from v1 - L22 send prevote", - input: proposal_input(Round::new(0), value, Round::Nil, Validity::Valid), - expected_output: prevote_output(Round::new(0), &my_addr, &my_sk), + desc: "timeout propopse, prevote for nil (v3)", + input: timeout_propose_input(Round::new(0)), + expected_output: prevote_nil_output(Round::new(0), &my_addr, &my_sk), expected_round: Round::new(0), - new_state: prevote_state_with_proposal( - Round::new(0), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), - ), + new_state: prevote_state(Round::new(0)), }, TestStep { - desc: "v3 prevotes the proposal", - input: prevote_input(&v3.address, &sk3), + desc: "v1 prevotes a proposal", + input: prevote_input(&v1.address, &sk1), expected_output: None, expected_round: Round::new(0), - new_state: prevote_state_with_proposal( - Round::new(0), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), - ), + new_state: prevote_state(Round::new(0)), }, TestStep { - desc: "v1 prevotes for same proposal, we get +2/3 prevotes, precommit", - input: prevote_input(&v1.address, &sk1), - expected_output: precommit_output(Round::new(0), value, &my_addr, &my_sk), + desc: "v2 prevotes for same proposal, we get +2/3 prevotes, start prevote timer", + input: prevote_input(&v2.address, &sk2), + expected_output: start_prevote_timer_output(Round::new(0)), expected_round: Round::new(0), - new_state: precommit_state_with_proposal_and_locked_and_valid( - Round::new(0), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), - ), + new_state: prevote_state(Round::new(0)), }, TestStep { desc: "Receive f+1 vote for round 1 from v3", - input: precommit_input(Round::new(1), Value::new(8888), &v3.address, &sk3), + input: prevote_input_at(Round::new(1), &v2.address, &sk2), expected_output: new_round_output(Round::new(1)), expected_round: Round::new(1), - new_state: new_round_with_proposal_and_locked_and_valid( - Round::new(1), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), - ), + new_state: new_round(Round::new(1)), }, TestStep { - desc: "start round 1, we are proposer with a valid value, propose it", + desc: "start round 1, we, v3, are not the proposer, start timeout propose", input: new_round_input(Round::new(1)), - expected_output: proposal_output(Round::new(1), value, Round::new(0)), + expected_output: start_propose_timer_output(Round::new(1)), expected_round: Round::new(1), - new_state: propose_state_with_proposal_and_locked_and_valid( - Round::new(1), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), - ), + new_state: propose_state(Round::new(1)), }, TestStep { - desc: "Receive our own proposal", + desc: "receive an invalid proposal for round 0", input: proposal_input(Round::new(1), value, Round::new(0), Validity::Invalid), expected_output: prevote_nil_output(Round::new(1), &my_addr, &my_sk), expected_round: Round::new(1), - new_state: prevote_state_with_proposal_and_locked_and_valid( - Round::new(1), - Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), - ), + new_state: prevote_state(Round::new(1)), }, ]; diff --git a/Code/vote/src/keeper.rs b/Code/vote/src/keeper.rs index e81001f6d..a0e500712 100644 --- a/Code/vote/src/keeper.rs +++ b/Code/vote/src/keeper.rs @@ -118,24 +118,24 @@ where weight: Weight, current_round: Round, ) -> Option>> { - let round = self + let per_round = self .per_round .entry(vote.round()) .or_insert_with(PerRound::new); - round.votes.add_vote( + per_round.votes.add_vote( vote.vote_type(), vote.validator_address().clone(), vote.value().clone(), weight, ); - round + per_round .addresses_weights .set_once(vote.validator_address().clone(), weight); if vote.round() > current_round { - let combined_weight = round.addresses_weights.sum(); + let combined_weight = per_round.addresses_weights.sum(); let skip_round = self .threshold_params @@ -143,26 +143,26 @@ where .is_met(combined_weight, self.total_weight); if skip_round { - let msg = Output::SkipRound(vote.round()); - round.emitted_outputs.insert(msg.clone()); - return Some(msg); + let output = Output::SkipRound(vote.round()); + per_round.emitted_outputs.insert(output.clone()); + return Some(output); } } let threshold = compute_threshold( vote.vote_type(), - round, + per_round, vote.value(), self.threshold_params.quorum, self.total_weight, ); - let msg = threshold_to_output(vote.vote_type(), vote.round(), threshold); + let output = threshold_to_output(vote.vote_type(), vote.round(), threshold); - match msg { - Some(msg) if !round.emitted_outputs.contains(&msg) => { - round.emitted_outputs.insert(msg.clone()); - Some(msg) + match output { + Some(output) if !per_round.emitted_outputs.contains(&output) => { + per_round.emitted_outputs.insert(output.clone()); + Some(output) } _ => None, } From 20b4ef3509fc0dce86c4e6ee0b70d47e82bf38eb Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 6 Dec 2023 15:23:49 +0100 Subject: [PATCH 2/9] Manually derive impls on round `Input` to avoid inferred bounds on `Ctx` --- Code/round/src/input.rs | 117 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/Code/round/src/input.rs b/Code/round/src/input.rs index c095f4dcd..33b0df1c6 100644 --- a/Code/round/src/input.rs +++ b/Code/round/src/input.rs @@ -1,6 +1,7 @@ +use core::fmt; + use malachite_common::{Context, Round, ValueId}; -#[derive(Clone, Debug, PartialEq, Eq)] pub enum Input where Ctx: Context, @@ -73,3 +74,117 @@ where /// L65 TimeoutPrecommit, } + +impl Clone for Input { + fn clone(&self) -> Self { + match self { + Input::NewRound => Input::NewRound, + Input::ProposeValue(value) => Input::ProposeValue(value.clone()), + Input::Proposal(proposal) => Input::Proposal(proposal.clone()), + Input::InvalidProposal => Input::InvalidProposal, + Input::ProposalAndPolkaPrevious(proposal) => { + Input::ProposalAndPolkaPrevious(proposal.clone()) + } + Input::InvalidProposalAndPolkaPrevious(proposal) => { + Input::InvalidProposalAndPolkaPrevious(proposal.clone()) + } + Input::PolkaValue(value_id) => Input::PolkaValue(value_id.clone()), + Input::PolkaAny => Input::PolkaAny, + Input::PolkaNil => Input::PolkaNil, + Input::ProposalAndPolkaCurrent(proposal) => { + Input::ProposalAndPolkaCurrent(proposal.clone()) + } + Input::PrecommitAny => Input::PrecommitAny, + Input::ProposalAndPrecommitValue(proposal) => { + Input::ProposalAndPrecommitValue(proposal.clone()) + } + Input::PrecommitValue(value_id) => Input::PrecommitValue(value_id.clone()), + Input::SkipRound(round) => Input::SkipRound(*round), + Input::TimeoutPropose => Input::TimeoutPropose, + Input::TimeoutPrevote => Input::TimeoutPrevote, + Input::TimeoutPrecommit => Input::TimeoutPrecommit, + } + } +} + +impl PartialEq for Input { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Input::NewRound, Input::NewRound) => true, + (Input::ProposeValue(value), Input::ProposeValue(other_value)) => value == other_value, + (Input::Proposal(proposal), Input::Proposal(other_proposal)) => { + proposal == other_proposal + } + (Input::InvalidProposal, Input::InvalidProposal) => true, + ( + Input::ProposalAndPolkaPrevious(proposal), + Input::ProposalAndPolkaPrevious(other_proposal), + ) => proposal == other_proposal, + ( + Input::InvalidProposalAndPolkaPrevious(proposal), + Input::InvalidProposalAndPolkaPrevious(other_proposal), + ) => proposal == other_proposal, + (Input::PolkaValue(value_id), Input::PolkaValue(other_value_id)) => { + value_id == other_value_id + } + (Input::PolkaAny, Input::PolkaAny) => true, + (Input::PolkaNil, Input::PolkaNil) => true, + ( + Input::ProposalAndPolkaCurrent(proposal), + Input::ProposalAndPolkaCurrent(other_proposal), + ) => proposal == other_proposal, + (Input::PrecommitAny, Input::PrecommitAny) => true, + ( + Input::ProposalAndPrecommitValue(proposal), + Input::ProposalAndPrecommitValue(other_proposal), + ) => proposal == other_proposal, + (Input::PrecommitValue(value_id), Input::PrecommitValue(other_value_id)) => { + value_id == other_value_id + } + (Input::SkipRound(round), Input::SkipRound(other_round)) => round == other_round, + (Input::TimeoutPropose, Input::TimeoutPropose) => true, + (Input::TimeoutPrevote, Input::TimeoutPrevote) => true, + (Input::TimeoutPrecommit, Input::TimeoutPrecommit) => true, + _ => false, + } + } +} + +impl Eq for Input {} + +impl fmt::Debug for Input +where + Ctx: Context, + Ctx::Value: fmt::Debug, + Ctx::Proposal: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Input::NewRound => write!(f, "NewRound"), + Input::ProposeValue(value) => write!(f, "ProposeValue({:?})", value), + Input::Proposal(proposal) => write!(f, "Proposal({:?})", proposal), + Input::InvalidProposal => write!(f, "InvalidProposal"), + Input::ProposalAndPolkaPrevious(proposal) => { + write!(f, "ProposalAndPolkaPrevious({:?})", proposal) + } + Input::InvalidProposalAndPolkaPrevious(proposal) => { + write!(f, "InvalidProposalAndPolkaPrevious({:?})", proposal) + } + Input::PolkaValue(value_id) => write!(f, "PolkaValue({:?})", value_id), + Input::PolkaAny => write!(f, "PolkaAny"), + Input::PolkaNil => write!(f, "PolkaNil"), + Input::ProposalAndPolkaCurrent(proposal) => { + write!(f, "ProposalAndPolkaCurrent({:?})", proposal) + } + Input::PrecommitAny => write!(f, "PrecommitAny"), + Input::ProposalAndPrecommitValue(proposal) => { + write!(f, "ProposalAndPrecommitValue({:?})", proposal) + } + Input::PrecommitValue(value_id) => write!(f, "PrecommitValue({:?})", value_id), + Input::SkipRound(round) => write!(f, "SkipRound({:?})", round), + Input::TimeoutPropose => write!(f, "TimeoutPropose"), + Input::TimeoutPrevote => write!(f, "TimeoutPrevote"), + Input::TimeoutPrecommit => write!(f, "TimeoutPrecommit"), + } + } +} From b8e0f9c5876629062676fd98386f949f5c49634b Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 6 Dec 2023 17:19:59 +0100 Subject: [PATCH 3/9] Exclude manual impls from coverage --- Code/round/src/input.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Code/round/src/input.rs b/Code/round/src/input.rs index 33b0df1c6..116d33abf 100644 --- a/Code/round/src/input.rs +++ b/Code/round/src/input.rs @@ -76,6 +76,7 @@ where } impl Clone for Input { + #[cfg_attr(coverage_nightly, coverage(off))] fn clone(&self) -> Self { match self { Input::NewRound => Input::NewRound, @@ -108,6 +109,7 @@ impl Clone for Input { } impl PartialEq for Input { + #[cfg_attr(coverage_nightly, coverage(off))] fn eq(&self, other: &Self) -> bool { match (self, other) { (Input::NewRound, Input::NewRound) => true, @@ -158,6 +160,7 @@ where Ctx::Value: fmt::Debug, Ctx::Proposal: fmt::Debug, { + #[cfg_attr(coverage_nightly, coverage(off))] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Input::NewRound => write!(f, "NewRound"), From 759dfda299a3f105bad17bb4588909c4c4d6fab7 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Thu, 7 Dec 2023 17:53:19 +0100 Subject: [PATCH 4/9] Dont fail the coverage job if it fails to upload the report to Codecov --- .github/workflows/coverage.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 8d424afc1..5adf373a0 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -48,7 +48,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: Code/lcov.info flags: integration - fail_ci_if_error: true + fail_ci_if_error: false mbt-coverage: name: MBT @@ -84,4 +84,4 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: Code/lcov.info flags: mbt - fail_ci_if_error: true + fail_ci_if_error: false From ccdbe8a1ce3654808c5b18e9cbd1424352ec6cbc Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Fri, 8 Dec 2023 11:53:16 +0100 Subject: [PATCH 5/9] code: Add multiplexer (#106) * Add tests (all failing) for polka occuring in propose step * Report coverage even when tests fail * Split event multiplexing into its own function * Small refactor * wip: Add mixer on step changes * Add case in step change mixer for polka value * Revert misakenly deleted comment * Process pending inputs automatically after processing an input (#108) * Cleanup * Naming * Multiplexer refactoring (#110) * Sketch for mixer reorg * Small cleanup * No fail fast on tests * Emit thresholds even when there are no proposals * Debugging * Fix emitted inputs when no proposal * Cleanup * Restore insertion of proposal * Remove debug statements --------- Co-authored-by: Anca Zamfir * Only store a single proposal in the driver (#107) * Only store a single proposal * Check proposal round matches input round * Remove dead code * Turn `multiplex_` functions into methods of the `Driver` * Some cleanup * Re-enable `no_std` in driver --------- Co-authored-by: Anca Zamfir --- .github/workflows/coverage.yml | 2 +- Code/.cargo/config.toml | 4 +- Code/driver/src/driver.rs | 206 ++++++++------------------ Code/driver/src/lib.rs | 2 +- Code/driver/src/mux.rs | 197 +++++++++++++++++++++++++ Code/driver/src/proposals.rs | 48 ------ Code/test/src/utils.rs | 12 +- Code/test/tests/driver.rs | 207 +++----------------------- Code/test/tests/driver_extra.rs | 252 ++++++++++++++++++++++++++++---- Code/vote/src/keeper.rs | 4 +- 10 files changed, 520 insertions(+), 414 deletions(-) create mode 100644 Code/driver/src/mux.rs delete mode 100644 Code/driver/src/proposals.rs diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 5adf373a0..7c24b03f5 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -39,7 +39,7 @@ jobs: - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - name: Generate code coverage - run: cargo llvm-cov nextest --workspace --exclude malachite-itf --all-features --lcov --output-path lcov.info + run: cargo llvm-cov nextest --workspace --exclude malachite-itf --all-features --ignore-run-fail --lcov --output-path lcov.info - name: Generate text report run: cargo llvm-cov report - name: Upload coverage to Codecov diff --git a/Code/.cargo/config.toml b/Code/.cargo/config.toml index e914d4462..77d5cd78e 100644 --- a/Code/.cargo/config.toml +++ b/Code/.cargo/config.toml @@ -1,3 +1,3 @@ [alias] -mbt = "nextest run -p malachite-itf --all-features" -integration = "nextest run --workspace --exclude malachite-itf" +mbt = "nextest run -p malachite-itf --all-features --no-fail-fast" +integration = "nextest run --workspace --exclude malachite-itf --no-fail-fast" diff --git a/Code/driver/src/driver.rs b/Code/driver/src/driver.rs index 137bd2bb7..c6c960fff 100644 --- a/Code/driver/src/driver.rs +++ b/Code/driver/src/driver.rs @@ -1,22 +1,20 @@ use alloc::boxed::Box; +use alloc::vec; +use alloc::vec::Vec; use core::fmt; use malachite_common::{ - Context, Proposal, Round, SignedVote, Timeout, TimeoutStep, Validator, ValidatorSet, Value, - Vote, VoteType, + Context, Proposal, Round, SignedVote, Timeout, TimeoutStep, Validator, ValidatorSet, Vote, }; use malachite_round::input::Input as RoundInput; use malachite_round::output::Output as RoundOutput; -use malachite_round::state::{State as RoundState, Step}; +use malachite_round::state::State as RoundState; use malachite_round::state_machine::Info; -use malachite_vote::keeper::Output as VoteKeeperOutput; use malachite_vote::keeper::VoteKeeper; -use malachite_vote::Threshold; use malachite_vote::ThresholdParams; use crate::input::Input; use crate::output::Output; -use crate::proposals::Proposals; use crate::Error; use crate::ProposerSelector; use crate::Validity; @@ -32,9 +30,10 @@ where pub address: Ctx::Address, pub validator_set: Ctx::ValidatorSet, - pub votes: VoteKeeper, + pub vote_keeper: VoteKeeper, pub round_state: RoundState, - pub proposals: Proposals, + pub proposal: Option, + pub pending_input: Option<(Round, RoundInput)>, } impl Driver @@ -57,9 +56,10 @@ where proposer_selector: Box::new(proposer_selector), address, validator_set, - votes, + vote_keeper: votes, round_state: RoundState::default(), - proposals: Proposals::new(), + proposal: None, + pending_input: None, } } @@ -84,13 +84,33 @@ where Ok(proposer) } - pub async fn execute(&mut self, msg: Input) -> Result>, Error> { + pub async fn process(&mut self, msg: Input) -> Result>, Error> { let round_output = match self.apply(msg).await? { Some(msg) => msg, - None => return Ok(None), + None => return Ok(Vec::new()), }; - let output = match round_output { + let output = self.lift_output(round_output); + let mut outputs = vec![output]; + + self.process_pending(&mut outputs)?; + + Ok(outputs) + } + + fn process_pending(&mut self, outputs: &mut Vec>) -> Result<(), Error> { + while let Some((round, input)) = self.pending_input.take() { + if let Some(round_output) = self.apply_input(round, input)? { + let output = self.lift_output(round_output); + outputs.push(output); + }; + } + + Ok(()) + } + + fn lift_output(&mut self, round_output: RoundOutput) -> Output { + match round_output { RoundOutput::NewRound(round) => Output::NewRound(self.height().clone(), round), RoundOutput::Proposal(proposal) => { @@ -113,9 +133,7 @@ where // TODO: update the state Output::Decide(value.round, value.value) } - }; - - Ok(Some(output)) + } } async fn apply(&mut self, input: Input) -> Result>, Error> { @@ -139,6 +157,7 @@ where } else { self.round_state = RoundState::new(height, round); } + self.apply_input(round, RoundInput::NewRound) } @@ -155,94 +174,12 @@ where proposal: Ctx::Proposal, validity: Validity, ) -> Result>, Error> { - // Check that there is an ongoing round - if self.round_state.round == Round::Nil { - return Ok(None); - } - - // Check that the proposal is for the current height - if self.round_state.height != proposal.height() { - return Ok(None); - } - - self.proposals.insert(proposal.clone()); + let round = proposal.round(); - let polka_for_pol = self.votes.is_threshold_met( - &proposal.pol_round(), - VoteType::Prevote, - Threshold::Value(proposal.value().id()), - ); - - let polka_previous = proposal.pol_round().is_defined() - && polka_for_pol - && proposal.pol_round() < self.round_state.round; - - // Handle invalid proposal - if !validity.is_valid() { - if self.round_state.step == Step::Propose { - if proposal.pol_round().is_nil() { - // L26 - return self.apply_input(proposal.round(), RoundInput::InvalidProposal); - } else if polka_previous { - // L32 - return self.apply_input( - proposal.round(), - RoundInput::InvalidProposalAndPolkaPrevious(proposal), - ); - } else { - return Ok(None); - } - } else { - return Ok(None); - } + match self.multiplex_proposal(proposal, validity) { + Some(round_input) => self.apply_input(round, round_input), + None => Ok(None), } - - // We have a valid proposal. - // L49 - // TODO - check if not already decided - if self.votes.is_threshold_met( - &proposal.round(), - VoteType::Precommit, - Threshold::Value(proposal.value().id()), - ) { - return self.apply_input( - proposal.round(), - RoundInput::ProposalAndPrecommitValue(proposal), - ); - } - - // If the proposal is for a different round, drop the proposal - if self.round() != proposal.round() { - return Ok(None); - } - - let polka_for_current = self.votes.is_threshold_met( - &proposal.round(), - VoteType::Prevote, - Threshold::Value(proposal.value().id()), - ); - - let polka_current = polka_for_current && self.round_state.step >= Step::Prevote; - - // L36 - if polka_current { - return self.apply_input( - proposal.round(), - RoundInput::ProposalAndPolkaCurrent(proposal), - ); - } - - // L28 - if self.round_state.step == Step::Propose && polka_previous { - // TODO: Check proposal vr is equal to threshold vr - return self.apply_input( - proposal.round(), - RoundInput::ProposalAndPolkaPrevious(proposal), - ); - } - - // TODO - Caller needs to store the proposal (valid or not) as the quorum (polka or commits) may be met later - self.apply_input(proposal.round(), RoundInput::Proposal(proposal)) } fn apply_vote( @@ -267,23 +204,20 @@ where let vote_round = signed_vote.vote.round(); let current_round = self.round(); - let Some(vote_output) = - self.votes - .apply_vote(signed_vote.vote, validator.voting_power(), current_round) - else { + let vote_output = + self.vote_keeper + .apply_vote(signed_vote.vote, validator.voting_power(), current_round); + + let Some(vote_output) = vote_output else { return Ok(None); }; - let round_input = match vote_output { - VoteKeeperOutput::PolkaAny => RoundInput::PolkaAny, - VoteKeeperOutput::PolkaNil => RoundInput::PolkaNil, - VoteKeeperOutput::PolkaValue(v) => RoundInput::PolkaValue(v), - VoteKeeperOutput::PrecommitAny => RoundInput::PrecommitAny, - VoteKeeperOutput::PrecommitValue(v) => RoundInput::PrecommitValue(v), - VoteKeeperOutput::SkipRound(r) => RoundInput::SkipRound(r), - }; + let round_input = self.multiplex_vote_threshold(vote_output); - self.apply_input(vote_round, round_input) + match round_input { + Some(input) => self.apply_input(vote_round, input), + None => Ok(None), + } } fn apply_timeout(&mut self, timeout: Timeout) -> Result>, Error> { @@ -303,39 +237,21 @@ where input: RoundInput, ) -> Result>, Error> { let round_state = core::mem::take(&mut self.round_state); - let proposer = self.get_proposer(round_state.round)?; - - let data = Info::new(input_round, &self.address, proposer.address()); + let current_step = round_state.step; - // Multiplex the event with the round state. - let mux_input = match input { - RoundInput::PolkaValue(value_id) => { - let proposal = self.proposals.find(&value_id, |p| p.round() == input_round); - - if let Some(proposal) = proposal { - assert_eq!(proposal.value().id(), value_id); - RoundInput::ProposalAndPolkaCurrent(proposal.clone()) - } else { - RoundInput::PolkaAny - } - } + let proposer = self.get_proposer(round_state.round)?; + let info = Info::new(input_round, &self.address, proposer.address()); - RoundInput::PrecommitValue(value_id) => { - let proposal = self.proposals.find(&value_id, |p| p.round() == input_round); + // Apply the input to the round state machine + let transition = round_state.apply(&info, input); - if let Some(proposal) = proposal { - assert_eq!(proposal.value().id(), value_id); - RoundInput::ProposalAndPrecommitValue(proposal.clone()) - } else { - RoundInput::PrecommitAny - } - } + let pending_step = transition.next_state.step; - _ => input, - }; + if current_step != pending_step { + let pending_input = self.multiplex_step_change(pending_step, input_round); - // Apply the input to the round state machine - let transition = round_state.apply(&data, mux_input); + self.pending_input = pending_input.map(|input| (input_round, input)); + } // Update state self.round_state = transition.next_state; @@ -354,8 +270,8 @@ where f.debug_struct("Driver") .field("address", &self.address) .field("validator_set", &self.validator_set) - .field("votes", &self.votes) - .field("proposals", &self.proposals.proposals) + .field("votes", &self.vote_keeper) + .field("proposal", &self.proposal) .field("round_state", &self.round_state) .finish() } diff --git a/Code/driver/src/lib.rs b/Code/driver/src/lib.rs index fd77260a5..caddfa65e 100644 --- a/Code/driver/src/lib.rs +++ b/Code/driver/src/lib.rs @@ -17,8 +17,8 @@ extern crate alloc; mod driver; mod error; mod input; +mod mux; mod output; -mod proposals; mod proposer; mod util; diff --git a/Code/driver/src/mux.rs b/Code/driver/src/mux.rs new file mode 100644 index 000000000..cb5e31d4d --- /dev/null +++ b/Code/driver/src/mux.rs @@ -0,0 +1,197 @@ +use malachite_common::ValueId; +use malachite_common::{Context, Proposal, Round, Value, VoteType}; +use malachite_round::input::Input as RoundInput; +use malachite_round::state::Step; +use malachite_vote::keeper::Output as VoteKeeperOutput; +use malachite_vote::keeper::VoteKeeper; +use malachite_vote::Threshold; + +use crate::{Driver, Validity}; + +impl Driver +where + Ctx: Context, +{ + pub fn multiplex_proposal( + &mut self, + proposal: Ctx::Proposal, + validity: Validity, + ) -> Option> { + // Check that there is an ongoing round + if self.round_state.round == Round::Nil { + return None; + } + + // Check that the proposal is for the current height + if self.round_state.height != proposal.height() { + return None; + } + + // Store the proposal + self.proposal = Some(proposal.clone()); + + let polka_for_pol = self.vote_keeper.is_threshold_met( + &proposal.pol_round(), + VoteType::Prevote, + Threshold::Value(proposal.value().id()), + ); + + let polka_previous = proposal.pol_round().is_defined() + && polka_for_pol + && proposal.pol_round() < self.round_state.round; + + // Handle invalid proposal + if !validity.is_valid() { + if self.round_state.step == Step::Propose { + if proposal.pol_round().is_nil() { + // L26 + return Some(RoundInput::InvalidProposal); + } else if polka_previous { + // L32 + return Some(RoundInput::InvalidProposalAndPolkaPrevious( + proposal.clone(), + )); + } else { + return None; + } + } else { + return None; + } + } + + // We have a valid proposal. + // L49 + // TODO - check if not already decided + if self.vote_keeper.is_threshold_met( + &proposal.round(), + VoteType::Precommit, + Threshold::Value(proposal.value().id()), + ) { + return Some(RoundInput::ProposalAndPrecommitValue(proposal.clone())); + } + + // If the proposal is for a different round, drop the proposal + if self.round_state.round != proposal.round() { + return None; + } + + let polka_for_current = self.vote_keeper.is_threshold_met( + &proposal.round(), + VoteType::Prevote, + Threshold::Value(proposal.value().id()), + ); + + let polka_current = polka_for_current && self.round_state.step >= Step::Prevote; + + // L36 + if polka_current { + return Some(RoundInput::ProposalAndPolkaCurrent(proposal)); + } + + // L28 + if self.round_state.step == Step::Propose && polka_previous { + // TODO: Check proposal vr is equal to threshold vr + return Some(RoundInput::ProposalAndPolkaPrevious(proposal)); + } + + Some(RoundInput::Proposal(proposal)) + } + + pub fn multiplex_vote_threshold( + &self, + new_threshold: VoteKeeperOutput>, + ) -> Option> { + if let Some(proposal) = &self.proposal { + match new_threshold { + VoteKeeperOutput::PolkaAny => Some(RoundInput::PolkaAny), + VoteKeeperOutput::PolkaNil => Some(RoundInput::PolkaNil), + VoteKeeperOutput::PolkaValue(v) => { + if v == proposal.value().id() { + Some(RoundInput::ProposalAndPolkaCurrent(proposal.clone())) + } else { + Some(RoundInput::PolkaAny) + } + } + VoteKeeperOutput::PrecommitAny => Some(RoundInput::PrecommitAny), + VoteKeeperOutput::PrecommitValue(v) => { + if v == proposal.value().id() { + Some(RoundInput::ProposalAndPrecommitValue(proposal.clone())) + } else { + Some(RoundInput::PrecommitAny) + } + } + VoteKeeperOutput::SkipRound(r) => Some(RoundInput::SkipRound(r)), + } + } else { + match new_threshold { + VoteKeeperOutput::PolkaAny => Some(RoundInput::PolkaAny), + VoteKeeperOutput::PolkaNil => Some(RoundInput::PolkaNil), + VoteKeeperOutput::PolkaValue(_) => Some(RoundInput::PolkaAny), + VoteKeeperOutput::PrecommitAny => Some(RoundInput::PrecommitAny), + VoteKeeperOutput::PrecommitValue(_) => Some(RoundInput::PrecommitAny), + VoteKeeperOutput::SkipRound(r) => Some(RoundInput::SkipRound(r)), + } + } + } + + pub fn multiplex_step_change( + &self, + pending_step: Step, + round: Round, + ) -> Option> { + match pending_step { + Step::NewRound => None, // Some(RoundInput::NewRound), + + Step::Prevote => { + if has_polka_nil(&self.vote_keeper, round) { + Some(RoundInput::PolkaNil) + } else if let Some(proposal) = + has_polka_value(&self.vote_keeper, round, self.proposal.as_ref()) + { + Some(RoundInput::ProposalAndPolkaCurrent(proposal.clone())) + } else if has_polka_any(&self.vote_keeper, round) { + Some(RoundInput::PolkaAny) + } else { + None + } + } + + Step::Propose => None, + Step::Precommit => None, + Step::Commit => None, + } + } +} + +fn has_polka_nil(votekeeper: &VoteKeeper, round: Round) -> bool +where + Ctx: Context, +{ + votekeeper.is_threshold_met(&round, VoteType::Prevote, Threshold::Nil) +} + +fn has_polka_value<'p, Ctx>( + votekeeper: &VoteKeeper, + round: Round, + proposal: Option<&'p Ctx::Proposal>, +) -> Option<&'p Ctx::Proposal> +where + Ctx: Context, +{ + let proposal = proposal?; + + votekeeper + .is_threshold_met( + &round, + VoteType::Prevote, + Threshold::Value(proposal.value().id()), + ) + .then_some(proposal) +} + +fn has_polka_any(votekeeper: &VoteKeeper, round: Round) -> bool +where + Ctx: Context, +{ + votekeeper.is_threshold_met(&round, VoteType::Prevote, Threshold::Any) +} diff --git a/Code/driver/src/proposals.rs b/Code/driver/src/proposals.rs deleted file mode 100644 index cf0173efe..000000000 --- a/Code/driver/src/proposals.rs +++ /dev/null @@ -1,48 +0,0 @@ -use alloc::collections::BTreeMap; -use alloc::vec::Vec; - -use malachite_common::ValueId; -use malachite_common::{Context, Proposal, Value}; - -/// Stores proposals at each round, indexed by their value id. -pub struct Proposals -where - Ctx: Context, -{ - pub(crate) proposals: BTreeMap, Vec>, -} - -impl Proposals -where - Ctx: Context, -{ - pub fn new() -> Self { - Self { - proposals: BTreeMap::new(), - } - } - - pub fn insert(&mut self, proposal: Ctx::Proposal) { - let value_id = proposal.value().id(); - self.proposals.entry(value_id).or_default().push(proposal); - } - - pub fn find( - &self, - value_id: &ValueId, - p: impl Fn(&Ctx::Proposal) -> bool, - ) -> Option<&Ctx::Proposal> { - self.proposals - .get(value_id) - .and_then(|proposals| proposals.iter().find(|proposal| p(proposal))) - } -} - -impl Default for Proposals -where - Ctx: Context, -{ - fn default() -> Self { - Self::new() - } -} diff --git a/Code/test/src/utils.rs b/Code/test/src/utils.rs index 4dfbe5092..a77367496 100644 --- a/Code/test/src/utils.rs +++ b/Code/test/src/utils.rs @@ -109,6 +109,10 @@ pub fn prevote_input(addr: &Address, sk: &PrivateKey) -> Input { ) } +pub fn prevote_nil_input(addr: &Address, sk: &PrivateKey) -> Input { + Input::Vote(Vote::new_prevote(Height::new(1), Round::new(0), None, *addr).signed(sk)) +} + pub fn prevote_input_at(round: Round, addr: &Address, sk: &PrivateKey) -> Input { let value = Value::new(9999); @@ -126,9 +130,13 @@ pub fn precommit_output( )) } -pub fn precommit_nil_output(addr: &Address, sk: &PrivateKey) -> Option> { +pub fn precommit_nil_output( + round: Round, + addr: &Address, + sk: &PrivateKey, +) -> Option> { Some(Output::Vote( - Vote::new_precommit(Height::new(1), Round::new(0), None, *addr).signed(sk), + Vote::new_precommit(Height::new(1), round, None, *addr).signed(sk), )) } diff --git a/Code/test/tests/driver.rs b/Code/test/tests/driver.rs index beec2c586..69cf3cb7e 100644 --- a/Code/test/tests/driver.rs +++ b/Code/test/tests/driver.rs @@ -83,7 +83,6 @@ fn driver_steps_proposer() { height: Height::new(1), round: Round::new(0), step: Step::Prevote, - // proposal: Some(proposal.clone()), locked: None, valid: None, }, @@ -97,7 +96,6 @@ fn driver_steps_proposer() { height: Height::new(1), round: Round::new(0), step: Step::Prevote, - // proposal: Some(proposal.clone()), locked: None, valid: None, }, @@ -114,7 +112,6 @@ fn driver_steps_proposer() { height: Height::new(1), round: Round::new(0), step: Step::Prevote, - // proposal: Some(proposal.clone()), locked: None, valid: None, }, @@ -134,7 +131,6 @@ fn driver_steps_proposer() { height: Height::new(1), round: Round::new(0), step: Step::Precommit, - // proposal: Some(proposal.clone()), locked: Some(RoundValue { value, round: Round::new(0), @@ -154,7 +150,6 @@ fn driver_steps_proposer() { height: Height::new(1), round: Round::new(0), step: Step::Precommit, - // proposal: Some(proposal.clone()), locked: Some(RoundValue { value, round: Round::new(0), @@ -177,7 +172,6 @@ fn driver_steps_proposer() { height: Height::new(1), round: Round::new(0), step: Step::Precommit, - // proposal: Some(proposal.clone()), locked: Some(RoundValue { value, round: Round::new(0), @@ -200,7 +194,6 @@ fn driver_steps_proposer() { height: Height::new(1), round: Round::new(0), step: Step::Commit, - // proposal: Some(proposal.clone()), locked: Some(RoundValue { value, round: Round::new(0), @@ -213,27 +206,7 @@ fn driver_steps_proposer() { }, ]; - let mut output_from_prev_input = None; - - for step in steps { - println!("Step: {}", step.desc); - - let input = step - .input - .unwrap_or_else(|| output_from_prev_input.unwrap()); - - let output = block_on(driver.execute(input)).expect("execute succeeded"); - assert_eq!(output, step.expected_output, "expected output"); - - assert_eq!( - driver.round_state.round, step.expected_round, - "expected round" - ); - - assert_eq!(driver.round_state, step.new_state, "expected state"); - - output_from_prev_input = output.and_then(output_to_input); - } + run_steps(&mut driver, steps); } #[test] @@ -281,27 +254,7 @@ fn driver_steps_proposer_timeout_get_value() { }, ]; - let mut output_from_prev_input = None; - - for step in steps { - println!("Step: {}", step.desc); - - let input = step - .input - .unwrap_or_else(|| output_from_prev_input.unwrap()); - - let output = block_on(driver.execute(input)).expect("execute succeeded"); - assert_eq!(output, step.expected_output, "expected output"); - - assert_eq!( - driver.round_state.round, step.expected_round, - "expected round" - ); - - assert_eq!(driver.round_state, step.new_state, "expected state"); - - output_from_prev_input = output.and_then(output_to_input); - } + run_steps(&mut driver, steps); } #[test] @@ -347,7 +300,6 @@ fn driver_steps_not_proposer_valid() { height: Height::new(1), round: Round::new(0), step: Step::Prevote, - // proposal: Some(proposal.clone()), locked: None, valid: None, }, @@ -361,7 +313,6 @@ fn driver_steps_not_proposer_valid() { height: Height::new(1), round: Round::new(0), step: Step::Prevote, - // proposal: Some(proposal.clone()), locked: None, valid: None, }, @@ -378,7 +329,6 @@ fn driver_steps_not_proposer_valid() { height: Height::new(1), round: Round::new(0), step: Step::Prevote, - // proposal: Some(proposal.clone()), locked: None, valid: None, }, @@ -398,7 +348,6 @@ fn driver_steps_not_proposer_valid() { height: Height::new(1), round: Round::new(0), step: Step::Precommit, - // proposal: Some(proposal.clone()), locked: Some(RoundValue { value, round: Round::new(0), @@ -418,7 +367,6 @@ fn driver_steps_not_proposer_valid() { height: Height::new(1), round: Round::new(0), step: Step::Precommit, - // proposal: Some(proposal.clone()), locked: Some(RoundValue { value, round: Round::new(0), @@ -441,7 +389,6 @@ fn driver_steps_not_proposer_valid() { height: Height::new(1), round: Round::new(0), step: Step::Precommit, - // proposal: Some(proposal.clone()), locked: Some(RoundValue { value, round: Round::new(0), @@ -464,7 +411,6 @@ fn driver_steps_not_proposer_valid() { height: Height::new(1), round: Round::new(0), step: Step::Commit, - // proposal: Some(proposal.clone()), locked: Some(RoundValue { value, round: Round::new(0), @@ -477,27 +423,7 @@ fn driver_steps_not_proposer_valid() { }, ]; - let mut output_from_prev_input = None; - - for step in steps { - println!("Step: {}", step.desc); - - let input = step - .input - .unwrap_or_else(|| output_from_prev_input.unwrap()); - - let output = block_on(driver.execute(input)).expect("execute succeeded"); - assert_eq!(output, step.expected_output, "expected output"); - - assert_eq!( - driver.round_state.round, step.expected_round, - "expected round" - ); - - assert_eq!(driver.round_state, step.new_state, "expected state"); - - output_from_prev_input = output.and_then(output_to_input); - } + run_steps(&mut driver, steps); } #[test] @@ -606,27 +532,7 @@ fn driver_steps_not_proposer_invalid() { }, ]; - let mut output_from_prev_input = None; - - for step in steps { - println!("Step: {}", step.desc); - - let input = step - .input - .unwrap_or_else(|| output_from_prev_input.unwrap()); - - let output = block_on(driver.execute(input)).expect("execute succeeded"); - assert_eq!(output, step.expected_output, "expected output"); - - assert_eq!( - driver.round_state.round, step.expected_round, - "expected round" - ); - - assert_eq!(driver.round_state, step.new_state, "expected state"); - - output_from_prev_input = output.and_then(output_to_input); - } + run_steps(&mut driver, steps); } #[test] @@ -676,27 +582,7 @@ fn driver_steps_not_proposer_other_height() { }, ]; - let mut output_from_prev_input = None; - - for step in steps { - println!("Step: {}", step.desc); - - let input = step - .input - .unwrap_or_else(|| output_from_prev_input.unwrap()); - - let output = block_on(driver.execute(input)).expect("execute succeeded"); - assert_eq!(output, step.expected_output, "expected output"); - - assert_eq!( - driver.round_state.round, step.expected_round, - "expected round" - ); - - assert_eq!(driver.round_state, step.new_state, "expected state"); - - output_from_prev_input = output.and_then(output_to_input); - } + run_steps(&mut driver, steps); } #[test] @@ -746,27 +632,7 @@ fn driver_steps_not_proposer_other_round() { }, ]; - let mut output_from_prev_input = None; - - for step in steps { - println!("Step: {}", step.desc); - - let input = step - .input - .unwrap_or_else(|| output_from_prev_input.unwrap()); - - let output = block_on(driver.execute(input)).expect("execute succeeded"); - assert_eq!(output, step.expected_output, "expected output"); - - assert_eq!( - driver.round_state.round, step.expected_round, - "expected round" - ); - - assert_eq!(driver.round_state, step.new_state, "expected state"); - - output_from_prev_input = output.and_then(output_to_input); - } + run_steps(&mut driver, steps); } #[test] @@ -916,7 +782,7 @@ fn driver_steps_not_proposer_timeout_multiple_rounds() { desc: "we receive a precommit timeout, start a new round", input: Some(Input::TimeoutElapsed(Timeout::precommit(Round::new(0)))), expected_output: Some(Output::NewRound(Height::new(1), Round::new(1))), - expected_round: Round::new(0), + expected_round: Round::new(1), new_state: State { height: Height::new(1), round: Round::new(1), @@ -940,22 +806,7 @@ fn driver_steps_not_proposer_timeout_multiple_rounds() { }, ]; - let mut output_from_prev_input = None; - - for step in steps { - println!("Step: {}", step.desc); - - let input = step - .input - .unwrap_or_else(|| output_from_prev_input.unwrap()); - - let output = block_on(driver.execute(input)).expect("execute succeeded"); - assert_eq!(output, step.expected_output, "expected output"); - - assert_eq!(driver.round_state, step.new_state, "new state"); - - output_from_prev_input = output.and_then(output_to_input); - } + run_steps(&mut driver, steps); } // No value to propose @@ -971,9 +822,11 @@ fn driver_steps_no_value_to_propose() { let mut driver = Driver::new(ctx, sel, vs, my_addr); - let output = block_on(driver.execute(Input::NewRound(Height::new(1), Round::new(0)))) + let mut outputs = block_on(driver.process(Input::NewRound(Height::new(1), Round::new(0)))) .expect("execute succeeded"); + let output = outputs.pop(); + assert_eq!( output, Some(Output::GetValueAndScheduleTimeout( @@ -997,7 +850,7 @@ fn driver_steps_proposer_not_found() { let mut driver = Driver::new(ctx, sel, vs, my_addr); - let output = block_on(driver.execute(Input::NewRound(Height::new(1), Round::new(0)))); + let output = block_on(driver.process(Input::NewRound(Height::new(1), Round::new(0)))); assert_eq!(output, Err(Error::ProposerNotFound(v1.address))); } @@ -1018,11 +871,11 @@ fn driver_steps_validator_not_found() { let mut driver = Driver::new(ctx, sel, vs, my_addr); // Start new height - block_on(driver.execute(Input::NewRound(Height::new(1), Round::new(0)))) + block_on(driver.process(Input::NewRound(Height::new(1), Round::new(0)))) .expect("execute succeeded"); // v2 prevotes for some proposal, we cannot find it in the validator set => error - let output = block_on(driver.execute(Input::Vote( + let output = block_on(driver.process(Input::Vote( Vote::new_prevote(Height::new(1), Round::new(0), Some(value.id()), v2.address).signed(&sk2), ))); @@ -1044,12 +897,12 @@ fn driver_steps_invalid_signature() { let mut driver = Driver::new(ctx, sel, vs, my_addr); // Start new round - block_on(driver.execute(Input::NewRound(Height::new(1), Round::new(0)))) + block_on(driver.process(Input::NewRound(Height::new(1), Round::new(0)))) .expect("execute succeeded"); // v2 prevotes for some proposal, with an invalid signature, // ie. signed by v1 instead of v2, just a way of forging an invalid signature - let output = block_on(driver.execute(Input::Vote( + let output = block_on(driver.process(Input::Vote( Vote::new_prevote(Height::new(1), Round::new(0), Some(value.id()), v2.address).signed(&sk1), ))); @@ -1152,23 +1005,7 @@ fn driver_steps_skip_round_skip_threshold() { }, ]; - let mut output_from_prev_input = None; - - for step in steps { - println!("Step: {}", step.desc); - - let input = step - .input - .unwrap_or_else(|| output_from_prev_input.unwrap()); - - let output = block_on(driver.execute(input)).expect("execute succeeded"); - assert_eq!(output, step.expected_output, "expected output"); - - assert_eq!(driver.round(), step.expected_round, "expected round"); - assert_eq!(driver.round_state, step.new_state, "new state"); - - output_from_prev_input = output.and_then(output_to_input); - } + run_steps(&mut driver, steps); } #[test] @@ -1267,6 +1104,10 @@ fn driver_steps_skip_round_quorum_threshold() { }, ]; + run_steps(&mut driver, steps); +} + +fn run_steps(driver: &mut Driver, steps: Vec) { let mut input_from_prev_output = None; for step in steps { @@ -1276,11 +1117,11 @@ fn driver_steps_skip_round_quorum_threshold() { .input .unwrap_or_else(|| input_from_prev_output.unwrap()); - let output = block_on(driver.execute(input)).expect("execute succeeded"); - assert_eq!(output, step.expected_output, "expected output"); + let mut outputs = block_on(driver.process(input)).expect("execute succeeded"); + let output = outputs.pop(); + assert_eq!(output, step.expected_output, "expected output"); assert_eq!(driver.round(), step.expected_round, "expected round"); - assert_eq!(driver.round_state, step.new_state, "new state"); input_from_prev_output = output.and_then(output_to_input); diff --git a/Code/test/tests/driver_extra.rs b/Code/test/tests/driver_extra.rs index 7397c6569..afcfe841f 100644 --- a/Code/test/tests/driver_extra.rs +++ b/Code/test/tests/driver_extra.rs @@ -26,6 +26,15 @@ use malachite_test::utils::*; // - L28 in round 1 with locked value, via L36 in round 0 with step prevote. // `driver_steps_polka_previous_with_locked() // +// - L44 with previously received polkaNil and entering prevote (due to timeoutPropose) +// `driver_steps_polka_nil_and_timout_propose()` +// +// - L36 with previoustly received polkaValue and proposal, and entering prevote (due to received proposal) +// `driver_steps_polka_value_then_proposal()` +// +// - L34 with previously received polkaAny and entering prevote (due to received poposal) +// `driver_steps_polka_any_then_proposal_other()` + // TODO - move all below to utils? struct TestStep { desc: &'static str, @@ -74,7 +83,7 @@ fn driver_steps_decide_current_with_no_locked_no_valid() { let steps = vec![ TestStep { - desc: "Start round 0, we, v2, are not the proposer, start timeout propose", + desc: "Start round 0, we, v3, are not the proposer, start timeout propose", input: new_round_input(Round::new(0)), expected_output: start_propose_timer_output(Round::new(0)), expected_round: Round::new(0), @@ -117,10 +126,10 @@ fn driver_steps_decide_current_with_no_locked_no_valid() { // Msg: propose_timer Prevote(nil) prevote_timer Precommit(nil) // Alg: L21 L57 L34 L61 // -// Ev: Timeout(precommit) NewRound(1) Proposal+ -// State: --> Precommit ----------> Precommit ---------------> NewRound -----------> Propose -----------------> Decided -// Msg: precommit_timer new_round(1) propose_timer decide -// Alg: L46 L65 L21 L49 +// Ev: Timeout(precommit) NewRound(1) Proposal+ +// State: Precommit ----------> Precommit ---------------> NewRound -----------> Propose -----------------> Decided +// Msg: precommit_timer new_round(1) propose_timer decide +// Alg: L46 L65 L21 L49 // // v1=2, v2=3, v3=2, we are v3 // L21 - v3 is not proposer starts propose timer (step propose) @@ -146,14 +155,14 @@ fn driver_steps_decide_previous_with_no_locked_no_valid() { let steps = vec![ TestStep { - desc: "Start round 0, we, v2, are not the proposer, start timeout propose", + desc: "Start round 0, we, v3, are not the proposer, start timeout propose", input: new_round_input(Round::new(0)), expected_output: start_propose_timer_output(Round::new(0)), expected_round: Round::new(0), new_state: propose_state(Round::new(0)), }, TestStep { - desc: "Timeout propopse, prevote for nil (v2)", + desc: "Timeout propopse, prevote for nil (v3)", input: timeout_propose_input(Round::new(0)), expected_output: prevote_nil_output(Round::new(0), &my_addr, &my_sk), expected_round: Round::new(0), @@ -217,10 +226,10 @@ fn driver_steps_decide_previous_with_no_locked_no_valid() { // Msg: propose_timer Prevote(nil) prevote_timer Precommit(value) // Alg: L21 L57 L34 L40 // -// Ev: NewRound(1) -// State: --> Precommit ---------------------------> NewRound -----------> Propose --------------------> Commit -// Msg: new_round(1) propose_timer decide(v, round=1) -// Alg: L56 L21 L49-L54 +// Ev: NewRound(1) +// State: Precommit ---------------------------> NewRound -----------> Propose --------------------> Commit +// Msg: new_round(1) propose_timer decide(v, round=1) +// Alg: L56 L21 L49-L54 // // v1=2, v2=3, v3=2, we are v3 // L21 - start round 0, we, v3, are not the proposer, start timeout propose (step propose) @@ -335,10 +344,10 @@ fn driver_steps_decide_previous_with_locked_and_valid() { // Msg: propose_timer prevote(v) precommit(v) new_round(1) // Alg: L21 L24 L37 L56 // -// Ev: NewRound(1) Proposal(+polka) -// State: --> NewRound ---------------> Propose ------------------> Prevote -// Msg: propose(v, pol) prevote(v,round=1) -// Alg: L16, L19 L28-L30 +// Ev: NewRound(1) Proposal(+polka) +// State: NewRound --------------> Propose -----------------> Prevote +// Msg: propose(v, pol) prevote(v,round=1) +// Alg: L16, L19 L28-L30 // // v1=2, v2=2, v3=3, we are v2 // Trying to arrive at L36 with step prevote and then L28 @@ -444,10 +453,10 @@ fn driver_steps_polka_previous_with_locked() { // Msg: propose_timer prevote(nil) prevote_timer new_round(1) // Alg: L21 L59 L35 L56 // -// Ev: NewRound(1) InvalidProposal(round=0) -// State: --> NewRound ---------------> Propose -------------------------> Prevote -// Msg: propose_timer prevote(nil,round=1) -// Alg: L21 L28-L32 +// Ev: NewRound(1) InvalidProposal(round=0) +// State: NewRound ---------------> Propose -----------------------> Prevote +// Msg: propose_timer prevote(nil,round=1) +// Alg: L21 L28-L32 // // v1=2, v2=3, v3=2, we are v3 // L21 - v3 is not proposer starts timeout propose (step propose) @@ -533,15 +542,15 @@ fn driver_steps_polka_previous_invalid_proposal() { // Msg: propose_timer Prevote(nil) prevote_timer Precommit(nil) // Alg: L21 L59 L34 L63 // -// Ev: Proposal(v) -// State: --> Precommit ---------------> Precommit ---------------------------> NewRound -// Msg: none new_round(1) -// Alg: L42, L43 L56 +// Ev: Proposal(v) +// State: Precommit ----------> Precommit --------------------------> NewRound --> +// Msg: none new_round(1) +// Alg: L42, L43 L56 // -// Ev: NewRound(1) Proposal(+polka) -// State: --> NewRound ---------------> Propose -------------------> Prevote -// Msg: propose(v, pol) prevote(nil,round=1) -// Alg: L16, L19 L28, L32 (not locked on v) +// Ev: NewRound(1) Proposal(+polka) +// State: NewRound --------------> Propose -------------------------> Prevote +// Msg: propose(v, pol) prevote(nil,round=1) +// Alg: L16, L19 L28, L32 (not locked on v) // // v1=2, v2=2, v3=3, we are v2 // Trying to be at L36 with step precommit @@ -571,7 +580,7 @@ fn driver_steps_polka_previous_with_no_locked() { let steps = vec![ TestStep { - desc: "Start round 0, we v2 are not the proposer, start timeout propose", + desc: "Start round 0, we, v2, are not the proposer, start timeout propose", input: new_round_input(Round::new(0)), expected_output: start_propose_timer_output(Round::new(0)), expected_round: Round::new(0), @@ -601,7 +610,7 @@ fn driver_steps_polka_previous_with_no_locked() { TestStep { desc: "timeout prevote, prevote for nil (v2)", input: timeout_prevote_input(Round::new(0)), - expected_output: precommit_nil_output(&my_addr, &my_sk), + expected_output: precommit_nil_output(Round::new(0), &my_addr, &my_sk), expected_round: Round::new(0), new_state: precommit_state(Round::new(0)), }, @@ -653,11 +662,194 @@ fn driver_steps_polka_previous_with_no_locked() { run_steps(&mut driver, steps); } +// Arrive at L44 with previously received polkaNil and entering prevote (due to timeoutPropose) +// +// Ev: NewRound(0) Timeout(propose) + replay +// State: NewRound ------------> Propose --------> Propose ---------------> Prevote -------------------> Precommit +// Msg: propose_timer None prevote_nil precommit_nil +// Alg: L21 L34 L44 +// +// +// v1=2, v2=3, v3=2, we are v3 +// L21 - v3 is not proposer starts propose timer (step propose) +// L34 - v3 gets +2/3 prevotes for nil (from v1 and v2), event ignored (step propose) +// L57 - v3 receives timeout propose, prevotes for nil (step prevote) +// L44 - polkaNil is replayed and v3 precommits for nil (step precommit) +#[test] +fn driver_steps_polka_nil_and_timout_propose() { + let [(v1, sk1), (v2, sk2), (v3, sk3)] = make_validators([2, 3, 2]); + let (my_sk, my_addr) = (sk3.clone(), v3.address); + + let ctx = TestContext::new(my_sk.clone()); + let sel = RotateProposer; + let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); + + let mut driver = Driver::new(ctx, sel, vs, my_addr); + + let steps = vec![ + TestStep { + desc: "Start round 0, we, v3, are not the proposer, start timeout propose", + input: new_round_input(Round::new(0)), + expected_output: start_propose_timer_output(Round::new(0)), + expected_round: Round::new(0), + new_state: propose_state(Round::new(0)), + }, + TestStep { + desc: "v1 prevotes nil", + input: prevote_nil_input(&v1.address, &sk1), + expected_output: None, + expected_round: Round::new(0), + new_state: propose_state(Round::new(0)), + }, + TestStep { + desc: "v2 prevotes for for nil, we get polkaNil, but we are in Propose step", + input: prevote_nil_input(&v2.address, &sk2), + expected_output: None, + expected_round: Round::new(0), + new_state: propose_state(Round::new(0)), + }, + TestStep { + desc: "Timeout propose, prevote for nil then precommit for nil", + input: timeout_propose_input(Round::new(0)), + expected_output: precommit_nil_output(Round::new(0), &my_addr, &my_sk), + expected_round: Round::new(0), + new_state: precommit_state(Round::new(0)), + }, + ]; + + run_steps(&mut driver, steps); +} + +// Arrive at L36 with previoustly received polkaValue and proposal, and entering prevote (due to received proposal) +// +// Ev: NewRound(0) Proposal + replay +// State: NewRound ------------> Propose -----------> Propose ---------> Prevote --------------------> Precommit +// Msg: propose_timer None prevote(v) precommit(v) +// Alg: L21 L24 L37, L37-L43 +// +// +// v1=2, v2=3, v3=2, we are v3 +// L21 - v3 is not proposer starts propose timer (step propose) +// L34 - v3 gets +2/3 prevotes (from v1 and v2), events ignored (step propose) +// L57 - v3 receives proposal, prevotes for value (step prevote) +// L36 - polka is replayed and v3 precommits for value (step precommit) +#[test] +fn driver_steps_polka_value_then_proposal() { + let value = Value::new(9999); + + let [(v1, sk1), (v2, sk2), (v3, sk3)] = make_validators([2, 3, 2]); + let (my_sk, my_addr) = (sk3.clone(), v3.address); + + let ctx = TestContext::new(my_sk.clone()); + let sel = RotateProposer; + let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); + + let mut driver = Driver::new(ctx, sel, vs, my_addr); + + let steps = vec![ + TestStep { + desc: "Start round 0, we, v3, are not the proposer, start timeout propose", + input: new_round_input(Round::new(0)), + expected_output: start_propose_timer_output(Round::new(0)), + expected_round: Round::new(0), + new_state: propose_state(Round::new(0)), + }, + TestStep { + desc: "v1 prevotes a proposal", + input: prevote_input(&v1.address, &sk1), + expected_output: None, + expected_round: Round::new(0), + new_state: propose_state(Round::new(0)), + }, + TestStep { + desc: "v2 prevotes for same proposal, we get +2/3 prevotes, but we are in Propose step", + input: prevote_input(&v2.address, &sk2), + expected_output: None, + expected_round: Round::new(0), + new_state: propose_state(Round::new(0)), + }, + TestStep { + desc: "receive a proposal from v1 - L22 send prevote", + input: proposal_input(Round::new(0), value, Round::Nil, Validity::Valid), + expected_output: precommit_output(Round::new(0), value, &my_addr, &my_sk), + expected_round: Round::new(0), + new_state: precommit_state_with_proposal_and_locked_and_valid( + Round::new(0), + Proposal::new(Height::new(1), Round::new(0), value, Round::Nil), + ), + }, + ]; + + run_steps(&mut driver, steps); +} + +// Arrive at L34 with previously received polkaAny and entering prevote (due to received poposal) +// +// Ev: NewRound(0) Proposal(v') + replay +// State: NewRound ------------> Propose -------------> Propose -----------> Prevote -------------------------> Prevote +// Msg: propose_timer None prevote(v) schedule_timeout(prevote) +// Alg: L21 L24 L34 +// +// +// v1=2, v2=3, v3=2, we are v3 +// L21 - v3 is not proposer starts propose timer (step propose) +// L34 - v3 gets +2/3 prevotes for v (from v1 and v2), events ignored (step propose) +// L57 - v3 receives proposal for v', prevotes for v' (step prevote) +// L34 - polka any is replayed and prevote timer is started (step prevote) +#[test] +fn driver_steps_polka_any_then_proposal_other() { + let value = Value::new(9999); + + let [(v1, sk1), (v2, sk2), (v3, sk3)] = make_validators([2, 3, 2]); + let (my_sk, my_addr) = (sk3.clone(), v3.address); + + let ctx = TestContext::new(my_sk.clone()); + let sel = RotateProposer; + let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); + + let mut driver = Driver::new(ctx, sel, vs, my_addr); + + let steps = vec![ + TestStep { + desc: "Start round 0, we, v3, are not the proposer, start timeout propose", + input: new_round_input(Round::new(0)), + expected_output: start_propose_timer_output(Round::new(0)), + expected_round: Round::new(0), + new_state: propose_state(Round::new(0)), + }, + TestStep { + desc: "v1 prevotes for nil", + input: prevote_nil_input(&v1.address, &sk1), + expected_output: None, + expected_round: Round::new(0), + new_state: propose_state(Round::new(0)), + }, + TestStep { + desc: "v2 prevotes for same proposal, we get polkaAny, but we are in Propose step", + input: prevote_input(&v2.address, &sk2), + expected_output: None, + expected_round: Round::new(0), + new_state: propose_state(Round::new(0)), + }, + TestStep { + desc: "receive a proposal from v1 - L22 send prevote, replay polkaAny, start timeout prevote", + input: proposal_input(Round::new(0), value, Round::Nil, Validity::Valid), + expected_output: start_prevote_timer_output(Round::new(0)), + expected_round: Round::new(0), + new_state: prevote_state(Round::new(0)), + }, + ]; + + run_steps(&mut driver, steps); +} + fn run_steps(driver: &mut Driver, steps: Vec) { for step in steps { println!("Step: {}", step.desc); - let output = block_on(driver.execute(step.input)).expect("execute succeeded"); + let mut outputs = block_on(driver.process(step.input)).expect("execute succeeded"); + let output = outputs.pop(); + assert_eq!(output, step.expected_output, "expected output"); assert_eq!( diff --git a/Code/vote/src/keeper.rs b/Code/vote/src/keeper.rs index a0e500712..15b3fdab1 100644 --- a/Code/vote/src/keeper.rs +++ b/Code/vote/src/keeper.rs @@ -175,8 +175,8 @@ where vote_type: VoteType, threshold: Threshold>, ) -> bool { - self.per_round.get(round).map_or(false, |round| { - round.votes.is_threshold_met( + self.per_round.get(round).map_or(false, |per_round| { + per_round.votes.is_threshold_met( vote_type, threshold, self.threshold_params.quorum, From db5f7566bc36b89bf48c0fa340feae913b738dcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A1n=20Vanzetto?= <15466498+hvanz@users.noreply.github.com> Date: Fri, 8 Dec 2023 12:17:56 +0100 Subject: [PATCH 6/9] spec: Refactor votekeeper using Quint sum types (#76) * Use sum type for VoteType * Use sum type for Threshold * Use sum type for ExecutorEvent * Use sum type for Value * Update executor using votekeeper's new types * Move unit tests near definition * Fix SM and tests after merge * Add types module * Comment out id in types * spec: remove initialRound * fix spec * Update itf tests * cargo fmt * Rename conflicting definitions * Use `itf` native support for sum types (#111) * spec driver: Bring some changes from main + some nits * spec driver: another missing fix * Rename voteBookkeepesSM to Models * Move votekeeper types to common types * Workspace dependencies * Small cleanup * Ensure syntax highlighting on GitHub * spec: Rename outputs to keep it consistent with other modules * Revert "spec: Rename outputs to keep it consistent with other modules" This reverts commit 661ab3350e44ec83e458f89b1daabcf319a07e1f. * Rename enum variants --------- Co-authored-by: Romain Ruetschi --- Code/Cargo.toml | 3 +- Code/itf/Cargo.toml | 6 +- Code/itf/src/lib.rs | 1 + Code/itf/src/types.rs | 34 ++++ Code/itf/src/votekeeper.rs | 75 ++++--- Code/itf/tests/votekeeper/runner.rs | 122 ++++++----- Code/itf/tests/votekeeper/utils.rs | 18 +- Specs/Quint/Makefile | 5 +- Specs/Quint/basicSpells.qnt | 3 +- Specs/Quint/consensus.qnt | 60 +++--- Specs/Quint/driver.qnt | 286 +++++++++++++------------- Specs/Quint/extraSpells.qnt | 3 +- Specs/Quint/types.qnt | 78 +++++++ Specs/Quint/voteBookkeeper.qnt | 294 ++++++++++++--------------- Specs/Quint/voteBookkeeperModels.qnt | 80 ++++++++ Specs/Quint/voteBookkeeperSM.qnt | 36 ---- Specs/Quint/voteBookkeeperTest.qnt | 144 ++++++------- 17 files changed, 691 insertions(+), 557 deletions(-) create mode 100644 Code/itf/src/types.rs create mode 100644 Specs/Quint/types.qnt create mode 100644 Specs/Quint/voteBookkeeperModels.qnt delete mode 100644 Specs/Quint/voteBookkeeperSM.qnt diff --git a/Code/Cargo.toml b/Code/Cargo.toml index 705510c26..daf378977 100644 --- a/Code/Cargo.toml +++ b/Code/Cargo.toml @@ -22,10 +22,11 @@ async-trait = "0.1" ed25519-consensus = "2.1.0" futures = "0.3" glob = "0.3.0" -itf = "0.2.1" +itf = "0.2.2" num-bigint = "0.4.4" rand = { version = "0.8.5", features = ["std_rng"] } serde = "1.0" serde_json = "1.0" +serde_with = "3.4" sha2 = "0.10.8" signature = "2.1.0" diff --git a/Code/itf/Cargo.toml b/Code/itf/Cargo.toml index b8a977cb8..011d60c72 100644 --- a/Code/itf/Cargo.toml +++ b/Code/itf/Cargo.toml @@ -9,14 +9,16 @@ license.workspace = true publish.workspace = true [dependencies] -itf = { workspace = true } -rand = { workspace = true } malachite-common = { version = "0.1.0", path = "../common" } malachite-vote = { version = "0.1.0", path = "../vote" } malachite-test = { version = "0.1.0", path = "../test" } + +itf = { workspace = true } +rand = { workspace = true } num-bigint = { workspace = true, features = ["serde"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +serde_with = { workspace = true } glob = { workspace = true } [dev-dependencies] diff --git a/Code/itf/src/lib.rs b/Code/itf/src/lib.rs index be66ce483..04d544d47 100644 --- a/Code/itf/src/lib.rs +++ b/Code/itf/src/lib.rs @@ -1,2 +1,3 @@ +pub mod types; pub mod utils; pub mod votekeeper; diff --git a/Code/itf/src/types.rs b/Code/itf/src/types.rs new file mode 100644 index 000000000..3eb23e993 --- /dev/null +++ b/Code/itf/src/types.rs @@ -0,0 +1,34 @@ +use itf::de::{As, Integer}; +use serde::Deserialize; + +pub type Height = i64; +pub type Weight = i64; +pub type Round = i64; +pub type Address = String; +pub type NonNilValue = String; + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize)] +#[serde(tag = "tag", content = "value")] +pub enum Value { + Nil, + Val(NonNilValue), +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +#[serde(tag = "tag", content = "value")] +pub enum VoteType { + Prevote, + Precommit, +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Vote { + pub vote_type: VoteType, + #[serde(with = "As::")] + pub height: Height, + #[serde(with = "As::")] + pub round: Round, + pub value_id: Value, + pub src_address: Address, +} diff --git a/Code/itf/src/votekeeper.rs b/Code/itf/src/votekeeper.rs index 41e8d181e..ae3327607 100644 --- a/Code/itf/src/votekeeper.rs +++ b/Code/itf/src/votekeeper.rs @@ -3,33 +3,59 @@ use std::collections::{HashMap, HashSet}; use serde::Deserialize; -pub type Height = i64; -pub type Weight = i64; -pub type Round = i64; -pub type Address = String; -pub type Value = String; -pub type VoteType = String; +use crate::types::{Address, Height, NonNilValue, Round, Value, Vote, Weight}; #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Bookkeeper { +#[serde(tag = "tag", content = "value")] +pub enum WeightedVote { + #[serde(rename = "NoWeightedVote")] + NoVote, + + #[serde(rename = "WV")] + #[serde(with = "As::<(Same, Integer, Integer)>")] + Vote(Vote, Weight, Round), +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize)] +#[serde(tag = "tag", content = "value")] +pub enum VoteKeeperOutput { + #[serde(rename = "NoVKOutput")] + NoOutput, + + #[serde(rename = "PolkaAnyVKOutput")] #[serde(with = "As::")] - pub height: Height, + PolkaAny(Round), + + #[serde(rename = "PolkaNilVKOutput")] #[serde(with = "As::")] - pub total_weight: Weight, - #[serde(with = "As::>")] - pub rounds: HashMap, + PolkaNil(Round), + + #[serde(rename = "PolkaValueVKOutput")] + #[serde(with = "As::<(Integer, Same)>")] + PolkaValue(Round, NonNilValue), + + #[serde(rename = "PrevoteAnyVKOutput")] + #[serde(with = "As::")] + PrecommitAny(Round), + + #[serde(rename = "PrevoteNilVKOutput")] + #[serde(with = "As::<(Integer, Same)>")] + PrecommitValue(Round, NonNilValue), + + #[serde(rename = "SkipVKOutput")] + #[serde(with = "As::")] + Skip(Round), } #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] -pub struct Vote { - pub typ: VoteType, +#[serde(rename_all = "camelCase")] +pub struct Bookkeeper { #[serde(with = "As::")] pub height: Height, #[serde(with = "As::")] - pub round: Round, - pub value: Value, - pub address: Address, + pub total_weight: Weight, + #[serde(with = "As::>")] + pub rounds: HashMap, } #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] @@ -56,21 +82,10 @@ pub struct VoteCount { pub votes_addresses: HashSet
, } -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Hash)] -pub struct VoteKeeperOutput { - #[serde(with = "As::")] - pub round: Round, - pub name: String, - pub value: Value, -} - #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct State { - #[serde(rename = "voteBookkeeperTest::voteBookkeeperSM::bookkeeper")] pub bookkeeper: Bookkeeper, - #[serde(rename = "voteBookkeeperTest::voteBookkeeperSM::lastEmitted")] pub last_emitted: VoteKeeperOutput, - #[serde(rename = "voteBookkeeperTest::voteBookkeeperSM::weightedVote")] - #[serde(with = "As::<(Same, Integer, Integer)>")] - pub weighted_vote: (Vote, Weight, Round), + pub weighted_vote: WeightedVote, } diff --git a/Code/itf/tests/votekeeper/runner.rs b/Code/itf/tests/votekeeper/runner.rs index 781629d1d..fbc9633b2 100644 --- a/Code/itf/tests/votekeeper/runner.rs +++ b/Code/itf/tests/votekeeper/runner.rs @@ -1,7 +1,9 @@ use std::collections::HashMap; use malachite_common::{Context, Round, Value}; -use malachite_itf::votekeeper::State; +use malachite_itf::types::{Value as ModelValue, VoteType}; +use malachite_itf::votekeeper::VoteKeeperOutput::*; +use malachite_itf::votekeeper::{State, WeightedVote}; use malachite_test::{Address, Height, TestContext, Vote}; use malachite_vote::{ keeper::{Output, VoteKeeper}, @@ -23,12 +25,11 @@ impl ItfRunner for VoteKeeperRunner { type Error = (); fn init(&mut self, expected: &Self::ExpectedState) -> Result { - // Initialize VoteKeeper from the initial total_weight from the first state in the model. - let (input_vote, weight, current_round) = &expected.weighted_vote; - let round = Round::new(input_vote.round); + let height = expected.bookkeeper.height as u64; + let total_weight = expected.bookkeeper.total_weight as u64; println!( - "🔵 init: vote={:?}, round={:?}, value={:?}, address={:?}, weight={:?}, current_round={:?}", - input_vote.typ, round, input_vote.value, input_vote.address, weight, current_round + "🔵 init: height={:?}, total_weight={:?}", + height, total_weight ); Ok(VoteKeeper::new( expected.bookkeeper.total_weight as u64, @@ -41,24 +42,31 @@ impl ItfRunner for VoteKeeperRunner { actual: &mut Self::ActualState, expected: &Self::ExpectedState, ) -> Result { - // Build step to execute. - let (input_vote, weight, current_round) = &expected.weighted_vote; - let round = Round::new(input_vote.round); - let height = Height::new(input_vote.height as u64); - let value = value_from_model(&input_vote.value); - let address = self.address_map.get(input_vote.address.as_str()).unwrap(); - let vote = match input_vote.typ.as_str() { - "Prevote" => Vote::new_prevote(height, round, value, *address), - "Precommit" => Vote::new_precommit(height, round, value, *address), - _ => unreachable!(), - }; - println!( - "🔵 step: vote={:?}, round={:?}, value={:?}, address={:?}, weight={:?}, current_round={:?}", - input_vote.typ, round, value, input_vote.address, weight, current_round - ); + match &expected.weighted_vote { + WeightedVote::NoVote => Err(()), + + WeightedVote::Vote(input_vote, weight, current_round) => { + // Build step to execute. + let round = Round::new(input_vote.round); + let height = Height::new(input_vote.height as u64); + let value = value_from_model(&input_vote.value_id); + let address = self + .address_map + .get(input_vote.src_address.as_str()) + .unwrap(); + let vote = match &input_vote.vote_type { + VoteType::Prevote => Vote::new_prevote(height, round, value, *address), + VoteType::Precommit => Vote::new_precommit(height, round, value, *address), + }; + println!( + "🔵 step: vote={:?}, round={:?}, value={:?}, address={:?}, weight={:?}, current_round={:?}", + input_vote.vote_type, round, value, input_vote.src_address, weight, current_round + ); - // Execute step. - Ok(actual.apply_vote(vote, *weight as u64, Round::new(*current_round))) + // Execute step. + Ok(actual.apply_vote(vote, *weight as u64, Round::new(*current_round))) + } + } } fn result_invariant( @@ -66,36 +74,36 @@ impl ItfRunner for VoteKeeperRunner { result: &Self::Result, expected: &Self::ExpectedState, ) -> Result { - // Get expected result. let expected_result = &expected.last_emitted; - println!( - "🟣 result: model={:?}({:?},{:?}), code={:?}", - expected_result.name, expected_result.value, expected_result.round, result - ); - // Check result against expected result. match result { - Some(result) => match result { - Output::PolkaValue(value) => { - assert_eq!(expected_result.name, "PolkaValue"); + Some(result) => match (result, expected_result) { + // TODO: check expected_round + (Output::PolkaNil, PolkaNil(_expected_round)) => (), + (Output::PolkaAny, PolkaAny(_expected_round)) => (), + (Output::PolkaValue(value), PolkaValue(_expected_round, expected_value)) => { assert_eq!( - value_from_model(&expected_result.value).as_ref(), - Some(value) + Some(value), + value_from_model(&ModelValue::Val(expected_value.to_string())).as_ref() ); } - Output::PrecommitValue(value) => { - assert_eq!(expected_result.name, "PrecommitValue"); + (Output::PrecommitAny, PrecommitAny(_expected_round)) => (), + ( + Output::PrecommitValue(value), + PrecommitValue(_expected_round, expected_value), + ) => { assert_eq!( - value_from_model(&expected_result.value).as_ref(), - Some(value) + Some(value), + value_from_model(&ModelValue::Val(expected_value.to_string())).as_ref() ); } - Output::SkipRound(round) => { - assert_eq!(expected_result.name, "Skip"); - assert_eq!(&Round::new(expected_result.round), round); + (Output::SkipRound(round), Skip(expected_round)) => { + assert_eq!(round, &Round::new(*expected_round)); + } + (actual, expected) => { + panic!("actual: {:?}, expected: {:?}", actual, expected) } - msg => assert_eq!(expected_result.name, format!("{msg:?}")), }, - None => assert_eq!(expected_result.name, "None"), + None => assert_eq!(*expected_result, NoOutput), } Ok(true) } @@ -135,18 +143,29 @@ impl ItfRunner for VoteKeeperRunner { let mut event_count = HashMap::new(); for event in expected_outputs { - let count = event_count.entry(event.name.clone()).or_insert(0); + let event_name = match event { + PolkaAny(_) => "PolkaAny".to_string(), + PolkaNil(_) => "PolkaNil".to_string(), + PolkaValue(_, _) => "PolkaValue".to_string(), + PrecommitAny(_) => "PrecommitAny".to_string(), + PrecommitValue(_, _) => "PrecommitValue".to_string(), + Skip(_) => "Skip".to_string(), + _ => format!("{event:?}"), + }; + + let count = event_count.entry(event_name).or_insert(0); *count += 1; } for event in actual_outputs { let event_name = match event { - Output::PolkaValue(_) => "PolkaValue".into(), - Output::PrecommitValue(_) => "PrecommitValue".into(), - Output::SkipRound(_) => "Skip".into(), + Output::PolkaValue(_) => "PolkaValue".to_string(), + Output::PrecommitValue(_) => "PrecommitValue".to_string(), + Output::SkipRound(_) => "Skip".to_string(), _ => format!("{event:?}"), }; - let count = event_count.entry(event_name.clone()).or_insert(0); + + let count = event_count.entry(event_name).or_insert(0); *count -= 1; } @@ -156,13 +175,10 @@ impl ItfRunner for VoteKeeperRunner { let expected_addresses_weights = &expected_round.votes_addresses_weights; let actual_addresses_weights = &actual_round.addresses_weights().get_inner(); - for address in expected_addresses_weights.keys() { + for (address, expected_weight) in expected_addresses_weights { assert_eq!( actual_addresses_weights.get(self.address_map.get(address).unwrap()), - expected_addresses_weights - .get(address) - .map(|&w| w as u64) - .as_ref(), + Some(&(*expected_weight as u64)), "weight for address {address:?}" ); } diff --git a/Code/itf/tests/votekeeper/utils.rs b/Code/itf/tests/votekeeper/utils.rs index 91504697d..1a75fbe44 100644 --- a/Code/itf/tests/votekeeper/utils.rs +++ b/Code/itf/tests/votekeeper/utils.rs @@ -1,19 +1,19 @@ use std::collections::HashMap; -use malachite_itf::votekeeper::Value; +use malachite_itf::types::Value; use malachite_test::{Address, ValueId}; pub const ADDRESSES: [&str; 3] = ["alice", "bob", "john"]; -pub const NIL_VALUE: &str = "nil"; pub fn value_from_model(value: &Value) -> Option { - match value.as_str() { - NIL_VALUE => None, - "proposal" => Some(0.into()), - "val1" => Some(1.into()), - "val2" => Some(2.into()), - "val3" => Some(3.into()), - _ => unimplemented!("unknown value {value:?}"), + match value { + Value::Nil => None, + Value::Val(v) => match v.as_str() { + "v1" => Some(1.into()), + "v2" => Some(2.into()), + "v3" => Some(3.into()), + _ => unimplemented!("unknown value {value:?}"), + }, } } diff --git a/Specs/Quint/Makefile b/Specs/Quint/Makefile index a7477d691..a42dbf825 100644 --- a/Specs/Quint/Makefile +++ b/Specs/Quint/Makefile @@ -22,7 +22,10 @@ test: test-con test-sm test-vk .PHONY: test verify: - npx @informalsystems/quint verify --max-steps 10 --invariant Inv voteBookkeeperTest.qnt + npx @informalsystems/quint verify --max-steps 10 \ + --main voteBookkeeperModels --init=VK1::init --step=VK1::step \ + --invariant VK1::Inv \ + voteBookkeeperModels.qnt .PHONY: verify # Generate traces by random simulation from properties describing the last desired state. diff --git a/Specs/Quint/basicSpells.qnt b/Specs/Quint/basicSpells.qnt index 78d722edb..a5bda8782 100644 --- a/Specs/Quint/basicSpells.qnt +++ b/Specs/Quint/basicSpells.qnt @@ -1,4 +1,5 @@ // -*- mode: Bluespec; -*- + /** * This module collects definitions that are ubiquitous. * One day they will become the standard library of Quint. @@ -109,4 +110,4 @@ module basicSpells { assert(Map(3 -> 4, 7 -> 8) == Map(3 -> 4, 5 -> 6, 7 -> 8).mapRemove(5)), assert(Map() == Map().mapRemove(3)), } -} \ No newline at end of file +} diff --git a/Specs/Quint/consensus.qnt b/Specs/Quint/consensus.qnt index e7cf66ece..98effc7ec 100644 --- a/Specs/Quint/consensus.qnt +++ b/Specs/Quint/consensus.qnt @@ -144,7 +144,7 @@ val defaultOutput : ConsensusOutput = { -pure def NewHeight (state: ConsensusState, input: ConsensusInput) : ConsResult = { +pure def handleNewHeight (state: ConsensusState, input: ConsensusInput) : ConsResult = { if (input.height > state.height) val newstate = { p: state.p, round: -1, @@ -163,7 +163,7 @@ pure def NewHeight (state: ConsensusState, input: ConsensusInput) : ConsResult = } // line 11.14 -pure def NewRoundProposer (state: ConsensusState, input: ConsensusInput) : ConsResult = { +pure def handleNewRoundProposer (state: ConsensusState, input: ConsensusInput) : ConsResult = { if (input.round > state.round) val newstate = { ...state, round: input.round, step: "propose"} val proposal = if (state.validValue != "nil") state.validValue @@ -180,7 +180,7 @@ pure def NewRoundProposer (state: ConsensusState, input: ConsensusInput) : ConsR } // line 11.20 -pure def NewRound (state: ConsensusState, input: ConsensusInput) : ConsResult = { +pure def handleNewRound (state: ConsensusState, input: ConsensusInput) : ConsResult = { // TODO: discuss comment "input.round must match state.round" if (input.round > state.round) val newstate = { ...state, round: input.round, step: "propose" } @@ -198,7 +198,7 @@ pure def NewRound (state: ConsensusState, input: ConsensusInput) : ConsResult = // - the value has been checked to be valid // - it is for the current round // The driver checks this upon receiving a propose message "ProposalMsg" -pure def Proposal (state: ConsensusState, input: ConsensusInput) : ConsResult = { +pure def handleProposal (state: ConsensusState, input: ConsensusInput) : ConsResult = { if (state.step == "propose") val newstate = { ...state, step: "Prevote" } if (state.lockedRound == -1 or state.lockedValue == input.value) @@ -223,7 +223,7 @@ pure def Proposal (state: ConsensusState, input: ConsensusInput) : ConsResult = } // line 26 -pure def ProposalInvalid (state: ConsensusState, input: ConsensusInput) : ConsResult = { +pure def handleProposalInvalid (state: ConsensusState, input: ConsensusInput) : ConsResult = { if (state.step == "propose") val newstate = state.with("step", "Prevote") val result = { ...defaultOutput, name: "votemessage", @@ -238,7 +238,7 @@ pure def ProposalInvalid (state: ConsensusState, input: ConsensusInput) : ConsRe } // line 28 -pure def ProposalAndPolkaPreviousAndValid (state: ConsensusState, input: ConsensusInput) : ConsResult = { +pure def handleProposalAndPolkaPreviousAndValid (state: ConsensusState, input: ConsensusInput) : ConsResult = { if (state.step == "propose" and input.vr >= 0 and input.vr < state.round) val newstate = state.with("step", "Prevote") if (state.lockedRound <= input.vr or state.lockedValue == input.value) @@ -264,7 +264,7 @@ pure def ProposalAndPolkaPreviousAndValid (state: ConsensusState, input: Consens } // line 34 -pure def PolkaAny (state: ConsensusState, input: ConsensusInput) : ConsResult = { +pure def handlePolkaAny (state: ConsensusState, input: ConsensusInput) : ConsResult = { if (state.step == "Prevote") val result = { ...defaultOutput, name: "timeout", timeout: "TimeoutPrevote" } // We just report that a timeout should be started. The driver must take care @@ -276,7 +276,7 @@ pure def PolkaAny (state: ConsensusState, input: ConsensusInput) : ConsResult = } // line 36 -pure def ProposalAndPolkaAndValid (state: ConsensusState, input: ConsensusInput) : ConsResult = { +pure def handleProposalAndPolkaAndValid (state: ConsensusState, input: ConsensusInput) : ConsResult = { val auxState = { ...state, validValue: input.value, validRound: state.round } if (state.step == "Prevote") val newstate = { ...auxState, lockedValue: input.value, @@ -301,7 +301,7 @@ pure def ProposalAndPolkaAndValid (state: ConsensusState, input: ConsensusInput) } // line 44 -pure def PolkaNil (state: ConsensusState, input: ConsensusInput) : ConsResult = { +pure def handlePolkaNil (state: ConsensusState, input: ConsensusInput) : ConsResult = { if (state.step == "Prevote") val newstate = { ...state, step: "Precommit"} val result = { ...defaultOutput, name: "votemessage", @@ -316,13 +316,13 @@ pure def PolkaNil (state: ConsensusState, input: ConsensusInput) : ConsResult = } // line 47 -pure def PrecommitAny (state: ConsensusState, input: ConsensusInput) : ConsResult = { +pure def handlePrecommitAny (state: ConsensusState, input: ConsensusInput) : ConsResult = { val result = { ...defaultOutput, name: "timeout", timeout: "TimeoutPrecommit" } {cs: state, out: result} } // line 49 -pure def ProposalAndCommitAndValid (state: ConsensusState, input: ConsensusInput) : ConsResult = { +pure def handleProposalAndCommitAndValid (state: ConsensusState, input: ConsensusInput) : ConsResult = { if (state.step != "decided") { val newstate = { ...state, step: "decided"} val result = { ...defaultOutput, name: "decided", decided: input.value} @@ -333,7 +333,7 @@ pure def ProposalAndCommitAndValid (state: ConsensusState, input: ConsensusInpu } // line 55 -pure def RoundSkip (state: ConsensusState, input: ConsensusInput) : ConsResult = { +pure def handleRoundSkip (state: ConsensusState, input: ConsensusInput) : ConsResult = { if (input.round > state.round) val result = { ...defaultOutput, name: "skipRound", skipRound: input.round } {cs: state, out: result} @@ -341,7 +341,7 @@ pure def RoundSkip (state: ConsensusState, input: ConsensusInput) : ConsResult {cs: state, out: defaultOutput} } -pure def TimeoutPropose (state: ConsensusState, input: ConsensusInput) : ConsResult = { +pure def handleTimeoutPropose (state: ConsensusState, input: ConsensusInput) : ConsResult = { if (input.height == state.height and input.round == state.round and state.step == "propose") val newstate = { ...state, step: "Prevote"} val result = { ...defaultOutput, name: "votemessage", @@ -355,7 +355,7 @@ pure def TimeoutPropose (state: ConsensusState, input: ConsensusInput) : ConsRes {cs: state, out: defaultOutput} } -pure def TimeoutPrevote (state: ConsensusState, input: ConsensusInput) : ConsResult = { +pure def handleTimeoutPrevote (state: ConsensusState, input: ConsensusInput) : ConsResult = { if (input.height == state.height and input.round == state.round and state.step == "Prevote") val newstate = { ...state, step: "Precommit"} // TODO: should we send precommit nil again ? @@ -370,7 +370,7 @@ pure def TimeoutPrevote (state: ConsensusState, input: ConsensusInput) : ConsRes {cs: state, out: defaultOutput} } -pure def TimeoutPrecommit (state: ConsensusState, input: ConsensusInput) : ConsResult = { +pure def handleTimeoutPrecommit (state: ConsensusState, input: ConsensusInput) : ConsResult = { if (input.height == state.height and input.round == state.round) // TODO: here we should call newRound. For this we would need to know whether // we are proposer for next round. @@ -387,35 +387,35 @@ pure def TimeoutPrecommit (state: ConsensusState, input: ConsensusInput) : ConsR pure def consensus (state: ConsensusState, input: ConsensusInput) : ConsResult = { if (input.name == "NewHeight") - NewHeight (state, input) + handleNewHeight (state, input) else if (input.name == "NewRoundProposer") - NewRoundProposer(state, input) + handleNewRoundProposer(state, input) else if (input.name == "NewRound") - NewRound(state, input) + handleNewRound(state, input) else if (input.name == "Proposal") - Proposal(state, input) + handleProposal(state, input) else if (input.name == "ProposalAndPolkaPreviousAndValid") - ProposalAndPolkaPreviousAndValid(state, input) + handleProposalAndPolkaPreviousAndValid(state, input) else if (input.name == "ProposalInvalid") - ProposalInvalid(state, input) + handleProposalInvalid(state, input) else if (input.name == "PolkaAny") - PolkaAny(state, input) + handlePolkaAny(state, input) else if (input.name == "ProposalAndPolkaAndValid") - ProposalAndPolkaAndValid(state, input) + handleProposalAndPolkaAndValid(state, input) else if (input.name == "PolkaNil") - PolkaNil(state, input) + handlePolkaNil(state, input) else if (input.name == "PrecommitAny") - PrecommitAny(state, input) + handlePrecommitAny(state, input) else if (input.name == "ProposalAndCommitAndValid") - ProposalAndCommitAndValid(state, input) + handleProposalAndCommitAndValid(state, input) else if (input.name == "RoundSkip") - RoundSkip (state, input) + handleRoundSkip (state, input) else if (input.name == "TimeoutPropose") - TimeoutPropose (state, input) + handleTimeoutPropose (state, input) else if (input.name == "TimeoutPrevote") - TimeoutPrevote (state, input) + handleTimeoutPrevote (state, input) else if (input.name == "TimeoutPrecommit") - TimeoutPrecommit (state, input) + handleTimeoutPrecommit (state, input) else {cs: state, out: defaultOutput} } diff --git a/Specs/Quint/driver.qnt b/Specs/Quint/driver.qnt index 2715dc83d..308f54615 100644 --- a/Specs/Quint/driver.qnt +++ b/Specs/Quint/driver.qnt @@ -7,13 +7,10 @@ module driver { +import types.* from "./types" import consensus.* from "./consensus" import voteBookkeeper.* from "./voteBookkeeper" -pure def initBookKeeper(totalVotingPower: int): Bookkeeper = - { height: 0, totalWeight: totalVotingPower, rounds: Map() } - - type DriverState = { bk : Bookkeeper, cs : ConsensusState, @@ -30,14 +27,14 @@ type DriverState = { pure def initDriver (v: Address_t, vs: Address_t -> int) : DriverState = { val tvp = vs.keys().fold(0, (sum, key) => sum + vs.get(key)) { - bk: initBookKeeper(tvp), + bk: initBookKeeper(0, tvp), cs: initConsensusState(v), proposals: Set(), valset: vs, executedInputs: List(), pendingInputs: Set(), started: false, - voteKeeperOutput: toVoteKeeperOutput(0, "", {name: "", value: ""}), // debug + voteKeeperOutput: NoVKOutput, // debug chain : List(), nextValueToPropose: "", } @@ -77,8 +74,10 @@ val defaultInput: DriverInput = { // this is to match the type from the bookkeeper. When we add heights there we should // unify -pure def toVote (v: Vote_t) : Vote = - { typ: v.step, height: v.height, round: v.round, value: v.id, address: v.src } +pure def toVote(v: Vote_t): Vote = + val voteType = if (v.step == "Precommit") Precommit else Prevote + val value = if (v.id == "nil") Nil else Val(v.id) + { voteType: voteType, height: v.height, round: v.round, valueId: value, srcAddress: v.src } val defaultConsensusInput : ConsensusInput = { name : "", height : 0, round: 0, value: "", vr: 0 } @@ -134,10 +133,6 @@ pure def valid(p: Proposal_t) :bool = { p.proposal != "invalid" } -// efficient hashing function -pure def id(v) = v - - type ConsensusCall = { es: DriverState, csInput: ConsensusInput, @@ -163,127 +158,136 @@ pure def callConsensus (es: DriverState, bk: Bookkeeper, csInput: ConsensusInput // We do this if the driver receives a Precommit -pure def Precommit (es: DriverState, input: DriverInput, vkOutput: VoteKeeperOutput) : (DriverState, ConsensusOutput) = { - if (vkOutput.name == "PrecommitValue") - if (es.proposals.exists(x => vkOutput.round == x.round and vkOutput.value == id(x.proposal))) { +pure def precommit(es: DriverState, input: DriverInput, vkOutput: VoteKeeperOutput): (DriverState, ConsensusOutput) = + match vkOutput { + | PrecommitValueVKOutput(round_value) => + val round = round_value._1 + val value = round_value._2 + if (es.proposals.exists(x => round == x.round and value == id(x.proposal))) callConsensus(es, es.bk, { name : "ProposalAndCommitAndValid", height : input.vote.height, round: input.vote.round, value: input.vote.id, vr: -1}) - } - else { - if (vkOutput.round == es.cs.round) { - callConsensus(es, es.bk, { name: "PrecommitAny", - height: input.vote.height, - round: input.vote.round, - value: input.vote.id, - vr: -1}) - } - else if (vkOutput.round > es.cs.round) { - // if it is for a future round I can trigger skipround - // TODO: should we really do this. It is dead code as the f+1 event already happened - callConsensus(es, es.bk, { name: "RoundSkip", - height: input.vote.height, - round: input.vote.round, - value: input.vote.id, - vr: -1}) - } - else { - // messages from past round -> ignore - (es, defaultOutput) - } - } - else if (vkOutput.name == "PrecommitAny" and vkOutput.round == es.cs.round) { - callConsensus(es, es.bk, { name: "PrecommitAny", - height: input.vote.height, - round: input.vote.round, - value: input.vote.id, - vr: -1}) - } - else if (vkOutput.name == "Skip" and vkOutput.round > es.cs.round) - callConsensus(es, es.bk, { name: "RoundSkip", - height: input.vote.height, - round: input.vote.round, - value: input.vote.id, - vr: -1}) - else - // none of the supported Precommit events. Do nothing - (es, defaultOutput) -} + else if (round == es.cs.round) + callConsensus(es, es.bk, { name: "PrecommitAny", + height: input.vote.height, + round: input.vote.round, + value: input.vote.id, + vr: -1}) + else if (round > es.cs.round) + // if it is for a future round I can trigger skipround + // TODO: should we really do this. It is dead code as the f+1 event already happened + callConsensus(es, es.bk, { name: "RoundSkip", + height: input.vote.height, + round: input.vote.round, + value: input.vote.id, + vr: -1}) + // ignore messages from past rounds + else (es, defaultOutput) + | PrecommitAnyVKOutput(round) => + if (round == es.cs.round) + callConsensus(es, es.bk, { name: "PrecommitAny", + height: input.vote.height, + round: input.vote.round, + value: input.vote.id, + vr: -1}) + else (es, defaultOutput) + + | SkipVKOutput(round) => + if (round > es.cs.round) + callConsensus(es, es.bk, { name: "RoundSkip", + height: input.vote.height, + round: input.vote.round, + value: input.vote.id, + vr: -1}) + else (es, defaultOutput) + // none of the supported Precommit events. Do nothing + | _ => (es, defaultOutput) + } // We do this if the driver receives a Prevote -pure def Prevote (es: DriverState, input: DriverInput, vkOutput: VoteKeeperOutput) : (DriverState, ConsensusOutput) = { +// TODO: input is only used for the Skip case. We could just use the round provided in +// SkipVKOutput(round), which is equal to input.vote.id. The height should probably also be +// recorded in the VoteKeeper output. +pure def prevote(es: DriverState, input: DriverInput, vkOutput: VoteKeeperOutput): (DriverState, ConsensusOutput) = // TODO: events do not have heights now. // TODO: Polka implications missing. - if (vkOutput.name == "PolkaValue") - if (vkOutput.round < es.cs.round and - es.proposals.exists(p => p.round == es.cs.round and - vkOutput.round == p.validRound and id(p.proposal) == vkOutput.value)) - callConsensus(es, es.bk, { name: "ProposalAndPolkaPreviousAndValid", - height: es.cs.height, - round: es.cs.round, - value: vkOutput.value, - vr: vkOutput.round}) - // TODO: the value should come from the proposal - else if (vkOutput.round == es.cs.round) - if (es.proposals.exists(p => p.round == es.cs.round and - id(p.proposal) == vkOutput.value)) - val pend = ( { name: "ProposalAndPolkaAndValid", - height: es.cs.height, - round: es.cs.round, - value: vkOutput.value, - vr: vkOutput.round}, es.cs.height, es.cs.round) - callConsensus( { ...es, pendingInputs: es.pendingInputs.union(Set(pend)) }, - es.bk, - { name: "PolkaAny", - height: es.cs.height, - round: es.cs.round, - value: vkOutput.value, - vr: vkOutput.round}) - else - // there is no matching proposal - callConsensus(es, es.bk, { name: "PolkaAny", - height: es.cs.height, - round: es.cs.round, - value: vkOutput.value, - vr: vkOutput.round}) + match vkOutput { + | PolkaValueVKOutput(round_value) => + val round = round_value._1 + val value = round_value._2 + if (round < es.cs.round and + es.proposals.exists(p => p.round == es.cs.round and + round == p.validRound and id(p.proposal) == value)) + callConsensus(es, es.bk, { name: "ProposalAndPolkaPreviousAndValid", + height: es.cs.height, + round: es.cs.round, + value: value, + vr: round}) + // TODO: the value should come from the proposal + else if (round == es.cs.round) + if (es.proposals.exists(p => p.round == es.cs.round and + id(p.proposal) == value)) + val pend = ( { name: "ProposalAndPolkaAndValid", + height: es.cs.height, + round: es.cs.round, + value: value, + vr: round}, es.cs.height, es.cs.round) + callConsensus( { ...es, pendingInputs: es.pendingInputs.union(Set(pend)) }, + es.bk, + { name: "PolkaAny", + height: es.cs.height, + round: es.cs.round, + value: value, + vr: round}) + else + // there is no matching proposal + callConsensus(es, es.bk, { name: "PolkaAny", + height: es.cs.height, + round: es.cs.round, + value: value, + vr: round}) - else // It is for a future round // TODO: we might check whether it is for a future round and jump - (es, defaultOutput) - else if (vkOutput.name == "PolkaAny") - if (vkOutput.round == es.cs.round) - // call consensus and remember that we did it - callConsensus(es, es.bk, { name: "PolkaAny", + else (es, defaultOutput) + + | PolkaAnyVKOutput(round) => + if (round == es.cs.round) + // call consensus and remember that we did it + callConsensus(es, es.bk, { name: "PolkaAny", + height: es.cs.height, + round: es.cs.round, + value: "???", // CHECK: what value to use here? + vr: round}) + // TODO: we might check whether it is for a future round and jump + else (es, defaultOutput) + + | PolkaNilVKOutput(round) => + if (round == es.cs.round) + callConsensus(es, es.bk, { name: "PolkaNil", height: es.cs.height, round: es.cs.round, - value: vkOutput.value, - vr: vkOutput.round}) - else - // TODO: we might check whether it is for a future round and jump - (es, defaultOutput) - else if (vkOutput.name == "PolkaNil" and vkOutput.round == es.cs.round) - callConsensus(es, es.bk, { name: "PolkaNil", - height: es.cs.height, - round: es.cs.round, - value: vkOutput.value, - vr: vkOutput.round}) - else if (vkOutput.name == "Skip" and vkOutput.round > es.cs.round) - callConsensus(es, es.bk, { name: "RoundSkip", - height: input.vote.height, - round: input.vote.round, - value: input.vote.id, - vr: -1}) - else - (es, defaultOutput) -} - + value: "nil", + vr: round}) + else (es, defaultOutput) + + | SkipVKOutput(round) => + if (round > es.cs.round) + callConsensus(es, es.bk, { name: "RoundSkip", + height: input.vote.height, + round: input.vote.round, + value: input.vote.id, + vr: -1}) + else (es, defaultOutput) + + | _ => (es, defaultOutput) + } // We do this if a timeout expires at the driver -pure def Timeout (es: DriverState, input: DriverInput) : (DriverState, ConsensusOutput) = { +pure def timeout(es: DriverState, input: DriverInput): (DriverState, ConsensusOutput) = // TODO: We assume that the timeout event is always for the current round. If this is not // the case, we need to encode it in the input to which round the timeout belongs val csInput: ConsensusInput = {name: input.timeout, @@ -292,11 +296,9 @@ pure def Timeout (es: DriverState, input: DriverInput) : (DriverState, Consensus value: "", vr: 0} callConsensus(es, es.bk, csInput) -} - // We do this if the driver receives a proposal -pure def ProposalMsg (es: DriverState, input: DriverInput) : (DriverState, ConsensusOutput) = { +pure def proposal(es: DriverState, input: DriverInput) : (DriverState, ConsensusOutput) = val newES = { ...es, proposals: es.proposals.union(Set(input.proposal))} if (input.proposal.src != Proposer(es.valset, input.proposal.height, input.proposal.round)) // proposer does not match the height/round of the proposal @@ -305,9 +307,8 @@ pure def ProposalMsg (es: DriverState, input: DriverInput) : (DriverState, Conse else if (valid(input.proposal)) val receivedCommit = checkThreshold( newES.bk, input.proposal.round, - "Precommit", - {name: "Value", - value: id(input.proposal.proposal)}) + Precommit, + ValueThreshold(id(input.proposal.proposal))) if (receivedCommit) // we have a commit that matches the proposal. We don't need to compare against // es.cs.round @@ -327,19 +328,16 @@ pure def ProposalMsg (es: DriverState, input: DriverInput) : (DriverState, Conse // for current round and q, valid, and from right proposer val receivedPolkaValidRoundVal = checkThreshold(newES.bk, input.proposal.validRound, - "Prevote", - {name: "Value", - value: id(input.proposal.proposal)}) + Prevote, + ValueThreshold(id(input.proposal.proposal))) val receivedPolkaCurrentVal = checkThreshold( newES.bk, newES.cs.round, - "Prevote", - {name: "Value", - value: id(input.proposal.proposal)}) + Prevote, + ValueThreshold(id(input.proposal.proposal))) val receivedCommitCurrentVal = checkThreshold( newES.bk, newES.cs.round, - "Precommit", - {name: "Value", - value: id(input.proposal.proposal)}) + Precommit, + ValueThreshold(id(input.proposal.proposal))) if (newES.cs.step == "propose") if (input.proposal.validRound == -1) if (receivedPolkaCurrentVal) @@ -402,7 +400,7 @@ pure def ProposalMsg (es: DriverState, input: DriverInput) : (DriverState, Conse // (not(valid(input.proposal))) // keep ES (don't use newES here), that is, drop proposal if (es.cs.step == "propose" and es.cs.round == input.proposal.round and es.cs.height == input.proposal.height) - if (checkThreshold(es.bk, es.cs.round, "Prevote", {name: "Value", value: id(input.proposal.proposal)})) + if (checkThreshold(es.bk, es.cs.round, Prevote, ValueThreshold(id(input.proposal.proposal)))) val csInput: ConsensusInput = {name: "ProposalAndPolkaAndInValid", height: es.cs.height, round: es.cs.round, @@ -418,8 +416,6 @@ pure def ProposalMsg (es: DriverState, input: DriverInput) : (DriverState, Conse callConsensus(es, es.bk, csInput) else (es, defaultOutput) -} - // We do this when we need to jump to a new round pure def skip (es: DriverState, r: int) : (DriverState, ConsensusOutput) = { @@ -481,8 +477,6 @@ pure def setValue(es: DriverState, value: Value_t) : (DriverState, ConsensusOutp ({ ...es, nextValueToPropose: value }, defaultOutput) - - /* ********************************************************* * Main entry point * ********************************************************/ @@ -491,7 +485,7 @@ pure def setValue(es: DriverState, value: Value_t) : (DriverState, ConsensusOutp pure def driver (es: DriverState, input: DriverInput) : (DriverState, ConsensusOutput) = { // TODO: shall we check whether the sender actually is in the validator set if (input.name == "proposal") { - val res = ProposalMsg(es, input) + val res = proposal(es, input) if (res._2.name == "decided") decided (res._1, res._2) else if (res._2.name == "skipRound") @@ -503,7 +497,7 @@ pure def driver (es: DriverState, input: DriverInput) : (DriverState, ConsensusO val res = applyVote(es.bk, toVote(input.vote), es.valset.get(input.vote.src), es.cs.round) val newES = { ...es, bk: res.bookkeeper, voteKeeperOutput: res.output } // only a commit event can come here. - val cons_res = Precommit(newES, input, res.output) + val cons_res = precommit(newES, input, res.output) if (cons_res._2.name == "decided") decided (cons_res._1, cons_res._2) else if (cons_res._2.name == "skipRound") @@ -515,7 +509,7 @@ pure def driver (es: DriverState, input: DriverInput) : (DriverState, ConsensusO val res = applyVote(es.bk, toVote(input.vote), es.valset.get(input.vote.src), es.cs.round) val newES = { ...es, bk: res.bookkeeper, voteKeeperOutput: res.output} // only a commit event can come here. - val cons_res = Prevote(newES, input, res.output) + val cons_res = prevote(newES, input, res.output) if (cons_res._2.name == "decided") // TODO: dead branch. But we should put this after consensus call logic into a function decided (cons_res._1, cons_res._2) @@ -525,7 +519,7 @@ pure def driver (es: DriverState, input: DriverInput) : (DriverState, ConsensusO cons_res } else if (input.name == "timeout") { - val res = Timeout(es, input) + val res = timeout(es, input) // result should be vote or skip if (res._2.name == "skipRound") skip (res._1, res._2.skipRound) @@ -584,9 +578,9 @@ pure def nextAction (state: NodeState) : (NodeState, DriverInput) = { val timeouts = state.timeout.filter(x => x._2 == state.es.cs.height and x._3 == state.es.cs.round) // val timeout = timeouts.chooseSome() - val timeout = timeouts.fold(("", 0, 0), (sum, y) => y) - val newstate = { ...state, timeout: state.timeout.exclude(Set(timeout))} - (newstate, { ...defaultInput, name: "timeout", timeout: timeout._1}) + val someTimeout = timeouts.fold(("", 0, 0), (sum, y) => y) + val newstate = { ...state, timeout: state.timeout.exclude(Set(someTimeout))} + (newstate, { ...defaultInput, name: "timeout", timeout: someTimeout._1}) else (state, defaultInput) } @@ -620,9 +614,9 @@ pure def nextActionCommand (state: NodeState, command: str) : (NodeState, Driver val timeouts = state.timeout.filter(x => x._2 == state.es.cs.height and x._3 == state.es.cs.round) // val timeout = timeouts.chooseSome() - val timeout = timeouts.fold(("", 0, 0), (sum, y) => y) - val newstate = { ...state, timeout: state.timeout.exclude(Set(timeout))} - (newstate, { ...defaultInput, name: "timeout", timeout: timeout._1}) + val someTimeout = timeouts.fold(("", 0, 0), (sum, y) => y) + val newstate = { ...state, timeout: state.timeout.exclude(Set(someTimeout))} + (newstate, { ...defaultInput, name: "timeout", timeout: someTimeout._1}) else (state, defaultInput) diff --git a/Specs/Quint/extraSpells.qnt b/Specs/Quint/extraSpells.qnt index 0400dfda0..9e9ff013e 100644 --- a/Specs/Quint/extraSpells.qnt +++ b/Specs/Quint/extraSpells.qnt @@ -1,4 +1,5 @@ // -*- mode: Bluespec; -*- + /** * This module collects definitions that are ubiquitous. * One day they will become the standard library of Quint. @@ -205,4 +206,4 @@ module extraSpells { pure def forallValues(__map: a -> b, __f: b => bool): bool = __map.keys().forall(__key => __f(__map.get(__key))) -} \ No newline at end of file +} diff --git a/Specs/Quint/types.qnt b/Specs/Quint/types.qnt new file mode 100644 index 000000000..ff3a01db4 --- /dev/null +++ b/Specs/Quint/types.qnt @@ -0,0 +1,78 @@ +// -*- mode: Bluespec; -*- + +module types { + + // A node address + type Address = str + val noAddress: Address = "" + + // Height in the Tendermint algorithm + type Height = int + + // Round in the Tendermint algorithm + type Round = int + + // Value proposed and voted to be included in the chain + // A value is either nil or a string + type NonNilValue = str + type Value = Nil | Val(NonNilValue) + pure def getVal(value: Value): NonNilValue = + match value { + | Val(v) => v + | Nil => "getVal error" // this should not happen + } + pure def isValid(value: NonNilValue): bool = + // for simulation, if the value is "invalid", so is the proposal + // if this is to become "non-deterministic", we must push it + // and its use into the state machine + value != "invalid" + + // The stake of an address + type Weight = int + + // Names of round steps + type Step = + | NewRoundStep + | ProposeStep + | PrevoteStep + | PrecommitStep + | DecidedStep + + type Timeout = + | ProposeTimeout // Timeout waiting for proposal. + | PrevoteTimeout // Timeout waiting for prevotes for a value. + | PrecommitTimeout // Timeout waiting for precommits for a value. + + type ValueId = Value + + // efficient hashing function + // pure def id(value: Value): ValueId = value + pure def id(value) = value + + // ************************************************************************* + // Messages + // ************************************************************************* + + type Proposal = { + srcAddress: Address, + height: Height, + round: Round, + proposal: NonNilValue, // an actual value. All other values are id(proposal) + validRound: Round, + } + + def mkProposal(src, height, round, value, validRound) = + { srcAddress: src, height: height, round: round, proposal: value, validRound: validRound } + + type VoteType = Prevote | Precommit + type Vote = { + voteType: VoteType, + srcAddress: Address, + height: Height, + round: Round, + valueId: ValueId, + } + pure def mkVote(voteType, srcAddress, height, round, valueId) = + { voteType: voteType, srcAddress: srcAddress, height: height, round: round, valueId: valueId } + +} diff --git a/Specs/Quint/voteBookkeeper.qnt b/Specs/Quint/voteBookkeeper.qnt index 1e1c1676f..a84b65b6b 100644 --- a/Specs/Quint/voteBookkeeper.qnt +++ b/Specs/Quint/voteBookkeeper.qnt @@ -2,6 +2,7 @@ module voteBookkeeper { + import types.* from "./types" import basicSpells.* from "./basicSpells" import extraSpells.* from "./extraSpells" @@ -9,40 +10,13 @@ module voteBookkeeper { // Types // **************************************************************************** - // Node address - type Address = str - - // Round in the Tendermint algorithm - type Round = int - - // Height in the Tendermint algorithm - type Height = int - - // Value proposed and voted to be included in the chain - type Value = str - val Nil: Value = "nil" // a special value of type Value - - // The stake of a node - type Weight = int - - type VoteType = str - val VoteTypes: Set[VoteType] = Set("Prevote", "Precommit") - def isPrevote(voteType) = voteType == "Prevote" - def isPrecommit(voteType) = voteType == "Precommit" - - type Vote = { - typ: VoteType, - height: Height, - round: Round, - value: Value, - address: Address - } - - type WeightedVote = (Vote, Weight, Round) + type WeightedVote = + | NoWeightedVote + | WV((Vote, Weight, Round)) type VoteCount = { totalWeight: Weight, - valuesWeights: Value -> Weight, // including the "nil" value + valuesWeights: Value -> Weight, votesAddresses: Set[Address] } @@ -55,29 +29,21 @@ module voteBookkeeper { votesAddressesWeights: Address -> Weight } - type Threshold = { - name: str, - value: Value - } - val unreachedThreshold = { name: "Unreached", value: "null" } - val nilThreshold = { name: "Nil", value: "null" } - val anyThreshold = { name: "Any", value: "null" } - val skipThreshold = { name: "Skip", value: "null" } - pure def valueThreshold(value) = { name: "Value", value: value } - pure def isThresholdOnValue(t: Threshold): bool = t.name == "Value" - - type VoteKeeperOutput = { - round: Round, - name: str, - value: Value - } - pure def noOutput(round) = { round: round, name: "None", value: "null" } - pure def polkaValueOutput(round, value) = { round: round, name: "PolkaValue", value: value } - pure def polkaNilOutput(round) = { round: round, name: "PolkaNil", value: "null" } - pure def polkaAnyOutput(round) = { round: round, name: "PolkaAny", value: "null" } - pure def precommitValueOutput(round, value) = { round: round, name: "PrecommitValue", value: value } - pure def precommitAnyOutput(round) = { round: round, name: "PrecommitAny", value: "null" } - pure def skipOutput(round) = { round: round, name: "Skip", value: "null" } + type Threshold = + | UnreachedThreshold + | NilThreshold + | AnyThreshold + | SkipThreshold + | ValueThreshold(NonNilValue) + + type VoteKeeperOutput = + | NoVKOutput + | PolkaAnyVKOutput(Round) + | PolkaNilVKOutput(Round) + | PolkaValueVKOutput((Round, NonNilValue)) + | PrecommitAnyVKOutput(Round) + | PrecommitValueVKOutput((Round, NonNilValue)) + | SkipVKOutput(Round) type Bookkeeper = { height: Height, @@ -85,6 +51,12 @@ module voteBookkeeper { rounds: Round -> RoundVotes } + pure def initBookKeeper(initialHeight: Height, totalVotingPower: Weight): Bookkeeper = { + height: initialHeight, + totalWeight: totalVotingPower, + rounds: Map() + } + // **************************************************************************** // Functional Layer // **************************************************************************** @@ -139,37 +111,37 @@ module voteBookkeeper { // Adds a weighted vote to a voteCount if there is no vote registered for the voter. pure def addVote(voteCount: VoteCount, vote: Vote, weight: Weight): VoteCount = - if (vote.address.in(voteCount.votesAddresses)) + if (vote.srcAddress.in(voteCount.votesAddresses)) // Do not count vote if address has already voted. voteCount else - val newWeight = voteCount.valuesWeights.getOrElse(vote.value, 0) + weight + val newWeight = voteCount.valuesWeights.getOrElse(vote.valueId, 0) + weight voteCount - .with("valuesWeights", voteCount.valuesWeights.mapSafeSet(vote.value, newWeight)) - .with("votesAddresses", voteCount.votesAddresses.setAdd(vote.address)) + .with("valuesWeights", voteCount.valuesWeights.mapSafeSet(vote.valueId, newWeight)) + .with("votesAddresses", voteCount.votesAddresses.setAdd(vote.srcAddress)) run addVoteTest = val voteCount = { totalWeight: 100, - valuesWeights: Map("val1" -> 30, "val2" -> 20), + valuesWeights: Map(Val("val1") -> 30, Val("val2") -> 20), votesAddresses: Set("alice", "bob") } - val vote = { typ: "Precommit", height: 1, round: 10, value: "val3", address: "john" } + val vote = { voteType: Precommit, height: 1, round: 10, valueId: Val("val3"), srcAddress: "john" } all { // new voter, new value assert(addVote(voteCount, vote, 10) == { totalWeight: 100, - valuesWeights: Map("val1" -> 30, "val2" -> 20, "val3" -> 10), + valuesWeights: Map(Val("val1") -> 30, Val("val2") -> 20, Val("val3") -> 10), votesAddresses: Set("alice", "bob", "john") }), // new voter, existing value - assert(addVote(voteCount, vote.with("value", "val2"), 10) == { + assert(addVote(voteCount, vote.with("valueId", Val("val2")), 10) == { totalWeight: 100, - valuesWeights: Map("val1" -> 30, "val2" -> 30), + valuesWeights: Map(Val("val1") -> 30, Val("val2") -> 30), votesAddresses: Set("alice", "bob", "john") }), // existing voter - assert(addVote(voteCount, vote.with("address", "alice"), 10) == voteCount), + assert(addVote(voteCount, vote.with("srcAddress", "alice"), 10) == voteCount), } // Given a voteCount and a value, the function returns: @@ -179,65 +151,70 @@ module voteBookkeeper { // - A threshold Unreached otherwise indicating that no quorum has been yet reached. pure def computeThreshold(voteCount: VoteCount, value: Value): Threshold = if (voteCount.hasQuorumOnValue(value)) { - if (value == Nil) - nilThreshold - else - valueThreshold(value) + match value { + | Nil => NilThreshold + | Val(v) => ValueThreshold(v) + } } else if (voteCount.hasQuorumOnAny()) { - anyThreshold + AnyThreshold } else - unreachedThreshold + UnreachedThreshold + + run computeThresholdTest = + val voteCount = {totalWeight: 100, valuesWeights: Map(), votesAddresses: Set("alice", "bob")} + val mapValueReached = Map(Val("v1") -> 67, Val("v2") -> 20) + val mapNilReached = Map(Nil -> 70, Val("v2") -> 20) + val mapNoneReached = Map(Nil -> 20, Val("v2") -> 20) + all { + assert(computeThreshold(voteCount, Val("v3")) == UnreachedThreshold), + assert(computeThreshold(voteCount.with("valuesWeights", mapValueReached), Val("v1")) == ValueThreshold("v1")), + assert(computeThreshold(voteCount.with("valuesWeights", mapValueReached), Val("v2")) == AnyThreshold), + assert(computeThreshold(voteCount.with("valuesWeights", mapNilReached), Nil) == NilThreshold), + assert(computeThreshold(voteCount.with("valuesWeights", mapNilReached), Val("v2")) == AnyThreshold), + assert(computeThreshold(voteCount.with("valuesWeights", mapNoneReached), Val("v1")) == UnreachedThreshold), + assert(computeThreshold(voteCount.with("valuesWeights", mapNoneReached), Nil) == UnreachedThreshold), + } // Given a round, voteType and threshold, return the corresponding VoteKeeperOutput pure def toVoteKeeperOutput(round: Round, voteType: VoteType, threshold: Threshold): VoteKeeperOutput = - if (threshold == unreachedThreshold) - noOutput(round) - - // prevote - else if (voteType.isPrevote() and threshold.isThresholdOnValue()) - polkaValueOutput(round, threshold.value) - else if (voteType.isPrevote() and threshold == nilThreshold) - polkaNilOutput(round) - else if (voteType.isPrevote() and threshold == anyThreshold) - polkaAnyOutput(round) - - // precommit - else if (voteType.isPrecommit() and threshold.isThresholdOnValue()) - precommitValueOutput(round, threshold.value) - else if (voteType.isPrecommit() and threshold.in(Set(anyThreshold, nilThreshold))) - precommitAnyOutput(round) - - else if (threshold == skipThreshold) - skipOutput(round) - - else - noOutput(round) + match threshold { + | UnreachedThreshold => + NoVKOutput + | ValueThreshold(value) => + match voteType { + | Prevote => PolkaValueVKOutput((round, value)) + | Precommit => PrecommitValueVKOutput((round, value)) + } + | NilThreshold => + match voteType { + | Prevote => PolkaNilVKOutput(round) + | Precommit => PrecommitAnyVKOutput(round) + } + | AnyThreshold => + match voteType { + | Prevote => PolkaAnyVKOutput(round) + | Precommit => PrecommitAnyVKOutput(round) + } + | SkipThreshold => + SkipVKOutput(round) + } run toVoteKeeperOutputTest = val round = 10 all { - assert(toVoteKeeperOutput(round, "Prevote", unreachedThreshold) == noOutput(round)), - assert(toVoteKeeperOutput(round, "Precommit", unreachedThreshold) == noOutput(round)), - - assert(toVoteKeeperOutput(round, "Prevote", anyThreshold) == polkaAnyOutput(round)), - assert(toVoteKeeperOutput(round, "Prevote", nilThreshold) == polkaNilOutput(round)), - assert(toVoteKeeperOutput(round, "Prevote", valueThreshold("val1")) == polkaValueOutput(round, "val1")), - - assert(toVoteKeeperOutput(round, "Precommit", anyThreshold) == precommitAnyOutput(round)), - assert(toVoteKeeperOutput(round, "Precommit", nilThreshold) == precommitAnyOutput(round)), - assert(toVoteKeeperOutput(round, "Precommit", valueThreshold("val1")) == precommitValueOutput(round, "val1")), - - assert(toVoteKeeperOutput(round, "Prevote", skipThreshold) == skipOutput(round)), - assert(toVoteKeeperOutput(round, "Precommit", skipThreshold) == skipOutput(round)), - - assert(toVoteKeeperOutput(round, "Precommit", { name: "Error", value: "null" }) == noOutput(round)), - assert(toVoteKeeperOutput(round, "Error", anyThreshold) == noOutput(round)), + assert(toVoteKeeperOutput(round, Prevote, UnreachedThreshold) == NoVKOutput), + assert(toVoteKeeperOutput(round, Precommit, UnreachedThreshold) == NoVKOutput), + assert(toVoteKeeperOutput(round, Prevote, AnyThreshold) == PolkaAnyVKOutput(round)), + assert(toVoteKeeperOutput(round, Prevote, NilThreshold) == PolkaNilVKOutput(round)), + assert(toVoteKeeperOutput(round, Prevote, ValueThreshold("v1")) == PolkaValueVKOutput((round, "v1"))), + assert(toVoteKeeperOutput(round, Precommit, AnyThreshold) == PrecommitAnyVKOutput(round)), + assert(toVoteKeeperOutput(round, Precommit, NilThreshold) == PrecommitAnyVKOutput(round)), + assert(toVoteKeeperOutput(round, Precommit, ValueThreshold("v1")) == PrecommitValueVKOutput((round, "v1"))), + assert(toVoteKeeperOutput(round, Prevote, SkipThreshold) == SkipVKOutput(round)), + assert(toVoteKeeperOutput(round, Precommit, SkipThreshold) == SkipVKOutput(round)), } - // Driver interface - type BKOutput = { bookkeeper: Bookkeeper, output: VoteKeeperOutput } - - // Called by the driver when it receives a vote. The function takes the following steps: + // Called by the executor when it receives a vote. The function takes the following steps: // - It first adds the vote and then computes a threshold. // - If there exist a threshold and has not emitted before, the function returns the corresponding VoteKeeperOutput. // - Otherwise, the function returns a no-threshold output. @@ -250,39 +227,39 @@ module voteBookkeeper { val roundVotes = keeper.rounds.getOrElse(round, newRoundVotes(keeper.height, round, keeper.totalWeight)) val updatedVoteCount = - if (vote.typ.isPrevote()) - roundVotes.prevotes.addVote(vote, weight) - else - roundVotes.precommits.addVote(vote, weight) + match vote.voteType { + | Prevote => roundVotes.prevotes.addVote(vote, weight) + | Precommit => roundVotes.precommits.addVote(vote, weight) + } val updatedVotesAddressesWeights = - if (roundVotes.votesAddressesWeights.has(vote.address)) + if (roundVotes.votesAddressesWeights.has(vote.srcAddress)) roundVotes.votesAddressesWeights else - roundVotes.votesAddressesWeights.mapSafeSet(vote.address, weight) + roundVotes.votesAddressesWeights.mapSafeSet(vote.srcAddress, weight) // Combined weight of all validators at this height val combinedWeight = updatedVotesAddressesWeights.mapSumValues() val finalOutput = if (vote.round > currentRound and isSkip(combinedWeight, keeper.totalWeight)) - skipOutput(vote.round) + SkipVKOutput(vote.round) else - val threshold = computeThreshold(updatedVoteCount, vote.value) - val output = toVoteKeeperOutput(vote.round, vote.typ, threshold) + val threshold = computeThreshold(updatedVoteCount, vote.valueId) + val output = toVoteKeeperOutput(vote.round, vote.voteType, threshold) if (not(output.in(roundVotes.emittedOutputs))) output else - noOutput(vote.round) + NoVKOutput val updatedRoundVotes = - if (vote.typ.isPrevote()) - roundVotes.with("prevotes", updatedVoteCount) - else - roundVotes.with("precommits", updatedVoteCount) + match vote.voteType { + | Prevote => roundVotes.with("prevotes", updatedVoteCount) + | Precommit => roundVotes.with("precommits", updatedVoteCount) + } val updatedRoundVotes2 = updatedRoundVotes .with("votesAddressesWeights", updatedVotesAddressesWeights) - .with("emittedOutputs", roundVotes.emittedOutputs.setAddIf(finalOutput, finalOutput.name != "None")) + .with("emittedOutputs", roundVotes.emittedOutputs.setAddIf(finalOutput, finalOutput != NoVKOutput)) val updatedBookkeeper = keeper .with("rounds", keeper.rounds.mapSafeSet(vote.round, updatedRoundVotes2)) @@ -293,23 +270,23 @@ module voteBookkeeper { val roundVotes: RoundVotes = { height: 0, round: 0, - prevotes: { totalWeight: 4, votesAddresses: Set(), valuesWeights: Map("v1" -> 1, Nil -> 3) }, + prevotes: { totalWeight: 4, votesAddresses: Set(), valuesWeights: Map(Val("v1") -> 1, Nil -> 3) }, precommits: { totalWeight: 4, votesAddresses: Set(), valuesWeights: Map() }, emittedOutputs: Set(), votesAddressesWeights: Map(), } val vk: Bookkeeper = { height: 0, totalWeight: 4, rounds: Map(0 -> roundVotes) } - val o1 = applyVote(vk, { height: 0, round: 0, address: "a0", typ: "Precommit", value: Nil }, 1, 0) - val o2 = applyVote(o1.bookkeeper, { height: 0, round: 0, address: "a1", typ: "Precommit", value: Nil }, 1, 0) - val o3 = applyVote(o2.bookkeeper, { height: 0, round: 0, address: "a2", typ: "Precommit", value: Nil }, 1, 0) - val o4 = applyVote(o3.bookkeeper, { height: 0, round: 0, address: "a3", typ: "Precommit", value: Nil }, 1, 0) - val o5 = applyVote(o4.bookkeeper, { height: 0, round: 1, address: "a4", typ: "Precommit", value: Nil }, 3, 0) + val o1 = applyVote(vk, { height: 0, round: 0, srcAddress: "a0", voteType: Precommit, valueId: Nil }, 1, 0) + val o2 = applyVote(o1.bookkeeper, { height: 0, round: 0, srcAddress: "a1", voteType: Precommit, valueId: Nil }, 1, 0) + val o3 = applyVote(o2.bookkeeper, { height: 0, round: 0, srcAddress: "a2", voteType: Precommit, valueId: Nil }, 1, 0) + val o4 = applyVote(o3.bookkeeper, { height: 0, round: 0, srcAddress: "a3", voteType: Precommit, valueId: Nil }, 1, 0) + val o5 = applyVote(o4.bookkeeper, { height: 0, round: 1, srcAddress: "a4", voteType: Precommit, valueId: Nil }, 3, 0) all { - assert(o1.output.name == "None"), - assert(o2.output.name == "None"), - assert(o3.output.name == "PrecommitAny"), - assert(o4.output.name == "None"), - assert(o5.output.name == "Skip"), + assert(o1.output == NoVKOutput), + assert(o2.output == NoVKOutput), + assert(o3.output == PrecommitAnyVKOutput(0)), + assert(o4.output == NoVKOutput), + assert(o5.output == SkipVKOutput(1)), } // Called by the driver to check if there is a specific threshold for a given round and voteType. @@ -320,32 +297,16 @@ module voteBookkeeper { pure def checkThreshold(keeper: Bookkeeper, round: Round, voteType: VoteType, threshold: Threshold): bool = if (keeper.rounds.has(round)) { val roundVotes = keeper.rounds.get(round) - val voteCount = if (voteType.isPrevote()) roundVotes.prevotes else roundVotes.precommits + val voteCount = if (voteType == Prevote) roundVotes.prevotes else roundVotes.precommits checkThresholdOnVoteCount(threshold, voteCount) } else false pure def checkThresholdOnVoteCount(threshold: Threshold, voteCount: VoteCount): bool = - if (threshold.isThresholdOnValue()) - voteCount.hasQuorumOnValue(threshold.value) - else if (threshold == nilThreshold) - voteCount.hasQuorumOnNil() - else if (threshold == anyThreshold) - voteCount.hasQuorumOnAny() - else false - - run computeThresholdTest = - val voteCount = { totalWeight: 100, valuesWeights: Map(), votesAddresses: Set("alice", "bob") } - val mapValueReached = Map("val1" -> 67, "val2" -> 20) - val mapNilReached = Map(Nil -> 70, "val2" -> 20) - val mapNoneReached = Map(Nil -> 20, "val2" -> 20) - all { - assert(computeThreshold(voteCount, "val3") == unreachedThreshold), - assert(computeThreshold(voteCount.with("valuesWeights", mapValueReached), "val1") == valueThreshold("val1")), - assert(computeThreshold(voteCount.with("valuesWeights", mapValueReached), "val2") == anyThreshold), - assert(computeThreshold(voteCount.with("valuesWeights", mapNilReached), Nil) == nilThreshold), - assert(computeThreshold(voteCount.with("valuesWeights", mapNilReached), "val2") == anyThreshold), - assert(computeThreshold(voteCount.with("valuesWeights", mapNoneReached), "val1") == unreachedThreshold), - assert(computeThreshold(voteCount.with("valuesWeights", mapNoneReached), Nil) == unreachedThreshold), + match threshold { + | ValueThreshold(v) => voteCount.hasQuorumOnValue(Val(v)) + | NilThreshold => voteCount.hasQuorumOnValue(Nil) + | AnyThreshold => voteCount.hasQuorumOnAny() + | _ => false } // ************************************************************************ @@ -393,17 +354,18 @@ module voteBookkeeper { lastEmitted' = lastEmitted, } - action initWith(initialHeight: Height, totalWeight: Weight): bool = all { - weightedVote' = ({ typ: "", height: initialHeight, round: -1, value: "", address: "" }, -1, -1), - bookkeeper' = { height: initialHeight, totalWeight: totalWeight, rounds: Map() }, - lastEmitted' = { round: -1, name: "", value: "null" } + action initWith(initialHeight: Height, initialTotalWeight: Weight): bool = all { + weightedVote' = NoWeightedVote, + bookkeeper' = initBookKeeper(initialHeight, initialTotalWeight), + lastEmitted' = NoVKOutput, } - // The vote keeper receives a weighted vote and produces an output. + // The vote bookkeeper receives a weighted vote for a round, and produces an output. action applyVoteAction(vote: Vote, weight: Weight, currentRound: Round): bool = + // HV: currentRound should be taken from the bookkeeper? val output = applyVote(bookkeeper, vote, weight, currentRound) all { - weightedVote' = (vote, weight, currentRound), + weightedVote' = WV((vote, weight, currentRound)), bookkeeper' = output.bookkeeper, lastEmitted' = output.output } diff --git a/Specs/Quint/voteBookkeeperModels.qnt b/Specs/Quint/voteBookkeeperModels.qnt new file mode 100644 index 000000000..f6d39bc74 --- /dev/null +++ b/Specs/Quint/voteBookkeeperModels.qnt @@ -0,0 +1,80 @@ +// -*- mode: Bluespec; -*- + +// **************************************************************************** +// Vote Bookkeeper State Machine +// **************************************************************************** + +module voteBookkeeperSM { + + import types.* from "./types" + import voteBookkeeper.* from "./voteBookkeeper" + export voteBookkeeper.* + + // ************************************************************************ + // Model parameters + // ************************************************************************ + + const INITIAL_HEIGHT: Height + const INITIAL_TOTAL_WEIGHT: Weight + const ADDRESS_WEIGHTS: Address -> Weight // an address has a constant weight during a height + const ROUNDS: Set[Round] + const VALUES: Set[NonNilValue] + + // ************************************************************************ + // State machine + // ************************************************************************ + + action init = + initWith(INITIAL_HEIGHT, INITIAL_TOTAL_WEIGHT) + + action step = + nondet voteType = oneOf(Set(Prevote, Precommit)) + nondet round = oneOf(ROUNDS) + nondet value = oneOf(VALUES.map(v => Val(v)).union(Set(Nil))) + nondet address = oneOf(ADDRESS_WEIGHTS.keys()) + val height = INITIAL_HEIGHT + val vote: Vote = mkVote(voteType, address, height, round, id(value)) + applyVoteAction(vote, ADDRESS_WEIGHTS.get(address), round) + + // **************************************************************************** + // Properties that define an expected final state (for generating traces) + // **************************************************************************** + + val emitPrecommitValueState = + nondet round = oneOf(ROUNDS) + nondet value = oneOf(VALUES) + lastEmitted == PrecommitValueVKOutput((round, value)) + val emitPrecommitValue = not(emitPrecommitValueState) + + val emitPolkaAnyState = + nondet round = oneOf(ROUNDS) + lastEmitted == PolkaAnyVKOutput(round) + val emitPolkaAny = not(emitPolkaAnyState) + + val emitPolkaNilState = + nondet round = oneOf(ROUNDS) + lastEmitted == PolkaNilVKOutput(round) + val emitPolkaNil = not(emitPolkaNilState) + + val emitSkipState = + nondet round = oneOf(ROUNDS) + lastEmitted == SkipVKOutput(round) + val emitSkip = not(emitSkipState) + +} + +// **************************************************************************** +// Vote Bookkeeper Models +// **************************************************************************** + +module voteBookkeeperModels { + + import voteBookkeeperSM( + INITIAL_HEIGHT = 1, + INITIAL_TOTAL_WEIGHT = 100, + ADDRESS_WEIGHTS = Map("alice" -> 10, "bob" -> 30, "john" -> 60), + ROUNDS = 0.to(2), + VALUES = Set("v1", "v2") + ) as VK1 + +} diff --git a/Specs/Quint/voteBookkeeperSM.qnt b/Specs/Quint/voteBookkeeperSM.qnt deleted file mode 100644 index e52921c48..000000000 --- a/Specs/Quint/voteBookkeeperSM.qnt +++ /dev/null @@ -1,36 +0,0 @@ -// **************************************************************************** -// Vote Bookkeeper State machine -// **************************************************************************** - -module voteBookkeeperSM { - - import voteBookkeeper.* from "./voteBookkeeper" - export voteBookkeeper.* - - // ************************************************************************ - // Model parameters - // ************************************************************************ - - const INITIAL_HEIGHT: Height - const INITIAL_TOTAL_WEIGHT: Weight - const ADDRESS_WEIGHTS: Address -> Weight // an address has a constant weight during a height - const ROUNDS: Set[Round] - const VALUES: Set[Value] - - // ************************************************************************ - // State machine - // ************************************************************************ - - action init = - initWith(INITIAL_HEIGHT, INITIAL_TOTAL_WEIGHT) - - action step = - nondet voteType = oneOf(VoteTypes) - nondet round = oneOf(ROUNDS) - nondet value = oneOf(VALUES.union(Set(Nil))) - nondet address = oneOf(ADDRESS_WEIGHTS.keys()) - val height = INITIAL_HEIGHT - val vote: Vote = { typ: voteType, height: height, round: round, value: value, address: address } - applyVoteAction(vote, ADDRESS_WEIGHTS.get(address), 1) - -} diff --git a/Specs/Quint/voteBookkeeperTest.qnt b/Specs/Quint/voteBookkeeperTest.qnt index 95c369e70..ef9bf9f5e 100644 --- a/Specs/Quint/voteBookkeeperTest.qnt +++ b/Specs/Quint/voteBookkeeperTest.qnt @@ -1,12 +1,9 @@ +// -*- mode: Bluespec; -*- + module voteBookkeeperTest { - import voteBookkeeperSM( - INITIAL_HEIGHT = 1, - INITIAL_TOTAL_WEIGHT = 100, - ADDRESS_WEIGHTS = Map("alice" -> 10, "bob" -> 30, "john" -> 60), - ROUNDS = 1.to(2), - VALUES = Set("val1", "val2") - ).* from "./voteBookkeeperSM" + import types.* from "./types" + import voteBookkeeper.* from "./voteBookkeeper" // **************************************************************************** // Tests @@ -21,114 +18,99 @@ module voteBookkeeperTest { // each of the total voting power run synchronousConsensusTest = initWith(1, 100) - .then(applyVoteAction({typ: "Prevote", height: 1, round: 1, value: "val1", address: "alice"}, 60, 1)) - .then(_assert(lastEmitted == noOutput(1))) - .then(applyVoteAction({typ: "Prevote", height: 1, round: 1, value: "val1", address: "john"}, 10, 1)) - .then(_assert(lastEmitted == polkaValueOutput(1, "val1"))) - .then(applyVoteAction({typ: "Prevote", height: 1, round: 1, value: "val1", address: "bob"}, 30, 1)) - .then(_assert(lastEmitted == noOutput(1))) - .then(applyVoteAction({typ: "Precommit", height: 1, round: 1, value: "val1", address: "bob"}, 30, 1)) - .then(_assert(lastEmitted == noOutput(1))) - .then(applyVoteAction({typ: "Precommit", height: 1, round: 1, value: "val1", address: "john"}, 10, 1)) - .then(_assert(lastEmitted == noOutput(1))) - .then(applyVoteAction({typ: "Precommit", height: 1, round: 1, value: "val1", address: "alice"}, 60, 1)) - .then(_assert(lastEmitted == precommitValueOutput(1, "val1"))) + .then(applyVoteAction(mkVote(Prevote, "alice", 1, 1, Val("v1")), 60, 1)) + .then(_assert(lastEmitted == NoVKOutput)) + .then(applyVoteAction(mkVote(Prevote, "john", 1, 1, Val("v1")), 10, 1)) + .then(_assert(lastEmitted == PolkaValueVKOutput((1, "v1")))) + .then(applyVoteAction(mkVote(Prevote, "bob", 1, 1, Val("v1")), 30, 1)) + .then(_assert(lastEmitted == NoVKOutput)) + .then(applyVoteAction(mkVote(Precommit, "bob", 1, 1, Val("v1")), 30, 1)) + .then(_assert(lastEmitted == NoVKOutput)) + .then(applyVoteAction(mkVote(Precommit, "john", 1, 1, Val("v1")), 10, 1)) + .then(_assert(lastEmitted == NoVKOutput)) + .then(applyVoteAction(mkVote(Precommit, "alice", 1, 1, Val("v1")), 60, 1)) + .then(_assert(lastEmitted == PrecommitValueVKOutput((1, "v1")))) // Reaching PolkaAny run polkaAnyTest = initWith(1, 100) - .then(applyVoteAction({typ: "Prevote", height: 1, round: 1, value: "val1", address: "alice"}, 60, 1)) - .then(_assert(lastEmitted == noOutput(1))) - .then(applyVoteAction({typ: "Prevote", height: 1, round: 1, value: "nil", address: "john"}, 10, 1)) - .then(_assert(lastEmitted == polkaAnyOutput(1))) + .then(applyVoteAction(mkVote(Prevote, "alice", 1, 1, Val("v1")), 60, 1)) + .then(_assert(lastEmitted == NoVKOutput)) + .then(applyVoteAction(mkVote(Prevote, "john", 1, 1, Nil), 10, 1)) + .then(_assert(lastEmitted == PolkaAnyVKOutput(1))) // Reaching PolkaNil run polkaNilTest = initWith(1, 100) - .then(applyVoteAction({typ: "Prevote", height: 1, round: 1, value: "nil", address: "alice"}, 60, 1)) - .then(_assert(lastEmitted == noOutput(1))) - .then(applyVoteAction({typ: "Prevote", height: 1, round: 1, value: "nil", address: "john"}, 10, 1)) - .then(_assert(lastEmitted == polkaNilOutput(1))) + .then(applyVoteAction(mkVote(Prevote, "alice", 1, 1, Nil), 60, 1)) + .then(_assert(lastEmitted == NoVKOutput)) + .then(applyVoteAction(mkVote(Prevote, "john", 1, 1, Nil), 10, 1)) + .then(_assert(lastEmitted == PolkaNilVKOutput(1))) // Reaching Skip via n+1 threshold with prevotes from two validators at a future round run skipSmallQuorumAllPrevotesTest = initWith(1, 100) - .then(applyVoteAction({typ: "Prevote", height: 1, round: 1, value: "val1", address: "alice"}, 60, 1)) - .then(_assert(lastEmitted == noOutput(1))) - .then(applyVoteAction({typ: "Prevote", height: 1, round: 2, value: "val1", address: "john"}, 10, 1)) - .then(_assert(lastEmitted == noOutput(2))) - .then(applyVoteAction({typ: "Prevote", height: 1, round: 2, value: "val1", address: "bob"}, 30, 1)) - .then(_assert(lastEmitted == skipOutput(2))) + .then(applyVoteAction(mkVote(Prevote, "alice", 1, 1, Val("v1")), 60, 1)) + .then(_assert(lastEmitted == NoVKOutput)) + .then(applyVoteAction(mkVote(Prevote, "john", 1, 2, Val("v1")), 10, 1)) + .then(_assert(lastEmitted == NoVKOutput)) + .then(applyVoteAction(mkVote(Prevote, "bob", 1, 2, Val("v1")), 30, 1)) + .then(_assert(lastEmitted == SkipVKOutput(2))) // Cannot reach Skip via f+1 threshold with one prevote and one precommit from the same validator at a future round run noSkipSmallQuorumMixedVotesSameValTest = initWith(1, 90) - .then(applyVoteAction({typ: "Prevote", height: 1, round: 1, value: "val1", address: "alice"}, 10, 1)) - .then(_assert(lastEmitted == noOutput(1))) - .then(applyVoteAction({typ: "Prevote", height: 1, round: 2, value: "val1", address: "john"}, 20, 1)) - .then(_assert(lastEmitted == noOutput(2))) - .then(applyVoteAction({typ: "Precommit", height: 1, round: 2, value: "val1", address: "john"}, 20, 1)) - .then(_assert(lastEmitted != skipOutput(2))) + .then(applyVoteAction(mkVote(Prevote, "alice", 1, 1, Val("v1")), 10, 1)) + .then(_assert(lastEmitted == NoVKOutput)) + .then(applyVoteAction(mkVote(Prevote, "john", 1, 2, Val("v1")), 20, 1)) + .then(_assert(lastEmitted == NoVKOutput)) + .then(applyVoteAction(mkVote(Precommit, "john", 1, 2, Val("v1")), 20, 1)) + .then(_assert(lastEmitted != SkipVKOutput(2))) // Reaching Skip via f+1 threshold with one prevote and one precommit from two validators at a future round run skipSmallQuorumMixedVotesTwoValsTest = initWith(1, 80) - .then(applyVoteAction({typ: "Prevote", height: 1, round: 1, value: "val1", address: "alice"}, 50, 1)) - .then(_assert(lastEmitted == noOutput(1))) - .then(applyVoteAction({typ: "Prevote", height: 1, round: 2, value: "val1", address: "john"}, 10, 1)) - .then(_assert(lastEmitted == noOutput(2))) - .then(applyVoteAction({typ: "Precommit", height: 1, round: 2, value: "val1", address: "bob"}, 20, 1)) - .then(_assert(lastEmitted == skipOutput(2))) - + .then(applyVoteAction(mkVote(Prevote, "alice", 1, 1, Val("v1")), 50, 1)) + .then(_assert(lastEmitted == NoVKOutput)) + .then(applyVoteAction(mkVote(Prevote, "john", 1, 2, Val("v1")), 10, 1)) + .then(_assert(lastEmitted == NoVKOutput)) + .then(applyVoteAction(mkVote(Prevote, "bob", 1, 2, Val("v1")), 20, 1)) + .then(_assert(lastEmitted == SkipVKOutput(2))) + // Reaching Skip via 2f+1 threshold with a single prevote from a single validator at a future round run skipQuorumSinglePrevoteTest = initWith(1, 100) - .then(applyVoteAction({typ: "Prevote", height: 1, round: 1, value: "val1", address: "alice"}, 10, 1)) - .then(all { assert(lastEmitted == noOutput(1)), allUnchanged }) - .then(applyVoteAction({typ: "Prevote", height: 1, round: 2, value: "val1", address: "john"}, 60, 1)) - .then(all { assert(lastEmitted == skipOutput(2)), allUnchanged }) + .then(applyVoteAction(mkVote(Prevote, "alice", 1, 1, Val("v1")), 10, 1)) + .then(_assert(lastEmitted == NoVKOutput)) + .then(applyVoteAction(mkVote(Prevote, "john", 1, 2, Val("v1")), 60, 1)) + .then(_assert(lastEmitted == SkipVKOutput(2))) // Reaching Skip via 2f+1 threshold with a single precommit from a single validator at a future round run skipQuorumSinglePrecommitTest = initWith(1, 100) - .then(applyVoteAction({typ: "Prevote", height: 1, round: 1, value: "val1", address: "alice"}, 10, 1)) - .then(_assert(lastEmitted == {round: 1, name: "None", value: "null"})) - .then(applyVoteAction({typ: "Precommit", height: 1, round: 2, value: "val1", address: "john"}, 60, 1)) - .then(_assert(lastEmitted == skipOutput(2))) + .then(applyVoteAction(mkVote(Prevote, "alice", 1, 1, Val("v1")), 10, 1)) + .then(_assert(lastEmitted == NoVKOutput)) + .then(applyVoteAction(mkVote(Precommit, "john", 1, 2, Val("v1")), 60, 1)) + .then(_assert(lastEmitted == SkipVKOutput(2))) // Cannot reach Skip via 2f+1 threshold with one prevote and one precommit from the same validator at a future round run noSkipQuorumMixedVotesSameValTest = initWith(1, 100) - .then(applyVoteAction({typ: "Prevote", height: 1, round: 1, value: "val1", address: "alice"}, 10, 1)) - .then(_assert(lastEmitted == noOutput(1))) - .then(applyVoteAction({typ: "Prevote", height: 1, round: 2, value: "val1", address: "john"}, 30, 1)) - .then(_assert(lastEmitted == noOutput(2))) - .then(applyVoteAction({typ: "Precommit", height: 1, round: 2, value: "val1", address: "john"}, 30, 1)) - .then(_assert(lastEmitted != skipOutput(2))) + .then(applyVoteAction(mkVote(Prevote, "alice", 1, 1, Val("v1")), 10, 1)) + .then(_assert(lastEmitted == NoVKOutput)) + .then(applyVoteAction(mkVote(Prevote, "john", 1, 2, Val("v1")), 30, 1)) + .then(_assert(lastEmitted == NoVKOutput)) + .then(applyVoteAction(mkVote(Precommit, "john", 1, 2, Val("v1")), 30, 1)) + .then(_assert(lastEmitted != SkipVKOutput(2))) // Reaching Skip via 2f+1 threshold with one prevote and one precommit from two validators at a future round run skipQuorumMixedVotesTwoValsTest = initWith(1, 80) - .then(applyVoteAction({typ: "Prevote", height: 1, round: 1, value: "val1", address: "alice"}, 20, 1)) - .then(_assert(lastEmitted == noOutput(1))) - .then(applyVoteAction({typ: "Prevote", height: 1, round: 2, value: "val1", address: "john"}, 10, 1)) - .then(_assert(lastEmitted == noOutput(2))) - .then(applyVoteAction({typ: "Precommit", height: 1, round: 2, value: "val1", address: "bob"}, 50, 1)) - .then(_assert(lastEmitted == skipOutput(2))) - - // **************************************************************************** - // Properties that define an expected final state (for generating traces) - // **************************************************************************** - - val emitPrecommitValueState = lastEmitted.name == "PrecommitValue" - val emitPrecommitValue = not(emitPrecommitValueState) - - val emitPolkaAnyState = lastEmitted.name == "PolkaAny" - val emitPolkaAny = not(emitPolkaAnyState) - - val emitPolkaNilState = lastEmitted.name == "PolkaNil" - val emitPolkaNil = not(emitPolkaNilState) + .then(applyVoteAction(mkVote(Prevote, "alice", 1, 1, Val("v1")), 20, 1)) + .then(_assert(lastEmitted == NoVKOutput)) + .then(applyVoteAction(mkVote(Prevote, "john", 1, 2, Val("v1")), 10, 1)) + .then(_assert(lastEmitted == NoVKOutput)) + .then(applyVoteAction(mkVote(Precommit, "bob", 1, 2, Val("v1")), 50, 1)) + .then(_assert(lastEmitted == SkipVKOutput(2))) - val emitSkipState = lastEmitted.name == "Skip" - val emitSkip = not(emitSkipState) } From a6afbb91764830ee270c5f821d9e46bc6c92d1fc Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Fri, 8 Dec 2023 13:27:56 +0100 Subject: [PATCH 7/9] code: Address some TODOs --- Code/common/src/validator_set.rs | 2 -- Code/driver/src/driver.rs | 6 ++---- Code/test/src/utils.rs | 5 ----- Code/test/src/validator_set.rs | 3 +-- Code/test/tests/driver.rs | 26 +++++++++++++------------- Code/test/tests/driver_extra.rs | 19 +++++++++---------- 6 files changed, 25 insertions(+), 36 deletions(-) diff --git a/Code/common/src/validator_set.rs b/Code/common/src/validator_set.rs index b0288c033..317085c44 100644 --- a/Code/common/src/validator_set.rs +++ b/Code/common/src/validator_set.rs @@ -3,8 +3,6 @@ use core::fmt::{Debug, Display}; use crate::{Context, PublicKey}; /// Voting power held by a validator. -/// -/// TODO: Do we need to abstract over this as well? pub type VotingPower = u64; /// Defines the requirements for an address. diff --git a/Code/driver/src/driver.rs b/Code/driver/src/driver.rs index c6c960fff..8802b101f 100644 --- a/Code/driver/src/driver.rs +++ b/Code/driver/src/driver.rs @@ -45,11 +45,9 @@ where proposer_selector: impl ProposerSelector + 'static, validator_set: Ctx::ValidatorSet, address: Ctx::Address, + threshold_params: ThresholdParams, ) -> Self { - let votes = VoteKeeper::new( - validator_set.total_voting_power(), - ThresholdParams::default(), // TODO: Make this configurable - ); + let votes = VoteKeeper::new(validator_set.total_voting_power(), threshold_params); Self { ctx, diff --git a/Code/test/src/utils.rs b/Code/test/src/utils.rs index a77367496..ad8eb6805 100644 --- a/Code/test/src/utils.rs +++ b/Code/test/src/utils.rs @@ -192,11 +192,6 @@ pub fn propose_state_with_proposal_and_valid( valid_round: Round, proposal: Proposal, ) -> State { - // TODO - set_valid doesn't work because the valid round is set to state round - // we need to set it to something different. - // propose_state(round) - // .set_proposal(proposal.clone()) - // .set_valid(proposal.value) State { height: Height::new(1), round: state_round, diff --git a/Code/test/src/validator_set.rs b/Code/test/src/validator_set.rs index 35b8462f5..122ca78b4 100644 --- a/Code/test/src/validator_set.rs +++ b/Code/test/src/validator_set.rs @@ -87,7 +87,6 @@ impl ValidatorSet { /// The total voting power of the validator set pub fn total_voting_power(&self) -> VotingPower { - // TODO: Cache this? self.validators.iter().map(|v| v.voting_power).sum() } @@ -115,7 +114,7 @@ impl ValidatorSet { pub fn remove(&mut self, address: &Address) { self.validators.retain(|v| &v.address != address); - Self::sort_validators(&mut self.validators); // TODO: Not needed + Self::sort_validators(&mut self.validators); } /// Get a validator by its address diff --git a/Code/test/tests/driver.rs b/Code/test/tests/driver.rs index 69cf3cb7e..2d5e017df 100644 --- a/Code/test/tests/driver.rs +++ b/Code/test/tests/driver.rs @@ -37,7 +37,7 @@ fn driver_steps_proposer() { let sel = FixedProposer::new(my_addr); let vs = ValidatorSet::new(vec![v1, v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); let proposal = Proposal::new(Height::new(1), Round::new(0), value, Round::new(-1)); @@ -218,7 +218,7 @@ fn driver_steps_proposer_timeout_get_value() { let sel = FixedProposer::new(my_addr); let vs = ValidatorSet::new(vec![v1, v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); let steps = vec![ TestStep { @@ -270,7 +270,7 @@ fn driver_steps_not_proposer_valid() { let sel = FixedProposer::new(v1.address); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); let proposal = Proposal::new(Height::new(1), Round::new(0), value, Round::new(-1)); @@ -439,7 +439,7 @@ fn driver_steps_not_proposer_invalid() { let sel = FixedProposer::new(v1.address); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); let proposal = Proposal::new(Height::new(1), Round::new(0), value, Round::new(-1)); @@ -548,7 +548,7 @@ fn driver_steps_not_proposer_other_height() { let sel = FixedProposer::new(v1.address); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); // Proposal is for another height let proposal = Proposal::new(Height::new(2), Round::new(0), value, Round::new(-1)); @@ -598,7 +598,7 @@ fn driver_steps_not_proposer_other_round() { let sel = FixedProposer::new(v1.address); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); // Proposal is for another round let proposal = Proposal::new(Height::new(1), Round::new(1), value, Round::new(-1)); @@ -648,7 +648,7 @@ fn driver_steps_not_proposer_timeout_multiple_rounds() { let sel = FixedProposer::new(v1.address); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); let steps = vec![ // Start round 0, we, v3, are not the proposer @@ -820,7 +820,7 @@ fn driver_steps_no_value_to_propose() { let sel = FixedProposer::new(v1.address); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); let mut outputs = block_on(driver.process(Input::NewRound(Height::new(1), Round::new(0)))) .expect("execute succeeded"); @@ -848,7 +848,7 @@ fn driver_steps_proposer_not_found() { let sel = FixedProposer::new(v1.address); let vs = ValidatorSet::new(vec![v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); let output = block_on(driver.process(Input::NewRound(Height::new(1), Round::new(0)))); assert_eq!(output, Err(Error::ProposerNotFound(v1.address))); @@ -868,7 +868,7 @@ fn driver_steps_validator_not_found() { // We omit v2 from the validator set let vs = ValidatorSet::new(vec![v1.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); // Start new height block_on(driver.process(Input::NewRound(Height::new(1), Round::new(0)))) @@ -894,7 +894,7 @@ fn driver_steps_invalid_signature() { let sel = FixedProposer::new(v1.address); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); // Start new round block_on(driver.process(Input::NewRound(Height::new(1), Round::new(0)))) @@ -924,7 +924,7 @@ fn driver_steps_skip_round_skip_threshold() { let height = Height::new(1); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); let steps = vec![ // Start round 0, we, v3, are not the proposer @@ -1023,7 +1023,7 @@ fn driver_steps_skip_round_quorum_threshold() { let height = Height::new(1); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); let steps = vec![ // Start round 0, we, v3, are not the proposer diff --git a/Code/test/tests/driver_extra.rs b/Code/test/tests/driver_extra.rs index afcfe841f..d49840909 100644 --- a/Code/test/tests/driver_extra.rs +++ b/Code/test/tests/driver_extra.rs @@ -35,7 +35,6 @@ use malachite_test::utils::*; // - L34 with previously received polkaAny and entering prevote (due to received poposal) // `driver_steps_polka_any_then_proposal_other()` -// TODO - move all below to utils? struct TestStep { desc: &'static str, input: Input, @@ -79,7 +78,7 @@ fn driver_steps_decide_current_with_no_locked_no_valid() { let sel = RotateProposer; let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); let steps = vec![ TestStep { @@ -151,7 +150,7 @@ fn driver_steps_decide_previous_with_no_locked_no_valid() { let sel = RotateProposer; let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); let steps = vec![ TestStep { @@ -251,7 +250,7 @@ fn driver_steps_decide_previous_with_locked_and_valid() { let sel = RotateProposer; let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); let steps = vec![ TestStep { @@ -371,7 +370,7 @@ fn driver_steps_polka_previous_with_locked() { let sel = RotateProposer; let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); let steps = vec![ TestStep { @@ -478,7 +477,7 @@ fn driver_steps_polka_previous_invalid_proposal() { let sel = RotateProposer; let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); let steps = vec![ TestStep { @@ -576,7 +575,7 @@ fn driver_steps_polka_previous_with_no_locked() { let sel = RotateProposer; let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); let steps = vec![ TestStep { @@ -684,7 +683,7 @@ fn driver_steps_polka_nil_and_timout_propose() { let sel = RotateProposer; let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); let steps = vec![ TestStep { @@ -744,7 +743,7 @@ fn driver_steps_polka_value_then_proposal() { let sel = RotateProposer; let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); let steps = vec![ TestStep { @@ -807,7 +806,7 @@ fn driver_steps_polka_any_then_proposal_other() { let sel = RotateProposer; let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr); + let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); let steps = vec![ TestStep { From 99e285cded0bdb2a17f0a3f05ced40b545fa7ac6 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Fri, 8 Dec 2023 15:41:19 +0100 Subject: [PATCH 8/9] Replace `Option` with bespoke `NilOrVal` type to better express intent (#114) --- Code/common/src/context.rs | 6 +- Code/common/src/lib.rs | 2 +- Code/common/src/value.rs | 45 +++++++ Code/common/src/vote.rs | 6 +- Code/itf/tests/votekeeper/runner.rs | 6 +- Code/itf/tests/votekeeper/utils.rs | 11 +- Code/round/src/output.rs | 6 +- Code/round/src/state_machine.rs | 27 ++-- Code/test/src/context.rs | 5 +- Code/test/src/utils.rs | 28 ++-- Code/test/src/vote.rs | 19 +-- Code/test/tests/driver.rs | 195 +++++++++++++++++++++------- Code/test/tests/round.rs | 9 +- Code/test/tests/round_votes.rs | 18 +-- Code/test/tests/vote_keeper.rs | 32 ++--- Code/vote/src/count.rs | 97 +++++++------- Code/vote/src/keeper.rs | 10 +- Code/vote/src/round_votes.rs | 8 +- 18 files changed, 351 insertions(+), 179 deletions(-) diff --git a/Code/common/src/context.rs b/Code/common/src/context.rs index 565f6b7c4..ede49af46 100644 --- a/Code/common/src/context.rs +++ b/Code/common/src/context.rs @@ -1,5 +1,5 @@ use crate::{ - Address, Height, Proposal, PublicKey, Round, SignedVote, SigningScheme, Validator, + Address, Height, NilOrVal, Proposal, PublicKey, Round, SignedVote, SigningScheme, Validator, ValidatorSet, Value, ValueId, Vote, }; @@ -42,7 +42,7 @@ where fn new_prevote( height: Self::Height, round: Round, - value_id: Option>, + value_id: NilOrVal>, address: Self::Address, ) -> Self::Vote; @@ -51,7 +51,7 @@ where fn new_precommit( height: Self::Height, round: Round, - value_id: Option>, + value_id: NilOrVal>, address: Self::Address, ) -> Self::Vote; } diff --git a/Code/common/src/lib.rs b/Code/common/src/lib.rs index bc0a00429..d9a7b3403 100644 --- a/Code/common/src/lib.rs +++ b/Code/common/src/lib.rs @@ -40,5 +40,5 @@ pub use signed_vote::SignedVote; pub use signing::SigningScheme; pub use timeout::{Timeout, TimeoutStep}; pub use validator_set::{Address, Validator, ValidatorSet, VotingPower}; -pub use value::Value; +pub use value::{NilOrVal, Value}; pub use vote::{Vote, VoteType}; diff --git a/Code/common/src/value.rs b/Code/common/src/value.rs index f1a6a656e..e17062ba3 100644 --- a/Code/common/src/value.rs +++ b/Code/common/src/value.rs @@ -1,5 +1,50 @@ use core::fmt::Debug; +/// Represents either `Nil` or a value of type `Value`. +/// +/// This type is isomorphic to `Option` but is more explicit about its intent. +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub enum NilOrVal { + #[default] + Nil, + + Val(Value), +} + +impl NilOrVal { + pub fn is_nil(&self) -> bool { + matches!(self, Self::Nil) + } + + pub fn is_val(&self) -> bool { + matches!(self, Self::Val(_)) + } + + pub fn map NewValue>(self, f: F) -> NilOrVal { + match self { + NilOrVal::Nil => NilOrVal::Nil, + NilOrVal::Val(value) => NilOrVal::Val(f(value)), + } + } + + pub fn as_ref(&self) -> NilOrVal<&Value> { + match self { + NilOrVal::Nil => NilOrVal::Nil, + NilOrVal::Val(value) => NilOrVal::Val(value), + } + } + + pub fn value_or_default(self) -> Value + where + Value: Default, + { + match self { + NilOrVal::Nil => Value::default(), + NilOrVal::Val(value) => value, + } + } +} + /// Defines the requirements for the type of value to decide on. pub trait Value where diff --git a/Code/common/src/vote.rs b/Code/common/src/vote.rs index 84bd6969b..572beea45 100644 --- a/Code/common/src/vote.rs +++ b/Code/common/src/vote.rs @@ -1,6 +1,6 @@ use core::fmt::Debug; -use crate::{Context, Round, Value}; +use crate::{Context, NilOrVal, Round, Value}; /// A type of vote. #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -28,10 +28,10 @@ where fn round(&self) -> Round; /// Get a reference to the value being voted for. - fn value(&self) -> &Option<::Id>; + fn value(&self) -> &NilOrVal<::Id>; /// Take ownership of the value being voted for. - fn take_value(self) -> Option<::Id>; + fn take_value(self) -> NilOrVal<::Id>; /// The type of vote. fn vote_type(&self) -> VoteType; diff --git a/Code/itf/tests/votekeeper/runner.rs b/Code/itf/tests/votekeeper/runner.rs index fbc9633b2..c21eaa906 100644 --- a/Code/itf/tests/votekeeper/runner.rs +++ b/Code/itf/tests/votekeeper/runner.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use malachite_common::{Context, Round, Value}; +use malachite_common::{Context, NilOrVal, Round, Value}; use malachite_itf::types::{Value as ModelValue, VoteType}; use malachite_itf::votekeeper::VoteKeeperOutput::*; use malachite_itf::votekeeper::{State, WeightedVote}; @@ -82,7 +82,7 @@ impl ItfRunner for VoteKeeperRunner { (Output::PolkaAny, PolkaAny(_expected_round)) => (), (Output::PolkaValue(value), PolkaValue(_expected_round, expected_value)) => { assert_eq!( - Some(value), + NilOrVal::Val(value), value_from_model(&ModelValue::Val(expected_value.to_string())).as_ref() ); } @@ -92,7 +92,7 @@ impl ItfRunner for VoteKeeperRunner { PrecommitValue(_expected_round, expected_value), ) => { assert_eq!( - Some(value), + NilOrVal::Val(value), value_from_model(&ModelValue::Val(expected_value.to_string())).as_ref() ); } diff --git a/Code/itf/tests/votekeeper/utils.rs b/Code/itf/tests/votekeeper/utils.rs index 1a75fbe44..17fff6215 100644 --- a/Code/itf/tests/votekeeper/utils.rs +++ b/Code/itf/tests/votekeeper/utils.rs @@ -1,17 +1,18 @@ use std::collections::HashMap; +use malachite_common::NilOrVal; use malachite_itf::types::Value; use malachite_test::{Address, ValueId}; pub const ADDRESSES: [&str; 3] = ["alice", "bob", "john"]; -pub fn value_from_model(value: &Value) -> Option { +pub fn value_from_model(value: &Value) -> NilOrVal { match value { - Value::Nil => None, + Value::Nil => NilOrVal::Nil, Value::Val(v) => match v.as_str() { - "v1" => Some(1.into()), - "v2" => Some(2.into()), - "v3" => Some(3.into()), + "v1" => NilOrVal::Val(1.into()), + "v2" => NilOrVal::Val(2.into()), + "v3" => NilOrVal::Val(3.into()), _ => unimplemented!("unknown value {value:?}"), }, } diff --git a/Code/round/src/output.rs b/Code/round/src/output.rs index a7f586015..c79feb2e3 100644 --- a/Code/round/src/output.rs +++ b/Code/round/src/output.rs @@ -1,6 +1,6 @@ use core::fmt; -use malachite_common::{Context, Round, Timeout, TimeoutStep, ValueId}; +use malachite_common::{Context, NilOrVal, Round, Timeout, TimeoutStep, ValueId}; use crate::state::RoundValue; @@ -29,7 +29,7 @@ impl Output { pub fn prevote( height: Ctx::Height, round: Round, - value_id: Option>, + value_id: NilOrVal>, address: Ctx::Address, ) -> Self { Output::Vote(Ctx::new_prevote(height, round, value_id, address)) @@ -38,7 +38,7 @@ impl Output { pub fn precommit( height: Ctx::Height, round: Round, - value_id: Option>, + value_id: NilOrVal>, address: Ctx::Address, ) -> Self { Output::Vote(Ctx::new_precommit(height, round, value_id, address)) diff --git a/Code/round/src/state_machine.rs b/Code/round/src/state_machine.rs index 82fbd1ded..09d42aeed 100644 --- a/Code/round/src/state_machine.rs +++ b/Code/round/src/state_machine.rs @@ -1,4 +1,4 @@ -use malachite_common::{Context, Proposal, Round, TimeoutStep, Value}; +use malachite_common::{Context, NilOrVal, Proposal, Round, TimeoutStep, Value}; use crate::input::Input; use crate::output::Output; @@ -259,10 +259,10 @@ where let vr = proposal.round(); let proposed = proposal.value().id(); let value = match &state.locked { - Some(locked) if locked.round <= vr => Some(proposed), // unlock and prevote - Some(locked) if locked.value.id() == proposed => Some(proposed), // already locked on value - Some(_) => None, // we're locked on a higher round with a different value, prevote nil - None => Some(proposed), // not locked, prevote the value + Some(locked) if locked.round <= vr => NilOrVal::Val(proposed), // unlock and prevote + Some(locked) if locked.value.id() == proposed => NilOrVal::Val(proposed), // already locked on value + Some(_) => NilOrVal::Nil, // we're locked on a higher round with a different value, prevote nil + None => NilOrVal::Val(proposed), // not locked, prevote the value }; let output = Output::prevote(state.height.clone(), state.round, value, address.clone()); @@ -276,7 +276,13 @@ pub fn prevote_nil(state: State, address: &Ctx::Address) -> Transition where Ctx: Context, { - let output = Output::prevote(state.height.clone(), state.round, None, address.clone()); + let output = Output::prevote( + state.height.clone(), + state.round, + NilOrVal::Nil, + address.clone(), + ); + Transition::to(state.with_step(Step::Prevote)).with_output(output) } @@ -306,7 +312,7 @@ where let output = Output::precommit( state.height.clone(), state.round, - Some(value.id()), + NilOrVal::Val(value.id()), address.clone(), ); @@ -325,7 +331,12 @@ pub fn precommit_nil(state: State, address: &Ctx::Address) -> Transiti where Ctx: Context, { - let output = Output::precommit(state.height.clone(), state.round, None, address.clone()); + let output = Output::precommit( + state.height.clone(), + state.round, + NilOrVal::Nil, + address.clone(), + ); Transition::to(state.with_step(Step::Precommit)).with_output(output) } diff --git a/Code/test/src/context.rs b/Code/test/src/context.rs index 28ff263be..9bfbdca95 100644 --- a/Code/test/src/context.rs +++ b/Code/test/src/context.rs @@ -1,4 +1,5 @@ use malachite_common::Context; +use malachite_common::NilOrVal; use malachite_common::Round; use malachite_common::SignedVote; @@ -50,7 +51,7 @@ impl Context for TestContext { fn new_prevote( height: Height, round: Round, - value_id: Option, + value_id: NilOrVal, address: Address, ) -> Vote { Vote::new_prevote(height, round, value_id, address) @@ -59,7 +60,7 @@ impl Context for TestContext { fn new_precommit( height: Height, round: Round, - value_id: Option, + value_id: NilOrVal, address: Address, ) -> Vote { Vote::new_precommit(height, round, value_id, address) diff --git a/Code/test/src/utils.rs b/Code/test/src/utils.rs index ad8eb6805..fc962cf44 100644 --- a/Code/test/src/utils.rs +++ b/Code/test/src/utils.rs @@ -1,7 +1,7 @@ use rand::rngs::StdRng; use rand::SeedableRng; -use malachite_common::{Round, Timeout, VotingPower}; +use malachite_common::{NilOrVal, Round, Timeout, VotingPower}; use malachite_driver::{Input, Output, ProposerSelector, Validity}; use malachite_round::state::{RoundValue, State, Step}; @@ -87,7 +87,7 @@ pub fn prevote_output( let value = Value::new(9999); Some(Output::Vote( - Vote::new_prevote(Height::new(1), round, Some(value.id()), *addr).signed(sk), + Vote::new_prevote(Height::new(1), round, NilOrVal::Val(value.id()), *addr).signed(sk), )) } @@ -97,7 +97,7 @@ pub fn prevote_nil_output( sk: &PrivateKey, ) -> Option> { Some(Output::Vote( - Vote::new_prevote(Height::new(1), round, None, *addr).signed(sk), + Vote::new_prevote(Height::new(1), round, NilOrVal::Nil, *addr).signed(sk), )) } @@ -105,18 +105,26 @@ pub fn prevote_input(addr: &Address, sk: &PrivateKey) -> Input { let value = Value::new(9999); Input::Vote( - Vote::new_prevote(Height::new(1), Round::new(0), Some(value.id()), *addr).signed(sk), + Vote::new_prevote( + Height::new(1), + Round::new(0), + NilOrVal::Val(value.id()), + *addr, + ) + .signed(sk), ) } pub fn prevote_nil_input(addr: &Address, sk: &PrivateKey) -> Input { - Input::Vote(Vote::new_prevote(Height::new(1), Round::new(0), None, *addr).signed(sk)) + Input::Vote(Vote::new_prevote(Height::new(1), Round::new(0), NilOrVal::Nil, *addr).signed(sk)) } pub fn prevote_input_at(round: Round, addr: &Address, sk: &PrivateKey) -> Input { let value = Value::new(9999); - Input::Vote(Vote::new_prevote(Height::new(1), round, Some(value.id()), *addr).signed(sk)) + Input::Vote( + Vote::new_prevote(Height::new(1), round, NilOrVal::Val(value.id()), *addr).signed(sk), + ) } pub fn precommit_output( @@ -126,7 +134,7 @@ pub fn precommit_output( sk: &PrivateKey, ) -> Option> { Some(Output::Vote( - Vote::new_precommit(Height::new(1), round, Some(value.id()), *addr).signed(sk), + Vote::new_precommit(Height::new(1), round, NilOrVal::Val(value.id()), *addr).signed(sk), )) } @@ -136,7 +144,7 @@ pub fn precommit_nil_output( sk: &PrivateKey, ) -> Option> { Some(Output::Vote( - Vote::new_precommit(Height::new(1), round, None, *addr).signed(sk), + Vote::new_precommit(Height::new(1), round, NilOrVal::Nil, *addr).signed(sk), )) } @@ -146,7 +154,9 @@ pub fn precommit_input( addr: &Address, sk: &PrivateKey, ) -> Input { - Input::Vote(Vote::new_precommit(Height::new(1), round, Some(value.id()), *addr).signed(sk)) + Input::Vote( + Vote::new_precommit(Height::new(1), round, NilOrVal::Val(value.id()), *addr).signed(sk), + ) } pub fn decide_output(round: Round, value: Value) -> Option> { diff --git a/Code/test/src/vote.rs b/Code/test/src/vote.rs index 35ae1117f..dc2ee249b 100644 --- a/Code/test/src/vote.rs +++ b/Code/test/src/vote.rs @@ -1,6 +1,6 @@ use signature::Signer; -use malachite_common::{Round, SignedVote, VoteType}; +use malachite_common::{NilOrVal, Round, SignedVote, VoteType}; use crate::{Address, Height, PrivateKey, TestContext, ValueId}; @@ -10,7 +10,7 @@ pub struct Vote { pub typ: VoteType, pub height: Height, pub round: Round, - pub value: Option, + pub value: NilOrVal, pub validator_address: Address, } @@ -18,7 +18,7 @@ impl Vote { pub fn new_prevote( height: Height, round: Round, - value: Option, + value: NilOrVal, validator_address: Address, ) -> Self { Self { @@ -33,7 +33,7 @@ impl Vote { pub fn new_precommit( height: Height, round: Round, - value: Option, + value: NilOrVal, address: Address, ) -> Self { Self { @@ -55,10 +55,11 @@ impl Vote { let mut bytes = vec![vtpe]; bytes.extend_from_slice(&self.round.as_i64().to_be_bytes()); bytes.extend_from_slice( - &self - .value + self.value + .as_ref() .map(|v| v.as_u64().to_be_bytes()) - .unwrap_or_default(), + .value_or_default() + .as_slice(), ); bytes } @@ -82,11 +83,11 @@ impl malachite_common::Vote for Vote { self.round } - fn value(&self) -> &Option { + fn value(&self) -> &NilOrVal { &self.value } - fn take_value(self) -> Option { + fn take_value(self) -> NilOrVal { self.value } diff --git a/Code/test/tests/driver.rs b/Code/test/tests/driver.rs index 2d5e017df..4ac4749ca 100644 --- a/Code/test/tests/driver.rs +++ b/Code/test/tests/driver.rs @@ -1,7 +1,7 @@ use futures::executor::block_on; use malachite_test::utils::{make_validators, FixedProposer, RotateProposer}; -use malachite_common::{Round, Timeout, TimeoutStep}; +use malachite_common::{NilOrVal, Round, Timeout, TimeoutStep}; use malachite_driver::{Driver, Error, Input, Output, Validity}; use malachite_round::state::{RoundValue, State, Step}; use malachite_test::{Height, Proposal, TestContext, ValidatorSet, Value, Vote}; @@ -75,8 +75,13 @@ fn driver_steps_proposer() { desc: "Receive our own proposal, prevote for it (v1)", input: None, expected_output: Some(Output::Vote( - Vote::new_prevote(Height::new(1), Round::new(0), Some(value.id()), my_addr) - .signed(&my_sk), + Vote::new_prevote( + Height::new(1), + Round::new(0), + NilOrVal::Val(value.id()), + my_addr, + ) + .signed(&my_sk), )), expected_round: Round::new(0), new_state: State { @@ -103,8 +108,13 @@ fn driver_steps_proposer() { TestStep { desc: "v2 prevotes for our proposal", input: Some(Input::Vote( - Vote::new_prevote(Height::new(1), Round::new(0), Some(value.id()), v2.address) - .signed(&sk2), + Vote::new_prevote( + Height::new(1), + Round::new(0), + NilOrVal::Val(value.id()), + v2.address, + ) + .signed(&sk2), )), expected_output: None, expected_round: Round::new(0), @@ -119,12 +129,22 @@ fn driver_steps_proposer() { TestStep { desc: "v3 prevotes for our proposal, we get +2/3 prevotes, precommit for it (v1)", input: Some(Input::Vote( - Vote::new_prevote(Height::new(1), Round::new(0), Some(value.id()), v3.address) - .signed(&sk3), + Vote::new_prevote( + Height::new(1), + Round::new(0), + NilOrVal::Val(value.id()), + v3.address, + ) + .signed(&sk3), )), expected_output: Some(Output::Vote( - Vote::new_precommit(Height::new(1), Round::new(0), Some(value.id()), my_addr) - .signed(&my_sk), + Vote::new_precommit( + Height::new(1), + Round::new(0), + NilOrVal::Val(value.id()), + my_addr, + ) + .signed(&my_sk), )), expected_round: Round::new(0), new_state: State { @@ -163,8 +183,13 @@ fn driver_steps_proposer() { TestStep { desc: "v2 precommits for our proposal", input: Some(Input::Vote( - Vote::new_precommit(Height::new(1), Round::new(0), Some(value.id()), v2.address) - .signed(&sk2), + Vote::new_precommit( + Height::new(1), + Round::new(0), + NilOrVal::Val(value.id()), + v2.address, + ) + .signed(&sk2), )), expected_output: None, expected_round: Round::new(0), @@ -185,8 +210,13 @@ fn driver_steps_proposer() { TestStep { desc: "v3 precommits for our proposal, we get +2/3 precommits, decide it (v1)", input: Some(Input::Vote( - Vote::new_precommit(Height::new(1), Round::new(0), Some(value.id()), v3.address) - .signed(&sk3), + Vote::new_precommit( + Height::new(1), + Round::new(0), + NilOrVal::Val(value.id()), + v3.address, + ) + .signed(&sk3), )), expected_output: Some(Output::Decide(Round::new(0), value)), expected_round: Round::new(0), @@ -241,7 +271,8 @@ fn driver_steps_proposer_timeout_get_value() { desc: "Receive a propose timeout", input: Some(Input::TimeoutElapsed(Timeout::propose(Round::new(0)))), expected_output: Some(Output::Vote( - Vote::new_prevote(Height::new(1), Round::new(0), None, my_addr).signed(&my_sk), + Vote::new_prevote(Height::new(1), Round::new(0), NilOrVal::Nil, my_addr) + .signed(&my_sk), )), expected_round: Round::new(0), new_state: State { @@ -292,8 +323,13 @@ fn driver_steps_not_proposer_valid() { desc: "Receive a proposal, prevote for it (v2)", input: Some(Input::Proposal(proposal.clone(), Validity::Valid)), expected_output: Some(Output::Vote( - Vote::new_prevote(Height::new(1), Round::new(0), Some(value.id()), my_addr) - .signed(&my_sk), + Vote::new_prevote( + Height::new(1), + Round::new(0), + NilOrVal::Val(value.id()), + my_addr, + ) + .signed(&my_sk), )), expected_round: Round::new(0), new_state: State { @@ -320,8 +356,13 @@ fn driver_steps_not_proposer_valid() { TestStep { desc: "v1 prevotes for its own proposal", input: Some(Input::Vote( - Vote::new_prevote(Height::new(1), Round::new(0), Some(value.id()), v1.address) - .signed(&sk1), + Vote::new_prevote( + Height::new(1), + Round::new(0), + NilOrVal::Val(value.id()), + v1.address, + ) + .signed(&sk1), )), expected_output: None, expected_round: Round::new(0), @@ -336,12 +377,22 @@ fn driver_steps_not_proposer_valid() { TestStep { desc: "v3 prevotes for v1's proposal, it gets +2/3 prevotes, precommit for it (v2)", input: Some(Input::Vote( - Vote::new_prevote(Height::new(1), Round::new(0), Some(value.id()), v3.address) - .signed(&sk3), + Vote::new_prevote( + Height::new(1), + Round::new(0), + NilOrVal::Val(value.id()), + v3.address, + ) + .signed(&sk3), )), expected_output: Some(Output::Vote( - Vote::new_precommit(Height::new(1), Round::new(0), Some(value.id()), my_addr) - .signed(&my_sk), + Vote::new_precommit( + Height::new(1), + Round::new(0), + NilOrVal::Val(value.id()), + my_addr, + ) + .signed(&my_sk), )), expected_round: Round::new(0), new_state: State { @@ -380,8 +431,13 @@ fn driver_steps_not_proposer_valid() { TestStep { desc: "v1 precommits its proposal", input: Some(Input::Vote( - Vote::new_precommit(Height::new(1), Round::new(0), Some(value.id()), v1.address) - .signed(&sk1), + Vote::new_precommit( + Height::new(1), + Round::new(0), + NilOrVal::Val(value.id()), + v1.address, + ) + .signed(&sk1), )), expected_output: None, expected_round: Round::new(0), @@ -402,8 +458,13 @@ fn driver_steps_not_proposer_valid() { TestStep { desc: "v3 precommits for v1's proposal, it gets +2/3 precommits, decide it", input: Some(Input::Vote( - Vote::new_precommit(Height::new(1), Round::new(0), Some(value.id()), v3.address) - .signed(&sk3), + Vote::new_precommit( + Height::new(1), + Round::new(0), + NilOrVal::Val(value.id()), + v3.address, + ) + .signed(&sk3), )), expected_output: Some(Output::Decide(Round::new(0), value)), expected_round: Round::new(0), @@ -461,7 +522,7 @@ fn driver_steps_not_proposer_invalid() { desc: "Receive an invalid proposal, prevote for nil (v2)", input: Some(Input::Proposal(proposal.clone(), Validity::Invalid)), expected_output: Some(Output::Vote( - Vote::new_prevote(Height::new(1),Round::new(0), None, my_addr).signed(&my_sk), + Vote::new_prevote(Height::new(1),Round::new(0), NilOrVal::Nil, my_addr).signed(&my_sk), )), expected_round: Round::new(0), new_state: State { @@ -488,7 +549,7 @@ fn driver_steps_not_proposer_invalid() { TestStep { desc: "v1 prevotes for its own proposal", input: Some(Input::Vote( - Vote::new_prevote(Height::new(1), Round::new(0), Some(value.id()), v1.address).signed(&sk1), + Vote::new_prevote(Height::new(1), Round::new(0), NilOrVal::Val(value.id()), v1.address).signed(&sk1), )), expected_output: None, expected_round: Round::new(0), @@ -503,7 +564,7 @@ fn driver_steps_not_proposer_invalid() { TestStep { desc: "v3 prevotes for v1's proposal, we have polka for any, schedule prevote timeout (v2)", input: Some(Input::Vote( - Vote::new_prevote(Height::new(1), Round::new(0), Some(value.id()), v3.address).signed(&sk3), + Vote::new_prevote(Height::new(1), Round::new(0), NilOrVal::Val(value.id()), v3.address).signed(&sk3), )), expected_output: Some(Output::ScheduleTimeout(Timeout::prevote(Round::new(0)))), expected_round: Round::new(0), @@ -519,7 +580,7 @@ fn driver_steps_not_proposer_invalid() { desc: "prevote timeout elapses, we precommit for nil (v2)", input: Some(Input::TimeoutElapsed(Timeout::prevote(Round::new(0)))), expected_output: Some(Output::Vote( - Vote::new_precommit(Height::new(1), Round::new(0), None, my_addr).signed(&my_sk), + Vote::new_precommit(Height::new(1), Round::new(0), NilOrVal::Nil, my_addr).signed(&my_sk), )), expected_round: Round::new(0), new_state: State { @@ -670,7 +731,8 @@ fn driver_steps_not_proposer_timeout_multiple_rounds() { desc: "Receive a propose timeout, prevote for nil (v3)", input: Some(Input::TimeoutElapsed(Timeout::propose(Round::new(0)))), expected_output: Some(Output::Vote( - Vote::new_prevote(Height::new(1), Round::new(0), None, my_addr).signed(&my_sk), + Vote::new_prevote(Height::new(1), Round::new(0), NilOrVal::Nil, my_addr) + .signed(&my_sk), )), expected_round: Round::new(0), new_state: State { @@ -699,8 +761,13 @@ fn driver_steps_not_proposer_timeout_multiple_rounds() { TestStep { desc: "v1 prevotes for its own proposal", input: Some(Input::Vote( - Vote::new_prevote(Height::new(1), Round::new(0), Some(value.id()), v1.address) - .signed(&sk1), + Vote::new_prevote( + Height::new(1), + Round::new(0), + NilOrVal::Val(value.id()), + v1.address, + ) + .signed(&sk1), )), expected_output: None, expected_round: Round::new(0), @@ -716,10 +783,12 @@ fn driver_steps_not_proposer_timeout_multiple_rounds() { TestStep { desc: "v2 prevotes for nil, we get +2/3 prevotes, precommit for nil", input: Some(Input::Vote( - Vote::new_prevote(Height::new(1), Round::new(0), None, v2.address).signed(&sk2), + Vote::new_prevote(Height::new(1), Round::new(0), NilOrVal::Nil, v2.address) + .signed(&sk2), )), expected_output: Some(Output::Vote( - Vote::new_precommit(Height::new(1), Round::new(0), None, my_addr).signed(&my_sk), + Vote::new_precommit(Height::new(1), Round::new(0), NilOrVal::Nil, my_addr) + .signed(&my_sk), )), expected_round: Round::new(0), new_state: State { @@ -748,8 +817,13 @@ fn driver_steps_not_proposer_timeout_multiple_rounds() { TestStep { desc: "v1 precommits its proposal", input: Some(Input::Vote( - Vote::new_precommit(Height::new(1), Round::new(0), Some(value.id()), v1.address) - .signed(&sk1), + Vote::new_precommit( + Height::new(1), + Round::new(0), + NilOrVal::Val(value.id()), + v1.address, + ) + .signed(&sk1), )), expected_output: None, expected_round: Round::new(0), @@ -765,7 +839,8 @@ fn driver_steps_not_proposer_timeout_multiple_rounds() { TestStep { desc: "v2 precommits for nil", input: Some(Input::Vote( - Vote::new_precommit(Height::new(1), Round::new(0), None, v2.address).signed(&sk2), + Vote::new_precommit(Height::new(1), Round::new(0), NilOrVal::Nil, v2.address) + .signed(&sk2), )), expected_output: Some(Output::ScheduleTimeout(Timeout::precommit(Round::new(0)))), expected_round: Round::new(0), @@ -875,9 +950,17 @@ fn driver_steps_validator_not_found() { .expect("execute succeeded"); // v2 prevotes for some proposal, we cannot find it in the validator set => error - let output = block_on(driver.process(Input::Vote( - Vote::new_prevote(Height::new(1), Round::new(0), Some(value.id()), v2.address).signed(&sk2), - ))); + let output = block_on( + driver.process(Input::Vote( + Vote::new_prevote( + Height::new(1), + Round::new(0), + NilOrVal::Val(value.id()), + v2.address, + ) + .signed(&sk2), + )), + ); assert_eq!(output, Err(Error::ValidatorNotFound(v2.address))); } @@ -902,9 +985,17 @@ fn driver_steps_invalid_signature() { // v2 prevotes for some proposal, with an invalid signature, // ie. signed by v1 instead of v2, just a way of forging an invalid signature - let output = block_on(driver.process(Input::Vote( - Vote::new_prevote(Height::new(1), Round::new(0), Some(value.id()), v2.address).signed(&sk1), - ))); + let output = block_on( + driver.process(Input::Vote( + Vote::new_prevote( + Height::new(1), + Round::new(0), + NilOrVal::Val(value.id()), + v2.address, + ) + .signed(&sk1), + )), + ); assert!(matches!(output, Err(Error::InvalidVoteSignature(_, _)))); } @@ -946,7 +1037,7 @@ fn driver_steps_skip_round_skip_threshold() { desc: "Receive a propose timeout, prevote for nil (v3)", input: Some(Input::TimeoutElapsed(Timeout::propose(Round::new(0)))), expected_output: Some(Output::Vote( - Vote::new_prevote(height, Round::new(0), None, my_addr).signed(&my_sk), + Vote::new_prevote(height, Round::new(0), NilOrVal::Nil, my_addr).signed(&my_sk), )), expected_round: Round::new(0), new_state: State { @@ -975,7 +1066,8 @@ fn driver_steps_skip_round_skip_threshold() { TestStep { desc: "v1 prevotes for its own proposal in round 1", input: Some(Input::Vote( - Vote::new_prevote(height, Round::new(1), Some(value.id()), v1.address).signed(&sk1), + Vote::new_prevote(height, Round::new(1), NilOrVal::Val(value.id()), v1.address) + .signed(&sk1), )), expected_output: None, expected_round: Round::new(0), @@ -991,7 +1083,8 @@ fn driver_steps_skip_round_skip_threshold() { TestStep { desc: "v2 prevotes for v1 proposal, we get +1/3 messages from future round", input: Some(Input::Vote( - Vote::new_prevote(height, Round::new(1), Some(value.id()), v2.address).signed(&sk2), + Vote::new_prevote(height, Round::new(1), NilOrVal::Val(value.id()), v2.address) + .signed(&sk2), )), expected_output: Some(Output::NewRound(height, Round::new(1))), expected_round: Round::new(1), @@ -1045,7 +1138,7 @@ fn driver_steps_skip_round_quorum_threshold() { desc: "Receive a propose timeout, prevote for nil (v3)", input: Some(Input::TimeoutElapsed(Timeout::propose(Round::new(0)))), expected_output: Some(Output::Vote( - Vote::new_prevote(height, Round::new(0), None, my_addr).signed(&my_sk), + Vote::new_prevote(height, Round::new(0), NilOrVal::Nil, my_addr).signed(&my_sk), )), expected_round: Round::new(0), new_state: State { @@ -1074,7 +1167,8 @@ fn driver_steps_skip_round_quorum_threshold() { TestStep { desc: "v1 prevotes for its own proposal in round 1", input: Some(Input::Vote( - Vote::new_prevote(height, Round::new(1), Some(value.id()), v1.address).signed(&sk1), + Vote::new_prevote(height, Round::new(1), NilOrVal::Val(value.id()), v1.address) + .signed(&sk1), )), expected_output: None, expected_round: Round::new(0), @@ -1090,7 +1184,8 @@ fn driver_steps_skip_round_quorum_threshold() { TestStep { desc: "v2 prevotes for v1 proposal, we get +1/3 messages from future round", input: Some(Input::Vote( - Vote::new_prevote(height, Round::new(1), Some(value.id()), v2.address).signed(&sk2), + Vote::new_prevote(height, Round::new(1), NilOrVal::Val(value.id()), v2.address) + .signed(&sk2), )), expected_output: Some(Output::NewRound(height, Round::new(1))), expected_round: Round::new(1), diff --git a/Code/test/tests/round.rs b/Code/test/tests/round.rs index 246194a7a..0cc47f9a8 100644 --- a/Code/test/tests/round.rs +++ b/Code/test/tests/round.rs @@ -1,6 +1,6 @@ use malachite_test::{Address, Height, Proposal, TestContext, Value}; -use malachite_common::{Round, Timeout, TimeoutStep}; +use malachite_common::{NilOrVal, Round, Timeout, TimeoutStep}; use malachite_round::input::Input; use malachite_round::output::Output; use malachite_round::state::{State, Step}; @@ -85,6 +85,11 @@ fn test_prevote() { assert_eq!(transition.next_state.step, Step::Prevote); assert_eq!( transition.output.unwrap(), - Output::prevote(Height::new(1), Round::new(1), Some(value.id()), ADDRESS) + Output::prevote( + Height::new(1), + Round::new(1), + NilOrVal::Val(value.id()), + ADDRESS + ) ); } diff --git a/Code/test/tests/round_votes.rs b/Code/test/tests/round_votes.rs index 6132734c9..0a08413f3 100644 --- a/Code/test/tests/round_votes.rs +++ b/Code/test/tests/round_votes.rs @@ -1,4 +1,4 @@ -use malachite_common::VoteType; +use malachite_common::{NilOrVal, VoteType}; use malachite_vote::round_votes::RoundVotes; use malachite_test::{Address, ValueId}; @@ -14,20 +14,20 @@ const ADDRESS6: Address = Address::new([46; 20]); fn add_votes_nil() { let mut round_votes: RoundVotes<_, ValueId> = RoundVotes::new(); - let w = round_votes.add_vote(VoteType::Prevote, ADDRESS1, None, 1); + let w = round_votes.add_vote(VoteType::Prevote, ADDRESS1, NilOrVal::Nil, 1); assert_eq!(w, 1); - let w = round_votes.add_vote(VoteType::Prevote, ADDRESS2, None, 1); + let w = round_votes.add_vote(VoteType::Prevote, ADDRESS2, NilOrVal::Nil, 1); assert_eq!(w, 2); - let w = round_votes.add_vote(VoteType::Prevote, ADDRESS3, None, 1); + let w = round_votes.add_vote(VoteType::Prevote, ADDRESS3, NilOrVal::Nil, 1); assert_eq!(w, 3); } #[test] fn add_votes_single_value() { let v = ValueId::new(1); - let val = Some(v); + let val = NilOrVal::Val(v); let weight = 1; let mut round_votes: RoundVotes<_, ValueId> = RoundVotes::new(); @@ -41,7 +41,7 @@ fn add_votes_single_value() { assert_eq!(w, 2); // add a vote for nil, get w::Any - let w = round_votes.add_vote(VoteType::Prevote, ADDRESS3, None, weight); + let w = round_votes.add_vote(VoteType::Prevote, ADDRESS3, NilOrVal::Nil, weight); assert_eq!(w, 1); // add vote for value, get w::Value @@ -53,8 +53,8 @@ fn add_votes_single_value() { fn add_votes_multi_values() { let v1 = ValueId::new(1); let v2 = ValueId::new(2); - let val1 = Some(v1); - let val2 = Some(v2); + let val1 = NilOrVal::Val(v1); + let val2 = NilOrVal::Val(v2); let mut round_votes: RoundVotes<_, ValueId> = RoundVotes::new(); @@ -64,7 +64,7 @@ fn add_votes_multi_values() { let w = round_votes.add_vote(VoteType::Precommit, ADDRESS2, val2, 1); assert_eq!(w, 1); - let w = round_votes.add_vote(VoteType::Precommit, ADDRESS3, None, 1); + let w = round_votes.add_vote(VoteType::Precommit, ADDRESS3, NilOrVal::Nil, 1); assert_eq!(w, 1); let w = round_votes.add_vote(VoteType::Precommit, ADDRESS4, val1, 1); diff --git a/Code/test/tests/vote_keeper.rs b/Code/test/tests/vote_keeper.rs index f3cd9df3f..64bd8fbca 100644 --- a/Code/test/tests/vote_keeper.rs +++ b/Code/test/tests/vote_keeper.rs @@ -1,4 +1,4 @@ -use malachite_common::Round; +use malachite_common::{NilOrVal, Round}; use malachite_vote::keeper::{Output, VoteKeeper}; use malachite_test::{Address, Height, TestContext, ValueId, Vote}; @@ -14,15 +14,15 @@ fn prevote_apply_nil() { let height = Height::new(1); let round = Round::new(0); - let vote = Vote::new_prevote(height, round, None, ADDRESS1); + let vote = Vote::new_prevote(height, round, NilOrVal::Nil, ADDRESS1); let msg = keeper.apply_vote(vote.clone(), 1, round); assert_eq!(msg, None); - let vote = Vote::new_prevote(height, round, None, ADDRESS2); + let vote = Vote::new_prevote(height, round, NilOrVal::Nil, ADDRESS2); let msg = keeper.apply_vote(vote.clone(), 1, round); assert_eq!(msg, None); - let vote = Vote::new_prevote(height, round, None, ADDRESS3); + let vote = Vote::new_prevote(height, round, NilOrVal::Nil, ADDRESS3); let msg = keeper.apply_vote(vote, 1, round); assert_eq!(msg, Some(Output::PolkaNil)); } @@ -33,15 +33,15 @@ fn precommit_apply_nil() { let height = Height::new(1); let round = Round::new(0); - let vote = Vote::new_precommit(height, round, None, ADDRESS1); + let vote = Vote::new_precommit(height, round, NilOrVal::Nil, ADDRESS1); let msg = keeper.apply_vote(vote.clone(), 1, round); assert_eq!(msg, None); - let vote = Vote::new_precommit(height, Round::new(0), None, ADDRESS2); + let vote = Vote::new_precommit(height, Round::new(0), NilOrVal::Nil, ADDRESS2); let msg = keeper.apply_vote(vote.clone(), 1, round); assert_eq!(msg, None); - let vote = Vote::new_precommit(height, Round::new(0), None, ADDRESS3); + let vote = Vote::new_precommit(height, Round::new(0), NilOrVal::Nil, ADDRESS3); let msg = keeper.apply_vote(vote, 1, round); assert_eq!(msg, Some(Output::PrecommitAny)); } @@ -51,7 +51,7 @@ fn prevote_apply_single_value() { let mut keeper: VoteKeeper = VoteKeeper::new(4, Default::default()); let id = ValueId::new(1); - let val = Some(id); + let val = NilOrVal::Val(id); let height = Height::new(1); let round = Round::new(0); @@ -63,7 +63,7 @@ fn prevote_apply_single_value() { let msg = keeper.apply_vote(vote.clone(), 1, round); assert_eq!(msg, None); - let vote_nil = Vote::new_prevote(height, Round::new(0), None, ADDRESS3); + let vote_nil = Vote::new_prevote(height, Round::new(0), NilOrVal::Nil, ADDRESS3); let msg = keeper.apply_vote(vote_nil, 1, round); assert_eq!(msg, Some(Output::PolkaAny)); @@ -77,7 +77,7 @@ fn precommit_apply_single_value() { let mut keeper: VoteKeeper = VoteKeeper::new(4, Default::default()); let id = ValueId::new(1); - let val = Some(id); + let val = NilOrVal::Val(id); let height = Height::new(1); let round = Round::new(0); @@ -89,7 +89,7 @@ fn precommit_apply_single_value() { let msg = keeper.apply_vote(vote.clone(), 1, round); assert_eq!(msg, None); - let vote_nil = Vote::new_precommit(height, Round::new(0), None, ADDRESS3); + let vote_nil = Vote::new_precommit(height, Round::new(0), NilOrVal::Nil, ADDRESS3); let msg = keeper.apply_vote(vote_nil, 1, round); assert_eq!(msg, Some(Output::PrecommitAny)); @@ -103,7 +103,7 @@ fn skip_round_small_quorum_prevotes_two_vals() { let mut keeper: VoteKeeper = VoteKeeper::new(4, Default::default()); let id = ValueId::new(1); - let val = Some(id); + let val = NilOrVal::Val(id); let height = Height::new(1); let cur_round = Round::new(0); let fut_round = Round::new(1); @@ -126,7 +126,7 @@ fn skip_round_small_quorum_with_prevote_precommit_two_vals() { let mut keeper: VoteKeeper = VoteKeeper::new(4, Default::default()); let id = ValueId::new(1); - let val = Some(id); + let val = NilOrVal::Val(id); let height = Height::new(1); let cur_round = Round::new(0); let fut_round = Round::new(1); @@ -149,7 +149,7 @@ fn skip_round_full_quorum_with_prevote_precommit_two_vals() { let mut keeper: VoteKeeper = VoteKeeper::new(5, Default::default()); let id = ValueId::new(1); - let val = Some(id); + let val = NilOrVal::Val(id); let height = Height::new(1); let cur_round = Round::new(0); let fut_round = Round::new(1); @@ -172,7 +172,7 @@ fn no_skip_round_small_quorum_with_same_val() { let mut keeper: VoteKeeper = VoteKeeper::new(4, Default::default()); let id = ValueId::new(1); - let val = Some(id); + let val = NilOrVal::Val(id); let height = Height::new(1); let cur_round = Round::new(0); let fut_round = Round::new(1); @@ -195,7 +195,7 @@ fn no_skip_round_full_quorum_with_same_val() { let mut keeper: VoteKeeper = VoteKeeper::new(5, Default::default()); let id = ValueId::new(1); - let val = Some(id); + let val = NilOrVal::Val(id); let height = Height::new(1); let cur_round = Round::new(0); let fut_round = Round::new(1); diff --git a/Code/vote/src/count.rs b/Code/vote/src/count.rs index dfb970773..e891c3c28 100644 --- a/Code/vote/src/count.rs +++ b/Code/vote/src/count.rs @@ -1,4 +1,5 @@ use alloc::collections::BTreeSet; +use malachite_common::NilOrVal; use crate::value_weights::ValuesWeights; use crate::{Threshold, ThresholdParam, Weight}; @@ -8,7 +9,7 @@ use crate::{Threshold, ThresholdParam, Weight}; #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct VoteCount { /// Weight of votes for the values, including nil - pub values_weights: ValuesWeights>, + pub values_weights: ValuesWeights>, /// Addresses of validators who voted for the values pub validator_addresses: BTreeSet
, @@ -24,7 +25,7 @@ impl VoteCount { /// Add vote for a value (or nil) to internal counters, but only if we haven't seen /// a vote from that particular validator yet. - pub fn add(&mut self, address: Address, value: Option, weight: Weight) -> Weight + pub fn add(&mut self, address: Address, value: NilOrVal, weight: Weight) -> Weight where Address: Clone + Ord, Value: Clone + Ord, @@ -38,7 +39,7 @@ impl VoteCount { } } - pub fn get(&self, value: &Option) -> Weight + pub fn get(&self, value: &NilOrVal) -> Weight where Value: Ord, { @@ -61,12 +62,12 @@ impl VoteCount { { match threshold { Threshold::Value(value) => { - let weight = self.values_weights.get(&Some(value)); + let weight = self.values_weights.get(&NilOrVal::Val(value)); param.is_met(weight, total_weight) } Threshold::Nil => { - let weight = self.values_weights.get(&None); + let weight = self.values_weights.get(&NilOrVal::Nil); param.is_met(weight, total_weight) } @@ -97,26 +98,26 @@ mod tests { let addr3 = [3]; let addr4 = [4]; - assert_eq!(vc.get(&None), 0); - assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.get(&NilOrVal::Nil), 0); + assert_eq!(vc.get(&NilOrVal::Val(1)), 0); assert_eq!(vc.is_threshold_met(Threshold::Unreached, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Any, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Nil, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Value(1), q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Value(2), q, t), false); - assert_eq!(vc.add(addr1, None, 1), 1); - assert_eq!(vc.get(&None), 1); - assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.add(addr1, NilOrVal::Nil, 1), 1); + assert_eq!(vc.get(&NilOrVal::Nil), 1); + assert_eq!(vc.get(&NilOrVal::Val(1)), 0); assert_eq!(vc.is_threshold_met(Threshold::Unreached, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Any, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Nil, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Value(1), q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Value(2), q, t), false); - assert_eq!(vc.add(addr2, None, 1), 2); - assert_eq!(vc.get(&None), 2); - assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.add(addr2, NilOrVal::Nil, 1), 2); + assert_eq!(vc.get(&NilOrVal::Nil), 2); + assert_eq!(vc.get(&NilOrVal::Val(1)), 0); assert_eq!(vc.is_threshold_met(Threshold::Unreached, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Any, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Nil, q, t), false); @@ -124,27 +125,27 @@ mod tests { assert_eq!(vc.is_threshold_met(Threshold::Value(2), q, t), false); // addr1 votes again, is ignored - assert_eq!(vc.add(addr1, None, 1), 2); - assert_eq!(vc.get(&None), 2); - assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.add(addr1, NilOrVal::Nil, 1), 2); + assert_eq!(vc.get(&NilOrVal::Nil), 2); + assert_eq!(vc.get(&NilOrVal::Val(1)), 0); assert_eq!(vc.is_threshold_met(Threshold::Unreached, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Any, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Nil, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Value(1), q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Value(2), q, t), false); - assert_eq!(vc.add(addr3, None, 1), 3); - assert_eq!(vc.get(&None), 3); - assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.add(addr3, NilOrVal::Nil, 1), 3); + assert_eq!(vc.get(&NilOrVal::Nil), 3); + assert_eq!(vc.get(&NilOrVal::Val(1)), 0); assert_eq!(vc.is_threshold_met(Threshold::Unreached, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Any, q, t), true); assert_eq!(vc.is_threshold_met(Threshold::Nil, q, t), true); assert_eq!(vc.is_threshold_met(Threshold::Value(1), q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Value(2), q, t), false); - assert_eq!(vc.add(addr4, Some(1), 1), 1); - assert_eq!(vc.get(&None), 3); - assert_eq!(vc.get(&Some(1)), 1); + assert_eq!(vc.add(addr4, NilOrVal::Val(1), 1), 1); + assert_eq!(vc.get(&NilOrVal::Nil), 3); + assert_eq!(vc.get(&NilOrVal::Val(1)), 1); assert_eq!(vc.is_threshold_met(Threshold::Unreached, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Any, q, t), true); assert_eq!(vc.is_threshold_met(Threshold::Nil, q, t), true); @@ -164,26 +165,26 @@ mod tests { let addr3 = [3]; let addr4 = [4]; - assert_eq!(vc.get(&None), 0); - assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.get(&NilOrVal::Nil), 0); + assert_eq!(vc.get(&NilOrVal::Val(1)), 0); assert_eq!(vc.is_threshold_met(Threshold::Unreached, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Any, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Nil, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Value(1), q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Value(2), q, t), false); - assert_eq!(vc.add(addr1, Some(1), 1), 1); - assert_eq!(vc.get(&None), 0); - assert_eq!(vc.get(&Some(1)), 1); + assert_eq!(vc.add(addr1, NilOrVal::Val(1), 1), 1); + assert_eq!(vc.get(&NilOrVal::Nil), 0); + assert_eq!(vc.get(&NilOrVal::Val(1)), 1); assert_eq!(vc.is_threshold_met(Threshold::Unreached, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Any, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Nil, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Value(1), q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Value(2), q, t), false); - assert_eq!(vc.add(addr2, Some(1), 1), 2); - assert_eq!(vc.get(&None), 0); - assert_eq!(vc.get(&Some(1)), 2); + assert_eq!(vc.add(addr2, NilOrVal::Val(1), 1), 2); + assert_eq!(vc.get(&NilOrVal::Nil), 0); + assert_eq!(vc.get(&NilOrVal::Val(1)), 2); assert_eq!(vc.is_threshold_met(Threshold::Unreached, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Any, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Nil, q, t), false); @@ -191,18 +192,18 @@ mod tests { assert_eq!(vc.is_threshold_met(Threshold::Value(2), q, t), false); // addr1 votes again, for nil this time, is ignored - assert_eq!(vc.add(addr1, None, 1), 0); - assert_eq!(vc.get(&None), 0); - assert_eq!(vc.get(&Some(1)), 2); + assert_eq!(vc.add(addr1, NilOrVal::Nil, 1), 0); + assert_eq!(vc.get(&NilOrVal::Nil), 0); + assert_eq!(vc.get(&NilOrVal::Val(1)), 2); assert_eq!(vc.is_threshold_met(Threshold::Unreached, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Any, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Nil, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Value(1), q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Value(2), q, t), false); - assert_eq!(vc.add(addr3, Some(1), 1), 3); - assert_eq!(vc.get(&None), 0); - assert_eq!(vc.get(&Some(1)), 3); + assert_eq!(vc.add(addr3, NilOrVal::Val(1), 1), 3); + assert_eq!(vc.get(&NilOrVal::Nil), 0); + assert_eq!(vc.get(&NilOrVal::Val(1)), 3); assert_eq!(vc.is_threshold_met(Threshold::Unreached, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Any, q, t), true); assert_eq!(vc.is_threshold_met(Threshold::Nil, q, t), false); @@ -210,19 +211,19 @@ mod tests { assert_eq!(vc.is_threshold_met(Threshold::Value(2), q, t), false); // addr2 votes again, for the same value, is ignored - assert_eq!(vc.add(addr2, Some(1), 1), 3); - assert_eq!(vc.get(&None), 0); - assert_eq!(vc.get(&Some(1)), 3); + assert_eq!(vc.add(addr2, NilOrVal::Val(1), 1), 3); + assert_eq!(vc.get(&NilOrVal::Nil), 0); + assert_eq!(vc.get(&NilOrVal::Val(1)), 3); assert_eq!(vc.is_threshold_met(Threshold::Unreached, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Any, q, t), true); assert_eq!(vc.is_threshold_met(Threshold::Nil, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Value(1), q, t), true); assert_eq!(vc.is_threshold_met(Threshold::Value(2), q, t), false); - assert_eq!(vc.add(addr4, Some(2), 1), 1); - assert_eq!(vc.get(&None), 0); - assert_eq!(vc.get(&Some(1)), 3); - assert_eq!(vc.get(&Some(2)), 1); + assert_eq!(vc.add(addr4, NilOrVal::Val(2), 1), 1); + assert_eq!(vc.get(&NilOrVal::Nil), 0); + assert_eq!(vc.get(&NilOrVal::Val(1)), 3); + assert_eq!(vc.get(&NilOrVal::Val(2)), 1); assert_eq!(vc.is_threshold_met(Threshold::Unreached, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Any, q, t), true); assert_eq!(vc.is_threshold_met(Threshold::Nil, q, t), false); @@ -230,11 +231,11 @@ mod tests { assert_eq!(vc.is_threshold_met(Threshold::Value(2), q, t), false); // addr4 votes again, for a different value, is ignored - assert_eq!(vc.add(addr4, Some(3), 1), 0); - assert_eq!(vc.get(&None), 0); - assert_eq!(vc.get(&Some(1)), 3); - assert_eq!(vc.get(&Some(2)), 1); - assert_eq!(vc.get(&Some(3)), 0); + assert_eq!(vc.add(addr4, NilOrVal::Val(3), 1), 0); + assert_eq!(vc.get(&NilOrVal::Nil), 0); + assert_eq!(vc.get(&NilOrVal::Val(1)), 3); + assert_eq!(vc.get(&NilOrVal::Val(2)), 1); + assert_eq!(vc.get(&NilOrVal::Val(3)), 0); assert_eq!(vc.is_threshold_met(Threshold::Unreached, q, t), false); assert_eq!(vc.is_threshold_met(Threshold::Any, q, t), true); assert_eq!(vc.is_threshold_met(Threshold::Nil, q, t), false); diff --git a/Code/vote/src/keeper.rs b/Code/vote/src/keeper.rs index 15b3fdab1..5f5f9e1b5 100644 --- a/Code/vote/src/keeper.rs +++ b/Code/vote/src/keeper.rs @@ -2,7 +2,7 @@ use core::fmt; use alloc::collections::{BTreeMap, BTreeSet}; -use malachite_common::{Context, Round, ValueId, Vote, VoteType}; +use malachite_common::{Context, NilOrVal, Round, ValueId, Vote, VoteType}; use crate::round_votes::RoundVotes; use crate::round_weights::RoundWeights; @@ -191,7 +191,7 @@ where fn compute_threshold( vote_type: VoteType, round: &PerRound, - value: &Option>, + value: &NilOrVal>, quorum: ThresholdParam, total_weight: Weight, ) -> Threshold> @@ -201,9 +201,11 @@ where let weight = round.votes.get_weight(vote_type, value); match value { - Some(value) if quorum.is_met(weight, total_weight) => Threshold::Value(value.clone()), + NilOrVal::Val(value) if quorum.is_met(weight, total_weight) => { + Threshold::Value(value.clone()) + } - None if quorum.is_met(weight, total_weight) => Threshold::Nil, + NilOrVal::Nil if quorum.is_met(weight, total_weight) => Threshold::Nil, _ => { let weight_sum = round.votes.weight_sum(vote_type); diff --git a/Code/vote/src/round_votes.rs b/Code/vote/src/round_votes.rs index c54aab7f0..7d9223e5b 100644 --- a/Code/vote/src/round_votes.rs +++ b/Code/vote/src/round_votes.rs @@ -1,4 +1,4 @@ -use malachite_common::VoteType; +use malachite_common::{NilOrVal, VoteType}; use crate::count::VoteCount; use crate::{Threshold, ThresholdParam, Weight}; @@ -30,7 +30,7 @@ impl RoundVotes { &mut self, vote_type: VoteType, address: Address, - value: Option, + value: NilOrVal, weight: Weight, ) -> Weight where @@ -43,7 +43,7 @@ impl RoundVotes { } } - pub fn get_weight(&self, vote_type: VoteType, value: &Option) -> Weight + pub fn get_weight(&self, vote_type: VoteType, value: &NilOrVal) -> Weight where Value: Ord, { @@ -60,7 +60,7 @@ impl RoundVotes { } } - pub fn combined_weight(&self, value: &Option) -> Weight + pub fn combined_weight(&self, value: &NilOrVal) -> Weight where Value: Ord, { From 265d3fe51d6844e11b60dce3ff40fd333fd583b9 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Mon, 11 Dec 2023 17:08:58 +0100 Subject: [PATCH 9/9] code: Document all items in the main crates (#118) * Document all items in `malachite-common` * Document all items in `malachite-vote` * Document all items in `malachite-round` * Document all items in `malachite-driver` --- Code/common/src/context.rs | 19 +++++++- Code/common/src/lib.rs | 8 ++- Code/common/src/signed_vote.rs | 6 +++ Code/common/src/signing.rs | 11 +++++ Code/common/src/timeout.rs | 12 +++++ Code/common/src/value.rs | 8 +++ Code/driver/src/driver.rs | 34 ++++++++++--- Code/driver/src/error.rs | 1 + Code/driver/src/lib.rs | 2 +- Code/driver/src/mux.rs | 86 +++++++++++++++++++++++++-------- Code/driver/src/proposer.rs | 1 + Code/round/src/input.rs | 3 ++ Code/round/src/lib.rs | 2 +- Code/round/src/output.rs | 32 +++++++++--- Code/round/src/state.rs | 29 +++++++++++ Code/round/src/state_machine.rs | 7 +++ Code/round/src/transition.rs | 9 ++++ Code/test/tests/driver.rs | 42 ++++++++++------ Code/test/tests/driver_extra.rs | 27 +++++++---- Code/vote/src/count.rs | 6 +++ Code/vote/src/keeper.rs | 28 +++++++++++ Code/vote/src/lib.rs | 26 +++++++--- Code/vote/src/round_votes.rs | 14 ++++++ Code/vote/src/round_weights.rs | 8 +++ Code/vote/src/value_weights.rs | 3 ++ 25 files changed, 357 insertions(+), 67 deletions(-) diff --git a/Code/common/src/context.rs b/Code/common/src/context.rs index ede49af46..49c15cf51 100644 --- a/Code/common/src/context.rs +++ b/Code/common/src/context.rs @@ -9,20 +9,35 @@ pub trait Context where Self: Sized, { + /// The type of address of a validator. type Address: Address; + + /// The type of the height of a block. type Height: Height; + + /// The interface provided by the proposal type. type Proposal: Proposal; + + /// The interface provided by the validator type. type Validator: Validator; + + /// The interface provided by the validator set type. type ValidatorSet: ValidatorSet; + + /// The type of values that can be proposed. type Value: Value; + + /// The type of votes that can be cast. type Vote: Vote; - type SigningScheme: SigningScheme; // TODO: Do we need to support multiple signing schemes? + + // TODO: Do we need to support multiple signing schemes? + /// The signing scheme used to sign votes. + type SigningScheme: SigningScheme; /// Sign the given vote our private key. fn sign_vote(&self, vote: Self::Vote) -> SignedVote; /// Verify the given vote's signature using the given public key. - /// TODO: Maybe move this as concrete methods in `SignedVote`? fn verify_signed_vote( &self, signed_vote: &SignedVote, diff --git a/Code/common/src/lib.rs b/Code/common/src/lib.rs index d9a7b3403..3abfccdcd 100644 --- a/Code/common/src/lib.rs +++ b/Code/common/src/lib.rs @@ -4,7 +4,7 @@ #![forbid(unsafe_code)] #![deny(unused_crate_dependencies, trivial_casts, trivial_numeric_casts)] #![warn( - // missing_docs, + missing_docs, rustdoc::broken_intra_doc_links, rustdoc::private_intra_doc_links, variant_size_differences @@ -28,8 +28,14 @@ pub use ::signature; /// Type alias to make it easier to refer the `ValueId` type of a given `Consensus` engine. pub type ValueId = <::Value as Value>::Id; + +/// Type alias to make it easier to refer the `PublicKey` type of a given `Consensus` engine. pub type PublicKey = <::SigningScheme as SigningScheme>::PublicKey; + +/// Type alias to make it easier to refer the `PrivateKey` type of a given `Consensus` engine. pub type PrivateKey = <::SigningScheme as SigningScheme>::PrivateKey; + +/// Type alias to make it easier to refer the `Signature` type of a given `Consensus` engine. pub type Signature = <::SigningScheme as SigningScheme>::Signature; pub use context::Context; diff --git a/Code/common/src/signed_vote.rs b/Code/common/src/signed_vote.rs index 65b579a83..3e9b6f52e 100644 --- a/Code/common/src/signed_vote.rs +++ b/Code/common/src/signed_vote.rs @@ -4,11 +4,15 @@ use crate::{Context, Signature, Vote}; // TODO: Do we need to abstract over `SignedVote` as well? +/// A signed vote, ie. a vote emitted by a validator and signed by its private key. pub struct SignedVote where Ctx: Context, { + /// The vote. pub vote: Ctx::Vote, + + /// The signature of the vote. pub signature: Signature, } @@ -16,10 +20,12 @@ impl SignedVote where Ctx: Context, { + /// Create a new signed vote from the given vote and signature. pub fn new(vote: Ctx::Vote, signature: Signature) -> Self { Self { vote, signature } } + /// Return the address of the validator that emitted this vote. pub fn validator_address(&self) -> &Ctx::Address { self.vote.validator_address() } diff --git a/Code/common/src/signing.rs b/Code/common/src/signing.rs index 8f52aef67..d91caabef 100644 --- a/Code/common/src/signing.rs +++ b/Code/common/src/signing.rs @@ -2,13 +2,24 @@ use core::fmt::Debug; use signature::{Keypair, Signer, Verifier}; +/// A signing scheme that can be used to sign votes and verify such signatures. +/// +/// This trait is used to abstract over the signature scheme used by the consensus engine. +/// +/// An example of a signing scheme is the Ed25519 signature scheme, +/// eg. as implemented in the [`ed25519-consensus`][ed25519-consensus] crate. +/// +/// [ed25519-consensus]: https://crates.io/crates/ed25519-consensus pub trait SigningScheme where Self: Clone + Debug + Eq, { + /// The type of signatures produced by this signing scheme. type Signature: Clone + Debug + Eq; + /// The type of public keys produced by this signing scheme. type PublicKey: Clone + Debug + Eq + Verifier; + /// The type of private keys produced by this signing scheme. type PrivateKey: Clone + Signer + Keypair; } diff --git a/Code/common/src/timeout.rs b/Code/common/src/timeout.rs index 5c72a75c8..30b3b76d6 100644 --- a/Code/common/src/timeout.rs +++ b/Code/common/src/timeout.rs @@ -3,31 +3,43 @@ use crate::Round; /// The round step for which the timeout is for. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum TimeoutStep { + /// The timeout is for the propose step. Propose, + + /// The timeout is for the prevote step. Prevote, + + /// The timeout is for the precommit step. Precommit, } /// A timeout for a round step. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct Timeout { + /// The round for which the timeout is for. pub round: Round, + + /// The round step for which the timeout is for. pub step: TimeoutStep, } impl Timeout { + /// Create a new timeout for the given round and step. pub fn new(round: Round, step: TimeoutStep) -> Self { Self { round, step } } + /// Create a new timeout for the propose step of the given round. pub fn propose(round: Round) -> Self { Self::new(round, TimeoutStep::Propose) } + /// Create a new timeout for the prevote step of the given round. pub fn prevote(round: Round) -> Self { Self::new(round, TimeoutStep::Prevote) } + /// Create a new timeout for the precommit step of the given round. pub fn precommit(round: Round) -> Self { Self::new(round, TimeoutStep::Precommit) } diff --git a/Code/common/src/value.rs b/Code/common/src/value.rs index e17062ba3..a16ac246e 100644 --- a/Code/common/src/value.rs +++ b/Code/common/src/value.rs @@ -5,21 +5,26 @@ use core::fmt::Debug; /// This type is isomorphic to `Option` but is more explicit about its intent. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub enum NilOrVal { + /// The value is `nil`. #[default] Nil, + /// The value is a value of type `Value`. Val(Value), } impl NilOrVal { + /// Whether this is `nil`. pub fn is_nil(&self) -> bool { matches!(self, Self::Nil) } + /// Whether this is an actual value. pub fn is_val(&self) -> bool { matches!(self, Self::Val(_)) } + /// Apply the given function to the value if it is not `nil`. pub fn map NewValue>(self, f: F) -> NilOrVal { match self { NilOrVal::Nil => NilOrVal::Nil, @@ -27,6 +32,7 @@ impl NilOrVal { } } + /// Convert this into an `NilOrVal<&Value>`, allowing to borrow the value. pub fn as_ref(&self) -> NilOrVal<&Value> { match self { NilOrVal::Nil => NilOrVal::Nil, @@ -34,6 +40,8 @@ impl NilOrVal { } } + /// Consumes this and returns the value if it is not `nil`, + /// otherwise returns the default `Value`. pub fn value_or_default(self) -> Value where Value: Default, diff --git a/Code/driver/src/driver.rs b/Code/driver/src/driver.rs index 8802b101f..82f2d49c9 100644 --- a/Code/driver/src/driver.rs +++ b/Code/driver/src/driver.rs @@ -24,15 +24,29 @@ pub struct Driver where Ctx: Context, { + /// The context of the consensus engine, + /// for defining the concrete data types and signature scheme. pub ctx: Ctx, + + /// The proposer selector. pub proposer_selector: Box>, + /// The address of the node. pub address: Ctx::Address, + + /// The validator set at the current height pub validator_set: Ctx::ValidatorSet, + /// The vote keeper. pub vote_keeper: VoteKeeper, + + /// The state of the round state machine. pub round_state: RoundState, + + /// The proposal to decide on, if any. pub proposal: Option, + + /// The pending input to be processed next, if any. pub pending_input: Option<(Round, RoundInput)>, } @@ -40,8 +54,13 @@ impl Driver where Ctx: Context, { + /// Create a new `Driver` instance for the given height. + /// + /// This instance is only valid for a single height + /// and should be discarded and re-created for the next height. pub fn new( ctx: Ctx, + height: Ctx::Height, proposer_selector: impl ProposerSelector + 'static, validator_set: Ctx::ValidatorSet, address: Ctx::Address, @@ -55,20 +74,23 @@ where address, validator_set, vote_keeper: votes, - round_state: RoundState::default(), + round_state: RoundState::new(height, Round::new(0)), proposal: None, pending_input: None, } } + /// Return the height of the consensus. pub fn height(&self) -> &Ctx::Height { &self.round_state.height } + /// Return the current round we are at. pub fn round(&self) -> Round { self.round_state.round } + /// Return the proposer for the current round. pub fn get_proposer(&self, round: Round) -> Result<&Ctx::Validator, Error> { let address = self .proposer_selector @@ -82,6 +104,7 @@ where Ok(proposer) } + /// Process the given input, returning the outputs to be broadcast to the network. pub async fn process(&mut self, msg: Input) -> Result>, Error> { let round_output = match self.apply(msg).await? { Some(msg) => msg, @@ -96,6 +119,7 @@ where Ok(outputs) } + /// Process the pending input, if any. fn process_pending(&mut self, outputs: &mut Vec>) -> Result<(), Error> { while let Some((round, input)) = self.pending_input.take() { if let Some(round_output) = self.apply_input(round, input)? { @@ -107,6 +131,7 @@ where Ok(()) } + /// Convert an output of the round state machine to the output type of the driver. fn lift_output(&mut self, round_output: RoundOutput) -> Output { match round_output { RoundOutput::NewRound(round) => Output::NewRound(self.height().clone(), round), @@ -134,6 +159,7 @@ where } } + /// Apply the given input to the state machine, returning the output, if any. async fn apply(&mut self, input: Input) -> Result>, Error> { match input { Input::NewRound(height, round) => self.apply_new_round(height, round).await, @@ -211,11 +237,7 @@ where }; let round_input = self.multiplex_vote_threshold(vote_output); - - match round_input { - Some(input) => self.apply_input(vote_round, input), - None => Ok(None), - } + self.apply_input(vote_round, round_input) } fn apply_timeout(&mut self, timeout: Timeout) -> Result>, Error> { diff --git a/Code/driver/src/error.rs b/Code/driver/src/error.rs index c659e4455..5846c161c 100644 --- a/Code/driver/src/error.rs +++ b/Code/driver/src/error.rs @@ -2,6 +2,7 @@ use core::fmt; use malachite_common::{Context, SignedVote, Validator}; +/// The type of errors that can be yielded by the `Driver`. #[derive(Clone, Debug)] pub enum Error where diff --git a/Code/driver/src/lib.rs b/Code/driver/src/lib.rs index caddfa65e..d1d968de8 100644 --- a/Code/driver/src/lib.rs +++ b/Code/driver/src/lib.rs @@ -4,7 +4,7 @@ #![forbid(unsafe_code)] #![deny(unused_crate_dependencies, trivial_casts, trivial_numeric_casts)] #![warn( - // missing_docs, + missing_docs, rustdoc::broken_intra_doc_links, rustdoc::private_intra_doc_links, variant_size_differences diff --git a/Code/driver/src/mux.rs b/Code/driver/src/mux.rs index cb5e31d4d..386316c11 100644 --- a/Code/driver/src/mux.rs +++ b/Code/driver/src/mux.rs @@ -1,3 +1,5 @@ +//! Multiplex inputs to the round state machine based on the current state. + use malachite_common::ValueId; use malachite_common::{Context, Proposal, Round, Value, VoteType}; use malachite_round::input::Input as RoundInput; @@ -12,6 +14,47 @@ impl Driver where Ctx: Context, { + /// Process a received proposal relative to the current state of the round, considering + /// its validity and performing various checks to determine the appropriate round input action. + /// + /// This is needed because, depending on the step we are at when we receive the proposal, + /// and the amount of votes we received for various values (or nil), we need to feed + /// different inputs to the round state machine, instead of a plain proposal. + /// + /// For example, if we have a proposal for a value, and we have a quorum of precommits + /// for that value, then we need to feed the round state machine a `ProposalAndPrecommitValue` + /// input instead of a plain `Proposal` input. + /// + /// The method follows these steps: + /// + /// 1. Check that there is an ongoing round, otherwise return `None` + /// + /// 2. Check that the proposal's height matches the current height, otherwise return `None`. + /// + /// 3. If the proposal is invalid, the method follows these steps: + /// a. If we are at propose step and the proposal's proof-of-lock (POL) round is `Nil`, return + /// `RoundInput::InvalidProposal`. + /// b. If we are at propose step and there is a polka for a prior-round proof-of-lock (POL), + /// return `RoundInput::InvalidProposalAndPolkaPrevious`. + /// c. For other steps or if there is no prior-round POL, return `None`. + /// + /// 4. Checks that the proposed value has already not already been decided, after storing the + /// proposal, but before further processing. + /// + /// 5. If a quorum of precommit votes is met for the proposal's value, + /// return `RoundInput::ProposalAndPrecommitValue` including the proposal. + /// + /// 6. If the proposal is for a different round than the current one, return `None`. + /// + /// 7. If a POL is present for the current round and we are beyond the prevote step, + /// return `RoundInput::ProposalAndPolkaCurrent`, including the proposal. + /// + /// 8. If we are at the propose step, and a prior round POL exists, + /// check if the proposal's valid round is equal to the threshold's valid round, + /// and then returns `RoundInput::ProposalAndPolkaPrevious`, including the proposal. + /// + /// 9. If none of the above conditions are met, simply wrap the proposal in + /// `RoundInput::Proposal` and return it. pub fn multiplex_proposal( &mut self, proposal: Ctx::Proposal, @@ -48,9 +91,7 @@ where return Some(RoundInput::InvalidProposal); } else if polka_previous { // L32 - return Some(RoundInput::InvalidProposalAndPolkaPrevious( - proposal.clone(), - )); + return Some(RoundInput::InvalidProposalAndPolkaPrevious(proposal)); } else { return None; } @@ -67,7 +108,7 @@ where VoteType::Precommit, Threshold::Value(proposal.value().id()), ) { - return Some(RoundInput::ProposalAndPrecommitValue(proposal.clone())); + return Some(RoundInput::ProposalAndPrecommitValue(proposal)); } // If the proposal is for a different round, drop the proposal @@ -97,43 +138,47 @@ where Some(RoundInput::Proposal(proposal)) } + /// After a vote threshold change, check if we have a polka for nil, some value or any, + /// based on the type of threshold and the current proposal. pub fn multiplex_vote_threshold( &self, new_threshold: VoteKeeperOutput>, - ) -> Option> { + ) -> RoundInput { if let Some(proposal) = &self.proposal { match new_threshold { - VoteKeeperOutput::PolkaAny => Some(RoundInput::PolkaAny), - VoteKeeperOutput::PolkaNil => Some(RoundInput::PolkaNil), + VoteKeeperOutput::PolkaAny => RoundInput::PolkaAny, + VoteKeeperOutput::PolkaNil => RoundInput::PolkaNil, VoteKeeperOutput::PolkaValue(v) => { if v == proposal.value().id() { - Some(RoundInput::ProposalAndPolkaCurrent(proposal.clone())) + RoundInput::ProposalAndPolkaCurrent(proposal.clone()) } else { - Some(RoundInput::PolkaAny) + RoundInput::PolkaAny } } - VoteKeeperOutput::PrecommitAny => Some(RoundInput::PrecommitAny), + VoteKeeperOutput::PrecommitAny => RoundInput::PrecommitAny, VoteKeeperOutput::PrecommitValue(v) => { if v == proposal.value().id() { - Some(RoundInput::ProposalAndPrecommitValue(proposal.clone())) + RoundInput::ProposalAndPrecommitValue(proposal.clone()) } else { - Some(RoundInput::PrecommitAny) + RoundInput::PrecommitAny } } - VoteKeeperOutput::SkipRound(r) => Some(RoundInput::SkipRound(r)), + VoteKeeperOutput::SkipRound(r) => RoundInput::SkipRound(r), } } else { match new_threshold { - VoteKeeperOutput::PolkaAny => Some(RoundInput::PolkaAny), - VoteKeeperOutput::PolkaNil => Some(RoundInput::PolkaNil), - VoteKeeperOutput::PolkaValue(_) => Some(RoundInput::PolkaAny), - VoteKeeperOutput::PrecommitAny => Some(RoundInput::PrecommitAny), - VoteKeeperOutput::PrecommitValue(_) => Some(RoundInput::PrecommitAny), - VoteKeeperOutput::SkipRound(r) => Some(RoundInput::SkipRound(r)), + VoteKeeperOutput::PolkaAny => RoundInput::PolkaAny, + VoteKeeperOutput::PolkaNil => RoundInput::PolkaNil, + VoteKeeperOutput::PolkaValue(_) => RoundInput::PolkaAny, + VoteKeeperOutput::PrecommitAny => RoundInput::PrecommitAny, + VoteKeeperOutput::PrecommitValue(_) => RoundInput::PrecommitAny, + VoteKeeperOutput::SkipRound(r) => RoundInput::SkipRound(r), } } } + /// After a step change, check if we have a polka for nil, some value or any, + /// and return the corresponding input for the round state machine. pub fn multiplex_step_change( &self, pending_step: Step, @@ -163,6 +208,7 @@ where } } +/// Check if we have a polka for nil fn has_polka_nil(votekeeper: &VoteKeeper, round: Round) -> bool where Ctx: Context, @@ -170,6 +216,7 @@ where votekeeper.is_threshold_met(&round, VoteType::Prevote, Threshold::Nil) } +/// Check if we have a polka for a value fn has_polka_value<'p, Ctx>( votekeeper: &VoteKeeper, round: Round, @@ -189,6 +236,7 @@ where .then_some(proposal) } +/// Check if we have a polka for any fn has_polka_any(votekeeper: &VoteKeeper, round: Round) -> bool where Ctx: Context, diff --git a/Code/driver/src/proposer.rs b/Code/driver/src/proposer.rs index 22bf99cd6..eb4abbd4a 100644 --- a/Code/driver/src/proposer.rs +++ b/Code/driver/src/proposer.rs @@ -1,5 +1,6 @@ use malachite_common::{Context, Round}; +/// Defines how to select a proposer amongst a validator set for a given round. pub trait ProposerSelector where Ctx: Context, diff --git a/Code/round/src/input.rs b/Code/round/src/input.rs index 116d33abf..7416ddf87 100644 --- a/Code/round/src/input.rs +++ b/Code/round/src/input.rs @@ -1,7 +1,10 @@ +//! Inputs to the round state machine. + use core::fmt; use malachite_common::{Context, Round, ValueId}; +/// Input to the round state machine. pub enum Input where Ctx: Context, diff --git a/Code/round/src/lib.rs b/Code/round/src/lib.rs index 33b43bcf6..d8f8cebb6 100644 --- a/Code/round/src/lib.rs +++ b/Code/round/src/lib.rs @@ -4,7 +4,7 @@ #![forbid(unsafe_code)] #![deny(unused_crate_dependencies, trivial_casts, trivial_numeric_casts)] #![warn( - // missing_docs, + missing_docs, rustdoc::broken_intra_doc_links, rustdoc::private_intra_doc_links, variant_size_differences diff --git a/Code/round/src/output.rs b/Code/round/src/output.rs index c79feb2e3..5d43b9d75 100644 --- a/Code/round/src/output.rs +++ b/Code/round/src/output.rs @@ -1,22 +1,37 @@ +//! Outputs of the round state machine. + use core::fmt; use malachite_common::{Context, NilOrVal, Round, Timeout, TimeoutStep, ValueId}; use crate::state::RoundValue; +/// Output of the round state machine. pub enum Output where Ctx: Context, { - NewRound(Round), // Move to the new round. - Proposal(Ctx::Proposal), // Broadcast the proposal. - Vote(Ctx::Vote), // Broadcast the vote. - ScheduleTimeout(Timeout), // Schedule the timeout. - GetValueAndScheduleTimeout(Round, Timeout), // Ask for a value and schedule a timeout. - Decision(RoundValue), // Decide the value. + /// Move to the new round. + NewRound(Round), + + /// Broadcast the proposal. + Proposal(Ctx::Proposal), + + /// Broadcast the vote. + Vote(Ctx::Vote), + + /// Schedule the timeout. + ScheduleTimeout(Timeout), + + /// Ask for a value and schedule a timeout. + GetValueAndScheduleTimeout(Round, Timeout), + + /// Decide the value. + Decision(RoundValue), } impl Output { + /// Build a `Proposal` output. pub fn proposal( height: Ctx::Height, round: Round, @@ -26,6 +41,7 @@ impl Output { Output::Proposal(Ctx::new_proposal(height, round, value, pol_round)) } + /// Build a `Vote` output for a prevote. pub fn prevote( height: Ctx::Height, round: Round, @@ -35,6 +51,7 @@ impl Output { Output::Vote(Ctx::new_prevote(height, round, value_id, address)) } + /// Build a `Vote` output for a precommit. pub fn precommit( height: Ctx::Height, round: Round, @@ -44,14 +61,17 @@ impl Output { Output::Vote(Ctx::new_precommit(height, round, value_id, address)) } + /// Build a `ScheduleTimeout` output. pub fn schedule_timeout(round: Round, step: TimeoutStep) -> Self { Output::ScheduleTimeout(Timeout { round, step }) } + /// Build a `GetValueAndScheduleTimeout` output. pub fn get_value_and_schedule_timeout(round: Round, step: TimeoutStep) -> Self { Output::GetValueAndScheduleTimeout(round, Timeout { round, step }) } + /// Build a `Decision` output. pub fn decision(round: Round, value: Ctx::Value) -> Self { Output::Decision(RoundValue { round, value }) } diff --git a/Code/round/src/state.rs b/Code/round/src/state.rs index c15d1e091..f367c4280 100644 --- a/Code/round/src/state.rs +++ b/Code/round/src/state.rs @@ -1,3 +1,5 @@ +//! The state maintained by the round state machine + use core::fmt; use crate::input::Input; @@ -9,11 +11,14 @@ use malachite_common::{Context, Round}; /// A value and its associated round #[derive(Clone, Debug, PartialEq, Eq)] pub struct RoundValue { + /// The value pub value: Value, + /// The round pub round: Round, } impl RoundValue { + /// Create a new `RoundValue` instance. pub fn new(value: Value, round: Round) -> Self { Self { value, round } } @@ -22,10 +27,20 @@ impl RoundValue { /// The step of consensus in this round #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum Step { + /// The round has just started NewRound, + + /// We are at the propose step. + /// Either we are the proposer or we are waiting for a proposal. Propose, + + /// We are at the prevote step. Prevote, + + /// We are at the precommit step. Precommit, + + /// We have committed and decided on a value Commit, } @@ -34,11 +49,19 @@ pub struct State where Ctx: Context, { + /// The height of the consensus pub height: Ctx::Height, + + /// The round we are at within a height pub round: Round, + /// The step we are at within a round pub step: Step, + + /// The value we are locked on, ie. we have received a polka for before we precommitted pub locked: Option>, + + /// The value for which we received a polka for after we already precommitted pub valid: Option>, } @@ -46,6 +69,7 @@ impl State where Ctx: Context, { + /// Create a new `State` instance at the given height and round. pub fn new(height: Ctx::Height, round: Round) -> Self { Self { height, @@ -56,14 +80,17 @@ where } } + /// Set the round. pub fn with_round(self, round: Round) -> Self { Self { round, ..self } } + /// Set the step. pub fn with_step(self, step: Step) -> Self { Self { step, ..self } } + /// Set the locked value. pub fn set_locked(self, value: Ctx::Value) -> Self { Self { locked: Some(RoundValue::new(value, self.round)), @@ -71,6 +98,7 @@ where } } + /// Set the valid value. pub fn set_valid(self, value: Ctx::Value) -> Self { Self { valid: Some(RoundValue::new(value, self.round)), @@ -78,6 +106,7 @@ where } } + /// Apply the given input to the current state, triggering a transition. pub fn apply(self, data: &Info, input: Input) -> Transition { crate::state_machine::apply(self, data, input) } diff --git a/Code/round/src/state_machine.rs b/Code/round/src/state_machine.rs index 09d42aeed..83b27f315 100644 --- a/Code/round/src/state_machine.rs +++ b/Code/round/src/state_machine.rs @@ -1,3 +1,5 @@ +//! The consensus state machine. + use malachite_common::{Context, NilOrVal, Proposal, Round, TimeoutStep, Value}; use crate::input::Input; @@ -13,8 +15,11 @@ pub struct Info<'a, Ctx> where Ctx: Context, { + /// The round for which the input is for, can be different than the round we are at pub input_round: Round, + /// Address of our node pub address: &'a Ctx::Address, + /// Proposer for the round we are at pub proposer: &'a Ctx::Address, } @@ -22,6 +27,7 @@ impl<'a, Ctx> Info<'a, Ctx> where Ctx: Context, { + /// Create a new `Info` instance. pub fn new(input_round: Round, address: &'a Ctx::Address, proposer: &'a Ctx::Address) -> Self { Self { input_round, @@ -30,6 +36,7 @@ where } } + /// Check if we are the proposer for the round we are at. pub fn is_proposer(&self) -> bool { self.address == self.proposer } diff --git a/Code/round/src/transition.rs b/Code/round/src/transition.rs index 5d28df5b0..e66f99359 100644 --- a/Code/round/src/transition.rs +++ b/Code/round/src/transition.rs @@ -1,15 +1,21 @@ +//! A transition taken by the state machine after processing an input. + use malachite_common::Context; use crate::output::Output; use crate::state::State; +/// A transition taken by the state machine after processing an input. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Transition where Ctx: Context, { + /// The next state to transition to. pub next_state: State, + /// The output to emit. pub output: Option>, + /// Whether the transition is valid or not. pub valid: bool, } @@ -17,6 +23,7 @@ impl Transition where Ctx: Context, { + /// Build a new valid transition to the given next state. pub fn to(next_state: State) -> Self { Self { next_state, @@ -25,6 +32,7 @@ where } } + /// Build a new invalid transition to the given next state. pub fn invalid(next_state: State) -> Self { Self { next_state, @@ -33,6 +41,7 @@ where } } + /// Set the output of the transition. pub fn with_output(mut self, output: Output) -> Self { self.output = Some(output); self diff --git a/Code/test/tests/driver.rs b/Code/test/tests/driver.rs index 4ac4749ca..199378ccd 100644 --- a/Code/test/tests/driver.rs +++ b/Code/test/tests/driver.rs @@ -33,11 +33,12 @@ fn driver_steps_proposer() { let [(v1, sk1), (v2, sk2), (v3, sk3)] = make_validators([1, 2, 3]); let (my_sk, my_addr) = (sk1, v1.address); + let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); let sel = FixedProposer::new(my_addr); let vs = ValidatorSet::new(vec![v1, v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); let proposal = Proposal::new(Height::new(1), Round::new(0), value, Round::new(-1)); @@ -244,11 +245,12 @@ fn driver_steps_proposer_timeout_get_value() { let [(v1, sk1), (v2, _sk2), (v3, _sk3)] = make_validators([1, 2, 3]); let (my_sk, my_addr) = (sk1, v1.address); + let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); let sel = FixedProposer::new(my_addr); let vs = ValidatorSet::new(vec![v1, v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); let steps = vec![ TestStep { @@ -297,11 +299,12 @@ fn driver_steps_not_proposer_valid() { // Proposer is v1, so we are not the proposer let (my_sk, my_addr) = (sk2, v2.address); + let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); let sel = FixedProposer::new(v1.address); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); let proposal = Proposal::new(Height::new(1), Round::new(0), value, Round::new(-1)); @@ -496,11 +499,12 @@ fn driver_steps_not_proposer_invalid() { // Proposer is v1, so we are not the proposer let (my_sk, my_addr) = (sk2, v2.address); + let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); let sel = FixedProposer::new(v1.address); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); let proposal = Proposal::new(Height::new(1), Round::new(0), value, Round::new(-1)); @@ -605,11 +609,12 @@ fn driver_steps_not_proposer_other_height() { // Proposer is v1, so we are not the proposer let (my_sk, my_addr) = (sk2, v2.address); + let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); let sel = FixedProposer::new(v1.address); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); // Proposal is for another height let proposal = Proposal::new(Height::new(2), Round::new(0), value, Round::new(-1)); @@ -655,11 +660,12 @@ fn driver_steps_not_proposer_other_round() { // Proposer is v1, so we are not the proposer let (my_sk, my_addr) = (sk2, v2.address); + let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); let sel = FixedProposer::new(v1.address); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); // Proposal is for another round let proposal = Proposal::new(Height::new(1), Round::new(1), value, Round::new(-1)); @@ -705,11 +711,12 @@ fn driver_steps_not_proposer_timeout_multiple_rounds() { // Proposer is v1, so we, v3, are not the proposer let (my_sk, my_addr) = (sk3, v3.address); + let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); let sel = FixedProposer::new(v1.address); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); let steps = vec![ // Start round 0, we, v3, are not the proposer @@ -889,13 +896,15 @@ fn driver_steps_not_proposer_timeout_multiple_rounds() { fn driver_steps_no_value_to_propose() { let [(v1, sk1), (v2, _sk2), (v3, _sk3)] = make_validators([1, 2, 3]); let (my_sk, my_addr) = (sk1, v1.address); + + let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); // We are the proposer let sel = FixedProposer::new(v1.address); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); let mut outputs = block_on(driver.process(Input::NewRound(Height::new(1), Round::new(0)))) .expect("execute succeeded"); @@ -917,13 +926,14 @@ fn driver_steps_proposer_not_found() { let (my_sk, my_addr) = (sk2, v2.address); + let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); // Proposer is v1, which is not in the validator set let sel = FixedProposer::new(v1.address); let vs = ValidatorSet::new(vec![v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); let output = block_on(driver.process(Input::NewRound(Height::new(1), Round::new(0)))); assert_eq!(output, Err(Error::ProposerNotFound(v1.address))); @@ -936,6 +946,8 @@ fn driver_steps_validator_not_found() { let [(v1, _sk1), (v2, sk2), (v3, sk3)] = make_validators([1, 2, 3]); let (my_sk, my_addr) = (sk3.clone(), v3.address); + + let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); // Proposer is v1 @@ -943,7 +955,7 @@ fn driver_steps_validator_not_found() { // We omit v2 from the validator set let vs = ValidatorSet::new(vec![v1.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); // Start new height block_on(driver.process(Input::NewRound(Height::new(1), Round::new(0)))) @@ -972,12 +984,14 @@ fn driver_steps_invalid_signature() { let [(v1, sk1), (v2, _sk2), (v3, sk3)] = make_validators([1, 2, 3]); let (my_sk, my_addr) = (sk3.clone(), v3.address); + + let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); let sel = FixedProposer::new(v1.address); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); // Start new round block_on(driver.process(Input::NewRound(Height::new(1), Round::new(0)))) @@ -1015,7 +1029,7 @@ fn driver_steps_skip_round_skip_threshold() { let height = Height::new(1); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); let steps = vec![ // Start round 0, we, v3, are not the proposer @@ -1112,11 +1126,11 @@ fn driver_steps_skip_round_quorum_threshold() { // Proposer is v1, so we, v3, are not the proposer let (my_sk, my_addr) = (sk3, v3.address); - let ctx = TestContext::new(my_sk.clone()); let height = Height::new(1); + let ctx = TestContext::new(my_sk.clone()); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); let steps = vec![ // Start round 0, we, v3, are not the proposer diff --git a/Code/test/tests/driver_extra.rs b/Code/test/tests/driver_extra.rs index d49840909..2557b500c 100644 --- a/Code/test/tests/driver_extra.rs +++ b/Code/test/tests/driver_extra.rs @@ -74,11 +74,12 @@ fn driver_steps_decide_current_with_no_locked_no_valid() { let [(v1, sk1), (v2, sk2), (v3, sk3)] = make_validators([2, 3, 2]); let (my_sk, my_addr) = (sk3.clone(), v3.address); + let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); let sel = RotateProposer; let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); let steps = vec![ TestStep { @@ -146,11 +147,12 @@ fn driver_steps_decide_previous_with_no_locked_no_valid() { let [(v1, sk1), (v2, sk2), (v3, sk3)] = make_validators([2, 3, 2]); let (my_sk, my_addr) = (sk3.clone(), v3.address); + let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); let sel = RotateProposer; let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); let steps = vec![ TestStep { @@ -246,11 +248,12 @@ fn driver_steps_decide_previous_with_locked_and_valid() { let [(v1, sk1), (v2, sk2), (v3, sk3)] = make_validators([2, 3, 2]); let (my_sk, my_addr) = (sk3.clone(), v3.address); + let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); let sel = RotateProposer; let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); let steps = vec![ TestStep { @@ -366,11 +369,12 @@ fn driver_steps_polka_previous_with_locked() { let [(v1, sk1), (v2, sk2), (v3, sk3)] = make_validators([2, 2, 3]); let (my_sk, my_addr) = (sk2, v2.address); + let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); let sel = RotateProposer; let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); let steps = vec![ TestStep { @@ -473,11 +477,12 @@ fn driver_steps_polka_previous_invalid_proposal() { let [(v1, sk1), (v2, sk2), (v3, sk3)] = make_validators([2, 3, 2]); let (my_sk, my_addr) = (sk3, v3.address); + let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); let sel = RotateProposer; let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); let steps = vec![ TestStep { @@ -571,11 +576,12 @@ fn driver_steps_polka_previous_with_no_locked() { let [(v1, sk1), (v2, sk2), (v3, sk3)] = make_validators([2, 2, 3]); let (my_sk, my_addr) = (sk2, v2.address); + let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); let sel = RotateProposer; let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); let steps = vec![ TestStep { @@ -679,11 +685,12 @@ fn driver_steps_polka_nil_and_timout_propose() { let [(v1, sk1), (v2, sk2), (v3, sk3)] = make_validators([2, 3, 2]); let (my_sk, my_addr) = (sk3.clone(), v3.address); + let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); let sel = RotateProposer; let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); let steps = vec![ TestStep { @@ -739,11 +746,12 @@ fn driver_steps_polka_value_then_proposal() { let [(v1, sk1), (v2, sk2), (v3, sk3)] = make_validators([2, 3, 2]); let (my_sk, my_addr) = (sk3.clone(), v3.address); + let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); let sel = RotateProposer; let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); let steps = vec![ TestStep { @@ -802,11 +810,12 @@ fn driver_steps_polka_any_then_proposal_other() { let [(v1, sk1), (v2, sk2), (v3, sk3)] = make_validators([2, 3, 2]); let (my_sk, my_addr) = (sk3.clone(), v3.address); + let height = Height::new(1); let ctx = TestContext::new(my_sk.clone()); let sel = RotateProposer; let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut driver = Driver::new(ctx, sel, vs, my_addr, Default::default()); + let mut driver = Driver::new(ctx, height, sel, vs, my_addr, Default::default()); let steps = vec![ TestStep { diff --git a/Code/vote/src/count.rs b/Code/vote/src/count.rs index e891c3c28..7d9e08c44 100644 --- a/Code/vote/src/count.rs +++ b/Code/vote/src/count.rs @@ -1,3 +1,5 @@ +//! For tallying votes of the same type. + use alloc::collections::BTreeSet; use malachite_common::NilOrVal; @@ -5,6 +7,7 @@ use crate::value_weights::ValuesWeights; use crate::{Threshold, ThresholdParam, Weight}; /// VoteCount tallys votes of the same type. +/// /// Votes are for nil or for some value. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct VoteCount { @@ -16,6 +19,7 @@ pub struct VoteCount { } impl VoteCount { + /// Create a new `VoteCount`. pub fn new() -> Self { VoteCount { values_weights: ValuesWeights::new(), @@ -39,6 +43,7 @@ impl VoteCount { } } + /// Return the weight of votes for the given value (or nil). pub fn get(&self, value: &NilOrVal) -> Weight where Value: Ord, @@ -46,6 +51,7 @@ impl VoteCount { self.values_weights.get(value) } + /// Return the sum of weights of votes for all values. pub fn sum(&self) -> Weight { self.values_weights.sum() } diff --git a/Code/vote/src/keeper.rs b/Code/vote/src/keeper.rs index 5f5f9e1b5..777c19704 100644 --- a/Code/vote/src/keeper.rs +++ b/Code/vote/src/keeper.rs @@ -1,3 +1,5 @@ +//! For tallying votes and emitting messages when certain thresholds are reached. + use core::fmt; use alloc::collections::{BTreeMap, BTreeSet}; @@ -11,20 +13,35 @@ use crate::{Threshold, ThresholdParam, ThresholdParams, Weight}; /// Messages emitted by the vote keeper #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Output { + /// We have a quorum of prevotes for some value or nil PolkaAny, + + /// We have a quorum of prevotes for nil PolkaNil, + + /// We have a quorum of prevotes for specific value PolkaValue(Value), + + /// We have a quorum of precommits for some value or nil PrecommitAny, + + /// We have a quorum of precommits for a specific value PrecommitValue(Value), + + /// We have f+1 honest votes for a value at a higher round SkipRound(Round), } +/// Keeps track of votes and emitted outputs for a given round. pub struct PerRound where Ctx: Context, { + /// The votes for this round. votes: RoundVotes>, + /// The addresses and their weights for this round. addresses_weights: RoundWeights, + /// The emitted outputs for this round. emitted_outputs: BTreeSet>>, } @@ -32,6 +49,7 @@ impl PerRound where Ctx: Context, { + /// Create a new `PerRound` instance. fn new() -> Self { Self { votes: RoundVotes::new(), @@ -40,14 +58,17 @@ where } } + /// Return the votes for this round. pub fn votes(&self) -> &RoundVotes> { &self.votes } + /// Return the addresses and their weights for this round. pub fn addresses_weights(&self) -> &RoundWeights { &self.addresses_weights } + /// Return the emitted outputs for this round. pub fn emitted_outputs(&self) -> &BTreeSet>> { &self.emitted_outputs } @@ -86,8 +107,11 @@ pub struct VoteKeeper where Ctx: Context, { + /// The total weight (ie. voting power) of the network. total_weight: Weight, + /// The threshold parameters. threshold_params: ThresholdParams, + /// The votes and emitted outputs for each round. per_round: BTreeMap>, } @@ -95,6 +119,8 @@ impl VoteKeeper where Ctx: Context, { + /// Create a new `VoteKeeper` instance, for the given + /// total network weight (ie. voting power) and threshold parameters. pub fn new(total_weight: Weight, threshold_params: ThresholdParams) -> Self { VoteKeeper { total_weight, @@ -103,10 +129,12 @@ where } } + /// Return the total weight (ie. voting power) of the network. pub fn total_weight(&self) -> &Weight { &self.total_weight } + /// Return the threshold parameters. pub fn per_round(&self) -> &BTreeMap> { &self.per_round } diff --git a/Code/vote/src/lib.rs b/Code/vote/src/lib.rs index d474eae37..35231ecee 100644 --- a/Code/vote/src/lib.rs +++ b/Code/vote/src/lib.rs @@ -1,10 +1,10 @@ -//! Tally votes of the same type (eg. prevote or precommit) +//! Infrastructre for tallying votes within the consensus engine. #![no_std] #![forbid(unsafe_code)] #![deny(unused_crate_dependencies, trivial_casts, trivial_numeric_casts)] #![warn( - // missing_docs, + missing_docs, rustdoc::broken_intra_doc_links, rustdoc::private_intra_doc_links, variant_size_differences @@ -12,6 +12,8 @@ #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::panic))] #![cfg_attr(coverage_nightly, feature(coverage_attribute))] +use malachite_common::VotingPower; + extern crate alloc; pub mod count; @@ -21,8 +23,9 @@ pub mod round_weights; pub mod value_weights; // TODO: Introduce newtype -// QUESTION: Over what type? i64? -pub type Weight = u64; +/// Represents the weight of a vote, +/// ie. the voting power of the validator that cast the vote. +pub type Weight = VotingPower; /// Represents the different quorum thresholds. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -44,6 +47,11 @@ pub enum Threshold { Value(ValueId), } +/// Represents the different quorum thresholds. +/// +/// There are two thresholds: +/// - The quorum threshold, which is the minimum number of votes required for a quorum. +/// - The honest threshold, which is the minimum number of votes required for a quorum of honest nodes. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct ThresholdParams { /// Threshold for a quorum (default: 2f+1) @@ -62,22 +70,24 @@ impl Default for ThresholdParams { } } +// TODO: Distinguish between quorum and honest thresholds at the type-level /// Represents the different quorum thresholds. -/// -/// TODO: Distinguish between quorum and honest thresholds at the type-level #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct ThresholdParam { + /// Numerator of the threshold pub numerator: u64, + /// Denominator of the threshold pub denominator: u64, } impl ThresholdParam { - /// 2f+1 + /// 2f+1, ie. more than two thirds of the total weight pub const TWO_F_PLUS_ONE: Self = Self::new(2, 3); - /// f+1 + /// f+1, ie. more than one third of the total weight pub const F_PLUS_ONE: Self = Self::new(1, 3); + /// Create a new threshold parameter with the given numerator and denominator. pub const fn new(numerator: u64, denominator: u64) -> Self { Self { numerator, diff --git a/Code/vote/src/round_votes.rs b/Code/vote/src/round_votes.rs index 7d9223e5b..2a1f29ccb 100644 --- a/Code/vote/src/round_votes.rs +++ b/Code/vote/src/round_votes.rs @@ -1,3 +1,5 @@ +//! For tallying all the votes for a single round + use malachite_common::{NilOrVal, VoteType}; use crate::count::VoteCount; @@ -6,11 +8,14 @@ use crate::{Threshold, ThresholdParam, Weight}; /// Tracks all the votes for a single round #[derive(Clone, Debug)] pub struct RoundVotes { + /// The prevotes for this round. prevotes: VoteCount, + /// The precommits for this round. precommits: VoteCount, } impl RoundVotes { + /// Create a new `RoundVotes` instance. pub fn new() -> Self { RoundVotes { prevotes: VoteCount::new(), @@ -18,14 +23,18 @@ impl RoundVotes { } } + /// Return the prevotes for this round. pub fn prevotes(&self) -> &VoteCount { &self.prevotes } + /// Return the precommits for this round. pub fn precommits(&self) -> &VoteCount { &self.precommits } + /// Add a vote to the round, of the given type, from the given address, + /// with the given value and weight. pub fn add_vote( &mut self, vote_type: VoteType, @@ -43,6 +52,9 @@ impl RoundVotes { } } + /// Get the weight of the vote of the given type for the given value. + /// + /// If there is no vote for that value, return 0. pub fn get_weight(&self, vote_type: VoteType, value: &NilOrVal) -> Weight where Value: Ord, @@ -53,6 +65,7 @@ impl RoundVotes { } } + /// Get the sum of the weights of the votes of the given type. pub fn weight_sum(&self, vote_type: VoteType) -> Weight { match vote_type { VoteType::Prevote => self.prevotes.sum(), @@ -60,6 +73,7 @@ impl RoundVotes { } } + /// Get the sum of the weights of all votes, regardless of type, for the given value. pub fn combined_weight(&self, value: &NilOrVal) -> Weight where Value: Ord, diff --git a/Code/vote/src/round_weights.rs b/Code/vote/src/round_weights.rs index 77c3396aa..026bf7ed1 100644 --- a/Code/vote/src/round_weights.rs +++ b/Code/vote/src/round_weights.rs @@ -1,23 +1,29 @@ +//! For tracking the weight (ie. voting power) of each validator. + use alloc::collections::BTreeMap; use crate::Weight; +/// Keeps track of the weight (ie. voting power) of each validator. #[derive(Clone, Debug)] pub struct RoundWeights
{ map: BTreeMap, } impl
RoundWeights
{ + /// Create a new `RoundWeights` instance. pub fn new() -> Self { RoundWeights { map: BTreeMap::new(), } } + /// Return the inner map. pub fn get_inner(&self) -> &BTreeMap { &self.map } + /// Set the weight of the given address, if it is not already set. pub fn set_once(&mut self, address: Address, weight: Weight) where Address: Ord, @@ -25,6 +31,7 @@ impl
RoundWeights
{ self.map.entry(address).or_insert(weight); } + /// Get the weight of the given address. pub fn get(&self, address: &Address) -> Weight where Address: Ord, @@ -32,6 +39,7 @@ impl
RoundWeights
{ *self.map.get(address).unwrap_or(&0) } + /// Return the sum of the weights of all the addresses. pub fn sum(&self) -> Weight { self.map.values().sum() } diff --git a/Code/vote/src/value_weights.rs b/Code/vote/src/value_weights.rs index 7b1e355af..b7f89a63e 100644 --- a/Code/vote/src/value_weights.rs +++ b/Code/vote/src/value_weights.rs @@ -1,3 +1,5 @@ +//! A value and the weight of votes for it. + use alloc::collections::BTreeMap; use crate::Weight; @@ -9,6 +11,7 @@ pub struct ValuesWeights { } impl ValuesWeights { + /// Create a new `ValuesWeights` instance. pub fn new() -> ValuesWeights { ValuesWeights { value_weights: BTreeMap::new(),