diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 5490bdb7a..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,10 +29,8 @@ 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 - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 diff --git a/.github/workflows/quint.yml b/.github/workflows/quint.yml new file mode 100644 index 000000000..03ebab6db --- /dev/null +++ b/.github/workflows/quint.yml @@ -0,0 +1,29 @@ +on: + workflow_dispatch: + push: + branches: + - main + paths: + - Specs/Quint/** + pull_request: + paths: + - Specs/Quint/** + +name: Quint + +jobs: + quint-typecheck: + name: Typecheck + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./Specs/Quint + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: "18" + - run: npm install -g @informalsystems/quint + - run: npx @informalsystems/quint typecheck consensus.qnt + - run: npx @informalsystems/quint typecheck voteBookkeeper.qnt + diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a46ab73b9..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: diff --git a/Code/QUESTIONS.md b/Code/QUESTIONS.md index cd292d13e..700c9fae4 100644 --- a/Code/QUESTIONS.md +++ b/Code/QUESTIONS.md @@ -1,2 +1 @@ - How do we deal with errors? -- How do we parametrize over values, id(v), etc. diff --git a/Code/TODO.md b/Code/TODO.md index 44d4c2a0b..8a7878e9e 100644 --- a/Code/TODO.md +++ b/Code/TODO.md @@ -8,6 +8,4 @@ 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 contextt - -abstract over values and validator sets +signing context diff --git a/Code/common/Cargo.toml b/Code/common/Cargo.toml index b46b5148e..37a2afea2 100644 --- a/Code/common/Cargo.toml +++ b/Code/common/Cargo.toml @@ -5,3 +5,5 @@ edition = "2021" publish = false [dependencies] +secrecy = "0.8.0" +signature = "2.1.0" diff --git a/Code/common/src/consensus.rs b/Code/common/src/context.rs similarity index 63% rename from Code/common/src/consensus.rs rename to Code/common/src/context.rs index 0fc01eaac..8469c6365 100644 --- a/Code/common/src/consensus.rs +++ b/Code/common/src/context.rs @@ -1,28 +1,34 @@ use crate::{ - Address, Height, Proposal, PublicKey, Round, Validator, ValidatorSet, Value, ValueId, Vote, + 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 Consensus +pub trait Context where Self: Sized, { type Address: Address; type Height: Height; type Proposal: Proposal; - type PublicKey: PublicKey; type Validator: Validator; type ValidatorSet: ValidatorSet; type Value: Value; type Vote: Vote; - - // FIXME: Remove this and thread it through where necessary - const DUMMY_ADDRESS: Self::Address; + 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, diff --git a/Code/common/src/lib.rs b/Code/common/src/lib.rs index 4da2130a2..b9a6306af 100644 --- a/Code/common/src/lib.rs +++ b/Code/common/src/lib.rs @@ -1,5 +1,6 @@ //! Common data types and abstractions for the consensus engine. +#![no_std] #![forbid(unsafe_code)] #![deny(unused_crate_dependencies, trivial_casts, trivial_numeric_casts)] #![warn( @@ -10,23 +11,33 @@ )] #![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::panic))] -mod consensus; +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 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 consensus::Consensus; +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, PublicKey, Validator, ValidatorSet, VotingPower}; +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 index fc5d8f703..4adcc3b38 100644 --- a/Code/common/src/proposal.rs +++ b/Code/common/src/proposal.rs @@ -1,20 +1,21 @@ use core::fmt::Debug; -use crate::{Consensus, Round}; +use crate::{Context, Round}; /// Defines the requirements for a proposal type. -pub trait Proposal +pub trait Proposal where Self: Clone + Debug + PartialEq + Eq, + Ctx: Context, { /// The height for which the proposal is for. - fn height(&self) -> C::Height; + 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) -> &C::Value; + 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 index a281a95c5..79fd567a6 100644 --- a/Code/common/src/round.rs +++ b/Code/common/src/round.rs @@ -1,3 +1,5 @@ +use core::cmp; + /// A round number. /// /// Can be either: @@ -69,13 +71,13 @@ impl Round { } impl PartialOrd for Round { - fn partial_cmp(&self, other: &Self) -> Option { + fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for Round { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { + fn cmp(&self, other: &Self) -> cmp::Ordering { self.as_i64().cmp(&other.as_i64()) } } 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/validator_set.rs b/Code/common/src/validator_set.rs index cd58f1698..408b1517b 100644 --- a/Code/common/src/validator_set.rs +++ b/Code/common/src/validator_set.rs @@ -1,19 +1,12 @@ use core::fmt::Debug; -use crate::Consensus; +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 a public key type. -pub trait PublicKey -where - Self: Clone + Debug + PartialEq + Eq, -{ -} - /// Defines the requirements for an address. /// /// TODO: Keep this trait or just add the bounds to Consensus::Address? @@ -24,16 +17,16 @@ where } /// Defines the requirements for a validator. -pub trait Validator +pub trait Validator where Self: Clone + Debug + PartialEq + Eq, - C: Consensus, + Ctx: Context, { /// The address of the validator, typically derived from its public key. - fn address(&self) -> &C::Address; + fn address(&self) -> &Ctx::Address; /// The public key of the validator, used to verify signatures. - fn public_key(&self) -> &C::PublicKey; + fn public_key(&self) -> &PublicKey; /// The voting power held by the validaror. fn voting_power(&self) -> VotingPower; @@ -42,19 +35,19 @@ where /// Defines the requirements for a validator set. /// /// A validator set is a collection of validators. -pub trait ValidatorSet +pub trait ValidatorSet where - C: Consensus, + 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) -> C::Validator; + fn get_proposer(&self) -> Ctx::Validator; /// Get the validator with the given public key. - fn get_by_public_key(&self, public_key: &C::PublicKey) -> Option<&C::Validator>; + 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: &C::Address) -> Option<&C::Validator>; + fn get_by_address(&self, address: &Ctx::Address) -> Option<&Ctx::Validator>; } diff --git a/Code/common/src/vote.rs b/Code/common/src/vote.rs index 6dd388d13..a67190a26 100644 --- a/Code/common/src/vote.rs +++ b/Code/common/src/vote.rs @@ -1,6 +1,6 @@ use core::fmt::Debug; -use crate::{Consensus, Round, Value}; +use crate::{Context, Round, Value}; /// A type of vote. #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -16,20 +16,23 @@ pub enum VoteType { /// /// Votes are signed messages from validators for a particular value which /// include information about the validator signing it. -pub trait Vote +pub trait Vote where - Self: Clone + Debug + PartialEq + Eq, + Self: Clone + Debug + Eq, + Ctx: Context, { /// The round for which the vote is for. fn round(&self) -> Round; - /// The value being voted for. - fn value(&self) -> Option<&::Id>; + /// 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; - // FIXME: round message votes should not include address - fn address(&self) -> &C::Address; - fn set_address(&mut self, address: C::Address); + /// Address of the validator who issued this vote + fn validator_address(&self) -> &Ctx::Address; } diff --git a/Code/consensus/Cargo.toml b/Code/consensus/Cargo.toml index 90fda0325..b4c137a2b 100644 --- a/Code/consensus/Cargo.toml +++ b/Code/consensus/Cargo.toml @@ -6,5 +6,7 @@ publish = false [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" } +malachite-round = { version = "0.1.0", path = "../round" } +malachite-vote = { version = "0.1.0", path = "../vote" } + +secrecy = "0.8.0" diff --git a/Code/consensus/src/executor.rs b/Code/consensus/src/executor.rs index 7add9bee1..ea9d23c4c 100644 --- a/Code/consensus/src/executor.rs +++ b/Code/consensus/src/executor.rs @@ -1,44 +1,67 @@ use std::collections::BTreeMap; +use malachite_round::state_machine::RoundData; +use secrecy::{ExposeSecret, Secret}; + +use malachite_common::signature::Keypair; use malachite_common::{ - Consensus, Proposal, Round, Timeout, TimeoutStep, Validator, ValidatorSet, Value, Vote, - VoteType, + 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::count::Threshold; +use malachite_vote::keeper::Message as VoteMessage; use malachite_vote::keeper::VoteKeeper; +/// Messages that can be received and broadcast by the consensus executor. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Event +where + Ctx: Context, +{ + NewRound(Round), + Proposal(Ctx::Proposal), + Vote(SignedVote), + TimeoutElapsed(Timeout), +} + #[derive(Clone, Debug)] -pub struct Executor +pub struct Executor where - C: Consensus, + Ctx: Context, { - height: C::Height, - key: C::PublicKey, - validator_set: C::ValidatorSet, + height: Ctx::Height, + private_key: Secret>, + address: Ctx::Address, + validator_set: Ctx::ValidatorSet, round: Round, - votes: VoteKeeper, - round_states: BTreeMap>, + votes: VoteKeeper, + round_states: BTreeMap>, } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum Message +pub enum Message where - C: Consensus, + Ctx: Context, { - NewRound(Round), - Proposal(C::Proposal), - Vote(C::Vote), - Timeout(Timeout), + Propose(Ctx::Proposal), + Vote(SignedVote), + Decide(Round, Ctx::Value), + ScheduleTimeout(Timeout), } -impl Executor +impl Executor where - C: Consensus, + Ctx: Context, { - pub fn new(height: C::Height, validator_set: C::ValidatorSet, key: C::PublicKey) -> Self { + pub fn new( + height: Ctx::Height, + validator_set: Ctx::ValidatorSet, + private_key: PrivateKey, + address: Ctx::Address, + ) -> Self { let votes = VoteKeeper::new( height.clone(), Round::INITIAL, @@ -47,7 +70,8 @@ where Self { height, - key, + private_key: Secret::new(private_key), + address, validator_set, round: Round::INITIAL, votes, @@ -55,12 +79,12 @@ where } } - pub fn get_value(&self) -> C::Value { + pub fn get_value(&self) -> Ctx::Value { // TODO - add external interface to get the value - C::DUMMY_VALUE + Ctx::DUMMY_VALUE } - pub fn execute(&mut self, msg: Message) -> Option> { + pub fn execute(&mut self, msg: Event) -> Option> { let round_msg = match self.apply(msg) { Some(msg) => msg, None => return None, @@ -70,57 +94,47 @@ where RoundMessage::NewRound(round) => { // TODO: check if we are the proposer + // XXX: Check if there is an existing state? self.round_states - .insert(round, RoundState::new(self.height.clone()).new_round(round)); + .insert(round, RoundState::default().new_round(round)); None } - RoundMessage::Proposal(p) => { + RoundMessage::Proposal(proposal) => { // sign the proposal - Some(Message::Proposal(p)) + Some(Message::Propose(proposal)) } - RoundMessage::Vote(mut v) => { - // sign the vote - - // FIXME: round message votes should not include address - let address = self - .validator_set - .get_by_public_key(&self.key)? - .address() - .clone(); - - v.set_address(address); + RoundMessage::Vote(vote) => { + let signature = Ctx::sign_vote(&vote, self.private_key.expose_secret()); + let signed_vote = SignedVote::new(vote, signature); - Some(Message::Vote(v)) + Some(Message::Vote(signed_vote)) } - RoundMessage::Timeout(_) => { - // schedule the timeout - None - } + RoundMessage::ScheduleTimeout(timeout) => Some(Message::ScheduleTimeout(timeout)), - RoundMessage::Decision(_) => { - // update the state - None + RoundMessage::Decision(value) => { + // TODO: update the state + Some(Message::Decide(value.round, value.value)) } } } - fn apply(&mut self, msg: Message) -> Option> { + fn apply(&mut self, msg: Event) -> Option> { match msg { - Message::NewRound(round) => self.apply_new_round(round), - Message::Proposal(proposal) => self.apply_proposal(proposal), - Message::Vote(vote) => self.apply_vote(vote), - Message::Timeout(timeout) => self.apply_timeout(timeout), + 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> { + fn apply_new_round(&mut self, round: Round) -> Option> { let proposer = self.validator_set.get_proposer(); - let event = if proposer.public_key() == &self.key { + let event = if proposer.public_key() == &self.private_key.expose_secret().verifying_key() { let value = self.get_value(); RoundEvent::NewRoundProposer(value) } else { @@ -130,7 +144,7 @@ where self.apply_event(round, event) } - fn apply_proposal(&mut self, proposal: C::Proposal) -> Option> { + fn apply_proposal(&mut self, proposal: Ctx::Proposal) -> Option> { // TODO: Check for invalid proposal let event = RoundEvent::Proposal(proposal.clone()); @@ -146,7 +160,7 @@ where } // Check that the proposal is for the current height and round - if round_state.height != proposal.height() || proposal.round() != self.round { + if self.height != proposal.height() || self.round != proposal.round() { return None; } @@ -165,7 +179,7 @@ where self.apply_event(proposal.round(), event) } Round::Some(_) - if self.votes.check_threshold( + if self.votes.is_threshold_met( &proposal.pol_round(), VoteType::Prevote, Threshold::Value(proposal.value().id()), @@ -177,23 +191,35 @@ where } } - fn apply_vote(&mut self, vote: C::Vote) -> Option> { - let Some(validator) = self.validator_set.get_by_address(vote.address()) else { - // TODO: Is this the correct behavior? How to log such "errors"? + 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 = vote.round(); + let round = signed_vote.vote.round(); - let event = match self.votes.apply_vote(vote, validator.voting_power()) { - Some(event) => event, - None => return None, + 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), }; - self.apply_event(round, event) + self.apply_event(round, round_event) } - fn apply_timeout(&mut self, timeout: Timeout) -> Option> { + fn apply_timeout(&mut self, timeout: Timeout) -> Option> { let event = match timeout.step { TimeoutStep::Propose => RoundEvent::TimeoutPropose, TimeoutStep::Prevote => RoundEvent::TimeoutPrevote, @@ -204,24 +230,23 @@ where } /// Apply the event, update the state. - fn apply_event(&mut self, round: Round, event: RoundEvent) -> Option> { + fn apply_event(&mut self, round: Round, event: RoundEvent) -> Option> { // Get the round state, or create a new one - let round_state = self - .round_states - .remove(&round) - .unwrap_or_else(|| RoundState::new(self.height.clone())); + let round_state = self.round_states.remove(&round).unwrap_or_default(); + + let data = RoundData::new(round, &self.height, &self.address); // Apply the event to the round state machine - let transition = round_state.apply_event(round, event); + let transition = round_state.apply_event(&data, event); // Update state - self.round_states.insert(round, transition.state); + self.round_states.insert(round, transition.next_state); // Return message, if any transition.message } - pub fn round_state(&self, round: Round) -> Option<&RoundState> { + pub fn round_state(&self, round: Round) -> Option<&RoundState> { self.round_states.get(&round) } } diff --git a/Code/round/src/events.rs b/Code/round/src/events.rs index 1f932e6a1..5e1349845 100644 --- a/Code/round/src/events.rs +++ b/Code/round/src/events.rs @@ -1,22 +1,24 @@ - -use malachite_common::{Consensus, ValueId}; +use malachite_common::{Context, ValueId}; #[derive(Clone, Debug, PartialEq, Eq)] -pub enum Event { - NewRound, // Start a new round, not as proposer.L20 - NewRoundProposer(C::Value), // Start a new round and propose the Value.L14 - Proposal(C::Proposal), // Receive a proposal. L22 + L23 (valid) - ProposalAndPolkaPrevious(C::Value), // 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(C::Value), // Receive +2/3 prevotes for Value in current round. L36 - PrecommitAny, // Receive +2/3 precommits for anything. L47 - ProposalAndPrecommitValue(C::Value), // Receive +2/3 precommits for Value. L49 - PrecommitValue(ValueId), // Receive +2/3 precommits for ValueId. L51 - RoundSkip, // Receive +1/3 messages from a higher round. OneCorrectProcessInHigherRound, L55 - TimeoutPropose, // Timeout waiting for proposal. L57 - TimeoutPrevote, // Timeout waiting for prevotes. L61 - TimeoutPrecommit, // Timeout waiting for precommits. L65 +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::Value), // Receive +2/3 prevotes for Value in current round. L36 + PrecommitAny, // Receive +2/3 precommits for anything. L47 + ProposalAndPrecommitValue(Ctx::Value), // Receive +2/3 precommits for Value. L49 + PrecommitValue(ValueId), // Receive +2/3 precommits for ValueId. L51 + RoundSkip, // Receive +1/3 messages from a higher round. OneCorrectProcessInHigherRound, L55 + TimeoutPropose, // Timeout waiting for proposal. L57 + TimeoutPrevote, // Timeout waiting for prevotes. L61 + TimeoutPrecommit, // Timeout waiting for precommits. L65 } diff --git a/Code/round/src/lib.rs b/Code/round/src/lib.rs index 79dddd8ae..34ae70a59 100644 --- a/Code/round/src/lib.rs +++ b/Code/round/src/lib.rs @@ -14,3 +14,4 @@ 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 index 2fb422cf1..8ec5a4556 100644 --- a/Code/round/src/message.rs +++ b/Code/round/src/message.rs @@ -1,52 +1,60 @@ -use malachite_common::{Consensus, Round, Timeout, TimeoutStep, ValueId}; +use malachite_common::{Context, Round, Timeout, TimeoutStep, ValueId}; use crate::state::RoundValue; #[derive(Debug, PartialEq, Eq)] -pub enum Message { - NewRound(Round), // Move to the new round. - Proposal(C::Proposal), // Broadcast the proposal. - Vote(C::Vote), // Broadcast the vote. - Timeout(Timeout), // Schedule the timeout. - Decision(RoundValue), // Decide the value. +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 +impl Clone for Message where - C: Consensus, + 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::Timeout(timeout) => Message::Timeout(*timeout), + Message::ScheduleTimeout(timeout) => Message::ScheduleTimeout(*timeout), Message::Decision(round_value) => Message::Decision(round_value.clone()), } } } -impl Message +impl Message where - C: Consensus, + Ctx: Context, { - pub fn proposal(height: C::Height, round: Round, value: C::Value, pol_round: Round) -> Self { - Message::Proposal(C::new_proposal(height, round, value, pol_round)) + pub fn 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: C::Address) -> Self { - Message::Vote(C::new_prevote(round, value_id, address)) + 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: C::Address) -> Self { - Message::Vote(C::new_precommit(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 timeout(round: Round, step: TimeoutStep) -> Self { - Message::Timeout(Timeout { round, step }) + pub fn schedule_timeout(round: Round, step: TimeoutStep) -> Self { + Message::ScheduleTimeout(Timeout { round, step }) } - pub fn decision(round: Round, value: C::Value) -> Self { + 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 index f543a8854..4d89770a5 100644 --- a/Code/round/src/state.rs +++ b/Code/round/src/state.rs @@ -1,7 +1,8 @@ use crate::events::Event; -use crate::state_machine::Transition; +use crate::state_machine::RoundData; +use crate::transition::Transition; -use malachite_common::{Consensus, Round}; +use malachite_common::{Context, Round}; /// A value and its associated round #[derive(Clone, Debug, PartialEq, Eq)] @@ -28,22 +29,23 @@ pub enum Step { /// The state of the consensus state machine #[derive(Debug, PartialEq, Eq)] -pub struct State { - pub height: C::Height, +pub struct State +where + Ctx: Context, +{ pub round: Round, pub step: Step, - pub proposal: Option, - pub locked: Option>, - pub valid: Option>, + pub proposal: Option, + pub locked: Option>, + pub valid: Option>, } -impl Clone for State +impl Clone for State where - C: Consensus, + Ctx: Context, { fn clone(&self) -> Self { Self { - height: self.height.clone(), round: self.round, step: self.step, proposal: self.proposal.clone(), @@ -53,13 +55,12 @@ where } } -impl State +impl State where - C: Consensus, + Ctx: Context, { - pub fn new(height: C::Height) -> Self { + pub fn new() -> Self { Self { - height, round: Round::INITIAL, step: Step::NewRound, proposal: None, @@ -87,6 +88,10 @@ where Self { step, ..self } } + pub fn with_step(self, step: Step) -> Self { + Self { step, ..self } + } + pub fn commit_step(self) -> Self { Self { step: Step::Commit, @@ -94,21 +99,30 @@ where } } - pub fn set_locked(self, value: C::Value) -> 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: C::Value) -> Self { + pub fn set_valid(self, value: Ctx::Value) -> Self { Self { valid: Some(RoundValue::new(value, self.round)), ..self } } - pub fn apply_event(self, round: Round, event: Event) -> Transition { - crate::state_machine::apply_event(self, round, event) + 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 index 9c07ef3fd..db0a08939 100644 --- a/Code/round/src/state_machine.rs +++ b/Code/round/src/state_machine.rs @@ -1,50 +1,42 @@ -use malachite_common::{Consensus, Proposal, Round, TimeoutStep, Value, ValueId}; +use malachite_common::{Context, Proposal, Round, TimeoutStep, Value, ValueId}; use crate::events::Event; use crate::message::Message; use crate::state::{State, Step}; +use crate::transition::Transition; -// FIXME: Where to get the address/public key from? -// IDEA: Add a Context parameter to `apply_state` -// const ADDRESS: Address = Address::new(42); - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Transition { - pub state: State, - pub message: Option>, - pub valid: bool, +/// 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 Transition +impl<'a, Ctx> RoundData<'a, Ctx> where - C: Consensus, + Ctx: Context, { - pub fn to(state: State) -> Self { - Self { - state, - message: None, - valid: true, - } - } - - pub fn invalid(state: State) -> Self { + pub fn new(round: Round, height: &'a Ctx::Height, address: &'a Ctx::Address) -> Self { Self { - state, - message: None, - valid: false, + round, + height, + address, } } - - pub fn with_message(mut self, message: Message) -> Self { - self.message = Some(message); - self - } } /// Check that a proposal has a valid Proof-Of-Lock round -fn is_valid_pol_round(state: &State, pol_round: Round) -> bool +fn is_valid_pol_round(state: &State, pol_round: Round) -> bool where - C: Consensus, + Ctx: Context, { pol_round.is_defined() && pol_round < state.round } @@ -57,15 +49,21 @@ where /// Valid transitions result in at least a change to the state and/or an output message. /// /// Commented numbers refer to line numbers in the spec paper. -pub fn apply_event(mut state: State, round: Round, event: Event) -> Transition +pub fn apply_event( + mut state: State, + data: &RoundData, + event: Event, +) -> Transition where - C: Consensus, + Ctx: Context, { - let this_round = state.round == round; + 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, value), // L11/L14 + (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. @@ -80,13 +78,14 @@ where .map_or(true, |locked| &locked.value == proposal.value()) { state.proposal = Some(proposal.clone()); - prevote(state, proposal.round(), proposal.value().id()) + prevote(state, data.address, proposal.round(), proposal.value().id()) } else { - prevote_nil(state) + prevote_nil(state, data.address) } } - (Step::Propose, Event::Proposal(proposal)) + // TODO - change to ProposalAndPolkaPrevious + (Step::Propose, Event::ProposalAndPolkaPrevious(proposal)) if this_round && is_valid_pol_round(&state, proposal.pol_round()) => { // L28 @@ -98,33 +97,35 @@ where if proposal.value().is_valid() && (locked.round <= proposal.pol_round() || &locked.value == proposal.value()) { - prevote(state, proposal.round(), proposal.value().id()) + prevote(state, data.address, proposal.round(), proposal.value().id()) } else { - prevote_nil(state) + prevote_nil(state, data.address) } } - (Step::Propose, Event::ProposalInvalid) if this_round => prevote_nil(state), // L22/L25, L28/L31 - (Step::Propose, Event::TimeoutPropose) if this_round => prevote_nil(state), // L57 + (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), // L44 - (Step::Prevote, Event::ProposalAndPolkaCurrent(value)) if this_round => precommit(state, value), // L36/L37 - NOTE: only once? - (Step::Prevote, Event::TimeoutPrevote) if this_round => precommit_nil(state), // L61 + (Step::Prevote, Event::PolkaNil) if this_round => precommit_nil(state, data.address), // L44 + (Step::Prevote, Event::ProposalAndPolkaCurrent(value)) if this_round => { + precommit(state, data.address, value) // 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(value)) if this_round => { - set_valid_value(state, value) - } // L36/L42 - NOTE: only once? + set_valid_value(state, value) // 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, round.increment()), // L65 - (_, Event::RoundSkip) if state.round < round => round_skip(state, round), // L55 - (_, Event::ProposalAndPrecommitValue(value)) => commit(state, round, value), // L49 + (_, Event::TimeoutPrecommit) if this_round => round_skip(state, data.round.increment()), // L65 + (_, Event::RoundSkip) if state.round < data.round => round_skip(state, data.round), // L55 + (_, Event::ProposalAndPrecommitValue(value)) => commit(state, data.round, value), // L49 // Invalid transition. _ => Transition::invalid(state), @@ -139,17 +140,17 @@ where /// otherwise propose the given value. /// /// Ref: L11/L14 -pub fn propose(state: State, value: C::Value) -> Transition +pub fn propose(state: State, height: &Ctx::Height, value: Ctx::Value) -> Transition where - C: Consensus, + 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(state.height.clone(), state.round, value, pol_round); - Transition::to(state.next_step()).with_message(proposal) + let proposal = Message::proposal(height.clone(), state.round, value, pol_round); + Transition::to(state.with_step(Step::Propose)).with_message(proposal) } //--------------------------------------------------------------------- @@ -160,9 +161,14 @@ where /// unless we are locked on something else at a higher round. /// /// Ref: L22/L28 -pub fn prevote(state: State, vr: Round, proposed: ValueId) -> Transition +pub fn prevote( + state: State, + address: &Ctx::Address, + vr: Round, + proposed: ValueId, +) -> Transition where - C: Consensus, + Ctx: Context, { let value = match &state.locked { Some(locked) if locked.round <= vr => Some(proposed), // unlock and prevote @@ -171,19 +177,19 @@ where None => Some(proposed), // not locked, prevote the value }; - let message = Message::prevote(state.round, value, C::DUMMY_ADDRESS); - Transition::to(state.next_step()).with_message(message) + 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) -> Transition +pub fn prevote_nil(state: State, address: &Ctx::Address) -> Transition where - C: Consensus, + Ctx: Context, { - let message = Message::prevote(state.round, None, C::DUMMY_ADDRESS); - Transition::to(state.next_step()).with_message(message) + let message = Message::prevote(state.round, None, address.clone()); + Transition::to(state.with_step(Step::Prevote)).with_message(message) } // --------------------------------------------------------------------- @@ -196,11 +202,19 @@ where /// /// NOTE: Only one of this and set_valid_value should be called once in a round /// How do we enforce this? -pub fn precommit(state: State, value: C::Value) -> Transition +pub fn precommit( + state: State, + address: &Ctx::Address, + value: Ctx::Value, +) -> Transition where - C: Consensus, + Ctx: Context, { - let message = Message::precommit(state.round, Some(value.id()), C::DUMMY_ADDRESS); + if state.step != Step::Prevote { + return Transition::to(state.clone()); + } + + let message = Message::precommit(state.round, Some(value.id()), address.clone()); let Some(value) = state .proposal @@ -208,23 +222,25 @@ where .map(|proposal| proposal.value().clone()) else { // TODO: Add logging - return Transition::invalid(state); + return Transition::invalid(state.clone()); }; - let next = state.set_locked(value.clone()).set_valid(value).next_step(); - + let next = state + .set_locked(value.clone()) + .set_valid(value) + .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) -> Transition +pub fn precommit_nil(state: State, address: &Ctx::Address) -> Transition where - C: Consensus, + Ctx: Context, { - let message = Message::precommit(state.round, None, C::DUMMY_ADDRESS); - Transition::to(state.next_step()).with_message(message) + let message = Message::precommit(state.round, None, address.clone()); + Transition::to(state.with_step(Step::Precommit)).with_message(message) } // --------------------------------------------------------------------- @@ -234,12 +250,12 @@ where /// We're not the proposer; schedule timeout propose. /// /// Ref: L11, L20 -pub fn schedule_timeout_propose(state: State) -> Transition +pub fn schedule_timeout_propose(state: State) -> Transition where - C: Consensus, + Ctx: Context, { - let timeout = Message::timeout(state.round, TimeoutStep::Propose); - Transition::to(state.next_step()).with_message(timeout) + 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. @@ -248,38 +264,42 @@ where /// /// NOTE: This should only be called once in a round, per the spec, /// but it's harmless to schedule more timeouts -pub fn schedule_timeout_prevote(state: State) -> Transition +pub fn schedule_timeout_prevote(state: State) -> Transition where - C: Consensus, + Ctx: Context, { - let message = Message::timeout(state.round, TimeoutStep::Prevote); - Transition::to(state.next_step()).with_message(message) + if state.step == Step::Prevote { + let message = Message::schedule_timeout(state.round, TimeoutStep::Prevote); + Transition::to(state).with_message(message) + } else { + Transition::to(state) + } } /// We received +2/3 precommits for any; schedule timeout precommit. /// /// Ref: L47 -pub fn schedule_timeout_precommit(state: State) -> Transition +pub fn schedule_timeout_precommit(state: State) -> Transition where - C: Consensus, + Ctx: Context, { - let message = Message::timeout(state.round, TimeoutStep::Precommit); - Transition::to(state.next_step()).with_message(message) + 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 precommited. +/// We received a polka for a value after we already precommitted. /// Set the valid value and current round. /// /// Ref: L36/L42 /// /// NOTE: only one of this and precommit should be called once in a round -pub fn set_valid_value(state: State, value: C::Value) -> Transition +pub fn set_valid_value(state: State, value: Ctx::Value) -> Transition where - C: Consensus, + Ctx: Context, { // Check that we're locked on this value let Some(locked) = state.locked.as_ref() else { @@ -303,19 +323,19 @@ where /// from a higher round. Move to the higher round. /// /// Ref: 65 -pub fn round_skip(state: State, round: Round) -> Transition +pub fn round_skip(state: State, round: Round) -> Transition where - C: Consensus, + Ctx: Context, { - Transition::to(state.new_round(round)).with_message(Message::NewRound(round)) + Transition::to(state).with_message(Message::NewRound(round)) } /// We received +2/3 precommits for a value - commit and decide that value! /// /// Ref: L49 -pub fn commit(state: State, round: Round, value: C::Value) -> Transition +pub fn commit(state: State, round: Round, value: Ctx::Value) -> Transition where - C: Consensus, + Ctx: Context, { // Check that we're locked on this value let Some(locked) = state.locked.as_ref() else { @@ -329,5 +349,5 @@ where } let message = Message::decision(round, locked.value.clone()); - Transition::to(state.commit_step()).with_message(message) + Transition::to(state.with_step(Step::Commit)).with_message(message) } diff --git a/Code/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/test/Cargo.toml b/Code/test/Cargo.toml index 43da1d089..f06726148 100644 --- a/Code/test/Cargo.toml +++ b/Code/test/Cargo.toml @@ -1,12 +1,17 @@ [package] -name = "malachite-test" +name = "malachite-test" version = "0.1.0" edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +publish = false [dependencies] -malachite-common = { version = "0.1.0", path = "../common" } +malachite-common = { version = "0.1.0", path = "../common" } malachite-consensus = { version = "0.1.0", path = "../consensus" } -malachite-round = { version = "0.1.0", path = "../round" } -malachite-vote = { version = "0.1.0", path = "../vote" } +malachite-round = { version = "0.1.0", path = "../round" } +malachite-vote = { version = "0.1.0", path = "../vote" } + +ed25519-consensus = "2.1.0" +signature = "2.1.0" +rand = { version = "0.8.5", features = ["std_rng"] } +sha2 = "0.10.8" +secrecy = "0.8.0" diff --git a/Code/test/src/consensus.rs b/Code/test/src/context.rs similarity index 59% rename from Code/test/src/consensus.rs rename to Code/test/src/context.rs index ebe110c5d..479c4d75e 100644 --- a/Code/test/src/consensus.rs +++ b/Code/test/src/context.rs @@ -1,29 +1,41 @@ -use malachite_common::Consensus; +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 TestConsensus; +pub struct TestContext; -impl Consensus for TestConsensus { +impl Context for TestContext { type Address = Address; type Height = Height; type Proposal = Proposal; - type PublicKey = PublicKey; type ValidatorSet = ValidatorSet; type Validator = Validator; type Value = Value; type Vote = Vote; - - const DUMMY_ADDRESS: Address = Address::new(42); + 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) } diff --git a/Code/test/src/lib.rs b/Code/test/src/lib.rs index 4e9cd3aeb..c39e1682a 100644 --- a/Code/test/src/lib.rs +++ b/Code/test/src/lib.rs @@ -1,16 +1,18 @@ #![forbid(unsafe_code)] #![deny(trivial_casts, trivial_numeric_casts)] -mod consensus; +mod context; mod height; mod proposal; +mod signing; mod validator_set; mod value; mod vote; -pub use crate::consensus::*; +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 index a0401f0d3..407b4be02 100644 --- a/Code/test/src/proposal.rs +++ b/Code/test/src/proposal.rs @@ -1,6 +1,6 @@ use malachite_common::Round; -use crate::{Height, TestConsensus, Value}; +use crate::{Height, TestContext, Value}; /// A proposal for a value in a round #[derive(Clone, Debug, PartialEq, Eq)] @@ -22,7 +22,7 @@ impl Proposal { } } -impl malachite_common::Proposal for Proposal { +impl malachite_common::Proposal for Proposal { fn height(&self) -> Height { self.height } 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 index e50b9a781..23b88a107 100644 --- a/Code/test/src/validator_set.rs +++ b/Code/test/src/validator_set.rs @@ -2,30 +2,24 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use malachite_common::VotingPower; -use crate::TestConsensus; - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct PublicKey(Vec); - -impl PublicKey { - pub const fn new(value: Vec) -> Self { - Self(value) - } - - pub fn hash(&self) -> u64 { - self.0.iter().fold(0, |acc, x| acc ^ *x as u64) - } -} - -impl malachite_common::PublicKey for PublicKey {} +use crate::{signing::PublicKey, TestContext}; #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct Address(u64); +pub struct Address([u8; Self::LENGTH]); impl Address { - pub const fn new(value: u64) -> Self { + 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 {} @@ -41,18 +35,14 @@ pub struct Validator { impl Validator { pub fn new(public_key: PublicKey, voting_power: VotingPower) -> Self { Self { - address: Address(public_key.hash()), + address: Address::from_public_key(&public_key), public_key, voting_power, } } - - pub fn hash(&self) -> u64 { - self.public_key.hash() // TODO - } } -impl malachite_common::Validator for Validator { +impl malachite_common::Validator for Validator { fn address(&self) -> &Address { &self.address } @@ -108,9 +98,7 @@ impl ValidatorSet { v.voting_power = val.voting_power; } - dbg!(self.total_voting_power()); Self::sort_validators(&mut self.validators); - dbg!(self.total_voting_power()); } /// Remove a validator from the set @@ -131,11 +119,16 @@ impl ValidatorSet { /// In place sort and deduplication of a list of validators fn sort_validators(vals: &mut Vec) { - use core::cmp::Reverse; - // Sort the validators according to the current Tendermint requirements + // + // use core::cmp::Reverse; + // // (v. 0.34 -> first by validator power, descending, then by address, ascending) - vals.sort_unstable_by_key(|v| (Reverse(v.voting_power), v.address)); + // vals.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(); } @@ -149,7 +142,7 @@ impl ValidatorSet { } } -impl malachite_common::ValidatorSet for ValidatorSet { +impl malachite_common::ValidatorSet for ValidatorSet { fn total_voting_power(&self) -> VotingPower { self.total_voting_power() } @@ -169,22 +162,36 @@ impl malachite_common::ValidatorSet for ValidatorSet { #[cfg(test)] mod tests { + use rand::rngs::StdRng; + use rand::SeedableRng; + use super::*; + use crate::PrivateKey; + #[test] fn add_update_remove() { - let v1 = Validator::new(PublicKey(vec![1]), 1); - let v2 = Validator::new(PublicKey(vec![2]), 2); - let v3 = Validator::new(PublicKey(vec![3]), 3); + let mut 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(PublicKey(vec![4]), 4); + let v4 = Validator::new(sk4.public_key(), 4); vs.add(v4); assert_eq!(vs.total_voting_power(), 10); - let mut v5 = Validator::new(PublicKey(vec![5]), 5); + let mut v5 = Validator::new(sk5.public_key(), 5); vs.update(v5.clone()); // no effect assert_eq!(vs.total_voting_power(), 10); @@ -198,7 +205,7 @@ mod tests { vs.remove(&v5.address); assert_eq!(vs.total_voting_power(), 10); - let v6 = Validator::new(PublicKey(vec![6]), 6); + 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 index 2f45f6abe..3954d2868 100644 --- a/Code/test/src/value.rs +++ b/Code/test/src/value.rs @@ -5,6 +5,10 @@ impl ValueId { pub const fn new(id: u64) -> Self { Self(id) } + + pub const fn as_u64(&self) -> u64 { + self.0 + } } /// The value to decide on diff --git a/Code/test/src/vote.rs b/Code/test/src/vote.rs index 761101aeb..d8a1a9aeb 100644 --- a/Code/test/src/vote.rs +++ b/Code/test/src/vote.rs @@ -1,6 +1,8 @@ -use malachite_common::{Round, VoteType}; +use signature::Signer; -use crate::{Address, TestConsensus, ValueId}; +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)] @@ -8,16 +10,16 @@ pub struct Vote { pub typ: VoteType, pub round: Round, pub value: Option, - pub address: Address, + pub validator_address: Address, } impl Vote { - pub fn new_prevote(round: Round, value: Option, address: Address) -> Self { + pub fn new_prevote(round: Round, value: Option, validator_address: Address) -> Self { Self { typ: VoteType::Prevote, round, value, - address, + validator_address, } } @@ -26,12 +28,39 @@ impl Vote { typ: VoteType::Precommit, round, value, - address, + 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 { +impl malachite_common::Vote for Vote { fn round(&self) -> Round { self.round } @@ -40,15 +69,15 @@ impl malachite_common::Vote for Vote { self.value.as_ref() } - fn vote_type(&self) -> VoteType { - self.typ + fn take_value(self) -> Option { + self.value } - fn address(&self) -> &Address { - &self.address + fn vote_type(&self) -> VoteType { + self.typ } - fn set_address(&mut self, address: Address) { - self.address = address; + fn validator_address(&self) -> &Address { + &self.validator_address } } diff --git a/Code/test/tests/consensus_executor.rs b/Code/test/tests/consensus_executor.rs index fc26fe778..e89afb9b2 100644 --- a/Code/test/tests/consensus_executor.rs +++ b/Code/test/tests/consensus_executor.rs @@ -1,40 +1,62 @@ -use malachite_common::{Consensus, Round, Timeout}; -use malachite_consensus::executor::{Executor, Message}; +use malachite_common::{Context, Round, Timeout}; +use malachite_consensus::executor::{Event, Executor, Message}; use malachite_round::state::{RoundValue, State, Step}; -use malachite_test::{Height, Proposal, PublicKey, TestConsensus, Validator, ValidatorSet, Vote}; +use malachite_test::{ + Address, Height, PrivateKey, Proposal, TestContext, Validator, ValidatorSet, Vote, +}; +use rand::rngs::StdRng; +use rand::SeedableRng; struct TestStep { desc: &'static str, - input_message: Option>, - expected_output_message: Option>, - new_state: State, + input_event: Option>, + expected_output: Option>, + 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, + } } #[test] fn executor_steps_proposer() { - let value = TestConsensus::DUMMY_VALUE; // TODO: get value from external source + let value = TestContext::DUMMY_VALUE; // TODO: get value from external source let value_id = value.id(); - let v1 = Validator::new(PublicKey::new(vec![1]), 1); - let v2 = Validator::new(PublicKey::new(vec![2]), 1); - let v3 = Validator::new(PublicKey::new(vec![3]), 1); - let my_address = v1.address; - let key = v1.clone().public_key; // we are proposer + + let 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 executor = Executor::new(Height::new(1), vs, key.clone()); + let mut executor = Executor::new(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![ - // Start round 0, we are proposer, propose value TestStep { desc: "Start round 0, we are proposer, propose value", - input_message: Some(Message::NewRound(Round::new(0))), - expected_output_message: Some(Message::Proposal(proposal.clone())), + input_event: Some(Event::NewRound(Round::new(0))), + expected_output: Some(Message::Propose(proposal.clone())), new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Propose, proposal: None, @@ -42,17 +64,13 @@ fn executor_steps_proposer() { valid: None, }, }, - // Receive our own proposal, prevote for it (v1) TestStep { desc: "Receive our own proposal, prevote for it (v1)", - input_message: None, - expected_output_message: Some(Message::Vote(Vote::new_prevote( - Round::new(0), - Some(value_id), - my_address, - ))), + input_event: None, + expected_output: Some(Message::Vote( + Vote::new_prevote(Round::new(0), Some(value_id), my_addr).signed(&my_sk), + )), new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Prevote, proposal: Some(proposal.clone()), @@ -60,13 +78,11 @@ fn executor_steps_proposer() { valid: None, }, }, - // Receive our own prevote v1 TestStep { desc: "Receive our own prevote v1", - input_message: None, - expected_output_message: None, + input_event: None, + expected_output: None, new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Prevote, proposal: Some(proposal.clone()), @@ -74,17 +90,13 @@ fn executor_steps_proposer() { valid: None, }, }, - // v2 prevotes for our proposal TestStep { desc: "v2 prevotes for our proposal", - input_message: Some(Message::Vote(Vote::new_prevote( - Round::new(0), - Some(value_id), - v2.address, - ))), - expected_output_message: None, + input_event: Some(Event::Vote( + Vote::new_prevote(Round::new(0), Some(value_id), addr2).signed(&sk2), + )), + expected_output: None, new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Prevote, proposal: Some(proposal.clone()), @@ -92,21 +104,15 @@ fn executor_steps_proposer() { valid: None, }, }, - // v3 prevotes for our proposal, we get +2/3 prevotes, precommit for it (v1) TestStep { desc: "v3 prevotes for our proposal, we get +2/3 prevotes, precommit for it (v1)", - input_message: Some(Message::Vote(Vote::new_prevote( - Round::new(0), - Some(value_id), - v3.address, - ))), - expected_output_message: Some(Message::Vote(Vote::new_precommit( - Round::new(0), - Some(value_id), - my_address, - ))), + 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), + )), new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Precommit, proposal: Some(proposal.clone()), @@ -120,13 +126,11 @@ fn executor_steps_proposer() { }), }, }, - // v1 receives its own precommit TestStep { desc: "v1 receives its own precommit", - input_message: None, - expected_output_message: None, + input_event: None, + expected_output: None, new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Precommit, proposal: Some(proposal.clone()), @@ -140,17 +144,13 @@ fn executor_steps_proposer() { }), }, }, - // v2 precommits for our proposal TestStep { desc: "v2 precommits for our proposal", - input_message: Some(Message::Vote(Vote::new_precommit( - Round::new(0), - Some(value_id), - v2.address, - ))), - expected_output_message: None, + input_event: Some(Event::Vote( + Vote::new_precommit(Round::new(0), Some(value_id), addr2).signed(&sk2), + )), + expected_output: None, new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Precommit, proposal: Some(proposal.clone()), @@ -164,17 +164,13 @@ fn executor_steps_proposer() { }), }, }, - // v3 precommits for our proposal, we get +2/3 precommits, decide it (v1) TestStep { desc: "v3 precommits for our proposal, we get +2/3 precommits, decide it (v1)", - input_message: Some(Message::Vote(Vote::new_precommit( - Round::new(0), - Some(value_id), - v2.address, - ))), - expected_output_message: None, + input_event: Some(Event::Vote( + Vote::new_precommit(Round::new(0), Some(value_id), addr2).signed(&sk2), + )), + expected_output: Some(Message::Decide(Round::new(0), value.clone())), new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Commit, proposal: Some(proposal.clone()), @@ -193,46 +189,55 @@ fn executor_steps_proposer() { let mut previous_message = None; for step in steps { + println!("Step: {}", step.desc); + let execute_message = step - .input_message + .input_event .unwrap_or_else(|| previous_message.unwrap()); - let message = executor.execute(execute_message); - assert_eq!(message, step.expected_output_message); + let output = executor.execute(execute_message); + assert_eq!(output, step.expected_output); let new_state = executor.round_state(Round::new(0)).unwrap(); assert_eq!(new_state, &step.new_state); - previous_message = message; + previous_message = output.and_then(to_input_msg); } } #[test] fn executor_steps_not_proposer() { - let value = TestConsensus::DUMMY_VALUE; // TODO: get value from external source + let value = TestContext::DUMMY_VALUE; // TODO: get value from external source let value_id = value.id(); - let v1 = Validator::new(PublicKey::new(vec![1]), 1); - let v2 = Validator::new(PublicKey::new(vec![2]), 1); - let v3 = Validator::new(PublicKey::new(vec![3]), 1); + let 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_address = v2.address; - let my_key = v2.public_key.clone(); + let (my_sk, my_addr) = (sk2, addr2); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut executor = Executor::new(Height::new(1), vs, my_key); + let mut executor = Executor::new(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![ - // Start round 0, we are not the proposer TestStep { desc: "Start round 0, we are not the proposer", - input_message: Some(Message::NewRound(Round::new(0))), - expected_output_message: None, + input_event: Some(Event::NewRound(Round::new(0))), + expected_output: Some(Message::ScheduleTimeout(Timeout::propose(Round::new(0)))), new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Propose, proposal: None, @@ -240,17 +245,13 @@ fn executor_steps_not_proposer() { valid: None, }, }, - // Receive a proposal, prevote for it (v1) TestStep { - desc: "Receive a proposal, prevote for it (v1)", - input_message: Some(Message::Proposal(proposal.clone())), - expected_output_message: Some(Message::Vote(Vote::new_prevote( - Round::new(0), - Some(value_id), - my_address, - ))), + 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), + )), new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Prevote, proposal: Some(proposal.clone()), @@ -258,13 +259,11 @@ fn executor_steps_not_proposer() { valid: None, }, }, - // Receive our own prevote v1 TestStep { - desc: "Receive our own prevote v1", - input_message: None, - expected_output_message: None, + desc: "Receive our own prevote (v2)", + input_event: None, + expected_output: None, new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Prevote, proposal: Some(proposal.clone()), @@ -272,17 +271,13 @@ fn executor_steps_not_proposer() { valid: None, }, }, - // v2 prevotes for its own proposal TestStep { - desc: "v2 prevotes for its own proposal", - input_message: Some(Message::Vote(Vote::new_prevote( - Round::new(0), - Some(value_id), - v2.address, - ))), - expected_output_message: None, + 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, new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Prevote, proposal: Some(proposal.clone()), @@ -290,21 +285,15 @@ fn executor_steps_not_proposer() { valid: None, }, }, - // v3 prevotes for v2's proposal, it gets +2/3 prevotes, precommit for it (v1) TestStep { - desc: "v3 prevotes for v2's proposal, it gets +2/3 prevotes, precommit for it (v1)", - input_message: Some(Message::Vote(Vote::new_prevote( - Round::new(0), - Some(value_id), - v3.address, - ))), - expected_output_message: Some(Message::Vote(Vote::new_precommit( - Round::new(0), - Some(value_id), - my_address, - ))), + 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), + )), new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Precommit, proposal: Some(proposal.clone()), @@ -318,13 +307,11 @@ fn executor_steps_not_proposer() { }), }, }, - // v1 receives its own precommit TestStep { - desc: "v1 receives its own precommit", - input_message: None, - expected_output_message: None, + desc: "we receive our own precommit", + input_event: None, + expected_output: None, new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Precommit, proposal: Some(proposal.clone()), @@ -338,17 +325,13 @@ fn executor_steps_not_proposer() { }), }, }, - // v2 precommits its proposal TestStep { - desc: "v2 precommits its proposal", - input_message: Some(Message::Vote(Vote::new_precommit( - Round::new(0), - Some(value_id), - v2.address, - ))), - expected_output_message: None, + 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, new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Precommit, proposal: Some(proposal.clone()), @@ -362,17 +345,13 @@ fn executor_steps_not_proposer() { }), }, }, - // v3 precommits for v2's proposal, it gets +2/3 precommits, decide it (v1) TestStep { - desc: "v3 precommits for v2's proposal, it gets +2/3 precommits, decide it (v1)", - input_message: Some(Message::Vote(Vote::new_precommit( - Round::new(0), - Some(value_id), - v2.address, - ))), - expected_output_message: None, + 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())), new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Commit, proposal: Some(proposal.clone()), @@ -391,44 +370,54 @@ fn executor_steps_not_proposer() { let mut previous_message = None; for step in steps { + println!("Step: {}", step.desc); + let execute_message = step - .input_message + .input_event .unwrap_or_else(|| previous_message.unwrap()); - let message = executor.execute(execute_message); - assert_eq!(message, step.expected_output_message); + let output = executor.execute(execute_message); + assert_eq!(output, step.expected_output); let new_state = executor.round_state(Round::new(0)).unwrap(); assert_eq!(new_state, &step.new_state); - previous_message = message; + previous_message = output.and_then(to_input_msg); } } #[test] fn executor_steps_not_proposer_timeout() { - let value = TestConsensus::DUMMY_VALUE; // TODO: get value from external source + let value = TestContext::DUMMY_VALUE; // TODO: get value from external source let value_id = value.id(); - let v1 = Validator::new(PublicKey::new(vec![1]), 1); - let v2 = Validator::new(PublicKey::new(vec![2]), 1); - let v3 = Validator::new(PublicKey::new(vec![3]), 2); + 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(), 1); + let v3 = Validator::new(sk3.public_key(), 3); // Proposer is v1, so we are not the proposer - let my_address = v2.address; - let my_key = v2.public_key.clone(); + let (my_sk, my_addr) = (sk2, addr2); let vs = ValidatorSet::new(vec![v1.clone(), v2.clone(), v3.clone()]); - let mut executor = Executor::new(Height::new(1), vs, my_key); + let mut executor = Executor::new(Height::new(1), vs, my_sk.clone(), my_addr); let steps = vec![ // Start round 0, we are not the proposer TestStep { desc: "Start round 0, we are not the proposer", - input_message: Some(Message::NewRound(Round::new(0))), - expected_output_message: None, + input_event: Some(Event::NewRound(Round::new(0))), + expected_output: Some(Message::ScheduleTimeout(Timeout::propose(Round::new(0)))), new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Propose, proposal: None, @@ -439,14 +428,11 @@ fn executor_steps_not_proposer_timeout() { // Receive a propose timeout, prevote for nil (v1) TestStep { desc: "Receive a propose timeout, prevote for nil (v1)", - input_message: Some(Message::Timeout(Timeout::propose(Round::new(0)))), - expected_output_message: Some(Message::Vote(Vote::new_prevote( - Round::new(0), - None, - my_address, - ))), + 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), + )), new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Prevote, proposal: None, @@ -457,10 +443,9 @@ fn executor_steps_not_proposer_timeout() { // Receive our own prevote v1 TestStep { desc: "Receive our own prevote v1", - input_message: None, - expected_output_message: None, + input_event: None, + expected_output: None, new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Prevote, proposal: None, @@ -471,14 +456,11 @@ fn executor_steps_not_proposer_timeout() { // v2 prevotes for its own proposal TestStep { desc: "v2 prevotes for its own proposal", - input_message: Some(Message::Vote(Vote::new_prevote( - Round::new(0), - Some(value_id), - v2.address, - ))), - expected_output_message: None, + input_event: Some(Event::Vote( + Vote::new_prevote(Round::new(0), Some(value_id), addr1).signed(&sk1), + )), + expected_output: None, new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Prevote, proposal: None, @@ -489,18 +471,13 @@ fn executor_steps_not_proposer_timeout() { // v3 prevotes for nil, it gets +2/3 prevotes, precommit for it (v1) TestStep { desc: "v3 prevotes for nil, it gets +2/3 prevotes, precommit for it (v1)", - input_message: Some(Message::Vote(Vote::new_prevote( - Round::new(0), - None, - v3.address, - ))), - expected_output_message: Some(Message::Vote(Vote::new_precommit( - Round::new(0), - None, - my_address, - ))), + input_event: Some(Event::Vote( + Vote::new_prevote(Round::new(0), None, addr3).signed(&sk3), + )), + expected_output: Some(Message::Vote( + Vote::new_precommit(Round::new(0), None, my_addr).signed(&my_sk), + )), new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Precommit, proposal: None, @@ -511,10 +488,9 @@ fn executor_steps_not_proposer_timeout() { // v1 receives its own precommit TestStep { desc: "v1 receives its own precommit", - input_message: None, - expected_output_message: None, + input_event: None, + expected_output: None, new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Precommit, proposal: None, @@ -525,14 +501,11 @@ fn executor_steps_not_proposer_timeout() { // v2 precommits its proposal TestStep { desc: "v2 precommits its proposal", - input_message: Some(Message::Vote(Vote::new_precommit( - Round::new(0), - Some(value_id), - v2.address, - ))), - expected_output_message: None, + input_event: Some(Event::Vote( + Vote::new_precommit(Round::new(0), Some(value_id), addr1).signed(&sk1), + )), + expected_output: None, new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Precommit, proposal: None, @@ -543,14 +516,11 @@ fn executor_steps_not_proposer_timeout() { // v3 precommits for nil TestStep { desc: "v3 precommits for nil", - input_message: Some(Message::Vote(Vote::new_precommit( - Round::new(0), - None, - v3.address, - ))), - expected_output_message: None, + input_event: Some(Event::Vote( + Vote::new_precommit(Round::new(0), None, addr3).signed(&sk3), + )), + expected_output: None, new_state: State { - height: Height::new(1), round: Round::new(0), step: Step::Precommit, proposal: None, @@ -561,10 +531,9 @@ fn executor_steps_not_proposer_timeout() { // we receive a precommit timeout, start a new round TestStep { desc: "we receive a precommit timeout, start a new round", - input_message: Some(Message::Timeout(Timeout::precommit(Round::new(0)))), - expected_output_message: None, + input_event: Some(Event::TimeoutElapsed(Timeout::precommit(Round::new(0)))), + expected_output: None, new_state: State { - height: Height::new(1), round: Round::new(1), step: Step::NewRound, proposal: None, @@ -580,18 +549,15 @@ fn executor_steps_not_proposer_timeout() { println!("Step: {}", step.desc); let execute_message = step - .input_message + .input_event .unwrap_or_else(|| previous_message.unwrap()); - let message = executor.execute(execute_message); - assert_eq!( - message, step.expected_output_message, - "expected output message" - ); + let output = executor.execute(execute_message); + assert_eq!(output, step.expected_output, "expected output message"); let new_state = executor.round_state(Round::new(0)).unwrap(); assert_eq!(new_state, &step.new_state, "new state"); - previous_message = message; + previous_message = output.and_then(to_input_msg); } } diff --git a/Code/test/tests/round.rs b/Code/test/tests/round.rs index 238e35b89..f90c13ea5 100644 --- a/Code/test/tests/round.rs +++ b/Code/test/tests/round.rs @@ -1,20 +1,25 @@ -use malachite_test::{Height, Proposal, TestConsensus, Value}; +use malachite_test::{Address, Height, Proposal, TestContext, Value}; -use malachite_common::{Consensus, Round, Timeout, TimeoutStep}; +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; +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 mut state: State = State::new(Height::new(10)); + let height = Height::new(10); + + let mut state: State = State::default(); + let data = RoundData::new(Round::new(0), &height, &ADDRESS); - let transition = apply_event(state.clone(), Round::new(0), Event::NewRoundProposer(value)); + let transition = apply_event(state.clone(), &data, Event::NewRoundProposer(value)); state.step = Step::Propose; - assert_eq!(transition.state, state); + assert_eq!(transition.next_state, state); assert_eq!( transition.message.unwrap(), @@ -25,24 +30,27 @@ fn test_propose() { #[test] fn test_prevote() { let value = Value::new(42); - let state: State = State::new(Height::new(1)).new_round(Round::new(1)); + let height = Height::new(1); - let transition = apply_event(state, Round::new(1), Event::NewRound); + let state: State = State::default().new_round(Round::new(1)); + let data = RoundData::new(Round::new(1), &height, &ADDRESS); - assert_eq!(transition.state.step, Step::Propose); + let transition = apply_event(state, &data, Event::NewRound); + + assert_eq!(transition.next_state.step, Step::Propose); assert_eq!( transition.message.unwrap(), - Message::Timeout(Timeout { + Message::ScheduleTimeout(Timeout { round: Round::new(1), step: TimeoutStep::Propose }) ); - let state = transition.state; + let state = transition.next_state; let transition = apply_event( state, - Round::new(1), + &data, Event::Proposal(Proposal::new( Height::new(1), Round::new(1), @@ -51,13 +59,9 @@ fn test_prevote() { )), ); - assert_eq!(transition.state.step, Step::Prevote); + assert_eq!(transition.next_state.step, Step::Prevote); assert_eq!( transition.message.unwrap(), - Message::prevote( - Round::new(1), - Some(value.id()), - TestConsensus::DUMMY_ADDRESS - ) + Message::prevote(Round::new(1), Some(value.id()), ADDRESS) ); } diff --git a/Code/test/tests/vote_count.rs b/Code/test/tests/round_votes.rs similarity index 78% rename from Code/test/tests/vote_count.rs rename to Code/test/tests/round_votes.rs index e1367e1c4..fb83ca498 100644 --- a/Code/test/tests/vote_count.rs +++ b/Code/test/tests/round_votes.rs @@ -2,17 +2,19 @@ use malachite_common::Round; use malachite_vote::count::Threshold; use malachite_vote::RoundVotes; -use malachite_test::{Address, Height, TestConsensus, ValueId, Vote}; +use malachite_test::{Address, Height, TestContext, ValueId, Vote}; + +const ADDRESS: Address = Address::new([42; 20]); #[test] fn add_votes_nil() { let total = 3; - let mut round_votes: RoundVotes = + let mut round_votes: RoundVotes = RoundVotes::new(Height::new(1), Round::new(0), total); // add a vote for nil. nothing changes. - let vote = Vote::new_prevote(Round::new(0), None, Address::new(1)); + let vote = Vote::new_prevote(Round::new(0), None, ADDRESS); let thresh = round_votes.add_vote(vote.clone(), 1); assert_eq!(thresh, Threshold::Init); @@ -32,11 +34,11 @@ fn add_votes_single_value() { let total = 4; let weight = 1; - let mut round_votes: RoundVotes = + let mut round_votes: RoundVotes = RoundVotes::new(Height::new(1), Round::new(0), total); // add a vote. nothing changes. - let vote = Vote::new_prevote(Round::new(0), val, Address::new(1)); + let vote = Vote::new_prevote(Round::new(0), val, ADDRESS); let thresh = round_votes.add_vote(vote.clone(), weight); assert_eq!(thresh, Threshold::Init); @@ -45,7 +47,7 @@ fn add_votes_single_value() { assert_eq!(thresh, Threshold::Init); // add a vote for nil, get Thresh::Any - let vote_nil = Vote::new_prevote(Round::new(0), None, Address::new(2)); + let vote_nil = Vote::new_prevote(Round::new(0), None, ADDRESS); let thresh = round_votes.add_vote(vote_nil, weight); assert_eq!(thresh, Threshold::Any); @@ -62,21 +64,21 @@ fn add_votes_multi_values() { let val2 = Some(v2); let total = 15; - let mut round_votes: RoundVotes = + let mut round_votes: RoundVotes = RoundVotes::new(Height::new(1), Round::new(0), total); // add a vote for v1. nothing changes. - let vote1 = Vote::new_precommit(Round::new(0), val1, Address::new(1)); + let vote1 = Vote::new_precommit(Round::new(0), val1, ADDRESS); let thresh = round_votes.add_vote(vote1.clone(), 1); assert_eq!(thresh, Threshold::Init); // add a vote for v2. nothing changes. - let vote2 = Vote::new_precommit(Round::new(0), val2, Address::new(2)); + let vote2 = Vote::new_precommit(Round::new(0), val2, ADDRESS); let thresh = round_votes.add_vote(vote2.clone(), 1); assert_eq!(thresh, Threshold::Init); // add a vote for nil. nothing changes. - let vote_nil = Vote::new_precommit(Round::new(0), None, Address::new(3)); + let vote_nil = Vote::new_precommit(Round::new(0), None, ADDRESS); let thresh = round_votes.add_vote(vote_nil.clone(), 1); assert_eq!(thresh, Threshold::Init); diff --git a/Code/test/tests/vote_keeper.rs b/Code/test/tests/vote_keeper.rs index c5c207edb..77eeae569 100644 --- a/Code/test/tests/vote_keeper.rs +++ b/Code/test/tests/vote_keeper.rs @@ -1,81 +1,82 @@ use malachite_common::Round; -use malachite_round::events::Event; -use malachite_vote::keeper::VoteKeeper; +use malachite_vote::keeper::{Message, VoteKeeper}; -use malachite_test::{Address, Height, TestConsensus, ValueId, Vote}; +use malachite_test::{Address, Height, TestContext, ValueId, Vote}; + +const ADDRESS: Address = Address::new([42; 20]); #[test] fn prevote_apply_nil() { - let mut keeper: VoteKeeper = VoteKeeper::new(Height::new(1), Round::INITIAL, 3); + let mut keeper: VoteKeeper = VoteKeeper::new(Height::new(1), Round::INITIAL, 3); - let vote = Vote::new_prevote(Round::new(0), None, Address::new(1)); + let vote = Vote::new_prevote(Round::new(0), None, ADDRESS); - let event = keeper.apply_vote(vote.clone(), 1); - assert_eq!(event, None); + let msg = keeper.apply_vote(vote.clone(), 1); + assert_eq!(msg, None); - let event = keeper.apply_vote(vote.clone(), 1); - assert_eq!(event, None); + let msg = keeper.apply_vote(vote.clone(), 1); + assert_eq!(msg, None); - let event = keeper.apply_vote(vote, 1); - assert_eq!(event, Some(Event::PolkaNil)); + let msg = keeper.apply_vote(vote, 1); + assert_eq!(msg, Some(Message::PolkaNil)); } #[test] fn precommit_apply_nil() { - let mut keeper: VoteKeeper = VoteKeeper::new(Height::new(1), Round::INITIAL, 3); + let mut keeper: VoteKeeper = VoteKeeper::new(Height::new(1), Round::INITIAL, 3); - let vote = Vote::new_precommit(Round::new(0), None, Address::new(1)); + let vote = Vote::new_precommit(Round::new(0), None, ADDRESS); - let event = keeper.apply_vote(vote.clone(), 1); - assert_eq!(event, None); + let msg = keeper.apply_vote(vote.clone(), 1); + assert_eq!(msg, None); - let event = keeper.apply_vote(vote.clone(), 1); - assert_eq!(event, None); + let msg = keeper.apply_vote(vote.clone(), 1); + assert_eq!(msg, None); - let event = keeper.apply_vote(vote, 1); - assert_eq!(event, None); + let msg = keeper.apply_vote(vote, 1); + assert_eq!(msg, None); } #[test] fn prevote_apply_single_value() { - let mut keeper: VoteKeeper = VoteKeeper::new(Height::new(1), Round::INITIAL, 4); + let mut keeper: VoteKeeper = VoteKeeper::new(Height::new(1), Round::INITIAL, 4); let v = ValueId::new(1); let val = Some(v); - let vote = Vote::new_prevote(Round::new(0), val, Address::new(1)); + let vote = Vote::new_prevote(Round::new(0), val, ADDRESS); - let event = keeper.apply_vote(vote.clone(), 1); - assert_eq!(event, None); + let msg = keeper.apply_vote(vote.clone(), 1); + assert_eq!(msg, None); - let event = keeper.apply_vote(vote.clone(), 1); - assert_eq!(event, None); + let msg = keeper.apply_vote(vote.clone(), 1); + assert_eq!(msg, None); - let vote_nil = Vote::new_prevote(Round::new(0), None, Address::new(2)); - let event = keeper.apply_vote(vote_nil, 1); - assert_eq!(event, Some(Event::PolkaAny)); + let vote_nil = Vote::new_prevote(Round::new(0), None, ADDRESS); + let msg = keeper.apply_vote(vote_nil, 1); + assert_eq!(msg, Some(Message::PolkaAny)); - let event = keeper.apply_vote(vote, 1); - assert_eq!(event, Some(Event::PolkaValue(v))); + let msg = keeper.apply_vote(vote, 1); + assert_eq!(msg, Some(Message::PolkaValue(v))); } #[test] fn precommit_apply_single_value() { - let mut keeper: VoteKeeper = VoteKeeper::new(Height::new(1), Round::INITIAL, 4); + let mut keeper: VoteKeeper = VoteKeeper::new(Height::new(1), Round::INITIAL, 4); let v = ValueId::new(1); let val = Some(v); - let vote = Vote::new_precommit(Round::new(0), val, Address::new(1)); + let vote = Vote::new_precommit(Round::new(0), val, ADDRESS); - let event = keeper.apply_vote(vote.clone(), 1); - assert_eq!(event, None); + let msg = keeper.apply_vote(vote.clone(), 1); + assert_eq!(msg, None); - let event = keeper.apply_vote(vote.clone(), 1); - assert_eq!(event, None); + let msg = keeper.apply_vote(vote.clone(), 1); + assert_eq!(msg, None); - let vote_nil = Vote::new_precommit(Round::new(0), None, Address::new(2)); - let event = keeper.apply_vote(vote_nil, 1); - assert_eq!(event, Some(Event::PrecommitAny)); + let vote_nil = Vote::new_precommit(Round::new(0), None, ADDRESS); + let msg = keeper.apply_vote(vote_nil, 1); + assert_eq!(msg, Some(Message::PrecommitAny)); - let event = keeper.apply_vote(vote, 1); - assert_eq!(event, Some(Event::PrecommitValue(v))); + 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 index 782235c1d..64d9e4306 100644 --- a/Code/vote/Cargo.toml +++ b/Code/vote/Cargo.toml @@ -6,4 +6,3 @@ publish = false [dependencies] malachite-common = { version = "0.1.0", path = "../common" } -malachite-round = { version = "0.1.0", path = "../round" } diff --git a/Code/vote/src/count.rs b/Code/vote/src/count.rs index fb627ca9b..b124e0b26 100644 --- a/Code/vote/src/count.rs +++ b/Code/vote/src/count.rs @@ -1,13 +1,13 @@ use alloc::collections::BTreeMap; -use malachite_common::{Consensus, ValueId, Vote}; - +// TODO: Introduce newtype +// QUESTION: Over what type? i64? pub type Weight = u64; /// A value and the weight of votes for it. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ValuesWeights { - value_weights: BTreeMap, + value_weights: BTreeMap, Weight>, } impl ValuesWeights { @@ -18,21 +18,26 @@ impl ValuesWeights { } /// Add weight to the value and return the new weight. - pub fn add_weight(&mut self, value: ValueId, weight: Weight) -> Weight + pub fn add(&mut self, value: Option, weight: Weight) -> Weight where ValueId: Ord, { let entry = self.value_weights.entry(value).or_insert(0); - *entry += weight; + *entry += weight; // FIXME: Deal with overflows *entry } - /// Return the value with the highest weight and said weight, if any. - pub fn highest_weighted_value(&self) -> Option<(&ValueId, Weight)> { - self.value_weights - .iter() - .max_by_key(|(_, weight)| *weight) - .map(|(value, weight)| (value, *weight)) + /// Return the weight of the value, or 0 if it is not present. + pub fn get(&self, value: &Option) -> Weight + where + ValueId: Ord, + { + self.value_weights.get(value).cloned().unwrap_or(0) + } + + /// Return the sum of the weights of all values. + pub fn sum(&self) -> Weight { + self.value_weights.values().sum() // FIXME: Deal with overflows } } @@ -45,64 +50,68 @@ impl Default for ValuesWeights { /// VoteCount tallys votes of the same type. /// Votes are for nil or for some value. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct VoteCount -where - C: Consensus, -{ - // Weight of votes for nil - pub nil: Weight, - /// Weight of votes for the values - pub values_weights: ValuesWeights>, +pub struct VoteCount { + /// Weight of votes for the values, including nil + pub values_weights: ValuesWeights, + /// Total weight pub total: Weight, } -impl VoteCount -where - C: Consensus, -{ +impl VoteCount { pub fn new(total: Weight) -> Self { VoteCount { - nil: 0, total, values_weights: ValuesWeights::new(), } } - /// Add vote to internal counters and return the highest threshold. - pub fn add_vote(&mut self, vote: C::Vote, weight: Weight) -> Threshold> { - if let Some(value) = vote.value() { - let new_weight = self.values_weights.add_weight(value.clone(), weight); + /// Add vote for a vlaue to internal counters and return the highest threshold. + pub fn add_vote(&mut self, value: Option, weight: Weight) -> Threshold + where + Value: Clone + Ord, + { + let new_weight = self.values_weights.add(value.clone(), weight); - // Check if we have a quorum for this value. - if is_quorum(new_weight, self.total) { - return Threshold::Value(value.clone()); - } - } else { - self.nil += weight; + match value { + Some(value) if is_quorum(new_weight, self.total) => Threshold::Value(value), - // Check if we have a quorum for nil. - if is_quorum(self.nil, self.total) { - return Threshold::Nil; - } - } + None if is_quorum(new_weight, self.total) => Threshold::Nil, + + _ => { + let sum_weight = self.values_weights.sum(); - // Check if we have a quorum for any value, using the highest weighted value, if any. - if let Some((_max_value, max_weight)) = self.values_weights.highest_weighted_value() { - if is_quorum(max_weight + self.nil, self.total) { - return Threshold::Any; + if is_quorum(sum_weight, self.total) { + Threshold::Any + } else { + Threshold::Init + } } } - - // No quorum - Threshold::Init } - pub fn check_threshold(&self, threshold: Threshold>) -> bool { + + /// Return whether or not the threshold is met, ie. if we have a quorum for that threshold. + pub fn is_threshold_met(&self, threshold: Threshold) -> bool + where + Value: Clone + Ord, + { match threshold { + Threshold::Value(value) => { + let weight = self.values_weights.get(&Some(value)); + is_quorum(weight, self.total) + } + + Threshold::Nil => { + let weight = self.values_weights.get(&None); + is_quorum(weight, self.total) + } + + Threshold::Any => { + let sum_weight = self.values_weights.sum(); + is_quorum(sum_weight, self.total) + } + Threshold::Init => false, - Threshold::Any => self.values_weights.highest_weighted_value().is_some(), - Threshold::Nil => self.nil > 0, - Threshold::Value(value) => self.values_weights.value_weights.contains_key(&value), } } } @@ -125,9 +134,122 @@ pub enum Threshold { } /// Returns whether or note `value > (2/3)*total`. -pub fn is_quorum(value: Weight, total: Weight) -> bool { +fn is_quorum(value: Weight, total: Weight) -> bool { 3 * value > 2 * total } #[cfg(test)] -mod tests {} +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 + } + + #[test] + #[allow(clippy::bool_assert_comparison)] + fn vote_count_nil() { + let mut vc = VoteCount::new(4); + + assert_eq!(vc.is_threshold_met(Threshold::Init), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add_vote(None, 1), Threshold::Init); + assert_eq!(vc.is_threshold_met(Threshold::Init), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add_vote(None, 1), Threshold::Init); + assert_eq!(vc.is_threshold_met(Threshold::Init), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add_vote(None, 1), Threshold::Nil); + assert_eq!(vc.is_threshold_met(Threshold::Init), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add_vote(Some(1), 1), Threshold::Any); + assert_eq!(vc.is_threshold_met(Threshold::Init), false); + assert_eq!(vc.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] + #[allow(clippy::bool_assert_comparison)] + fn vote_count_value() { + let mut vc = VoteCount::new(4); + + assert_eq!(vc.is_threshold_met(Threshold::Init), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add_vote(Some(1), 1), Threshold::Init); + assert_eq!(vc.is_threshold_met(Threshold::Init), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add_vote(Some(1), 1), Threshold::Init); + assert_eq!(vc.is_threshold_met(Threshold::Init), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), false); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add_vote(Some(1), 1), Threshold::Value(1)); + assert_eq!(vc.is_threshold_met(Threshold::Init), false); + assert_eq!(vc.is_threshold_met(Threshold::Any), true); + assert_eq!(vc.is_threshold_met(Threshold::Nil), false); + assert_eq!(vc.is_threshold_met(Threshold::Value(1)), true); + assert_eq!(vc.is_threshold_met(Threshold::Value(2)), false); + + assert_eq!(vc.add_vote(Some(2), 1), Threshold::Any); + assert_eq!(vc.is_threshold_met(Threshold::Init), false); + 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 index a24f63829..6149e3927 100644 --- a/Code/vote/src/keeper.rs +++ b/Code/vote/src/keeper.rs @@ -1,29 +1,38 @@ use alloc::collections::BTreeMap; -use malachite_common::{Consensus, Round, ValueId, Vote, VoteType}; -use malachite_round::events::Event; +use malachite_common::{Context, Round, ValueId, Vote, VoteType}; use crate::{ count::{Threshold, Weight}, RoundVotes, }; -/// Keeps track of votes and emits events when thresholds are reached. +/// Messages emitted by the vote keeper +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Message { + PolkaAny, + PolkaNil, + PolkaValue(Value), + PrecommitAny, + PrecommitValue(Value), +} + +/// Keeps track of votes and emits messages when thresholds are reached. #[derive(Clone, Debug)] -pub struct VoteKeeper +pub struct VoteKeeper where - C: Consensus, + Ctx: Context, { - height: C::Height, + height: Ctx::Height, total_weight: Weight, - rounds: BTreeMap>, + rounds: BTreeMap>, } -impl VoteKeeper +impl VoteKeeper where - C: Consensus, + Ctx: Context, { - pub fn new(height: C::Height, round: Round, total_weight: Weight) -> Self { + pub fn new(height: Ctx::Height, round: Round, total_weight: Weight) -> Self { let mut rounds = BTreeMap::new(); rounds.insert(round, RoundVotes::new(height.clone(), round, total_weight)); @@ -36,7 +45,7 @@ where } /// Apply a vote with a given weight, potentially triggering an event. - pub fn apply_vote(&mut self, vote: C::Vote, weight: Weight) -> Option> { + pub fn apply_vote(&mut self, vote: Ctx::Vote, weight: Weight) -> Option>> { let round = self.rounds.entry(vote.round()).or_insert_with(|| { RoundVotes::new(self.height.clone(), vote.round(), self.total_weight) }); @@ -44,14 +53,15 @@ where let vote_type = vote.vote_type(); let threshold = round.add_vote(vote, weight); - Self::to_event(vote_type, threshold) + Self::to_message(vote_type, threshold) } - pub fn check_threshold( + /// 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>, + threshold: Threshold>, ) -> bool { let round = match self.rounds.get(round) { Some(round) => round, @@ -59,23 +69,26 @@ where }; match vote_type { - VoteType::Prevote => round.prevotes.check_threshold(threshold), - VoteType::Precommit => round.precommits.check_threshold(threshold), + VoteType::Prevote => round.prevotes.is_threshold_met(threshold), + VoteType::Precommit => round.precommits.is_threshold_met(threshold), } } /// Map a vote type and a threshold to a state machine event. - fn to_event(typ: VoteType, threshold: Threshold>) -> Option> { + fn to_message( + typ: VoteType, + threshold: Threshold>, + ) -> Option>> { match (typ, threshold) { (_, Threshold::Init) => None, - (VoteType::Prevote, Threshold::Any) => Some(Event::PolkaAny), - (VoteType::Prevote, Threshold::Nil) => Some(Event::PolkaNil), - (VoteType::Prevote, Threshold::Value(v)) => Some(Event::PolkaValue(v)), + (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(Event::PrecommitAny), + (VoteType::Precommit, Threshold::Any) => Some(Message::PrecommitAny), (VoteType::Precommit, Threshold::Nil) => None, - (VoteType::Precommit, Threshold::Value(v)) => Some(Event::PrecommitValue(v)), + (VoteType::Precommit, Threshold::Value(v)) => Some(Message::PrecommitValue(v)), } } } diff --git a/Code/vote/src/lib.rs b/Code/vote/src/lib.rs index 78863e9c8..6d66b9448 100644 --- a/Code/vote/src/lib.rs +++ b/Code/vote/src/lib.rs @@ -15,28 +15,28 @@ extern crate alloc; pub mod count; pub mod keeper; -use malachite_common::{Consensus, Round, ValueId, Vote, VoteType}; +use malachite_common::{Context, Round, ValueId, Vote, VoteType}; use crate::count::{Threshold, VoteCount, Weight}; /// Tracks all the votes for a single round #[derive(Clone, Debug)] -pub struct RoundVotes +pub struct RoundVotes where - C: Consensus, + Ctx: Context, { - pub height: C::Height, + pub height: Ctx::Height, pub round: Round, - pub prevotes: VoteCount, - pub precommits: VoteCount, + pub prevotes: VoteCount>, + pub precommits: VoteCount>, } -impl RoundVotes +impl RoundVotes where - C: Consensus, + Ctx: Context, { - pub fn new(height: C::Height, round: Round, total: Weight) -> Self { + pub fn new(height: Ctx::Height, round: Round, total: Weight) -> Self { RoundVotes { height, round, @@ -45,10 +45,10 @@ where } } - pub fn add_vote(&mut self, vote: C::Vote, weight: Weight) -> Threshold> { + pub fn add_vote(&mut self, vote: Ctx::Vote, weight: Weight) -> Threshold> { match vote.vote_type() { - VoteType::Prevote => self.prevotes.add_vote(vote, weight), - VoteType::Precommit => self.precommits.add_vote(vote, weight), + VoteType::Prevote => self.prevotes.add_vote(vote.take_value(), weight), + VoteType::Precommit => self.precommits.add_vote(vote.take_value(), weight), } } } 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/Specs/Quint/0DecideNonProposerTest.itf.json b/Specs/Quint/0DecideNonProposerTest.itf.json new file mode 100644 index 000000000..5d30f6911 --- /dev/null +++ b/Specs/Quint/0DecideNonProposerTest.itf.json @@ -0,0 +1 @@ +{"#meta":{"format":"ITF","format-description":"https://apalache.informal.systems/docs/adr/015adr-trace.html","source":"consensus.qnt","status":"passed","description":"Created by Quint on Wed Oct 25 2023 15:38:28 GMT+0200 (Central European Summer Time)","timestamp":1698241108633},"vars":["system","_Event","_Result"],"states":[{"#meta":{"index":0},"_Event":{"height":-1,"name":"Initial","round":-1,"value":"","vr":-1},"_Result":{"decided":"","name":"","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":1,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":0,"step":"newRound","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":1},"_Event":{"height":1,"name":"NewRound","round":0,"value":"","vr":-1},"_Result":{"decided":"","name":"timeout","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"timeoutPropose","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":1,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":0,"step":"propose","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":2},"_Event":{"height":1,"name":"NewRound","round":0,"value":"","vr":-1},"_Result":{"decided":"","name":"timeout","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"timeoutPropose","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":1,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":0,"step":"propose","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":3},"_Event":{"height":1,"name":"Proposal","round":0,"value":"block","vr":-1},"_Result":{"decided":"","name":"votemessage","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"","voteMessage":{"height":1,"id":"block","round":0,"src":"Josef","step":"prevote"}},"system":{"#map":[["Josef",{"height":1,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":0,"step":"prevote","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":4},"_Event":{"height":1,"name":"Proposal","round":0,"value":"block","vr":-1},"_Result":{"decided":"","name":"votemessage","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"","voteMessage":{"height":1,"id":"block","round":0,"src":"Josef","step":"prevote"}},"system":{"#map":[["Josef",{"height":1,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":0,"step":"prevote","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":5},"_Event":{"height":1,"name":"ProposalAndPolkaAndValid","round":0,"value":"block","vr":-1},"_Result":{"decided":"","name":"votemessage","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"","voteMessage":{"height":1,"id":"block","round":0,"src":"Josef","step":"precommit"}},"system":{"#map":[["Josef",{"height":1,"lockedRound":0,"lockedValue":"block","p":"Josef","round":0,"step":"precommit","validRound":0,"validValue":"block"}]]}},{"#meta":{"index":6},"_Event":{"height":1,"name":"ProposalAndPolkaAndValid","round":0,"value":"block","vr":-1},"_Result":{"decided":"","name":"votemessage","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"","voteMessage":{"height":1,"id":"block","round":0,"src":"Josef","step":"precommit"}},"system":{"#map":[["Josef",{"height":1,"lockedRound":0,"lockedValue":"block","p":"Josef","round":0,"step":"precommit","validRound":0,"validValue":"block"}]]}},{"#meta":{"index":7},"_Event":{"height":1,"name":"ProposalAndCommitAndValid","round":0,"value":"block","vr":-1},"_Result":{"decided":"block","name":"decided","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":1,"lockedRound":0,"lockedValue":"block","p":"Josef","round":0,"step":"decided","validRound":0,"validValue":"block"}]]}},{"#meta":{"index":8},"_Event":{"height":1,"name":"ProposalAndCommitAndValid","round":0,"value":"block","vr":-1},"_Result":{"decided":"block","name":"decided","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":1,"lockedRound":0,"lockedValue":"block","p":"Josef","round":0,"step":"decided","validRound":0,"validValue":"block"}]]}},{"#meta":{"index":9},"_Event":{"height":2,"name":"NewHeight","round":0,"value":"","vr":-1},"_Result":{"decided":"","name":"","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":0,"step":"newRound","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":10},"_Event":{"height":2,"name":"NewHeight","round":0,"value":"","vr":-1},"_Result":{"decided":"","name":"","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":0,"step":"newRound","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":11},"_Event":{"height":2,"name":"NewRoundProposer","round":0,"value":"nextBlock","vr":-1},"_Result":{"decided":"","name":"proposal","proposal":{"height":2,"proposal":"nextBlock","round":0,"src":"Josef","validRound":-1},"skipRound":-1,"timeout":"","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":0,"step":"propose","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":12},"_Event":{"height":2,"name":"NewRoundProposer","round":0,"value":"nextBlock","vr":-1},"_Result":{"decided":"","name":"proposal","proposal":{"height":2,"proposal":"nextBlock","round":0,"src":"Josef","validRound":-1},"skipRound":-1,"timeout":"","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":0,"step":"propose","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":13},"_Event":{"height":2,"name":"Proposal","round":0,"value":"nextBlock","vr":-1},"_Result":{"decided":"","name":"votemessage","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"","voteMessage":{"height":2,"id":"nextBlock","round":0,"src":"Josef","step":"prevote"}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":0,"step":"prevote","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":14},"_Event":{"height":2,"name":"Proposal","round":0,"value":"nextBlock","vr":-1},"_Result":{"decided":"","name":"votemessage","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"","voteMessage":{"height":2,"id":"nextBlock","round":0,"src":"Josef","step":"prevote"}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":0,"step":"prevote","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":15},"_Event":{"height":2,"name":"PolkaAny","round":0,"value":"nextBlock","vr":-1},"_Result":{"decided":"","name":"timeout","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"timeoutPrevote","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":0,"step":"prevote","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":16},"_Event":{"height":2,"name":"PolkaAny","round":0,"value":"nextBlock","vr":-1},"_Result":{"decided":"","name":"timeout","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"timeoutPrevote","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":0,"step":"prevote","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":17},"_Event":{"height":2,"name":"TimeoutPrevote","round":0,"value":"nextBlock","vr":-1},"_Result":{"decided":"","name":"votemessage","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"","voteMessage":{"height":2,"id":"nil","round":0,"src":"Josef","step":"precommit"}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":0,"step":"precommit","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":18},"_Event":{"height":2,"name":"TimeoutPrevote","round":0,"value":"nextBlock","vr":-1},"_Result":{"decided":"","name":"votemessage","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"","voteMessage":{"height":2,"id":"nil","round":0,"src":"Josef","step":"precommit"}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":0,"step":"precommit","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":19},"_Event":{"height":2,"name":"PrecommitAny","round":0,"value":"nextBlock","vr":-1},"_Result":{"decided":"","name":"timeout","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"timeoutPrecommit","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":0,"step":"precommit","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":20},"_Event":{"height":2,"name":"PrecommitAny","round":0,"value":"nextBlock","vr":-1},"_Result":{"decided":"","name":"timeout","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"timeoutPrecommit","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":0,"step":"precommit","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":21},"_Event":{"height":2,"name":"TimeoutPrecommit","round":0,"value":"nextBlock","vr":-1},"_Result":{"decided":"","name":"skipRound","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":1,"timeout":"","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":0,"step":"precommit","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":22},"_Event":{"height":2,"name":"TimeoutPrecommit","round":0,"value":"nextBlock","vr":-1},"_Result":{"decided":"","name":"skipRound","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":1,"timeout":"","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":0,"step":"precommit","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":23},"_Event":{"height":2,"name":"NewRound","round":1,"value":"","vr":-1},"_Result":{"decided":"","name":"timeout","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"timeoutPropose","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":1,"step":"propose","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":24},"_Event":{"height":2,"name":"NewRound","round":1,"value":"","vr":-1},"_Result":{"decided":"","name":"timeout","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"timeoutPropose","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":1,"step":"propose","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":25},"_Event":{"height":2,"name":"TimeoutPropose","round":1,"value":"nextBlock","vr":-1},"_Result":{"decided":"","name":"votemessage","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"","voteMessage":{"height":2,"id":"nil","round":1,"src":"Josef","step":"prevote"}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":1,"step":"prevote","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":26},"_Event":{"height":2,"name":"TimeoutPropose","round":1,"value":"nextBlock","vr":-1},"_Result":{"decided":"","name":"votemessage","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"","voteMessage":{"height":2,"id":"nil","round":1,"src":"Josef","step":"prevote"}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":1,"step":"prevote","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":27},"_Event":{"height":2,"name":"PolkaNil","round":1,"value":"nextBlock","vr":-1},"_Result":{"decided":"","name":"votemessage","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"","voteMessage":{"height":2,"id":"nil","round":1,"src":"Josef","step":"precommit"}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":1,"step":"precommit","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":28},"_Event":{"height":2,"name":"PolkaNil","round":1,"value":"nextBlock","vr":-1},"_Result":{"decided":"","name":"votemessage","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"","voteMessage":{"height":2,"id":"nil","round":1,"src":"Josef","step":"precommit"}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":1,"step":"precommit","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":29},"_Event":{"height":2,"name":"PrecommitAny","round":1,"value":"nextBlock","vr":-1},"_Result":{"decided":"","name":"timeout","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"timeoutPrecommit","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":1,"step":"precommit","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":30},"_Event":{"height":2,"name":"PrecommitAny","round":1,"value":"nextBlock","vr":-1},"_Result":{"decided":"","name":"timeout","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"timeoutPrecommit","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":1,"step":"precommit","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":31},"_Event":{"height":2,"name":"TimeoutPrecommit","round":1,"value":"nextBlock","vr":-1},"_Result":{"decided":"","name":"skipRound","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":2,"timeout":"","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":1,"step":"precommit","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":32},"_Event":{"height":2,"name":"TimeoutPrecommit","round":1,"value":"nextBlock","vr":-1},"_Result":{"decided":"","name":"skipRound","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":2,"timeout":"","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":1,"step":"precommit","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":33},"_Event":{"height":2,"name":"NewRound","round":2,"value":"","vr":-1},"_Result":{"decided":"","name":"timeout","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"timeoutPropose","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":2,"step":"propose","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":34},"_Event":{"height":2,"name":"NewRound","round":2,"value":"","vr":-1},"_Result":{"decided":"","name":"timeout","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"timeoutPropose","voteMessage":{"height":-1,"id":"","round":-1,"src":"","step":""}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":2,"step":"propose","validRound":-1,"validValue":"nil"}]]}},{"#meta":{"index":35},"_Event":{"height":2,"name":"ProposalInvalid","round":2,"value":"","vr":-1},"_Result":{"decided":"","name":"votemessage","proposal":{"height":-1,"proposal":"","round":-1,"src":"","validRound":-1},"skipRound":-1,"timeout":"","voteMessage":{"height":2,"id":"nil","round":2,"src":"Josef","step":"prevote"}},"system":{"#map":[["Josef",{"height":2,"lockedRound":-1,"lockedValue":"nil","p":"Josef","round":2,"step":"prevote","validRound":-1,"validValue":"nil"}]]}}]} \ No newline at end of file diff --git a/Specs/Quint/basicSpells.qnt b/Specs/Quint/basicSpells.qnt new file mode 100644 index 000000000..78d722edb --- /dev/null +++ b/Specs/Quint/basicSpells.qnt @@ -0,0 +1,112 @@ +// -*- mode: Bluespec; -*- +/** + * This module collects definitions that are ubiquitous. + * One day they will become the standard library of Quint. + */ +module basicSpells { + /// An annotation for writing preconditions. + /// - @param __cond condition to check + /// - @returns true if and only if __cond evaluates to true + pure def require(__cond: bool): bool = __cond + + run requireTest = all { + assert(require(4 > 3)), + assert(not(require(false))), + } + + /// A convenience operator that returns a string error code, + /// if the condition does not hold true. + /// + /// - @param __cond condition to check + /// - @param __error a non-empty error message + /// - @returns "", when __cond holds true; otherwise __error + pure def requires(__cond: bool, __error: str): str = { + if (__cond) "" else __error + } + + run requiresTest = all { + assert(requires(4 > 3, "4 > 3") == ""), + assert(requires(4 < 3, "false: 4 < 3") == "false: 4 < 3"), + } + + + /// Compute the maximum of two integers. + /// + /// - @param __i first integer + /// - @param __j second integer + /// - @returns the maximum of __i and __j + pure def max(__i: int, __j: int): int = { + if (__i > __j) __i else __j + } + + run maxTest = all { + assert(max(3, 4) == 4), + assert(max(6, 3) == 6), + assert(max(10, 10) == 10), + assert(max(-3, -5) == -3), + assert(max(-5, -3) == -3), + } + + /// Remove a set element. + /// + /// - @param __set a set to remove an element from + /// - @param __elem an element to remove + /// - @returns a new set that contains all elements of __set but __elem + pure def setRemove(__set: Set[a], __elem: a): Set[a] = { + __set.exclude(Set(__elem)) + } + + run setRemoveTest = all { + assert(Set(2, 4) == Set(2, 3, 4).setRemove(3)), + assert(Set() == Set().setRemove(3)), + } + + /// Test whether a key is present in a map + /// + /// - @param __map a map to query + /// - @param __key the key to look for + /// - @returns true if and only __map has an entry associated with __key + pure def has(__map: a -> b, __key: a): bool = { + __map.keys().contains(__key) + } + + run hasTest = all { + assert(Map(2 -> 3, 4 -> 5).has(2)), + assert(not(Map(2 -> 3, 4 -> 5).has(6))), + } + + /// Get the map value associated with a key, or the default, + /// if the key is not present. + /// + /// - @param __map the map to query + /// - @param __key the key to search for + /// - @returns the value associated with the key, if __key is + /// present in the map, and __default otherwise + pure def getOrElse(__map: a -> b, __key: a, __default: b): b = { + if (__map.has(__key)) { + __map.get(__key) + } else { + __default + } + } + + run getOrElseTest = all { + assert(Map(2 -> 3, 4 -> 5).getOrElse(2, 0) == 3), + assert(Map(2 -> 3, 4 -> 5).getOrElse(7, 11) == 11), + } + + /// Remove a map entry. + /// + /// - @param __map a map to remove an entry from + /// - @param __key the key of an entry to remove + /// - @returns a new map that contains all entries of __map + /// that do not have the key __key + pure def mapRemove(__map: a -> b, __key: a): a -> b = { + __map.keys().setRemove(__key).mapBy(__k => __map.get(__k)) + } + + run mapRemoveTest = all { + assert(Map(3 -> 4, 7 -> 8) == Map(3 -> 4, 5 -> 6, 7 -> 8).mapRemove(5)), + assert(Map() == Map().mapRemove(3)), + } +} \ No newline at end of file diff --git a/Specs/Quint/consensus.qnt b/Specs/Quint/consensus.qnt new file mode 100644 index 000000000..a8f4ba50a --- /dev/null +++ b/Specs/Quint/consensus.qnt @@ -0,0 +1,506 @@ +// -*- mode: Bluespec; -*- + +module consensus { + + // a process name is just a string in our specification + type Address_t = str + // a value is also a string + type Value_t = str + // a state is also a string + type Step_t = str + // a round is an integer + type Round_t = int + // a height is an integer + type Height_t = int + // a state is also a string + type Timeout_t = str + +// the type of propose messages + type ProposeMsg_t = { + src: Address_t, + height: Height_t, + round: Round_t, + proposal: Value_t, + validRound: Round_t + } + + // the type of Prevote and Precommit messages + type VoteMsg_t = { + src: Address_t, + height: Height_t, + round: Round_t, + step: Step_t, // "prevote" or "precommit" + id: Value_t, + } + +type ConsensusState = { + p: Address_t, + height : int, + round: Round_t, + step: Step_t, // "newRound", propose, prevote, precommit, decided + lockedRound: Round_t, + lockedValue: Value_t, + validRound: Round_t, + validValue: Value_t, + //continue +} + +type Event = { + name : str, + height : int, + round: Round_t, + value: Value_t, + vr: Round_t +} + +// what is a good way to encode optionals? I do with default values +type Result = { + name : str, + proposal: ProposeMsg_t, + voteMessage: VoteMsg_t, + timeout: Timeout_t, + decided: Value_t, + skipRound: Round_t +} + +val consensusEvents = Set( + "NewHeight", + "NewRound", // Start a new round, not as proposer. + "NewRoundProposer(Value)", // Start a new round and propose the Value. + "Proposal", // Receive a proposal with possible polka round. + "ProposalAndPolkaPreviousAndValid", //28 when valid + "ProposalInvalid", // 26 and 32 when invalid in step propose + "PolkaNil", // Receive +2/3 prevotes for nil. + "PolkaAny", // Receive +2/3 prevotes for anything and they are not the same + "ProposalAndPolkaAndValid", // 36 when valid and step >= prevote + "PrecommitAny", // Receive +2/3 precommits for anything. + "ProposalAndCommitAndValid", // decide + "RoundSkip", // Receive +1/3 votes from a higher round. + "TimeoutPropose", // Timeout waiting for proposal. + "TimeoutPrevote", // Timeout waiting for prevotes. + "TimeoutPrecommit" // Timeout waiting for precommits +) + +/* + + "PolkaValue(ValueId)", // Receive +2/3 prevotes for Value. + "PrecommitValue(ValueId)", // Receive +2/3 precommits for Value. + +) */ + +val noProp : ProposeMsg_t = {src: "", height: -1, round: -1, proposal: "", validRound: -1} +val noVote : VoteMsg_t = {src: "", height: -1, round: -1, step: "", id: ""} +val noTimeout : Timeout_t = "" +val noDecided = "" +val noSkipRound : Round_t = -1 +val defaultResult : Result = { + name: "", + proposal: noProp, + voteMessage: noVote, + timeout: noTimeout, + decided: noDecided, + skipRound: noSkipRound} + + +pure def NewHeight (state: ConsensusState, ev: Event) : (ConsensusState, Result) = { + val newstate = { ...state, round: ev.round, + step: "newRound", + height : ev.height, + lockedRound: -1, + lockedValue: "nil", + validRound: -1, + validValue: "nil" + } + (newstate, defaultResult) +} + +// line 11.14 +pure def NewRoundProposer (state: ConsensusState, ev: Event) : (ConsensusState, Result) = { + val newstate = { ...state, round: ev.round, step: "propose"} + val proposal = if (state.validValue != "nil") state.validValue + else ev.value + val result = { ...defaultResult, name: "proposal", + proposal: { src: state.p, + height: state.height, + round: ev.round, + proposal: proposal, + validRound: state.validRound}} + (newstate, result) +} + +// line 11.20 +pure def NewRound (state: ConsensusState, ev: Event) : (ConsensusState, Result) = { + val newstate = { ...state, round: ev.round, step: "propose" } + val result = { ...defaultResult, name: "timeout", timeout: "timeoutPropose"} // do we need the roundnumber here? + (newstate, result) +} + +// line 22 +pure def Proposal (state: ConsensusState, ev: Event) : (ConsensusState, Result) = { + if (state.step == "propose") { + val newstate = state.with("step", "prevote") + if (state.lockedRound == -1 or state.lockedValue == ev.value) + val result = { ...defaultResult, name: "votemessage", + voteMessage: { src: state.p, + height: state.height, + round: state.round, + step: "prevote", // "prevote" or "precommit" + id: ev.value}} + (newstate, result) + else + val result = { ...defaultResult, name: "votemessage", + voteMessage: { src: state.p, + height: state.height, + round: state.round, + step: "prevote", // "prevote" or "precommit" + id: "nil"}} + (newstate, result) + } + else + (state, defaultResult) +} + +// line 28 +pure def ProposalAndPolkaPreviousAndValid (state: ConsensusState, ev: Event) : (ConsensusState, Result) = { + if (state.step == "propose" and ev.vr >= 0 and ev.vr < state.round) { + val newstate = state.with("step", "prevote") + if (state.lockedRound <= ev.vr or state.lockedValue == ev.value) + val result = { ...defaultResult, name: "votemessage", + voteMessage: { src: state.p, + height: state.height, + round: state.round, + step: "prevote", // "prevote" or "precommit" + id: ev.value}} + (newstate, result) + else + val result = { ...defaultResult, name: "votemessage", + voteMessage: { src: state.p, + height: state.height, + round: state.round, + step: "prevote", // "prevote" or "precommit" + id: "nil"}} + (newstate, result) + } + else + (state, defaultResult) +} + +pure def ProposalInvalid (state: ConsensusState, ev: Event) : (ConsensusState, Result) = { + if (state.step == "propose") { + val newstate = state.with("step", "prevote") + val result = { ...defaultResult, name: "votemessage", + voteMessage: { src: state.p, + height: state.height, + round: state.round, + step: "prevote", // "prevote" or "precommit" + id: "nil"}} + (newstate, result) + } + else { + (state, defaultResult ) + } +} + +// line 34 +pure def PolkaAny (state: ConsensusState, ev: Event) : (ConsensusState, Result) = { + if (state.step == "prevote") { + val result = { ...defaultResult, name: "timeout", timeout: "timeoutPrevote" } // do we need the roundnumber here? + (state, result) + } + else + (state, defaultResult) +} + +// line 36 +pure def ProposalAndPolkaAndValid (state: ConsensusState, ev: Event) : (ConsensusState, Result) = { + val auxState = { ...state, validValue: ev.value, validRound: state.round } + if (state.step == "prevote") { + val newstate = { ...auxState, lockedValue: ev.value, + lockedRound: state.round, + step: "precommit" } + val result = { ...defaultResult, name: "votemessage", + voteMessage: { src: state.p, + height: state.height, + round: state.round, + step: "precommit", + id: ev.value}} + (newstate, result) + } + else { + (state, defaultResult) + } +} + +// line 44 +pure def PolkaNil (state: ConsensusState, ev: Event) : (ConsensusState, Result) = { + if (state.step != "prevote") + (state, defaultResult) + else + val newstate = { ...state, step: "precommit"} + val result = { ...defaultResult, name: "votemessage", + voteMessage: { src: state.p, + height: state.height, + round: state.round, + step: "precommit", + id: "nil"}} + (newstate, result) +} + +// line 47 +pure def PrecommitAny (state: ConsensusState, ev: Event) : (ConsensusState, Result) = { + if (state.step == "precommit") { + val result = { ...defaultResult, name: "timeout", timeout: "timeoutPrecommit" } // do we need the roundnumber here? + (state, result) + } + else + (state, defaultResult) +} + +// line 49 +pure def ProposalAndCommitAndValid (state: ConsensusState, ev: Event) : (ConsensusState, Result) = { + if (state.step != "decided") { + val newstate = { ...state, step: "decided"} + val result = { ...defaultResult, name: "decided", decided: ev.value} + (newstate, result) + } + else + (state, defaultResult) +} + +// line 55 +pure def RoundSkip (state: ConsensusState, ev: Event) : (ConsensusState, Result) = { + if (ev.round > state.round) + val result = { ...defaultResult, name: "skipRound", skipRound: ev.round } + (state, result) + else + (state, defaultResult) +} + +pure def TimeoutPropose (state: ConsensusState, ev: Event) : (ConsensusState, Result) = { + if (ev.height == state.height and ev.round == state.round and state.step == "propose") + val newstate = { ...state, step: "prevote"} + val result = { ...defaultResult, name: "votemessage", + voteMessage: { src: state.p, + height: state.height, + round: state.round, + step: "prevote", // "prevote" or "precommit" + id: "nil"}} + (newstate, result) + else + (state, defaultResult) +} + +pure def TimeoutPrevote (state: ConsensusState, ev: Event) : (ConsensusState, Result) = { + if (ev.height == state.height and ev.round == state.round and state.step == "prevote") + val newstate = { ...state, step: "precommit"} + val result = { ...defaultResult, name: "votemessage", + voteMessage: { src: state.p, + height: state.height, + round: state.round, + step: "precommit", // "prevote" or "precommit" + id: "nil"}} + (newstate, result) + else + (state, defaultResult) +} + +pure def TimeoutPrecommit (state: ConsensusState, ev: Event) : (ConsensusState, Result) = { + if (ev.height == state.height and ev.round == state.round) + val result = {...defaultResult, name: "skipRound", skipRound: state.round + 1} + (state, result) + else + (state, defaultResult) +} + + +pure def consensus (state: ConsensusState, ev: Event) : (ConsensusState, Result) = { + if (ev.name == "NewHeight") + NewHeight (state, ev) + else if (ev.name == "NewRoundProposer") + NewRoundProposer(state, ev) + else if (ev.name == "NewRound") + NewRound(state, ev) + else if (ev.name == "Proposal") + Proposal(state, ev) + else if (ev.name == "ProposalAndPolkaPreviousAndValid") + ProposalAndPolkaPreviousAndValid(state, ev) + else if (ev.name == "ProposalInvalid") + ProposalInvalid(state, ev) + else if (ev.name == "PolkaAny") + PolkaAny(state, ev) + else if (ev.name == "ProposalAndPolkaAndValid") + ProposalAndPolkaAndValid(state, ev) + else if (ev.name == "PolkaNil") + PolkaNil(state, ev) + else if (ev.name == "PrecommitAny") + PrecommitAny(state, ev) + else if (ev.name == "ProposalAndCommitAndValid") + ProposalAndCommitAndValid(state, ev) + else if (ev.name == "TimeoutPropose") + TimeoutPropose (state, ev) + else if (ev.name == "TimeoutPrevote") + TimeoutPrevote (state, ev) + else if (ev.name == "TimeoutPrecommit") + TimeoutPrecommit (state, ev) + else + (state, defaultResult) +} + +/* **************************************************************************** + * Global state + * ************************************************************************* */ + +var system : Address_t -> ConsensusState +var _Result : Result +var _Event : Event + + +pure def initialProcess (name: Address_t) : ConsensusState = { + { p: name, height : 1, round: 0, step: "newRound", lockedRound: -1, lockedValue: "nil", validRound: -1, validValue: "nil"} +} + +action init = all { + system' = Map ("Josef" -> initialProcess("Josef")), + _Result' = defaultResult, + _Event' = {name : "Initial", + height : -1, + round: -1, + value: "", + vr: -1} +} + + + +// just to write a test. +action FireEvent(eventName: str, proc: Address_t, h: Height_t, r: Round_t, value: Value_t, vr: Round_t) : bool = all { + val event = {name : eventName, + height : h, + round: r, + value: value, + vr: vr} + val res = consensus(system.get(proc), event ) + all { + system' = system.put(proc, res._1), + _Result' = res._2, + _Event' = event + } +} + +action step = any { + nondet name = oneOf(consensusEvents) + nondet height = 1//oneOf(1.to(4)) + nondet round = 0//oneOf(1.to(4)) + nondet value = oneOf(Set("block 1", "block 2", "block 3")) + nondet vr = oneOf(Set(-1, 1, 2, 3, 4)) + FireEvent(name, "Josef", height, round, value, vr) +} + +action unchangedAll = all { + system' = system, + _Result' = _Result, + _Event' = _Event, +} + + +// This test should call each event at least once +run DecideNonProposerTest = { + init + .then(FireEvent("NewRound", "Josef", 1, 0, "", -1)) + .then(all{ + assert(_Result.timeout == "timeoutPropose"), + unchangedAll + }) + .then(FireEvent("Proposal", "Josef", 1, 0, "block", -1)) + .then(all{ + assert(_Result.voteMessage.step == "prevote" and _Result.voteMessage.id == "block"), + unchangedAll + }) + .then(FireEvent("ProposalAndPolkaAndValid", "Josef", 1, 0, "block", -1)) + .then(all{ + assert(_Result.voteMessage.step == "precommit" and _Result.voteMessage.id == "block"), + unchangedAll + }) + .then(FireEvent("ProposalAndCommitAndValid", "Josef", 1, 0, "block", -1)) + .then(all{ + assert(_Result.decided == "block"), + unchangedAll + }) + .then(FireEvent("NewHeight", "Josef", system.get("Josef").height + 1, 0, "", -1)) + .then(all{ + assert(system.get("Josef").height == 2), + unchangedAll + }) + .then(FireEvent("NewRoundProposer", "Josef", 2, 0, "nextBlock", -1)) + .then(all{ + assert(_Result.timeout != "timeoutPropose" and _Result.proposal.proposal == "nextBlock"), + unchangedAll + }) + .then(FireEvent("Proposal", "Josef", 2, 0, "nextBlock", -1)) // it is assumed that the proposer receives its own message + .then(all{ + assert(_Result.voteMessage.step == "prevote" and system.get("Josef").step == "prevote"), + unchangedAll + }) + .then(FireEvent("PolkaAny", "Josef", 2, 0, "nextBlock", -1)) + .then(all{ + assert(_Result.timeout == "timeoutPrevote"), + unchangedAll + }) + .then(FireEvent("TimeoutPrevote", "Josef", 2, 0, "nextBlock", -1)) + .then(all{ + assert(_Result.voteMessage.step == "precommit" and _Result.voteMessage.id == "nil" and + system.get("Josef").step == "precommit"), + unchangedAll + }) + .then(FireEvent("PrecommitAny", "Josef", 2, 0, "nextBlock", -1)) + .then(all{ + assert(_Result.timeout == "timeoutPrecommit"), + unchangedAll + }) + .then(FireEvent("TimeoutPrecommit", "Josef", 2, 0, "nextBlock", -1)) + .then(all{ + assert(_Result.skipRound == 1), + unchangedAll + }) + .then(FireEvent("NewRound", "Josef", 2, 1, "", -1)) + .then(all{ + assert(_Result.timeout == "timeoutPropose"), + unchangedAll + }) + .then(FireEvent("TimeoutPropose", "Josef", 2, 1, "nextBlock", -1)) + .then(all{ + assert(_Result.voteMessage.step == "prevote" and _Result.voteMessage.id == "nil" and + system.get("Josef").step == "prevote"), + unchangedAll + }) + .then(FireEvent("PolkaNil", "Josef", 2, 1, "nextBlock", -1)) + .then(all{ + assert(_Result.voteMessage.step == "precommit" and _Result.voteMessage.id == "nil" and + system.get("Josef").step == "precommit"), + unchangedAll + }) + .then(FireEvent("PrecommitAny", "Josef", 2, 1, "nextBlock", -1)) + .then(all{ + assert(_Result.timeout == "timeoutPrecommit"), + unchangedAll + }) + .then(FireEvent("TimeoutPrecommit", "Josef", 2, 1, "nextBlock", -1)) + .then(all{ + assert(_Result.skipRound == 2), + unchangedAll + }) + .then(FireEvent("NewRound", "Josef", 2, 2, "", -1)) + .then(all{ + assert(_Result.timeout == "timeoutPropose"), + unchangedAll + }) + .then(FireEvent("ProposalInvalid", "Josef", 2, 2, "", -1)) + .then(all{ + assert(_Result.voteMessage.step == "prevote" and _Result.voteMessage.id == "nil" and + system.get("Josef").step == "prevote"), + unchangedAll + }) +} + + +} + diff --git a/Specs/Quint/extraSpells.qnt b/Specs/Quint/extraSpells.qnt new file mode 100644 index 000000000..b0444ed17 --- /dev/null +++ b/Specs/Quint/extraSpells.qnt @@ -0,0 +1,170 @@ +// -*- mode: Bluespec; -*- +/** + * This module collects definitions that are ubiquitous. + * One day they will become the standard library of Quint. + * + * Manuel Bravo, Informal Systems, 2023 + */ +module extraSpells { + + import basicSpells.* from "./basicSpells" + + pure def printVariable(name: str, variable: a): bool = true + + pure def printMapVariables(__map: a -> b): bool = true + + /// Compute the minimum of two integers. + /// + /// - @param __i first integer + /// - @param __j second integer + /// - @returns the minimum of __i and __j + pure def min(__i: int, __j: int): int = { + if (__i > __j) __j else __i + } + + run minTest = all { + assert(min(3, 4) == 3), + assert(min(6, 3) == 3), + assert(min(10, 10) == 10), + assert(min(-3, -5) == -5), + assert(min(-5, -3) == -5), + } + + /// Compute the max of a set of integers. + /// The set must be non-empty + /// + /// - @param __set a set of integers + /// - @returns the max + pure def maxSet(__set: Set[int]): int = { + __set.fold((true, 0), (__max, __i) => if (__max._1) (false, __i) + else if (__i > __max._2) (false, __i) else (false, __max._2))._2 + } + + run maxSetTest = all { + assert(maxSet(Set(3, 4, 8)) == 8), + assert(maxSet(Set(3, 8, 4)) == 8), + assert(maxSet(Set(8, 4, 3)) == 8), + assert(maxSet(Set(8, 8, 8)) == 8), + assert(maxSet(Set(-3, -4, -8)) == -3), + assert(maxSet(Set(-3, -8, -4)) == -3), + assert(maxSet(Set(-8, -4, -3)) == -3), + } + + /// Compute the min of a set of integers. + /// The set must be non-empty + /// + /// - @param __set a set of integers + /// - @returns the min + pure def minSet(__set: Set[int]): int = { + __set.fold((true, 0), (__min, __i) => if (__min._1) (false, __i) + else if (__i < __min._2) (false, __i) else (false, __min._2))._2 + } + + run minSetTest = all { + assert(minSet(Set(3, 4, 8)) == 3), + assert(minSet(Set(3, 8, 4)) == 3), + assert(minSet(Set(8, 4, 3)) == 3), + assert(minSet(Set(8, 8, 8)) == 8), + assert(minSet(Set(-3, -4, -8)) == -8), + assert(minSet(Set(-3, -8, -4)) == -8), + assert(minSet(Set(-8, -4, -3)) == -8), + } + + /// Orders a set of integers in decreasing order. + /// + /// - @param __set a set of integers + /// - @returns a list with the integers ordered in decreasing order + pure def sortSetDecreasing(__set: Set[int]): List[int] = { + __set.fold({unordered: __set, ordered: List()}, (acc, _) => val __max = maxSet(acc.unordered) + {unordered: acc.unordered.setRemove(__max), ordered: acc.ordered.append(__max)}).ordered + } + + run sortSetDecreasingTest = all { + assert(sortSetDecreasing(Set(3, 8, 4)) == [8, 4, 3]), + assert(sortSetDecreasing(Set(8, 4, 3)) == [8, 4, 3]), + assert(sortSetDecreasing(Set()) == []), + assert(sortSetDecreasing(Set(9, -2, 5, 10)) == [10, 9, 5, -2]), + } + + /// Orders a set of integers in increasing order. + /// + /// - @param __set a set of integers + /// - @returns a list with the integers ordered in increasing order + pure def sortSetIncreasing(__set: Set[int]): List[int] = { + __set.fold({unordered: __set, ordered: List()}, (acc, _) => val __max = minSet(acc.unordered) + {unordered: acc.unordered.setRemove(__max), ordered: acc.ordered.append(__max)}).ordered + } + + run sortSetIncreasingTest = all { + assert(sortSetIncreasing(Set(3, 8, 4)) == [3, 4, 8]), + assert(sortSetIncreasing(Set(8, 4, 3)) == [3, 4, 8]), + assert(sortSetIncreasing(Set()) == []), + assert(sortSetIncreasing(Set(9, -2, 5, 10)) == [-2, 5, 9, 10]), + } + + /// Add a set element. + /// - @param __set a set to add an element to + /// - @param __elem an element to add + /// - @returns a new set that contains all elements of __set and __elem + pure def setAdd(__set: Set[a], __elem: a): Set[a] = { + __set.union(Set(__elem)) + } + + run setAddTest = all { + assert(Set(2, 3, 4, 5) == Set(2, 3, 4).setAdd(5)), + assert(Set(3) == Set(3).setAdd(3)), + } + + /// Safely set a map entry. + /// + /// - @param __map a map to set an entry to + /// - @param __key the key of an entry to set + /// - @returns a new map that contains all entries of __map + /// and an entry for the key __key + pure def mapSafeSet(__map: a -> b, __key: a, __value: b): a -> b = { + if (__map.has(__key)) { + __map.set(__key, __value) + } else { + __map.keys().union(Set(__key)).mapBy(__k => if (__k == __key) __value else __map.get(__k)) + } + } + + run mapSafeSetTest = all { + assert(Map(2 -> 3, 4 -> 5).mapSafeSet(1, 7) == Map(1 -> 7, 2 -> 3, 4 -> 5)), + assert(Map(2 -> 3, 4 -> 5).mapSafeSet(2, 7) == Map(2 -> 7, 4 -> 5)) + } + + /// Remove a set of map entries. + /// + /// - @param __map a map to remove the set of entries from + /// - @param __keys the set of keys to remove + /// - @returns a new map that contains all entries of __map + /// but the ones in __keys + pure def mapRemoveSet(__map: a -> b, __keys: Set[a]): a -> b = { + __map.keys().exclude(__keys).mapBy(__k => __map.get(__k)) + } + + run mapRemoveSetTest = all { + assert(Map(2 -> 3, 4 -> 5, 7 -> 8).mapRemoveSet(Set(2, 7)) == Map(4 -> 5)), + assert(Map(2 -> 3, 4 -> 5, 7 -> 8).mapRemoveSet(Set(1, 7)) == Map(2 -> 3, 4 -> 5)), + assert(Map(2 -> 3, 4 -> 5, 7 -> 8).mapRemoveSet(Set(1, 3)) == Map(2 -> 3, 4 -> 5, 7 -> 8)), + assert(Map(2 -> 3, 4 -> 5, 7 -> 8).mapRemoveSet(Set(2, 4, 7)) == Map()), + } + + /// Add a set of map entries to a map, add if entry exists. + /// The values must be integers + /// + /// - @param __map a map to add the set of entries to + /// - @param __entries the set of new entries to add + /// - @returns a new map that contains all entries of __map + /// plus the ones in __entries + pure def mapAddSet(__map: a -> int, __entries: a -> int): a -> int = { + __map.keys().union(__entries.keys()).mapBy(__k => if (__map.has(__k) and __entries.has(__k)) __map.get(__k) + __entries.get(__k) + else if (__map.has(__k)) __map.get(__k) else __entries.get(__k)) + } + + run mapAddSetTest = all { + assert(Map(2 -> 3, 4 -> 5, 7 -> 8).mapAddSet(Map(3 -> 6, 8 -> 9)) == Map(2 -> 3, 4 -> 5, 3 -> 6, 7 -> 8, 8 -> 9)), + assert(Map(2 -> 3, 4 -> 5, 7 -> 8).mapAddSet(Map(3 -> 6, 7 -> 9)) == Map(2 -> 3, 4 -> 5, 3 -> 6, 7 -> 17)), + } +} \ No newline at end of file diff --git a/Specs/Quint/voteBookkeeper.qnt b/Specs/Quint/voteBookkeeper.qnt new file mode 100644 index 000000000..dab0a6be2 --- /dev/null +++ b/Specs/Quint/voteBookkeeper.qnt @@ -0,0 +1,124 @@ +// -*- mode: Bluespec; -*- + +module voteBookkeeper { + + import basicSpells.* from "./basicSpells" + import extraSpells.* from "./extraSpells" + + // A round is an integer + type Round = int + + // A height is an integer + type Height = int + + // A value is a string + type Value = str + + // Weigth is an integer + type Weight = int + + type ValueWeights = Value -> Weight + + // The vote type + type Vote = { + typ: str, + round: Round, + value: Value + } + + type VoteCount = { + total: Weight, + // includes nil + valuesWeights: ValueWeights, + } + + type RoundVotes = { + height: Height, + round: Round, + prevotes: VoteCount, + precommits: VoteCount, + } + + type Threshold = { + name: str, + value: Value + } + + + type ExecutorEvent = { + name: str, + value: Value + } + + type Bookkeeper = { + height: Height, + totalWeight: int, + rounds: Round -> RoundVotes + } + + // Internal functions + + pure def newVoteCount(total: Weight): VoteCount = { + {total: total, valuesWeights: Map()} + } + + pure def isQuorum(weight: int, total: int): bool = { + 3 * weight > 2 * total + } + + pure def addVote(voteCount: VoteCount, vote: Vote, weight: Weight): {voteCount: VoteCount, threshold: Threshold} = { + val value = vote.value + val total = voteCount.total + val newWeight = voteCount.valuesWeights.getOrElse(value, 0) + weight + val updatedVoteCount = voteCount.with("valuesWeights", voteCount.valuesWeights.mapSafeSet(value, newWeight)) + val sumWeight = updatedVoteCount.valuesWeights.keys().fold(0, (sum, v) => sum + updatedVoteCount.valuesWeights.get(v)) + val threshold = if (value != "nil" and isQuorum(newWeight, total)) {name: "Value", value: value} + else if (value == "nil" and isQuorum(newWeight, total)) {name: "Nil", value: "null"} + else if (isQuorum(sumWeight, total)) {name: "Any", value: "null"} + else {name: "Unreached", value: "null"} + {voteCount: updatedVoteCount, threshold: threshold} + } + + pure def checkThresholdCount(voteCount: VoteCount, threshold: Threshold): bool = { + val total = voteCount.total + val sumWeight = voteCount.valuesWeights.keys().fold(0, (sum, v) => sum + voteCount.valuesWeights.get(v)) + if (threshold.name == "Value") isQuorum(voteCount.valuesWeights.getOrElse(threshold.value, 0), total) + else if (threshold.name == "Nil") isQuorum(voteCount.valuesWeights.getOrElse("nil", 0), total) + else if (threshold.name == "Any") isQuorum(sumWeight, total) + else false + } + + pure def toEvent(voteType: str, threshold: Threshold): ExecutorEvent = { + if (threshold.name == "Unreached") {name: "None", value: "null"} + else if (voteType == "Prevote" and threshold.name == "Value") {name: "PolkaValue", value: threshold.value} + else if (voteType == "Prevote" and threshold.name == "Nil") {name: "PolkaNil", value: "null"} + else if (voteType == "Prevote" and threshold.name == "Any") {name: "PolkaAny", value: "null"} + else if (voteType == "Precommit" and threshold.name == "Value") {name: "PrecommitValue", value: threshold.value} + else if (voteType == "Precommit" and threshold.name == "Any") {name: "PrecommitAny", value: "null"} + else {name: "None", value: "null"} + } + + // Executor interface + + pure def applyVote(keeper: Bookkeeper, vote: Vote, weight: int): {bookkeper: Bookkeeper, event: ExecutorEvent} = { + val height = keeper.height + val total = keeper.totalWeight + val roundVotes = keeper.rounds.getOrElse(vote.round, {height: height, round: vote.round, prevotes: newVoteCount(total), precommits: newVoteCount(total)}) + val resultAddVote = if (vote.typ == "Prevote") roundVotes.prevotes.addVote(vote, weight) + else roundVotes.precommits.addVote(vote, weight) + val updatedRoundVotes = if (vote.typ == "Prevote") roundVotes.with("prevotes", resultAddVote.voteCount) + else roundVotes.with("precommits", resultAddVote.voteCount) + val updatedBookkeeper = keeper.with("rounds", keeper.rounds.mapSafeSet(vote.round, updatedRoundVotes)) + {bookkeper: updatedBookkeeper, event: toEvent(vote.typ, resultAddVote.threshold)} + } + + pure def checkThreshold(keeper: Bookkeeper, round: Round, voteType: str, threshold: Threshold): bool = { + if (keeper.rounds.has(round)) { + val roundVotes = keeper.rounds.get(round) + val voteCount = if (voteType == "Prevote") roundVotes.prevotes + else roundVotes.precommits + checkThresholdCount(voteCount, threshold) + } else false + } + +} \ No newline at end of file diff --git a/codecov.yml b/codecov.yml index 97d7c517a..4a394a747 100644 --- a/codecov.yml +++ b/codecov.yml @@ -21,4 +21,6 @@ coverage: paths: - "Code" - changes: true + changes: + default: + informational: true