Skip to content

Commit

Permalink
Add vote counting facility (#3)
Browse files Browse the repository at this point in the history
* Add vote counting facility, with multi-value support

* Show coverage summary in job output

* Formatting

* Comment out unused code

* Fix code coverage workflow
  • Loading branch information
romac authored Oct 19, 2023
1 parent 98b1203 commit dd0dbdc
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 5 deletions.
5 changes: 2 additions & 3 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,10 @@ jobs:
run: cargo llvm-cov nextest --all-features --workspace --lcov --output-path lcov.info
- name: Generate text report
working-directory: Code
run: cargo llvm-cov report --text
run: cargo llvm-cov report
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
working_dir: Code
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: lcov.info
files: Code/lcov.info
fail_ci_if_error: true
4 changes: 3 additions & 1 deletion Code/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ resolver = "2"

members = [
"common",
"round"
"consensus",
"round",
"vote",
]
8 changes: 8 additions & 0 deletions Code/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
pub struct Height(u64);

impl Height {
pub fn new(height: u64) -> Self {
Self(height)
}

pub fn as_u64(&self) -> u64 {
self.0
}
Expand Down Expand Up @@ -57,6 +61,10 @@ impl PartialOrd for Round {
pub struct Value(u64);

impl Value {
pub fn new(value: u64) -> Self {
Self(value)
}

pub fn as_u64(&self) -> u64 {
self.0
}
Expand Down
6 changes: 6 additions & 0 deletions Code/consensus/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[package]
name = "malachite-consensus"
version = "0.1.0"
edition = "2021"

[dependencies]
3 changes: 3 additions & 0 deletions Code/consensus/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}
2 changes: 1 addition & 1 deletion Code/round/src/state_machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ pub fn handle(state: State, round: Round, event: Event) -> Transition {
}
(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

// 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
Expand Down
7 changes: 7 additions & 0 deletions Code/vote/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "malachite-vote"
version = "0.1.0"
edition = "2021"

[dependencies]
malachite-common = { version = "0.1.0", path = "../common" }
240 changes: 240 additions & 0 deletions Code/vote/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
//! Tally votes of the same type (eg. prevote or precommit)
extern crate alloc;

use std::sync::Arc;

use alloc::collections::BTreeMap;

use malachite_common::{Height, Round, Value, Vote, VoteType};

pub type Weight = u64;

/// A value and the weight of votes for it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ValuesWeights {
value_weights: BTreeMap<Arc<Value>, Weight>,
}

impl ValuesWeights {
pub fn new() -> ValuesWeights {
ValuesWeights {
value_weights: BTreeMap::new(),
}
}

pub fn add_weight(&mut self, value: Arc<Value>, weight: Weight) -> Weight {
let entry = self.value_weights.entry(value).or_insert(0);
*entry += weight;
*entry
}

// pub fn weight_for(&self, value: &Value) -> Weight {
// self.value_weights.get(value).copied().unwrap_or(0)
// }

pub fn highest_weighted_value(&self) -> Option<(&Value, Weight)> {
self.value_weights
.iter()
.max_by_key(|(_, weight)| *weight)
.map(|(value, weight)| (value.as_ref(), *weight))
}
}

impl Default for ValuesWeights {
fn default() -> Self {
Self::new()
}
}

/// VoteCount tallys votes of the same type.
/// Votes are for nil or for some value.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct VoteCount {
// Weight of votes for nil
pub nil: Weight,
/// Weight of votes for the values
pub values_weights: ValuesWeights,
/// Total weight
pub total: Weight,
}

impl VoteCount {
pub fn new(total: Weight) -> VoteCount {
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: Vote, weight: Weight) -> Threshold {
if let Some(value) = vote.value {
let value = Arc::new(value);
let new_weight = self.values_weights.add_weight(value.clone(), weight);

// Check if we have a quorum for this value.
if is_quorum(new_weight, self.total) {
return Threshold::Value(value);
}
} else {
self.nil += weight;

// Check if we have a quorum for nil.
if is_quorum(self.nil, self.total) {
return Threshold::Nil;
}
}

// 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;
}
}

// No quorum
Threshold::Init
}
}

