The ~19/40k event shortfall in the geode↔geode sync is not a geode event-loss bug: geode is proven lossless across the store, the concurrent IngestQueue pipeline, RelaySession (OK-true only post-commit), and the real MirrorWorker WebSocket path (50k/50k). The shortfall is in the benchmark's hand-rolled fetchByIds+publish delta transfer. Points the fix at the harness (or at driving convergence through geode's real mirror) rather than geode. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EZeWww5TJnzBZKPoc6mvU
4.7 KiB
relayBench sync fairness: account for NIP-62 / NIP-09 / NIP-40
Status: note / not yet implemented. Captures a measurement bug found while validating the negentropy work, so the next person doesn't re-derive it.
The problem
SyncBenchmark.effectiveEvents builds the "reference" set the harness
reconciles against by collapsing replaceable/addressable kinds to their
newest version — and nothing else. But a spec-compliant relay stores strictly
less than that, because it also honors:
- NIP-62 Request to Vanish (kind 62). A
["relay","ALL_RELAYS"](or this-relay) vanish request means the relay deletes all events from that pubkey. geode implements this (RightToVanishModule); strfry does not. - NIP-09 deletions (kind 5). Both geode and strfry apply these, but the harness reference keeps the deleted events and the deletion.
- NIP-40 expiration (
expirationtag). geode drops expired events; strfry's support differs.
So the harness reference over-counts: it retains events a compliant relay is required to have removed.
The symptom
The sync pair is flagged "did not converge ✗" even when the relays are
behaving correctly. In the 1M-corpus run this showed as geode "missing" 28
events vs the reference — all traced to a single NIP-62 vanish pubkey
(fa0778c1ccbb…, ["relay","ALL_RELAYS"]): geode correctly purged that user's
31 events, strfry kept them, and the harness expected them present.
Two concrete distortions:
- False non-convergence.
converged(SyncBenchmark ~line 273) compares the relays' stored sets to the unfiltered reference, so a NIP-62/40-compliant relay can never converge against a corpus that contains vanish/expiry events. - Inflated reconcile cost. The identical-set reconcile uses the unfiltered
effectiveas the initiator's local set. Those phantom events are real leaves negentropy must hunt, so geode's steady-state reconcile ran 3 rounds / ~520 ms instead of the 1 round / ~110 ms it would take against a matching set — which masks negentropy speedups (v1.1.1/v1.2.0) at the wire behind the extra round-trips.
The census (first 200k of the damus 1M corpus)
- NIP-62 vanish requests: 7 distinct pubkeys
- NIP-09 deletions: 0
- NIP-40 expiration-tagged events: 381
The fix (when someone picks this up)
Make the reference model what a compliant relay actually stores, in
effectiveEvents (or a new compliantEffective):
- Collect vanish pubkeys (kind 62); drop every event from them, and the vanish
request itself, whose
created_atis at/under the vanish request's time (or unconditionally forALL_RELAYS). - Apply kind-5 deletions: drop referenced
e/atargets authored by the same pubkey. - Drop events whose
expirationtag is in the past (needs a fixed "now").
Then a compliant relay (geode) converges cleanly, and a relay that ignores these NIPs (strfry) is correctly flagged as retaining events it should have dropped — which is the honest comparison, not a geode penalty. The steady-state reconcile also becomes a real 1-round measurement, so negentropy improvements show at the wire.
Until then, a clean wire-level sync measurement needs a pre-filtered corpus (drop vanish-pubkey events + expiry-tagged events); see the geode↔geode run that accompanies this note.
Follow-up: a ~0.05% shortfall is in the harness delta transfer, not geode
Even on the pre-filtered corpus, the geode↔geode run showed one side ending ~19 of ~40k events short (all kind-1, empty tags, scattered across pubkeys). That is not a geode event-loss bug — geode is proven lossless at every layer:
quartz …store.BatchInsertLossTest— sequentialbatchInsertEvents, 199,612 in / 199,612 stored.quartz …relay.prodbench.ConcurrentIngestLossTest— the fullIngestQueuepipeline (parallel verify, greedy-drain group commit, a concurrent FTS catch-up worker on the pool writer, windowed concurrent submits) — 0 lost.RelaySession.handleEventsendsOK trueonly from the writer callback, i.e. after the row commits.geode …mirror.MirrorSyncLossTest— the real relay-to-relay path (upstreamKtorRelayWebSocket ⇄MirrorWorkerOkHttp client, trustedskipVerifyingest) delivers 50,000 / 50,000, 0 missing.
So the shortfall is an artifact of the benchmark's hand-rolled delta transfer
(SyncBenchmark.fetchByIds + IngestBenchmark.publishSlice's windowed
publish/OK-counting), which stands in for what strfry sync does. geode's own
sync is lossless. The proper fix is to harden that harness path — or, better,
drive convergence through geode's real MirrorWorker instead of simulating it —
so the benchmark stops reporting phantom loss.