Advanced Consensus Mechanisms

Implementing advanced consensus algorithms: PBFT, Raft, Tendermint, and Byzantine Fault Tolerance.

Advanced⏱️ 60 min📚 Prerequisites: 3

Advanced Consensus Mechanisms

Beyond Proof of Work and Proof of Stake, advanced consensus mechanisms provide different trade-offs.

Consensus Requirements

  1. Safety: All honest nodes agree on the same value
  2. Liveness: The system eventually makes progress
  3. Fault Tolerance: Works despite some nodes failing or being malicious

Byzantine Fault Tolerance (BFT)

BFT systems tolerate Byzantine failures - nodes that can behave arbitrarily (lie, crash, etc.).

PBFT (Practical Byzantine Fault Tolerance)

  • Assumption: At most f Byzantine nodes out of 3f+1 total
  • Phases: Pre-prepare, Prepare, Commit
  • Finality: Immediate (no waiting for confirmations)
  • Use Cases: Permissioned blockchains, enterprise

Tendermint Consensus

  • Voting Rounds: Validators vote on blocks
  • Lock Mechanism: Prevents double-spending
  • Finality: Blocks are final once committed
  • Use Cases: Cosmos, Binance Chain

Raft Consensus

  • Leader Election: One leader coordinates
  • Log Replication: Leader replicates log to followers
  • Crash Fault Tolerant: Doesn't handle Byzantine faults
  • Use Cases: Distributed databases, private blockchains

PBFT Implementation

RUST
enum PBFTPhase {
    PrePrepare,
    Prepare,
    Commit,
}

struct PBFTMessage {
    phase: PBFTPhase,
    view: u64,
    sequence: u64,
    block_hash: String,
    sender: String,
    signature: Vec<u8>,
}

struct PBFTNode {
    id: String,
    view: u64,
    prepared: HashMap<u64, HashSet<String>>, // sequence -> set of prepare messages
    committed: HashSet<u64>,
}

impl PBFTNode {
    fn pre_prepare(&mut self, block_hash: String, sequence: u64) -> PBFTMessage {
        // Leader sends pre-prepare
        PBFTMessage {
            phase: PBFTPhase::PrePrepare,
            view: self.view,
            sequence,
            block_hash,
            sender: self.id.clone(),
            signature: vec![],
        }
    }
    
    fn prepare(&mut self, msg: &PBFTMessage) -> Option<PBFTMessage> {
        // Node sends prepare after receiving pre-prepare
        // Need 2f prepares to move to commit
        let prepares = self.prepared.entry(msg.sequence).or_insert_with(HashSet::new);
        prepares.insert(msg.sender.clone());
        
        if prepares.len() >= 2 {
            // Have enough prepares
            Some(PBFTMessage {
                phase: PBFTPhase::Prepare,
                view: self.view,
                sequence: msg.sequence,
                block_hash: msg.block_hash.clone(),
                sender: self.id.clone(),
                signature: vec![],
            })
        } else {
            None
        }
    }
    
    fn commit(&mut self, msg: &PBFTMessage) -> bool {
        // Need 2f+1 commits to finalize
        if msg.phase == PBFTPhase::Commit {
            self.committed.insert(msg.sequence);
            self.committed.len() >= 3 // 2f+1 for f=1
        } else {
            false
        }
    }
}

Tendermint Consensus

RUST
enum TendermintStep {
    Propose,
    Prevote,
    Precommit,
    Commit,
}

struct TendermintVote {
    step: TendermintStep,
    height: u64,
    round: u64,
    block_hash: Option<String>,
    validator: String,
}

struct TendermintNode {
    height: u64,
    round: u64,
    locked_block: Option<String>,
    locked_round: Option<u64>,
    valid_round: Option<u64>,
}

impl TendermintNode {
    fn propose(&mut self, block_hash: String) -> TendermintVote {
        TendermintVote {
            step: TendermintStep::Propose,
            height: self.height,
            round: self.round,
            block_hash: Some(block_hash),
            validator: String::from("validator1"),
        }
    }
    
