P3.3.1 — Gossip Protocol — IHAVE/IWANT Wire — Verification

Step 5 of the 5-step chain (CLAUDE.md §6). Evidence for the gate.

§1. Commits

Step SHA Subject
1. Audit a194a7fe audit(p3-3-1-gossip-ihave-iwant): inventory surface
2. Contract 525eacf4 contract(p3-3-1-gossip-ihave-iwant): behavioral contract
3. Packet 0d8dfff5 packet(p3-3-1-gossip-ihave-iwant): execution plan
4. Implement e35007dc feat(p3-3-1-gossip-ihave-iwant): IHAVE/IWANT wire + triple-anchor validator — Option C (R89 θ Wave 2)
5. Verify this commit verify(p3-3-1-gossip-ihave-iwant): test evidence

Base SHA: e63a8bcf (origin/main after P3.1.1).

§2. Build gate

npm run build
> colibri@0.0.1 build
> tsc

> colibri@0.0.1 postbuild
> node scripts/copy-migrations.mjs
copy-migrations: copied 8 migration(s)

Status: PASS — TypeScript strict mode compiles cleanly. No errors, no warnings emitted by tsc.

§3. Lint gate

npm run lint
> colibri@0.0.1 lint
> eslint src
(no output — clean exit code 0)

Status: PASS — eslint clean across all src/ files including the two new files in this slice.

§4. Test gate

npm test
Test Suites: 58 passed, 58 total
Tests:       2716 passed, 2716 total

Status: PASS — 58 suites, 2716 tests, zero failures.

Delta from baseline:

  • Baseline at base SHA e63a8bcf: 2687 total (2685 passing + 2 intermittently-flaky in reputation/tools.test.ts and similar — known pre-existing flakes documented in memory). On a clean run the baseline reaches 2687.
  • After P3.3.1: 2716. Net +29 new tests from gossip-wire.test.ts.

4.1 New test enumeration

src/__tests__/domains/consensus/gossip-wire.test.ts — 16 groups, 29 tests:

Group Count Coverage
1. Module surface 2 I1
2. IHAVE / IWANT shape 3 I1, I11, I12
3. Sign / verify roundtrip 3 I2, I3
4. Triple-anchor — all pass 1 I4
5. Triple-anchor — rule_version failure 2 I4, I5
6. Triple-anchor — state_root continuity 3 I4
7. Triple-anchor — fork_id divergence 1 I4
8. Triple-anchor — first-fail short-circuit 2 I4, I13
9. buildIWANT — locally_have 2 I6
10. buildIWANT — seen dedup 2 I6
11. buildIWANT — single-arbiter 1 I7
12. withinRetentionHorizon 3 I8
13. Sig-before-anchor composition 1 I9
14. Phase-0 schema fit 1 I11, I12
15. Static scanner 1 I10
16. No partial acceptance 1 I13

All 29 pass.

§5. Acceptance criteria coverage (from task prompt §P3.3.1)

Criterion Evidence
Wire types IHAVE, IWANT exported Group 1 (signGossipMessage, etc. all importable); types compile-check OK
IHAVE field set Group 2 — every field type confirmed
IWANT field set Group 2
validateTripleAnchor with {valid, failed_anchor} return Groups 4–8, 16
Rule version anchor Group 5
State root continuity anchor (gap > 1 epoch, with known-set fast path) Group 6 (3 tests: gap fail, gap=1 accept, known-set bypass)
Fork id anchor Group 7
All three must pass; single failure rejects entire batch Group 16 + Group 8
Retention horizon: > 2 epochs dropped Group 12 — boundary, beyond, custom retention
Dedup interface seen?: (event_id: Buffer) => boolean Group 10 — both custom and default
All messages Ed25519-signed; sig verified before anchor Group 3 (signing) + Group 13 (sig-before-anchor composition)
Lamport logical clocks only; NO wall-clock Group 15 static scanner over gossip-wire.ts catches any wall-clock
Single-arbiter clause: n=1 no-op Group 11

All 13 criteria covered.

§6. ADR-003 disclosure

This slice ships as the Option C in-process spike per R89.C staging. Concretely:

  • No new npm dependencies added (package.json untouched)
  • No socket I/O, no libp2p, no @chainsafe/libp2p-gossipsub imports
  • The IHAVE / IWANT shapes + validator form a pure data-transformation library
  • Publish/subscribe is deferred to future P3.3.x slices

If ADR-003 later resolves to Option B (libp2p), this slice’s validator and message shapes remain reusable — gossipsub would only replace the publish/subscribe transport, not the triple-anchor logic. So this slice is also compatible with a future Option-B variant.

