Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vote accumulation and round-estimate logic #2

Merged
merged 11 commits into from
Aug 24, 2018
43 changes: 36 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,40 @@

//! Finality gadget for blockchains.
//!
//! https://hackmd.io/svMTltnGQsSR1GCjRKOPbw
//! https://hackmd.io/iA4XazxWRJ21LqMxwPSEZg?view

mod round;
mod vote_graph;

#[cfg(test)]
mod testing;

use std::fmt;

/// A prevote for a block and its ancestors.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Prevote<H> {
round: u64,
target: H,
weight: usize,
target_hash: H,
target_number: u32,
}

impl<H> Prevote<H> {
pub fn new(target_hash: H, target_number: u32) -> Self {
Prevote { target_hash, target_number }
}
}

/// A precommit for a block and its ancestors.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Precommit<H> {
round: u64,
target: H,
weight: usize,
target_hash: H,
target_number: u32,
}

impl<H> Precommit<H> {
pub fn new(target_hash: H, target_number: u32) -> Self {
Precommit { target_hash, target_number }
}
}

#[derive(Clone, PartialEq, Debug)]
Expand Down Expand Up @@ -65,3 +81,16 @@ pub trait Chain<H> {
/// If the block is not a descendent of `base`, returns an error.
fn ancestry(&self, base: H, block: H) -> Result<Vec<H>, Error>;
}

/// An equivocation (double-vote) in a given round.
#[derive(Debug, Clone, PartialEq)]
pub struct Equivocation<Id, V, S> {
/// The round number equivocated in.
pub round_number: u64,
/// The identity of the equivocator.
pub identity: Id,
/// The first vote in the equivocation.
pub first: (V, S),
/// The second vote in the equivocation.
pub second: (V, S),
}
327 changes: 327 additions & 0 deletions src/round.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
// Copyright 2018 Parity Technologies (UK) Ltd.
// This file is part of finality-afg.

// finality-afg is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// finality-afg is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with finality-afg. If not, see <http://www.gnu.org/licenses/>.

//! Logic for a single round of AfG.

use vote_graph::VoteGraph;

use std::collections::hash_map::{HashMap, Entry};
use std::hash::Hash;
use std::ops::AddAssign;

use super::{Equivocation, Prevote, Precommit, Chain};

#[derive(Hash, Eq, PartialEq)]
struct Address;

#[derive(Default, Debug, Clone)]
struct VoteCount {
prevote: usize,
precommit: usize,
}

impl AddAssign for VoteCount {
fn add_assign(&mut self, rhs: VoteCount) {
self.prevote += rhs.prevote;
self.precommit += rhs.precommit;
}
}

struct VoteTracker<Id: Hash + Eq, Vote, Signature> {
votes: HashMap<Id, (Vote, Signature)>,
current_weight: usize,
}

