P3.1.2 — Quorum Computation — Audit
Step 1 of the 5-step chain (CLAUDE.md §6). Phase 3 θ Wave 2 — the BFT quorum primitive every downstream θ slice consumes (finality SM, view-change, equivocation, governance).
§1. Surface inventory at base SHA e63a8bcf
| Path | Exists? | Role |
|---|---|---|
src/domains/consensus/ |
Yes (P3.1.1, e63a8bcf) |
Existing θ domain directory; only houses messages.ts today |
src/domains/consensus/messages.ts |
Yes (P3.1.1) | REUSE — Vote type, hashMessage() for canonical-tuple grouping |
src/domains/consensus/quorum.ts |
No — to create | Pure BFT-quorum module |
src/__tests__/domains/consensus/quorum.test.ts |
No — to create | Worked-table fixture + property tests + n=1 single-arbiter |
src/__tests__/domains/consensus/messages.test.ts |
Yes (P3.1.1) | Reference for module-corpus pattern |
src/domains/rules/canonical.ts |
Yes (κ P1.5.4) | Transitive — messages.ts already wraps it |
src/domains/rules/integer-math.ts |
Yes (κ P1.1.1) | Reference for bigint-only convention |
src/__tests__/domains/rules/determinism.test.ts |
Yes (κ P1.1.2) | Reference for property-test pattern (random sampling over bigint domain) |
No source path collides with the new file. The audit confirms a clean
greenfield slice: zero edits to existing modules. P3.1.1’s exported
Vote interface is consumed via type-only import.
§2. Spec inventory
2.1 Quorum formula (consensus.md §Quorum formula §20–40)
quorum = floor(2n / 3) + 1
f = floor((n - 1) / 3)
The arithmetic is integer (BPS / bigint). For any n:
quorum + f = n + 1 - (n mod 3 == 0 ? 1 : 0)
This is the honest-majority invariant any two quorums must intersect in.
2.2 Worked table (consensus.md §Quorum math §31–38)
n |
f |
quorum |
Notes |
|---|---|---|---|
| 4 | 1 | 3 | Minimum viable BFT |
| 7 | 2 | 5 | Comfortable small set |
| 10 | 3 | 7 | Phase-5+ fork checkpoint (matches ι 7-of-10) |
| 13 | 4 | 9 | Scaled arbiter committee |
These four rows are the fixture corpus. Each row must round-trip
through both quorumThreshold(n) and maxFaulty(n).
2.3 Single-arbiter clause (consensus.md §Phase 0 posture §193–198)
The runtime accepts θ-shaped APIs but always returns “trivially finalized” because
n = 1.
Hence:
quorumThreshold(1n) === 1nmaxFaulty(1n) === 0nhasQuorum([oneSignedVote], 1n) === true
The single-arbiter clause is the Phase-0 posture; the runtime never exits this state until the multi-model milestone (ADR-005).
2.4 Honest-majority intersection property
For any n ≥ 4 and any two quorum-sized sets A, B drawn from
{1,…,n}, |A ∩ B| ≥ 1 — at least one honest arbiter is in both
quorums. This follows from |A| + |B| > n because
2 * quorumThreshold(n) > n for all n ≥ 1.
2.5 Equivocation primitive
Per consensus.md §Equivocation §122-147:
An arbiter that signs two distinct tuples at the same finality level in the same round is equivocating.
detectDoubleVote(arbiter_id, round_id, votes) returns the list of
conflicting Vote PAIRS so that P3.5.1 can construct one
EquivocationProof per pair via P3.1.1’s buildEquivocationProof().
Distinction:
- Same tuple twice → benign retry, returns
[] - Different
(merkle_root, rule_version_hash)→ equivocation, returns the pair[voteA, voteB]
Grouping is by the canonical hash of the (round_id, merkle_root,
rule_version_hash) 3-tuple, NOT by object identity (per gotcha line
523-528 of the prompt). We reuse P3.1.1’s hashMessage(vote) indirectly
by building a small derived key ${round_id}|${merkle_root.hex}|${rule_version_hash.hex}
since hashMessage covers the entire Vote (including signature, sender,
clock) and we need to group by the SUB-tuple.
§3. Vote-shape sanity check (against P3.1.1)
P3.1.1 exports:
export interface Vote extends VoteTuple {
readonly sender_id: string;
readonly vote_type: VoteType; // 'ACCEPT' | 'REJECT' | 'ABSTAIN'
readonly timestamp_logical: bigint;
readonly signature: Buffer;
}
export interface VoteTuple {
readonly round_id: bigint;
readonly merkle_root: Buffer;
readonly rule_version_hash: Buffer;
}
round_id: bigint— directmerkle_root: Buffer—.toString('hex')for the group keyrule_version_hash: Buffer—.toString('hex')for the group keysender_id: string— used as the arbiter identity indetectDoubleVote
Signature is NOT part of the group key — two Votes with identical 3-tuple-plus-signature are still benign retries (the same signed bytes). Two Votes with identical 3-tuple but different signatures are not equivocation either — equivocation requires DIFFERENT TUPLES from the same arbiter in the same round.
No surprises in the Vote shape — proceeding to Step 2.
§4. Files to create
src/domains/consensus/quorum.ts(Step 4 commitfeat)src/__tests__/domains/consensus/quorum.test.ts(Step 4 commitfeat)
§5. Files NOT touched
src/domains/consensus/messages.ts— type-only import; no edit- All other existing modules — out of scope
§6. Step 1 conclusion
Greenfield slice. P3.1.1’s Vote shape is compatible with all
acceptance criteria. Worked table + single-arbiter clause + honest-
majority invariant + equivocation primitive are all spec-grounded.
Proceeding to Step 2 (contract).