§7. Forbidden-token static scanner

Group 15 in gossip-wire.test.ts mirrors messages.test.ts Group 9 — same FORBIDDEN_PATTERNS regex array. Runs against src/domains/consensus/gossip-wire.ts after stripping block + line comments. Zero hits at this slice’s commit.

Token classes covered:

  • Math.*, Date.*, new Date (no clock reads)
  • setTimeout / setInterval / setImmediate (no time-based loops)
  • fetch / XMLHttpRequest (no network I/O)
  • require(...fs...) / from ...fs... (no filesystem I/O)
  • crypto.X dotted access (named imports are OK; we use named sign, verify, KeyObject)
  • process.hrtime / process.nextTick (no host timing)
  • await / async function (no async)
  • Float literals (bigint arithmetic only)
  • [native code] (anti-stringify probe)

The signing path uses node:crypto named imports sign and verify — which the regex \bcrypto\.[A-Za-z_]\w* correctly does NOT match.

§8. Notable design decisions

8.1 The msg_epoch field

The acceptance criteria from §P3.3.1 list state_root_pre, rule_version_hash, fork_id, sender_id, timestamp_logical, signature for IHAVE — but not msg_epoch. We added msg_epoch because:

  1. The retention horizon helper signature (per the prompt) is withinRetentionHorizon(msg_timestamp_logical, current_epoch, retention_epochs) — but timestamp_logical is a Lamport counter (s08 + consensus.md §181), not an epoch. The prompt’s gotcha §1122-1124 explicitly says “Retention is computed in epochs, so the helper needs both.”
  2. The triple-anchor’s state_root continuity check needs “what epoch is the sender claiming to be in” to apply the “no gaps > 1 epoch” rule.

Two options were considered:

  • (a) Add msg_epoch as an external argument to validator and retention helper, leaving the IHAVE shape unchanged
  • (b) Add msg_epoch to the IHAVE shape itself

Chose (b) — the IHAVE is a self-contained wire message; deriving the sender’s epoch from external context defeats the validator’s purity. The Lamport timestamp_logical rides along separately for vote-ordering.

This is a documented design extension, not a deviation from acceptance criteria — the criteria don’t forbid additional fields. Documented in gossip-wire.ts §3 JSDoc.

8.2 Sign/verify locally in gossip-wire.ts

messages.ts’s signMessage / verifySignature are typed against Vote | Commit | Reveal | ViewChange — adding IHAVE/IWANT to that union would require editing messages.ts, which is out-of-scope for this slice (P3.1.1 is sealed).

So gossip-wire.ts declares local signGossipMessage / verifyGossipMessage that reuse canonicalSerialize (the canonical encoder is type-discriminator-agnostic at runtime — it walks the value graph structurally). The compile-time bridge is a documented as unknown as ConsensusMessage cast inside canonicalUnsigned. The cast is isolated to that one function and called out in JSDoc.

Verified at the test level: Group 3 demonstrates roundtrip sign/verify across keypairs for both IHAVE and IWANT, including mutation-detection for every non-signature field (including event_ids order, state_root_pre bytes, msg_epoch value).

8.3 known_state_roots: ReadonlySet<string> keying

Used hex-encoded strings for set membership because Buffer identity is by reference, not bytes — set.has(bufA) where bufA has the same bytes as a previously-inserted Buffer but is a different object would return false. Hex-keying makes membership byte-equality. Receiver pre-encodes its known roots via buf.toString('hex') before populating the set.

8.4 buildIWANT options + signature placeholder

buildIWANT returns an IWANT with signature: Buffer.alloc(0). The caller signs externally via signGossipMessage and overwrites the field. This separation lets buildIWANT stay a pure transformer without depending on private-key injection at every call site (which would be inappropriate — the IWANT-builder doesn’t own keys; the gossip-publisher does).

§9. Files in this slice

Path Lines Role
src/domains/consensus/gossip-wire.ts 432 Implementation
src/__tests__/domains/consensus/gossip-wire.test.ts 415 Tests (29 tests across 16 groups)
docs/audits/p3-3-1-gossip-ihave-iwant-audit.md 243 Step 1
docs/contracts/p3-3-1-gossip-ihave-iwant-contract.md 253 Step 2
docs/packets/p3-3-1-gossip-ihave-iwant-packet.md 260 Step 3
docs/verification/p3-3-1-gossip-ihave-iwant-verification.md this file Step 5

§10. Sign-off

5-step chain complete. All three gates (build + lint + test) green. No regressions. ADR-003 Option C disclosure included. Ready for PM review and merge.


Back to top

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

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