fix: deduplicate archival deletion requests

This commit is contained in:
DanConwayDev
2026-06-30 15:38:34 +01:00
parent 54730530c9
commit 645af52639
2 changed files with 38 additions and 29 deletions
+13 -6
View File
@@ -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
+25 -23
View File
@@ -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