Skip to content

relay: a publish racing a kind-62 vanish request survives it #975

Open

Description

@chappie-daemon

Low priority. Inherited from the TypeScript capture, so leaving it open regresses nothing against the relay as it ships today; it needs a concurrency window (a publish passing the vanish check while a kind-62 completes) and the fix has an open design question, which is why it is a decision rather than a patch. Tracked in the Nostream 4.0 Rust Rewrite project as Todo.

A publish that has already passed the vanish check can store its event after a kind-62 request finishes, and nothing removes it afterwards.

The window

handlers/event.rs:462 checks is_vanished once per inbound event, before the strategy runs. Each message is handled in its own task (relay.rs's JoinSet), so this interleaving is possible:

  1. publish from author A passes is_vanished → false,
  2. A's kind-62 vanish runs delete_by_pubkey_except_kinds (line 33), creates the request (37), set_vanished(A, true) (42),
  3. the publish from step 1 stores its event.

Reads exclude deleted rows, not vanished authors, so the event from step 3 is readable indefinitely. set_vanished moving earlier would not close the window: the publish already passed the check.

Found by the static review of job 011-relay-strategies at 0dac5fe (crates/nostream-relay/src/strategies/vanish.rs:33-42).

Why it is not that job's defect

The capture has the same window, in the same order: vanish-event-strategy.ts deletes, creates, then calls setVanished, and event-message-handler.ts:385 checks isVanished once before dispatch. The port reproduces the capture exactly, which is what a parity job owes. Fixing it here would be a deviation that needs its own decision.

What a fix has to decide

  • Whether to coordinate per pubkey (a write lock or a per-author queue) or to filter vanished authors at read time — the second is cheaper but changes query shape and index needs.
  • Whether an in-flight event should fail with a policy refusal or be stored and then swept.
  • What the capture's users experience today, since any real deployment of the TypeScript relay has this behaviour: a fix is an improvement, not a parity repair, and should be recorded as one.

Expected cost: $2.00 — a small concurrency fix plus tests, but the read-path alternative makes it a design decision first.

Activity

  1. added
    bugSomething isn't working
    rewritenostream 4.0 Rust/LMDB rewrite
    rustRust implementation work
    factoryBuilt by the software factory
    on Oct 24, 2026
  2. chappie-daemon commented on Nov 3, 2026

    @chappie-daemon
    CollaboratorAuthor

    Do not pick this up as a patch job. This issue opens by saying it "needs a concurrency window … and the fix has an open design question, which is why it is a decision rather than a patch" — and that is still true. A recon→spec→build run would have the factory settle an ADR question by accident, which is how a design decision gets made by whoever happened to be next in the queue.

    First step is an ADR, in docs/rewrite/decisions/, settling how a publish that has already passed is_vanished is prevented from storing after a kind-62 completes — whether a second check at the write seam, a vanish generation counter, or an ordering guarantee in the store. Only after that lands does this become a buildable issue.

    It sits at the top of the Todo column by number, so it is the first thing a queue reader hits. That is why this is written here rather than in a review folder.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingfactoryBuilt by the software factoryneeds-adrRequires a decision record before it can be builtrewritenostream 4.0 Rust/LMDB rewriterustRust implementation work

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions