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)
  • Buffer values serialize as lowercase hex strings (no 0x prefix, never base64)
  • EquivocationProof carries 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_type on 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_type discriminator (Vote is leaf; 4 wire types discriminated) — messages.ts §4
  • All bigint for ids/counts; Buffer for hashes/sigs — types in messages.ts §4
  • Ed25519 sign+verify roundtrip tested — Group 5
  • Canonical serialization delegates to κ P1.5.4 (no duplicate impl) — canonicalSerializeUnchecked calls canonicalize from ../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 test pass — §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:

  1. canonicalSerialize(vote) × 1000 — Buffer.compare(reference, actual) === 0 for every iteration
  2. hashMessage(vote) × 1000 — same digest every time
  3. buildEquivocationProof(...) × 1000 — evidence_hash identical 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') from node: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 true
  • verifySignature(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 key kpA; verification succeeds.
  • Node D produces two conflicting signed votes on the same round with merkle_root=0xab12... and merkle_root=0xcafe... under kpD.
  • buildEquivocationProof produces a 32-byte evidence_hash.
  • Re-verification: both embedded votes verify under kpD.publicKey.
  • Reconstruction: a second buildEquivocationProof call with the same inputs produces the SAME evidence_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 Vote shape)
  • P3.3.1 — Gossip Envelope (consumes the 4 wire types + their canonical bytes)
  • P3.4.1 — Signed Time Anchors (reuses signMessage / verifySignature path)
  • P3.5.1 — Equivocation Detection (consumes EquivocationProof + reads conflicting tuples)
  • P3.6.1 — VRF Leader Selection (commit will plug into Commit proof 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)

Back to top

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

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