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 inreputation/tools.test.tsand 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.jsonuntouched) - No socket I/O, no libp2p, no
@chainsafe/libp2p-gossipsubimports - 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.Xdotted access (named imports are OK; we use namedsign,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:
- 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.” - 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_epochas an external argument to validator and retention helper, leaving the IHAVE shape unchanged - (b) Add
msg_epochto 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.