Skip to content

fix(relay): force monotonic created_at on the NIP-IA archive snapshot - #5117

Open
AI-OWEN wants to merge 1 commit into
block:mainfrom
AI-OWEN:fix/nipia-snapshot-monotonic-created-at
Open

fix(relay): force monotonic created_at on the NIP-IA archive snapshot#5117
AI-OWEN wants to merge 1 commit into
block:mainfrom
AI-OWEN:fix/nipia-snapshot-monotonic-created-at

Conversation

@AI-OWEN

@AI-OWEN AI-OWEN commented Aug 6, 2026

Copy link
Copy Markdown

Fixes #3848.

The bug

A burst of NIP-IA archive/unarchive mutations publishes several kind:13535 snapshots inside one wall-clock second. created_at is second-resolution, so NIP-16 resolves the tie on the lowest event id (buzz-db/src/lib.rs"On same-second tie, lowest event id (lexicographic) wins"). That id is a content hash, so which snapshot survives is effectively arbitrary — it is not the first or the last, and an intermediate list can remain authoritative.

Every kind:8002 delta and every archived_identities row is accepted. Nothing anywhere reports the mismatch.

Why it is worse than a stale count

useIsArchivedPredicate (desktop/src/features/identity-archive/hooks.ts) filters mention autocomplete, the new-message recipient picker, search results and the add-member dialog on this snapshot — useMentions.ts:246 drops archived pubkeys from candidates entirely.

So a lost snapshot update leaves those identities fully selectable indefinitely, and from the operator's side the archive looks like it silently failed: the receipt says ok, the table is right, the UI keeps offering the identity.

Seen twice in the field. Most recently, six archive requests ~330 ms apart grew archived_identities 7 → 12 while the published snapshot stayed at 7 — and stayed there for six days across relay restarts, because there is no error, no warning and no drift check in the path. It was only caught by comparing select count(*) from archived_identities against buzz agents archived by hand.

The fix

Force the snapshot's created_at strictly past the previous snapshot's, exactly as emit_addressable_discovery_event and publish_dm_visibility_snapshot already do for their replaceable snapshots.

The arithmetic is extracted into a small pure helper, monotonic_snapshot_created_at(now, previous), so the burst behaviour is unit-testable without a database.

Deliberately left alone: the two existing call sites keep their inline copies. Their semantics are identical to the helper, but migrating them is unrelated to this bug and I would rather keep the diff to what #3848 reports. Happy to fold them in if you would prefer one shared guard.

Tests

crates/buzz-relay/src/handlers/side_effects.rs:

  • snapshots_within_one_second_get_strictly_increasing_timestamps — the case from the issue: six updates at a fixed now, asserting T through T+5 and strict monotonicity.
  • first_snapshot_uses_wall_clock — no previous snapshot.
  • snapshot_follows_wall_clock_once_it_overtakes_the_previous — a burst can push created_at ahead of the clock; once real time passes it, the snapshot returns to wall-clock rather than drifting further ahead.
cargo test -p buzz-relay --lib handlers::side_effects   8 passed
cargo clippy -p buzz-relay --all-targets               clean
cargo fmt --all -- --check                             clean

The full cargo test -p buzz-relay --lib run is 852 passed / 9 failed in my environment. All nine failures are api::admin and api::media tests that need Postgres (Sqlx(PoolTimedOut)); I confirmed the identical nine fail on unmodified origin/main here, so they are environmental and not from this change. Worth a second look on CI.

Related

#4617 is the other half of this. handlers/identity_archive.rs early-returns on if !changed before reaching publish_nipia_archival_list, so re-archiving an already-archived identity is a no-op that never republishes — which is why an operator stuck in this state cannot repair it by simply re-archiving the missing entries. The two fixes are complementary: monotonic created_at prevents the loss, unconditional republish makes it recoverable.

For anyone currently stuck, the workaround is one state-changing cycle, since publish_nipia_archival_list rebuilds the full list from list_archived() rather than applying a delta:

buzz agents unarchive <any-already-archived-hex>
sleep 5
buzz agents archive   <same-hex> --reason bot-rebuilt

Rapid archive/unarchive mutations publish several kind:13535 snapshots
inside one wall-clock second. created_at is second-resolution, so NIP-16
breaks the tie on the lowest event id — a content hash, and therefore an
arbitrary winner. An intermediate snapshot can stay authoritative while
every kind:8002 delta and every archived_identities row is accepted, and
nothing surfaces the mismatch: no error, no warning, no drift check.

Observed in the field twice. Six archive requests ~330ms apart grew the
table 7 -> 12 while the published snapshot stayed at 7 and remained there
for six days across relay restarts. Because Desktop filters mention
autocomplete, the DM recipient picker, search and the add-member dialog
on this snapshot, the five lost identities stayed fully selectable, and
the archive looked like it had silently failed.

Force the snapshot's created_at strictly past the previous snapshot's,
the same guard emit_addressable_discovery_event and
publish_dm_visibility_snapshot already apply. The shared arithmetic is
extracted into monotonic_snapshot_created_at so it can be unit-tested
without a database; the two existing call sites are left inline and
unchanged to keep this diff to the reported bug.

Regression coverage simulates six snapshot updates in one wall-clock
second and asserts strictly increasing timestamps from T through T+5,
plus first-publish and clock-overtakes-previous cases.

Refs: block#3848
Signed-off-by: AI-OWEN <AI-OWEN@users.noreply.github.com>
@AI-OWEN
AI-OWEN requested a review from a team as a code owner August 6, 2026 22:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NIP-IA archive snapshot can lose rapid same-second updates

1 participant