impl<Id: Hash + Eq + Clone, Vote: Clone + Eq, Signature: Clone> VoteTracker<Id, Vote, Signature> {
fn new() -> Self {
VoteTracker {
votes: HashMap::new(),
current_weight: 0,
}
}

// track a vote. if the vote is an equivocation, returns a proof-of-equivocation and
// otherwise notes the current amount of weight on the tracked vote-set.
//
// since this struct doesn't track the round number of votes, that must be set
// by the caller.
fn add_vote(&mut self, id: Id, vote: Vote, signature: Signature, weight: usize)
-> Result<(), Equivocation<Id, Vote, Signature>>
{
match self.votes.entry(id.clone()) {
Entry::Vacant(mut vacant) => {
vacant.insert((vote, signature));
}
Entry::Occupied(mut occupied) => {
if occupied.get().0 != vote {
return Err(Equivocation {
round_number: 0,
identity: id,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the clone self.votes.entry(id.clone()) { above is unnecessary if you did here: identity: occupied.key().clone(). Then we'd only clone in case of of an equivocation.

first: occupied.get().clone(),
second: (vote, signature),
})
}
}
}

self.current_weight += weight;

Ok(())
}
}

/// Parameters for starting a round.
pub struct RoundParams<Id: Hash + Eq, H> {
/// The round number for votes.
pub round_number: u64,
/// Actors and weights in the round.
pub voters: HashMap<Id, usize>,
/// The base block to build on.
pub base: (H, usize),
}

#[derive(Debug)]
pub enum Error<Id, H, S> {
PrevoteEquivocation(Equivocation<Id, Prevote<H>, S>),
PrecommitEquivocation(Equivocation<Id, Precommit<H>, S>),
Chain(::Error),
}

/// Stores data for a round.
pub struct Round<Id: Hash + Eq, H: Hash + Eq, Signature> {
graph: VoteGraph<H, VoteCount>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pub struct fields should probably have some docs, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't hurt, but the fields aren't pub so my convention is not to add doc comments. in this case I will add a few because the code isn't easy to understand without

prevote: VoteTracker<Id, Prevote<H>, Signature>,
precommit: VoteTracker<Id, Precommit<H>, Signature>,
round_number: u64,
voters: HashMap<Id, usize>,
faulty_weight: usize,
total_weight: usize,
prevote_ghost: Option<(H, usize)>,
estimate: Option<(H, usize)>,
completable: bool,
}

impl<Id: Hash + Clone + Eq, H: Hash + Clone + Eq + Ord, Signature: Eq + Clone> Round<Id, H, Signature> {
/// Create a new round accumulator for given round number and with given weight.
/// Not guaranteed to work correctly unless total_weight more than 3x larger than faulty_weight
pub fn new(round_params: RoundParams<Id, H>) -> Self {
let (base_hash, base_number) = round_params.base;
let total_weight: usize = round_params.voters.values().cloned().sum();
let faulty_weight = total_weight.saturating_sub(1) / 3;

Round {
round_number: round_params.round_number,
faulty_weight: faulty_weight,
total_weight: total_weight,
voters: round_params.voters,
graph: VoteGraph::new(base_hash, base_number),
prevote: VoteTracker::new(),
precommit: VoteTracker::new(),
prevote_ghost: None,
estimate: None,
completable: false,
}
}

/// Import a prevote. Has no effect on internal state if an equivocation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • "or if signature is not by any current voters".

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also means that we need to a +self.faulty_weight in the expression for remaining_commit_votes since if we saw v vote for B_2, that doesn't rule out there being a vote for B_1 which could be used to finalise a block.

pub fn import_prevote<C: Chain<H>>(
&mut self,
chain: &C,
vote: Prevote<H>,
signer: Id,
signature: Signature,
) -> Result<(), Error<Id, H, Signature>> {
let weight = match self.voters.get(&signer) {
Some(weight) => *weight,
None => return Ok(()),
};

self.prevote.add_vote(signer, vote.clone(), signature, weight)
.map_err(|mut e| { e.round_number = self.round_number; e })
.map_err(Error::PrevoteEquivocation)?;

let vc = VoteCount {
prevote: weight,
precommit: 0,
};

self.graph.insert(vote.target_hash, vote.target_number as usize, vc, chain)
.map_err(Error::Chain)?;

// update prevote-GHOST
let threshold = self.threshold();
if self.prevote.current_weight >= threshold {
self.prevote_ghost = self.graph.find_ghost(self.prevote_ghost.take(), |v| v.prevote >= threshold);
}

self.update_estimate();
Ok(())
}

/// Import a prevote. Has no effect on internal state if an equivocation.
pub fn import_precommit<C: Chain<H>>(
&mut self,
chain: &C,
vote: Precommit<H>,
signer: Id,
signature: Signature,
) -> Result<(), Error<Id, H, Signature>> {
let weight = match self.voters.get(&signer) {
Some(weight) => *weight,
None => return Ok(()),
};

self.precommit.add_vote(signer, vote.clone(), signature, weight)
.map_err(|mut e| { e.round_number = self.round_number; e })
.map_err(Error::PrecommitEquivocation)?;

let vc = VoteCount {
prevote: 0,
precommit: weight,
};

self.graph.insert(vote.target_hash, vote.target_number as usize, vc, chain)
.map_err(Error::Chain)?;

self.update_estimate();
Ok(())
}

// update the round-estimate and whether the round is completable.
fn update_estimate(&mut self) {
let threshold = self.threshold();
if self.prevote.current_weight < threshold { return }

let remaining_commit_votes = self.total_weight - self.precommit.current_weight;
let (g_hash, g_num) = match self.prevote_ghost.clone() {
None => return,
Some(x) => x,
};

self.estimate = self.graph.find_ancestor(
g_hash.clone(),
g_num,
|vote| vote.precommit + remaining_commit_votes >= threshold,
);

self.completable = self.estimate.clone().map_or(false, |(b_hash, b_num)| {
b_hash != g_hash || {
// round-estimate is the same as the prevote-ghost.
// this round is still completable if no further blocks
// could have commit-supermajority.
let remaining_commit_votes = self.total_weight - self.precommit.current_weight;
let threshold = self.threshold();

// when the remaining votes are at least the threshold,
// we can always have commit-supermajority.
//
// once it's below that level, we only need to consider already
// blocks referenced in the graph, because no new leaf nodes
// could ever have enough commits.
remaining_commit_votes < threshold &&
self.graph.find_ghost(Some((b_hash, b_num)), |count|
count.precommit + remaining_commit_votes >= threshold
).map_or(true, |x| x == (g_hash, g_num))
}
})
}

/// Fetch the "round-estimate": the best block which might have been finalized
/// in this round.
///
/// Returns `None` when new new blocks could have been finalized in this round,
/// according to our estimate.
pub fn estimate(&self) -> Option<&(H, usize)> {
self.estimate.as_ref()
}

/// Returns `true` when the round is completable.
///
/// This is the case when the round-estimate is an ancestor of the prevote-ghost head,
/// or when they are the same block _and_ none of its children could possibly have
/// enough precommits.
pub fn completable(&self) -> bool {
self.completable
}

// Threshold number of weight for supermajority.
pub fn threshold(&self) -> usize {
threshold(self.total_weight, self.faulty_weight)
}
}

fn threshold(total_weight: usize, faulty_weight: usize) -> usize {
let mut double_supermajority = total_weight + faulty_weight + 1;
double_supermajority += double_supermajority & 1;
double_supermajority / 2
}

#[cfg(test)]
mod tests {
use super::*;
use testing::{GENESIS_HASH, DummyChain};

fn voters() -> HashMap<&'static str, usize> {
[
("Alice", 5),
("Bob", 7),
("Eve", 3),
].iter().cloned().collect()
}

#[derive(PartialEq, Eq, Hash, Clone, Debug)]
struct Signature(&'static str);

#[test]
fn threshold_is_right() {
assert_eq!(threshold(10, 3), 7);
assert_eq!(threshold(100, 33), 67);
assert_eq!(threshold(101, 33), 68);
assert_eq!(threshold(102, 33), 68);
}

#[test]
fn estimate_is_valid() {
let mut chain = DummyChain::new();
chain.push_blocks(GENESIS_HASH, &["A", "B", "C", "D", "E", "F"]);
chain.push_blocks("E", &["EA", "EB", "EC", "ED"]);
chain.push_blocks("F", &["FA", "FB", "FC"]);

let mut round = Round::new(RoundParams {
round_number: 1,
voters: voters(),
base: ("C", 4),
});

round.import_prevote(
&chain,
Prevote::new("FC", 10),
"Alice",
Signature("Alice"),
).unwrap();

round.import_prevote(
&chain,
Prevote::new("ED", 10),
"Bob",
Signature("Bob"),
).unwrap();

assert_eq!(round.prevote_ghost, Some(("E", 6)));
assert_eq!(round.estimate(), Some(&("E", 6)));
assert!(!round.completable());
}
}
Loading