diff --git a/docs/explanation/repository-lifecycle.md b/docs/explanation/repository-lifecycle.md index 7a18db7..349fdb2 100644 --- a/docs/explanation/repository-lifecycle.md +++ b/docs/explanation/repository-lifecycle.md @@ -328,11 +328,16 @@ When `deletion_request_disrespector = true`: ``` 1. Kind 5 deletion request arrives ↓ -2. Store deletion request event in main database +2. Check idempotency: + - if every actionable `e`/`a` target is already covered by an existing + same-author kind-5 request, return a duplicate success response without + storing the new deletion event ↓ -3. Do NOT process deletion +3. Store deletion request event in main database ↓ -4. Repository and events remain fully accessible +4. Do NOT process deletion + ↓ +5. Repository and events remain fully accessible ↓ Result: Archival relay preserves all content ``` @@ -349,9 +354,11 @@ NIP-62 vanish requests. It does NOT prevent blacklist-triggered deletions. **Implementation Note:** Implemented. The LMDB backend's automatic NIP-09 and NIP-62 processing is disabled (`process_nip09(false)`, `process_nip62(false)`); ngit-grasp owns deletion handling in relay policy code, which short-circuits -when `deletion_request_disrespector` is set — storing but not acting on kind-5 -or kind-62 requests. NIP-09 and NIP-62 are omitted from the NIP-11 -`supported_nips` list in this mode. +already-covered NIP-09 deletions before the `deletion_request_disrespector` +archival-mode branch. When `deletion_request_disrespector` is set, new kind-5 +requests are stored but not acted on, and kind-62 requests are stored but not +acted on. NIP-09 and NIP-62 are omitted from the NIP-11 `supported_nips` list +in this mode. ## Recovery Mechanism diff --git a/src/nostr/lifecycle/deletion/policy.rs b/src/nostr/lifecycle/deletion/policy.rs index 35e2c0a..7b75aa7 100644 --- a/src/nostr/lifecycle/deletion/policy.rs +++ b/src/nostr/lifecycle/deletion/policy.rs @@ -73,9 +73,32 @@ 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. The same archival-mode contract is applied to NIP-62 vanish - /// requests in [`DeletionService::handle_vanish`](super::service::DeletionService::handle_vanish). + /// accessible. Already-covered kind-5 requests are still treated as + /// duplicates before archival-mode storage to avoid polluting the served + /// deletion-request stream. 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 { + match self + .ctx + .tombstones + .deletion_targets_already_covered(event) + .await + { + Ok(true) => { + tracing::info!( + event_id = %event.id.to_hex(), + author = %event.pubkey.to_hex(), + "Skipping duplicate NIP-09 deletion request; all actionable targets are already covered" + ); + return duplicate("deletion target(s) already covered"); + } + Ok(false) => {} + Err(e) => { + tracing::warn!(error = %e, "Tombstone lookup failed during duplicate deletion check"); + return reject_error(format!("internal error checking deletion coverage: {e}")); + } + } + // Archival mode: store the deletion request but do not process it. if self.ctx.config.deletion_request_disrespector { tracing::info!( @@ -104,27 +127,6 @@ impl DeletionPolicy { return reject_invalid("too many deletion targets"); } - match self - .ctx - .tombstones - .deletion_targets_already_covered(event) - .await - { - Ok(true) => { - tracing::info!( - event_id = %event.id.to_hex(), - author = %event.pubkey.to_hex(), - "Skipping duplicate NIP-09 deletion request; all actionable targets are already covered" - ); - return duplicate("deletion target(s) already covered"); - } - Ok(false) => {} - Err(e) => { - tracing::warn!(error = %e, "Tombstone lookup failed during duplicate deletion check"); - return reject_error(format!("internal error checking deletion coverage: {e}")); - } - } - // Validate authorship of all targets first. If any `e`-tag target exists // in the main DB and is owned by someone else, the whole deletion is // invalid (mirrors the backend's `handle_deletion_event` returning