DOCS / PROTOCOL / HOTSTUFF-2

HotStuff-2

HotStuff-2 is the BFT consensus protocol that gives ShardoChain its safety property — no two finalized blocks at the same height — and its liveness under fault — the chain advances as long as fewer than f = ⌊(N-1)/3⌋ validators are Byzantine. Affianca PoH (clock) without replacing it.

Three pillars

PillarRole
Vote + QC2f+1 signatures over (slot, block_hash, view) form a Quorum Certificate. Embedded in the next header's justify_qc.
View-changeSingle-round leader rotation when the default leader fails to ship a block within the deadline.
Lock-on-QCOnce a validator votes for a block, future view-change leaders must re-propose the same block. Cannot rewrite.

Quorum sizes

NfQuorum
413
615
7 (mainnet target)25
1037
21614

Header v4 wire format

struct BlockHeader {
    version: u32,                            // = 4 since v5.1.1
    height: u64,
    previous_hash: Hash256,
    state_root: Hash256,
    transactions_root: Hash256,
    receipts_root: Hash256,
    validator: Address,
    validator_signature: [u8; 64],
    slot: u64,                               // PoH-derived
    poh_hash: [u8; 32],
    poh_sequence: u64,
    justify_qc: Option<QuorumCertificate>, // parent's QC
    // v4 ADDITIONS
    view: u64,                               // 0 = default leader
    new_view_qc: Option<NewViewQC>,    // present iff view > 0
}

Normal path (view = 0)

slot S starts
    ↓
each validator computes leader_for(active_set, height=tip+1, view=0)
    ↓
IF leader == self:
    produce block H with view=0 + parent_qc
    gossip GossipMessage::NewBlock
ELSE:
    wait NewBlock(H) within view_timeout_ms
    ↓
on NewBlock(H) received:
    validate (parent QC, signature, state_root)
    import H
    emit PohVote(slot, hash(H), view=0) signed
    gossip
    ↓
on 2f+1 PohVotes accumulated for (slot, hash(H)):
    construct QuorumCertificate(H)
    embed in header(H+1).justify_qc

Typical timing on testnet (slot 700 ms, cross-DC RTT 30-80 ms):

Finality ~1 slot in steady-state.

View-change algorithm

If the default leader fails to ship a block within view_timeout_ms (default 800 ms = 1 slot, mainnet target 2100 ms = 3 slots):

  1. Each validator increments local view.
  2. Signs (height, current_view, locked) and broadcasts GossipMessage::NewView.
  3. On receiving a NewView at V' > local_view: jump local view to V' (mirror).
  4. Records the signature in NewViewCollector::pool[(H, V')].
  5. When pool[(H, V')].len() ≥ 2f+1: build NewViewQC<H, V'>.
  6. If self is leader for (H, V'): produce block with view = V' and new_view_qc = Some(qc).

Mirror peer views (v5.1.0)

Pre-fix, each node emitted NewView at local view + 1 on deadline → every node landed in different (H, V) buckets → 2f+1 distinct signers never accumulated at the same view.

Fix (commit 72cff9c): the producer reads NewViewCollector::max_view_for(H) — the highest view observed via gossip — and mirrors it instead of incrementing. All nodes converge at the highest view.

Lock-on-QC (v5.1.1)

Once a validator imports a block at view ≥ 1 with a valid new_view_qc, it stores LockedBlock { view, block_hash } at that height. The leader of the next view (V'+1) must re-propose the locked block by hash, not produce a competing one.

This prevents a malicious leader at V' > V from rewriting history. Every honest signer's NewView signature commits to its lock — the leader cannot strip it without invalidating the signature.

Accept-gate (v5.1.5)

NewViewCollector::record(...) drops signatures whose height exceeds local_tip + 32. Without this gate, a stranded validator broadcasting NewView at tip+194 would pollute the pool's prune-cutoff and evict valid entries at our own pending height. Closes a wedge observed during the chain_id=586 soak.

The window is configurable via chain_config.new_view_accept_ahead (default 32).

Recovery paths

Hardening (v5.1.8)

Differences from the academic paper