P3.5.1 — Equivocation Slasher — Verification

Step 5 of the 5-step chain. Captures gate evidence and the AC-coverage matrix.

§1. Gates

All three gates pass against the worktree at the Step 4 commit (after the implementation commit feat(p3-5-1-equivocation)).

Gate Command Result
Build npm run build PASS — zero TS errors; postbuild migrations copy
Lint npm run lint PASS — zero ESLint errors
Test npm test PASS — 2848 / 2848 (final stable run)

§2. Test baseline reconciliation

Snapshot Test count Source
Prompt-cited baseline 2744 Pre-P3.1.2 (#237)
Base SHA 498e6ea5 actual 2820 Confirmed by running on a tree with equivocation.{ts,test.ts} removed
After P3.5.1 lands 2848 This slice (+28)

Delta: +28 new tests, all in src/__tests__/domains/consensus/equivocation.test.ts. Net delta against the prompt-cited 2744 baseline is +104, of which +76 belong to already-merged R89 Wave 2 slices (#234, #235, #236, #237) and +28 to this slice.

§3. Flake observation (carryover)

The first npm test run hit a SQLite test-setup race in src/__tests__/domains/reputation/{tools,witnesses}.test.ts (“no such table: reputations”). Re-running the suite was clean — the third consecutive npm test produced 2848/2848 passing. This is a pre-existing flake, NOT a regression from this slice:

  • Confirmed by checking out origin/main’s src/ (with my changes removed) and re-running — same race surfaces under parallel test execution.
  • The slice does NOT touch src/domains/reputation/{tools,witnesses}.ts or any DB / migration code.
  • The flake is in the test setup’s table-creation ordering under --runInBand-less parallelism.

Recommended to PM: track the reputation-tools / witnesses migration race as an out-of-scope follow-up; out of P3.5.1 scope.

§4. Acceptance corpus coverage

All 28 AC IDs from docs/contracts/p3-5-1-equivocation-contract.md §8 map 1:1 to a passing it() / test() block.

AC# Describe / Test name Result
AC#1 verifyEquivocationProof / happy path returns {valid: true} PASS
AC#2 verifyEquivocationProof / tampered signed_vote_a → sig_a_invalid PASS
AC#3 verifyEquivocationProof / tampered signed_vote_b → sig_b_invalid PASS
AC#4 verifyEquivocationProof / wrong pubkey for sig_a (short-circuits) PASS
AC#5 verifyEquivocationProof / same tuple in both votes → same_tuple PASS
AC#6 verifyEquivocationProof / different round_id → different_round_or_level PASS
AC#7 verifyEquivocationProof / short-circuit order — sig_a wins over same_tuple PASS
AC#8 verifyEquivocationProof / malformed signature does not throw PASS
AC#9 evidenceHashHex / lowercase 64-char hex PASS
AC#10 evidenceHashHex / stable across calls PASS
AC#11 applyEquivocationSlash / happy path — score 10000 → 2000 PASS
AC#12 applyEquivocationSlash / Set guard — duplicate on second call PASS
AC#13 applyEquivocationSlash / invalid proof → invalid_proof PASS
AC#14 applyEquivocationSlash / λ double-jeopardy → lambda_double_jeopardy PASS
AC#15 applyEquivocationSlash / history_event.reason has band:critical| prefix PASS
AC#16 applyEquivocationSlash / event_id === evidence_hash_hex PASS
AC#17 applyEquivocationSlash / domain === arbitration PASS
AC#18 applyEquivocationSlash / ban_until_epoch = current_epoch + 100 PASS
AC#19 applyEquivocationSlash / row.node_id mismatch throws PASS
AC#20 applyEquivocationSlash / row.domain mismatch throws PASS
AC#21 single-arbiter (n=1) self-equivocation slashable PASS
AC#22 EQUIVOCATION_BAND === critical PASS
AC#23 EQUIVOCATION_DOMAIN === arbitration PASS
AC#24 EQUIVOCATION_SLASH_BPS === DAMAGE_CRITICAL === 8000n PASS
AC#25 integration with detectDoubleVote / pair → buildProof → verify → slash PASS
AC#26 integration / re-slash via Set → duplicate PASS
AC#27 source body — forbidden-token corpus scan PASS
AC#28 buildEquivocationProof re-exported and callable PASS

§5. Invariant evidence

Invariant Source-of-truth
I1 — Pure module AC#27 forbidden-token scanner over JSDoc-stripped body
I2 — verify path never throws AC#8 malformed-sig test wraps in expect(…).not.toThrow
I3 — No double-slash via Set AC#12, AC#26
I4 — λ delegation honors band/domain mapping AC#15, AC#16, AC#17
I5 — Constant 8000bps amount AC#11 (score 10000 → 2000 = -8000), AC#24
I6 — Single-arbiter clause AC#21
I7 — No λ surface edits src/domains/reputation/penalties.ts untouched (git diff confirms)
I8 — Verify short-circuit order AC#7

§6. Prompt forbiddens check

Forbidden Status
--no-verify Not used; commits ran pre-commit hooks normally
--amend Not used; 5 distinct commits
--force-push Not used; branch publishes via standard push
Main edits None — all work in .worktrees/claude/p3-5-1-equivocation
Math.* / Date.* / Math.random AC#27 corpus scan asserts absence
Floats All arithmetic is bigint; ReputationRow.score crosses to number at λ’s stable boundary
Modifying λ penalty surface apply_penalty imported as-is; no override
Double-slash Set guard at top of applyEquivocationSlash; AC#12 + AC#26

§7. Out of scope (deferred — for downstream slices)

  • π suspension flow (Phase 6).
  • View-change trigger on equivocation_observed (P3.1.3).
  • Reputation history persistence (insertHistoryEvent — caller-owned per λ P2.1.1 invariant).
  • Multi-arbiter slashing concurrency control (DB row locking).
  • Network propagation of equivocation proofs (P3.3.x gossip layer).

§8. Step 5 conclusion

P3.5.1 ships clean. 28/28 acceptance criteria green. Build + lint + test gates all pass at the final commit. Pre-existing reputation-suite flake observed and confirmed unrelated to this slice. Ready for PR review.


Back to top

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

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