    fn prevote(&mut self, block_hash: &str) -> TendermintVote {
        // Lock on block if valid
        if self.is_valid_block(block_hash) {
            self.locked_block = Some(block_hash.to_string());
            self.locked_round = Some(self.round);
        }
        
        TendermintVote {
            step: TendermintStep::Prevote,
            height: self.height,
            round: self.round,
            block_hash: Some(block_hash.to_string()),
            validator: String::from("validator1"),
        }
    }
    
    fn is_valid_block(&self, _block_hash: &str) -> bool {
        // Validate block
        true
    }
}

Comparison

MechanismFault ToleranceFinalityThroughputUse Case
PoW50% honestProbabilisticLowPublic, permissionless
PoS33% honestProbabilisticMediumPublic, permissionless
PBFT33% ByzantineImmediateHighPermissioned
Tendermint33% ByzantineImmediateHighPublic, permissioned
Raft50% crashImmediateVery HighPrivate

Choosing a Consensus

  • Public, Permissionless: PoW or PoS
  • High Throughput Needed: PBFT or Tendermint
  • Immediate Finality: BFT-based (PBFT, Tendermint)
  • Private Network: Raft or PBFT
  • Energy Efficiency: PoS or BFT

Real-World Examples

  • Hyperledger Fabric: Uses PBFT
  • Cosmos: Uses Tendermint
  • Ethereum 2.0: Uses PoS with BFT finality
  • Solana: Uses Proof of History + PoS

Code Examples

Simplified PBFT

Basic PBFT consensus implementation

RUST
use std::collections::{HashMap, HashSet};

enum Phase {
    PrePrepare,
    Prepare,
    Commit,
}

struct Message {
    phase: Phase,
    view: u64,
    sequence: u64,
    block_hash: String,
    sender: String,
}

struct PBFTNode {
    id: String,
    view: u64,
    prepares: HashMap<u64, HashSet<String>>,
    commits: HashMap<u64, HashSet<String>>,
    total_nodes: usize,
}

impl PBFTNode {
    fn new(id: String, total_nodes: usize) -> Self {
        PBFTNode {
            id,
            view: 0,
            prepares: HashMap::new(),
            commits: HashMap::new(),
            total_nodes,
        }
    }
    
    fn receive_prepare(&mut self, msg: &Message) -> Option<Message> {
        let prepares = self.prepares.entry(msg.sequence).or_insert_with(HashSet::new);
        prepares.insert(msg.sender.clone());
        
        // Need 2f prepares (where f = (total_nodes - 1) / 3)
        let f = (self.total_nodes - 1) / 3;
        let required = 2 * f;
        
        if prepares.len() >= required && !self.commits.contains_key(&msg.sequence) {
            // Send commit
            Some(Message {
                phase: Phase::Commit,
                view: self.view,
                sequence: msg.sequence,
                block_hash: msg.block_hash.clone(),
                sender: self.id.clone(),
            })
        } else {
            None
        }
    }
    
    fn receive_commit(&mut self, msg: &Message) -> bool {
        let commits = self.commits.entry(msg.sequence).or_insert_with(HashSet::new);
        commits.insert(msg.sender.clone());
        
        // Need 2f+1 commits to finalize
        let f = (self.total_nodes - 1) / 3;
        let required = 2 * f + 1;
        
        commits.len() >= required
    }
}

