P3.6.1 — VRF Stub (Leader Election) — Verification
Step 5 of the 5-step chain. The implementation (Step 4) landed at
SHA 1ea60a04. This document captures the test evidence binding the
deliverable to contract §9 (AC#1–AC#23).
§1. Commits — 5-step chain
| Step | Output | SHA |
|---|---|---|
| 1 — audit | docs/audits/p3-6-1-vrf-stub-audit.md |
ffc38127 |
| 2 — contract | docs/contracts/p3-6-1-vrf-stub-contract.md |
80266e15 |
| 3 — packet | docs/packets/p3-6-1-vrf-stub-packet.md |
6743271c |
| 4 — implement | src/domains/consensus/vrf-stub.ts + test file |
1ea60a04 |
| 5 — verify | this file | (this commit) |
Branch: feature/p3-6-1-vrf-stub
Worktree: .worktrees/claude/p3-6-1-vrf-stub
Base: origin/main @ 498e6ea5 (R89 θ Wave 2 close)
§2. Gate results
All three gates pass on 1ea60a04 (the implementation commit):
$ npm run build
> tsc
> node scripts/copy-migrations.mjs
copy-migrations: copied 8 migration(s) ... -> dist/db/migrations
# exit 0
$ npm run lint
> eslint src
# exit 0 — zero errors, zero new warnings
$ npm test
Test Suites: 61 passed, 61 total
Tests: 2861 passed, 2861 total
Snapshots: 0 total
Time: 31.672 s
Test delta
| Surface | Count |
|---|---|
Baseline at 498e6ea5 (Wave 2 close, pre-this-slice) |
2820 |
This slice (vrf-stub.test.ts) |
+41 |
Total at 1ea60a04 |
2861 |
(Baseline 2820 = 2861 − 41 derived from the suite total minus the exclusive contribution of the new test file. The 2744 number quoted in the dispatch packet predates Waves 1 + 2; the worktree’s actual starting point includes their +76 tests.)
Per-slice isolation run
vrf-stub.test.ts runs cleanly in isolation:
$ node --experimental-vm-modules node_modules/jest/bin/jest.js \
src/__tests__/domains/consensus/vrf-stub.test.ts
Test Suites: 1 passed, 1 total
Tests: 41 passed, 41 total
Coverage of src/domains/consensus/vrf-stub.ts is 100% stmts /
100% funcs / 100% lines / 94.4% branches — the one uncovered branch
is the ?? '' fallback at the end of selectLeader, which is
unreachable by construction (the bigint modulo guarantees the index
exists) and exists only to satisfy TS narrowing.
§3. AC traceability — AC#1 through AC#23
| AC | Description | Test path | Result |
|---|---|---|---|
| AC#1 | output and proof are both 32 bytes | vrfEval — shape > output and proof are both 32 bytes for fixed args |
PASS |
| AC#2 | determinism over 10000× | vrfEval — determinism > 10000 calls on identical args produce byte-identical outputs |
PASS (234 ms) |
| AC#3 | collision-free over 10000 distinct inputs | vrfEval — collision-free > 10000 distinct (seed, input) pairs under K1 yield 10000 distinct outputs |
PASS (Set.size === 10000) |
| AC#4 | key sensitivity | vrfEval — key sensitivity > distinct privKeys ... |
PASS |
| AC#5 | rejects non-Buffer seed | vrfEval — input validation > rejects non-Buffer seed with VrfError |
PASS |
| AC#6 | rejects non-Buffer input | ... > rejects non-Buffer input with VrfError |
PASS |
| AC#7 | rejects non-Buffer privKey | ... > rejects non-Buffer privKey with VrfError |
PASS |
| AC#8 | rejects empty privKey | ... > rejects empty privKey with VrfError |
PASS |
| AC#9 | accepts zero-length seed/input | vrfEval — shape > accepts zero-length seed + accepts zero-length input |
PASS (2 tests) |
| AC#10 | verify roundtrip — correct key | vrfVerify — roundtrip > eval then verify with same key returns true |
PASS |
| AC#11 | verify rejects wrong key | vrfVerify — rejection paths > wrong pubKey returns false |
PASS |
| AC#12 | verify rejects tampered output | ... > tampered output (bit flip) returns false |
PASS |
| AC#13 | verify rejects tampered proof | ... > tampered proof (bit flip) returns false |
PASS |
| AC#14 | verify rejects tampered seed | ... > tampered seed returns false |
PASS |
| AC#15 | verify defensive — non-Buffer args | ... > non-Buffer arguments return false without throwing |
PASS (each position swapped) |
| AC#16 | selectLeader determinism | selectLeader — determinism + single-arbiter > determinism: 1000 iterations on identical args agree |
PASS |
| AC#17 | selectLeader single-arbiter | ... > single-arbiter committee returns the sole arbiter |
PASS |
| AC#18 | selectLeader uniformity smoke | selectLeader — uniformity smoke > 1000 round_ids over 4 arbiters: chi-squared below 16.27 (99% CI) |
PASS (loose 99.9% CI for df=3) |
| AC#19 | selectLeader variable sizes | selectLeader — variable arbiter sizes > returns a valid arbiter for n = %i (parameterized: 1, 2, 3, 4, 7, 10, 13, 100) |
PASS (8 cases) |
| AC#20 | selectLeader empty | ... > empty arbiters list throws VrfError |
PASS |
| AC#21 | VrfProvider interface | VrfProvider interface > defaultVrf is a HmacVrfProvider instance ... + vrfEval delegates byte-identically through defaultVrf + HmacVrfProvider satisfies the VrfProvider interface shape |
PASS (3 tests) |
| AC#22 | module-corpus token scan | vrf-stub.ts — module-corpus invariants > no Math.* / no Date.* / no dotted crypto.X / no Number( / no floating-point literal |
PASS (5 tests) |
| AC#23 | header RFC 9381 + externally verifiable | ... > header declares NOT RFC 9381 and externally verifiable + header cites ADR-002 Option A and ADR-005 Decision |
PASS (2 tests) |
Additional safety nets (beyond the formal AC list):
selectLeader — determinism + single-arbiter > distinct round_ids produce distinct leaders at least once(catches a stuck/biased implementation; observed >1 distinct arbiter across 100 round_ids).selectLeader — variable arbiter sizes > non-array arbiters throws VrfErrorand> non-Buffer seed throws VrfError(input-validation rounding-out beyond AC#20).vrfVerify — rejection paths > non-Buffer arguments return false without throwingexercises eight argument-mutation cases plus empty pubKey and wrong-length output/proof.
§4. Acceptance summary
- 41 / 41 new tests pass in isolation.
- 2861 / 2861 total tests pass across 61 suites.
- 100% statement / 100% function / 100% line coverage of
vrf-stub.ts; 94.4% branch (single unreachable narrowing branch). npm run buildandnpm run lintboth exit 0.- No new npm dependency.
@noble/curvesdeferred to Phase 1.5 per ADR-002 §Option B. - Module header carries the mandatory “NOT RFC 9381” and “externally verifiable” disclosures (test-asserted).
- VrfProvider interface is implementation-agnostic; the Phase 1.5
Option B swap touches only
defaultVrf’s right-hand side. - selectLeader correctly serves the Phase 0 single-arbiter posture (fast path) and the multi-arbiter view-change flow alike.
§5. ADR-002 status disclosure
ADR-002 remains PROPOSED at slice close. This deliverable ships Option A per the dispatch packet’s unconditional ship directive. The PROPOSED status is reflected in:
- Module-header citation: “ADR-002 §Option A” (not “ADR-002 §Accepted-Decision”).
- The mandatory NOT-RFC-9381 / NOT-externally-verifiable disclosures.
- Audit §2.2 acknowledges Option B as the future swap target without asserting it has been adopted.
When ADR-002 graduates to Accepted with Option B, the swap is:
npm install @noble/curves(or peer-dep route if already present in the tree via@noble/hashes).- Implement
NobleCurvesVrfProviderinvrf-stub.ts(or move both intosrc/domains/consensus/vrf/index.tswith two provider files). - Replace
export const defaultVrf: VrfProvider = new HmacVrfProvider();with... = new NobleCurvesVrfProvider();. - Update the module-header warning to point at the new behavior (drop the “externally verifiable” caveat).
- Existing callers of
vrfEval/vrfVerify/selectLeadersee no API change. The 41 tests survive unchanged; new ECVRF-specific tests are added alongside.
§6. Risks confirmed mitigated
| Risk | Mitigation | Verification |
|---|---|---|
| Caller misuses VRF stub for external proofs | Header warning + AC#23 test | PASS |
Modulo bias in selectLeader |
Documented in source + AC#18 uniformity smoke | PASS (χ² well under the loose 16.27 cap) |
Premature @noble/curves swap |
No dep added; package.json untouched |
Confirmed by git diff origin/main -- package.json showing no change |
| Non-power-of-2 arbiter sizes | AC#19 covers {1,2,3,4,7,10,13,100} | PASS (8 cases) |
| Module-corpus token leak | AC#22 self-scan | PASS (5 token classes asserted absent) |
§7. Self-review
- All gates green on
1ea60a04. - 41 new tests landed in
src/__tests__/domains/consensus/vrf-stub.test.ts. - 100% / 100% / 100% / 94.4% coverage of the new module.
- AC#1–AC#23 traced to specific test names.
- No new npm dependency.
- Module header citation chain complete (audit / contract / packet + ADR-002 §Option A + ADR-005 §Decision + consensus.md §View-change).
- Forbidden-token scan inheritance verified (matches gossip-wire / time-anchors pattern).
selectLeaderPhase 0 single-arbiter fast path tested.selectLeaderPhase 5+ multi-arbiter uniformity tested.
Ready for PR.