Next Generation Consensus Algorithm - BFT on DAG
/ 3 min read
Table of Contents
Next Generation Consensus Algorithm - BFT on DAG
Narwhal and Tusk: A DAG-based Mempool and Efficient BFT Consensus
2. Bullshark
Bullshark: DAG BFT Protocols Made Practical
Mysticeti: Low-Latency DAG Consensus with Fast Commit Path
PBFT

Overview
We assume a known Δ and say that an execution of a protocol is eventually synchronous if there is a global stabilization time (GST) after which all messages sent among honest parties are delivered within Δ time. An execution is synchronous if GST occurs at time 0, and asynchronous if GST never occurs.

Consensus

Worker

Life of a Transaction

Header
pub struct Header { pub author: PublicKey, pub round: Round, pub payload: BTreeMap<Digest, WorkerId>, pub parents: BTreeSet<Digest>, pub id: Digest, pub signature: Signature,}
Certificate
pub struct Certificate { pub header: Header, pub votes: Vec<(PublicKey, Signature)>,}
DAG
type Dag = HashMap<Round, HashMap<PublicKey, (Digest, Certificate)>>;struct State { /// The last committed round. last_committed_round: Round, // Keeps the last committed round for each authority. This map is used to clean up the dag and // ensure we don't commit twice the same certificate. last_committed: HashMap<PublicKey, Round>, /// Keeps the latest committed certificate (and its parents) for every authority. Anything older /// must be regularly cleaned up through the function `update`. dag: Dag,}

for leader in self.order_leaders(leader, &state).iter().rev() { // Starting from the oldest leader, flatten the sub-dag referenced by the leader. for x in self.order_dag(leader, &state) { // Update and clean up internal state. state.update(&x, self.gc_depth);
// Add the certificate to the sequence. sequence.push(x); }}/// Order the past leaders that we didn't already commit.fn order_leaders(&self, leader: &Certificate, state: &State) -> Vec<Certificate> { let mut to_commit = vec![leader.clone()]; let mut leader = leader; for r in (state.last_committed_round + 2..leader.round()) .rev() .step_by(2) { // Get the certificate proposed by the previous leader. let (_, prev_leader) = match self.leader(r, &state.dag) { Some(x) => x, None => continue, };
// Check whether there is a path between the last two leaders. if self.linked(leader, prev_leader, &state.dag) { to_commit.push(prev_leader.clone()); leader = prev_leader; } } to_commit}
/// Checks if there is a path between two leaders.fn linked(&self, leader: &Certificate, prev_leader: &Certificate, dag: &Dag) -> bool { let mut parents = vec![leader]; for r in (prev_leader.round()..leader.round()).rev() { parents = dag .get(&(r)) .expect("We should have the whole history by now") .values() .filter(|(digest, _)| parents.iter().any(|x| x.header.parents.contains(digest))) .map(|(_, certificate)| certificate) .collect(); } parents.contains(&prev_leader)}
/// Flatten the dag referenced by the input certificate. This is a classic depth-first search (pre-order):/// https://en.wikipedia.org/wiki/Tree_traversal#Pre-orderfn order_dag(&self, leader: &Certificate, state: &State) -> Vec<Certificate> { debug!("Processing sub-dag of {:?}", leader); let mut ordered = Vec::new(); let mut already_ordered = HashSet::new();
let mut buffer = vec![leader]; while let Some(x) = buffer.pop() { debug!("Sequencing {:?}", x); ordered.push(x.clone()); for parent in &x.header.parents { let (digest, certificate) = match state .dag .get(&(x.round() - 1)) .map(|x| x.values().find(|(x, _)| x == parent)) .flatten() { Some(x) => x, None => continue, // We already ordered or GC up to here. };
// We skip the certificate if we (1) already processed it or (2) we reached a round that we already // committed for this authority. let mut skip = already_ordered.contains(&digest); skip |= state .last_committed .get(&certificate.origin()) .map_or_else(|| false, |r| r == &certificate.round()); if !skip { buffer.push(certificate); already_ordered.insert(digest); } } }
// Ensure we do not commit garbage collected certificates. ordered.retain(|x| x.round() + self.gc_depth >= state.last_committed_round);
// Ordering the output by round is not really necessary but it makes the commit sequence prettier. ordered.sort_by_key(|x| x.round()); ordered }}

