Consensus (θ)
θ is how multiple arbiters agree on a single order of events when they each see a potentially different subset of the network. It is the bridge between “one process wrote something” (η’s guarantee) and “the network agrees it happened” (the legitimacy axis’s guarantee).
Phase 3 reality (R89, 2026-05-13): θ ships end-to-end. The 13 Phase 3 sub-tasks landed in R89 (PRs #234–#246), composing into a working BFT-style consensus layer: P3.1.1 5 typed vote message shapes with Ed25519 signatures + κ-canonical serialization (#234), P3.1.2 quorum + honest-majority intersection (#237), P3.1.3 commit-reveal round-state FSM with view-change wiring (#241), P3.2.1 5-level monotonic finality FSM (#238), P3.3.1 IHAVE/IWANT gossip wire with triple-anchor validation (#236), P3.3.2 per-round sized Bloom dedup (#243), P3.3.3 adaptive fanout [3,10] (#242), P3.4.1 STA broadcast + median + drift detection (#235), P3.5.1 equivocation proof verification + idempotent λ slashing (#240), P3.6.1 HMAC-SHA256 VRF stub per ADR-002 Option A (#239), P3.7.1 5 θ MCP tools growing the surface from 18 to 23 (#244), P3.8.1 4-scenario multi-arbiter parity harness (#246), and P3.9.1 ForkHookRegistry with ι-handoff stub (#245). The implementation strategy follows ADR-003 Option C — a minimal in-process BFT spike without external network transport — so this phase ships consensus as a deterministic state machine; full multi-node P2P transport is a later-phase activation. Phase 4+ extensions (libp2p transport, RFC 9381 EC-VRF, fork-aware governance integration) remain spec-only.
Authoritative spec: ../../../spec/s06-consensus.md. BFT library choice: ADR-003.
Quorum formula
For n arbiters with Byzantine tolerance f:
quorum = floor(2n / 3) + 1
f = floor((n - 1) / 3)
The system tolerates f < n / 3 adversarial or failing arbiters. Below that bound, safety (no conflicting finalized states) is preserved. Above that bound, no BFT protocol can guarantee safety; θ’s response is to halt and signal a governance event rather than proceed with a corrupted chain.
Quorum math — worked table
| n arbiters | f tolerated |
quorum required |
Notes |
|---|---|---|---|
| 4 | 1 | 3 | Minimum viable BFT set — 1 Byzantine tolerated |
| 7 | 2 | 5 | Comfortable small set; fork merge quorum (7/10 checkpoint = relaxed variant) |
| 10 | 3 | 7 | Phase-5+ fork checkpoint target (matches ι 7-of-10 signature rule) |
| 13 | 4 | 9 | Scaled arbiter committee |
The formula is quorum = floor(2n/3) + 1; the Byzantine ceiling is f = floor((n-1)/3). Together, q + f = n for all n ≥ 1, i.e. the quorum and the maximum-faulty count partition the validator set — an honest-majority invariant any two quorums must intersect in.
The five finality levels
A round moves through staged finality:
| Level | What holds |
|---|---|
PENDING |
Event received; no arbiter has voted |
SOFT |
A single arbiter has signed |
QUORUM |
quorum arbiters have signed the same root |
HARD |
Two consecutive rounds have reached QUORUM without conflicting votes |
ABSOLUTE |
Epoch-sealed: the round’s root is embedded in the next epoch’s genesis |
Downstream systems choose the level they require. A read of “current state” accepts SOFT. A cross-system integration that mutates external resources should wait for QUORUM at minimum and may require HARD for irreversible effects.
Two-gate structure (R90 clarification). QUORUM → HARD and HARD → ABSOLUTE are governed by distinct gates and should not be conflated. QUORUM → HARD fires on two consecutive rounds at QUORUM with the same (round_id, merkle_root, rule_version_hash) triple and no observed equivocation between them — this is what P3.2.1 ships in src/domains/consensus/finality.ts (AC#6 + AC#23 in docs/verification/p3-2-1-finality-verification.md). HARD → ABSOLUTE fires on a dispute window measured in epochs, with the Phase 0 default of 100n epochs (also in P3.2.1; see s06-consensus.md § Finality levels for the spec form). The spec’s prose “≥24h dispute window” maps onto whichever wall-clock duration 100 epochs covers under current scheduling — round-counted for the first gate, epoch-counted for the second.
What the arbiters vote on
Each vote is a signature over a 3-tuple: (round_id, merkle_root, rule_version_hash). Voting over all three prevents an arbiter from signing the same root under a different rule set — a class of attack the rule engine’s versioning (see rule-engine.md) makes detectable.
Voting protocol (commit-reveal pseudocode)
θ uses a two-phase commit-reveal to defeat last-mover bias: no arbiter sees another’s vote before committing its own.
# Constants (Phase 0 defaults; governable via π under rule_version_hash)
T_round = 30s # one full commit+reveal cycle
T_timeout = 2 * T_round # 60s: leader-unresponsive threshold
T_commit_phase = 10s # time to collect commitments
T_reveal_phase = 10s # time to collect reveals
# Phase A: commit
salt = random_bytes(32)
vote = sign(my_key, (round_id, merkle_root, rule_version_hash))
c = SHA-256(vote || salt)
broadcast( COMMIT(round_id, arbiter_id, c) )
wait_until commit_count >= quorum OR elapsed > T_commit_phase
# Phase B: reveal
broadcast( REVEAL(round_id, arbiter_id, vote, salt) )
wait_until reveal_count >= quorum OR elapsed > T_reveal_phase
# Phase C: verify
for each (vote_i, salt_i) received:
require SHA-256(vote_i || salt_i) == committed_c_i
require signature valid under arbiter_i public key
aggregate signatures → threshold signature σ
emit QUORUM(round_id, merkle_root, σ)
An arbiter that fails to reveal after committing is penalized as a liveness fault (softer than equivocation, handled by λ reputation decay rather than an immediate scar).
Worked example — 4-node BFT vote on event E
Setup: n = 4, f = 1, quorum = 3. Nodes: A, B, C honest; D Byzantine. Event E has merkle_root = 0xab12….
Commit phase (t=0→t=10s):
| Node | Broadcasts | Commit hash |
|---|---|---|
| A | COMMIT(round=42, A, c_A) |
c_A = SHA-256(sig_A(42, 0xab12, rv) || salt_A) |
| B | COMMIT(round=42, B, c_B) |
c_B = SHA-256(sig_B(42, 0xab12, rv) || salt_B) |
| C | COMMIT(round=42, C, c_C) |
c_C = SHA-256(sig_C(42, 0xab12, rv) || salt_C) |
| D | COMMIT(round=42, D, c_D) |
c_D = SHA-256(sig_D(42, 0xCAFE, rv) || salt_D) — divergent root |
At t=10s: 4/4 commits received — proceed to reveal.
Reveal phase (t=10s→t=20s):
- A, B, C reveal their honest votes; each verifies
SHA-256(vote || salt) == committed_c. All three check out againstmerkle_root = 0xab12…. - D reveals its vote with
merkle_root = 0xCAFE…. Hash verifies (D committed faithfully) but the root is the minority — D’s vote is isolated.
Aggregation:
- Count of matching
(merkle_root, rule_version_hash)on reveal:0xab12…= 3 (≥ quorum),0xCAFE…= 1. - Aggregate signatures from {A, B, C} into threshold signature σ.
- Finality transition:
PENDING → SOFT(at first arbiter sign) →SOFT → QUORUM(at 3rd matching signature).
Next round: if round 43 also reaches QUORUM without a conflicting reveal, round 42 transitions QUORUM → HARD. Epoch sealing later promotes it to ABSOLUTE.
D’s divergent vote: stored as a candidate equivocation trigger — if D also signed 0xab12… this round (double-commitment on the same (round_id, finality_level)), it becomes an equivocation proof (see below).
Equivocation
An arbiter that signs two distinct tuples at the same finality level in the same round is equivocating. θ detects this deterministically: any two conflicting signatures by the same key constitute a proof-of-fault. The λ reputation engine (see ../../social/reputation.md) applies a hard penalty; the κ rule engine may cap the arbiter’s future voting weight or suspend them via the π governance process.
Equivocation proof structure
{
"type": "equivocation",
"attacker_id": "soul:<sha256-of-pubkey>",
"epoch": 17,
"round_id": 42,
"signed_vote_a": {
"tuple": [42, "0xab12…", "rv:sha256:…"],
"signature": "<ed25519-sig-A>"
},
"signed_vote_b": {
"tuple": [42, "0xCAFE…", "rv:sha256:…"],
"signature": "<ed25519-sig-B>"
},
"submitter": "soul:<who-observed-it>",
"evidence_hash": "<sha256-over-the-two-signed-votes>"
}
Verification is pure and local: re-check both signatures against the attacker’s published public key; both valid + distinct tuples + same (round_id, finality_level) ⇒ provable fault. Penalty application is governed by s04-reputation (hard scar on arbitration domain, weight −10; see ../../social/reputation.md) and escalated to π for potential suspension.
View-change procedure
If the current round’s leader (the arbiter responsible for proposing the block) fails to emit a proposal within T_timeout = 2 * T_round (default 60s), any participating arbiter initiates a view change:
broadcast( VIEW_CHANGE(round_id, current_leader, reason="timeout") )
collect VIEW_CHANGE messages from peers
if view_change_count >= quorum:
next_leader = VRF(prev_merkle_root, round_id) # see ADR-002
proceed round_id with leader = next_leader
The VRF seed is the previous round’s Merkle root, making leader selection deterministic-given-history but unpredictable-in-advance — an arbiter cannot campaign to be the next leader. Reasons recognized by the view-change flow: timeout, equivocation_observed, malformed_proposal. A view change emits a VIEW_CHANGE_ACCEPTED event into ζ (decision trail) so later audits can reconstruct why leadership rotated mid-round.
Gossip
Arbiters share signed messages via a gossip protocol whose topology is undirected, with bounded fan-out. The spec (not implemented in Phase 0) requires:
- Each message is signed by its sender.
- Duplicate messages are dropped on receipt (deduplication by content hash).
- A message older than the retention horizon is dropped without reply.
- Network partitions resolve by longest-chain rule only if the tip satisfies the rule-version gate (see
rule-engine.md).
Gossip message envelope
All gossip messages share a common envelope:
{
"msg_type": "COMMIT" | "REVEAL" | "VIEW_CHANGE" | "EQUIVOCATION_PROOF",
"round_id": <uint64>,
"sender_id": "soul:<sha256-of-pubkey>",
"payload": { ... msg-specific ... },
"timestamp_logical": <uint64>, # Lamport clock, not wall-clock
"signature": "<ed25519-sig-over-canonical-serialization>"
}
Wall-clock is never part of the signed payload (κ forbids clock reads in rule bodies; θ inherits the constraint). Ordering uses logical clocks plus the round_id. Retention horizon default: 2 epochs — beyond that the message is evidence-only, not consensus-relevant.
Interaction with ι (fork)
When θ cannot reach quorum — either because an arbiter partition lasts too long or because the vote space genuinely splits — the engine creates a fork via ι. See state-fork.md. A fork is not a failure of consensus; it is consensus’s graceful mode for “we do not yet agree.” Forks reconcile when a later epoch embeds both roots in its genesis and one branch becomes canonical.
Phase 3 posture
- The runtime now ships a deterministic BFT-style consensus state machine. 13 Phase 3 sub-tasks landed in R89 (#234–#246); see the Phase 3 reality paragraph above for the per-PR mapping.
- 5 MCP tools register the θ surface:
consensus_propose,consensus_vote,consensus_finality,consensus_gossip,vrf_eval. The total MCP surface is now 23 tools (was 18 post-R89 Phase A). - The vote tables (
consensus_round,consensus_vote,consensus_equivocation_proof) replace the donormcp_consensus_votesstub and are populated by the live message flow. - VRF is the HMAC-SHA256 stub per ADR-002 Option A — internally verifiable, not RFC 9381 EC-VRF. ADR status remains
PROPOSED; a separate ADR-acceptance PR is required to move it toACCEPTED. - BFT-layer strategy is ADR-003 Option C — a minimal in-process spike without libp2p or external transport. Multi-node P2P activation is a later-phase milestone tracked in
../../../5-time/roadmap.md. - Equivocation enforcement is wired through λ reputation (#240): a verified equivocation proof routes to the P2.2.2 penalty surface as a
criticaloffense (8000 bps loss), idempotent on proof-hash dedup. - Phase 4+ remaining: libp2p network transport, RFC 9381 EC-VRF migration, π-governed BFT parameter updates, fork-aware ι handoff (#245 shipped the registry; consumer-side ι integration is a Phase 5 milestone).
What θ is not
- Not an ordering engine for free. θ orders what its arbiters see. If an event never reaches an arbiter, θ cannot order it.
- Not a liveness guarantee under adversity. BFT protocols sacrifice liveness for safety when
fis exceeded; θ halts rather than proceeds. - Not a replacement for the audit chain. θ signs roots; it does not produce them. The roots come from η over ζ.
See also
proof-store.md— η, whose Merkle root is the subject of θ’s vote; also provides the threshold-signing primitive consensus aggregates into σstate-fork.md— ι, the graceful divergence path when θ splitsrule-engine.md— κ, whoserule_version_hashis part of the vote tuple../../social/reputation.md— λ, which penalizes equivocation../enforcement/governance.md— π, which may suspend a faulting arbiter../../../spec/s06-consensus.md— authoritative θ spec../../../spec/s08-gossip.md— authoritative gossip-protocol spec../../../spec/s09-arbitration.md— arbiter selection + equivocation response../../../architecture/decisions/ADR-003-bft-library.md— BFT library choice