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 against merkle_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 donor mcp_consensus_votes stub 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 to ACCEPTED.
  • 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 critical offense (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 f is 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


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.