P3.1.3 — Round / View State Machine — Packet
Step 3 of the 5-step chain (CLAUDE.md §6). Execution plan converting the contract into a concrete file layout, ordered edits, and verification gates.
§1. File layout
src/domains/consensus/
round-state.ts [NEW] ~640 LOC
src/__tests__/domains/consensus/
round-state.test.ts [NEW] ~860 LOC
No other files touched. No package.json edits. No new npm deps.
§2. round-state.ts outline
§1 File JSDoc (purpose, references, forbidden-token self-scan note)
§2 Imports (NAMED only — createHash, randomBytes, sign, verify, KeyObject)
§3 Constants (ROUND_STATES, T_round, T_commit_phase, T_reveal_phase, T_timeout)
§4 Error class (RoundStateError)
§5 Type exports (RoundState [as type alias of union], ZetaEvent,
ZetaSink, LivenessSink, QuorumOutcome, RoundStateConfig)
§6 Internal helpers:
- _bigintFromLen(arr.length) — pure conversion via concat-loop avoiding Number()
- _defaultClock — closure-bound monotonic counter factory
- _defaultRng — randomBytes(32) wrapper
- _hashCommit(vote, salt) — SHA-256(canonicalSerialize(vote) ++ salt)
§7 RoundState class:
- constructor (precondition validation; init state)
- public getters (state, round_id, arbiter_id, current_leader, clock,
myCommit, receivedCommits, receivedReveals, receivedViewChanges,
quorumOutcome, livenessFaults, zetaEvents)
- begin()
- receiveCommit(c)
- receiveReveal(r)
- receiveViewChange(vc)
- tick()
- triggerViewChange(reason)
- myReveal()
- verify()
- private __advance(target) — state-transition with side-effect emission
- private __broadcastMyViewChange(reason) — used by tick + triggerViewChange
Three private state buckets:
__phaseStart: bigint— clock snapshot at entry into the current phase.__myKeyMaterial—{ vote: Vote, salt: Buffer, commit: Commit }afterbegin().__commits: Map<string, Commit>,__reveals: Map<string, Reveal>,__viewChanges: Map<string, ViewChange>— keyed by sender_id, set-semantic.
§3. Implementation order
- §1–§5 surface — empty class with all method signatures, no logic.
tsc --noEmitclean. - Constructor + getters — straight assignments + validation. Lint clean.
- begin() — generate salt, sign Vote, hash, build Commit, auto-
receiveCommit(myCommit). - receiveCommit + Phase A → REVEAL transition — quorum check.
- myReveal() — produce my Reveal once state is
REVEAL_PHASE. - receiveReveal + Phase B → VERIFY transition — quorum check + hash binding.
- verify() + Phase C → COMPLETED OR VIEW_CHANGE — group-by-key, detectDoubleVote, QuorumOutcome assembly.
- receiveViewChange + VIEW_CHANGE → COMMIT_PHASE — VRF selectLeader, ζ emit.
- tick() + timeouts — drives
COMMIT_PHASEandREVEAL_PHASEintoVIEW_CHANGEwhen their phase budget elapses without quorum. - triggerViewChange(reason) — caller-initiated VC.
- Liveness fault sink wiring — on hash mismatch + on REVEAL_PHASE timeout.
- Single-arbiter clause — short-circuit in constructor;
begin()runs Phase A.tick()fromCOMMIT_PHASEwithn=1nadvances through REVEAL → VERIFY → COMPLETED on three ticks. - __advance state-transition — recompute
__phaseStart, emit ζ on QUORUM_REACHED / VIEW_CHANGE_ACCEPTED. - JSDoc § block — finalize.
§4. Test plan — round-state.test.ts outline
Common fixtures at top:
makeKeyPair(): KeyPairviagenerateKeyPairSync('ed25519').makeCommittee(n: number)returns{arbiters, keysBySender, pubKeyMap}with deterministic ids'arbiter:1'...makeVoteTuple(rootTag, rvTag)returns aVoteTuple.recordingZetaSink()returns{sink, events}for inspection.recordingLivenessSink()returns{sink, calls}.stepClock()returns a manual-advance clock function for tick tests.seededRng()returns a fixed-32-byte salt source.
Test groups in order:
- AC#1–AC#5 constants — 5 tests.
- AC#6–AC#9 Phase A — 5–6 tests.
- AC#10–AC#13 Phase B — 5–6 tests.
- AC#14–AC#17 Phase C — 5–6 tests.
- AC#18–AC#21 View-change — 5–6 tests.
- AC#22–AC#23 Liveness — 3 tests.
- AC#24–AC#25 ζ emission — 3–4 tests.
- AC#26 4-node BFT worked example — 2–3 tests (happy path + variant with D double-voting → equivocation_observed VC).
- AC#27 Single-arbiter — 3 tests (begin/reveal/verify chain; tick-driven chain; quorum outcome shape).
- AC#28 Forbidden-token scan — 1 test (reads the file via readFileSync, strips block comments, asserts none of the forbidden patterns match).
Target test count: 35–45 individual test(...) calls.
§5. Build / lint / test gates
cd .worktrees/claude/p3-1-3-round-state-machine
npm run build
npm run lint
npm test
Expected outcome (baseline 2929 + 1 pre-existing flake):
- New tests: +30–50 (target 35–45).
- Pre-existing flake remains (
reputation/toolsmigration-path race) — not introduced by this slice; rerun isolates green. - All other tests untouched.
npm run buildclean (notscdiagnostics).npm run lintclean (noeslinterrors).
§6. Step 4 → Step 5 commits
- Step 4 (feat):
feat(p3-1-3-round-state-machine): commit-reveal FSM + view-change wiring VRF— addsround-state.ts+round-state.test.ts. - Step 5 (verify):
verify(p3-1-3-round-state-machine): test evidence— addsdocs/verification/p3-1-3-round-state-machine-verification.mdcapturing the build/lint/test outputs (counts only — not full logs).
Total chain: 5 commits (audit + contract + packet + feat + verify).
§7. Risk register
| # | Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|---|
| 1 | BigInt(x.length) conversion edge case (empty array) |
Low | Low | Tests cover length === 0 paths; BigInt(0) === 0n is standard JS |
| 2 | ζ sink throws | Low | Med | Wrap emit in try/catch in FSM body? No — fail-fast is correct; let exception propagate (tests use a recording sink that never throws) |
| 3 | Liveness sink throws | Low | Med | Same as #2 |
| 4 | randomBytes(32) non-determinism in tests |
Cert | High | All test fixtures inject a seeded RNG |
| 5 | Cross-test logical-clock contamination via nextLogical() global |
Med | Med | Tests inject a per-test clock via RoundStateConfig.clock; the global Lamport counter is never read by round-state.ts |
| 6 | 4-node BFT test runtime | Low | Low | Pure CPU work; Ed25519 sign+verify ~100µs; ~20 sigs per test → < 5ms |
| 7 | Forbidden-token false-positive on JSDoc literals | Med | Low | Mirror the messages.ts / finality.ts pattern — strip block comments before scanning |
§8. Approval gate
This packet is approved for Step 4 (implementation). Subsequent edits to the contract or this packet are scoped to in-flight clarifications only; material changes restart the chain.