diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 341fe08d4..b64ce2dbe 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -2,7 +2,7 @@ name: Coverage on: push: - branches: master + branches: main paths: - Code/** pull_request: @@ -12,6 +12,9 @@ on: jobs: coverage: runs-on: ubuntu-latest + defaults: + run: + working-directory: Code env: CARGO_TERM_COLOR: always steps: @@ -26,11 +29,9 @@ jobs: - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - name: Generate code coverage - working-directory: Code run: cargo llvm-cov nextest --all-features --workspace --lcov --output-path lcov.info - name: Generate text report - working-directory: Code - run: cargo llvm-cov report --text + run: cargo llvm-cov report - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a81cf0956..df6b8d3ee 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,16 +2,13 @@ name: Rust on: push: - branches: master + branches: main paths: - Code/** pull_request: paths: - Code/** -# permissions: -# checks: write - env: CARGO_INCREMENTAL: 0 CARGO_PROFILE_DEV_DEBUG: 1 @@ -24,6 +21,9 @@ jobs: test: name: Test runs-on: ubuntu-latest + defaults: + run: + working-directory: Code steps: - name: Checkout uses: actions/checkout@v4 @@ -32,10 +32,8 @@ jobs: - name: Install cargo-nextest uses: taiki-e/install-action@cargo-nextest - name: Build code - working-directory: Code run: cargo nextest run --workspace --all-features --no-run - name: Run tests - working-directory: Code run: cargo nextest run --workspace --all-features clippy: @@ -49,10 +47,10 @@ jobs: with: components: clippy - name: Run clippy - uses: auguwu/clippy-action@1.3.0 + uses: actions-rs/clippy@master with: token: ${{secrets.GITHUB_TOKEN}} - args: --manifest-path Code/Cargo.toml + args: --all-features --all-targets --manifest-path Code/Cargo.toml fmt: name: Formatting 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/.gitkeep b/Code/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/Code/Cargo.toml b/Code/Cargo.toml index e444397d9..b96fe3227 100644 --- a/Code/Cargo.toml +++ b/Code/Cargo.toml @@ -1,4 +1,24 @@ [workspace] resolver = "2" -members = [] +members = [ + "common", + "driver", + "round", + "vote", + "test", +] + +[workspace.package] +version = "0.1.0" +edition = "2021" +repository = "https://github.com/informalsystems/malachite" +license = "Apache-2.0" +publish = false + +[workspace.dependencies] +ed25519-consensus = "2.1.0" +rand = { version = "0.8.5", features = ["std_rng"] } +secrecy = "0.8.0" +sha2 = "0.10.8" +signature = "2.1.0" diff --git a/Code/QUESTIONS.md b/Code/QUESTIONS.md new file mode 100644 index 000000000..700c9fae4 --- /dev/null +++ b/Code/QUESTIONS.md @@ -0,0 +1 @@ +- How do we deal with errors? diff --git a/Code/TODO.md b/Code/TODO.md new file mode 100644 index 000000000..8a7878e9e --- /dev/null +++ b/Code/TODO.md @@ -0,0 +1,11 @@ +is proposal complete +if polka not nil, then need to see proof of lock (2f+1 votes) +then send proposal + +count votes for cur, prev, 1/2 next round + +if complete proposal from a past round => to current one +if we have some threshold (f+1) of votes for a future round => skip to that round + +context (get proposer, get value) +signing context diff --git a/Code/common/Cargo.toml b/Code/common/Cargo.toml new file mode 100644 index 000000000..cd02c85dc --- /dev/null +++ b/Code/common/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "malachite-common" +description = "Common datatypes and interfaces for the Malachite consensus engine" + +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +secrecy.workspace = true +signature.workspace = true diff --git a/Code/common/src/context.rs b/Code/common/src/context.rs new file mode 100644 index 000000000..8469c6365 --- /dev/null +++ b/Code/common/src/context.rs @@ -0,0 +1,55 @@ +use crate::{ + Address, Height, PrivateKey, Proposal, PublicKey, Round, Signature, SignedVote, SigningScheme, + Validator, ValidatorSet, Value, ValueId, Vote, +}; + +/// This trait allows to abstract over the various datatypes +/// that are used in the consensus engine. +pub trait Context +where + Self: Sized, +{ + type Address: Address; + type Height: Height; + type Proposal: Proposal; + type Validator: Validator; + type ValidatorSet: ValidatorSet; + type Value: Value; + type Vote: Vote; + type SigningScheme: SigningScheme; // TODO: Do we need to support multiple signing schemes? + + // FIXME: Remove altogether + const DUMMY_VALUE: Self::Value; + + /// Sign the given vote using the given private key. + /// TODO: Maybe move this as concrete methods in `SignedVote`? + fn sign_vote(vote: &Self::Vote, private_key: &PrivateKey) -> Signature; + + /// Verify the given vote's signature using the given public key. + /// TODO: Maybe move this as concrete methods in `SignedVote`? + fn verify_signed_vote(signed_vote: &SignedVote, public_key: &PublicKey) -> bool; + + /// 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 new file mode 100644 index 000000000..511f69503 --- /dev/null +++ b/Code/common/src/height.rs @@ -0,0 +1,14 @@ +use core::fmt::Debug; + +// 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 new file mode 100644 index 000000000..b9a6306af --- /dev/null +++ b/Code/common/src/lib.rs @@ -0,0 +1,43 @@ +//! Common data types and abstractions for the consensus engine. + +#![no_std] +#![forbid(unsafe_code)] +#![deny(unused_crate_dependencies, trivial_casts, trivial_numeric_casts)] +#![warn( + // missing_docs, + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, + variant_size_differences +)] +#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::panic))] + +mod context; +mod height; +mod proposal; +mod round; +mod signed_vote; +mod signing; +mod timeout; +mod validator_set; +mod value; +mod vote; + +// Re-export `signature` crate for convenience +pub use ::signature; + +/// Type alias to make it easier to refer the `ValueId` type of a given `Consensus` engine. +pub type ValueId = <::Value as Value>::Id; +pub type PublicKey = <::SigningScheme as SigningScheme>::PublicKey; +pub type PrivateKey = <::SigningScheme as SigningScheme>::PrivateKey; +pub type Signature = <::SigningScheme as SigningScheme>::Signature; + +pub use context::Context; +pub use height::Height; +pub use proposal::Proposal; +pub use round::Round; +pub use signed_vote::SignedVote; +pub use signing::SigningScheme; +pub use timeout::{Timeout, TimeoutStep}; +pub use validator_set::{Address, Validator, ValidatorSet, VotingPower}; +pub use value::Value; +pub use vote::{Vote, VoteType}; diff --git a/Code/common/src/proposal.rs b/Code/common/src/proposal.rs new file mode 100644 index 000000000..4adcc3b38 --- /dev/null +++ b/Code/common/src/proposal.rs @@ -0,0 +1,22 @@ +use core::fmt::Debug; + +use crate::{Context, Round}; + +/// Defines the requirements for a proposal type. +pub trait Proposal +where + Self: Clone + Debug + PartialEq + Eq, + Ctx: Context, +{ + /// The height for which the proposal is for. + fn height(&self) -> Ctx::Height; + + /// The round for which the proposal is for. + fn round(&self) -> Round; + + /// The value that is proposed. + fn value(&self) -> &Ctx::Value; + + /// 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 new file mode 100644 index 000000000..fb45993c9 --- /dev/null +++ b/Code/common/src/round.rs @@ -0,0 +1,104 @@ +use core::cmp; + +/// A round number. +/// +/// Can be either: +/// - `Round::Nil` (ie. `-1`) +/// - `Round::Some(r)` where `r >= 0` +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum Round { + /// No round, ie. `-1` + Nil, + + /// Some round `r` where `r >= 0` + Some(i64), +} + +impl Round { + /// The initial, zero round. + pub const INITIAL: Round = Round::new(0); + pub const NIL: Round = Round::new(-1); + + /// 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::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::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::Nil) + } + + /// 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::Nil => Round::new(0), + Round::Some(r) => Round::new(r + 1), + } + } +} + +impl PartialOrd for Round { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Round { + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.as_i64().cmp(&other.as_i64()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_round() { + // Test Round::new() + 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::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::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/signed_vote.rs b/Code/common/src/signed_vote.rs new file mode 100644 index 000000000..29777599b --- /dev/null +++ b/Code/common/src/signed_vote.rs @@ -0,0 +1,25 @@ +use crate::{Context, Signature, Vote}; + +// TODO: Do we need to abstract over `SignedVote` as well? + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SignedVote +where + Ctx: Context, +{ + pub vote: Ctx::Vote, + pub signature: Signature, +} + +impl SignedVote +where + Ctx: Context, +{ + pub fn new(vote: Ctx::Vote, signature: Signature) -> Self { + Self { vote, signature } + } + + pub fn validator_address(&self) -> &Ctx::Address { + self.vote.validator_address() + } +} diff --git a/Code/common/src/signing.rs b/Code/common/src/signing.rs new file mode 100644 index 000000000..07abc4ae5 --- /dev/null +++ b/Code/common/src/signing.rs @@ -0,0 +1,20 @@ +use core::fmt::Debug; + +use secrecy::{CloneableSecret, DebugSecret, Zeroize}; +use signature::{Keypair, Signer, Verifier}; + +pub trait SigningScheme +where + Self: Clone + Debug + Eq, +{ + type Signature: Clone + Debug + Eq; + + type PublicKey: Clone + Debug + Eq + Verifier; + + type PrivateKey: Clone + + Signer + + Keypair + + Zeroize + + DebugSecret + + CloneableSecret; +} diff --git a/Code/common/src/timeout.rs b/Code/common/src/timeout.rs new file mode 100644 index 000000000..5c72a75c8 --- /dev/null +++ b/Code/common/src/timeout.rs @@ -0,0 +1,34 @@ +use crate::Round; + +/// The round step for which the timeout is for. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum TimeoutStep { + Propose, + Prevote, + Precommit, +} + +/// 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 new file mode 100644 index 000000000..0c21dccf9 --- /dev/null +++ b/Code/common/src/validator_set.rs @@ -0,0 +1,53 @@ +use core::fmt::Debug; + +use crate::{Context, PublicKey}; + +/// Voting power held by a validator. +/// +/// TODO: Do we need to abstract over this as well? +pub type VotingPower = u64; + +/// Defines the requirements for an address. +/// +/// TODO: Keep this trait or just add the bounds to Consensus::Address? +pub trait Address +where + Self: Clone + Debug + Eq + Ord, +{ +} + +/// Defines the requirements for a validator. +pub trait Validator +where + Self: Clone + Debug + PartialEq + Eq, + Ctx: Context, +{ + /// The address of the validator, typically derived from its public key. + fn address(&self) -> &Ctx::Address; + + /// The public key of the validator, used to verify signatures. + fn public_key(&self) -> &PublicKey; + + /// The voting power held by the validaror. + fn voting_power(&self) -> VotingPower; +} + +/// Defines the requirements for a validator set. +/// +/// A validator set is a collection of validators. +pub trait ValidatorSet +where + Ctx: Context, +{ + /// The total voting power of the validator set. + fn total_voting_power(&self) -> VotingPower; + + /// The proposer in the validator set. + fn get_proposer(&self) -> &Ctx::Validator; + + /// Get the validator with the given public key. + fn get_by_public_key(&self, public_key: &PublicKey) -> Option<&Ctx::Validator>; + + /// Get the validator with the given address. + fn get_by_address(&self, address: &Ctx::Address) -> Option<&Ctx::Validator>; +} diff --git a/Code/common/src/value.rs b/Code/common/src/value.rs new file mode 100644 index 000000000..f1a6a656e --- /dev/null +++ b/Code/common/src/value.rs @@ -0,0 +1,14 @@ +use core::fmt::Debug; + +/// 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; + + /// The ID of the value. + fn id(&self) -> Self::Id; +} diff --git a/Code/common/src/vote.rs b/Code/common/src/vote.rs new file mode 100644 index 000000000..a67190a26 --- /dev/null +++ b/Code/common/src/vote.rs @@ -0,0 +1,38 @@ +use core::fmt::Debug; + +use crate::{Context, 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, +} + +/// 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 + Eq, + Ctx: Context, +{ + /// The round for which the vote is for. + fn round(&self) -> Round; + + /// Get a reference to the value being voted for. + fn value(&self) -> Option<&::Id>; + + /// Take ownership of the value being voted for. + fn take_value(self) -> Option<::Id>; + + /// The type of vote. + fn vote_type(&self) -> VoteType; + + /// Address of the validator who issued this vote + fn validator_address(&self) -> &Ctx::Address; +} diff --git a/Code/driver/Cargo.toml b/Code/driver/Cargo.toml new file mode 100644 index 000000000..bf2b34458 --- /dev/null +++ b/Code/driver/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "malachite-driver" +description = "Driver for the state machine of Malachite consensus engine" + +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +malachite-common = { version = "0.1.0", path = "../common" } +malachite-round = { version = "0.1.0", path = "../round" } +malachite-vote = { version = "0.1.0", path = "../vote" } + +secrecy.workspace = true diff --git a/Code/driver/src/client.rs b/Code/driver/src/client.rs new file mode 100644 index 000000000..1e240d730 --- /dev/null +++ b/Code/driver/src/client.rs @@ -0,0 +1,14 @@ +use malachite_common::Context; + +/// Client for use by the [`Driver`](crate::Driver) to ask +/// for a value to propose and validate proposals. +pub trait Client +where + Ctx: Context, +{ + /// Get the value to propose. + fn get_value(&self) -> Ctx::Value; + + /// Validate a proposal. + fn validate_proposal(&self, proposal: &Ctx::Proposal) -> bool; +} diff --git a/Code/driver/src/driver.rs b/Code/driver/src/driver.rs new file mode 100644 index 000000000..7720b219b --- /dev/null +++ b/Code/driver/src/driver.rs @@ -0,0 +1,269 @@ +use std::collections::BTreeMap; + +use malachite_round::state_machine::RoundData; +use secrecy::{ExposeSecret, Secret}; + +use malachite_common::signature::Keypair; +use malachite_common::{ + Context, PrivateKey, Proposal, Round, SignedVote, Timeout, TimeoutStep, Validator, + ValidatorSet, Value, Vote, VoteType, +}; +use malachite_round::events::Event as RoundEvent; +use malachite_round::message::Message as RoundMessage; +use malachite_round::state::State as RoundState; +use malachite_vote::keeper::Message as VoteMessage; +use malachite_vote::keeper::VoteKeeper; +use malachite_vote::Threshold; + +use crate::event::Event; +use crate::message::Message; + +/// Driver for the state machine of the Malachite consensus engine. +#[derive(Clone, Debug)] +pub struct Driver +where + Ctx: Context, + Client: crate::client::Client, +{ + pub client: Client, + pub height: Ctx::Height, + pub private_key: Secret>, + pub address: Ctx::Address, + pub validator_set: Ctx::ValidatorSet, + pub round: Round, + pub votes: VoteKeeper, + pub round_states: BTreeMap>, +} + +impl Driver +where + Ctx: Context, + Client: crate::client::Client, +{ + pub fn new( + client: Client, + height: Ctx::Height, + validator_set: Ctx::ValidatorSet, + private_key: PrivateKey, + address: Ctx::Address, + ) -> Self { + let votes = VoteKeeper::new(validator_set.total_voting_power()); + + Self { + client, + height, + private_key: Secret::new(private_key), + address, + validator_set, + round: Round::NIL, + votes, + round_states: BTreeMap::new(), + } + } + + fn get_value(&self) -> Ctx::Value { + self.client.get_value() + } + + fn validate_proposal(&self, proposal: &Ctx::Proposal) -> bool { + self.client.validate_proposal(proposal) + } + + pub fn execute(&mut self, msg: Event) -> Option> { + let round_msg = match self.apply(msg) { + Some(msg) => msg, + None => return None, + }; + + match round_msg { + RoundMessage::NewRound(round) => { + // XXX: Check if there is an existing state? + assert!(self.round < round); + Some(Message::NewRound(round)) + } + + RoundMessage::Proposal(proposal) => { + // sign the proposal + Some(Message::Propose(proposal)) + } + + RoundMessage::Vote(vote) => { + let signature = Ctx::sign_vote(&vote, self.private_key.expose_secret()); + let signed_vote = SignedVote::new(vote, signature); + + Some(Message::Vote(signed_vote)) + } + + RoundMessage::ScheduleTimeout(timeout) => Some(Message::ScheduleTimeout(timeout)), + + RoundMessage::Decision(value) => { + // TODO: update the state + Some(Message::Decide(value.round, value.value)) + } + } + } + + fn apply(&mut self, msg: Event) -> Option> { + match msg { + Event::NewRound(round) => self.apply_new_round(round), + Event::Proposal(proposal) => self.apply_proposal(proposal), + Event::Vote(signed_vote) => self.apply_vote(signed_vote), + Event::TimeoutElapsed(timeout) => self.apply_timeout(timeout), + } + } + + fn apply_new_round(&mut self, round: Round) -> Option> { + let proposer = self.validator_set.get_proposer(); + + let event = if proposer.public_key() == &self.private_key.expose_secret().verifying_key() { + let value = self.get_value(); + RoundEvent::NewRoundProposer(value) + } else { + RoundEvent::NewRound + }; + + assert!(self.round < round); + self.round_states + .insert(round, RoundState::default().new_round(round)); + self.round = round; + + self.apply_event(round, event) + } + + fn apply_proposal(&mut self, proposal: Ctx::Proposal) -> Option> { + // 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; + } + + // Check that the proposal is for the current height and round + if self.height != proposal.height() || self.round != proposal.round() { + return None; + } + + // TODO: Document + if 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) + + let is_valid = self.validate_proposal(&proposal); + + 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? + let round = proposal.round(); + let event = if is_valid { + RoundEvent::Proposal(proposal) + } else { + RoundEvent::ProposalInvalid + }; + + self.apply_event(round, event) + } + Round::Some(_) + if self.votes.is_threshold_met( + &proposal.pol_round(), + VoteType::Prevote, + Threshold::Value(proposal.value().id()), + ) => + { + let round = proposal.round(); + let event = if is_valid { + RoundEvent::Proposal(proposal) + } else { + RoundEvent::ProposalInvalid + }; + + self.apply_event(round, event) + } + _ => None, + } + } + + fn apply_vote(&mut self, signed_vote: SignedVote) -> Option> { + // TODO: How to handle missing validator? + let validator = self + .validator_set + .get_by_address(signed_vote.validator_address())?; + + if !Ctx::verify_signed_vote(&signed_vote, validator.public_key()) { + // TODO: How to handle invalid votes? + return None; + } + + let round = signed_vote.vote.round(); + + let vote_msg = self + .votes + .apply_vote(signed_vote.vote, validator.voting_power())?; + + let round_event = match vote_msg { + VoteMessage::PolkaAny => RoundEvent::PolkaAny, + VoteMessage::PolkaNil => RoundEvent::PolkaNil, + VoteMessage::PolkaValue(v) => RoundEvent::PolkaValue(v), + VoteMessage::PrecommitAny => RoundEvent::PrecommitAny, + VoteMessage::PrecommitValue(v) => RoundEvent::PrecommitValue(v), + VoteMessage::SkipRound(r) => RoundEvent::SkipRound(r), + }; + + self.apply_event(round, round_event) + } + + fn apply_timeout(&mut self, timeout: Timeout) -> Option> { + let event = match timeout.step { + TimeoutStep::Propose => RoundEvent::TimeoutPropose, + TimeoutStep::Prevote => RoundEvent::TimeoutPrevote, + TimeoutStep::Precommit => RoundEvent::TimeoutPrecommit, + }; + + self.apply_event(timeout.round, event) + } + + /// Apply the event, update the state. + 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_default(); + + let data = RoundData::new(round, &self.height, &self.address); + + // Multiplex the event with the round state. + let mux_event = match event { + RoundEvent::PolkaValue(value_id) => match round_state.proposal { + Some(ref proposal) if proposal.value().id() == value_id => { + RoundEvent::ProposalAndPolkaCurrent(proposal.clone()) + } + _ => RoundEvent::PolkaAny, + }, + RoundEvent::PrecommitValue(value_id) => match round_state.proposal { + Some(ref proposal) if proposal.value().id() == value_id => { + RoundEvent::ProposalAndPrecommitValue(proposal.clone()) + } + _ => RoundEvent::PrecommitAny, + }, + + _ => event, + }; + + // Apply the event to the round state machine + let transition = round_state.apply_event(&data, mux_event); + + // Update state + self.round_states.insert(round, transition.next_state); + + // Return message, if any + transition.message + } + + pub fn round_state(&self, round: Round) -> Option<&RoundState> { + self.round_states.get(&round) + } +} diff --git a/Code/driver/src/event.rs b/Code/driver/src/event.rs new file mode 100644 index 000000000..060e330ce --- /dev/null +++ b/Code/driver/src/event.rs @@ -0,0 +1,13 @@ +use malachite_common::{Context, Round, SignedVote, Timeout}; + +/// Events that can be received by the [`Driver`](crate::Driver). +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Event +where + Ctx: Context, +{ + NewRound(Round), + Proposal(Ctx::Proposal), + Vote(SignedVote), + TimeoutElapsed(Timeout), +} diff --git a/Code/driver/src/lib.rs b/Code/driver/src/lib.rs new file mode 100644 index 000000000..ce115e1e7 --- /dev/null +++ b/Code/driver/src/lib.rs @@ -0,0 +1,21 @@ +//! Driver for the state machine of the Malachite consensus engine + +#![forbid(unsafe_code)] +#![deny(unused_crate_dependencies, trivial_casts, trivial_numeric_casts)] +#![warn( + // missing_docs, + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, + variant_size_differences +)] +#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::panic))] + +mod client; +mod driver; +mod event; +mod message; + +pub use client::Client; +pub use driver::Driver; +pub use event::Event; +pub use message::Message; diff --git a/Code/driver/src/message.rs b/Code/driver/src/message.rs new file mode 100644 index 000000000..e4ebde55d --- /dev/null +++ b/Code/driver/src/message.rs @@ -0,0 +1,14 @@ +use malachite_common::{Context, Round, SignedVote, Timeout}; + +/// Messages emitted by the [`Driver`](crate::Driver) +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Message +where + Ctx: Context, +{ + Propose(Ctx::Proposal), + Vote(SignedVote), + Decide(Round, Ctx::Value), + ScheduleTimeout(Timeout), + NewRound(Round), +} diff --git a/Code/round/Cargo.toml b/Code/round/Cargo.toml new file mode 100644 index 000000000..d672a7b42 --- /dev/null +++ b/Code/round/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "malachite-round" +description = "Per-round state-machine for the Malachite consenss engine" + +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +malachite-common = { version = "0.1.0", path = "../common" } diff --git a/Code/round/src/events.rs b/Code/round/src/events.rs new file mode 100644 index 000000000..a0e1e4bba --- /dev/null +++ b/Code/round/src/events.rs @@ -0,0 +1,24 @@ +use malachite_common::{Context, Round, ValueId}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Event +where + Ctx: Context, +{ + NewRound, // Start a new round, not as proposer.L20 + NewRoundProposer(Ctx::Value), // Start a new round and propose the Value.L14 + Proposal(Ctx::Proposal), // Receive a proposal. L22 + L23 (valid) + ProposalAndPolkaPrevious(Ctx::Proposal), // Recieved a proposal and a polka value from a previous round. L28 + L29 (valid) + ProposalInvalid, // Receive an invalid proposal. L26 + L32 (invalid) + PolkaValue(ValueId), // Receive +2/3 prevotes for valueId. L44 + PolkaAny, // Receive +2/3 prevotes for anything. L34 + PolkaNil, // Receive +2/3 prevotes for nil. L44 + ProposalAndPolkaCurrent(Ctx::Proposal), // Receive +2/3 prevotes for Value in current round. L36 + PrecommitAny, // Receive +2/3 precommits for anything. L47 + ProposalAndPrecommitValue(Ctx::Proposal), // Receive +2/3 precommits for Value. L49 + PrecommitValue(ValueId), // Receive +2/3 precommits for ValueId. L51 + SkipRound(Round), // Receive +1/3 messages from a higher round. OneCorrectProcessInHigherRound, L55 + TimeoutPropose, // Timeout waiting for proposal. L57 + TimeoutPrevote, // Timeout waiting for prevotes. L61 + TimeoutPrecommit, // Timeout waiting for precommits. L65 +} diff --git a/Code/round/src/lib.rs b/Code/round/src/lib.rs new file mode 100644 index 000000000..34ae70a59 --- /dev/null +++ b/Code/round/src/lib.rs @@ -0,0 +1,17 @@ +//! Per-round consensus state machine + +#![forbid(unsafe_code)] +#![deny(unused_crate_dependencies, trivial_casts, trivial_numeric_casts)] +#![warn( + // missing_docs, + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, + variant_size_differences +)] +#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::panic))] + +pub mod events; +pub mod message; +pub mod state; +pub mod state_machine; +pub mod transition; diff --git a/Code/round/src/message.rs b/Code/round/src/message.rs new file mode 100644 index 000000000..8ec5a4556 --- /dev/null +++ b/Code/round/src/message.rs @@ -0,0 +1,60 @@ +use malachite_common::{Context, Round, Timeout, TimeoutStep, ValueId}; + +use crate::state::RoundValue; + +#[derive(Debug, PartialEq, Eq)] +pub enum Message +where + Ctx: Context, +{ + NewRound(Round), // Move to the new round. + Proposal(Ctx::Proposal), // Broadcast the proposal. + Vote(Ctx::Vote), // Broadcast the vote. + ScheduleTimeout(Timeout), // Schedule the timeout. + Decision(RoundValue), // Decide the value. +} + +impl Clone for Message +where + Ctx: Context, +{ + 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::ScheduleTimeout(timeout) => Message::ScheduleTimeout(*timeout), + Message::Decision(round_value) => Message::Decision(round_value.clone()), + } + } +} + +impl Message +where + Ctx: Context, +{ + pub fn proposal( + height: Ctx::Height, + round: Round, + value: Ctx::Value, + pol_round: Round, + ) -> Self { + Message::Proposal(Ctx::new_proposal(height, round, value, pol_round)) + } + + pub fn prevote(round: Round, value_id: Option>, address: Ctx::Address) -> Self { + Message::Vote(Ctx::new_prevote(round, value_id, address)) + } + + pub fn precommit(round: Round, value_id: Option>, address: Ctx::Address) -> Self { + Message::Vote(Ctx::new_precommit(round, value_id, address)) + } + + pub fn schedule_timeout(round: Round, step: TimeoutStep) -> Self { + Message::ScheduleTimeout(Timeout { round, step }) + } + + pub fn decision(round: Round, value: Ctx::Value) -> Self { + Message::Decision(RoundValue { round, value }) + } +} diff --git a/Code/round/src/state.rs b/Code/round/src/state.rs new file mode 100644 index 000000000..55ef389b4 --- /dev/null +++ b/Code/round/src/state.rs @@ -0,0 +1,109 @@ +use crate::events::Event; +use crate::state_machine::RoundData; +use crate::transition::Transition; + +use malachite_common::{Context, Round}; + +/// A value and its associated round +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RoundValue { + pub value: Value, + pub round: Round, +} + +impl RoundValue { + pub fn new(value: Value, round: Round) -> Self { + Self { value, round } + } +} + +/// The step of consensus in this round +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum Step { + NewRound, + Propose, + Prevote, + Precommit, + Commit, +} + +/// The state of the consensus state machine +#[derive(Debug, PartialEq, Eq)] +pub struct State +where + Ctx: Context, +{ + pub round: Round, + pub step: Step, + pub proposal: Option, + pub locked: Option>, + pub valid: Option>, +} + +impl Clone for State +where + Ctx: Context, +{ + fn clone(&self) -> Self { + Self { + round: self.round, + step: self.step, + proposal: self.proposal.clone(), + locked: self.locked.clone(), + valid: self.valid.clone(), + } + } +} + +impl State +where + Ctx: Context, +{ + pub fn new() -> Self { + Self { + round: Round::INITIAL, + step: Step::NewRound, + proposal: None, + locked: None, + valid: None, + } + } + + pub fn new_round(self, round: Round) -> Self { + Self { + round, + step: Step::NewRound, + ..self + } + } + pub fn with_step(self, step: Step) -> Self { + Self { step, ..self } + } + + pub fn set_locked(self, value: Ctx::Value) -> Self { + Self { + locked: Some(RoundValue::new(value, self.round)), + ..self + } + } + + pub fn set_valid(self, value: Ctx::Value) -> Self { + Self { + valid: Some(RoundValue::new(value, self.round)), + ..self + } + } + + pub fn apply_event(self, data: &RoundData, event: Event) -> Transition { + crate::state_machine::apply_event(self, data, event) + } +} + +impl Default for State +where + Ctx: Context, +{ + fn default() -> Self { + Self::new() + } +} diff --git a/Code/round/src/state_machine.rs b/Code/round/src/state_machine.rs new file mode 100644 index 000000000..595af5bc0 --- /dev/null +++ b/Code/round/src/state_machine.rs @@ -0,0 +1,352 @@ +use malachite_common::{Context, Proposal, Round, TimeoutStep, Value}; + +use crate::events::Event; +use crate::message::Message; +use crate::state::{State, Step}; +use crate::transition::Transition; + +/// Immutable data about the current round, +/// height and address of the node. +/// +/// Because this data is immutable for a given round, +/// it is purposefully not included in the state, +/// but rather passed in as a reference. +pub struct RoundData<'a, Ctx> +where + Ctx: Context, +{ + pub round: Round, + pub height: &'a Ctx::Height, + pub address: &'a Ctx::Address, +} + +impl<'a, Ctx> RoundData<'a, Ctx> +where + Ctx: Context, +{ + pub fn new(round: Round, height: &'a Ctx::Height, address: &'a Ctx::Address) -> Self { + Self { + round, + height, + address, + } + } +} + +/// Check that a proposal has a valid Proof-Of-Lock round +fn is_valid_pol_round(state: &State, pol_round: Round) -> bool +where + Ctx: Context, +{ + pol_round.is_defined() && pol_round < state.round +} + +/// Apply an event to the current state at the current round. +/// +/// This function takes the current state and round, and an event, +/// and returns the next state and an optional message for the driver to act on. +/// +/// 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, + data: &RoundData, + event: Event, +) -> Transition +where + Ctx: Context, +{ + let this_round = state.round == data.round; + + match (state.step, event) { + // From NewRound. Event must be for current round. + (Step::NewRound, Event::NewRoundProposer(value)) if this_round => { + propose(state, data.height, value) // L11/L14 + } + (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() => + { + // L22 + if state + .locked + .as_ref() + .map_or(true, |locked| &locked.value == proposal.value()) + { + state.proposal = Some(proposal.clone()); + prevote(state, data.address, &proposal) + } else { + prevote_nil(state, data.address) + } + } + + (Step::Propose, Event::ProposalAndPolkaPrevious(proposal)) + if this_round && is_valid_pol_round(&state, proposal.pol_round()) => + { + // L28 + let Some(locked) = state.locked.as_ref() else { + return prevote_nil(state, data.address); + }; + + if locked.round <= proposal.pol_round() || &locked.value == proposal.value() { + prevote(state, data.address, &proposal) + } else { + prevote_nil(state, data.address) + } + } + (Step::Propose, Event::ProposalInvalid) if this_round => prevote_nil(state, data.address), // L22/L25, L28/L31 + (Step::Propose, Event::TimeoutPropose) if this_round => prevote_nil(state, data.address), // L57 + + // From Prevote. Event must be for current round. + (Step::Prevote, Event::PolkaAny) if this_round => schedule_timeout_prevote(state), // L34 + (Step::Prevote, Event::PolkaNil) if this_round => precommit_nil(state, data.address), // L44 + (Step::Prevote, Event::ProposalAndPolkaCurrent(proposal)) if this_round => { + precommit(state, data.address, proposal) // L36/L37 - NOTE: only once? + } + (Step::Prevote, Event::TimeoutPrevote) if this_round => precommit_nil(state, data.address), // L61 + + // From Precommit. Event must be for current round. + (Step::Precommit, Event::ProposalAndPolkaCurrent(proposal)) if this_round => { + set_valid_value(state, proposal.value().clone()) // L36/L42 - NOTE: only once? + } + + // From Commit. No more state transitions. + (Step::Commit, _) => Transition::invalid(state), + + // From all (except Commit). Various round guards. + (_, Event::PrecommitAny) if this_round => schedule_timeout_precommit(state), // L47 + (_, Event::TimeoutPrecommit) if this_round => round_skip(state, data.round.increment()), // L65 + (_, Event::SkipRound(round)) if state.round < round => round_skip(state, round), // L55 + (_, Event::ProposalAndPrecommitValue(proposal)) => commit(state, data.round, proposal), // L49 + + // Invalid transition. + _ => Transition::invalid(state), + } +} + +//--------------------------------------------------------------------- +// Propose +//--------------------------------------------------------------------- + +/// We are the proposer; propose the valid value if it exists, +/// otherwise propose the given value. +/// +/// Ref: L11/L14 +pub fn propose(state: State, height: &Ctx::Height, value: Ctx::Value) -> Transition +where + Ctx: Context, +{ + let (value, pol_round) = match &state.valid { + Some(round_value) => (round_value.value.clone(), round_value.round), + None => (value, Round::Nil), + }; + + let proposal = Message::proposal(height.clone(), state.round, value, pol_round); + Transition::to(state.with_step(Step::Propose)).with_message(proposal) +} + +//--------------------------------------------------------------------- +// Prevote +//--------------------------------------------------------------------- + +/// Received a complete proposal; prevote the value, +/// unless we are locked on something else at a higher round. +/// +/// Ref: L22/L28 +pub fn prevote( + state: State, + address: &Ctx::Address, + proposal: &Ctx::Proposal, +) -> Transition +where + Ctx: Context, +{ + let vr = proposal.round(); + let proposed = proposal.value().id(); + let value = match &state.locked { + Some(locked) if locked.round <= vr => Some(proposed), // unlock and prevote + Some(locked) if locked.value.id() == proposed => Some(proposed), // already locked on value + Some(_) => None, // we're locked on a higher round with a different value, prevote nil + None => Some(proposed), // not locked, prevote the value + }; + + let message = Message::prevote(state.round, value, address.clone()); + Transition::to(state.with_step(Step::Prevote)).with_message(message) +} + +/// Received a complete proposal for an empty or invalid value, or timed out; prevote nil. +/// +/// Ref: L22/L25, L28/L31, L57 +pub fn prevote_nil(state: State, address: &Ctx::Address) -> Transition +where + Ctx: Context, +{ + let message = Message::prevote(state.round, None, address.clone()); + Transition::to(state.with_step(Step::Prevote)).with_message(message) +} + +// --------------------------------------------------------------------- +// Precommit +// --------------------------------------------------------------------- + +/// Received a polka for a value; precommit the value. +/// +/// Ref: L36 +/// +/// NOTE: Only one of this and set_valid_value should be called once in a round +/// How do we enforce this? +pub fn precommit( + mut state: State, + address: &Ctx::Address, + proposal: Ctx::Proposal, +) -> Transition +where + Ctx: Context, +{ + if state.step != Step::Prevote { + return Transition::to(state.clone()); + } + + let value = proposal.value(); + let message = Message::precommit(state.round, Some(value.id()), address.clone()); + + let current_value = match state.proposal { + Some(ref proposal) => proposal.value().clone(), + None => { + state.proposal = Some(proposal.clone()); + proposal.value().clone() + } + }; + + assert_eq!(current_value.id(), value.id()); + + let next = state + .set_locked(value.clone()) + .set_valid(value.clone()) + .with_step(Step::Precommit); + + Transition::to(next).with_message(message) +} + +/// Received a polka for nil or timed out of prevote; precommit nil. +/// +/// Ref: L44, L61 +pub fn precommit_nil(state: State, address: &Ctx::Address) -> Transition +where + Ctx: Context, +{ + let message = Message::precommit(state.round, None, address.clone()); + Transition::to(state.with_step(Step::Precommit)).with_message(message) +} + +// --------------------------------------------------------------------- +// Schedule timeouts +// --------------------------------------------------------------------- + +/// We're not the proposer; schedule timeout propose. +/// +/// Ref: L11, L20 +pub fn schedule_timeout_propose(state: State) -> Transition +where + Ctx: Context, +{ + let timeout = Message::schedule_timeout(state.round, TimeoutStep::Propose); + Transition::to(state.with_step(Step::Propose)).with_message(timeout) +} + +/// We received a polka for any; schedule timeout prevote. +/// +/// Ref: L34 +/// +/// 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 +where + Ctx: Context, +{ + if state.step == Step::Prevote { + let message = Message::schedule_timeout(state.round, TimeoutStep::Prevote); + Transition::to(state).with_message(message) + } else { + Transition::to(state) + } +} + +/// We received +2/3 precommits for any; schedule timeout precommit. +/// +/// Ref: L47 +pub fn schedule_timeout_precommit(state: State) -> Transition +where + Ctx: Context, +{ + let message = Message::schedule_timeout(state.round, TimeoutStep::Precommit); + Transition::to(state).with_message(message) +} + +//--------------------------------------------------------------------- +// Set the valid value. +//--------------------------------------------------------------------- + +/// We received a polka for a value after we already precommitted. +/// Set the valid value and current round. +/// +/// Ref: L36/L42 +/// +/// NOTE: only one of this and precommit should be called once in a round +pub fn set_valid_value(state: State, value: Ctx::Value) -> Transition +where + Ctx: Context, +{ + // Check that we're locked on this value + let Some(locked) = state.locked.as_ref() else { + // TODO: Add logging + return Transition::invalid(state); + }; + + if locked.value.id() != value.id() { + // TODO: Add logging + return Transition::invalid(state); + } + + Transition::to(state.clone().set_valid(locked.value.clone())) +} + +//--------------------------------------------------------------------- +// New round or height +//--------------------------------------------------------------------- + +/// We finished a round (timeout precommit) or received +1/3 votes +/// from a higher round. Move to the higher round. +/// +/// Ref: 65 +pub fn round_skip(state: State, round: Round) -> Transition +where + Ctx: Context, +{ + 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, proposal: Ctx::Proposal) -> Transition +where + Ctx: Context, +{ + // Check that we're locked on this value + let Some(locked) = state.locked.as_ref() else { + // TODO: Add logging + return Transition::invalid(state); + }; + + if locked.value.id() != proposal.value().id() { + // TODO: Add logging + return Transition::invalid(state); + } + + let message = Message::decision(round, locked.value.clone()); + Transition::to(state.with_step(Step::Commit)).with_message(message) +} diff --git a/Code/round/src/transition.rs b/Code/round/src/transition.rs new file mode 100644 index 000000000..33bed24fb --- /dev/null +++ b/Code/round/src/transition.rs @@ -0,0 +1,40 @@ +use malachite_common::Context; + +use crate::message::Message; +use crate::state::State; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Transition +where + Ctx: Context, +{ + pub next_state: State, + pub message: Option>, + pub valid: bool, +} + +impl Transition +where + Ctx: Context, +{ + pub fn to(next_state: State) -> Self { + Self { + next_state, + message: None, + valid: true, + } + } + + pub fn invalid(next_state: State) -> Self { + Self { + next_state, + message: None, + valid: false, + } + } + + pub fn with_message(mut self, message: Message) -> Self { + self.message = Some(message); + self + } +} diff --git a/Code/rust-toolchain.toml b/Code/rust-toolchain.toml deleted file mode 100644 index 292fe499e..000000000 --- a/Code/rust-toolchain.toml +++ /dev/null @@ -1,2 +0,0 @@ -[toolchain] -channel = "stable" diff --git a/Code/rustfmt.toml b/Code/rustfmt.toml deleted file mode 100644 index 6641a0f46..000000000 --- a/Code/rustfmt.toml +++ /dev/null @@ -1,2 +0,0 @@ -max_width = 120 -comment_width = 100 diff --git a/Code/test/Cargo.toml b/Code/test/Cargo.toml new file mode 100644 index 000000000..6b536bce3 --- /dev/null +++ b/Code/test/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "malachite-test" +description = "Testing framework for the Malachite consensus engine" +publish = false + +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true + +[dependencies] +malachite-common = { version = "0.1.0", path = "../common" } +malachite-driver = { version = "0.1.0", path = "../driver" } +malachite-round = { version = "0.1.0", path = "../round" } +malachite-vote = { version = "0.1.0", path = "../vote" } + +ed25519-consensus.workspace = true +signature.workspace = true +rand.workspace = true +sha2.workspace = true +secrecy.workspace = true diff --git a/Code/test/src/client.rs b/Code/test/src/client.rs new file mode 100644 index 000000000..cc66f2381 --- /dev/null +++ b/Code/test/src/client.rs @@ -0,0 +1,24 @@ +use malachite_driver::Client; + +use crate::{Proposal, TestContext, Value}; + +pub struct TestClient { + pub value: Value, + pub is_valid: fn(&Proposal) -> bool, +} + +impl TestClient { + pub fn new(value: Value, is_valid: fn(&Proposal) -> bool) -> Self { + Self { value, is_valid } + } +} + +impl Client for TestClient { + fn get_value(&self) -> Value { + self.value.clone() + } + + fn validate_proposal(&self, proposal: &Proposal) -> bool { + (self.is_valid)(proposal) + } +} diff --git a/Code/test/src/context.rs b/Code/test/src/context.rs new file mode 100644 index 000000000..479c4d75e --- /dev/null +++ b/Code/test/src/context.rs @@ -0,0 +1,50 @@ +use malachite_common::Context; +use malachite_common::Round; +use malachite_common::SignedVote; + +use crate::height::*; +use crate::proposal::*; +use crate::signing::{Ed25519, PrivateKey, PublicKey, Signature}; +use crate::validator_set::*; +use crate::value::*; +use crate::vote::*; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct TestContext; + +impl Context for TestContext { + type Address = Address; + type Height = Height; + type Proposal = Proposal; + type ValidatorSet = ValidatorSet; + type Validator = Validator; + type Value = Value; + type Vote = Vote; + type SigningScheme = Ed25519; + + const DUMMY_VALUE: Self::Value = Value::new(9999); + + fn sign_vote(vote: &Self::Vote, private_key: &PrivateKey) -> Signature { + use signature::Signer; + private_key.sign(&vote.to_bytes()) + } + + fn verify_signed_vote(signed_vote: &SignedVote, public_key: &PublicKey) -> bool { + use signature::Verifier; + public_key + .verify(&signed_vote.vote.to_bytes(), &signed_vote.signature) + .is_ok() + } + + 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..fc979bccb --- /dev/null +++ b/Code/test/src/lib.rs @@ -0,0 +1,20 @@ +#![forbid(unsafe_code)] +#![deny(trivial_casts, trivial_numeric_casts)] + +mod client; +mod context; +mod height; +mod proposal; +mod signing; +mod validator_set; +mod value; +mod vote; + +pub use crate::client::*; +pub use crate::context::*; +pub use crate::height::*; +pub use crate::proposal::*; +pub use crate::signing::*; +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..407b4be02 --- /dev/null +++ b/Code/test/src/proposal.rs @@ -0,0 +1,41 @@ +use malachite_common::Round; + +use crate::{Height, TestContext, 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/signing.rs b/Code/test/src/signing.rs new file mode 100644 index 000000000..af120d071 --- /dev/null +++ b/Code/test/src/signing.rs @@ -0,0 +1,89 @@ +use malachite_common::SigningScheme; +use rand::{CryptoRng, RngCore}; +use secrecy::{CloneableSecret, DebugSecret, Zeroize}; +use signature::{Keypair, Signer, Verifier}; + +pub use ed25519_consensus::Signature; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Ed25519; + +impl Ed25519 { + pub fn generate_keypair(rng: R) -> PrivateKey + where + R: RngCore + CryptoRng, + { + PrivateKey::generate(rng) + } +} + +impl SigningScheme for Ed25519 { + type Signature = Signature; + type PublicKey = PublicKey; + type PrivateKey = PrivateKey; +} + +#[derive(Clone, Debug)] +pub struct PrivateKey(ed25519_consensus::SigningKey); + +impl PrivateKey { + pub fn generate(rng: R) -> Self + where + R: RngCore + CryptoRng, + { + let signing_key = ed25519_consensus::SigningKey::new(rng); + + Self(signing_key) + } + + pub fn public_key(&self) -> PublicKey { + PublicKey::new(self.0.verification_key()) + } +} + +impl Signer for PrivateKey { + fn try_sign(&self, msg: &[u8]) -> Result { + Ok(self.0.sign(msg)) + } +} + +impl Keypair for PrivateKey { + type VerifyingKey = PublicKey; + + fn verifying_key(&self) -> Self::VerifyingKey { + self.public_key() + } +} + +impl Zeroize for PrivateKey { + fn zeroize(&mut self) { + self.0.zeroize() + } +} + +impl DebugSecret for PrivateKey {} +impl CloneableSecret for PrivateKey {} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct PublicKey(ed25519_consensus::VerificationKey); + +impl PublicKey { + pub fn new(key: impl Into) -> Self { + Self(key.into()) + } + + pub fn hash(&self) -> [u8; 32] { + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(self.0.as_bytes()); + hasher.finalize().into() + } +} + +impl Verifier for PublicKey { + fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> { + self.0 + .verify(signature, msg) + .map_err(|_| signature::Error::new()) + } +} diff --git a/Code/test/src/validator_set.rs b/Code/test/src/validator_set.rs new file mode 100644 index 000000000..a5cd18e81 --- /dev/null +++ b/Code/test/src/validator_set.rs @@ -0,0 +1,214 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; + +use malachite_common::VotingPower; + +use crate::{signing::PublicKey, TestContext}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Address([u8; Self::LENGTH]); + +impl Address { + const LENGTH: usize = 20; + + pub const fn new(value: [u8; Self::LENGTH]) -> Self { + Self(value) + } + + pub fn from_public_key(public_key: &PublicKey) -> Self { + let hash = public_key.hash(); + let mut address = [0; Self::LENGTH]; + address.copy_from_slice(&hash[..Self::LENGTH]); + Self(address) + } +} + +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::from_public_key(&public_key), + public_key, + voting_power, + } + } +} + +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; + } + + Self::sort_validators(&mut self.validators); + } + + /// 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) { + // Sort the validators according to the current Tendermint requirements + // + // use core::cmp::Reverse; + // + // (v. 0.34 -> first by validator power, descending, then by address, ascending) + // vals.sort_unstable_by(|v1, v2| { + // let a = (Reverse(v1.voting_power), &v1.address); + // let b = (Reverse(v2.voting_power), &v2.address); + // a.cmp(&b) + // }); + + vals.dedup(); + } + + pub fn get_proposer(&self) -> &Validator { + // TODO: Proper implementation + assert!(!self.validators.is_empty()); + + let idx = self.proposer.load(Ordering::Relaxed) % self.validators.len(); + self.proposer.fetch_add(1, Ordering::Relaxed); + + &self.validators[idx] + } +} + +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 rand::rngs::StdRng; + use rand::SeedableRng; + + use super::*; + + use crate::PrivateKey; + + #[test] + fn add_update_remove() { + let mut rng = StdRng::seed_from_u64(0x42); + + let sk1 = PrivateKey::generate(&mut rng); + let sk2 = PrivateKey::generate(&mut rng); + let sk3 = PrivateKey::generate(&mut rng); + let sk4 = PrivateKey::generate(&mut rng); + let sk5 = PrivateKey::generate(&mut rng); + let sk6 = PrivateKey::generate(&mut rng); + + let v1 = Validator::new(sk1.public_key(), 1); + let v2 = Validator::new(sk2.public_key(), 2); + let v3 = Validator::new(sk3.public_key(), 3); + + let mut vs = ValidatorSet::new(vec![v1, v2, v3]); + assert_eq!(vs.total_voting_power(), 6); + + let v4 = Validator::new(sk4.public_key(), 4); + vs.add(v4); + assert_eq!(vs.total_voting_power(), 10); + + let mut v5 = Validator::new(sk5.public_key(), 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(sk6.public_key(), 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..272cf1713 --- /dev/null +++ b/Code/test/src/value.rs @@ -0,0 +1,54 @@ +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Copy)] +pub struct ValueId(u64); + +impl ValueId { + pub const fn new(id: u64) -> Self { + Self(id) + } + + pub const fn as_u64(&self) -> u64 { + self.0 + } +} + +impl From for ValueId { + fn from(value: u64) -> Self { + Self::new(value) + } +} + +/// The value to decide on +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Value(u64); + +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 id(&self) -> ValueId { + self.id() + } +} + +impl From for Value { + fn from(value: u64) -> Self { + Self::new(value) + } +} diff --git a/Code/test/src/vote.rs b/Code/test/src/vote.rs new file mode 100644 index 000000000..d8a1a9aeb --- /dev/null +++ b/Code/test/src/vote.rs @@ -0,0 +1,83 @@ +use signature::Signer; + +use malachite_common::{Round, SignedVote, VoteType}; + +use crate::{Address, PrivateKey, TestContext, 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 validator_address: Address, +} + +impl Vote { + pub fn new_prevote(round: Round, value: Option, validator_address: Address) -> Self { + Self { + typ: VoteType::Prevote, + round, + value, + validator_address, + } + } + + pub fn new_precommit(round: Round, value: Option, address: Address) -> Self { + Self { + typ: VoteType::Precommit, + round, + value, + validator_address: address, + } + } + + // TODO: Use a canonical vote + pub fn to_bytes(&self) -> Vec { + let vtpe = match self.typ { + VoteType::Prevote => 0, + VoteType::Precommit => 1, + }; + + let mut bytes = vec![vtpe]; + bytes.extend_from_slice(&self.round.as_i64().to_be_bytes()); + bytes.extend_from_slice( + &self + .value + .map(|v| v.as_u64().to_be_bytes()) + .unwrap_or_default(), + ); + bytes + } + + pub fn signed(self, private_key: &PrivateKey) -> SignedVote { + let signature = private_key.sign(&self.to_bytes()); + + SignedVote { + vote: self, + signature, + } + } +} + +impl malachite_common::Vote for Vote { + fn round(&self) -> Round { + self.round + } + + fn value(&self) -> Option<&ValueId> { + self.value.as_ref() + } + + fn take_value(self) -> Option { + self.value + } + + fn vote_type(&self) -> VoteType { + self.typ + } + + fn validator_address(&self) -> &Address { + &self.validator_address + } +} diff --git a/Code/test/tests/driver.rs b/Code/test/tests/driver.rs new file mode 100644 index 000000000..267fe1a1e --- /dev/null +++ b/Code/test/tests/driver.rs @@ -0,0 +1,752 @@ +use malachite_common::{Context, Round, Timeout}; +use malachite_driver::{Driver, Event, Message}; +use malachite_round::state::{RoundValue, State, Step}; + +use malachite_test::{ + Address, Height, PrivateKey, Proposal, TestClient, TestContext, Validator, ValidatorSet, Vote, +}; +use rand::rngs::StdRng; +use rand::SeedableRng; + +struct TestStep { + desc: &'static str, + input_event: Option>, + expected_output: Option>, + expected_round: Round, + new_state: State, +} + +fn to_input_msg(output: Message) -> Option> { + match output { + Message::Propose(p) => Some(Event::Proposal(p)), + Message::Vote(v) => Some(Event::Vote(v)), + Message::Decide(_, _) => None, + Message::ScheduleTimeout(_) => None, + Message::NewRound(round) => Some(Event::NewRound(round)), + } +} + +#[test] +fn driver_steps_proposer() { + let value = TestContext::DUMMY_VALUE; + let value_id = value.id(); + + let client = TestClient::new(value.clone(), |_| true); + + let mut rng = StdRng::seed_from_u64(0x42); + + let sk1 = PrivateKey::generate(&mut rng); + let sk2 = PrivateKey::generate(&mut rng); + let sk3 = PrivateKey::generate(&mut rng); + + let addr1 = Address::from_public_key(&sk1.public_key()); + let addr2 = Address::from_public_key(&sk2.public_key()); + let addr3 = Address::from_public_key(&sk3.public_key()); + + let v1 = Validator::new(sk1.public_key(), 1); + let v2 = Validator::new(sk2.public_key(), 2); + let v3 = Validator::new(sk3.public_key(), 3); + + let (my_sk, my_addr) = (sk1, addr1); + + let vs = ValidatorSet::new(vec![v1, v2.clone(), v3.clone()]); + let mut driver = Driver::new(client, Height::new(1), vs, my_sk.clone(), my_addr); + + let proposal = Proposal::new(Height::new(1), Round::new(0), value.clone(), Round::new(-1)); + + let steps = vec![ + TestStep { + desc: "Start round 0, we are proposer, propose value", + input_event: Some(Event::NewRound(Round::new(0))), + expected_output: Some(Message::Propose(proposal.clone())), + expected_round: Round::new(0), + new_state: State { + round: Round::new(0), + step: Step::Propose, + proposal: None, + locked: None, + valid: None, + }, + }, + TestStep { + desc: "Receive our own proposal, prevote for it (v1)", + input_event: None, + expected_output: Some(Message::Vote( + Vote::new_prevote(Round::new(0), Some(value_id), my_addr).signed(&my_sk), + )), + expected_round: Round::new(0), + new_state: State { + round: Round::new(0), + step: Step::Prevote, + proposal: Some(proposal.clone()), + locked: None, + valid: None, + }, + }, + TestStep { + desc: "Receive our own prevote v1", + input_event: None, + expected_output: None, + expected_round: Round::new(0), + new_state: State { + round: Round::new(0), + step: Step::Prevote, + proposal: Some(proposal.clone()), + locked: None, + valid: None, + }, + }, + TestStep { + desc: "v2 prevotes for our proposal", + input_event: Some(Event::Vote( + Vote::new_prevote(Round::new(0), Some(value_id), addr2).signed(&sk2), + )), + expected_output: None, + expected_round: Round::new(0), + new_state: State { + round: Round::new(0), + step: Step::Prevote, + proposal: Some(proposal.clone()), + locked: None, + valid: None, + }, + }, + TestStep { + desc: "v3 prevotes for our proposal, we get +2/3 prevotes, precommit for it (v1)", + input_event: Some(Event::Vote( + Vote::new_prevote(Round::new(0), Some(value_id), addr3).signed(&sk3), + )), + expected_output: Some(Message::Vote( + Vote::new_precommit(Round::new(0), Some(value_id), my_addr).signed(&my_sk), + )), + expected_round: Round::new(0), + new_state: State { + 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), + }), + }, + }, + TestStep { + desc: "v1 receives its own precommit", + input_event: None, + expected_output: None, + expected_round: Round::new(0), + new_state: State { + 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), + }), + }, + }, + TestStep { + desc: "v2 precommits for our proposal", + input_event: Some(Event::Vote( + Vote::new_precommit(Round::new(0), Some(value_id), addr2).signed(&sk2), + )), + expected_output: None, + expected_round: Round::new(0), + new_state: State { + 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), + }), + }, + }, + TestStep { + desc: "v3 precommits for our proposal, we get +2/3 precommits, decide it (v1)", + input_event: Some(Event::Vote( + Vote::new_precommit(Round::new(0), Some(value_id), addr3).signed(&sk3), + )), + expected_output: Some(Message::Decide(Round::new(0), value.clone())), + expected_round: Round::new(0), + new_state: State { + 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 { + println!("Step: {}", step.desc); + + let execute_message = step + .input_event + .unwrap_or_else(|| previous_message.unwrap()); + + let output = driver.execute(execute_message); + assert_eq!(output, step.expected_output, "expected output message"); + + assert_eq!(driver.round, step.expected_round, "expected round"); + + let new_state = driver.round_state(Round::new(0)).unwrap(); + assert_eq!(new_state, &step.new_state, "expected state"); + + previous_message = output.and_then(to_input_msg); + } +} + +#[test] +fn driver_steps_not_proposer_valid() { + let value = TestContext::DUMMY_VALUE; + let value_id = value.id(); + + let client = TestClient::new(value.clone(), |_| true); + + let mut rng = StdRng::seed_from_u64(0x42); + + let sk1 = PrivateKey::generate(&mut rng); + let sk2 = PrivateKey::generate(&mut rng); + let sk3 = PrivateKey::generate(&mut rng); + + let addr1 = Address::from_public_key(&sk1.public_key()); + let addr2 = Address::from_public_key(&sk2.public_key()); + let addr3 = Address::from_public_key(&sk3.public_key()); + + let v1 = Validator::new(sk1.public_key(), 1); + let v2 = Validator::new(sk2.public_key(), 2); + let v3 = Validator::new(sk3.public_key(), 3); + + // Proposer is v1, so we are not the proposer + let (my_sk, my_addr) = (sk2, addr2); + + let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); + let mut driver = Driver::new(client, Height::new(1), vs, my_sk.clone(), my_addr); + + let proposal = Proposal::new(Height::new(1), Round::new(0), value.clone(), Round::new(-1)); + + let steps = vec![ + TestStep { + desc: "Start round 0, we are not the proposer", + input_event: Some(Event::NewRound(Round::new(0))), + expected_output: Some(Message::ScheduleTimeout(Timeout::propose(Round::new(0)))), + expected_round: Round::new(0), + new_state: State { + round: Round::new(0), + step: Step::Propose, + proposal: None, + locked: None, + valid: None, + }, + }, + TestStep { + desc: "Receive a proposal, prevote for it (v2)", + input_event: Some(Event::Proposal(proposal.clone())), + expected_output: Some(Message::Vote( + Vote::new_prevote(Round::new(0), Some(value_id), my_addr).signed(&my_sk), + )), + expected_round: Round::new(0), + new_state: State { + round: Round::new(0), + step: Step::Prevote, + proposal: Some(proposal.clone()), + locked: None, + valid: None, + }, + }, + TestStep { + desc: "Receive our own prevote (v2)", + input_event: None, + expected_output: None, + expected_round: Round::new(0), + new_state: State { + round: Round::new(0), + step: Step::Prevote, + proposal: Some(proposal.clone()), + locked: None, + valid: None, + }, + }, + TestStep { + desc: "v1 prevotes for its own proposal", + input_event: Some(Event::Vote( + Vote::new_prevote(Round::new(0), Some(value_id), addr1).signed(&sk1), + )), + expected_output: None, + expected_round: Round::new(0), + new_state: State { + round: Round::new(0), + step: Step::Prevote, + proposal: Some(proposal.clone()), + locked: None, + valid: None, + }, + }, + TestStep { + desc: "v3 prevotes for v1's proposal, it gets +2/3 prevotes, precommit for it (v2)", + input_event: Some(Event::Vote( + Vote::new_prevote(Round::new(0), Some(value_id), addr3).signed(&sk3), + )), + expected_output: Some(Message::Vote( + Vote::new_precommit(Round::new(0), Some(value_id), my_addr).signed(&my_sk), + )), + expected_round: Round::new(0), + new_state: State { + 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), + }), + }, + }, + TestStep { + desc: "we receive our own precommit", + input_event: None, + expected_output: None, + expected_round: Round::new(0), + new_state: State { + 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), + }), + }, + }, + TestStep { + desc: "v1 precommits its proposal", + input_event: Some(Event::Vote( + Vote::new_precommit(Round::new(0), Some(value_id), addr1).signed(&sk1), + )), + expected_output: None, + expected_round: Round::new(0), + new_state: State { + 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), + }), + }, + }, + TestStep { + desc: "v3 precommits for v1's proposal, it gets +2/3 precommits, decide it", + input_event: Some(Event::Vote( + Vote::new_precommit(Round::new(0), Some(value_id), addr3).signed(&sk3), + )), + expected_output: Some(Message::Decide(Round::new(0), value.clone())), + expected_round: Round::new(0), + new_state: State { + 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 { + println!("Step: {}", step.desc); + + let execute_message = step + .input_event + .unwrap_or_else(|| previous_message.unwrap()); + + let output = driver.execute(execute_message); + assert_eq!(output, step.expected_output, "expected output message"); + + assert_eq!(driver.round, step.expected_round, "expected round"); + + let new_state = driver.round_state(Round::new(0)).unwrap(); + assert_eq!(new_state, &step.new_state, "expected state"); + + previous_message = output.and_then(to_input_msg); + } +} + +#[test] +fn driver_steps_not_proposer_invalid() { + let value = TestContext::DUMMY_VALUE; + let value_id = value.id(); + + let client = TestClient::new(value.clone(), |_| false); + + let mut rng = StdRng::seed_from_u64(0x42); + + let sk1 = PrivateKey::generate(&mut rng); + let sk2 = PrivateKey::generate(&mut rng); + let sk3 = PrivateKey::generate(&mut rng); + + let addr1 = Address::from_public_key(&sk1.public_key()); + let addr2 = Address::from_public_key(&sk2.public_key()); + let addr3 = Address::from_public_key(&sk3.public_key()); + + let v1 = Validator::new(sk1.public_key(), 1); + let v2 = Validator::new(sk2.public_key(), 2); + let v3 = Validator::new(sk3.public_key(), 3); + + // Proposer is v1, so we are not the proposer + let (my_sk, my_addr) = (sk2, addr2); + + let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); + let mut driver = Driver::new(client, Height::new(1), vs, my_sk.clone(), my_addr); + + let proposal = Proposal::new(Height::new(1), Round::new(0), value.clone(), Round::new(-1)); + + let steps = vec![ + TestStep { + desc: "Start round 0, we are not the proposer", + input_event: Some(Event::NewRound(Round::new(0))), + expected_output: Some(Message::ScheduleTimeout(Timeout::propose(Round::new(0)))), + expected_round: Round::new(0), + new_state: State { + round: Round::new(0), + step: Step::Propose, + proposal: None, + locked: None, + valid: None, + }, + }, + TestStep { + desc: "Receive an invalid proposal, prevote for nil (v2)", + input_event: Some(Event::Proposal(proposal.clone())), + expected_output: Some(Message::Vote( + Vote::new_prevote(Round::new(0), None, my_addr).signed(&my_sk), + )), + expected_round: Round::new(0), + new_state: State { + round: Round::new(0), + step: Step::Prevote, + proposal: None, + locked: None, + valid: None, + }, + }, + TestStep { + desc: "Receive our own prevote (v2)", + input_event: None, + expected_output: None, + expected_round: Round::new(0), + new_state: State { + round: Round::new(0), + step: Step::Prevote, + proposal: None, + locked: None, + valid: None, + }, + }, + TestStep { + desc: "v1 prevotes for its own proposal", + input_event: Some(Event::Vote( + Vote::new_prevote(Round::new(0), Some(value_id), addr1).signed(&sk1), + )), + expected_output: None, + expected_round: Round::new(0), + new_state: State { + round: Round::new(0), + step: Step::Prevote, + proposal: None, + locked: None, + valid: None, + }, + }, + TestStep { + desc: "v3 prevotes for v1's proposal, we have polka for any, schedule prevote timeout (v2)", + input_event: Some(Event::Vote( + Vote::new_prevote(Round::new(0), Some(value_id), addr3).signed(&sk3), + )), + expected_output: Some(Message::ScheduleTimeout(Timeout::prevote(Round::new(0)))), + expected_round: Round::new(0), + new_state: State { + round: Round::new(0), + step: Step::Prevote, + proposal: None, + locked: None, + valid: None, + }, + }, + TestStep { + desc: "prevote timeout elapses, we precommit for nil (v2)", + input_event: Some(Event::TimeoutElapsed(Timeout::prevote(Round::new(0)))), + expected_output: Some(Message::Vote( + Vote::new_precommit(Round::new(0), None, my_addr).signed(&my_sk), + )), + expected_round: Round::new(0), + new_state: State { + round: Round::new(0), + step: Step::Precommit, + proposal: None, + locked: None, + valid: None, + }, + }, + ]; + + let mut previous_message = None; + + for step in steps { + println!("Step: {}", step.desc); + + let execute_message = step + .input_event + .unwrap_or_else(|| previous_message.unwrap()); + + let output = driver.execute(execute_message); + assert_eq!(output, step.expected_output, "expected output"); + + assert_eq!(driver.round, step.expected_round, "expected round"); + + let new_state = driver.round_state(driver.round).unwrap(); + assert_eq!(new_state, &step.new_state, "expected state"); + + previous_message = output.and_then(to_input_msg); + } +} + +#[test] +fn driver_steps_not_proposer_timeout_multiple_rounds() { + let value = TestContext::DUMMY_VALUE; + let value_id = value.id(); + + let mut rng = StdRng::seed_from_u64(0x42); + + let sk1 = PrivateKey::generate(&mut rng); + let sk2 = PrivateKey::generate(&mut rng); + let sk3 = PrivateKey::generate(&mut rng); + + let addr1 = Address::from_public_key(&sk1.public_key()); + let addr2 = Address::from_public_key(&sk2.public_key()); + let addr3 = Address::from_public_key(&sk3.public_key()); + + let v1 = Validator::new(sk1.public_key(), 1); + let v2 = Validator::new(sk2.public_key(), 3); + let v3 = Validator::new(sk3.public_key(), 1); + + // Proposer is v1, so we, v3, are not the proposer + let (my_sk, my_addr) = (sk3, addr3); + + let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); + let client = TestClient::new(value.clone(), |_| true); + let mut driver = Driver::new(client, Height::new(1), vs, my_sk.clone(), my_addr); + + let steps = vec![ + // Start round 0, we, v3, are not the proposer + TestStep { + desc: "Start round 0, we, v3, are not the proposer", + input_event: Some(Event::NewRound(Round::new(0))), + expected_output: Some(Message::ScheduleTimeout(Timeout::propose(Round::new(0)))), + expected_round: Round::new(0), + new_state: State { + round: Round::new(0), + step: Step::Propose, + proposal: None, + locked: None, + valid: None, + }, + }, + // Receive a propose timeout, prevote for nil (from v3) + TestStep { + desc: "Receive a propose timeout, prevote for nil (v3)", + input_event: Some(Event::TimeoutElapsed(Timeout::propose(Round::new(0)))), + expected_output: Some(Message::Vote( + Vote::new_prevote(Round::new(0), None, my_addr).signed(&my_sk), + )), + expected_round: Round::new(0), + new_state: State { + round: Round::new(0), + step: Step::Prevote, + proposal: None, + locked: None, + valid: None, + }, + }, + // Receive our own prevote v3 + TestStep { + desc: "Receive our own prevote v3", + input_event: None, + expected_output: None, + expected_round: Round::new(0), + new_state: State { + round: Round::new(0), + step: Step::Prevote, + proposal: None, + locked: None, + valid: None, + }, + }, + // v1 prevotes for its own proposal + TestStep { + desc: "v1 prevotes for its own proposal", + input_event: Some(Event::Vote( + Vote::new_prevote(Round::new(0), Some(value_id), addr1).signed(&sk1), + )), + expected_output: None, + expected_round: Round::new(0), + new_state: State { + round: Round::new(0), + step: Step::Prevote, + proposal: None, + locked: None, + valid: None, + }, + }, + // v2 prevotes for nil, we get +2/3 nil prevotes and precommit for nil + TestStep { + desc: "v2 prevotes for nil, we get +2/3 prevotes, precommit for nil", + input_event: Some(Event::Vote( + Vote::new_prevote(Round::new(0), None, addr2).signed(&sk2), + )), + expected_output: Some(Message::Vote( + Vote::new_precommit(Round::new(0), None, my_addr).signed(&my_sk), + )), + expected_round: Round::new(0), + new_state: State { + round: Round::new(0), + step: Step::Precommit, + proposal: None, + locked: None, + valid: None, + }, + }, + // v3 receives its own precommit + TestStep { + desc: "v3 receives its own precommit", + input_event: None, + expected_output: None, + expected_round: Round::new(0), + new_state: State { + round: Round::new(0), + step: Step::Precommit, + proposal: None, + locked: None, + valid: None, + }, + }, + // v1 precommits its proposal + TestStep { + desc: "v1 precommits its proposal", + input_event: Some(Event::Vote( + Vote::new_precommit(Round::new(0), Some(value_id), addr1).signed(&sk1), + )), + expected_output: None, + expected_round: Round::new(0), + new_state: State { + round: Round::new(0), + step: Step::Precommit, + proposal: None, + locked: None, + valid: None, + }, + }, + // v2 precommits for nil + TestStep { + desc: "v2 precommits for nil", + input_event: Some(Event::Vote( + Vote::new_precommit(Round::new(0), None, addr2).signed(&sk2), + )), + expected_output: Some(Message::ScheduleTimeout(Timeout::precommit(Round::new(0)))), + expected_round: Round::new(0), + new_state: State { + 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_event: Some(Event::TimeoutElapsed(Timeout::precommit(Round::new(0)))), + expected_output: Some(Message::NewRound(Round::new(1))), + expected_round: Round::new(0), + new_state: State { + round: Round::new(1), + step: Step::NewRound, + proposal: None, + locked: None, + valid: None, + }, + }, + TestStep { + desc: "Start round 1, we are not the proposer", + input_event: Some(Event::NewRound(Round::new(1))), + expected_output: Some(Message::ScheduleTimeout(Timeout::propose(Round::new(1)))), + expected_round: Round::new(1), + new_state: State { + round: Round::new(1), + step: Step::Propose, + proposal: None, + locked: None, + valid: None, + }, + }, + ]; + + let mut previous_message = None; + + for step in steps { + println!("Step: {}", step.desc); + + let execute_message = step + .input_event + .unwrap_or_else(|| previous_message.unwrap()); + + let output = driver.execute(execute_message); + assert_eq!(output, step.expected_output, "expected output message"); + + assert_eq!(driver.round, step.expected_round, "expected round"); + + let new_state = driver.round_state(driver.round).unwrap(); + assert_eq!(new_state, &step.new_state, "new state"); + + previous_message = output.and_then(to_input_msg); + } +} diff --git a/Code/test/tests/round.rs b/Code/test/tests/round.rs new file mode 100644 index 000000000..9f9b606fc --- /dev/null +++ b/Code/test/tests/round.rs @@ -0,0 +1,72 @@ +use malachite_test::{Address, Height, Proposal, TestContext, Value}; + +use malachite_common::{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, RoundData}; + +const ADDRESS: Address = Address::new([42; 20]); + +#[test] +fn test_propose() { + let value = Value::new(42); + let height = Height::new(10); + let round = Round::new(0); + + let mut state: State = State { + round, + ..Default::default() + }; + + let data = RoundData::new(round, &height, &ADDRESS); + + let transition = apply_event(state.clone(), &data, Event::NewRoundProposer(value)); + + state.step = Step::Propose; + assert_eq!(transition.next_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 height = Height::new(1); + + let state: State = State::default().new_round(Round::new(1)); + let data = RoundData::new(Round::new(1), &height, &ADDRESS); + + let transition = apply_event(state, &data, Event::NewRound); + + assert_eq!(transition.next_state.step, Step::Propose); + assert_eq!( + transition.message.unwrap(), + Message::ScheduleTimeout(Timeout { + round: Round::new(1), + step: TimeoutStep::Propose + }) + ); + + let state = transition.next_state; + + let transition = apply_event( + state, + &data, + Event::Proposal(Proposal::new( + Height::new(1), + Round::new(1), + value.clone(), + Round::Nil, + )), + ); + + assert_eq!(transition.next_state.step, Step::Prevote); + assert_eq!( + transition.message.unwrap(), + Message::prevote(Round::new(1), Some(value.id()), ADDRESS) + ); +} diff --git a/Code/test/tests/round_votes.rs b/Code/test/tests/round_votes.rs new file mode 100644 index 000000000..8be3f46e4 --- /dev/null +++ b/Code/test/tests/round_votes.rs @@ -0,0 +1,92 @@ +use malachite_common::VoteType; +use malachite_vote::round_votes::RoundVotes; +use malachite_vote::Threshold; + +use malachite_test::{Address, ValueId}; + +const ADDRESS1: Address = Address::new([41; 20]); +const ADDRESS2: Address = Address::new([42; 20]); +const ADDRESS3: Address = Address::new([43; 20]); +const ADDRESS4: Address = Address::new([44; 20]); +const ADDRESS5: Address = Address::new([45; 20]); +const ADDRESS6: Address = Address::new([46; 20]); + +#[test] +fn add_votes_nil() { + let total = 3; + + let mut round_votes: RoundVotes<_, ValueId> = RoundVotes::new(total, Default::default()); + + // add a vote for nil. nothing changes. + let thresh = round_votes.add_vote(VoteType::Prevote, ADDRESS1, None, 1); + assert_eq!(thresh, Threshold::Unreached); + + // add it again, nothing changes. + let thresh = round_votes.add_vote(VoteType::Prevote, ADDRESS2, None, 1); + assert_eq!(thresh, Threshold::Unreached); + + // add it again, get Nil + let thresh = round_votes.add_vote(VoteType::Prevote, ADDRESS3, None, 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<_, ValueId> = RoundVotes::new(total, Default::default()); + + // add a vote. nothing changes. + let thresh = round_votes.add_vote(VoteType::Prevote, ADDRESS1, val, weight); + assert_eq!(thresh, Threshold::Unreached); + + // add it again, nothing changes. + let thresh = round_votes.add_vote(VoteType::Prevote, ADDRESS2, val, weight); + assert_eq!(thresh, Threshold::Unreached); + + // add a vote for nil, get Thresh::Any + let thresh = round_votes.add_vote(VoteType::Prevote, ADDRESS3, None, weight); + assert_eq!(thresh, Threshold::Any); + + // add vote for value, get Thresh::Value + let thresh = round_votes.add_vote(VoteType::Prevote, ADDRESS4, val, 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<_, ValueId> = RoundVotes::new(total, Default::default()); + + // add a vote for v1. nothing changes. + let thresh = round_votes.add_vote(VoteType::Precommit, ADDRESS1, val1, 1); + assert_eq!(thresh, Threshold::Unreached); + + // add a vote for v2. nothing changes. + let thresh = round_votes.add_vote(VoteType::Precommit, ADDRESS2, val2, 1); + assert_eq!(thresh, Threshold::Unreached); + + // add a vote for nil. nothing changes. + let thresh = round_votes.add_vote(VoteType::Precommit, ADDRESS3, None, 1); + assert_eq!(thresh, Threshold::Unreached); + + // add a vote for v1. nothing changes + let thresh = round_votes.add_vote(VoteType::Precommit, ADDRESS4, val1, 1); + assert_eq!(thresh, Threshold::Unreached); + + // add a vote for v2. nothing changes + let thresh = round_votes.add_vote(VoteType::Precommit, ADDRESS5, val2, 1); + assert_eq!(thresh, Threshold::Unreached); + + // add a big vote for v2. get Value(v2) + let thresh = round_votes.add_vote(VoteType::Precommit, ADDRESS6, val2, 10); + assert_eq!(thresh, Threshold::Value(v2)); +} diff --git a/Code/test/tests/vote_count.rs b/Code/test/tests/vote_count.rs new file mode 100644 index 000000000..179222dda --- /dev/null +++ b/Code/test/tests/vote_count.rs @@ -0,0 +1,155 @@ +#![allow(clippy::bool_assert_comparison)] + +use malachite_vote::count::VoteCount; +use malachite_vote::Threshold; + +#[test] +fn vote_count_nil() { + let mut vc = VoteCount::new(4, Default::default()); + + let addr1 = [1]; + let addr2 = [2]; + let addr3 = [3]; + let addr4 = [4]; + + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr1, None, 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 1); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr2, None, 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 2); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + // addr1 votes again, is ignored + assert_eq!(vc.add(addr1, None, 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 2); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr3, None, 1), Threshold::Nil); + assert_eq!(vc.get(&None), 3); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr4, Some(1), 1), Threshold::Any); + assert_eq!(vc.get(&None), 3); + assert_eq!(vc.get(&Some(1)), 1); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); +} + +#[test] +fn vote_count_value() { + let mut vc = VoteCount::new(4, Default::default()); + + let addr1 = [1]; + let addr2 = [2]; + let addr3 = [3]; + let addr4 = [4]; + + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr1, Some(1), 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 1); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr2, Some(1), 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 2); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + // addr1 votes again, for nil this time, is ignored + assert_eq!(vc.add(addr1, None, 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 2); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr3, Some(1), 1), Threshold::Value(1)); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 3); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + // addr2 votes again, for the same value, is ignored + assert_eq!(vc.add(addr2, Some(1), 1), Threshold::Value(1)); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 3); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr4, Some(2), 1), Threshold::Any); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 3); + assert_eq!(vc.get(&Some(2)), 1); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + // addr4 votes again, for a different value, is ignored + assert_eq!(vc.add(addr4, Some(3), 1), Threshold::Any); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 3); + assert_eq!(vc.get(&Some(2)), 1); + assert_eq!(vc.get(&Some(3)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); +} diff --git a/Code/test/tests/vote_keeper.rs b/Code/test/tests/vote_keeper.rs new file mode 100644 index 000000000..8dedc2b3c --- /dev/null +++ b/Code/test/tests/vote_keeper.rs @@ -0,0 +1,91 @@ +use malachite_common::Round; +use malachite_vote::keeper::{Message, VoteKeeper}; + +use malachite_test::{Address, TestContext, ValueId, Vote}; + +const ADDRESS1: Address = Address::new([41; 20]); +const ADDRESS2: Address = Address::new([42; 20]); +const ADDRESS3: Address = Address::new([43; 20]); +const ADDRESS4: Address = Address::new([44; 20]); + +#[test] +fn prevote_apply_nil() { + let mut keeper: VoteKeeper = VoteKeeper::new(3); + + let vote = Vote::new_prevote(Round::new(0), None, ADDRESS1); + let msg = keeper.apply_vote(vote.clone(), 1); + assert_eq!(msg, None); + + let vote = Vote::new_prevote(Round::new(0), None, ADDRESS2); + let msg = keeper.apply_vote(vote.clone(), 1); + assert_eq!(msg, None); + + let vote = Vote::new_prevote(Round::new(0), None, ADDRESS3); + let msg = keeper.apply_vote(vote, 1); + assert_eq!(msg, Some(Message::PolkaNil)); +} + +#[test] +fn precommit_apply_nil() { + let mut keeper: VoteKeeper = VoteKeeper::new(3); + + let vote = Vote::new_precommit(Round::new(0), None, ADDRESS1); + let msg = keeper.apply_vote(vote.clone(), 1); + assert_eq!(msg, None); + + let vote = Vote::new_precommit(Round::new(0), None, ADDRESS2); + let msg = keeper.apply_vote(vote.clone(), 1); + assert_eq!(msg, None); + + let vote = Vote::new_precommit(Round::new(0), None, ADDRESS3); + let msg = keeper.apply_vote(vote, 1); + assert_eq!(msg, Some(Message::PrecommitAny)); +} + +#[test] +fn prevote_apply_single_value() { + let mut keeper: VoteKeeper = VoteKeeper::new(4); + + let v = ValueId::new(1); + let val = Some(v); + + let vote = Vote::new_prevote(Round::new(0), val, ADDRESS1); + let msg = keeper.apply_vote(vote.clone(), 1); + assert_eq!(msg, None); + + let vote = Vote::new_prevote(Round::new(0), val, ADDRESS2); + let msg = keeper.apply_vote(vote.clone(), 1); + assert_eq!(msg, None); + + let vote_nil = Vote::new_prevote(Round::new(0), None, ADDRESS3); + let msg = keeper.apply_vote(vote_nil, 1); + assert_eq!(msg, Some(Message::PolkaAny)); + + let vote = Vote::new_prevote(Round::new(0), val, ADDRESS4); + let msg = keeper.apply_vote(vote, 1); + assert_eq!(msg, Some(Message::PolkaValue(v))); +} + +#[test] +fn precommit_apply_single_value() { + let mut keeper: VoteKeeper = VoteKeeper::new(4); + + let v = ValueId::new(1); + let val = Some(v); + + let vote = Vote::new_precommit(Round::new(0), val, ADDRESS1); + let msg = keeper.apply_vote(vote.clone(), 1); + assert_eq!(msg, None); + + let vote = Vote::new_precommit(Round::new(0), val, ADDRESS2); + let msg = keeper.apply_vote(vote.clone(), 1); + assert_eq!(msg, None); + + let vote_nil = Vote::new_precommit(Round::new(0), None, ADDRESS3); + let msg = keeper.apply_vote(vote_nil, 1); + assert_eq!(msg, Some(Message::PrecommitAny)); + + let vote = Vote::new_precommit(Round::new(0), val, ADDRESS4); + let msg = keeper.apply_vote(vote, 1); + assert_eq!(msg, Some(Message::PrecommitValue(v))); +} diff --git a/Code/vote/Cargo.toml b/Code/vote/Cargo.toml new file mode 100644 index 000000000..184172208 --- /dev/null +++ b/Code/vote/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "malachite-vote" +description = "Voting system for the Malachite consensus engine" + +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +malachite-common = { version = "0.1.0", path = "../common" } diff --git a/Code/vote/src/count.rs b/Code/vote/src/count.rs new file mode 100644 index 000000000..5c7b66dcc --- /dev/null +++ b/Code/vote/src/count.rs @@ -0,0 +1,276 @@ +use alloc::collections::BTreeSet; + +use crate::value_weights::ValuesWeights; +use crate::{Threshold, ThresholdParams, Weight}; + +/// VoteCount tallys votes of the same type. +/// Votes are for nil or for some value. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct VoteCount { + /// Total weight + pub total_weight: Weight, + + /// The threshold parameters + pub threshold_params: ThresholdParams, + + /// Weight of votes for the values, including nil + pub values_weights: ValuesWeights>, + + /// Addresses of validators who voted for the values + pub validator_addresses: BTreeSet
, +} + +impl VoteCount { + pub fn new(total_weight: Weight, threshold_params: ThresholdParams) -> Self { + VoteCount { + total_weight, + threshold_params, + values_weights: ValuesWeights::new(), + validator_addresses: BTreeSet::new(), + } + } + + /// Add vote for a value (or nil) to internal counters, but only if we haven't seen + /// a vote from that particular validator yet. + pub fn add( + &mut self, + address: Address, + value: Option, + weight: Weight, + ) -> Threshold + where + Address: Clone + Ord, + Value: Clone + Ord, + { + let already_voted = !self.validator_addresses.insert(address); + + if !already_voted { + self.values_weights.add(value.clone(), weight); + } + + self.compute_threshold(value) + } + + /// Compute whether or not we have reached a threshold for the given value, + /// and return that threshold. + pub fn compute_threshold(&self, value: Option) -> Threshold + where + Address: Ord, + Value: Ord, + { + let weight = self.values_weights.get(&value); + + match value { + Some(value) if self.is_quorum(weight, self.total_weight) => Threshold::Value(value), + + None if self.is_quorum(weight, self.total_weight) => Threshold::Nil, + + _ => { + let sum_weight = self.values_weights.sum(); + + if self.is_quorum(sum_weight, self.total_weight) { + Threshold::Any + } else { + Threshold::Unreached + } + } + } + } + + /// Return whether or not the threshold is met, ie. if we have a quorum for that threshold. + pub fn is_threshold_met(&self, threshold: Threshold) -> bool + where + Value: Ord, + { + match threshold { + Threshold::Value(value) => { + let weight = self.values_weights.get(&Some(value)); + self.is_quorum(weight, self.total_weight) + } + + Threshold::Nil => { + let weight = self.values_weights.get(&None); + self.is_quorum(weight, self.total_weight) + } + + Threshold::Any => { + let sum_weight = self.values_weights.sum(); + self.is_quorum(sum_weight, self.total_weight) + } + + Threshold::Skip | Threshold::Unreached => false, + } + } + + pub fn get(&self, value: &Option) -> Weight + where + Value: Ord, + { + self.values_weights.get(value) + } + + pub fn total_weight(&self) -> Weight { + self.total_weight + } + + fn is_quorum(&self, sum: Weight, total: Weight) -> bool { + self.threshold_params.quorum.is_met(sum, total) + } +} + +#[cfg(test)] +#[allow(clippy::bool_assert_comparison)] +mod tests { + use super::*; + + #[test] + fn vote_count_nil() { + let mut vc = VoteCount::new(4, Default::default()); + + let addr1 = [1]; + let addr2 = [2]; + let addr3 = [3]; + let addr4 = [4]; + + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr1, None, 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 1); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr2, None, 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 2); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + // addr1 votes again, is ignored + assert_eq!(vc.add(addr1, None, 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 2); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr3, None, 1), Threshold::Nil); + assert_eq!(vc.get(&None), 3); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr4, Some(1), 1), Threshold::Any); + assert_eq!(vc.get(&None), 3); + assert_eq!(vc.get(&Some(1)), 1); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + } + + #[test] + fn vote_count_value() { + let mut vc = VoteCount::new(4, Default::default()); + + let addr1 = [1]; + let addr2 = [2]; + let addr3 = [3]; + let addr4 = [4]; + + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr1, Some(1), 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 1); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr2, Some(1), 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 2); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + // addr1 votes again, for nil this time, is ignored + assert_eq!(vc.add(addr1, None, 1), Threshold::Unreached); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 2); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr3, Some(1), 1), Threshold::Value(1)); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 3); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + // addr2 votes again, for the same value, is ignored + assert_eq!(vc.add(addr2, Some(1), 1), Threshold::Value(1)); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 3); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add(addr4, Some(2), 1), Threshold::Any); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 3); + assert_eq!(vc.get(&Some(2)), 1); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + // addr4 votes again, for a different value, is ignored + assert_eq!(vc.add(addr4, Some(3), 1), Threshold::Any); + assert_eq!(vc.get(&None), 0); + assert_eq!(vc.get(&Some(1)), 3); + assert_eq!(vc.get(&Some(2)), 1); + assert_eq!(vc.get(&Some(3)), 0); + assert_eq!(vc.is_threshold_met(Threshold::Unreached), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + } +} diff --git a/Code/vote/src/keeper.rs b/Code/vote/src/keeper.rs new file mode 100644 index 000000000..5694ec2d7 --- /dev/null +++ b/Code/vote/src/keeper.rs @@ -0,0 +1,145 @@ +use alloc::collections::{BTreeMap, BTreeSet}; + +use malachite_common::{Context, Round, ValueId, Vote, VoteType}; + +use crate::round_votes::RoundVotes; +use crate::round_weights::RoundWeights; +use crate::{Threshold, ThresholdParam, ThresholdParams, Weight}; + +/// Messages emitted by the vote keeper +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Message { + PolkaAny, + PolkaNil, + PolkaValue(Value), + PrecommitAny, + PrecommitValue(Value), + SkipRound(Round), +} + +#[derive(Clone, Debug)] +struct PerRound +where + Ctx: Context, +{ + votes: RoundVotes>, + addresses_weights: RoundWeights, + emitted_msgs: BTreeSet>>, +} + +impl PerRound +where + Ctx: Context, +{ + fn new(total_weight: Weight, threshold_params: ThresholdParams) -> Self { + Self { + votes: RoundVotes::new(total_weight, threshold_params), + addresses_weights: RoundWeights::new(), + emitted_msgs: BTreeSet::new(), + } + } +} + +/// Keeps track of votes and emits messages when thresholds are reached. +#[derive(Clone, Debug)] +pub struct VoteKeeper +where + Ctx: Context, +{ + threshold_params: ThresholdParams, + total_weight: Weight, + per_round: BTreeMap>, +} + +impl VoteKeeper +where + Ctx: Context, +{ + pub fn new(total_weight: Weight) -> Self { + VoteKeeper { + // TODO: Make these configurable + threshold_params: ThresholdParams::default(), + + total_weight, + per_round: BTreeMap::new(), + } + } + + /// Apply a vote with a given weight, potentially triggering an event. + pub fn apply_vote(&mut self, vote: Ctx::Vote, weight: Weight) -> Option>> { + let round = self + .per_round + .entry(vote.round()) + .or_insert_with(|| PerRound::new(self.total_weight, self.threshold_params)); + + let threshold = round.votes.add_vote( + vote.vote_type(), + vote.validator_address().clone(), + vote.value().cloned(), + weight, + ); + + round + .addresses_weights + .set_once(vote.validator_address().clone(), weight); + + let msg = threshold_to_message(vote.vote_type(), vote.round(), threshold)?; + + let final_msg = if !round.emitted_msgs.contains(&msg) { + Some(msg) + } else if Self::skip_round(round, self.total_weight, self.threshold_params.honest) { + Some(Message::SkipRound(vote.round())) + } else { + None + }; + + if let Some(final_msg) = &final_msg { + round.emitted_msgs.insert(final_msg.clone()); + } + + final_msg + } + + /// Check if a threshold is met, ie. if we have a quorum for that threshold. + pub fn is_threshold_met( + &self, + round: &Round, + vote_type: VoteType, + threshold: Threshold>, + ) -> bool { + self.per_round.get(round).map_or(false, |round| { + round.votes.is_threshold_met(vote_type, threshold) + }) + } + + /// Check whether or not we should skip this round, in case we haven't emitted any messages + /// yet, and we have reached an honest threshold for the round. + fn skip_round( + round: &PerRound, + total_weight: Weight, + threshold_param: ThresholdParam, + ) -> bool { + round.emitted_msgs.is_empty() + && threshold_param.is_met(round.addresses_weights.sum(), total_weight) + } +} + +/// Map a vote type and a threshold to a state machine event. +fn threshold_to_message( + typ: VoteType, + round: Round, + threshold: Threshold, +) -> Option> { + match (typ, threshold) { + (_, Threshold::Unreached) => None, + (_, Threshold::Skip) => Some(Message::SkipRound(round)), + + (VoteType::Prevote, Threshold::Any) => Some(Message::PolkaAny), + (VoteType::Prevote, Threshold::Nil) => Some(Message::PolkaNil), + (VoteType::Prevote, Threshold::Value(v)) => Some(Message::PolkaValue(v)), + + (VoteType::Precommit, Threshold::Any) => Some(Message::PrecommitAny), + (VoteType::Precommit, Threshold::Nil) => Some(Message::PrecommitAny), + (VoteType::Precommit, Threshold::Value(v)) => Some(Message::PrecommitValue(v)), + } +} diff --git a/Code/vote/src/lib.rs b/Code/vote/src/lib.rs new file mode 100644 index 000000000..cc27fe823 --- /dev/null +++ b/Code/vote/src/lib.rs @@ -0,0 +1,91 @@ +//! Tally votes of the same type (eg. prevote or precommit) + +#![forbid(unsafe_code)] +#![deny(unused_crate_dependencies, trivial_casts, trivial_numeric_casts)] +#![warn( + // missing_docs, + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, + variant_size_differences +)] +#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::panic))] + +extern crate alloc; + +pub mod count; +pub mod keeper; +pub mod round_votes; +pub mod round_weights; +pub mod value_weights; + +// TODO: Introduce newtype +// QUESTION: Over what type? i64? +pub type Weight = u64; + +/// Represents the different quorum thresholds. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Threshold { + /// No quorum has been reached yet + Unreached, + + /// Minimum number of votes correct processes, + /// if at a round higher than current then skip to that round. + Skip, + + /// Quorum of votes but not for the same value + Any, + + /// Quorum of votes for nil + Nil, + + /// Quorum (+2/3) of votes for a value + Value(ValueId), +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct ThresholdParams { + /// Threshold for a quorum (default: 2f+1) + pub quorum: ThresholdParam, + + /// Threshold for the minimum number of honest nodes (default: f+1) + pub honest: ThresholdParam, +} + +impl Default for ThresholdParams { + fn default() -> Self { + Self { + quorum: ThresholdParam::TWO_F_PLUS_ONE, + honest: ThresholdParam::F_PLUS_ONE, + } + } +} + +/// Represents the different quorum thresholds. +/// +/// TODO: Distinguish between quorum and honest thresholds at the type-level +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct ThresholdParam { + pub numerator: u64, + pub denominator: u64, +} + +impl ThresholdParam { + /// 2f+1 + pub const TWO_F_PLUS_ONE: Self = Self::new(2, 3); + + /// f+1 + pub const F_PLUS_ONE: Self = Self::new(1, 3); + + pub const fn new(numerator: u64, denominator: u64) -> Self { + Self { + numerator, + denominator, + } + } + + /// Check whether the threshold is met. + pub const fn is_met(&self, weight: Weight, total: Weight) -> bool { + // FIXME: Deal with overflows + weight * self.denominator > total * self.numerator + } +} diff --git a/Code/vote/src/round_votes.rs b/Code/vote/src/round_votes.rs new file mode 100644 index 000000000..21a14f80b --- /dev/null +++ b/Code/vote/src/round_votes.rs @@ -0,0 +1,47 @@ +use malachite_common::VoteType; + +use crate::count::VoteCount; +use crate::{Threshold, ThresholdParams, Weight}; + +/// Tracks all the votes for a single round +#[derive(Clone, Debug)] +pub struct RoundVotes { + prevotes: VoteCount, + precommits: VoteCount, +} + +impl RoundVotes { + pub fn new(total_weight: Weight, threshold_params: ThresholdParams) -> Self { + RoundVotes { + prevotes: VoteCount::new(total_weight, threshold_params), + precommits: VoteCount::new(total_weight, threshold_params), + } + } + + pub fn add_vote( + &mut self, + vote_type: VoteType, + address: Address, + value: Option, + weight: Weight, + ) -> Threshold + where + Address: Clone + Ord, + Value: Clone + Ord, + { + match vote_type { + VoteType::Prevote => self.prevotes.add(address, value, weight), + VoteType::Precommit => self.precommits.add(address, value, weight), + } + } + + pub fn is_threshold_met(&self, vote_type: VoteType, threshold: Threshold) -> bool + where + Value: Ord, + { + match vote_type { + VoteType::Prevote => self.prevotes.is_threshold_met(threshold), + VoteType::Precommit => self.precommits.is_threshold_met(threshold), + } + } +} diff --git a/Code/vote/src/round_weights.rs b/Code/vote/src/round_weights.rs new file mode 100644 index 000000000..63213782d --- /dev/null +++ b/Code/vote/src/round_weights.rs @@ -0,0 +1,40 @@ +use alloc::collections::BTreeMap; + +use crate::Weight; + +#[derive(Clone, Debug)] +pub struct RoundWeights
{ + map: BTreeMap, +} + +impl
RoundWeights
{ + pub fn new() -> Self { + RoundWeights { + map: BTreeMap::new(), + } + } + + pub fn set_once(&mut self, address: Address, weight: Weight) + where + Address: Ord, + { + self.map.entry(address).or_insert(weight); + } + + pub fn get(&self, address: &Address) -> Weight + where + Address: Ord, + { + *self.map.get(address).unwrap_or(&0) + } + + pub fn sum(&self) -> Weight { + self.map.values().sum() + } +} + +impl
Default for RoundWeights
{ + fn default() -> Self { + Self::new() + } +} diff --git a/Code/vote/src/value_weights.rs b/Code/vote/src/value_weights.rs new file mode 100644 index 000000000..7b1e355af --- /dev/null +++ b/Code/vote/src/value_weights.rs @@ -0,0 +1,82 @@ +use alloc::collections::BTreeMap; + +use crate::Weight; + +/// A value and the weight of votes for it. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ValuesWeights { + value_weights: BTreeMap, +} + +impl ValuesWeights { + pub fn new() -> ValuesWeights { + ValuesWeights { + value_weights: BTreeMap::new(), + } + } + + /// Add weight to the value and return the new weight. + pub fn add(&mut self, value: Value, weight: Weight) -> Weight + where + Value: Ord, + { + let entry = self.value_weights.entry(value).or_insert(0); + *entry += weight; // FIXME: Deal with overflows + *entry + } + + /// Return the weight of the value, or 0 if it is not present. + pub fn get(&self, value: &Value) -> Weight + where + Value: Ord, + { + self.value_weights.get(value).copied().unwrap_or(0) + } + + /// Return the sum of the weights of all values. + pub fn sum(&self) -> Weight { + self.value_weights.values().sum() // FIXME: Deal with overflows + } +} + +impl Default for ValuesWeights { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn values_weights() { + let mut vw = ValuesWeights::new(); + + assert_eq!(vw.get(&None), 0); + assert_eq!(vw.get(&Some(1)), 0); + + assert_eq!(vw.add(None, 1), 1); + assert_eq!(vw.get(&None), 1); + assert_eq!(vw.get(&Some(1)), 0); + + assert_eq!(vw.add(Some(1), 1), 1); + assert_eq!(vw.get(&None), 1); + assert_eq!(vw.get(&Some(1)), 1); + + assert_eq!(vw.add(None, 1), 2); + assert_eq!(vw.get(&None), 2); + assert_eq!(vw.get(&Some(1)), 1); + + assert_eq!(vw.add(Some(1), 1), 2); + assert_eq!(vw.get(&None), 2); + assert_eq!(vw.get(&Some(1)), 2); + + assert_eq!(vw.add(Some(2), 1), 1); + assert_eq!(vw.get(&None), 2); + assert_eq!(vw.get(&Some(1)), 2); + assert_eq!(vw.get(&Some(2)), 1); + + // FIXME: Test for and deal with overflows + } +} diff --git a/Docs/The latest gossip on BFT consenus.pdf b/Docs/The latest gossip on BFT consenus.pdf new file mode 100644 index 000000000..ea677bec4 Binary files /dev/null and b/Docs/The latest gossip on BFT consenus.pdf differ diff --git a/Docs/architecture/adr-template.md b/Docs/architecture/adr-template.md new file mode 100644 index 000000000..28a5ecfbb --- /dev/null +++ b/Docs/architecture/adr-template.md @@ -0,0 +1,36 @@ +# ADR {ADR-NUMBER}: {TITLE} + +## Changelog +* {date}: {changelog} + +## Context + +> This section contains all the context one needs to understand the current state, and why there is a problem. It should be as succinct as possible and introduce the high level idea behind the solution. +## Decision + +> This section explains all of the details of the proposed solution, including implementation details. +It should also describe affects / corollary items that may need to be changed as a part of this. +If the proposed change will be large, please also indicate a way to do the change to maximize ease of review. +(e.g. the optimal split of things to do between separate PR's) + +## Status + +> A decision may be "proposed" if it hasn't been agreed upon yet, or "accepted" once it is agreed upon. If a later ADR changes or reverses a decision, it may be marked as "deprecated" or "superseded" with a reference to its replacement. + +{Deprecated|Proposed|Accepted} + +## Consequences + +> This section describes the consequences, after applying the decision. All consequences should be summarized here, not just the "positive" ones. + +### Positive + +### Negative + +### Neutral + +## References + +> Are there any relevant PR comments, issues that led up to this, or articles referrenced for why we made the given design choice? If so link them here! + +* {reference link} diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..4a394a747 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,26 @@ +codecov: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: nearest + range: "50...100" + + status: + project: + default: + target: auto + threshold: 5% + removed_code_behavior: adjust_base + paths: + - "Code" + patch: + default: + target: auto + threshold: 5% + paths: + - "Code" + + changes: + default: + informational: true