fn main() {
    // 4 nodes: can tolerate 1 Byzantine fault (f=1)
    let mut node1 = PBFTNode::new(String::from("node1"), 4);
    
    // Simulate receiving prepares
    let prepare_msg = Message {
        phase: Phase::Prepare,
        view: 0,
        sequence: 1,
        block_hash: String::from("block_hash_123"),
        sender: String::from("node2"),
    };
    
    // Receive 2 prepares (need 2f = 2 for f=1)
    node1.receive_prepare(&prepare_msg);
    let mut prepare2 = prepare_msg;
    prepare2.sender = String::from("node3");
    
    if let Some(commit_msg) = node1.receive_prepare(&prepare2) {
        println!("Node1 sending commit for sequence {}", commit_msg.sequence);
        
        // Receive commits
        let mut commit1 = commit_msg.clone();
        commit1.sender = String::from("node2");
        let mut commit2 = commit_msg.clone();
        commit2.sender = String::from("node3");
        
        if node1.receive_commit(&commit_msg) {
            println!("Block finalized!");
        }
        if node1.receive_commit(&commit1) {
            println!("Block finalized!");
        }
        if node1.receive_commit(&commit2) {
            println!("Block finalized!");
        }
    }
}

Explanation:

PBFT requires 3 phases: pre-prepare (leader proposes), prepare (nodes agree), and commit (nodes finalize). With 4 nodes (f=1), we need 2 prepares and 3 commits to finalize a block. This provides immediate finality unlike PoW/PoS.

Tendermint Voting

Tendermint consensus voting mechanism

RUST
enum VoteType {
    Prevote,
    Precommit,
}

struct Vote {
    vote_type: VoteType,
    height: u64,
    round: u64,
    block_hash: Option<String>,
    validator: String,
}

struct TendermintValidator {
    id: String,
    height: u64,
    round: u64,
    prevotes: Vec<Vote>,
    precommits: Vec<Vote>,
    locked_block: Option<String>,
}

impl TendermintValidator {
    fn new(id: String) -> Self {
        TendermintValidator {
            id,
            height: 1,
            round: 0,
            prevotes: Vec::new(),
            precommits: Vec::new(),
            locked_block: None,
        }
    }
    
    fn receive_prevote(&mut self, vote: Vote) -> bool {
        if vote.height == self.height && vote.round == self.round {
            self.prevotes.push(vote);
            
            // Check if we have 2/3+ prevotes for a block
            let block_votes: usize = self.prevotes
                .iter()
                .filter(|v| v.block_hash == vote.block_hash)
                .count();
            
            let total_validators = 4; // Simplified
            let required = (2 * total_validators) / 3 + 1;
            
            if block_votes >= required && vote.block_hash.is_some() {
                // Lock on this block
                self.locked_block = vote.block_hash.clone();
                return true;
            }
        }
        false
    }
    
    fn can_precommit(&self) -> bool {
        self.locked_block.is_some()
    }
    
    fn create_precommit(&self) -> Option<Vote> {
        if self.can_precommit() {
            Some(Vote {
                vote_type: VoteType::Precommit,
                height: self.height,
                round: self.round,
                block_hash: self.locked_block.clone(),
                validator: self.id.clone(),
            })
        } else {
            None
        }
    }
}

fn main() {
    let mut validator = TendermintValidator::new(String::from("validator1"));
    
    // Receive prevotes
    for i in 0..3 {
        let vote = Vote {
            vote_type: VoteType::Prevote,
            height: 1,
            round: 0,
            block_hash: Some(String::from("block123")),
            validator: format!("validator{}", i + 1),
        };
        validator.receive_prevote(vote);
    }
    
    if let Some(precommit) = validator.create_precommit() {
        println!("Validator can precommit for block: {:?}", precommit.block_hash);
        println!("Block is locked and ready to commit");
    }
}

Explanation:

Tendermint uses voting rounds: propose, prevote, precommit. Validators lock on a block after receiving 2/3+ prevotes. Once locked, they can precommit. With 2/3+ precommits, the block is committed. This provides immediate finality.

Exercises

Simple BFT Voting

Create a simple BFT voting system!

Hard

Starter Code:

RUST
struct Vote {
    block_hash: String,
    voter: String,
}

fn main() {
    // Collect votes
    // Check if we have 2/3+ votes
}