P3.1.3 — Round / View State Machine — Verification
Step 5 of the 5-step chain (CLAUDE.md §6). Verification evidence that
the contract (§docs/contracts/p3-1-3-round-state-machine-contract.md)
is honored by the implementation at feat_sha 3e8d2516.
§1. Three-gate evidence
1.1 npm run build — PASS
> colibri@0.0.1 build
> tsc
> colibri@0.0.1 postbuild
> node scripts/copy-migrations.mjs
copy-migrations: copied 8 migration(s)
Zero TypeScript diagnostics. --noEmit clean against tsconfig.json
with exactOptionalPropertyTypes: true.
1.2 npm run lint — PASS
> colibri@0.0.1 lint
> eslint src
Zero ESLint errors across the consensus domain.
1.3 npm test — PASS
Test Suites: 64 passed, 64 total
Tests: 2970 passed, 2970 total
Snapshots: 0 total
Time: 30.103 s, estimated 44 s
Delta vs. baseline (origin/main @ 4ab4e8e8 = 2929 passing + 1 pre-existing reputation/tools migration-race flake = 2930):
+40new tests inround-state.test.ts.- Pre-existing flake did NOT fire under full-suite this run (the same intermittent surface reported in earlier rounds).
2970 / 2970 = 100%green across all 64 suites.
§2. Acceptance-criteria checklist
All 28 acceptance criteria from the contract §5 are mapped to tests in
src/__tests__/domains/consensus/round-state.test.ts:
| AC | Description | Test names |
|---|---|---|
| AC#1 | ROUND_STATES tuple shape | “AC#1 ROUND_STATES has 5 labels in canonical order” |
| AC#2 | T_round === 30000n | “AC#2 T_round === 30000n” |
| AC#3 | T_commit_phase === 10000n | “AC#3 T_commit_phase === 10000n” |
| AC#4 | T_reveal_phase === 10000n | “AC#4 T_reveal_phase === 10000n” |
| AC#5 | T_timeout === 60000n | “AC#5 T_timeout === 60000n (== 2n * T_round)” |
| AC#6 | New FSM at COMMIT_PHASE | “AC#6 new FSM at COMMIT_PHASE” |
| AC#7 | begin() commit-hash construction | “AC#7 begin() commit-hash construction” |
| AC#8 | COMMIT_PHASE → REVEAL_PHASE on quorum-many commits | “AC#8 COMMIT_PHASE -> REVEAL_PHASE on quorum-many commits” |
| AC#9 | COMMIT_PHASE timeout → VIEW_CHANGE | “AC#9 COMMIT_PHASE timeout -> VIEW_CHANGE” |
| AC#10 | myReveal() in REVEAL_PHASE | “AC#10 myReveal() in REVEAL_PHASE” |
| AC#11 | REVEAL_PHASE → VERIFY_PHASE on quorum-many reveals | “AC#11 REVEAL_PHASE -> VERIFY_PHASE on quorum-many reveals” |
| AC#12 | Mismatched commit/reveal hash → liveness fault | “AC#12 mismatched commit/reveal hash -> liveness fault” |
| AC#13 | REVEAL_PHASE timeout flags committed-but-not-revealed | “AC#13 REVEAL_PHASE timeout flags committed-but-not-revealed” |
| AC#14 | Happy-path verify → COMPLETED with QuorumOutcome | “AC#14 happy path verify -> COMPLETED with QuorumOutcome” |
| AC#15 | detectDoubleVote non-empty → equivocation_observed | “AC#15 detectDoubleVote non-empty -> VIEW_CHANGE equivocation_observed” |
| AC#16 | Largest group < quorum → malformed_proposal | “AC#16 largest group < quorum -> VIEW_CHANGE malformed_proposal” |
| AC#17 | verify() idempotency | “AC#17 verify() idempotency” |
| AC#18 | triggerViewChange broadcasts my VC | “AC#18 triggerViewChange broadcasts my VC” |
| AC#19 | Quorum-many VCs → COMMIT_PHASE rotated leader | “AC#19 quorum-many VCs -> COMMIT_PHASE with rotated leader” |
| AC#20 | selectLeader invoked with (arbiters, prev_merkle_root, round_id) | “AC#20 selectLeader called with (arbiters, prev_merkle_root, round_id)” |
| AC#21 | View-change reasons accumulate in payload | “AC#21 view-change reasons accumulate” |
| AC#22 | Hash-binding mismatch calls markLivenessFault | “AC#22 hash-binding mismatch calls markLivenessFault” |
| AC#23 | REVEAL_PHASE timeout flags all non-revealing arbiters | “AC#23 REVEAL_PHASE timeout flags all committed-but-not-revealed” |
| AC#24 | VIEW_CHANGE_ACCEPTED event shape | “AC#24 VIEW_CHANGE_ACCEPTED event shape” |
| AC#25 | QUORUM_REACHED event shape | “AC#25 QUORUM_REACHED event shape” |
| AC#26 | 4-node BFT worked example | “AC#26 consensus.md sec.Worked example - 4-node BFT vote on event E” |
| AC#27 | Single-arbiter clause | “AC#27 single-arbiter clause (n=1)” |
| AC#28 | Forbidden-token corpus self-scan | “AC#28 round-state.ts body uses none of kappa-forbidden tokens” |
28 / 28 ACs covered. Test surface: 39 test(...) calls grouped
into 14 describe(...) blocks.
§3. Worked example confirmation
The consensus.md §Worked example — 4-node BFT vote on event E trace
is reproduced bit-for-bit by the AC#26 test:
| Step | Spec event | Implementation |
|---|---|---|
| t=0 commit phase | A, B, C, D each broadcast COMMIT | makePeerCommit for arb:2/3/4 + fsm.begin() for arb:1 |
| … | All 4 commits received | receivedCommits().length === 4 |
| Phase A end | Advance to REVEAL | state() === 'REVEAL_PHASE' |
| t=10s reveal phase | All 4 reveals broadcast + hash-verified | buildPeerReveal for arb:2/3/4 + fsm.myReveal() |
| Phase B end | Advance to VERIFY | (implicit via auto-record after 3rd reveal — threshold reached) |
| Aggregation | 3-of-4 winners {A,B,C} on 0xab12 | winning_voters === ['arb:1', 'arb:2', 'arb:3']; merkle_root === tagBuf(0xab12) |
| Finality | QUORUM emitted | state() === 'COMPLETED'; quorumOutcome() populated |
| D’s vote | Isolated minority, not slashed | livenessFaults empty; D’s vote present in receivedReveals but not in winning_voters |
§4. Forbidden-token self-scan evidence
AC#28 reads round-state.ts via readFileSync, strips block + line
comments, then asserts none of:
/Math\.[A-Za-z]//Date\.[A-Za-z]//Math\.random//setTimeout//setInterval//setImmediate//process\.hrtime//performance\.now//\bcrypto\.[A-Za-z]/— NAMED imports only/\bNumber\(/— no implicit Number-coercion/\b\d+\.\d+\b/and/[^.\w]\.\d+\b/— no float literals
Plus a second test asserting:
import { ... } from 'node:crypto'block present- No
import * as crypto from 'node:crypto' - No
import crypto from 'node:crypto'
Both subtests pass under npm test.
§5. ζ schema notes (carried from audit §2.4)
The runtime ζ schema’s THOUGHT_TYPES tuple (src/domains/trail/
schema.ts §47-52) is unchanged by this slice. The VIEW_CHANGE_
ACCEPTED and QUORUM_REACHED events flow through the injected
ZetaSink.emit(...) callback; integration with the
thought_record MCP tool is a downstream concern (an adapter slice
may either widen THOUGHT_TYPES to include 'consensus' or map the
event onto 'analysis' with a discriminator inside content).
The task prompt’s instruction to “set thought_type='consensus'
(new value, document it)” is recorded here for the downstream
adapter task.
§6. λ.markLivenessFault notes (carried from audit §2.5)
λ’s reputation surface (P2.2.2 penalties.ts) exposes
apply_penalty(band) but does not export markLivenessFault. The
P3.1.3 slice defines its own LivenessSink interface and leaves the
binding to a future λ slice. The single hook point is the constructor;
when λ exposes a typed liveness API, one-line wiring in the consensus
caller is sufficient.
§7. Commit chain
| Step | SHA | Subject |
|---|---|---|
| 1. Audit | a8e4cc96 |
audit(p3-1-3-round-state-machine): inventory commit-reveal FSM + view-change surface (R89 θ Wave 3b) |
| 2. Contract | 2fba8524 |
contract(p3-1-3-round-state-machine): behavioural contract for commit-reveal FSM (R89 θ Wave 3b) |
| 3. Packet | 30354add |
packet(p3-1-3-round-state-machine): execution plan for commit-reveal FSM (R89 θ Wave 3b) |
| 4. Feat | 3e8d2516 |
feat(p3-1-3-round-state-machine): commit-reveal FSM + view-change wiring VRF (R89 θ Wave 3b) |
| 5. Verify | (this commit) | verify(p3-1-3-round-state-machine): test evidence |
§8. Sign-off
All three gates pass. All 28 acceptance criteria honored. Ready for PR open + PM gate.