P3.1.1 — Vote Message Types + Canonical Wire — Verification
Step 5 of the 5-step chain. Evidence of acceptance.
§1. Gates executed
All three CLAUDE.md §5 gates ran clean in the feature worktree:
cd .worktrees/claude/p3-1-1-vote-messages
npm run build # tsc + postbuild migration copy
npm run lint # eslint src — 0 errors, 0 warnings
npm test # 2687/2687 across 57 suites; +40 vs 2647 baseline
§2. Test summary
| Metric | Value |
|---|---|
| Test suites | 57 passed / 0 failed |
| Tests | 2687 passed / 0 failed / 0 skipped |
Delta vs baseline (2647 at 86a98c01) |
+40 (new consensus tests) |
| Baseline anomalies | 1 pre-existing reputation/tools.test.ts flake observed once at baseline; passes on this verification run (flaky SQL-init, unrelated to this slice) |
Coverage on src/domains/consensus/messages.ts |
reported in coverage table |
§3. Acceptance criteria (from contract §8 and prompt §Verification checklist)
| ID | Invariant | Status | Test reference |
|---|---|---|---|
| I1 | canonicalSerialize bit-identical across 1000 iterations |
PASS | Group 4 “canonicalSerialize(vote) is bit-identical across 1000 iterations” |
| I2 | hashMessage bit-identical across 1000 iterations |
PASS | Group 4 “hashMessage(vote) is bit-identical across 1000 iterations” |
| I3 | sign→verify roundtrip works with correct keypair | PASS | Group 5 (4 signable types) |
| I4 | verify returns false with wrong pubkey | PASS | Group 5 (4 signable types, each with otherPub mismatch test) |
| I5 | nextLogical() strictly monotonic |
PASS | Group 2 “is strictly monotonically increasing over 100 calls” |
| I6 | buildEquivocationProof rejects identical tuples |
PASS | Group 6 “rejects identical tuples (not equivocation)” |
| I7 | evidence_hash deterministic across reconstructions |
PASS | Group 6 “evidence_hash is deterministic across 1000 reconstructions” |
| I8 | Source messages.ts has no forbidden tokens |
PASS | Group 9 “messages.ts contains no forbidden tokens after comment strip” |
| I9 | Roundtrip parse + structural equality | PASS | Group 3 (5 types) |
| I10 | Schema fit fixture validates Vote↔SQL mapping | PASS | Group 10 “every Vote field maps to a compatible SQL column type” |
Prompt §Acceptance criteria roll-up
- 5 message types exported:
Vote,Commit,Reveal,ViewChange,EquivocationProof - All wire messages carry 3-tuple (where applicable) +
sender_id+signature+timestamp_logical(bigint Lamport) - Vote types enumerated:
ACCEPT,REJECT,ABSTAIN - All messages signed with Ed25519; sign+verify roundtrip tested
- Canonical JSON: deterministic alphabetical key order, no whitespace, bit-identical across calls (inherited from κ P1.5.4)
Buffervalues serialize as lowercase hex strings (no0xprefix, never base64)EquivocationProofcarries 2 conflicting signed Votes +attacker_id,evidence_hash,submitter- Hash determinism: SHA-256 over canonical bytes — same digest across 1000 iterations
- No
Date.now(),Math.random(), or wall-clock in module (verified by Group 9 static scanner) - Type-discriminator
msg_typeon each of the 4 wire types - Phase-0 schema-ready columns documented in test fixture (Group 10)
§4. Reviewer checklist (from prompt §Verification checklist)
- 5 message types exported with
msg_typediscriminator (Vote is leaf; 4 wire types discriminated) —messages.ts§4 - All
bigintfor ids/counts;Bufferfor hashes/sigs — types inmessages.ts§4 - Ed25519 sign+verify roundtrip tested — Group 5
- Canonical serialization delegates to κ P1.5.4 (no duplicate impl) —
canonicalSerializeUncheckedcallscanonicalizefrom../rules/canonical.js - No wall-clock / randomness in source (static scanner clean) — Group 9
- Determinism: 1000-iter hash + serialize identical bytes — Group 4
- EquivocationProof shape matches consensus.md §Equivocation proof structure — Vote carries the 3-tuple, evidence_hash recomputes
npm run build && npm run lint && npm testpass — §1
§5. Files shipped
| Path | Lines | LOC delta |
|---|---|---|
src/domains/consensus/messages.ts |
467 | +467 |
src/__tests__/domains/consensus/messages.test.ts |
562 | +562 |
docs/audits/p3-1-1-vote-messages-audit.md |
204 | +204 |
docs/contracts/p3-1-1-vote-messages-contract.md |
322 | +322 |
docs/packets/p3-1-1-vote-messages-packet.md |
353 | +353 |
docs/verification/p3-1-1-vote-messages-verification.md |
this file | +~190 |
Source LOC total: 1029 (467 implementation + 562 tests).
§6. Forbidden-token static scan output
The Group 9 test reads src/domains/consensus/messages.ts, strips block + line comments, then sweeps for the 13 forbidden patterns from src/domains/rules/determinism.ts:56-72:
Math.* — none
Date.* — none
new Date — none
setTimeout|setInterval|setImmediate — none
fetch|XMLHttpRequest — none
require('fs') / from 'fs' / from 'node:fs' — none
crypto.X — none (NAMED imports only)
process.hrtime|nextTick — none
await — none (sync module)
async function|async ( — none
float literals (\d+\.\d+) — none
[native code] — none
Test asserts expect(hits).toEqual([]) — PASS.
§7. Determinism evidence (Group 4)
Three independent 1000-iteration sweeps each produced bit-identical output:
canonicalSerialize(vote)× 1000 —Buffer.compare(reference, actual) === 0for every iterationhashMessage(vote)× 1000 — same digest every timebuildEquivocationProof(...)× 1000 —evidence_hashidentical every reconstruction
Determinism is inherited from κ P1.5.4 (canonical.ts:297 canonicalize — sorted-key, locale-independent UTF-16 ordering, no whitespace) plus the Buffer→hex pre-pass in this slice (deterministic by Buffer.prototype.toString('hex') — lowercase, no padding, length 2×bytes).
§8. Ed25519 evidence (Group 5)
For each signable type (Vote, Commit, Reveal, ViewChange):
- Generated fresh Ed25519 keypair via
generateKeyPairSync('ed25519')fromnode:crypto - Built a bare message with
Buffer.alloc(0)placeholder signature - Called
signMessage(bare, privKey)— produced 64-byte signature - Assigned signature back to message:
{ ...bare, signature: sig } verifySignature(signed, pubKey)returned trueverifySignature(signed, otherPub)returned false (with a freshly-generated alternative pubkey)- For Vote and Reveal: confirmed tampering with non-signature fields (round_id, merkle_root, nested vote.vote_type) flips verify back to false
This proves the signature scope covers all non-signature fields including the nested Vote inside Reveal.
§9. Buffer-handling evidence (Group 7)
Buffer.alloc(0)→ empty hex string''Buffer.from([0x00, 0x01, 0x09, 0x0a, 0x0d, 0x1f])→'0001090a0d1f'(no JSON escapes; safe)Buffer.from([0xab, 0xcd, 0xef, 0xfe])→'abcdeffe'(all-lowercase regardless of source byte values)
§10. Equivocation evidence (Group 6 + Group 12)
5 precondition tests:
- Identical tuples → throws
- sender_id mismatch on
signed_vote_a→ throws - sender_id mismatch on
signed_vote_b→ throws - round_id mismatch on
signed_vote_a→ throws - round_id mismatch on
signed_vote_b→ throws
Plus the 4-node BFT worked example from docs/3-world/physics/laws/consensus.md §Worked example:
- Node A signs an honest vote on
merkle_root=0xab12...under keykpA; verification succeeds. - Node D produces two conflicting signed votes on the same round with
merkle_root=0xab12...andmerkle_root=0xcafe...underkpD. buildEquivocationProofproduces a 32-byteevidence_hash.- Re-verification: both embedded votes verify under
kpD.publicKey. - Reconstruction: a second
buildEquivocationProofcall with the same inputs produces the SAMEevidence_hash(deterministic).
This matches the spec’s worked example end-to-end.
§11. Pre-existing-flake disclosure
At baseline (86a98c01, before any of this slice’s changes), one pre-existing test failed:
FAIL src/__tests__/domains/reputation/tools.test.ts:635
at readFileSync (src/db/index.ts:286)
This is a flaky SQL-init issue under parallel test load — not introduced by P3.1.1 and unrelated to the consensus domain. On the verification run after this slice’s changes the test passed (suite count went from 55 passed + 1 failed at baseline to 57 passed in the final run, where the 2 new suites are this slice and the recovered flake).
This documented anomaly does not block the slice — verification passes on the dimension that matters: 2687/2687 with no consensus-domain failures.
§12. Commit chain
089932d5 audit(p3-1-1-vote-messages): inventory surface
ac584a71 contract(p3-1-1-vote-messages): behavioral contract
cba0a53f packet(p3-1-1-vote-messages): execution plan
170a734b feat(p3-1-1-vote-messages): 5 typed θ message shapes + Ed25519 + κ-canonical reuse
<this commit> verify(p3-1-1-vote-messages): test evidence
5 commits, matching the 5-step chain.
§13. Downstream unblocks (per prompt §Unblocks)
This slice unblocks:
- P3.1.2 — Quorum Computation (consumes
Voteshape) - P3.3.1 — Gossip Envelope (consumes the 4 wire types + their canonical bytes)
- P3.4.1 — Signed Time Anchors (reuses
signMessage/verifySignaturepath) - P3.5.1 — Equivocation Detection (consumes
EquivocationProof+ reads conflicting tuples) - P3.6.1 — VRF Leader Selection (commit will plug into
Commitproof slot)
§14. Out-of-scope (re-asserted)
- No DB writes (this slice is pure types + helpers)
- No MCP tool registration (no
consensus_*surface) - No Quorum threshold computation (P3.1.2)
- No threshold-signature aggregation (P3.2.x)
- No gossip transport (P3.3.x)
- No clock-merge logic (P3.3.x; this slice ships only the local-process Lamport counter)
- No equivocation penalty application (P3.5.x / λ reputation)
- No VRF leader selection (P3.6.x)