//-------------------------------------------------------------------------
// Round votes
//-------------------------------------------------------------------------

// Thresh represents the different quorum thresholds.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Threshold {
/// No quorum
Init, // no quorum
/// Qorum of votes but not for the same value
Any,
/// Quorum for nil
Nil,
/// Quorum for a value
Value(Arc<Value>),
}

/// Returns whether or note `value > (2/3)*total`.
pub fn is_quorum(value: Weight, total: Weight) -> bool {
3 * value > 2 * total
}

/// Tracks all the votes for a single round
pub struct RoundVotes {
pub height: Height,
pub round: Round,

pub prevotes: VoteCount,
pub precommits: VoteCount,
}

impl RoundVotes {
pub fn new(height: Height, round: Round, total: Weight) -> RoundVotes {
RoundVotes {
height,
round,
prevotes: VoteCount::new(total),
precommits: VoteCount::new(total),
}
}

pub fn add_vote(&mut self, vote: Vote, weight: Weight) -> Threshold {
match vote.typ {
VoteType::Prevote => self.prevotes.add_vote(vote, weight),
VoteType::Precommit => self.precommits.add_vote(vote, weight),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn add_votes_nil() {
let total = 3;

let mut round_votes = RoundVotes::new(Height::new(1), Round::new(0), total);

// add a vote for nil. nothing changes.
let vote = Vote::new_prevote(Round::new(0), None);
let thresh = round_votes.add_vote(vote.clone(), 1);
assert_eq!(thresh, Threshold::Init);

// add it again, nothing changes.
let thresh = round_votes.add_vote(vote.clone(), 1);
assert_eq!(thresh, Threshold::Init);

// add it again, get Nil
let thresh = round_votes.add_vote(vote.clone(), 1);
assert_eq!(thresh, Threshold::Nil);
}

#[test]
fn add_votes_single_value() {
let v = Value::new(1);
let val = Some(v.clone());
let total = 4;
let weight = 1;

let mut round_votes = RoundVotes::new(Height::new(1), Round::new(0), total);

// add a vote. nothing changes.
let vote = Vote::new_prevote(Round::new(0), val);
let thresh = round_votes.add_vote(vote.clone(), weight);
assert_eq!(thresh, Threshold::Init);

// add it again, nothing changes.
let thresh = round_votes.add_vote(vote.clone(), weight);
assert_eq!(thresh, Threshold::Init);

// add a vote for nil, get Thresh::Any
let vote_nil = Vote::new_prevote(Round::new(0), None);
let thresh = round_votes.add_vote(vote_nil, weight);
assert_eq!(thresh, Threshold::Any);

// add vote for value, get Thresh::Value
let thresh = round_votes.add_vote(vote, weight);
assert_eq!(thresh, Threshold::Value(Arc::new(v)));
}

#[test]
fn add_votes_multi_values() {
let v1 = Value::new(1);
let v2 = Value::new(2);
let val1 = Some(v1.clone());
let val2 = Some(v2.clone());
let total = 15;

let mut round_votes = RoundVotes::new(Height::new(1), Round::new(0), total);

// add a vote for v1. nothing changes.
let vote1 = Vote::new_precommit(Round::new(0), val1);
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);
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);
let thresh = round_votes.add_vote(vote_nil.clone(), 1);
assert_eq!(thresh, Threshold::Init);

// add a vote for v1. nothing changes
let thresh = round_votes.add_vote(vote1.clone(), 1);
assert_eq!(thresh, Threshold::Init);

// add a vote for v2. nothing changes
let thresh = round_votes.add_vote(vote2.clone(), 1);
assert_eq!(thresh, Threshold::Init);

// add a big vote for v2. get Value(v2)
let thresh = round_votes.add_vote(vote2.clone(), 10);
assert_eq!(thresh, Threshold::Value(Arc::new(v2)));
}
}

0 comments on commit dd0dbdc

Please sign in to comment.