From 3fea11320d068c8df4a711fead06b600811003bf Mon Sep 17 00:00:00 2001 From: Romain Ruetschi Date: Wed, 25 Oct 2023 09:22:02 +0200 Subject: [PATCH] Abstract over the various data types needed by the consensus engine (#8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes: #11 This gist of this PR is that it adds a `Consensus` trait which defines the various datatypes that the consensus engine operates over, as well as a trait per such datatype that defines the requirement for said datatype. ```rust pub trait Consensus where Self: Sized, { type Address: Address; type Height: Height; type Proposal: Proposal; type PublicKey: PublicKey; type Validator: Validator; type ValidatorSet: ValidatorSet; type Value: Value; type Vote: Vote; ``` ```rust pub trait Height where Self: Clone + Debug + PartialEq + Eq + PartialOrd + Ord, { } ``` etc. In test code, we define our own simple versions of these datatypes as well as a `TestConsensus` struct, for which we implement the `Consensus` trait. ```rust #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Address(u64); impl Address { pub const fn new(value: u64) -> Self { Self(value) } } impl malachite_common::Address for Address {} ``` ```rust pub struct TestConsensus; impl Consensus for TestConsensus { type Address = Address; type Height = Height; type Proposal = Proposal; type PublicKey = PublicKey; type ValidatorSet = ValidatorSet; type Validator = Validator; type Value = Value; type Vote = Vote; } ``` --- * Attempt to abstract over the various data types needed by the consensus engine * Fix tests * Fix lints names * Move tests and associated data types into their own crate (#10) * Move tests and associated data types into their own crate * Add small threshold to codecov * Small cleanup * Move tests into `tests` folder * Adjust base when computing coverage in the presence of removed code See: https://docs.codecov.com/docs/commit-status#removed_code_behavior * Document the `Round` type and rename `Round::None` to `Round::Nil` * More doc comments * Remove `PublicKey::hash` requirement * Few more comments * Add propose+precommit timeout test --- .gitignore | 4 + Code/Cargo.toml | 1 + Code/common/src/consensus.rs | 49 +++ Code/common/src/height.rs | 25 +- Code/common/src/lib.rs | 25 +- Code/common/src/proposal.rs | 36 +- Code/common/src/round.rs | 51 ++- Code/common/src/timeout.rs | 22 +- Code/common/src/validator_set.rs | 193 ++------- Code/common/src/value.rs | 39 +- Code/common/src/vote.rs | 51 ++- Code/consensus/src/executor.rs | 315 ++++---------- Code/consensus/src/lib.rs | 4 +- Code/round/src/events.rs | 32 +- Code/round/src/lib.rs | 6 +- Code/round/src/message.rs | 59 ++- Code/round/src/state.rs | 49 ++- Code/round/src/state_machine.rs | 181 ++++---- Code/test/Cargo.toml | 12 + Code/test/src/consensus.rs | 38 ++ Code/test/src/height.rs | 15 + Code/test/src/lib.rs | 16 + Code/test/src/proposal.rs | 41 ++ Code/test/src/validator_set.rs | 205 +++++++++ Code/test/src/value.rs | 42 ++ Code/test/src/vote.rs | 54 +++ Code/test/tests/consensus_executor.rs | 597 ++++++++++++++++++++++++++ Code/test/tests/round.rs | 63 +++ Code/test/tests/vote_count.rs | 94 ++++ Code/test/tests/vote_keeper.rs | 81 ++++ Code/vote/src/count.rs | 141 ++---- Code/vote/src/keeper.rs | 122 +----- Code/vote/src/lib.rs | 28 +- codecov.yml | 16 +- 34 files changed, 1823 insertions(+), 884 deletions(-) create mode 100644 Code/common/src/consensus.rs create mode 100644 Code/test/Cargo.toml create mode 100644 Code/test/src/consensus.rs create mode 100644 Code/test/src/height.rs create mode 100644 Code/test/src/lib.rs create mode 100644 Code/test/src/proposal.rs create mode 100644 Code/test/src/validator_set.rs create mode 100644 Code/test/src/value.rs create mode 100644 Code/test/src/vote.rs create mode 100644 Code/test/tests/consensus_executor.rs create mode 100644 Code/test/tests/round.rs create mode 100644 Code/test/tests/vote_count.rs create mode 100644 Code/test/tests/vote_keeper.rs diff --git a/.gitignore b/.gitignore index 5166e1ec1..2784a33a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# macOS Finder and Windows Thumbs.db files +.DS_Store +Thumbs.db + # Generated by Cargo # will have compiled files and executables debug/ diff --git a/Code/Cargo.toml b/Code/Cargo.toml index 63a6d8dbe..d522ca3a4 100644 --- a/Code/Cargo.toml +++ b/Code/Cargo.toml @@ -6,4 +6,5 @@ members = [ "consensus", "round", "vote", + "test", ] diff --git a/Code/common/src/consensus.rs b/Code/common/src/consensus.rs new file mode 100644 index 000000000..0fc01eaac --- /dev/null +++ b/Code/common/src/consensus.rs @@ -0,0 +1,49 @@ +use crate::{ + Address, Height, Proposal, PublicKey, Round, Validator, ValidatorSet, Value, ValueId, Vote, +}; + +/// This trait allows to abstract over the various datatypes +/// that are used in the consensus engine. +pub trait Consensus +where + Self: Sized, +{ + type Address: Address; + type Height: Height; + type Proposal: Proposal; + type PublicKey: PublicKey; + type Validator: Validator; + type ValidatorSet: ValidatorSet; + type Value: Value; + type Vote: Vote; + + // FIXME: Remove this and thread it through where necessary + const DUMMY_ADDRESS: Self::Address; + + // FIXME: Remove altogether + const DUMMY_VALUE: Self::Value; + + /// Build a new proposal for the given value at the given height, round and POL round. + fn new_proposal( + height: Self::Height, + round: Round, + value: Self::Value, + pol_round: Round, + ) -> Self::Proposal; + + /// Build a new prevote vote by the validator with the given address, + /// for the value identified by the given value id, at the given round. + fn new_prevote( + round: Round, + value_id: Option>, + address: Self::Address, + ) -> Self::Vote; + + /// Build a new precommit vote by the validator with the given address, + /// for the value identified by the given value id, at the given round. + fn new_precommit( + round: Round, + value_id: Option>, + address: Self::Address, + ) -> Self::Vote; +} diff --git a/Code/common/src/height.rs b/Code/common/src/height.rs index 156cc44be..511f69503 100644 --- a/Code/common/src/height.rs +++ b/Code/common/src/height.rs @@ -1,15 +1,14 @@ -// TODO: Abstract over Height +use core::fmt::Debug; -/// A blockchain height -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] -pub struct Height(u64); - -impl Height { - pub fn new(height: u64) -> Self { - Self(height) - } - - pub fn as_u64(&self) -> u64 { - self.0 - } +// TODO: Keep the trait or just add the bounds to Consensus::Height? +/// Defines the requirements for a height type. +/// +/// A height denotes the number of blocks (values) created since the chain began. +/// +/// A height of 0 represents a chain which has not yet produced a block. +pub trait Height +where + // TODO: Require Copy as well? + Self: Clone + Debug + PartialEq + Eq + PartialOrd + Ord, +{ } diff --git a/Code/common/src/lib.rs b/Code/common/src/lib.rs index 44b33eb84..4da2130a2 100644 --- a/Code/common/src/lib.rs +++ b/Code/common/src/lib.rs @@ -1,15 +1,16 @@ -//! Common data types and abstractions +//! Common data types and abstractions for the consensus engine. #![forbid(unsafe_code)] #![deny(unused_crate_dependencies, trivial_casts, trivial_numeric_casts)] #![warn( // missing_docs, - broken_intra_doc_links, - private_intra_doc_links, + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, variant_size_differences )] #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::panic))] +mod consensus; mod height; mod proposal; mod round; @@ -18,10 +19,14 @@ mod validator_set; mod value; mod vote; -pub use height::*; -pub use proposal::*; -pub use round::*; -pub use timeout::*; -pub use validator_set::*; -pub use value::*; -pub use vote::*; +/// Type alias to make it easier to refer the `ValueId` type of a given `Consensus` engine. +pub type ValueId = <::Value as Value>::Id; + +pub use consensus::Consensus; +pub use height::Height; +pub use proposal::Proposal; +pub use round::Round; +pub use timeout::{Timeout, TimeoutStep}; +pub use validator_set::{Address, PublicKey, Validator, ValidatorSet, VotingPower}; +pub use value::Value; +pub use vote::{Vote, VoteType}; diff --git a/Code/common/src/proposal.rs b/Code/common/src/proposal.rs index 43b59584a..fc5d8f703 100644 --- a/Code/common/src/proposal.rs +++ b/Code/common/src/proposal.rs @@ -1,21 +1,21 @@ -use crate::{Height, Round, Value}; +use core::fmt::Debug; -/// A proposal for a value in a round -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Proposal { - pub height: Height, - pub round: Round, - pub value: Value, - pub pol_round: Round, -} +use crate::{Consensus, Round}; + +/// Defines the requirements for a proposal type. +pub trait Proposal +where + Self: Clone + Debug + PartialEq + Eq, +{ + /// The height for which the proposal is for. + fn height(&self) -> C::Height; + + /// The round for which the proposal is for. + fn round(&self) -> Round; + + /// The value that is proposed. + fn value(&self) -> &C::Value; -impl Proposal { - pub fn new(height: Height, round: Round, value: Value, pol_round: Round) -> Self { - Self { - height, - round, - value, - pol_round, - } - } + /// The POL round for which the proposal is for. + fn pol_round(&self) -> Round; } diff --git a/Code/common/src/round.rs b/Code/common/src/round.rs index bdb1fe530..a281a95c5 100644 --- a/Code/common/src/round.rs +++ b/Code/common/src/round.rs @@ -1,49 +1,68 @@ -/// A round number, ie. a natural number +/// A round number. +/// +/// Can be either: +/// - `Round::Nil` (ie. `-1`) +/// - `Round::Some(r)` where `r >= 0` #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Round { - /// No round - None, + /// No round, ie. `-1` + Nil, - /// Some round + /// Some round `r` where `r >= 0` Some(i64), } impl Round { + /// The initial, zero round. pub const INITIAL: Round = Round::new(0); + /// Create a new round. + /// + /// If `round < 0`, then `Round::Nil` is returned. + /// Otherwise, `Round::Some(round)` is returned. pub const fn new(round: i64) -> Self { if round < 0 { - Self::None + Self::Nil } else { Self::Some(round) } } + /// Convert the round to an `i64`. + /// + /// `Round::Nil` is converted to `-1`. + /// `Round::Some(r)` is converted to `r`. pub fn as_i64(&self) -> i64 { match self { - Round::None => -1, + Round::Nil => -1, Round::Some(r) => *r, } } + /// Wether the round is defined, ie. `Round::Some(r)` where `r >= 0`. pub fn is_defined(&self) -> bool { matches!(self, Round::Some(r) if *r >= 0) } + /// Wether the round is `Round::Nil`. pub fn is_nil(&self) -> bool { - matches!(self, Round::None) + matches!(self, Round::Nil) } + /// Whether the round is valid, ie. either nil or defined. pub fn is_valid(&self) -> bool { - match self { - Round::None => true, - Round::Some(r) => *r >= 0, - } + // FIXME: This is always true, given that the constructor upholds + // the invariant that `Round::Some(r)` is only created for `r >= 0`. + self.is_nil() || self.is_defined() } + /// Increment the round. + /// + /// If the round is nil, then the initial zero round is returned. + /// Otherwise, the round is incremented by one. pub fn increment(&self) -> Round { match self { - Round::None => Round::new(0), + Round::Nil => Round::new(0), Round::Some(r) => Round::new(r + 1), } } @@ -68,20 +87,20 @@ mod tests { #[test] fn test_round() { // Test Round::new() - assert_eq!(Round::new(-42), Round::None); - assert_eq!(Round::new(-1), Round::None); + assert_eq!(Round::new(-42), Round::Nil); + assert_eq!(Round::new(-1), Round::Nil); assert_eq!(Round::new(0), Round::Some(0)); assert_eq!(Round::new(1), Round::Some(1)); assert_eq!(Round::new(2), Round::Some(2)); // Test Round::as_i64() - assert_eq!(Round::None.as_i64(), -1); + assert_eq!(Round::Nil.as_i64(), -1); assert_eq!(Round::Some(0).as_i64(), 0); assert_eq!(Round::Some(1).as_i64(), 1); assert_eq!(Round::Some(2).as_i64(), 2); // Test Round::is_defined() - assert!(!Round::None.is_defined()); + assert!(!Round::Nil.is_defined()); assert!(Round::Some(0).is_defined()); assert!(Round::Some(1).is_defined()); assert!(Round::Some(2).is_defined()); diff --git a/Code/common/src/timeout.rs b/Code/common/src/timeout.rs index 56c6e632d..5c72a75c8 100644 --- a/Code/common/src/timeout.rs +++ b/Code/common/src/timeout.rs @@ -1,5 +1,6 @@ use crate::Round; +/// The round step for which the timeout is for. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum TimeoutStep { Propose, @@ -7,8 +8,27 @@ pub enum TimeoutStep { Precommit, } -#[derive(Clone, Debug, PartialEq, Eq)] +/// A timeout for a round step. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct Timeout { pub round: Round, pub step: TimeoutStep, } + +impl Timeout { + pub fn new(round: Round, step: TimeoutStep) -> Self { + Self { round, step } + } + + pub fn propose(round: Round) -> Self { + Self::new(round, TimeoutStep::Propose) + } + + pub fn prevote(round: Round) -> Self { + Self::new(round, TimeoutStep::Prevote) + } + + pub fn precommit(round: Round) -> Self { + Self::new(round, TimeoutStep::Precommit) + } +} diff --git a/Code/common/src/validator_set.rs b/Code/common/src/validator_set.rs index e90cda9cd..cd58f1698 100644 --- a/Code/common/src/validator_set.rs +++ b/Code/common/src/validator_set.rs @@ -1,165 +1,60 @@ -// TODO: Abstract over all of this +use core::fmt::Debug; -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct PublicKey(Vec); - -impl PublicKey { - pub const fn new(value: Vec) -> Self { - Self(value) - } - - pub fn hash(&self) -> u64 { - // TODO - self.0.iter().fold(0, |acc, x| acc ^ *x as u64) - } -} +use crate::Consensus; +/// Voting power held by a validator. +/// +/// TODO: Do we need to abstract over this as well? pub type VotingPower = u64; -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct Address(u64); - -impl Address { - pub const fn new(value: u64) -> Self { - Self(value) - } +/// Defines the requirements for a public key type. +pub trait PublicKey +where + Self: Clone + Debug + PartialEq + Eq, +{ } -/// A validator is a public key and voting power -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Validator { - pub public_key: PublicKey, - pub voting_power: VotingPower, +/// Defines the requirements for an address. +/// +/// TODO: Keep this trait or just add the bounds to Consensus::Address? +pub trait Address +where + Self: Clone + Debug + PartialEq + Eq, +{ } -impl Validator { - pub fn new(public_key: PublicKey, voting_power: VotingPower) -> Self { - Self { - public_key, - voting_power, - } - } +/// Defines the requirements for a validator. +pub trait Validator +where + Self: Clone + Debug + PartialEq + Eq, + C: Consensus, +{ + /// The address of the validator, typically derived from its public key. + fn address(&self) -> &C::Address; - pub fn hash(&self) -> Vec { - self.public_key.0.clone() // TODO - } + /// The public key of the validator, used to verify signatures. + fn public_key(&self) -> &C::PublicKey; - pub fn address(&self) -> Address { - Address(self.public_key.hash()) // TODO - } + /// The voting power held by the validaror. + fn voting_power(&self) -> VotingPower; } -/// A validator set contains a list of validators sorted by address. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ValidatorSet { - validators: Vec, -} - -impl ValidatorSet { - pub fn new(validators: impl IntoIterator) -> Self { - let mut validators: Vec<_> = validators.into_iter().collect(); - ValidatorSet::sort_validators(&mut validators); - - Self { validators } - } - - /// 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() - } - - /// Add a validator to the set - pub fn add(&mut self, validator: Validator) { - self.validators.push(validator); - - ValidatorSet::sort_validators(&mut self.validators); - } - - /// Update the voting power of the given validator - pub fn update(&mut self, val: Validator) { - if let Some(v) = self - .validators - .iter_mut() - .find(|v| v.address() == val.address()) - { - v.voting_power = val.voting_power; - } - - dbg!(self.total_voting_power()); - Self::sort_validators(&mut self.validators); - dbg!(self.total_voting_power()); - } - - /// Remove a validator from the set - pub fn remove(&mut self, address: &Address) { - self.validators.retain(|v| &v.address() != address); - - Self::sort_validators(&mut self.validators); // TODO: Not needed - } - - /// Get a validator by its address - pub fn get_by_address(&self, address: &Address) -> Option<&Validator> { - self.validators.iter().find(|v| &v.address() == address) - } - - pub fn get_by_public_key(&self, public_key: &PublicKey) -> Option<&Validator> { - self.validators.iter().find(|v| &v.public_key == public_key) - } - - /// In place sort and deduplication of a list of validators - fn sort_validators(vals: &mut Vec) { - use core::cmp::Reverse; - - // Sort the validators according to the current Tendermint requirements - // (v. 0.34 -> first by validator power, descending, then by address, ascending) - vals.sort_unstable_by_key(|v| (Reverse(v.voting_power), v.address())); - - vals.dedup(); - } - - pub fn get_proposer(&mut self) -> Validator { - // TODO: Proper implementation - assert!(!self.validators.is_empty()); - let proposer = self.validators[0].clone(); - self.validators.rotate_left(1); - proposer - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn add_update_remove() { - let v1 = Validator::new(PublicKey(vec![1]), 1); - let v2 = Validator::new(PublicKey(vec![2]), 2); - let v3 = Validator::new(PublicKey(vec![3]), 3); - - let mut vs = ValidatorSet::new(vec![v1, v2, v3]); - assert_eq!(vs.total_voting_power(), 6); - - let v4 = Validator::new(PublicKey(vec![4]), 4); - vs.add(v4); - assert_eq!(vs.total_voting_power(), 10); - - let mut v5 = Validator::new(PublicKey(vec![5]), 5); - vs.update(v5.clone()); // no effect - assert_eq!(vs.total_voting_power(), 10); - - vs.add(v5.clone()); - assert_eq!(vs.total_voting_power(), 15); +/// Defines the requirements for a validator set. +/// +/// A validator set is a collection of validators. +pub trait ValidatorSet +where + C: Consensus, +{ + /// The total voting power of the validator set. + fn total_voting_power(&self) -> VotingPower; - v5.voting_power = 100; - vs.update(v5.clone()); - assert_eq!(vs.total_voting_power(), 110); + /// The proposer in the validator set. + fn get_proposer(&self) -> C::Validator; - vs.remove(&v5.address()); - assert_eq!(vs.total_voting_power(), 10); + /// Get the validator with the given public key. + fn get_by_public_key(&self, public_key: &C::PublicKey) -> Option<&C::Validator>; - let v6 = Validator::new(PublicKey(vec![6]), 6); - vs.remove(&v6.address()); // no effect - assert_eq!(vs.total_voting_power(), 10); - } + /// Get the validator with the given address. + fn get_by_address(&self, address: &C::Address) -> Option<&C::Validator>; } diff --git a/Code/common/src/value.rs b/Code/common/src/value.rs index d06ea6a53..51eb784d9 100644 --- a/Code/common/src/value.rs +++ b/Code/common/src/value.rs @@ -1,30 +1,17 @@ -/// The value to decide on -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct Value(u64); +use core::fmt::Debug; -impl Value { - pub fn new(value: u64) -> Self { - Self(value) - } +/// Defines the requirements for the type of value to decide on. +pub trait Value +where + Self: Clone + Debug + PartialEq + Eq + PartialOrd + Ord, +{ + /// The type of the ID of the value. + /// Typically a representation of the value with a lower memory footprint. + type Id: Clone + Debug + PartialEq + Eq + PartialOrd + Ord; - pub fn as_u64(&self) -> u64 { - self.0 - } + /// The ID of the value. + fn id(&self) -> Self::Id; - pub fn valid(&self) -> bool { - self.0 > 0 - } - - pub fn id(&self) -> ValueId { - ValueId(self.0) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Copy)] -pub struct ValueId(u64); - -impl ValueId { - pub fn new(id: u64) -> Self { - Self(id) - } + /// Whether the value is valid. + fn is_valid(&self) -> bool; } diff --git a/Code/common/src/vote.rs b/Code/common/src/vote.rs index 9ca3a6eab..6dd388d13 100644 --- a/Code/common/src/vote.rs +++ b/Code/common/src/vote.rs @@ -1,36 +1,35 @@ -use crate::{Address, Round, ValueId}; +use core::fmt::Debug; +use crate::{Consensus, Round, Value}; + +/// A type of vote. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum VoteType { + /// Votes for values which validators observe are valid for a given round. Prevote, + + /// Votes to commit to a particular value for a given round. Precommit, } -/// A vote for a value in a round -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Vote { - pub typ: VoteType, - pub round: Round, - pub value: Option, - pub address: Address, -} +/// Defines the requirements for a vote. +/// +/// Votes are signed messages from validators for a particular value which +/// include information about the validator signing it. +pub trait Vote +where + Self: Clone + Debug + PartialEq + Eq, +{ + /// The round for which the vote is for. + fn round(&self) -> Round; + + /// The value being voted for. + fn value(&self) -> Option<&::Id>; -impl Vote { - pub fn new_prevote(round: Round, value: Option, address: Address) -> Self { - Self { - typ: VoteType::Prevote, - round, - value, - address, - } - } + /// The type of vote. + fn vote_type(&self) -> VoteType; - pub fn new_precommit(round: Round, value: Option, address: Address) -> Self { - Self { - typ: VoteType::Precommit, - round, - value, - address, - } - } + // FIXME: round message votes should not include address + fn address(&self) -> &C::Address; + fn set_address(&mut self, address: C::Address); } diff --git a/Code/consensus/src/executor.rs b/Code/consensus/src/executor.rs index d7de326f6..7add9bee1 100644 --- a/Code/consensus/src/executor.rs +++ b/Code/consensus/src/executor.rs @@ -1,8 +1,8 @@ use std::collections::BTreeMap; -use std::sync::Arc; use malachite_common::{ - Height, Proposal, PublicKey, Round, Timeout, TimeoutStep, ValidatorSet, Value, Vote, VoteType, + Consensus, Proposal, Round, Timeout, TimeoutStep, Validator, ValidatorSet, Value, Vote, + VoteType, }; use malachite_round::events::Event as RoundEvent; use malachite_round::message::Message as RoundMessage; @@ -11,26 +11,39 @@ use malachite_vote::count::Threshold; use malachite_vote::keeper::VoteKeeper; #[derive(Clone, Debug)] -pub struct Executor { - height: Height, - key: PublicKey, - validator_set: ValidatorSet, +pub struct Executor +where + C: Consensus, +{ + height: C::Height, + key: C::PublicKey, + validator_set: C::ValidatorSet, round: Round, - votes: VoteKeeper, - round_states: BTreeMap, + votes: VoteKeeper, + round_states: BTreeMap>, } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum Message { +pub enum Message +where + C: Consensus, +{ NewRound(Round), - Proposal(Proposal), - Vote(Vote), + Proposal(C::Proposal), + Vote(C::Vote), Timeout(Timeout), } -impl Executor { - pub fn new(height: Height, validator_set: ValidatorSet, key: PublicKey) -> Self { - let votes = VoteKeeper::new(height, Round::INITIAL, validator_set.total_voting_power()); +impl Executor +where + C: Consensus, +{ + pub fn new(height: C::Height, validator_set: C::ValidatorSet, key: C::PublicKey) -> Self { + let votes = VoteKeeper::new( + height.clone(), + Round::INITIAL, + validator_set.total_voting_power(), + ); Self { height, @@ -42,11 +55,12 @@ impl Executor { } } - pub fn get_value(&self) -> Value { + pub fn get_value(&self) -> C::Value { // TODO - add external interface to get the value - Value::new(9999) + C::DUMMY_VALUE } - pub fn execute(&mut self, msg: Message) -> Option { + + pub fn execute(&mut self, msg: Message) -> Option> { let round_msg = match self.apply(msg) { Some(msg) => msg, None => return None, @@ -57,23 +71,36 @@ impl Executor { // TODO: check if we are the proposer self.round_states - .insert(round, RoundState::new(self.height).new_round(round)); + .insert(round, RoundState::new(self.height.clone()).new_round(round)); + None } + RoundMessage::Proposal(p) => { // sign the proposal Some(Message::Proposal(p)) } + RoundMessage::Vote(mut v) => { // sign the vote - // TODO - round message votes should not include address - v.address = self.validator_set.get_by_public_key(&self.key)?.address(); + + // FIXME: round message votes should not include address + let address = self + .validator_set + .get_by_public_key(&self.key)? + .address() + .clone(); + + v.set_address(address); + Some(Message::Vote(v)) } + RoundMessage::Timeout(_) => { // schedule the timeout None } + RoundMessage::Decision(_) => { // update the state None @@ -81,7 +108,7 @@ impl Executor { } } - fn apply(&mut self, msg: Message) -> Option { + fn apply(&mut self, msg: Message) -> Option> { match msg { Message::NewRound(round) => self.apply_new_round(round), Message::Proposal(proposal) => self.apply_proposal(proposal), @@ -90,71 +117,75 @@ impl Executor { } } - fn apply_new_round(&mut self, round: Round) -> Option { + fn apply_new_round(&mut self, round: Round) -> Option> { let proposer = self.validator_set.get_proposer(); - let event = if proposer.public_key == self.key { + + let event = if proposer.public_key() == &self.key { let value = self.get_value(); RoundEvent::NewRoundProposer(value) } else { RoundEvent::NewRound }; + self.apply_event(round, event) } - fn apply_proposal(&mut self, proposal: Proposal) -> Option { + fn apply_proposal(&mut self, proposal: C::Proposal) -> Option> { // TODO: Check for invalid proposal - let round = proposal.round; let event = RoundEvent::Proposal(proposal.clone()); + // Check that there is an ongoing round let Some(round_state) = self.round_states.get(&self.round) else { // TODO: Add logging return None; }; + // Only process the proposal if there is no other proposal if round_state.proposal.is_some() { return None; } - if round_state.height != proposal.height || proposal.round != self.round { + // Check that the proposal is for the current height and round + if round_state.height != proposal.height() || proposal.round() != self.round { return None; } - if !proposal.pol_round.is_valid() - || proposal.pol_round.is_defined() && proposal.pol_round >= round_state.round + // TODO: Document + if !proposal.pol_round().is_valid() + || proposal.pol_round().is_defined() && proposal.pol_round() >= round_state.round { return None; } - // TODO verify proposal signature (make some of these checks part of message validation) - - match proposal.pol_round { - Round::None => { + // TODO: Verify proposal signature (make some of these checks part of message validation) + match proposal.pol_round() { + Round::Nil => { // Is it possible to get +2/3 prevotes before the proposal? // Do we wait for our own prevote to check the threshold? - self.apply_event(round, event) + self.apply_event(proposal.round(), event) } Round::Some(_) if self.votes.check_threshold( - &proposal.pol_round, + &proposal.pol_round(), VoteType::Prevote, - Threshold::Value(Arc::from(proposal.value.id())), + Threshold::Value(proposal.value().id()), ) => { - self.apply_event(round, event) + self.apply_event(proposal.round(), event) } _ => None, } } - fn apply_vote(&mut self, vote: Vote) -> Option { - let Some(validator) = self.validator_set.get_by_address(&vote.address) else { + fn apply_vote(&mut self, vote: C::Vote) -> Option> { + let Some(validator) = self.validator_set.get_by_address(vote.address()) else { // TODO: Is this the correct behavior? How to log such "errors"? return None; }; - let round = vote.round; + let round = vote.round(); - let event = match self.votes.apply_vote(vote, validator.voting_power) { + let event = match self.votes.apply_vote(vote, validator.voting_power()) { Some(event) => event, None => return None, }; @@ -162,7 +193,7 @@ impl Executor { self.apply_event(round, event) } - fn apply_timeout(&mut self, timeout: Timeout) -> Option { + fn apply_timeout(&mut self, timeout: Timeout) -> Option> { let event = match timeout.step { TimeoutStep::Propose => RoundEvent::TimeoutPropose, TimeoutStep::Prevote => RoundEvent::TimeoutPrevote, @@ -173,12 +204,12 @@ impl Executor { } /// Apply the event, update the state. - fn apply_event(&mut self, round: Round, event: RoundEvent) -> Option { + fn apply_event(&mut self, round: Round, event: RoundEvent) -> Option> { // Get the round state, or create a new one let round_state = self .round_states .remove(&round) - .unwrap_or_else(|| RoundState::new(self.height)); + .unwrap_or_else(|| RoundState::new(self.height.clone())); // Apply the event to the round state machine let transition = round_state.apply_event(round, event); @@ -189,202 +220,8 @@ impl Executor { // Return message, if any transition.message } -} -#[cfg(test)] -mod tests { - use super::*; - use malachite_common::{Proposal, Validator, Value}; - use malachite_round::state::{RoundValue, State, Step}; - - #[test] - fn test_executor_steps() { - let value = Value::new(9999); // TODO: get value from external source - let value_id = value.id(); - let v1 = Validator::new(PublicKey::new(vec![1]), 1); - let v2 = Validator::new(PublicKey::new(vec![2]), 1); - let v3 = Validator::new(PublicKey::new(vec![3]), 1); - let my_address = v1.clone().address(); - let key = v1.clone().public_key; // we are proposer - - let vs = ValidatorSet::new(vec![v1, v2.clone(), v3.clone()]); - - let mut executor = Executor::new(Height::new(1), vs, key.clone()); - - let proposal = Proposal::new(Height::new(1), Round::new(0), value.clone(), Round::new(-1)); - struct TestStep { - input_message: Option, - expected_output_message: Option, - new_state: State, - } - let steps: Vec = vec![ - // Start round 0, we are proposer, propose value - TestStep { - input_message: Some(Message::NewRound(Round::new(0))), - expected_output_message: Some(Message::Proposal(proposal.clone())), - new_state: State { - height: Height::new(1), - round: Round::new(0), - step: Step::Propose, - proposal: None, - locked: None, - valid: None, - }, - }, - // Receive our own proposal, prevote for it (v1) - TestStep { - input_message: None, - expected_output_message: Some(Message::Vote(Vote::new_prevote( - Round::new(0), - Some(value_id), - my_address, - ))), - new_state: State { - height: Height::new(1), - round: Round::new(0), - step: Step::Prevote, - proposal: Some(proposal.clone()), - locked: None, - valid: None, - }, - }, - // Receive our own prevote v1 - TestStep { - input_message: None, - expected_output_message: None, - new_state: State { - height: Height::new(1), - round: Round::new(0), - step: Step::Prevote, - proposal: Some(proposal.clone()), - locked: None, - valid: None, - }, - }, - // v2 prevotes for our proposal - TestStep { - input_message: Some(Message::Vote(Vote::new_prevote( - Round::new(0), - Some(value_id), - v2.clone().address(), - ))), - expected_output_message: None, - new_state: State { - height: Height::new(1), - round: Round::new(0), - step: Step::Prevote, - proposal: Some(proposal.clone()), - locked: None, - valid: None, - }, - }, - // v3 prevotes for our proposal, we get +2/3 prevotes, precommit for it (v1) - TestStep { - input_message: Some(Message::Vote(Vote::new_prevote( - Round::new(0), - Some(value_id), - v3.clone().address(), - ))), - expected_output_message: Some(Message::Vote(Vote::new_precommit( - Round::new(0), - Some(value_id), - my_address, - ))), - new_state: State { - height: Height::new(1), - round: Round::new(0), - step: Step::Precommit, - proposal: Some(proposal.clone()), - locked: Some(RoundValue { - value: value.clone(), - round: Round::new(0), - }), - valid: Some(RoundValue { - value: value.clone(), - round: Round::new(0), - }), - }, - }, - // v1 receives its own precommit - TestStep { - input_message: None, - expected_output_message: None, - new_state: State { - height: Height::new(1), - round: Round::new(0), - step: Step::Precommit, - proposal: Some(proposal.clone()), - locked: Some(RoundValue { - value: value.clone(), - round: Round::new(0), - }), - valid: Some(RoundValue { - value: value.clone(), - round: Round::new(0), - }), - }, - }, - // v2 precommits for our proposal - TestStep { - input_message: Some(Message::Vote(Vote::new_precommit( - Round::new(0), - Some(value_id), - v2.clone().address(), - ))), - expected_output_message: None, - new_state: State { - height: Height::new(1), - round: Round::new(0), - step: Step::Precommit, - proposal: Some(proposal.clone()), - locked: Some(RoundValue { - value: value.clone(), - round: Round::new(0), - }), - valid: Some(RoundValue { - value: value.clone(), - round: Round::new(0), - }), - }, - }, - // v3 precommits for our proposal, we get +2/3 precommits, decide it (v1) - TestStep { - input_message: Some(Message::Vote(Vote::new_precommit( - Round::new(0), - Some(value_id), - v2.clone().address(), - ))), - expected_output_message: None, - new_state: State { - height: Height::new(1), - round: Round::new(0), - step: Step::Commit, - proposal: Some(proposal.clone()), - locked: Some(RoundValue { - value: value.clone(), - round: Round::new(0), - }), - valid: Some(RoundValue { - value: value.clone(), - round: Round::new(0), - }), - }, - }, - ]; - - let mut previous_message = None; - for step in steps { - let execute_message = if step.input_message.is_none() { - previous_message.clone() - } else { - step.input_message - } - .unwrap(); - let message = executor.execute(execute_message); - assert_eq!(message, step.expected_output_message); - let new_state = executor.round_states.get(&Round::new(0)).unwrap(); - assert_eq!(new_state, &step.new_state); - previous_message = message; - } + pub fn round_state(&self, round: Round) -> Option<&RoundState> { + self.round_states.get(&round) } } diff --git a/Code/consensus/src/lib.rs b/Code/consensus/src/lib.rs index 756b4ca4b..9f5b7756d 100644 --- a/Code/consensus/src/lib.rs +++ b/Code/consensus/src/lib.rs @@ -4,8 +4,8 @@ #![deny(unused_crate_dependencies, trivial_casts, trivial_numeric_casts)] #![warn( // missing_docs, - broken_intra_doc_links, - private_intra_doc_links, + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, variant_size_differences )] #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::panic))] diff --git a/Code/round/src/events.rs b/Code/round/src/events.rs index aeb9ea8b8..0ca8fd944 100644 --- a/Code/round/src/events.rs +++ b/Code/round/src/events.rs @@ -1,20 +1,18 @@ -use malachite_common::Proposal; - -use crate::{Value, ValueId}; +use malachite_common::{Consensus, ValueId}; #[derive(Clone, Debug, PartialEq, Eq)] -pub enum Event { - NewRound, // Start a new round, not as proposer. - NewRoundProposer(Value), // Start a new round and propose the Value. - Proposal(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. +pub enum Event { + NewRound, // Start a new round, not as proposer. + NewRoundProposer(C::Value), // Start a new round and propose the Value. + Proposal(C::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. } diff --git a/Code/round/src/lib.rs b/Code/round/src/lib.rs index dca5bb0e8..79dddd8ae 100644 --- a/Code/round/src/lib.rs +++ b/Code/round/src/lib.rs @@ -4,14 +4,12 @@ #![deny(unused_crate_dependencies, trivial_casts, trivial_numeric_casts)] #![warn( // missing_docs, - broken_intra_doc_links, - private_intra_doc_links, + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, variant_size_differences )] #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::panic))] -pub use malachite_common::*; - pub mod events; pub mod message; pub mod state; diff --git a/Code/round/src/message.rs b/Code/round/src/message.rs index 4c3b62a0e..2fb422cf1 100644 --- a/Code/round/src/message.rs +++ b/Code/round/src/message.rs @@ -1,39 +1,52 @@ -use malachite_common::{Address, Height}; +use malachite_common::{Consensus, Round, Timeout, TimeoutStep, ValueId}; -use crate::{state::RoundValue, Proposal, Round, Timeout, TimeoutStep, Value, ValueId, Vote}; +use crate::state::RoundValue; -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Message { - NewRound(Round), // Move to the new round. - Proposal(Proposal), // Broadcast the proposal. - Vote(Vote), // Broadcast the vote. - Timeout(Timeout), // Schedule the timeout. - Decision(RoundValue), // Decide the value. +#[derive(Debug, PartialEq, Eq)] +pub enum Message { + NewRound(Round), // Move to the new round. + Proposal(C::Proposal), // Broadcast the proposal. + Vote(C::Vote), // Broadcast the vote. + Timeout(Timeout), // Schedule the timeout. + Decision(RoundValue), // Decide the value. } -impl Message { - pub fn proposal(height: Height, round: Round, value: Value, pol_round: Round) -> Message { - Message::Proposal(Proposal { - height, - round, - value, - pol_round, - }) +impl Clone for Message +where + C: Consensus, +{ + fn clone(&self) -> Self { + match self { + Message::NewRound(round) => Message::NewRound(*round), + Message::Proposal(proposal) => Message::Proposal(proposal.clone()), + Message::Vote(vote) => Message::Vote(vote.clone()), + Message::Timeout(timeout) => Message::Timeout(*timeout), + Message::Decision(round_value) => Message::Decision(round_value.clone()), + } + } +} + +impl Message +where + C: Consensus, +{ + pub fn proposal(height: C::Height, round: Round, value: C::Value, pol_round: Round) -> Self { + Message::Proposal(C::new_proposal(height, round, value, pol_round)) } - pub fn prevote(round: Round, value_id: Option, address: Address) -> Message { - Message::Vote(Vote::new_prevote(round, value_id, address)) + pub fn prevote(round: Round, value_id: Option>, address: C::Address) -> Self { + Message::Vote(C::new_prevote(round, value_id, address)) } - pub fn precommit(round: Round, value_id: Option, address: Address) -> Message { - Message::Vote(Vote::new_precommit(round, value_id, address)) + pub fn precommit(round: Round, value_id: Option>, address: C::Address) -> Self { + Message::Vote(C::new_precommit(round, value_id, address)) } - pub fn timeout(round: Round, step: TimeoutStep) -> Message { + pub fn timeout(round: Round, step: TimeoutStep) -> Self { Message::Timeout(Timeout { round, step }) } - pub fn decision(round: Round, value: Value) -> Message { + pub fn decision(round: Round, value: C::Value) -> Self { Message::Decision(RoundValue { round, value }) } } diff --git a/Code/round/src/state.rs b/Code/round/src/state.rs index f77bb053f..f543a8854 100644 --- a/Code/round/src/state.rs +++ b/Code/round/src/state.rs @@ -1,16 +1,16 @@ use crate::events::Event; use crate::state_machine::Transition; -use crate::{Height, Round, Value}; -use malachite_common::Proposal; + +use malachite_common::{Consensus, Round}; /// A value and its associated round #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RoundValue { +pub struct RoundValue { pub value: Value, pub round: Round, } -impl RoundValue { +impl RoundValue { pub fn new(value: Value, round: Round) -> Self { Self { value, round } } @@ -27,18 +27,37 @@ pub enum Step { } /// The state of the consensus state machine -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct State { - pub height: Height, +#[derive(Debug, PartialEq, Eq)] +pub struct State { + pub height: C::Height, pub round: Round, pub step: Step, - pub proposal: Option, - pub locked: Option, - pub valid: Option, + pub proposal: Option, + pub locked: Option>, + pub valid: Option>, +} + +impl Clone for State +where + C: Consensus, +{ + fn clone(&self) -> Self { + Self { + height: self.height.clone(), + round: self.round, + step: self.step, + proposal: self.proposal.clone(), + locked: self.locked.clone(), + valid: self.valid.clone(), + } + } } -impl State { - pub fn new(height: Height) -> Self { +impl State +where + C: Consensus, +{ + pub fn new(height: C::Height) -> Self { Self { height, round: Round::INITIAL, @@ -75,21 +94,21 @@ impl State { } } - pub fn set_locked(self, value: Value) -> Self { + pub fn set_locked(self, value: C::Value) -> Self { Self { locked: Some(RoundValue::new(value, self.round)), ..self } } - pub fn set_valid(self, value: Value) -> Self { + pub fn set_valid(self, value: C::Value) -> Self { Self { valid: Some(RoundValue::new(value, self.round)), ..self } } - pub fn apply_event(self, round: Round, event: Event) -> Transition { + pub fn apply_event(self, round: Round, event: Event) -> Transition { crate::state_machine::apply_event(self, round, event) } } diff --git a/Code/round/src/state_machine.rs b/Code/round/src/state_machine.rs index fc6c7ad91..884029d30 100644 --- a/Code/round/src/state_machine.rs +++ b/Code/round/src/state_machine.rs @@ -1,23 +1,25 @@ -use malachite_common::Address; +use malachite_common::{Consensus, Proposal, Round, TimeoutStep, Value, ValueId}; use crate::events::Event; use crate::message::Message; use crate::state::{State, Step}; -use crate::{Round, TimeoutStep, Value, ValueId}; // FIXME: Where to get the address/public key from? // IDEA: Add a Context parameter to `apply_state` -const ADDRESS: Address = Address::new(42); +// const ADDRESS: Address = Address::new(42); #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Transition { - pub state: State, - pub message: Option, +pub struct Transition { + pub state: State, + pub message: Option>, pub valid: bool, } -impl Transition { - pub fn to(state: State) -> Self { +impl Transition +where + C: Consensus, +{ + pub fn to(state: State) -> Self { Self { state, message: None, @@ -25,7 +27,7 @@ impl Transition { } } - pub fn invalid(state: State) -> Self { + pub fn invalid(state: State) -> Self { Self { state, message: None, @@ -33,14 +35,17 @@ impl Transition { } } - pub fn with_message(mut self, message: Message) -> Self { + pub fn with_message(mut self, message: Message) -> Self { self.message = Some(message); self } } /// Check that a proposal has a valid Proof-Of-Lock round -fn is_valid_pol_round(state: &State, pol_round: Round) -> bool { +fn is_valid_pol_round(state: &State, pol_round: Round) -> bool +where + C: Consensus, +{ pol_round.is_defined() && pol_round < state.round } @@ -52,7 +57,10 @@ fn is_valid_pol_round(state: &State, pol_round: Round) -> bool { /// Valid transitions result in at least a change to the state and/or an output message. /// /// Commented numbers refer to line numbers in the spec paper. -pub fn apply_event(mut state: State, round: Round, event: Event) -> Transition { +pub fn apply_event(mut state: State, round: Round, event: Event) -> Transition +where + C: Consensus, +{ let this_round = state.round == round; match (state.step, event) { @@ -61,23 +69,25 @@ pub fn apply_event(mut state: State, round: Round, event: Event) -> Transition { (Step::NewRound, Event::NewRound) if this_round => schedule_timeout_propose(state), // L11/L20 // From Propose. Event must be for current round. - (Step::Propose, Event::Proposal(proposal)) if this_round && proposal.pol_round.is_nil() => { + (Step::Propose, Event::Proposal(proposal)) + if this_round && proposal.pol_round().is_nil() => + { // L22 - if proposal.value.valid() + if proposal.value().is_valid() && state .locked .as_ref() - .map_or(true, |locked| locked.value == proposal.value) + .map_or(true, |locked| &locked.value == proposal.value()) { state.proposal = Some(proposal.clone()); - prevote(state, proposal.round, proposal.value.id()) + prevote(state, proposal.round(), proposal.value().id()) } else { prevote_nil(state) } } (Step::Propose, Event::Proposal(proposal)) - if this_round && is_valid_pol_round(&state, proposal.pol_round) => + if this_round && is_valid_pol_round(&state, proposal.pol_round()) => { // L28 let Some(locked) = state.locked.as_ref() else { @@ -85,10 +95,10 @@ pub fn apply_event(mut state: State, round: Round, event: Event) -> Transition { return Transition::invalid(state); }; - if proposal.value.valid() - && (locked.round <= proposal.pol_round || locked.value == proposal.value) + if proposal.value().is_valid() + && (locked.round <= proposal.pol_round() || &locked.value == proposal.value()) { - prevote(state, proposal.round, proposal.value.id()) + prevote(state, proposal.round(), proposal.value().id()) } else { prevote_nil(state) } @@ -129,13 +139,16 @@ pub fn apply_event(mut state: State, round: Round, event: Event) -> Transition { /// otherwise propose the given value. /// /// Ref: L11/L14 -pub fn propose(state: State, value: Value) -> Transition { +pub fn propose(state: State, value: C::Value) -> Transition +where + C: Consensus, +{ let (value, pol_round) = match &state.valid { Some(round_value) => (round_value.value.clone(), round_value.round), - None => (value, Round::None), + None => (value, Round::Nil), }; - let proposal = Message::proposal(state.height, state.round, value, pol_round); + let proposal = Message::proposal(state.height.clone(), state.round, value, pol_round); Transition::to(state.next_step()).with_message(proposal) } @@ -147,7 +160,10 @@ pub fn propose(state: State, value: Value) -> Transition { /// unless we are locked on something else at a higher round. /// /// Ref: L22/L28 -pub fn prevote(state: State, vr: Round, proposed: ValueId) -> Transition { +pub fn prevote(state: State, vr: Round, proposed: ValueId) -> Transition +where + C: Consensus, +{ 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 @@ -155,15 +171,18 @@ pub fn prevote(state: State, vr: Round, proposed: ValueId) -> Transition { None => Some(proposed), // not locked, prevote the value }; - let message = Message::prevote(state.round, value, ADDRESS); + let message = Message::prevote(state.round, value, C::DUMMY_ADDRESS); Transition::to(state.next_step()).with_message(message) } /// Received a complete proposal for an empty or invalid value, or timed out; prevote nil. /// /// Ref: L22/L25, L28/L31, L57 -pub fn prevote_nil(state: State) -> Transition { - let message = Message::prevote(state.round, None, ADDRESS); +pub fn prevote_nil(state: State) -> Transition +where + C: Consensus, +{ + let message = Message::prevote(state.round, None, C::DUMMY_ADDRESS); Transition::to(state.next_step()).with_message(message) } @@ -177,13 +196,16 @@ pub fn prevote_nil(state: State) -> Transition { /// /// 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, value_id: ValueId) -> Transition { - let message = Message::precommit(state.round, Some(value_id), ADDRESS); +pub fn precommit(state: State, value_id: ValueId) -> Transition +where + C: Consensus, +{ + let message = Message::precommit(state.round, Some(value_id), C::DUMMY_ADDRESS); let Some(value) = state .proposal .as_ref() - .map(|proposal| proposal.value.clone()) + .map(|proposal| proposal.value().clone()) else { // TODO: Add logging return Transition::invalid(state); @@ -197,8 +219,11 @@ pub fn precommit(state: State, value_id: ValueId) -> Transition { /// Received a polka for nil or timed out of prevote; precommit nil. /// /// Ref: L44, L61 -pub fn precommit_nil(state: State) -> Transition { - let message = Message::precommit(state.round, None, ADDRESS); +pub fn precommit_nil(state: State) -> Transition +where + C: Consensus, +{ + let message = Message::precommit(state.round, None, C::DUMMY_ADDRESS); Transition::to(state.next_step()).with_message(message) } @@ -209,7 +234,10 @@ pub fn precommit_nil(state: State) -> Transition { /// We're not the proposer; schedule timeout propose. /// /// Ref: L11, L20 -pub fn schedule_timeout_propose(state: State) -> Transition { +pub fn schedule_timeout_propose(state: State) -> Transition +where + C: Consensus, +{ let timeout = Message::timeout(state.round, TimeoutStep::Propose); Transition::to(state.next_step()).with_message(timeout) } @@ -220,7 +248,10 @@ pub fn schedule_timeout_propose(state: State) -> Transition { /// /// NOTE: This should only be called once in a round, per the spec, /// but it's harmless to schedule more timeouts -pub fn schedule_timeout_prevote(state: State) -> Transition { +pub fn schedule_timeout_prevote(state: State) -> Transition +where + C: Consensus, +{ let message = Message::timeout(state.round, TimeoutStep::Prevote); Transition::to(state.next_step()).with_message(message) } @@ -228,7 +259,10 @@ pub fn schedule_timeout_prevote(state: State) -> Transition { /// We received +2/3 precommits for any; schedule timeout precommit. /// /// Ref: L47 -pub fn schedule_timeout_precommit(state: State) -> Transition { +pub fn schedule_timeout_precommit(state: State) -> Transition +where + C: Consensus, +{ let message = Message::timeout(state.round, TimeoutStep::Precommit); Transition::to(state.next_step()).with_message(message) } @@ -243,9 +277,11 @@ pub fn schedule_timeout_precommit(state: State) -> Transition { /// 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 { - // check that we're locked on this value - +pub fn set_valid_value(state: State, value_id: ValueId) -> Transition +where + C: Consensus, +{ + // Check that we're locked on this value let Some(locked) = state.locked.as_ref() else { // TODO: Add logging return Transition::invalid(state); @@ -267,14 +303,20 @@ pub fn set_valid_value(state: State, value_id: ValueId) -> Transition { /// from a higher round. Move to the higher round. /// /// Ref: 65 -pub fn round_skip(state: State, round: Round) -> Transition { +pub fn round_skip(state: State, round: Round) -> Transition +where + C: Consensus, +{ Transition::to(state.new_round(round)).with_message(Message::NewRound(round)) } /// 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, value_id: ValueId) -> Transition +where + C: Consensus, +{ // Check that we're locked on this value let Some(locked) = state.locked.as_ref() else { // TODO: Add logging @@ -289,62 +331,3 @@ pub fn commit(state: State, round: Round, value_id: ValueId) -> Transition { let message = Message::decision(round, locked.value.clone()); Transition::to(state.commit_step()).with_message(message) } - -#[cfg(test)] -mod tests { - use malachite_common::{Height, Proposal, Timeout}; - - use super::*; - - #[test] - fn test_propose() { - let value = Value::new(42); - let mut state = State::new(Height::new(10)); - - let transition = apply_event(state.clone(), Round::new(0), Event::NewRoundProposer(value)); - - state.step = Step::Propose; - assert_eq!(transition.state, state); - - assert_eq!( - transition.message.unwrap(), - Message::proposal(Height::new(10), Round::new(0), Value::new(42), Round::None) - ); - } - - #[test] - fn test_prevote() { - let value = Value::new(42); - let state = State::new(Height::new(1)).new_round(Round::new(1)); - - let transition = apply_event(state, Round::new(1), Event::NewRound); - - assert_eq!(transition.state.step, Step::Propose); - assert_eq!( - transition.message.unwrap(), - Message::Timeout(Timeout { - round: Round::new(1), - step: TimeoutStep::Propose - }) - ); - - let state = transition.state; - - let transition = apply_event( - state, - Round::new(1), - Event::Proposal(Proposal::new( - Height::new(1), - Round::new(1), - value.clone(), - Round::None, - )), - ); - - assert_eq!(transition.state.step, Step::Prevote); - assert_eq!( - transition.message.unwrap(), - Message::prevote(Round::new(1), Some(value.id()), ADDRESS) - ); - } -} diff --git a/Code/test/Cargo.toml b/Code/test/Cargo.toml new file mode 100644 index 000000000..43da1d089 --- /dev/null +++ b/Code/test/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "malachite-test" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +malachite-common = { version = "0.1.0", path = "../common" } +malachite-consensus = { version = "0.1.0", path = "../consensus" } +malachite-round = { version = "0.1.0", path = "../round" } +malachite-vote = { version = "0.1.0", path = "../vote" } diff --git a/Code/test/src/consensus.rs b/Code/test/src/consensus.rs new file mode 100644 index 000000000..ebe110c5d --- /dev/null +++ b/Code/test/src/consensus.rs @@ -0,0 +1,38 @@ +use malachite_common::Consensus; +use malachite_common::Round; + +use crate::height::*; +use crate::proposal::*; +use crate::validator_set::*; +use crate::value::*; +use crate::vote::*; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct TestConsensus; + +impl Consensus for TestConsensus { + type Address = Address; + type Height = Height; + type Proposal = Proposal; + type PublicKey = PublicKey; + type ValidatorSet = ValidatorSet; + type Validator = Validator; + type Value = Value; + type Vote = Vote; + + const DUMMY_ADDRESS: Address = Address::new(42); + + const DUMMY_VALUE: Self::Value = Value::new(9999); + + fn new_proposal(height: Height, round: Round, value: Value, pol_round: Round) -> Proposal { + Proposal::new(height, round, value, pol_round) + } + + fn new_prevote(round: Round, value_id: Option, address: Address) -> Vote { + Vote::new_prevote(round, value_id, address) + } + + fn new_precommit(round: Round, value_id: Option, address: Address) -> Vote { + Vote::new_precommit(round, value_id, address) + } +} diff --git a/Code/test/src/height.rs b/Code/test/src/height.rs new file mode 100644 index 000000000..3f4a32c3c --- /dev/null +++ b/Code/test/src/height.rs @@ -0,0 +1,15 @@ +/// A blockchain height +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub struct Height(u64); + +impl Height { + pub fn new(height: u64) -> Self { + Self(height) + } + + pub fn as_u64(&self) -> u64 { + self.0 + } +} + +impl malachite_common::Height for Height {} diff --git a/Code/test/src/lib.rs b/Code/test/src/lib.rs new file mode 100644 index 000000000..4e9cd3aeb --- /dev/null +++ b/Code/test/src/lib.rs @@ -0,0 +1,16 @@ +#![forbid(unsafe_code)] +#![deny(trivial_casts, trivial_numeric_casts)] + +mod consensus; +mod height; +mod proposal; +mod validator_set; +mod value; +mod vote; + +pub use crate::consensus::*; +pub use crate::height::*; +pub use crate::proposal::*; +pub use crate::validator_set::*; +pub use crate::value::*; +pub use crate::vote::*; diff --git a/Code/test/src/proposal.rs b/Code/test/src/proposal.rs new file mode 100644 index 000000000..a0401f0d3 --- /dev/null +++ b/Code/test/src/proposal.rs @@ -0,0 +1,41 @@ +use malachite_common::Round; + +use crate::{Height, TestConsensus, Value}; + +/// A proposal for a value in a round +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Proposal { + pub height: Height, + pub round: Round, + pub value: Value, + pub pol_round: Round, +} + +impl Proposal { + pub fn new(height: Height, round: Round, value: Value, pol_round: Round) -> Self { + Self { + height, + round, + value, + pol_round, + } + } +} + +impl malachite_common::Proposal for Proposal { + fn height(&self) -> Height { + self.height + } + + fn round(&self) -> Round { + self.round + } + + fn value(&self) -> &Value { + &self.value + } + + fn pol_round(&self) -> Round { + self.pol_round + } +} diff --git a/Code/test/src/validator_set.rs b/Code/test/src/validator_set.rs new file mode 100644 index 000000000..e50b9a781 --- /dev/null +++ b/Code/test/src/validator_set.rs @@ -0,0 +1,205 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; + +use malachite_common::VotingPower; + +use crate::TestConsensus; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct PublicKey(Vec); + +impl PublicKey { + pub const fn new(value: Vec) -> Self { + Self(value) + } + + pub fn hash(&self) -> u64 { + self.0.iter().fold(0, |acc, x| acc ^ *x as u64) + } +} + +impl malachite_common::PublicKey for PublicKey {} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Address(u64); + +impl Address { + pub const fn new(value: u64) -> Self { + Self(value) + } +} + +impl malachite_common::Address for Address {} + +/// A validator is a public key and voting power +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Validator { + pub address: Address, + pub public_key: PublicKey, + pub voting_power: VotingPower, +} + +impl Validator { + pub fn new(public_key: PublicKey, voting_power: VotingPower) -> Self { + Self { + address: Address(public_key.hash()), + public_key, + voting_power, + } + } + + pub fn hash(&self) -> u64 { + self.public_key.hash() // TODO + } +} + +impl malachite_common::Validator for Validator { + fn address(&self) -> &Address { + &self.address + } + + fn public_key(&self) -> &PublicKey { + &self.public_key + } + + fn voting_power(&self) -> VotingPower { + self.voting_power + } +} + +/// A validator set contains a list of validators sorted by address. +pub struct ValidatorSet { + validators: Vec, + proposer: AtomicUsize, +} + +impl ValidatorSet { + pub fn new(validators: impl IntoIterator) -> Self { + let mut validators: Vec<_> = validators.into_iter().collect(); + ValidatorSet::sort_validators(&mut validators); + + assert!(!validators.is_empty()); + + Self { + validators, + proposer: AtomicUsize::new(0), + } + } + + /// 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() + } + + /// Add a validator to the set + pub fn add(&mut self, validator: Validator) { + self.validators.push(validator); + + ValidatorSet::sort_validators(&mut self.validators); + } + + /// Update the voting power of the given validator + pub fn update(&mut self, val: Validator) { + if let Some(v) = self + .validators + .iter_mut() + .find(|v| v.address == val.address) + { + v.voting_power = val.voting_power; + } + + dbg!(self.total_voting_power()); + Self::sort_validators(&mut self.validators); + dbg!(self.total_voting_power()); + } + + /// Remove a validator from the set + pub fn remove(&mut self, address: &Address) { + self.validators.retain(|v| &v.address != address); + + Self::sort_validators(&mut self.validators); // TODO: Not needed + } + + /// Get a validator by its address + pub fn get_by_address(&self, address: &Address) -> Option<&Validator> { + self.validators.iter().find(|v| &v.address == address) + } + + pub fn get_by_public_key(&self, public_key: &PublicKey) -> Option<&Validator> { + self.validators.iter().find(|v| &v.public_key == public_key) + } + + /// In place sort and deduplication of a list of validators + fn sort_validators(vals: &mut Vec) { + use core::cmp::Reverse; + + // Sort the validators according to the current Tendermint requirements + // (v. 0.34 -> first by validator power, descending, then by address, ascending) + vals.sort_unstable_by_key(|v| (Reverse(v.voting_power), v.address)); + + vals.dedup(); + } + + pub fn get_proposer(&self) -> Validator { + // TODO: Proper implementation + assert!(!self.validators.is_empty()); + let proposer = self.validators[self.proposer.load(Ordering::Relaxed)].clone(); + self.proposer.fetch_add(1, Ordering::Relaxed); + proposer + } +} + +impl malachite_common::ValidatorSet for ValidatorSet { + fn total_voting_power(&self) -> VotingPower { + self.total_voting_power() + } + + fn get_by_public_key(&self, public_key: &PublicKey) -> Option<&Validator> { + self.get_by_public_key(public_key) + } + + fn get_proposer(&self) -> Validator { + self.get_proposer() + } + + fn get_by_address(&self, address: &Address) -> Option<&Validator> { + self.get_by_address(address) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn add_update_remove() { + let v1 = Validator::new(PublicKey(vec![1]), 1); + let v2 = Validator::new(PublicKey(vec![2]), 2); + let v3 = Validator::new(PublicKey(vec![3]), 3); + + let mut vs = ValidatorSet::new(vec![v1, v2, v3]); + assert_eq!(vs.total_voting_power(), 6); + + let v4 = Validator::new(PublicKey(vec![4]), 4); + vs.add(v4); + assert_eq!(vs.total_voting_power(), 10); + + let mut v5 = Validator::new(PublicKey(vec![5]), 5); + vs.update(v5.clone()); // no effect + assert_eq!(vs.total_voting_power(), 10); + + vs.add(v5.clone()); + assert_eq!(vs.total_voting_power(), 15); + + v5.voting_power = 100; + vs.update(v5.clone()); + assert_eq!(vs.total_voting_power(), 110); + + vs.remove(&v5.address); + assert_eq!(vs.total_voting_power(), 10); + + let v6 = Validator::new(PublicKey(vec![6]), 6); + vs.remove(&v6.address); // no effect + assert_eq!(vs.total_voting_power(), 10); + } +} diff --git a/Code/test/src/value.rs b/Code/test/src/value.rs new file mode 100644 index 000000000..2f45f6abe --- /dev/null +++ b/Code/test/src/value.rs @@ -0,0 +1,42 @@ +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Copy)] +pub struct ValueId(u64); + +impl ValueId { + pub const fn new(id: u64) -> Self { + Self(id) + } +} + +/// The value to decide on +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Value(u64); + +impl Value { + pub const fn new(value: u64) -> Self { + Self(value) + } + + pub const fn as_u64(&self) -> u64 { + self.0 + } + + pub const fn valid(&self) -> bool { + self.0 > 0 + } + + pub const fn id(&self) -> ValueId { + ValueId(self.0) + } +} + +impl malachite_common::Value for Value { + type Id = ValueId; + + fn is_valid(&self) -> bool { + self.valid() + } + + fn id(&self) -> ValueId { + self.id() + } +} diff --git a/Code/test/src/vote.rs b/Code/test/src/vote.rs new file mode 100644 index 000000000..761101aeb --- /dev/null +++ b/Code/test/src/vote.rs @@ -0,0 +1,54 @@ +use malachite_common::{Round, VoteType}; + +use crate::{Address, TestConsensus, ValueId}; + +/// A vote for a value in a round +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Vote { + pub typ: VoteType, + pub round: Round, + pub value: Option, + pub address: Address, +} + +impl Vote { + pub fn new_prevote(round: Round, value: Option, address: Address) -> Self { + Self { + typ: VoteType::Prevote, + round, + value, + address, + } + } + + pub fn new_precommit(round: Round, value: Option, address: Address) -> Self { + Self { + typ: VoteType::Precommit, + round, + value, + address, + } + } +} + +impl malachite_common::Vote for Vote { + fn round(&self) -> Round { + self.round + } + + fn value(&self) -> Option<&ValueId> { + self.value.as_ref() + } + + fn vote_type(&self) -> VoteType { + self.typ + } + + fn address(&self) -> &Address { + &self.address + } + + fn set_address(&mut self, address: Address) { + self.address = address; + } +} diff --git a/Code/test/tests/consensus_executor.rs b/Code/test/tests/consensus_executor.rs new file mode 100644 index 000000000..fc26fe778 --- /dev/null +++ b/Code/test/tests/consensus_executor.rs @@ -0,0 +1,597 @@ +use malachite_common::{Consensus, Round, Timeout}; +use malachite_consensus::executor::{Executor, Message}; +use malachite_round::state::{RoundValue, State, Step}; + +use malachite_test::{Height, Proposal, PublicKey, TestConsensus, Validator, ValidatorSet, Vote}; + +struct TestStep { + desc: &'static str, + input_message: Option>, + expected_output_message: Option>, + new_state: State, +} + +#[test] +fn executor_steps_proposer() { + let value = TestConsensus::DUMMY_VALUE; // TODO: get value from external source + let value_id = value.id(); + let v1 = Validator::new(PublicKey::new(vec![1]), 1); + let v2 = Validator::new(PublicKey::new(vec![2]), 1); + let v3 = Validator::new(PublicKey::new(vec![3]), 1); + let my_address = v1.address; + let key = v1.clone().public_key; // we are proposer + + let vs = ValidatorSet::new(vec![v1, v2.clone(), v3.clone()]); + + let mut executor = Executor::new(Height::new(1), vs, key.clone()); + + let proposal = Proposal::new(Height::new(1), Round::new(0), value.clone(), Round::new(-1)); + + let steps = vec![ + // Start round 0, we are proposer, propose value + TestStep { + desc: "Start round 0, we are proposer, propose value", + input_message: Some(Message::NewRound(Round::new(0))), + expected_output_message: Some(Message::Proposal(proposal.clone())), + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Propose, + proposal: None, + locked: None, + valid: None, + }, + }, + // Receive our own proposal, prevote for it (v1) + TestStep { + desc: "Receive our own proposal, prevote for it (v1)", + input_message: None, + expected_output_message: Some(Message::Vote(Vote::new_prevote( + Round::new(0), + Some(value_id), + my_address, + ))), + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Prevote, + proposal: Some(proposal.clone()), + locked: None, + valid: None, + }, + }, + // Receive our own prevote v1 + TestStep { + desc: "Receive our own prevote v1", + input_message: None, + expected_output_message: None, + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Prevote, + proposal: Some(proposal.clone()), + locked: None, + valid: None, + }, + }, + // v2 prevotes for our proposal + TestStep { + desc: "v2 prevotes for our proposal", + input_message: Some(Message::Vote(Vote::new_prevote( + Round::new(0), + Some(value_id), + v2.address, + ))), + expected_output_message: None, + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Prevote, + proposal: Some(proposal.clone()), + locked: None, + valid: None, + }, + }, + // v3 prevotes for our proposal, we get +2/3 prevotes, precommit for it (v1) + TestStep { + desc: "v3 prevotes for our proposal, we get +2/3 prevotes, precommit for it (v1)", + input_message: Some(Message::Vote(Vote::new_prevote( + Round::new(0), + Some(value_id), + v3.address, + ))), + expected_output_message: Some(Message::Vote(Vote::new_precommit( + Round::new(0), + Some(value_id), + my_address, + ))), + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Precommit, + proposal: Some(proposal.clone()), + locked: Some(RoundValue { + value: value.clone(), + round: Round::new(0), + }), + valid: Some(RoundValue { + value: value.clone(), + round: Round::new(0), + }), + }, + }, + // v1 receives its own precommit + TestStep { + desc: "v1 receives its own precommit", + input_message: None, + expected_output_message: None, + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Precommit, + proposal: Some(proposal.clone()), + locked: Some(RoundValue { + value: value.clone(), + round: Round::new(0), + }), + valid: Some(RoundValue { + value: value.clone(), + round: Round::new(0), + }), + }, + }, + // v2 precommits for our proposal + TestStep { + desc: "v2 precommits for our proposal", + input_message: Some(Message::Vote(Vote::new_precommit( + Round::new(0), + Some(value_id), + v2.address, + ))), + expected_output_message: None, + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Precommit, + proposal: Some(proposal.clone()), + locked: Some(RoundValue { + value: value.clone(), + round: Round::new(0), + }), + valid: Some(RoundValue { + value: value.clone(), + round: Round::new(0), + }), + }, + }, + // v3 precommits for our proposal, we get +2/3 precommits, decide it (v1) + TestStep { + desc: "v3 precommits for our proposal, we get +2/3 precommits, decide it (v1)", + input_message: Some(Message::Vote(Vote::new_precommit( + Round::new(0), + Some(value_id), + v2.address, + ))), + expected_output_message: None, + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Commit, + proposal: Some(proposal.clone()), + locked: Some(RoundValue { + value: value.clone(), + round: Round::new(0), + }), + valid: Some(RoundValue { + value: value.clone(), + round: Round::new(0), + }), + }, + }, + ]; + + let mut previous_message = None; + + for step in steps { + let execute_message = step + .input_message + .unwrap_or_else(|| previous_message.unwrap()); + + let message = executor.execute(execute_message); + assert_eq!(message, step.expected_output_message); + + let new_state = executor.round_state(Round::new(0)).unwrap(); + assert_eq!(new_state, &step.new_state); + + previous_message = message; + } +} + +#[test] +fn executor_steps_not_proposer() { + let value = TestConsensus::DUMMY_VALUE; // TODO: get value from external source + let value_id = value.id(); + + let v1 = Validator::new(PublicKey::new(vec![1]), 1); + let v2 = Validator::new(PublicKey::new(vec![2]), 1); + let v3 = Validator::new(PublicKey::new(vec![3]), 1); + + // Proposer is v1, so we are not the proposer + let my_address = v2.address; + let my_key = v2.public_key.clone(); + + let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); + let mut executor = Executor::new(Height::new(1), vs, my_key); + + let proposal = Proposal::new(Height::new(1), Round::new(0), value.clone(), Round::new(-1)); + + let steps = vec![ + // Start round 0, we are not the proposer + TestStep { + desc: "Start round 0, we are not the proposer", + input_message: Some(Message::NewRound(Round::new(0))), + expected_output_message: None, + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Propose, + proposal: None, + locked: None, + valid: None, + }, + }, + // Receive a proposal, prevote for it (v1) + TestStep { + desc: "Receive a proposal, prevote for it (v1)", + input_message: Some(Message::Proposal(proposal.clone())), + expected_output_message: Some(Message::Vote(Vote::new_prevote( + Round::new(0), + Some(value_id), + my_address, + ))), + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Prevote, + proposal: Some(proposal.clone()), + locked: None, + valid: None, + }, + }, + // Receive our own prevote v1 + TestStep { + desc: "Receive our own prevote v1", + input_message: None, + expected_output_message: None, + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Prevote, + proposal: Some(proposal.clone()), + locked: None, + valid: None, + }, + }, + // v2 prevotes for its own proposal + TestStep { + desc: "v2 prevotes for its own proposal", + input_message: Some(Message::Vote(Vote::new_prevote( + Round::new(0), + Some(value_id), + v2.address, + ))), + expected_output_message: None, + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Prevote, + proposal: Some(proposal.clone()), + locked: None, + valid: None, + }, + }, + // v3 prevotes for v2's proposal, it gets +2/3 prevotes, precommit for it (v1) + TestStep { + desc: "v3 prevotes for v2's proposal, it gets +2/3 prevotes, precommit for it (v1)", + input_message: Some(Message::Vote(Vote::new_prevote( + Round::new(0), + Some(value_id), + v3.address, + ))), + expected_output_message: Some(Message::Vote(Vote::new_precommit( + Round::new(0), + Some(value_id), + my_address, + ))), + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Precommit, + proposal: Some(proposal.clone()), + locked: Some(RoundValue { + value: value.clone(), + round: Round::new(0), + }), + valid: Some(RoundValue { + value: value.clone(), + round: Round::new(0), + }), + }, + }, + // v1 receives its own precommit + TestStep { + desc: "v1 receives its own precommit", + input_message: None, + expected_output_message: None, + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Precommit, + proposal: Some(proposal.clone()), + locked: Some(RoundValue { + value: value.clone(), + round: Round::new(0), + }), + valid: Some(RoundValue { + value: value.clone(), + round: Round::new(0), + }), + }, + }, + // v2 precommits its proposal + TestStep { + desc: "v2 precommits its proposal", + input_message: Some(Message::Vote(Vote::new_precommit( + Round::new(0), + Some(value_id), + v2.address, + ))), + expected_output_message: None, + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Precommit, + proposal: Some(proposal.clone()), + locked: Some(RoundValue { + value: value.clone(), + round: Round::new(0), + }), + valid: Some(RoundValue { + value: value.clone(), + round: Round::new(0), + }), + }, + }, + // v3 precommits for v2's proposal, it gets +2/3 precommits, decide it (v1) + TestStep { + desc: "v3 precommits for v2's proposal, it gets +2/3 precommits, decide it (v1)", + input_message: Some(Message::Vote(Vote::new_precommit( + Round::new(0), + Some(value_id), + v2.address, + ))), + expected_output_message: None, + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Commit, + proposal: Some(proposal.clone()), + locked: Some(RoundValue { + value: value.clone(), + round: Round::new(0), + }), + valid: Some(RoundValue { + value: value.clone(), + round: Round::new(0), + }), + }, + }, + ]; + + let mut previous_message = None; + + for step in steps { + let execute_message = step + .input_message + .unwrap_or_else(|| previous_message.unwrap()); + + let message = executor.execute(execute_message); + assert_eq!(message, step.expected_output_message); + + let new_state = executor.round_state(Round::new(0)).unwrap(); + assert_eq!(new_state, &step.new_state); + + previous_message = message; + } +} + +#[test] +fn executor_steps_not_proposer_timeout() { + let value = TestConsensus::DUMMY_VALUE; // TODO: get value from external source + let value_id = value.id(); + + let v1 = Validator::new(PublicKey::new(vec![1]), 1); + let v2 = Validator::new(PublicKey::new(vec![2]), 1); + let v3 = Validator::new(PublicKey::new(vec![3]), 2); + + // Proposer is v1, so we are not the proposer + let my_address = v2.address; + let my_key = v2.public_key.clone(); + + let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); + let mut executor = Executor::new(Height::new(1), vs, my_key); + + let steps = vec![ + // Start round 0, we are not the proposer + TestStep { + desc: "Start round 0, we are not the proposer", + input_message: Some(Message::NewRound(Round::new(0))), + expected_output_message: None, + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Propose, + proposal: None, + locked: None, + valid: None, + }, + }, + // Receive a propose timeout, prevote for nil (v1) + TestStep { + desc: "Receive a propose timeout, prevote for nil (v1)", + input_message: Some(Message::Timeout(Timeout::propose(Round::new(0)))), + expected_output_message: Some(Message::Vote(Vote::new_prevote( + Round::new(0), + None, + my_address, + ))), + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Prevote, + proposal: None, + locked: None, + valid: None, + }, + }, + // Receive our own prevote v1 + TestStep { + desc: "Receive our own prevote v1", + input_message: None, + expected_output_message: None, + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Prevote, + proposal: None, + locked: None, + valid: None, + }, + }, + // v2 prevotes for its own proposal + TestStep { + desc: "v2 prevotes for its own proposal", + input_message: Some(Message::Vote(Vote::new_prevote( + Round::new(0), + Some(value_id), + v2.address, + ))), + expected_output_message: None, + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Prevote, + proposal: None, + locked: None, + valid: None, + }, + }, + // v3 prevotes for nil, it gets +2/3 prevotes, precommit for it (v1) + TestStep { + desc: "v3 prevotes for nil, it gets +2/3 prevotes, precommit for it (v1)", + input_message: Some(Message::Vote(Vote::new_prevote( + Round::new(0), + None, + v3.address, + ))), + expected_output_message: Some(Message::Vote(Vote::new_precommit( + Round::new(0), + None, + my_address, + ))), + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Precommit, + proposal: None, + locked: None, + valid: None, + }, + }, + // v1 receives its own precommit + TestStep { + desc: "v1 receives its own precommit", + input_message: None, + expected_output_message: None, + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Precommit, + proposal: None, + locked: None, + valid: None, + }, + }, + // v2 precommits its proposal + TestStep { + desc: "v2 precommits its proposal", + input_message: Some(Message::Vote(Vote::new_precommit( + Round::new(0), + Some(value_id), + v2.address, + ))), + expected_output_message: None, + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Precommit, + proposal: None, + locked: None, + valid: None, + }, + }, + // v3 precommits for nil + TestStep { + desc: "v3 precommits for nil", + input_message: Some(Message::Vote(Vote::new_precommit( + Round::new(0), + None, + v3.address, + ))), + expected_output_message: None, + new_state: State { + height: Height::new(1), + round: Round::new(0), + step: Step::Precommit, + proposal: None, + locked: None, + valid: None, + }, + }, + // we receive a precommit timeout, start a new round + TestStep { + desc: "we receive a precommit timeout, start a new round", + input_message: Some(Message::Timeout(Timeout::precommit(Round::new(0)))), + expected_output_message: None, + new_state: State { + height: Height::new(1), + round: Round::new(1), + step: Step::NewRound, + proposal: None, + locked: None, + valid: None, + }, + }, + ]; + + let mut previous_message = None; + + for step in steps { + println!("Step: {}", step.desc); + + let execute_message = step + .input_message + .unwrap_or_else(|| previous_message.unwrap()); + + let message = executor.execute(execute_message); + assert_eq!( + message, step.expected_output_message, + "expected output message" + ); + + let new_state = executor.round_state(Round::new(0)).unwrap(); + assert_eq!(new_state, &step.new_state, "new state"); + + previous_message = message; + } +} diff --git a/Code/test/tests/round.rs b/Code/test/tests/round.rs new file mode 100644 index 000000000..238e35b89 --- /dev/null +++ b/Code/test/tests/round.rs @@ -0,0 +1,63 @@ +use malachite_test::{Height, Proposal, TestConsensus, Value}; + +use malachite_common::{Consensus, Round, Timeout, TimeoutStep}; +use malachite_round::events::Event; +use malachite_round::message::Message; +use malachite_round::state::{State, Step}; +use malachite_round::state_machine::apply_event; + +#[test] +fn test_propose() { + let value = Value::new(42); + let mut state: State = State::new(Height::new(10)); + + let transition = apply_event(state.clone(), Round::new(0), Event::NewRoundProposer(value)); + + state.step = Step::Propose; + assert_eq!(transition.state, state); + + assert_eq!( + transition.message.unwrap(), + Message::proposal(Height::new(10), Round::new(0), Value::new(42), Round::Nil) + ); +} + +#[test] +fn test_prevote() { + let value = Value::new(42); + let state: State = State::new(Height::new(1)).new_round(Round::new(1)); + + let transition = apply_event(state, Round::new(1), Event::NewRound); + + assert_eq!(transition.state.step, Step::Propose); + assert_eq!( + transition.message.unwrap(), + Message::Timeout(Timeout { + round: Round::new(1), + step: TimeoutStep::Propose + }) + ); + + let state = transition.state; + + let transition = apply_event( + state, + Round::new(1), + Event::Proposal(Proposal::new( + Height::new(1), + Round::new(1), + value.clone(), + Round::Nil, + )), + ); + + assert_eq!(transition.state.step, Step::Prevote); + assert_eq!( + transition.message.unwrap(), + Message::prevote( + Round::new(1), + Some(value.id()), + TestConsensus::DUMMY_ADDRESS + ) + ); +} diff --git a/Code/test/tests/vote_count.rs b/Code/test/tests/vote_count.rs new file mode 100644 index 000000000..e1367e1c4 --- /dev/null +++ b/Code/test/tests/vote_count.rs @@ -0,0 +1,94 @@ +use malachite_common::Round; +use malachite_vote::count::Threshold; +use malachite_vote::RoundVotes; + +use malachite_test::{Address, Height, TestConsensus, ValueId, Vote}; + +#[test] +fn add_votes_nil() { + let total = 3; + + let mut round_votes: RoundVotes = + RoundVotes::new(Height::new(1), Round::new(0), total); + + // add a vote for nil. nothing changes. + let vote = Vote::new_prevote(Round::new(0), None, Address::new(1)); + let thresh = round_votes.add_vote(vote.clone(), 1); + assert_eq!(thresh, Threshold::Init); + + // add it again, nothing changes. + let thresh = round_votes.add_vote(vote.clone(), 1); + assert_eq!(thresh, Threshold::Init); + + // add it again, get Nil + let thresh = round_votes.add_vote(vote.clone(), 1); + assert_eq!(thresh, Threshold::Nil); +} + +#[test] +fn add_votes_single_value() { + let v = ValueId::new(1); + let val = Some(v); + let total = 4; + let weight = 1; + + let mut round_votes: RoundVotes = + RoundVotes::new(Height::new(1), Round::new(0), total); + + // add a vote. nothing changes. + let vote = Vote::new_prevote(Round::new(0), val, Address::new(1)); + let thresh = round_votes.add_vote(vote.clone(), weight); + assert_eq!(thresh, Threshold::Init); + + // add it again, nothing changes. + let thresh = round_votes.add_vote(vote.clone(), weight); + assert_eq!(thresh, Threshold::Init); + + // add a vote for nil, get Thresh::Any + let vote_nil = Vote::new_prevote(Round::new(0), None, Address::new(2)); + let thresh = round_votes.add_vote(vote_nil, weight); + assert_eq!(thresh, Threshold::Any); + + // add vote for value, get Thresh::Value + let thresh = round_votes.add_vote(vote, weight); + assert_eq!(thresh, Threshold::Value(v)); +} + +#[test] +fn add_votes_multi_values() { + let v1 = ValueId::new(1); + let v2 = ValueId::new(2); + let val1 = Some(v1); + let val2 = Some(v2); + let total = 15; + + let mut round_votes: RoundVotes = + RoundVotes::new(Height::new(1), Round::new(0), total); + + // add a vote for v1. nothing changes. + let vote1 = Vote::new_precommit(Round::new(0), val1, Address::new(1)); + let thresh = round_votes.add_vote(vote1.clone(), 1); + assert_eq!(thresh, Threshold::Init); + + // add a vote for v2. nothing changes. + let vote2 = Vote::new_precommit(Round::new(0), val2, Address::new(2)); + let thresh = round_votes.add_vote(vote2.clone(), 1); + assert_eq!(thresh, Threshold::Init); + + // add a vote for nil. nothing changes. + let vote_nil = Vote::new_precommit(Round::new(0), None, Address::new(3)); + let thresh = round_votes.add_vote(vote_nil.clone(), 1); + assert_eq!(thresh, Threshold::Init); + + // add a vote for v1. nothing changes + let thresh = round_votes.add_vote(vote1.clone(), 1); + assert_eq!(thresh, Threshold::Init); + + // add a vote for v2. nothing changes + let thresh = round_votes.add_vote(vote2.clone(), 1); + assert_eq!(thresh, Threshold::Init); + + // add a big vote for v2. get Value(v2) + let thresh = round_votes.add_vote(vote2.clone(), 10); + assert_eq!(thresh, Threshold::Value(v2)); +} diff --git a/Code/test/tests/vote_keeper.rs b/Code/test/tests/vote_keeper.rs new file mode 100644 index 000000000..c5c207edb --- /dev/null +++ b/Code/test/tests/vote_keeper.rs @@ -0,0 +1,81 @@ +use malachite_common::Round; +use malachite_round::events::Event; +use malachite_vote::keeper::VoteKeeper; + +use malachite_test::{Address, Height, TestConsensus, ValueId, Vote}; + +#[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::new(1)); + + let event = keeper.apply_vote(vote.clone(), 1); + assert_eq!(event, None); + + let event = keeper.apply_vote(vote.clone(), 1); + assert_eq!(event, None); + + let event = keeper.apply_vote(vote, 1); + assert_eq!(event, Some(Event::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::new(1)); + + let event = keeper.apply_vote(vote.clone(), 1); + assert_eq!(event, None); + + let event = keeper.apply_vote(vote.clone(), 1); + assert_eq!(event, None); + + let event = keeper.apply_vote(vote, 1); + assert_eq!(event, None); +} + +#[test] +fn prevote_apply_single_value() { + let mut keeper: VoteKeeper = VoteKeeper::new(Height::new(1), Round::INITIAL, 4); + + let v = ValueId::new(1); + let val = Some(v); + let vote = Vote::new_prevote(Round::new(0), val, Address::new(1)); + + let event = keeper.apply_vote(vote.clone(), 1); + assert_eq!(event, None); + + let event = keeper.apply_vote(vote.clone(), 1); + assert_eq!(event, None); + + let vote_nil = Vote::new_prevote(Round::new(0), None, Address::new(2)); + let event = keeper.apply_vote(vote_nil, 1); + assert_eq!(event, Some(Event::PolkaAny)); + + let event = keeper.apply_vote(vote, 1); + assert_eq!(event, Some(Event::PolkaValue(v))); +} + +#[test] +fn precommit_apply_single_value() { + let mut keeper: VoteKeeper = VoteKeeper::new(Height::new(1), Round::INITIAL, 4); + + let v = ValueId::new(1); + let val = Some(v); + let vote = Vote::new_precommit(Round::new(0), val, Address::new(1)); + + let event = keeper.apply_vote(vote.clone(), 1); + assert_eq!(event, None); + + let event = keeper.apply_vote(vote.clone(), 1); + assert_eq!(event, None); + + let vote_nil = Vote::new_precommit(Round::new(0), None, Address::new(2)); + let event = keeper.apply_vote(vote_nil, 1); + assert_eq!(event, Some(Event::PrecommitAny)); + + let event = keeper.apply_vote(vote, 1); + assert_eq!(event, Some(Event::PrecommitValue(v))); +} diff --git a/Code/vote/src/count.rs b/Code/vote/src/count.rs index 32e7310f6..fb627ca9b 100644 --- a/Code/vote/src/count.rs +++ b/Code/vote/src/count.rs @@ -1,25 +1,27 @@ use alloc::collections::BTreeMap; -use alloc::sync::Arc; -use malachite_common::{ValueId, Vote}; +use malachite_common::{Consensus, ValueId, Vote}; 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>, +pub struct ValuesWeights { + value_weights: BTreeMap, } -impl ValuesWeights { - pub fn new() -> ValuesWeights { +impl ValuesWeights { + pub fn new() -> ValuesWeights { ValuesWeights { value_weights: BTreeMap::new(), } } /// Add weight to the value and return the new weight. - pub fn add_weight(&mut self, value: Arc, weight: Weight) -> Weight { + pub fn add_weight(&mut self, value: ValueId, weight: Weight) -> Weight + where + ValueId: Ord, + { let entry = self.value_weights.entry(value).or_insert(0); *entry += weight; *entry @@ -30,11 +32,11 @@ impl ValuesWeights { self.value_weights .iter() .max_by_key(|(_, weight)| *weight) - .map(|(value, weight)| (value.as_ref(), *weight)) + .map(|(value, weight)| (value, *weight)) } } -impl Default for ValuesWeights { +impl Default for ValuesWeights { fn default() -> Self { Self::new() } @@ -43,17 +45,23 @@ impl Default for ValuesWeights { /// 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 +where + C: Consensus, +{ // Weight of votes for nil pub nil: Weight, /// Weight of votes for the values - pub values_weights: ValuesWeights, + pub values_weights: ValuesWeights>, /// Total weight pub total: Weight, } -impl VoteCount { - pub fn new(total: Weight) -> VoteCount { +impl VoteCount +where + C: Consensus, +{ + pub fn new(total: Weight) -> Self { VoteCount { nil: 0, total, @@ -62,14 +70,13 @@ impl VoteCount { } /// Add vote to internal counters and return the highest threshold. - pub fn add_vote(&mut self, vote: Vote, weight: Weight) -> Threshold { - if let Some(value) = vote.value { - let value = Arc::new(value); + pub fn add_vote(&mut self, vote: C::Vote, weight: Weight) -> Threshold> { + if let Some(value) = vote.value() { let new_weight = self.values_weights.add_weight(value.clone(), weight); // Check if we have a quorum for this value. if is_quorum(new_weight, self.total) { - return Threshold::Value(value); + return Threshold::Value(value.clone()); } } else { self.nil += weight; @@ -90,7 +97,7 @@ impl VoteCount { // No quorum Threshold::Init } - pub fn check_threshold(&self, threshold: Threshold) -> bool { + pub fn check_threshold(&self, threshold: Threshold>) -> bool { match threshold { Threshold::Init => false, Threshold::Any => self.values_weights.highest_weighted_value().is_some(), @@ -106,7 +113,7 @@ impl VoteCount { // Thresh represents the different quorum thresholds. #[derive(Clone, Debug, PartialEq, Eq)] -pub enum Threshold { +pub enum Threshold { /// No quorum Init, // no quorum /// Qorum of votes but not for the same value @@ -114,7 +121,7 @@ pub enum Threshold { /// Quorum for nil Nil, /// Quorum for a value - Value(Arc), + Value(ValueId), } /// Returns whether or note `value > (2/3)*total`. @@ -123,96 +130,4 @@ pub fn is_quorum(value: Weight, total: Weight) -> bool { } #[cfg(test)] -mod tests { - use malachite_common::{Address, Height, Round}; - - use crate::RoundVotes; - - use super::*; - - #[test] - fn add_votes_nil() { - let total = 3; - - let mut round_votes = RoundVotes::new(Height::new(1), Round::new(0), total); - - // add a vote for nil. nothing changes. - let vote = Vote::new_prevote(Round::new(0), None, Address::new(1)); - let thresh = round_votes.add_vote(vote.clone(), 1); - assert_eq!(thresh, Threshold::Init); - - // add it again, nothing changes. - let thresh = round_votes.add_vote(vote.clone(), 1); - assert_eq!(thresh, Threshold::Init); - - // add it again, get Nil - let thresh = round_votes.add_vote(vote.clone(), 1); - assert_eq!(thresh, Threshold::Nil); - } - - #[test] - fn add_votes_single_value() { - let v = ValueId::new(1); - let val = Some(v); - let total = 4; - let weight = 1; - - let mut round_votes = RoundVotes::new(Height::new(1), Round::new(0), total); - - // add a vote. nothing changes. - let vote = Vote::new_prevote(Round::new(0), val, Address::new(1)); - let thresh = round_votes.add_vote(vote.clone(), weight); - assert_eq!(thresh, Threshold::Init); - - // add it again, nothing changes. - let thresh = round_votes.add_vote(vote.clone(), weight); - assert_eq!(thresh, Threshold::Init); - - // add a vote for nil, get Thresh::Any - let vote_nil = Vote::new_prevote(Round::new(0), None, Address::new(2)); - let thresh = round_votes.add_vote(vote_nil, weight); - assert_eq!(thresh, Threshold::Any); - - // add vote for value, get Thresh::Value - let thresh = round_votes.add_vote(vote, weight); - assert_eq!(thresh, Threshold::Value(Arc::new(v))); - } - - #[test] - fn add_votes_multi_values() { - let v1 = ValueId::new(1); - let v2 = ValueId::new(2); - let val1 = Some(v1); - let val2 = Some(v2); - let total = 15; - - let mut round_votes = RoundVotes::new(Height::new(1), Round::new(0), total); - - // add a vote for v1. nothing changes. - let vote1 = Vote::new_precommit(Round::new(0), val1, Address::new(1)); - let thresh = round_votes.add_vote(vote1.clone(), 1); - assert_eq!(thresh, Threshold::Init); - - // add a vote for v2. nothing changes. - let vote2 = Vote::new_precommit(Round::new(0), val2, Address::new(2)); - let thresh = round_votes.add_vote(vote2.clone(), 1); - assert_eq!(thresh, Threshold::Init); - - // add a vote for nil. nothing changes. - let vote_nil = Vote::new_precommit(Round::new(0), None, Address::new(3)); - let thresh = round_votes.add_vote(vote_nil.clone(), 1); - assert_eq!(thresh, Threshold::Init); - - // add a vote for v1. nothing changes - let thresh = round_votes.add_vote(vote1.clone(), 1); - assert_eq!(thresh, Threshold::Init); - - // add a vote for v2. nothing changes - let thresh = round_votes.add_vote(vote2.clone(), 1); - assert_eq!(thresh, Threshold::Init); - - // add a big vote for v2. get Value(v2) - let thresh = round_votes.add_vote(vote2.clone(), 10); - assert_eq!(thresh, Threshold::Value(Arc::new(v2))); - } -} +mod tests {} diff --git a/Code/vote/src/keeper.rs b/Code/vote/src/keeper.rs index d24a5b778..a24f63829 100644 --- a/Code/vote/src/keeper.rs +++ b/Code/vote/src/keeper.rs @@ -1,6 +1,6 @@ use alloc::collections::BTreeMap; -use malachite_common::{Height, Round, Vote, VoteType}; +use malachite_common::{Consensus, Round, ValueId, Vote, VoteType}; use malachite_round::events::Event; use crate::{ @@ -10,17 +10,23 @@ use crate::{ /// Keeps track of votes and emits events when thresholds are reached. #[derive(Clone, Debug)] -pub struct VoteKeeper { - height: Height, +pub struct VoteKeeper +where + C: Consensus, +{ + height: C::Height, total_weight: Weight, - rounds: BTreeMap, + rounds: BTreeMap>, } -impl VoteKeeper { - pub fn new(height: Height, round: Round, total_weight: Weight) -> Self { +impl VoteKeeper +where + C: Consensus, +{ + pub fn new(height: C::Height, round: Round, total_weight: Weight) -> Self { let mut rounds = BTreeMap::new(); - rounds.insert(round, RoundVotes::new(height, round, total_weight)); + rounds.insert(round, RoundVotes::new(height.clone(), round, total_weight)); VoteKeeper { height, @@ -30,13 +36,12 @@ impl VoteKeeper { } /// Apply a vote with a given weight, potentially triggering an event. - pub fn apply_vote(&mut self, vote: Vote, weight: Weight) -> Option { - let round = self - .rounds - .entry(vote.round) - .or_insert_with(|| RoundVotes::new(self.height, vote.round, self.total_weight)); + pub fn apply_vote(&mut self, vote: C::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 vote_type = vote.typ; + let vote_type = vote.vote_type(); let threshold = round.add_vote(vote, weight); Self::to_event(vote_type, threshold) @@ -46,7 +51,7 @@ impl VoteKeeper { &self, round: &Round, vote_type: VoteType, - threshold: Threshold, + threshold: Threshold>, ) -> bool { let round = match self.rounds.get(round) { Some(round) => round, @@ -60,100 +65,17 @@ impl VoteKeeper { } /// Map a vote type and a threshold to a state machine event. - fn to_event(typ: VoteType, threshold: Threshold) -> Option { + fn to_event(typ: VoteType, threshold: Threshold>) -> Option> { match (typ, threshold) { (_, Threshold::Init) => None, (VoteType::Prevote, Threshold::Any) => Some(Event::PolkaAny), (VoteType::Prevote, Threshold::Nil) => Some(Event::PolkaNil), - (VoteType::Prevote, Threshold::Value(v)) => Some(Event::PolkaValue(*v.as_ref())), + (VoteType::Prevote, Threshold::Value(v)) => Some(Event::PolkaValue(v)), (VoteType::Precommit, Threshold::Any) => Some(Event::PrecommitAny), (VoteType::Precommit, Threshold::Nil) => None, - (VoteType::Precommit, Threshold::Value(v)) => Some(Event::PrecommitValue(*v.as_ref())), + (VoteType::Precommit, Threshold::Value(v)) => Some(Event::PrecommitValue(v)), } } } - -#[cfg(test)] -mod tests { - use malachite_common::{Address, ValueId}; - - use super::*; - - #[test] - fn prevote_apply_nil() { - let mut keeper = VoteKeeper::new(Height::new(1), Round::INITIAL, 3); - - let vote = Vote::new_prevote(Round::new(0), None, Address::new(1)); - - let event = keeper.apply_vote(vote.clone(), 1); - assert_eq!(event, None); - - let event = keeper.apply_vote(vote.clone(), 1); - assert_eq!(event, None); - - let event = keeper.apply_vote(vote, 1); - assert_eq!(event, Some(Event::PolkaNil)); - } - - #[test] - fn precommit_apply_nil() { - let mut keeper = VoteKeeper::new(Height::new(1), Round::INITIAL, 3); - - let vote = Vote::new_precommit(Round::new(0), None, Address::new(1)); - - let event = keeper.apply_vote(vote.clone(), 1); - assert_eq!(event, None); - - let event = keeper.apply_vote(vote.clone(), 1); - assert_eq!(event, None); - - let event = keeper.apply_vote(vote, 1); - assert_eq!(event, None); - } - - #[test] - fn prevote_apply_single_value() { - let mut keeper = VoteKeeper::new(Height::new(1), Round::INITIAL, 4); - - let v = ValueId::new(1); - let val = Some(v); - let vote = Vote::new_prevote(Round::new(0), val, Address::new(1)); - - let event = keeper.apply_vote(vote.clone(), 1); - assert_eq!(event, None); - - let event = keeper.apply_vote(vote.clone(), 1); - assert_eq!(event, None); - - let vote_nil = Vote::new_prevote(Round::new(0), None, Address::new(2)); - let event = keeper.apply_vote(vote_nil, 1); - assert_eq!(event, Some(Event::PolkaAny)); - - let event = keeper.apply_vote(vote, 1); - assert_eq!(event, Some(Event::PolkaValue(v))); - } - - #[test] - fn precommit_apply_single_value() { - let mut keeper = VoteKeeper::new(Height::new(1), Round::INITIAL, 4); - - let v = ValueId::new(1); - let val = Some(v); - let vote = Vote::new_precommit(Round::new(0), val, Address::new(1)); - - let event = keeper.apply_vote(vote.clone(), 1); - assert_eq!(event, None); - - let event = keeper.apply_vote(vote.clone(), 1); - assert_eq!(event, None); - - let vote_nil = Vote::new_precommit(Round::new(0), None, Address::new(2)); - let event = keeper.apply_vote(vote_nil, 1); - assert_eq!(event, Some(Event::PrecommitAny)); - - let event = keeper.apply_vote(vote, 1); - assert_eq!(event, Some(Event::PrecommitValue(v))); - } -} diff --git a/Code/vote/src/lib.rs b/Code/vote/src/lib.rs index b0cd03df0..78863e9c8 100644 --- a/Code/vote/src/lib.rs +++ b/Code/vote/src/lib.rs @@ -4,8 +4,8 @@ #![deny(unused_crate_dependencies, trivial_casts, trivial_numeric_casts)] #![warn( // missing_docs, - broken_intra_doc_links, - private_intra_doc_links, + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, variant_size_differences )] #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::panic))] @@ -15,22 +15,28 @@ extern crate alloc; pub mod count; pub mod keeper; -use malachite_common::{Height, Round, Vote, VoteType}; +use malachite_common::{Consensus, Round, ValueId, Vote, VoteType}; use crate::count::{Threshold, VoteCount, Weight}; /// Tracks all the votes for a single round #[derive(Clone, Debug)] -pub struct RoundVotes { - pub height: Height, +pub struct RoundVotes +where + C: Consensus, +{ + pub height: C::Height, pub round: Round, - pub prevotes: VoteCount, - pub precommits: VoteCount, + pub prevotes: VoteCount, + pub precommits: VoteCount, } -impl RoundVotes { - pub fn new(height: Height, round: Round, total: Weight) -> RoundVotes { +impl RoundVotes +where + C: Consensus, +{ + pub fn new(height: C::Height, round: Round, total: Weight) -> Self { RoundVotes { height, round, @@ -39,8 +45,8 @@ impl RoundVotes { } } - pub fn add_vote(&mut self, vote: Vote, weight: Weight) -> Threshold { - match vote.typ { + pub fn add_vote(&mut self, vote: C::Vote, weight: Weight) -> Threshold> { + match vote.vote_type() { VoteType::Prevote => self.prevotes.add_vote(vote, weight), VoteType::Precommit => self.precommits.add_vote(vote, weight), } diff --git a/codecov.yml b/codecov.yml index ade8d082c..97d7c517a 100644 --- a/codecov.yml +++ b/codecov.yml @@ -7,6 +7,18 @@ coverage: range: "50...100" status: - project: true - patch: true + project: + default: + target: auto + threshold: 5% + removed_code_behavior: adjust_base + paths: + - "Code" + patch: + default: + target: auto + threshold: 5% + paths: + - "Code" + changes: true