From 3b4ed48df18f14a4e7fafdc0044ee285549e48b4 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 24 Jun 2026 10:53:24 +0100 Subject: [PATCH] fix(nostr): ignore vanish in disrespector mode --- docs/explanation/deletion-requests.md | 14 +++- src/nostr/lifecycle/deletion/policy.rs | 3 +- src/nostr/lifecycle/deletion/service.rs | 10 +++ tests/nip09_disrespector.rs | 90 +++++++++++++++++++++++-- 4 files changed, 109 insertions(+), 8 deletions(-) diff --git a/docs/explanation/deletion-requests.md b/docs/explanation/deletion-requests.md index d7ad8ca..d74f996 100644 --- a/docs/explanation/deletion-requests.md +++ b/docs/explanation/deletion-requests.md @@ -31,8 +31,16 @@ relay policy code: - holding DB uses `process_nip09(false)` and `process_nip62(false)` (`src/nostr/holding.rs`) -This keeps deletion behavior auditable and consistent across cascade, holding, -archive, and recovery flows. +This keeps deletion behavior auditable and prevents the storage backends from +silently applying deletion semantics outside ngit-grasp policy code. + +The full cascade, holding, archive, and recovery flow currently applies to +NIP-09 repository/content deletions and operator-driven blacklist/whitelist +deletions. NIP-62 request-to-vanish handling is narrower: in normal mode a +targeted vanish request records a vanish tombstone, removes that author's events +from the main database, and evicts that author's purgatory entries. It does not +currently move those events through the holding database or archive/recovery +pipeline. ## The Left-Pad Problem @@ -677,7 +685,7 @@ Deletion support is **conditionally advertised** in NIP-11 relay information (implemented in [`src/http/nip11.rs`](../../src/http/nip11.rs)): - **When `deletion_request_disrespector = false`:** include `9` (`"deletion"`) and `62` (`"request to vanish"`) in the supported NIPs array -- **When `deletion_request_disrespector = true`:** do NOT include `9` or `62` (archival mode doesn't honor deletions) +- **When `deletion_request_disrespector = true`:** do NOT include `9` or `62` (archival mode stores but does not honor deletion or vanish requests) This allows clients to discover whether a relay respects deletion requests. diff --git a/src/nostr/lifecycle/deletion/policy.rs b/src/nostr/lifecycle/deletion/policy.rs index 80a193d..1dd3067 100644 --- a/src/nostr/lifecycle/deletion/policy.rs +++ b/src/nostr/lifecycle/deletion/policy.rs @@ -73,7 +73,8 @@ impl DeletionPolicy { /// main database) so clients see an OK and the request is preserved, but it /// is NOT acted upon — no purgatory eviction, no main-DB deletion, and no /// tombstone recording. The targeted events therefore remain fully - /// accessible. This only affects NIP-09 user-initiated deletions. + /// accessible. The same archival-mode contract is applied to NIP-62 vanish + /// requests in [`DeletionService::handle_vanish`](super::service::DeletionService::handle_vanish). pub async fn handle(&self, event: &Event) -> WritePolicyResult { // Archival mode: store the deletion request but do not process it. if self.ctx.config.deletion_request_disrespector { diff --git a/src/nostr/lifecycle/deletion/service.rs b/src/nostr/lifecycle/deletion/service.rs index ac49472..4b1c6a1 100644 --- a/src/nostr/lifecycle/deletion/service.rs +++ b/src/nostr/lifecycle/deletion/service.rs @@ -78,6 +78,16 @@ impl DeletionService { } pub async fn handle_vanish(&self, event: &Event) -> WritePolicyResult { + // Archival mode: store the vanish request but do not process it. + if self.ctx.config.deletion_request_disrespector { + tracing::info!( + event_id = %event.id.to_hex(), + author = %event.pubkey.to_hex(), + "Disrespector mode: storing NIP-62 vanish request without acting on it" + ); + return WritePolicyResult::Accept; + } + // Only honour requests that target this relay (or all relays). We do not // configure a relay_url on the database, matching the historical // behaviour where only ALL_RELAYS requests were actioned; but we accept diff --git a/tests/nip09_disrespector.rs b/tests/nip09_disrespector.rs index 70d4009..4e0f220 100644 --- a/tests/nip09_disrespector.rs +++ b/tests/nip09_disrespector.rs @@ -9,15 +9,16 @@ //! purgatory based, so the disrespector simply accepts the kind-5 request and //! does nothing with it. The two properties under test are: //! -//! 1. **Disrespector accepts but ignores deletion** — a kind-5 request gets an -//! `OK`, yet the targeted announcement remains queryable afterwards. +//! 1. **Disrespector accepts but ignores deletion/vanish** — a kind-5 or +//! kind-62 request gets an `OK`, yet the targeted/author events remain +//! queryable afterwards and no vanish tombstone is laid down. //! 2. **NIP-11 advertisement** — a disrespector relay omits `9` and `62` from //! `supported_nips`; a normal relay advertises both. //! //! The normal-mode baseline ("a default relay honours the deletion") lives in //! `nip09_validation.rs` as `normal_mode_honours_deletion`. Shared helpers //! (`publish_served_repo`, `announcement_served_by_coordinate`, -//! `announcement_coordinate`, `build_deletion`) live in +//! `announcement_coordinate`, `build_deletion`, `build_vanish`) live in //! `tests/common/nip09_helpers.rs`. //! //! # Running @@ -30,7 +31,7 @@ mod common; use common::{ - announcement_coordinate, announcement_served_by_coordinate, build_deletion, + announcement_coordinate, announcement_served_by_coordinate, build_deletion, build_vanish, publish_served_repo, TestRelay, }; @@ -98,6 +99,87 @@ async fn disrespector_accepts_but_ignores_deletion() { ); } +/// Disrespector mode: the kind-62 vanish request is accepted (OK) but ignored — +/// the author's events remain queryable and future writes are not blocked by a +/// vanish tombstone. +#[tokio::test] +async fn disrespector_accepts_but_ignores_vanish() { + let relay = TestRelay::start_with_deletion_disrespector().await; + let client = AuditClient::new(relay.url(), AuditConfig::isolated()) + .await + .expect("create audit client"); + + let (announcement, repo_id) = publish_served_repo(&client, "disrespector-vanish").await; + let vanish = build_vanish(&client, None); + let vanish_id = vanish.id; + + // The relay MUST accept the vanish request (archival mode still stores/acks it). + client + .send_event(vanish) + .await + .expect("disrespector relay should OK the kind-62 vanish request"); + + tokio::time::sleep(std::time::Duration::from_millis(300)).await; + + let vanish_served = client + .is_event_on_relay(vanish_id) + .await + .expect("query kind-62 vanish after send"); + let target_served_by_id = client + .is_event_on_relay(announcement.id) + .await + .expect("query announcement by id after vanish"); + let target_served_by_coordinate = + announcement_served_by_coordinate(&client, &announcement, &repo_id).await; + + let later_issue = client + .create_issue( + &announcement, + "Post-ignored-vanish issue", + "still allowed", + vec![], + ) + .expect("build post-vanish issue"); + let later_issue_id = later_issue.id; + let later_issue_result = client.send_event(later_issue).await; + + tokio::time::sleep(std::time::Duration::from_millis(300)).await; + let later_issue_served = client + .is_event_on_relay(later_issue_id) + .await + .expect("query post-ignored-vanish issue"); + + relay.stop().await; + + assert!( + vanish_served, + "disrespector relay must store/serve the kind-62 vanish request itself (id {})", + vanish_id + ); + assert!( + target_served_by_id, + "disrespector relay must ignore vanish: announcement {} should still be \ + queryable by id", + announcement.id + ); + assert!( + target_served_by_coordinate, + "disrespector relay must ignore vanish: announcement {} should still be \ + served for an author+kind+identifier query", + announcement.id + ); + assert!( + later_issue_result.is_ok(), + "disrespector relay must not lay down a vanish tombstone; later write was {:?}", + later_issue_result + ); + assert!( + later_issue_served, + "later event {} must be served because disrespector mode ignored vanish", + later_issue_id + ); +} + /// Fetch the NIP-11 relay information document via HTTP and return the list of /// advertised `supported_nips`. ///