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 0 reality: Colibri runs with a single writer against a WAL-mode SQLite database. θ is specified but not activated. A one-arbiter run is trivially consistent, so consensus has nothing to do. The spec below is the shape the runtime will grow into.
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). For any n, quorum + f = n + 1 - (n mod 3 == 0 ? 1 : 0) — 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.
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 0 posture
- The runtime accepts θ-shaped APIs but always returns “trivially finalized” because
n = 1. - Voting, gossip, and equivocation detection are schema-ready: columns exist, tools are stubbed.
- Schema for
mcp_consensus_votesstub:(round_id, arbiter_id, merkle_root, rule_version_hash, signed BLOB, threshold_count INT, finality_level ENUM('PENDING','SOFT','QUORUM','HARD','ABSOLUTE')). Written exclusively by the single writer; all rows havefinality_level='QUORUM'via trivial 1-of-1. - The first real θ activation is not scheduled before Phase 1.5 (the multi-model milestone in ADR-005) at earliest; BFT activation target is R121+ per
../../../5-time/roadmap.md; BFT library selection tracked separately in ADR-003.
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