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 } after begin().
  • __commits: Map<string, Commit>, __reveals: Map<string, Reveal>, __viewChanges: Map<string, ViewChange> — keyed by sender_id, set-semantic.

§3. Implementation order

  1. §1–§5 surface — empty class with all method signatures, no logic. tsc --noEmit clean.
  2. Constructor + getters — straight assignments + validation. Lint clean.
  3. begin() — generate salt, sign Vote, hash, build Commit, auto- receiveCommit(myCommit).
  4. receiveCommit + Phase A → REVEAL transition — quorum check.
  5. myReveal() — produce my Reveal once state is REVEAL_PHASE.
  6. receiveReveal + Phase B → VERIFY transition — quorum check + hash binding.
  7. verify() + Phase C → COMPLETED OR VIEW_CHANGE — group-by-key, detectDoubleVote, QuorumOutcome assembly.
  8. receiveViewChange + VIEW_CHANGE → COMMIT_PHASE — VRF selectLeader, ζ emit.
  9. tick() + timeouts — drives COMMIT_PHASE and REVEAL_PHASE into VIEW_CHANGE when their phase budget elapses without quorum.
  10. triggerViewChange(reason) — caller-initiated VC.
  11. Liveness fault sink wiring — on hash mismatch + on REVEAL_PHASE timeout.
  12. Single-arbiter clause — short-circuit in constructor; begin() runs Phase A. tick() from COMMIT_PHASE with n=1n advances through REVEAL → VERIFY → COMPLETED on three ticks.
  13. __advance state-transition — recompute __phaseStart, emit ζ on QUORUM_REACHED / VIEW_CHANGE_ACCEPTED.
  14. JSDoc § block — finalize.

§4. Test plan — round-state.test.ts outline

Common fixtures at top:

  • makeKeyPair(): KeyPair via generateKeyPairSync('ed25519').
  • makeCommittee(n: number) returns {arbiters, keysBySender, pubKeyMap} with deterministic ids 'arbiter:1'...
  • makeVoteTuple(rootTag, rvTag) returns a VoteTuple.
  • 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:

  1. AC#1–AC#5 constants — 5 tests.
  2. AC#6–AC#9 Phase A — 5–6 tests.
  3. AC#10–AC#13 Phase B — 5–6 tests.
  4. AC#14–AC#17 Phase C — 5–6 tests.
  5. AC#18–AC#21 View-change — 5–6 tests.
  6. AC#22–AC#23 Liveness — 3 tests.
  7. AC#24–AC#25 ζ emission — 3–4 tests.
  8. AC#26 4-node BFT worked example — 2–3 tests (happy path + variant with D double-voting → equivocation_observed VC).
  9. AC#27 Single-arbiter — 3 tests (begin/reveal/verify chain; tick-driven chain; quorum outcome shape).
  10. 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/tools migration-path race) — not introduced by this slice; rerun isolates green.
  • All other tests untouched.
  • npm run build clean (no tsc diagnostics).
  • npm run lint clean (no eslint errors).

§6. Step 4 → Step 5 commits

  • Step 4 (feat): feat(p3-1-3-round-state-machine): commit-reveal FSM + view-change wiring VRF — adds round-state.ts + round-state.test.ts.
  • Step 5 (verify): verify(p3-1-3-round-state-machine): test evidence — adds docs/verification/p3-1-3-round-state-machine-verification.md capturing 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.


Back to top

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

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