Skip to content

fix(relay): stamp each NIP-IA archive list strictly after the previous one - #9476

Merged
brow merged 2 commits into
mainfrom
nipia-snapshot-monotonic
Nov 14, 2026
Merged

brow merged 2 commits into
mainfrom
nipia-snapshot-monotonic

Conversation

@brow

@brow brow commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator

Why

When an admin archives or unarchives an identity, the relay publishes a new archived identities list (kind 13535). This list replaces the previous one. It contains all the identities that are archived in the community. Clients use it to hide archived agents from pickers and search.

The list can be large (about 228 KB on Block's Builderlab), and it seldom changes. Thus the mobile app wants to download it only when it changed. To do this, the app keeps the last list it received, and asks the relay only for a list that is newer than that one (since its created_at plus one, in #9445 archive list cache).

This works only if each new list has a created_at that is later than the previous list's. Today the relay does not always do this:

  • The relay calculates the new list's created_at as "the current list's created_at plus one second". But it reads the current list before it gets the lock that controls replacement.
  • Thus, when two archive requests occur at the same time, two publishers can read the same current list. Then both sign a list with the same created_at. The store keeps the one with the lower event id.
  • A client that already has the other list does not see the replacement, because it asks only for lists with a later created_at. It continues to hide or show the wrong agents until the next archive change or an app restart.
  • With many concurrent publishers it is worse: a publisher can lose that tie again and again, and then stop with failed to publish kind:13535 after 8 concurrent replacements. Then no list contains the latest archive change.

What changes

  • New Db::replace_addressable_event_after_head. It gets the replacement lock first. Then it reads the current list. Then it signs and stores the new list. Thus only one publisher at a time calculates the next created_at, and each list is stamped strictly later than the previous one.
  • publish_nipia_archival_list uses this method. The check that publishes again when the archive state changed during a publish does not change.
  • NIP-IA now makes this a rule: a relay MUST publish each kind:13535 with a created_at strictly greater than its previous one. Clients MAY then ask only for lists since their list's created_at plus one.

Validation

New relay test: 16 concurrent publishers all complete, and each stored list (superseded rows included) has a strictly later created_at. On main the same test fails, because a publisher uses all its retries. The test needs Postgres and Redis, so it is in postgres_tests and runs in the PostgreSQL Tests lane; a missing database or Redis makes it fail, not pass. The existing archive tests pass against a local Postgres, including the stale-publisher test. just test passes all 160 steps except one NIP-FI session deadline test (deadline_refused_permit_still_queues_one_denial_when_cancel_wins), which this change does not touch and which passes 5 of 5 reruns alone. Clippy passes.

Regressions

None expected.

  • A publisher now holds the replacement lock a little longer: for one read and one signature. Only archive-list writes for the same community wait on this lock, and archive changes are rare.
  • The new Db method is an addition. replace_addressable_event and its callers do not change.
  • Until this is deployed, relays keep the old behavior. #9445 archive list cache accepts that same-second case on its own.

Accepted limitation: a database restore. The rule holds only while the relay keeps its newest list. If an operator restores the relay from a backup, the relay calculates the next created_at from the restored list (max(restored + 1, now)). If earlier fast publishes went ahead of the clock, that value can be at or below a created_at that a client already has. That client then stays stale until a list passes its cached created_at, or the app restarts. This change adds no repair for a restore.

Move the concurrent-publisher test under postgres_tests with an ignore
reason so the PostgreSQL Tests lane discovers it, and fail instead of pass
when Postgres or Redis is missing. Qualify the NIP-IA since + 1
reconciliation for a relay restored to an older database.

Signed-off-by: Tom Brow <[email protected]>
@brow
brow marked this pull request as ready for review November 13, 2026 22:26
@brow
brow requested a review from a team as a code owner November 13, 2026 22:26
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-13T22:28:49.048658Z 89ad92c Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@brow
brow deployed to codex-review November 13, 2026 22:27 — with GitHub Actions Active
@brow
brow enabled auto-merge (squash) November 13, 2026 22:29

@kazemek21 kazemek21 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kyle reviewed this PR and the Nexus review-tracker notes and approved with no comments.

Posted by Codex (gpt-6.1-sol) on behalf of kylez

@brow
brow merged commit fc29f60 into main Nov 14, 2026
97 checks passed
@brow
brow deleted the nipia-snapshot-monotonic branch November 14, 2026 17:21

This branch was successfully deployed

1 active deployment
codex-review — 89ad92c9 Deployed Nov 13, 2026 by brow via Run Codex Security Review #15100
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.

3 participants