Skip to content

fix(relay): NIP-09 a-tag deletion soft-deletes addressable rows (#714)#716

Closed
tlongwell-block wants to merge 1 commit into
mainfrom
dawn/relay-a-tag-deletion
Closed

fix(relay): NIP-09 a-tag deletion soft-deletes addressable rows (#714)#716
tlongwell-block wants to merge 1 commit into
mainfrom
dawn/relay-a-tag-deletion

Conversation

@tlongwell-block
Copy link
Copy Markdown
Collaborator

Closes #714.

What

handle_a_tag_deletion in sprout-relay previously only handled KIND_WORKFLOW_DEF. Every other addressable kind — including kind:30023 (NIP-23 long-form) — fell through to a debug-only _ => arm. The kind:5 event was accepted and stored, but the target row was never soft-deleted, so REQs kept returning it.

This PR generalises the side-effect path to soft-delete any parameterized-replaceable row by coordinate.

How

  • sprout_db::event::soft_delete_by_coordinate(pool, kind, pubkey, d_tag) -> Result<bool> — new helper mirroring soft_delete_event. WHERE clause is lifted from replace_parameterized_event (the NIP-33 replacement path) so coordinate semantics stay consistent: channel_id is intentionally not in the key. Wrapped on Db for caller ergonomics.
  • handle_a_tag_deletion — new arm k if is_parameterized_replaceable(k) => between the workflow branch and _ =>. Workflow's bespoke deletion keeps its precedence (delete_workflow only touches the workflows table by design — the underlying kind:30620 row in events is left alive, see Note below). For every other NIP-33 kind, the new helper runs.
  • e2e_long_form.rs::test_long_form_a_tag_deletion — full round-trip: publish kind:30023 → verify queryable → emit kind:5 with ["a", "30023:pubkey:d-tag"] → verify REQ returns 0. #[ignore]'d, matching the rest of that file.

Read-path filtering in req.rs already gates on deleted_at IS NULL, so once the soft-delete fires, REQs hide the row automatically. No filter changes needed.

Note on workflow

delete_workflow (workflow's existing branch) only deletes from workflows; the underlying kind:30620 row in events stays alive. Same bug shape as #714 for a different kind, but fixing it could change workflow lifecycle assumptions. Scoped narrowly here — my new arm fires for all NIP-33 kinds except workflow. Happy to file a separate issue if anyone agrees that's a gap worth closing.

Verified

  • cargo build -p sprout-db -p sprout-relay -p sprout-test-client
  • cargo test -p sprout-db --lib → 64/64 (0 failures, 10 ignored as before)
  • cargo test -p sprout-relay --lib → 192/192
  • cargo clippy -p sprout-db -p sprout-relay -p sprout-test-client --all-targets -- -D warnings
  • cargo fmt --check

The new e2e case (test_long_form_a_tag_deletion) needs a live relay; CI will exercise it.

`handle_a_tag_deletion` in `crates/sprout-relay/src/handlers/side_effects.rs`
previously only had a side-effect branch for `KIND_WORKFLOW_DEF`. Every other
addressable kind — including kind:30023 (NIP-23 long-form) — fell through to
the `_ =>` arm which only emitted a debug log. The kind:5 deletion event was
accepted and stored, but the target row was never soft-deleted, so subsequent
REQs still returned it.

This was discovered while building `sprout notes` (CLI for NIP-23 long-form
content): the semantically correct way to delete a parameterized-replaceable
note is by `a` coordinate, but until this fix the relay silently no-op'd.
The CLI shipped a dual-tag workaround (`["e", id]` + `["a", coord]`); once
this lands, the a-tag becomes load-bearing.

Changes:

- `sprout_db::event::soft_delete_by_coordinate(pool, kind, pubkey, d_tag)`:
  new helper mirroring `soft_delete_event`. WHERE clause matches the NIP-33
  replacement key used by `replace_parameterized_event` exactly — `channel_id`
  is intentionally not in the key (NIP-33 replacement is global per the
  spec; `channel_id` is for query scoping, not identity).
- `Db::soft_delete_by_coordinate` wrapper for caller ergonomics.
- `handle_a_tag_deletion`: new arm `k if is_parameterized_replaceable(k) =>`
  between the existing workflow branch and `_ =>`. Workflow keeps its
  bespoke deletion (`delete_workflow` only touches `workflows`; the
  underlying kind:30620 row in `events` is left alive by design — out of
  scope for this fix). For every other NIP-33 kind the new helper runs and
  the row is soft-deleted. `_ =>` is now correctly scoped to "non-NIP-33
  kind" only.
- `e2e_long_form.rs::test_long_form_a_tag_deletion`: full round-trip
  regression test (publish → verify queryable → a-tag kind:5 →
  verify REQ returns 0). `#[ignore]`'d like the rest of that file
  (needs a running relay).

Read-path filtering in `req.rs` already gates on `deleted_at IS NULL`, so
once the side-effect path soft-deletes the row, REQs hide it automatically.
No filter changes needed.

Verified locally:
- `cargo build -p sprout-db -p sprout-relay -p sprout-test-client`
- `cargo test -p sprout-db --lib` → 64/64
- `cargo test -p sprout-relay --lib` → 192/192
- `cargo clippy -p sprout-db -p sprout-relay -p sprout-test-client --all-targets -- -D warnings`
- `cargo fmt --check`
- New e2e test compiles cleanly; runs against a live relay via CI.

Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
@tlongwell-block
Copy link
Copy Markdown
Collaborator Author

Superseded by #719 (combined into one PR per Tyler). Relay a-tag fix carried over verbatim.

@tlongwell-block tlongwell-block deleted the dawn/relay-a-tag-deletion branch May 22, 2026 19:06
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.

relay: NIP-09 a-tag deletion is a no-op for kind:30023 (and all non-workflow addressables)

1 participant