From f53355f2a38de95c71ff65765630178eb08a4663 Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 7 Nov 2023 11:50:03 +0100 Subject: [PATCH 1/2] code: Events and SM improvements (#19) * Event name changes and other small fixes * Add event multiplexing in executor and fix tests * Fix formatting * Fix executor handling of new round * Fix executor initialization * Add test step for NewRound, fix precommit for nil quorum to emit PrecommitAny * Review comments * Remove dead code --------- Co-authored-by: Anca Zamfir --- Code/common/src/round.rs | 1 + Code/consensus/src/executor.rs | 40 +++++++++--- Code/round/src/events.rs | 29 +++++---- Code/round/src/state.rs | 17 +---- Code/round/src/state_machine.rs | 93 +++++++++++++++------------ Code/test/tests/consensus_executor.rs | 66 +++++++++++-------- Code/test/tests/round.rs | 5 +- Code/test/tests/vote_keeper.rs | 2 +- Code/vote/src/keeper.rs | 6 +- 9 files changed, 149 insertions(+), 110 deletions(-) diff --git a/Code/common/src/round.rs b/Code/common/src/round.rs index 79fd567a6..e3c4d7735 100644 --- a/Code/common/src/round.rs +++ b/Code/common/src/round.rs @@ -17,6 +17,7 @@ pub enum Round { impl Round { /// The initial, zero round. pub const INITIAL: Round = Round::new(0); + pub const NIL: Round = Round::new(-1); /// Create a new round. /// diff --git a/Code/consensus/src/executor.rs b/Code/consensus/src/executor.rs index 46e2b0769..af3a9b0de 100644 --- a/Code/consensus/src/executor.rs +++ b/Code/consensus/src/executor.rs @@ -36,7 +36,7 @@ where private_key: Secret>, address: Ctx::Address, validator_set: Ctx::ValidatorSet, - round: Round, + pub round: Round, votes: VoteKeeper, round_states: BTreeMap>, } @@ -50,6 +50,7 @@ where Vote(SignedVote), Decide(Round, Ctx::Value), ScheduleTimeout(Timeout), + NewRound(Round), } impl Executor @@ -64,7 +65,7 @@ where ) -> Self { let votes = VoteKeeper::new( height.clone(), - Round::INITIAL, + Round::NIL, validator_set.total_voting_power(), ); @@ -73,7 +74,7 @@ where private_key: Secret::new(private_key), address, validator_set, - round: Round::INITIAL, + round: Round::NIL, votes, round_states: BTreeMap::new(), } @@ -92,13 +93,9 @@ where match round_msg { RoundMessage::NewRound(round) => { - // TODO: check if we are the proposer - // XXX: Check if there is an existing state? - self.round_states - .insert(round, RoundState::default().new_round(round)); - - None + assert!(self.round < round); + Some(Message::NewRound(round)) } RoundMessage::Proposal(proposal) => { @@ -141,6 +138,11 @@ where RoundEvent::NewRound }; + assert!(self.round < round); + self.round_states + .insert(round, RoundState::default().new_round(round)); + self.round = round; + self.apply_event(round, event) } @@ -239,8 +241,26 @@ where let data = RoundData::new(round, &self.height, &self.address); + // Multiplex the event with the round state. + let mux_event = match event { + RoundEvent::PolkaValue(value_id) => match round_state.proposal { + Some(ref proposal) if proposal.value().id() == value_id => { + RoundEvent::ProposalAndPolkaCurrent(proposal.clone()) + } + _ => RoundEvent::PolkaAny, + }, + RoundEvent::PrecommitValue(value_id) => match round_state.proposal { + Some(ref proposal) if proposal.value().id() == value_id => { + RoundEvent::ProposalAndPrecommitValue(proposal.clone()) + } + _ => RoundEvent::PrecommitAny, + }, + + _ => event, + }; + // Apply the event to the round state machine - let transition = round_state.apply_event(&data, event); + let transition = round_state.apply_event(&data, mux_event); // Update state self.round_states.insert(round, transition.next_state); diff --git a/Code/round/src/events.rs b/Code/round/src/events.rs index 74c316be3..8ac2374ec 100644 --- a/Code/round/src/events.rs +++ b/Code/round/src/events.rs @@ -5,17 +5,20 @@ pub enum Event where Ctx: Context, { - NewRound, // Start a new round, not as proposer. - NewRoundProposer(Ctx::Value), // Start a new round and propose the Value. - Proposal(Ctx::Proposal), // Receive a proposal with possible polka round. - ProposalInvalid, // Receive an invalid proposal. - PolkaAny, // Receive +2/3 prevotes for anything. - PolkaNil, // Receive +2/3 prevotes for nil. - PolkaValue(ValueId), // Receive +2/3 prevotes for Value. - PrecommitAny, // Receive +2/3 precommits for anything. - PrecommitValue(ValueId), // Receive +2/3 precommits for Value. - RoundSkip, // Receive +1/3 votes from a higher round. - TimeoutPropose, // Timeout waiting for proposal. - TimeoutPrevote, // Timeout waiting for prevotes. - TimeoutPrecommit, // Timeout waiting for precommits. + NewRound, // Start a new round, not as proposer.L20 + NewRoundProposer(Ctx::Value), // Start a new round and propose the Value.L14 + Proposal(Ctx::Proposal), // Receive a proposal. L22 + L23 (valid) + ProposalAndPolkaPrevious(Ctx::Proposal), // Recieved a proposal and a polka value from a previous round. L28 + L29 (valid) + ProposalInvalid, // Receive an invalid proposal. L26 + L32 (invalid) + PolkaValue(ValueId), // Receive +2/3 prevotes for valueId. L44 + PolkaAny, // Receive +2/3 prevotes for anything. L34 + PolkaNil, // Receive +2/3 prevotes for nil. L44 + ProposalAndPolkaCurrent(Ctx::Proposal), // Receive +2/3 prevotes for Value in current round. L36 + PrecommitAny, // Receive +2/3 precommits for anything. L47 + ProposalAndPrecommitValue(Ctx::Proposal), // Receive +2/3 precommits for Value. L49 + PrecommitValue(ValueId), // Receive +2/3 precommits for ValueId. L51 + RoundSkip, // Receive +1/3 messages from a higher round. OneCorrectProcessInHigherRound, L55 + TimeoutPropose, // Timeout waiting for proposal. L57 + TimeoutPrevote, // Timeout waiting for prevotes. L61 + TimeoutPrecommit, // Timeout waiting for precommits. L65 } diff --git a/Code/round/src/state.rs b/Code/round/src/state.rs index 743ea684c..55ef389b4 100644 --- a/Code/round/src/state.rs +++ b/Code/round/src/state.rs @@ -76,25 +76,10 @@ where ..self } } - - pub fn next_step(self) -> Self { - let step = match self.step { - Step::NewRound => Step::Propose, - Step::Propose => Step::Prevote, - Step::Prevote => Step::Precommit, - _ => self.step, - }; - + pub fn with_step(self, step: Step) -> Self { Self { step, ..self } } - pub fn commit_step(self) -> Self { - Self { - step: Step::Commit, - ..self - } - } - pub fn set_locked(self, value: Ctx::Value) -> Self { Self { locked: Some(RoundValue::new(value, self.round)), diff --git a/Code/round/src/state_machine.rs b/Code/round/src/state_machine.rs index 50d80b4a1..dd9350b1c 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, ValueId}; +use malachite_common::{Context, Proposal, Round, TimeoutStep, Value}; use crate::events::Event; use crate::message::Message; @@ -78,25 +78,22 @@ where .map_or(true, |locked| &locked.value == proposal.value()) { state.proposal = Some(proposal.clone()); - prevote(state, data.address, proposal.round(), proposal.value().id()) + prevote(state, data.address, &proposal) } else { prevote_nil(state, data.address) } } - (Step::Propose, Event::Proposal(proposal)) + (Step::Propose, Event::ProposalAndPolkaPrevious(proposal)) if this_round && is_valid_pol_round(&state, proposal.pol_round()) => { // L28 let Some(locked) = state.locked.as_ref() else { - // TODO: Add logging - return Transition::invalid(state); + return prevote_nil(state, data.address); }; - if proposal.value().is_valid() - && (locked.round <= proposal.pol_round() || &locked.value == proposal.value()) - { - prevote(state, data.address, proposal.round(), proposal.value().id()) + if locked.round <= proposal.pol_round() || &locked.value == proposal.value() { + prevote(state, data.address, &proposal) } else { prevote_nil(state, data.address) } @@ -107,14 +104,14 @@ where // From Prevote. Event must be for current round. (Step::Prevote, Event::PolkaAny) if this_round => schedule_timeout_prevote(state), // L34 (Step::Prevote, Event::PolkaNil) if this_round => precommit_nil(state, data.address), // L44 - (Step::Prevote, Event::PolkaValue(value_id)) if this_round => { - precommit(state, data.address, value_id) // L36/L37 - NOTE: only once? + (Step::Prevote, Event::ProposalAndPolkaCurrent(proposal)) if this_round => { + precommit(state, data.address, proposal) // L36/L37 - NOTE: only once? } (Step::Prevote, Event::TimeoutPrevote) if this_round => precommit_nil(state, data.address), // L61 // From Precommit. Event must be for current round. - (Step::Precommit, Event::PolkaValue(value_id)) if this_round => { - set_valid_value(state, value_id) // L36/L42 - NOTE: only once? + (Step::Precommit, Event::ProposalAndPolkaCurrent(proposal)) if this_round => { + set_valid_value(state, proposal.value().clone()) // L36/L42 - NOTE: only once? } // From Commit. No more state transitions. @@ -124,7 +121,7 @@ where (_, Event::PrecommitAny) if this_round => schedule_timeout_precommit(state), // L47 (_, Event::TimeoutPrecommit) if this_round => round_skip(state, data.round.increment()), // L65 (_, Event::RoundSkip) if state.round < data.round => round_skip(state, data.round), // L55 - (_, Event::PrecommitValue(value_id)) => commit(state, data.round, value_id), // L49 + (_, Event::ProposalAndPrecommitValue(proposal)) => commit(state, data.round, proposal), // L49 // Invalid transition. _ => Transition::invalid(state), @@ -149,7 +146,7 @@ where }; let proposal = Message::proposal(height.clone(), state.round, value, pol_round); - Transition::to(state.next_step()).with_message(proposal) + Transition::to(state.with_step(Step::Propose)).with_message(proposal) } //--------------------------------------------------------------------- @@ -163,12 +160,13 @@ where pub fn prevote( state: State, address: &Ctx::Address, - vr: Round, - proposed: ValueId, + proposal: &Ctx::Proposal, ) -> Transition where Ctx: Context, { + 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 @@ -177,7 +175,7 @@ where }; let message = Message::prevote(state.round, value, address.clone()); - Transition::to(state.next_step()).with_message(message) + Transition::to(state.with_step(Step::Prevote)).with_message(message) } /// Received a complete proposal for an empty or invalid value, or timed out; prevote nil. @@ -188,7 +186,7 @@ where Ctx: Context, { let message = Message::prevote(state.round, None, address.clone()); - Transition::to(state.next_step()).with_message(message) + Transition::to(state.with_step(Step::Prevote)).with_message(message) } // --------------------------------------------------------------------- @@ -202,25 +200,34 @@ 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( - state: State, + mut state: State, address: &Ctx::Address, - value_id: ValueId, + proposal: Ctx::Proposal, ) -> Transition where Ctx: Context, { - let message = Message::precommit(state.round, Some(value_id), address.clone()); + if state.step != Step::Prevote { + return Transition::to(state.clone()); + } - let Some(value) = state - .proposal - .as_ref() - .map(|proposal| proposal.value().clone()) - else { - // TODO: Add logging - return Transition::invalid(state); + let value = proposal.value(); + let message = Message::precommit(state.round, Some(value.id()), address.clone()); + + let current_value = match state.proposal { + Some(ref proposal) => proposal.value().clone(), + None => { + state.proposal = Some(proposal.clone()); + proposal.value().clone() + } }; - let next = state.set_locked(value.clone()).set_valid(value).next_step(); + assert_eq!(current_value.id(), value.id()); + + let next = state + .set_locked(value.clone()) + .set_valid(value.clone()) + .with_step(Step::Precommit); Transition::to(next).with_message(message) } @@ -233,7 +240,7 @@ where Ctx: Context, { let message = Message::precommit(state.round, None, address.clone()); - Transition::to(state.next_step()).with_message(message) + Transition::to(state.with_step(Step::Precommit)).with_message(message) } // --------------------------------------------------------------------- @@ -248,7 +255,7 @@ where Ctx: Context, { let timeout = Message::schedule_timeout(state.round, TimeoutStep::Propose); - Transition::to(state.next_step()).with_message(timeout) + Transition::to(state.with_step(Step::Propose)).with_message(timeout) } /// We received a polka for any; schedule timeout prevote. @@ -261,8 +268,12 @@ pub fn schedule_timeout_prevote(state: State) -> Transition where Ctx: Context, { - let message = Message::schedule_timeout(state.round, TimeoutStep::Prevote); - Transition::to(state.next_step()).with_message(message) + if state.step == Step::Prevote { + let message = Message::schedule_timeout(state.round, TimeoutStep::Prevote); + Transition::to(state).with_message(message) + } else { + Transition::to(state) + } } /// We received +2/3 precommits for any; schedule timeout precommit. @@ -273,20 +284,20 @@ where Ctx: Context, { let message = Message::schedule_timeout(state.round, TimeoutStep::Precommit); - Transition::to(state.next_step()).with_message(message) + Transition::to(state).with_message(message) } //--------------------------------------------------------------------- // Set the valid value. //--------------------------------------------------------------------- -/// We received a polka for a value after we already precommited. +/// We received a polka for a value after we already precommitted. /// Set the valid value and current round. /// /// Ref: L36/L42 /// /// NOTE: only one of this and precommit should be called once in a round -pub fn set_valid_value(state: State, value_id: ValueId) -> Transition +pub fn set_valid_value(state: State, value: Ctx::Value) -> Transition where Ctx: Context, { @@ -296,7 +307,7 @@ where return Transition::invalid(state); }; - if locked.value.id() != value_id { + if locked.value.id() != value.id() { // TODO: Add logging return Transition::invalid(state); } @@ -322,7 +333,7 @@ where /// We received +2/3 precommits for a value - commit and decide that value! /// /// Ref: L49 -pub fn commit(state: State, round: Round, value_id: ValueId) -> Transition +pub fn commit(state: State, round: Round, proposal: Ctx::Proposal) -> Transition where Ctx: Context, { @@ -332,11 +343,11 @@ where return Transition::invalid(state); }; - if locked.value.id() != value_id { + if locked.value.id() != proposal.value().id() { // TODO: Add logging return Transition::invalid(state); } let message = Message::decision(round, locked.value.clone()); - Transition::to(state.commit_step()).with_message(message) + Transition::to(state.with_step(Step::Commit)).with_message(message) } diff --git a/Code/test/tests/consensus_executor.rs b/Code/test/tests/consensus_executor.rs index e89afb9b2..821eb0fb7 100644 --- a/Code/test/tests/consensus_executor.rs +++ b/Code/test/tests/consensus_executor.rs @@ -21,6 +21,7 @@ fn to_input_msg(output: Message) -> Option> { Message::Vote(v) => Some(Event::Vote(v)), Message::Decide(_, _) => None, Message::ScheduleTimeout(_) => None, + Message::NewRound(round) => Some(Event::NewRound(round)), } } @@ -387,7 +388,7 @@ fn executor_steps_not_proposer() { } #[test] -fn executor_steps_not_proposer_timeout() { +fn executor_steps_not_proposer_timeout_multiple_rounds() { let value = TestContext::DUMMY_VALUE; // TODO: get value from external source let value_id = value.id(); @@ -402,19 +403,19 @@ fn executor_steps_not_proposer_timeout() { let addr3 = Address::from_public_key(&sk3.public_key()); let v1 = Validator::new(sk1.public_key(), 1); - let v2 = Validator::new(sk2.public_key(), 1); - let v3 = Validator::new(sk3.public_key(), 3); + let v2 = Validator::new(sk2.public_key(), 3); + let v3 = Validator::new(sk3.public_key(), 1); - // Proposer is v1, so we are not the proposer - let (my_sk, my_addr) = (sk2, addr2); + // Proposer is v1, so we, v3, are not the proposer + let (my_sk, my_addr) = (sk3, addr3); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); let mut executor = Executor::new(Height::new(1), vs, my_sk.clone(), my_addr); let steps = vec![ - // Start round 0, we are not the proposer + // Start round 0, we, v3, are not the proposer TestStep { - desc: "Start round 0, we are not the proposer", + desc: "Start round 0, we, v3, are not the proposer", input_event: Some(Event::NewRound(Round::new(0))), expected_output: Some(Message::ScheduleTimeout(Timeout::propose(Round::new(0)))), new_state: State { @@ -425,9 +426,9 @@ fn executor_steps_not_proposer_timeout() { valid: None, }, }, - // Receive a propose timeout, prevote for nil (v1) + // Receive a propose timeout, prevote for nil (from v3) TestStep { - desc: "Receive a propose timeout, prevote for nil (v1)", + desc: "Receive a propose timeout, prevote for nil (v3)", input_event: Some(Event::TimeoutElapsed(Timeout::propose(Round::new(0)))), expected_output: Some(Message::Vote( Vote::new_prevote(Round::new(0), None, my_addr).signed(&my_sk), @@ -440,9 +441,9 @@ fn executor_steps_not_proposer_timeout() { valid: None, }, }, - // Receive our own prevote v1 + // Receive our own prevote v3 TestStep { - desc: "Receive our own prevote v1", + desc: "Receive our own prevote v3", input_event: None, expected_output: None, new_state: State { @@ -453,9 +454,9 @@ fn executor_steps_not_proposer_timeout() { valid: None, }, }, - // v2 prevotes for its own proposal + // v1 prevotes for its own proposal TestStep { - desc: "v2 prevotes for its own proposal", + desc: "v1 prevotes for its own proposal", input_event: Some(Event::Vote( Vote::new_prevote(Round::new(0), Some(value_id), addr1).signed(&sk1), )), @@ -468,11 +469,11 @@ fn executor_steps_not_proposer_timeout() { valid: None, }, }, - // v3 prevotes for nil, it gets +2/3 prevotes, precommit for it (v1) + // v2 prevotes for nil, we get +2/3 nil prevotes and precommit for nil TestStep { - desc: "v3 prevotes for nil, it gets +2/3 prevotes, precommit for it (v1)", + desc: "v2 prevotes for nil, we get +2/3 prevotes, precommit for nil", input_event: Some(Event::Vote( - Vote::new_prevote(Round::new(0), None, addr3).signed(&sk3), + Vote::new_prevote(Round::new(0), None, addr2).signed(&sk2), )), expected_output: Some(Message::Vote( Vote::new_precommit(Round::new(0), None, my_addr).signed(&my_sk), @@ -485,9 +486,9 @@ fn executor_steps_not_proposer_timeout() { valid: None, }, }, - // v1 receives its own precommit + // v3 receives its own precommit TestStep { - desc: "v1 receives its own precommit", + desc: "v3 receives its own precommit", input_event: None, expected_output: None, new_state: State { @@ -498,9 +499,9 @@ fn executor_steps_not_proposer_timeout() { valid: None, }, }, - // v2 precommits its proposal + // v1 precommits its proposal TestStep { - desc: "v2 precommits its proposal", + desc: "v1 precommits its proposal", input_event: Some(Event::Vote( Vote::new_precommit(Round::new(0), Some(value_id), addr1).signed(&sk1), )), @@ -513,13 +514,13 @@ fn executor_steps_not_proposer_timeout() { valid: None, }, }, - // v3 precommits for nil + // v2 precommits for nil TestStep { - desc: "v3 precommits for nil", + desc: "v2 precommits for nil", input_event: Some(Event::Vote( - Vote::new_precommit(Round::new(0), None, addr3).signed(&sk3), + Vote::new_precommit(Round::new(0), None, addr2).signed(&sk2), )), - expected_output: None, + expected_output: Some(Message::ScheduleTimeout(Timeout::precommit(Round::new(0)))), new_state: State { round: Round::new(0), step: Step::Precommit, @@ -532,7 +533,7 @@ fn executor_steps_not_proposer_timeout() { TestStep { desc: "we receive a precommit timeout, start a new round", input_event: Some(Event::TimeoutElapsed(Timeout::precommit(Round::new(0)))), - expected_output: None, + expected_output: Some(Message::NewRound(Round::new(1))), new_state: State { round: Round::new(1), step: Step::NewRound, @@ -541,6 +542,18 @@ fn executor_steps_not_proposer_timeout() { valid: None, }, }, + TestStep { + desc: "Start round 1, we are not the proposer", + input_event: Some(Event::NewRound(Round::new(1))), + expected_output: Some(Message::ScheduleTimeout(Timeout::propose(Round::new(1)))), + new_state: State { + round: Round::new(1), + step: Step::Propose, + proposal: None, + locked: None, + valid: None, + }, + }, ]; let mut previous_message = None; @@ -555,7 +568,8 @@ fn executor_steps_not_proposer_timeout() { let output = executor.execute(execute_message); assert_eq!(output, step.expected_output, "expected output message"); - let new_state = executor.round_state(Round::new(0)).unwrap(); + // TODO - add expected executor round to test and assert before the new_state check below + let new_state = executor.round_state(executor.round).unwrap(); assert_eq!(new_state, &step.new_state, "new state"); previous_message = output.and_then(to_input_msg); diff --git a/Code/test/tests/round.rs b/Code/test/tests/round.rs index f90c13ea5..4f0dff9e0 100644 --- a/Code/test/tests/round.rs +++ b/Code/test/tests/round.rs @@ -12,9 +12,12 @@ const ADDRESS: Address = Address::new([42; 20]); fn test_propose() { let value = Value::new(42); let height = Height::new(10); + let round = Round::new(0); let mut state: State = State::default(); - let data = RoundData::new(Round::new(0), &height, &ADDRESS); + state.round = round; + + let data = RoundData::new(round, &height, &ADDRESS); let transition = apply_event(state.clone(), &data, Event::NewRoundProposer(value)); diff --git a/Code/test/tests/vote_keeper.rs b/Code/test/tests/vote_keeper.rs index 77eeae569..2eed7ad6a 100644 --- a/Code/test/tests/vote_keeper.rs +++ b/Code/test/tests/vote_keeper.rs @@ -34,7 +34,7 @@ fn precommit_apply_nil() { assert_eq!(msg, None); let msg = keeper.apply_vote(vote, 1); - assert_eq!(msg, None); + assert_eq!(msg, Some(Message::PrecommitAny)); } #[test] diff --git a/Code/vote/src/keeper.rs b/Code/vote/src/keeper.rs index 6149e3927..1240dcc70 100644 --- a/Code/vote/src/keeper.rs +++ b/Code/vote/src/keeper.rs @@ -35,7 +35,9 @@ where pub fn new(height: Ctx::Height, round: Round, total_weight: Weight) -> Self { let mut rounds = BTreeMap::new(); - rounds.insert(round, RoundVotes::new(height.clone(), round, total_weight)); + if round != Round::NIL { + rounds.insert(round, RoundVotes::new(height.clone(), round, total_weight)); + } VoteKeeper { height, @@ -87,7 +89,7 @@ where (VoteType::Prevote, Threshold::Value(v)) => Some(Message::PolkaValue(v)), (VoteType::Precommit, Threshold::Any) => Some(Message::PrecommitAny), - (VoteType::Precommit, Threshold::Nil) => None, + (VoteType::Precommit, Threshold::Nil) => Some(Message::PrecommitAny), (VoteType::Precommit, Threshold::Value(v)) => Some(Message::PrecommitValue(v)), } } From 52d51a4604c6a11428325fa54b9696c521e3dece Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Tue, 7 Nov 2023 13:24:47 +0100 Subject: [PATCH 2/2] vote: Complete vote keeper (#39) * Rename `Threshold::Init` to `Unreached` * WIP: Pass votes directly to `VoteCount` * Cleanup * Split `compute_threshold` out of `add_vote` * Only count a single vote per validator address * Emit `PrecommitAny` message when we reach `Nil` threshold for precommits * Doc comment * Refactor `VoteCount` implementation * Fix tests * Split `vote` crate into more modules * Add Skip threshold * Refactor data kept by the `VoteKeeper` per-round * Move dealing with skip threshold into `VoteKeeper` * Rename `Event::RoundSkip` to `Event::SkipRound` * Cleanup * Include number of round to skip to in `SkipRound` message * Parametrize over thresholds --- Code/common/src/round.rs | 2 +- Code/common/src/validator_set.rs | 2 +- Code/consensus/src/executor.rs | 9 +- Code/round/src/events.rs | 8 +- Code/round/src/state_machine.rs | 2 +- Code/test/src/value.rs | 12 ++ Code/test/tests/consensus_executor.rs | 3 +- Code/test/tests/round.rs | 6 +- Code/test/tests/round_votes.rs | 74 +++---- Code/test/tests/vote_count.rs | 155 ++++++++++++++ Code/test/tests/vote_keeper.rs | 37 ++-- Code/vote/src/count.rs | 297 ++++++++++++++------------ Code/vote/src/keeper.rs | 139 ++++++++---- Code/vote/src/lib.rs | 89 +++++--- Code/vote/src/round_votes.rs | 47 ++++ Code/vote/src/round_weights.rs | 40 ++++ Code/vote/src/value_weights.rs | 82 +++++++ 17 files changed, 726 insertions(+), 278 deletions(-) create mode 100644 Code/test/tests/vote_count.rs create mode 100644 Code/vote/src/round_votes.rs create mode 100644 Code/vote/src/round_weights.rs create mode 100644 Code/vote/src/value_weights.rs diff --git a/Code/common/src/round.rs b/Code/common/src/round.rs index e3c4d7735..7b7e6e879 100644 --- a/Code/common/src/round.rs +++ b/Code/common/src/round.rs @@ -5,7 +5,7 @@ use core::cmp; /// Can be either: /// - `Round::Nil` (ie. `-1`) /// - `Round::Some(r)` where `r >= 0` -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub enum Round { /// No round, ie. `-1` Nil, diff --git a/Code/common/src/validator_set.rs b/Code/common/src/validator_set.rs index 092ac4dc8..0c21dccf9 100644 --- a/Code/common/src/validator_set.rs +++ b/Code/common/src/validator_set.rs @@ -12,7 +12,7 @@ pub type VotingPower = u64; /// TODO: Keep this trait or just add the bounds to Consensus::Address? pub trait Address where - Self: Clone + Debug + PartialEq + Eq, + Self: Clone + Debug + Eq + Ord, { } diff --git a/Code/consensus/src/executor.rs b/Code/consensus/src/executor.rs index af3a9b0de..029dd77c4 100644 --- a/Code/consensus/src/executor.rs +++ b/Code/consensus/src/executor.rs @@ -11,9 +11,9 @@ use malachite_common::{ use malachite_round::events::Event as RoundEvent; use malachite_round::message::Message as RoundMessage; use malachite_round::state::State as RoundState; -use malachite_vote::count::Threshold; use malachite_vote::keeper::Message as VoteMessage; use malachite_vote::keeper::VoteKeeper; +use malachite_vote::Threshold; /// Messages that can be received and broadcast by the consensus executor. #[derive(Clone, Debug, PartialEq, Eq)] @@ -63,11 +63,7 @@ where private_key: PrivateKey, address: Ctx::Address, ) -> Self { - let votes = VoteKeeper::new( - height.clone(), - Round::NIL, - validator_set.total_voting_power(), - ); + let votes = VoteKeeper::new(validator_set.total_voting_power()); Self { height, @@ -219,6 +215,7 @@ where VoteMessage::PolkaValue(v) => RoundEvent::PolkaValue(v), VoteMessage::PrecommitAny => RoundEvent::PrecommitAny, VoteMessage::PrecommitValue(v) => RoundEvent::PrecommitValue(v), + VoteMessage::SkipRound(r) => RoundEvent::SkipRound(r), }; self.apply_event(round, round_event) diff --git a/Code/round/src/events.rs b/Code/round/src/events.rs index 8ac2374ec..a0e1e4bba 100644 --- a/Code/round/src/events.rs +++ b/Code/round/src/events.rs @@ -1,4 +1,4 @@ -use malachite_common::{Context, ValueId}; +use malachite_common::{Context, Round, ValueId}; #[derive(Clone, Debug, PartialEq, Eq)] pub enum Event @@ -17,8 +17,8 @@ where PrecommitAny, // Receive +2/3 precommits for anything. L47 ProposalAndPrecommitValue(Ctx::Proposal), // Receive +2/3 precommits for Value. L49 PrecommitValue(ValueId), // Receive +2/3 precommits for ValueId. L51 - RoundSkip, // Receive +1/3 messages from a higher round. OneCorrectProcessInHigherRound, L55 - TimeoutPropose, // Timeout waiting for proposal. L57 - TimeoutPrevote, // Timeout waiting for prevotes. L61 + SkipRound(Round), // Receive +1/3 messages from a higher round. OneCorrectProcessInHigherRound, L55 + TimeoutPropose, // Timeout waiting for proposal. L57 + TimeoutPrevote, // Timeout waiting for prevotes. L61 TimeoutPrecommit, // Timeout waiting for precommits. L65 } diff --git a/Code/round/src/state_machine.rs b/Code/round/src/state_machine.rs index dd9350b1c..ade9d53e0 100644 --- a/Code/round/src/state_machine.rs +++ b/Code/round/src/state_machine.rs @@ -120,7 +120,7 @@ where // From all (except Commit). Various round guards. (_, Event::PrecommitAny) if this_round => schedule_timeout_precommit(state), // L47 (_, Event::TimeoutPrecommit) if this_round => round_skip(state, data.round.increment()), // L65 - (_, Event::RoundSkip) if state.round < data.round => round_skip(state, data.round), // L55 + (_, Event::SkipRound(round)) if state.round < round => round_skip(state, round), // L55 (_, Event::ProposalAndPrecommitValue(proposal)) => commit(state, data.round, proposal), // L49 // Invalid transition. diff --git a/Code/test/src/value.rs b/Code/test/src/value.rs index 3954d2868..47b5736bc 100644 --- a/Code/test/src/value.rs +++ b/Code/test/src/value.rs @@ -11,6 +11,12 @@ impl ValueId { } } +impl From for ValueId { + fn from(value: u64) -> Self { + Self::new(value) + } +} + /// The value to decide on #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Value(u64); @@ -44,3 +50,9 @@ impl malachite_common::Value for Value { self.id() } } + +impl From for Value { + fn from(value: u64) -> Self { + Self::new(value) + } +} diff --git a/Code/test/tests/consensus_executor.rs b/Code/test/tests/consensus_executor.rs index 821eb0fb7..b17cbca88 100644 --- a/Code/test/tests/consensus_executor.rs +++ b/Code/test/tests/consensus_executor.rs @@ -168,7 +168,7 @@ fn executor_steps_proposer() { TestStep { desc: "v3 precommits for our proposal, we get +2/3 precommits, decide it (v1)", input_event: Some(Event::Vote( - Vote::new_precommit(Round::new(0), Some(value_id), addr2).signed(&sk2), + Vote::new_precommit(Round::new(0), Some(value_id), addr3).signed(&sk3), )), expected_output: Some(Message::Decide(Round::new(0), value.clone())), new_state: State { @@ -571,6 +571,7 @@ fn executor_steps_not_proposer_timeout_multiple_rounds() { // TODO - add expected executor round to test and assert before the new_state check below let new_state = executor.round_state(executor.round).unwrap(); assert_eq!(new_state, &step.new_state, "new state"); + assert_eq!(output, step.expected_output, "expected output message"); previous_message = output.and_then(to_input_msg); } diff --git a/Code/test/tests/round.rs b/Code/test/tests/round.rs index 4f0dff9e0..9f9b606fc 100644 --- a/Code/test/tests/round.rs +++ b/Code/test/tests/round.rs @@ -14,8 +14,10 @@ fn test_propose() { let height = Height::new(10); let round = Round::new(0); - let mut state: State = State::default(); - state.round = round; + let mut state: State = State { + round, + ..Default::default() + }; let data = RoundData::new(round, &height, &ADDRESS); diff --git a/Code/test/tests/round_votes.rs b/Code/test/tests/round_votes.rs index fb83ca498..8be3f46e4 100644 --- a/Code/test/tests/round_votes.rs +++ b/Code/test/tests/round_votes.rs @@ -1,29 +1,32 @@ -use malachite_common::Round; -use malachite_vote::count::Threshold; -use malachite_vote::RoundVotes; +use malachite_common::VoteType; +use malachite_vote::round_votes::RoundVotes; +use malachite_vote::Threshold; -use malachite_test::{Address, Height, TestContext, ValueId, Vote}; +use malachite_test::{Address, ValueId}; -const ADDRESS: Address = Address::new([42; 20]); +const ADDRESS1: Address = Address::new([41; 20]); +const ADDRESS2: Address = Address::new([42; 20]); +const ADDRESS3: Address = Address::new([43; 20]); +const ADDRESS4: Address = Address::new([44; 20]); +const ADDRESS5: Address = Address::new([45; 20]); +const ADDRESS6: Address = Address::new([46; 20]); #[test] fn add_votes_nil() { let total = 3; - let mut round_votes: RoundVotes = - RoundVotes::new(Height::new(1), Round::new(0), total); + let mut round_votes: RoundVotes<_, ValueId> = RoundVotes::new(total, Default::default()); // add a vote for nil. nothing changes. - let vote = Vote::new_prevote(Round::new(0), None, ADDRESS); - let thresh = round_votes.add_vote(vote.clone(), 1); - assert_eq!(thresh, Threshold::Init); + let thresh = round_votes.add_vote(VoteType::Prevote, ADDRESS1, None, 1); + assert_eq!(thresh, Threshold::Unreached); // add it again, nothing changes. - let thresh = round_votes.add_vote(vote.clone(), 1); - assert_eq!(thresh, Threshold::Init); + let thresh = round_votes.add_vote(VoteType::Prevote, ADDRESS2, None, 1); + assert_eq!(thresh, Threshold::Unreached); // add it again, get Nil - let thresh = round_votes.add_vote(vote.clone(), 1); + let thresh = round_votes.add_vote(VoteType::Prevote, ADDRESS3, None, 1); assert_eq!(thresh, Threshold::Nil); } @@ -34,25 +37,22 @@ fn add_votes_single_value() { let total = 4; let weight = 1; - let mut round_votes: RoundVotes = - RoundVotes::new(Height::new(1), Round::new(0), total); + let mut round_votes: RoundVotes<_, ValueId> = RoundVotes::new(total, Default::default()); // add a vote. nothing changes. - let vote = Vote::new_prevote(Round::new(0), val, ADDRESS); - let thresh = round_votes.add_vote(vote.clone(), weight); - assert_eq!(thresh, Threshold::Init); + let thresh = round_votes.add_vote(VoteType::Prevote, ADDRESS1, val, weight); + assert_eq!(thresh, Threshold::Unreached); // add it again, nothing changes. - let thresh = round_votes.add_vote(vote.clone(), weight); - assert_eq!(thresh, Threshold::Init); + let thresh = round_votes.add_vote(VoteType::Prevote, ADDRESS2, val, weight); + assert_eq!(thresh, Threshold::Unreached); // add a vote for nil, get Thresh::Any - let vote_nil = Vote::new_prevote(Round::new(0), None, ADDRESS); - let thresh = round_votes.add_vote(vote_nil, weight); + let thresh = round_votes.add_vote(VoteType::Prevote, ADDRESS3, None, weight); assert_eq!(thresh, Threshold::Any); // add vote for value, get Thresh::Value - let thresh = round_votes.add_vote(vote, weight); + let thresh = round_votes.add_vote(VoteType::Prevote, ADDRESS4, val, weight); assert_eq!(thresh, Threshold::Value(v)); } @@ -64,33 +64,29 @@ fn add_votes_multi_values() { let val2 = Some(v2); let total = 15; - let mut round_votes: RoundVotes = - RoundVotes::new(Height::new(1), Round::new(0), total); + let mut round_votes: RoundVotes<_, ValueId> = RoundVotes::new(total, Default::default()); // add a vote for v1. nothing changes. - let vote1 = Vote::new_precommit(Round::new(0), val1, ADDRESS); - let thresh = round_votes.add_vote(vote1.clone(), 1); - assert_eq!(thresh, Threshold::Init); + let thresh = round_votes.add_vote(VoteType::Precommit, ADDRESS1, val1, 1); + assert_eq!(thresh, Threshold::Unreached); // add a vote for v2. nothing changes. - let vote2 = Vote::new_precommit(Round::new(0), val2, ADDRESS); - let thresh = round_votes.add_vote(vote2.clone(), 1); - assert_eq!(thresh, Threshold::Init); + let thresh = round_votes.add_vote(VoteType::Precommit, ADDRESS2, val2, 1); + assert_eq!(thresh, Threshold::Unreached); // add a vote for nil. nothing changes. - let vote_nil = Vote::new_precommit(Round::new(0), None, ADDRESS); - let thresh = round_votes.add_vote(vote_nil.clone(), 1); - assert_eq!(thresh, Threshold::Init); + let thresh = round_votes.add_vote(VoteType::Precommit, ADDRESS3, None, 1); + assert_eq!(thresh, Threshold::Unreached); // add a vote for v1. nothing changes - let thresh = round_votes.add_vote(vote1.clone(), 1); - assert_eq!(thresh, Threshold::Init); + let thresh = round_votes.add_vote(VoteType::Precommit, ADDRESS4, val1, 1); + assert_eq!(thresh, Threshold::Unreached); // add a vote for v2. nothing changes - let thresh = round_votes.add_vote(vote2.clone(), 1); - assert_eq!(thresh, Threshold::Init); + let thresh = round_votes.add_vote(VoteType::Precommit, ADDRESS5, val2, 1); + assert_eq!(thresh, Threshold::Unreached); // add a big vote for v2. get Value(v2) - let thresh = round_votes.add_vote(vote2.clone(), 10); + let thresh = round_votes.add_vote(VoteType::Precommit, ADDRESS6, val2, 10); assert_eq!(thresh, Threshold::Value(v2)); } diff --git a/Code/test/tests/vote_count.rs b/Code/test/tests/vote_count.rs new file mode 100644 index 000000000..179222dda --- /dev/null +++ b/Code/test/tests/vote_count.rs @@ -0,0 +1,155 @@ +#![allow(clippy::bool_assert_comparison)] + +use malachite_vote::count::VoteCount; +use malachite_vote::Threshold; + +#[test] +fn vote_count_nil() { + let mut vc = VoteCount::new(4, Default::default()); + + let addr1 = [1]; + let addr2 = [2]; + let addr3 = [3]; + let addr4 = [4]; + + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr1, None, 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 1); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr2, None, 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 2); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + // addr1 votes again, is ignored + assert_eq!(vc.add(addr1, None, 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 2); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr3, None, 1), Threshold::Nil); + assert_eq!(vc.get(&None), 3); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr4, Some(1), 1), Threshold::Any); + assert_eq!(vc.get(&None), 3); + assert_eq!(vc.get(&Some(1)), 1); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); +} + +#[test] +fn vote_count_value() { + let mut vc = VoteCount::new(4, Default::default()); + + let addr1 = [1]; + let addr2 = [2]; + let addr3 = [3]; + let addr4 = [4]; + + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr1, Some(1), 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 1); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr2, Some(1), 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 2); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + // addr1 votes again, for nil this time, is ignored + assert_eq!(vc.add(addr1, None, 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 2); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr3, Some(1), 1), Threshold::Value(1)); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 3); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + // addr2 votes again, for the same value, is ignored + assert_eq!(vc.add(addr2, Some(1), 1), Threshold::Value(1)); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 3); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr4, Some(2), 1), Threshold::Any); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 3); + assert_eq!(vc.get(&Some(2)), 1); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + // addr4 votes again, for a different value, is ignored + assert_eq!(vc.add(addr4, Some(3), 1), Threshold::Any); + 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.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); +} diff --git a/Code/test/tests/vote_keeper.rs b/Code/test/tests/vote_keeper.rs index 2eed7ad6a..8dedc2b3c 100644 --- a/Code/test/tests/vote_keeper.rs +++ b/Code/test/tests/vote_keeper.rs @@ -1,82 +1,91 @@ use malachite_common::Round; use malachite_vote::keeper::{Message, VoteKeeper}; -use malachite_test::{Address, Height, TestContext, ValueId, Vote}; +use malachite_test::{Address, TestContext, ValueId, Vote}; -const ADDRESS: Address = Address::new([42; 20]); +const ADDRESS1: Address = Address::new([41; 20]); +const ADDRESS2: Address = Address::new([42; 20]); +const ADDRESS3: Address = Address::new([43; 20]); +const ADDRESS4: Address = Address::new([44; 20]); #[test] fn prevote_apply_nil() { - let mut keeper: VoteKeeper = VoteKeeper::new(Height::new(1), Round::INITIAL, 3); - - let vote = Vote::new_prevote(Round::new(0), None, ADDRESS); + let mut keeper: VoteKeeper = VoteKeeper::new(3); + let vote = Vote::new_prevote(Round::new(0), None, ADDRESS1); let msg = keeper.apply_vote(vote.clone(), 1); assert_eq!(msg, None); + let vote = Vote::new_prevote(Round::new(0), None, ADDRESS2); let msg = keeper.apply_vote(vote.clone(), 1); assert_eq!(msg, None); + let vote = Vote::new_prevote(Round::new(0), None, ADDRESS3); let msg = keeper.apply_vote(vote, 1); assert_eq!(msg, Some(Message::PolkaNil)); } #[test] fn precommit_apply_nil() { - let mut keeper: VoteKeeper = VoteKeeper::new(Height::new(1), Round::INITIAL, 3); - - let vote = Vote::new_precommit(Round::new(0), None, ADDRESS); + let mut keeper: VoteKeeper = VoteKeeper::new(3); + let vote = Vote::new_precommit(Round::new(0), None, ADDRESS1); let msg = keeper.apply_vote(vote.clone(), 1); assert_eq!(msg, None); + let vote = Vote::new_precommit(Round::new(0), None, ADDRESS2); let msg = keeper.apply_vote(vote.clone(), 1); assert_eq!(msg, None); + let vote = Vote::new_precommit(Round::new(0), None, ADDRESS3); let msg = keeper.apply_vote(vote, 1); assert_eq!(msg, Some(Message::PrecommitAny)); } #[test] fn prevote_apply_single_value() { - let mut keeper: VoteKeeper = VoteKeeper::new(Height::new(1), Round::INITIAL, 4); + let mut keeper: VoteKeeper = VoteKeeper::new(4); let v = ValueId::new(1); let val = Some(v); - let vote = Vote::new_prevote(Round::new(0), val, ADDRESS); + let vote = Vote::new_prevote(Round::new(0), val, ADDRESS1); let msg = keeper.apply_vote(vote.clone(), 1); assert_eq!(msg, None); + let vote = Vote::new_prevote(Round::new(0), val, ADDRESS2); let msg = keeper.apply_vote(vote.clone(), 1); assert_eq!(msg, None); - let vote_nil = Vote::new_prevote(Round::new(0), None, ADDRESS); + let vote_nil = Vote::new_prevote(Round::new(0), None, ADDRESS3); let msg = keeper.apply_vote(vote_nil, 1); assert_eq!(msg, Some(Message::PolkaAny)); + let vote = Vote::new_prevote(Round::new(0), val, ADDRESS4); let msg = keeper.apply_vote(vote, 1); assert_eq!(msg, Some(Message::PolkaValue(v))); } #[test] fn precommit_apply_single_value() { - let mut keeper: VoteKeeper = VoteKeeper::new(Height::new(1), Round::INITIAL, 4); + let mut keeper: VoteKeeper = VoteKeeper::new(4); let v = ValueId::new(1); let val = Some(v); - let vote = Vote::new_precommit(Round::new(0), val, ADDRESS); + let vote = Vote::new_precommit(Round::new(0), val, ADDRESS1); let msg = keeper.apply_vote(vote.clone(), 1); assert_eq!(msg, None); + let vote = Vote::new_precommit(Round::new(0), val, ADDRESS2); let msg = keeper.apply_vote(vote.clone(), 1); assert_eq!(msg, None); - let vote_nil = Vote::new_precommit(Round::new(0), None, ADDRESS); + let vote_nil = Vote::new_precommit(Round::new(0), None, ADDRESS3); let msg = keeper.apply_vote(vote_nil, 1); assert_eq!(msg, Some(Message::PrecommitAny)); + let vote = Vote::new_precommit(Round::new(0), val, ADDRESS4); let msg = keeper.apply_vote(vote, 1); assert_eq!(msg, Some(Message::PrecommitValue(v))); } diff --git a/Code/vote/src/count.rs b/Code/vote/src/count.rs index b124e0b26..5c7b66dcc 100644 --- a/Code/vote/src/count.rs +++ b/Code/vote/src/count.rs @@ -1,90 +1,77 @@ -use alloc::collections::BTreeMap; +use alloc::collections::BTreeSet; -// TODO: Introduce newtype -// QUESTION: Over what type? i64? -pub type Weight = u64; - -/// A value and the weight of votes for it. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ValuesWeights { - value_weights: BTreeMap, Weight>, -} - -impl ValuesWeights { - pub fn new() -> ValuesWeights { - ValuesWeights { - value_weights: BTreeMap::new(), - } - } - - /// Add weight to the value and return the new weight. - pub fn add(&mut self, value: Option, weight: Weight) -> Weight - where - ValueId: Ord, - { - let entry = self.value_weights.entry(value).or_insert(0); - *entry += weight; // FIXME: Deal with overflows - *entry - } - - /// Return the weight of the value, or 0 if it is not present. - pub fn get(&self, value: &Option) -> Weight - where - ValueId: Ord, - { - self.value_weights.get(value).cloned().unwrap_or(0) - } - - /// Return the sum of the weights of all values. - pub fn sum(&self) -> Weight { - self.value_weights.values().sum() // FIXME: Deal with overflows - } -} - -impl Default for ValuesWeights { - fn default() -> Self { - Self::new() - } -} +use crate::value_weights::ValuesWeights; +use crate::{Threshold, ThresholdParams, 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 { +pub struct VoteCount { + /// Total weight + pub total_weight: Weight, + + /// The threshold parameters + pub threshold_params: ThresholdParams, + /// Weight of votes for the values, including nil - pub values_weights: ValuesWeights, + pub values_weights: ValuesWeights>, - /// Total weight - pub total: Weight, + /// Addresses of validators who voted for the values + pub validator_addresses: BTreeSet
, } -impl VoteCount { - pub fn new(total: Weight) -> Self { +impl VoteCount { + pub fn new(total_weight: Weight, threshold_params: ThresholdParams) -> Self { VoteCount { - total, + total_weight, + threshold_params, values_weights: ValuesWeights::new(), + validator_addresses: BTreeSet::new(), } } - /// Add vote for a vlaue to internal counters and return the highest threshold. - pub fn add_vote(&mut self, value: Option, weight: Weight) -> Threshold + /// 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, + ) -> Threshold where + Address: Clone + Ord, Value: Clone + Ord, { - let new_weight = self.values_weights.add(value.clone(), weight); + let already_voted = !self.validator_addresses.insert(address); + + if !already_voted { + self.values_weights.add(value.clone(), weight); + } + + self.compute_threshold(value) + } + + /// Compute whether or not we have reached a threshold for the given value, + /// and return that threshold. + pub fn compute_threshold(&self, value: Option) -> Threshold + where + Address: Ord, + Value: Ord, + { + let weight = self.values_weights.get(&value); match value { - Some(value) if is_quorum(new_weight, self.total) => Threshold::Value(value), + Some(value) if self.is_quorum(weight, self.total_weight) => Threshold::Value(value), - None if is_quorum(new_weight, self.total) => Threshold::Nil, + None if self.is_quorum(weight, self.total_weight) => Threshold::Nil, _ => { let sum_weight = self.values_weights.sum(); - if is_quorum(sum_weight, self.total) { + if self.is_quorum(sum_weight, self.total_weight) { Threshold::Any } else { - Threshold::Init + Threshold::Unreached } } } @@ -93,120 +80,107 @@ impl VoteCount { /// Return whether or not the threshold is met, ie. if we have a quorum for that threshold. pub fn is_threshold_met(&self, threshold: Threshold) -> bool where - Value: Clone + Ord, + Value: Ord, { match threshold { Threshold::Value(value) => { let weight = self.values_weights.get(&Some(value)); - is_quorum(weight, self.total) + self.is_quorum(weight, self.total_weight) } Threshold::Nil => { let weight = self.values_weights.get(&None); - is_quorum(weight, self.total) + self.is_quorum(weight, self.total_weight) } Threshold::Any => { let sum_weight = self.values_weights.sum(); - is_quorum(sum_weight, self.total) + self.is_quorum(sum_weight, self.total_weight) } - Threshold::Init => false, + Threshold::Skip | Threshold::Unreached => false, } } -} -//------------------------------------------------------------------------- -// Round votes -//------------------------------------------------------------------------- - -// Thresh represents the different quorum thresholds. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Threshold { - /// No quorum - Init, // no quorum - /// Qorum of votes but not for the same value - Any, - /// Quorum for nil - Nil, - /// Quorum for a value - Value(ValueId), -} + pub fn get(&self, value: &Option) -> Weight + where + Value: Ord, + { + self.values_weights.get(value) + } + + pub fn total_weight(&self) -> Weight { + self.total_weight + } -/// Returns whether or note `value > (2/3)*total`. -fn is_quorum(value: Weight, total: Weight) -> bool { - 3 * value > 2 * total + fn is_quorum(&self, sum: Weight, total: Weight) -> bool { + self.threshold_params.quorum.is_met(sum, total) + } } #[cfg(test)] +#[allow(clippy::bool_assert_comparison)] mod tests { use super::*; #[test] - fn values_weights() { - let mut vw = ValuesWeights::new(); - - assert_eq!(vw.get(&None), 0); - assert_eq!(vw.get(&Some(1)), 0); - - assert_eq!(vw.add(None, 1), 1); - assert_eq!(vw.get(&None), 1); - assert_eq!(vw.get(&Some(1)), 0); - - assert_eq!(vw.add(Some(1), 1), 1); - assert_eq!(vw.get(&None), 1); - assert_eq!(vw.get(&Some(1)), 1); - - assert_eq!(vw.add(None, 1), 2); - assert_eq!(vw.get(&None), 2); - assert_eq!(vw.get(&Some(1)), 1); - - assert_eq!(vw.add(Some(1), 1), 2); - assert_eq!(vw.get(&None), 2); - assert_eq!(vw.get(&Some(1)), 2); - - assert_eq!(vw.add(Some(2), 1), 1); - assert_eq!(vw.get(&None), 2); - assert_eq!(vw.get(&Some(1)), 2); - assert_eq!(vw.get(&Some(2)), 1); + fn vote_count_nil() { + let mut vc = VoteCount::new(4, Default::default()); - // FIXME: Test for and deal with overflows - } + let addr1 = [1]; + let addr2 = [2]; + let addr3 = [3]; + let addr4 = [4]; - #[test] - #[allow(clippy::bool_assert_comparison)] - fn vote_count_nil() { - let mut vc = VoteCount::new(4); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); - assert_eq!(vc.is_threshold_met(Threshold::Init), false); + assert_eq!(vc.add(addr1, None, 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 1); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); assert_eq!(vc.is_threshold_met(Threshold::Any), false); assert_eq!(vc.is_threshold_met(Threshold::Nil), false); assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); - assert_eq!(vc.add_vote(None, 1), Threshold::Init); - assert_eq!(vc.is_threshold_met(Threshold::Init), false); + assert_eq!(vc.add(addr2, None, 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 2); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); assert_eq!(vc.is_threshold_met(Threshold::Any), false); assert_eq!(vc.is_threshold_met(Threshold::Nil), false); assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); - assert_eq!(vc.add_vote(None, 1), Threshold::Init); - assert_eq!(vc.is_threshold_met(Threshold::Init), false); + // addr1 votes again, is ignored + assert_eq!(vc.add(addr1, None, 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 2); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); assert_eq!(vc.is_threshold_met(Threshold::Any), false); assert_eq!(vc.is_threshold_met(Threshold::Nil), false); assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); - assert_eq!(vc.add_vote(None, 1), Threshold::Nil); - assert_eq!(vc.is_threshold_met(Threshold::Init), false); + assert_eq!(vc.add(addr3, None, 1), Threshold::Nil); + assert_eq!(vc.get(&None), 3); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); assert_eq!(vc.is_threshold_met(Threshold::Any), true); assert_eq!(vc.is_threshold_met(Threshold::Nil), true); assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); - assert_eq!(vc.add_vote(Some(1), 1), Threshold::Any); - assert_eq!(vc.is_threshold_met(Threshold::Init), false); + assert_eq!(vc.add(addr4, Some(1), 1), Threshold::Any); + assert_eq!(vc.get(&None), 3); + assert_eq!(vc.get(&Some(1)), 1); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); assert_eq!(vc.is_threshold_met(Threshold::Any), true); assert_eq!(vc.is_threshold_met(Threshold::Nil), true); assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); @@ -214,39 +188,86 @@ mod tests { } #[test] - #[allow(clippy::bool_assert_comparison)] fn vote_count_value() { - let mut vc = VoteCount::new(4); + let mut vc = VoteCount::new(4, Default::default()); + + let addr1 = [1]; + let addr2 = [2]; + let addr3 = [3]; + let addr4 = [4]; - assert_eq!(vc.is_threshold_met(Threshold::Init), false); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); assert_eq!(vc.is_threshold_met(Threshold::Any), false); assert_eq!(vc.is_threshold_met(Threshold::Nil), false); assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); - assert_eq!(vc.add_vote(Some(1), 1), Threshold::Init); - assert_eq!(vc.is_threshold_met(Threshold::Init), false); + assert_eq!(vc.add(addr1, Some(1), 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 1); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); assert_eq!(vc.is_threshold_met(Threshold::Any), false); assert_eq!(vc.is_threshold_met(Threshold::Nil), false); assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); - assert_eq!(vc.add_vote(Some(1), 1), Threshold::Init); - assert_eq!(vc.is_threshold_met(Threshold::Init), false); + assert_eq!(vc.add(addr2, Some(1), 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 2); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); assert_eq!(vc.is_threshold_met(Threshold::Any), false); assert_eq!(vc.is_threshold_met(Threshold::Nil), false); assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); - assert_eq!(vc.add_vote(Some(1), 1), Threshold::Value(1)); - assert_eq!(vc.is_threshold_met(Threshold::Init), false); + // addr1 votes again, for nil this time, is ignored + assert_eq!(vc.add(addr1, None, 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 2); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr3, Some(1), 1), Threshold::Value(1)); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 3); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + // addr2 votes again, for the same value, is ignored + assert_eq!(vc.add(addr2, Some(1), 1), Threshold::Value(1)); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 3); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr4, Some(2), 1), Threshold::Any); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 3); + assert_eq!(vc.get(&Some(2)), 1); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); assert_eq!(vc.is_threshold_met(Threshold::Any), true); assert_eq!(vc.is_threshold_met(Threshold::Nil), false); assert_eq!(vc.is_threshold_met(Threshold::Value(1)), true); assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); - assert_eq!(vc.add_vote(Some(2), 1), Threshold::Any); - assert_eq!(vc.is_threshold_met(Threshold::Init), false); + // addr4 votes again, for a different value, is ignored + assert_eq!(vc.add(addr4, Some(3), 1), Threshold::Any); + 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.is_threshold_met(Threshold::Unreached), false); assert_eq!(vc.is_threshold_met(Threshold::Any), true); assert_eq!(vc.is_threshold_met(Threshold::Nil), false); assert_eq!(vc.is_threshold_met(Threshold::Value(1)), true); diff --git a/Code/vote/src/keeper.rs b/Code/vote/src/keeper.rs index 1240dcc70..5694ec2d7 100644 --- a/Code/vote/src/keeper.rs +++ b/Code/vote/src/keeper.rs @@ -1,20 +1,43 @@ -use alloc::collections::BTreeMap; +use alloc::collections::{BTreeMap, BTreeSet}; use malachite_common::{Context, Round, ValueId, Vote, VoteType}; -use crate::{ - count::{Threshold, Weight}, - RoundVotes, -}; +use crate::round_votes::RoundVotes; +use crate::round_weights::RoundWeights; +use crate::{Threshold, ThresholdParam, ThresholdParams, Weight}; /// Messages emitted by the vote keeper -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Message { PolkaAny, PolkaNil, PolkaValue(Value), PrecommitAny, PrecommitValue(Value), + SkipRound(Round), +} + +#[derive(Clone, Debug)] +struct PerRound +where + Ctx: Context, +{ + votes: RoundVotes>, + addresses_weights: RoundWeights, + emitted_msgs: BTreeSet>>, +} + +impl PerRound +where + Ctx: Context, +{ + fn new(total_weight: Weight, threshold_params: ThresholdParams) -> Self { + Self { + votes: RoundVotes::new(total_weight, threshold_params), + addresses_weights: RoundWeights::new(), + emitted_msgs: BTreeSet::new(), + } + } } /// Keeps track of votes and emits messages when thresholds are reached. @@ -23,39 +46,58 @@ pub struct VoteKeeper where Ctx: Context, { - height: Ctx::Height, + threshold_params: ThresholdParams, total_weight: Weight, - rounds: BTreeMap>, + per_round: BTreeMap>, } impl VoteKeeper where Ctx: Context, { - pub fn new(height: Ctx::Height, round: Round, total_weight: Weight) -> Self { - let mut rounds = BTreeMap::new(); - - if round != Round::NIL { - rounds.insert(round, RoundVotes::new(height.clone(), round, total_weight)); - } - + pub fn new(total_weight: Weight) -> Self { VoteKeeper { - height, + // TODO: Make these configurable + threshold_params: ThresholdParams::default(), + total_weight, - rounds, + per_round: BTreeMap::new(), } } /// Apply a vote with a given weight, potentially triggering an event. pub fn apply_vote(&mut self, vote: Ctx::Vote, weight: Weight) -> Option>> { - let round = self.rounds.entry(vote.round()).or_insert_with(|| { - RoundVotes::new(self.height.clone(), vote.round(), self.total_weight) - }); + let round = self + .per_round + .entry(vote.round()) + .or_insert_with(|| PerRound::new(self.total_weight, self.threshold_params)); + + let threshold = round.votes.add_vote( + vote.vote_type(), + vote.validator_address().clone(), + vote.value().cloned(), + weight, + ); + + round + .addresses_weights + .set_once(vote.validator_address().clone(), weight); - let vote_type = vote.vote_type(); - let threshold = round.add_vote(vote, weight); + let msg = threshold_to_message(vote.vote_type(), vote.round(), threshold)?; + + let final_msg = if !round.emitted_msgs.contains(&msg) { + Some(msg) + } else if Self::skip_round(round, self.total_weight, self.threshold_params.honest) { + Some(Message::SkipRound(vote.round())) + } else { + None + }; + + if let Some(final_msg) = &final_msg { + round.emitted_msgs.insert(final_msg.clone()); + } - Self::to_message(vote_type, threshold) + final_msg } /// Check if a threshold is met, ie. if we have a quorum for that threshold. @@ -65,32 +107,39 @@ where vote_type: VoteType, threshold: Threshold>, ) -> bool { - let round = match self.rounds.get(round) { - Some(round) => round, - None => return false, - }; + self.per_round.get(round).map_or(false, |round| { + round.votes.is_threshold_met(vote_type, threshold) + }) + } - match vote_type { - VoteType::Prevote => round.prevotes.is_threshold_met(threshold), - VoteType::Precommit => round.precommits.is_threshold_met(threshold), - } + /// Check whether or not we should skip this round, in case we haven't emitted any messages + /// yet, and we have reached an honest threshold for the round. + fn skip_round( + round: &PerRound, + total_weight: Weight, + threshold_param: ThresholdParam, + ) -> bool { + round.emitted_msgs.is_empty() + && threshold_param.is_met(round.addresses_weights.sum(), total_weight) } +} - /// Map a vote type and a threshold to a state machine event. - fn to_message( - typ: VoteType, - threshold: Threshold>, - ) -> Option>> { - match (typ, threshold) { - (_, Threshold::Init) => None, +/// Map a vote type and a threshold to a state machine event. +fn threshold_to_message( + typ: VoteType, + round: Round, + threshold: Threshold, +) -> Option> { + match (typ, threshold) { + (_, Threshold::Unreached) => None, + (_, Threshold::Skip) => Some(Message::SkipRound(round)), - (VoteType::Prevote, Threshold::Any) => Some(Message::PolkaAny), - (VoteType::Prevote, Threshold::Nil) => Some(Message::PolkaNil), - (VoteType::Prevote, Threshold::Value(v)) => Some(Message::PolkaValue(v)), + (VoteType::Prevote, Threshold::Any) => Some(Message::PolkaAny), + (VoteType::Prevote, Threshold::Nil) => Some(Message::PolkaNil), + (VoteType::Prevote, Threshold::Value(v)) => Some(Message::PolkaValue(v)), - (VoteType::Precommit, Threshold::Any) => Some(Message::PrecommitAny), - (VoteType::Precommit, Threshold::Nil) => Some(Message::PrecommitAny), - (VoteType::Precommit, Threshold::Value(v)) => Some(Message::PrecommitValue(v)), - } + (VoteType::Precommit, Threshold::Any) => Some(Message::PrecommitAny), + (VoteType::Precommit, Threshold::Nil) => Some(Message::PrecommitAny), + (VoteType::Precommit, Threshold::Value(v)) => Some(Message::PrecommitValue(v)), } } diff --git a/Code/vote/src/lib.rs b/Code/vote/src/lib.rs index 6d66b9448..cc27fe823 100644 --- a/Code/vote/src/lib.rs +++ b/Code/vote/src/lib.rs @@ -14,41 +14,78 @@ extern crate alloc; pub mod count; pub mod keeper; +pub mod round_votes; +pub mod round_weights; +pub mod value_weights; -use malachite_common::{Context, Round, ValueId, Vote, VoteType}; +// TODO: Introduce newtype +// QUESTION: Over what type? i64? +pub type Weight = u64; -use crate::count::{Threshold, VoteCount, Weight}; +/// Represents the different quorum thresholds. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Threshold { + /// No quorum has been reached yet + Unreached, -/// Tracks all the votes for a single round -#[derive(Clone, Debug)] -pub struct RoundVotes -where - Ctx: Context, -{ - pub height: Ctx::Height, - pub round: Round, + /// Minimum number of votes correct processes, + /// if at a round higher than current then skip to that round. + Skip, - pub prevotes: VoteCount>, - pub precommits: VoteCount>, + /// Quorum of votes but not for the same value + Any, + + /// Quorum of votes for nil + Nil, + + /// Quorum (+2/3) of votes for a value + Value(ValueId), +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct ThresholdParams { + /// Threshold for a quorum (default: 2f+1) + pub quorum: ThresholdParam, + + /// Threshold for the minimum number of honest nodes (default: f+1) + pub honest: ThresholdParam, } -impl RoundVotes -where - Ctx: Context, -{ - pub fn new(height: Ctx::Height, round: Round, total: Weight) -> Self { - RoundVotes { - height, - round, - prevotes: VoteCount::new(total), - precommits: VoteCount::new(total), +impl Default for ThresholdParams { + fn default() -> Self { + Self { + quorum: ThresholdParam::TWO_F_PLUS_ONE, + honest: ThresholdParam::F_PLUS_ONE, } } +} - pub fn add_vote(&mut self, vote: Ctx::Vote, weight: Weight) -> Threshold> { - match vote.vote_type() { - VoteType::Prevote => self.prevotes.add_vote(vote.take_value(), weight), - VoteType::Precommit => self.precommits.add_vote(vote.take_value(), weight), +/// 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 { + pub numerator: u64, + pub denominator: u64, +} + +impl ThresholdParam { + /// 2f+1 + pub const TWO_F_PLUS_ONE: Self = Self::new(2, 3); + + /// f+1 + pub const F_PLUS_ONE: Self = Self::new(1, 3); + + pub const fn new(numerator: u64, denominator: u64) -> Self { + Self { + numerator, + denominator, } } + + /// Check whether the threshold is met. + pub const fn is_met(&self, weight: Weight, total: Weight) -> bool { + // FIXME: Deal with overflows + weight * self.denominator > total * self.numerator + } } diff --git a/Code/vote/src/round_votes.rs b/Code/vote/src/round_votes.rs new file mode 100644 index 000000000..21a14f80b --- /dev/null +++ b/Code/vote/src/round_votes.rs @@ -0,0 +1,47 @@ +use malachite_common::VoteType; + +use crate::count::VoteCount; +use crate::{Threshold, ThresholdParams, Weight}; + +/// Tracks all the votes for a single round +#[derive(Clone, Debug)] +pub struct RoundVotes { + prevotes: VoteCount, + precommits: VoteCount, +} + +impl RoundVotes { + pub fn new(total_weight: Weight, threshold_params: ThresholdParams) -> Self { + RoundVotes { + prevotes: VoteCount::new(total_weight, threshold_params), + precommits: VoteCount::new(total_weight, threshold_params), + } + } + + pub fn add_vote( + &mut self, + vote_type: VoteType, + address: Address, + value: Option, + weight: Weight, + ) -> Threshold + where + Address: Clone + Ord, + Value: Clone + Ord, + { + match vote_type { + VoteType::Prevote => self.prevotes.add(address, value, weight), + VoteType::Precommit => self.precommits.add(address, value, weight), + } + } + + pub fn is_threshold_met(&self, vote_type: VoteType, threshold: Threshold) -> bool + where + Value: Ord, + { + match vote_type { + VoteType::Prevote => self.prevotes.is_threshold_met(threshold), + VoteType::Precommit => self.precommits.is_threshold_met(threshold), + } + } +} diff --git a/Code/vote/src/round_weights.rs b/Code/vote/src/round_weights.rs new file mode 100644 index 000000000..63213782d --- /dev/null +++ b/Code/vote/src/round_weights.rs @@ -0,0 +1,40 @@ +use alloc::collections::BTreeMap; + +use crate::Weight; + +#[derive(Clone, Debug)] +pub struct RoundWeights
{ + map: BTreeMap, +} + +impl
RoundWeights
{ + pub fn new() -> Self { + RoundWeights { + map: BTreeMap::new(), + } + } + + pub fn set_once(&mut self, address: Address, weight: Weight) + where + Address: Ord, + { + self.map.entry(address).or_insert(weight); + } + + pub fn get(&self, address: &Address) -> Weight + where + Address: Ord, + { + *self.map.get(address).unwrap_or(&0) + } + + pub fn sum(&self) -> Weight { + self.map.values().sum() + } +} + +impl
Default for RoundWeights
{ + fn default() -> Self { + Self::new() + } +} diff --git a/Code/vote/src/value_weights.rs b/Code/vote/src/value_weights.rs new file mode 100644 index 000000000..7b1e355af --- /dev/null +++ b/Code/vote/src/value_weights.rs @@ -0,0 +1,82 @@ +use alloc::collections::BTreeMap; + +use crate::Weight; + +/// A value and the weight of votes for it. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ValuesWeights { + value_weights: BTreeMap, +} + +impl ValuesWeights { + pub fn new() -> ValuesWeights { + ValuesWeights { + value_weights: BTreeMap::new(), + } + } + + /// Add weight to the value and return the new weight. + pub fn add(&mut self, value: Value, weight: Weight) -> Weight + where + Value: Ord, + { + let entry = self.value_weights.entry(value).or_insert(0); + *entry += weight; // FIXME: Deal with overflows + *entry + } + + /// Return the weight of the value, or 0 if it is not present. + pub fn get(&self, value: &Value) -> Weight + where + Value: Ord, + { + self.value_weights.get(value).copied().unwrap_or(0) + } + + /// Return the sum of the weights of all values. + pub fn sum(&self) -> Weight { + self.value_weights.values().sum() // FIXME: Deal with overflows + } +} + +impl Default for ValuesWeights { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn values_weights() { + let mut vw = ValuesWeights::new(); + + assert_eq!(vw.get(&None), 0); + assert_eq!(vw.get(&Some(1)), 0); + + assert_eq!(vw.add(None, 1), 1); + assert_eq!(vw.get(&None), 1); + assert_eq!(vw.get(&Some(1)), 0); + + assert_eq!(vw.add(Some(1), 1), 1); + assert_eq!(vw.get(&None), 1); + assert_eq!(vw.get(&Some(1)), 1); + + assert_eq!(vw.add(None, 1), 2); + assert_eq!(vw.get(&None), 2); + assert_eq!(vw.get(&Some(1)), 1); + + assert_eq!(vw.add(Some(1), 1), 2); + assert_eq!(vw.get(&None), 2); + assert_eq!(vw.get(&Some(1)), 2); + + assert_eq!(vw.add(Some(2), 1), 1); + assert_eq!(vw.get(&None), 2); + assert_eq!(vw.get(&Some(1)), 2); + assert_eq!(vw.get(&Some(2)), 1); + + // FIXME: Test for and deal with overflows + } +}