Status: Stage-1 core implemented and measured, 2026-07-11. Plan §4.4, BD-3.
Crate: solidus-hotstuff2. Every number below is a local measurement with
the hardware named; nothing here is a network-throughput or audit claim.
Two-chain HotStuff (HotStuff-2/Jolteon family), consecutive-view commit:
commit(B@v) ⇔ QC(B@v) ∧ QC(B′@v+1) where
B′.parent = BandB′'s embedded justify is exactlyQC(B@v).
Replica state (in safety.rs, mirrored by tla+/HotStuff2Commit.tla):
- R1 — vote monotonicity: vote at most once per view, strictly
increasing (
last_voted_view). - R3 — lock:
high_qc= the highest-view QC ever observed. Vote for a proposal only if its justify's view ≥ the lock's view (the justify is merged into the lock first — merge-then-check). - R2 — view continuity (in
core.rs): a proposal at viewvmust carry justify from viewv−1, or a TC forv−1whose reported high-QC its justify matches-or-beats.
Suppose B commits via QC(B,v) ∧ QC(B′,v+1), B′.parent = B. Let S
be the honest signers of QC(B′,v+1) — at least quorum − f ≥ f+1 ≥ 1
replicas. Each r ∈ S merged B′'s justify (QC(B,v)) into its lock
when voting, so from that moment lock(r) ≥ v, and by R1 r never votes
again at views ≤ v+1.
Claim: every QC at view w > v certifies a block extending B. Strong
induction on w:
- Any
QC(C,w)shares an honest replicar ∈ SwithQC(B′,v+1)(quorum intersection:2q − n ≥ f+1).rvoted forConly withC.justify.view ≥ lock(r) ≥ v, soC.justify.view ∈ [v, w). - If
C.justify.view = v: per-view QC uniqueness (two same-view QCs on different blocks would requiref+1honest double-voters, impossible under R1) forcesC.justify = QC(B,v)— soCextendsB. - If
C.justify.view = u ∈ (v, w): by induction the block certified atuextendsB, andCextends it.
Hence every later-certified chain extends B; a conflicting commit would
need a QC on a non-descendant at some view > v (or a second same-view QC),
both excluded. ∎
The TLA+ spec encodes exactly these rules with Byzantine replicas voting
arbitrarily and equivocating leaders (2 blocks/view) and checks
NoConflictingCommits + QcUniquePerView. MODEL-CHECKED 2026-07-12
(TLC — exhaustive MaxView=2 complete state graph + simulation MaxView=4
124,354,945 states / 2M traces, no safety violation, with a Byzantine
replica + equivocating leaders; see tla+/TLC_RESULTS.md). Re-run TLC on
any change to the R1/R3/commit rules.
400ms base view timeout, ×2 backoff capped at 3200ms (pacemaker.rs) —
replaces the live chain's 2000→16000ms ladder. Backoff resets on QC
progress only, never on TC entry. Timeout votes re-broadcast on each
re-armed timer for the stuck view.
Timeout certificates: every timeout signer of view v signs the same
domain-separated (chain_id, v) message, so the TC aggregate remains
fast_aggregate_verify-able; the highest QC travels alongside as
independently-verifiable data (a QC self-certifies — a Byzantine TC
carrier can withhold but not forge).
The core is a pure event-driven state machine (core.rs); its action
vocabulary is the complete list of network effects: BroadcastProposal,
BroadcastQc, BroadcastTc, BroadcastTimeoutVote (gossip) and
SendVote { to } (point-to-point to the next leader). There is no way to
express a per-peer proposal send. The gossipsub transport binding lands in
solidus-p2p2; the live chain's O(N) request-response proposal path is
structurally unrepresentable here.
Proposals are proposer-signed (proposal_message domain) so leadership
cannot be impersonated; blocks carry batch-certificate digests only.
- 4-validator in-process harness (
tests/four_node.rs, tokio, real timers, loopback channels): finality p50 = 7.6ms, p99 = 11.5ms over 600 commit observations; chains byte-identical across nodes; leader-crash test: 3 live nodes keep committing through TC rotation with a crashed leader/aggregator. Loopback RTT is ~µs — these numbers are protocol+crypto overhead, not network finality. The ≤600ms p50 / ≤1500ms p99 4-node-LAN acceptance bounds are met with ~80× margin on this box; the real-LAN and geo measurements remain Stage-7 items (R-TOPOLOGY). - BLS hot path (criterion, 21-validator committee, quorum 14): sign 194µs · single-vote verify 435µs · aggregate-14 385µs · QC-verify 418µs. The per-view budget driver is vote verification (14 × 435µs ≈ 6ms if serial), not aggregation. Consequences: (a) the node layer must run vote verification off the consensus thread (§4.4) and SHOULD batch it; (b) a known optimization if it ever binds: aggregate optimistically and verify the aggregate once (~0.4ms), bisecting only on failure.
- Block sync / fetch-missing-parent (node layer; the core refuses to vote or propose over unknown parents and lets the pacemaker rotate).
- VRF-by-stake leader election (elector trait is pluggable; round-robin ships now; safety is elector-agnostic and the TLA+ spec quantifies over arbitrary proposers).
- Epoch/committee rotation from the staking tree; slashing evidence (Stage 7 closes the loop).
- Real gossipsub transport (
solidus-p2p2) andspawn_blockingplacement in the node runtime (node2).