diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-23 13:42:57 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-23 13:42:57 +0000 |
| commit | 0c71e191963bec729c3ca13c212b231af7582f06 (patch) | |
| tree | fe15c92d6f6e1af194d5eebfd74dd8ac30f3d7de /src | |
| parent | 65ac6ef83205c41653e6ffe2acd664f968926fb2 (diff) | |
fix: rewrite deletion integration tests to avoid shared-state side effects
The previous tests deleted purgatory announcements (kind 30617) and checked
for bare-repo absence via git ls-remote, which would corrupt shared-mode
test state by destroying repos other tests depend on.
New approach tests deletion of purgatory state events (kind 30618) instead:
- e-tag test: promotes a repo, creates a unique commit locally, submits a
state event pointing to it (enters purgatory), deletes the state event by
event ID, then verifies git push of that commit is rejected.
- a-tag coordinate test: promotes a repo, generates a fresh maintainer
keypair, sends a replacement announcement adding that maintainer, submits
a state event signed by the new maintainer (enters purgatory), deletes by
coordinate 30618:<new_maintainer_pubkey>:<identifier>, then verifies git
push is rejected.
Also extends DeletionPolicy to handle kind 30618 state events in purgatory
for both e-tag (event ID) and a-tag (coordinate) deletion paths.
Diffstat (limited to 'src')
| -rw-r--r-- | src/nostr/policy/deletion.rs | 138 |
1 files changed, 89 insertions, 49 deletions
diff --git a/src/nostr/policy/deletion.rs b/src/nostr/policy/deletion.rs index 69a5758..01241c9 100644 --- a/src/nostr/policy/deletion.rs +++ b/src/nostr/policy/deletion.rs | |||
| @@ -1,7 +1,7 @@ | |||
| 1 | /// Deletion Policy - NIP-09 event deletion request handling | 1 | /// Deletion Policy - NIP-09 event deletion request handling |
| 2 | /// | 2 | /// |
| 3 | /// Handles kind 5 (EventDeletion) events that request removal of repository | 3 | /// Handles kind 5 (EventDeletion) events that request removal of purgatory entries |
| 4 | /// announcements (kind 30617) from purgatory. | 4 | /// for repository announcements (kind 30617) and state events (kind 30618). |
| 5 | /// | 5 | /// |
| 6 | /// ## NIP-09 Rules Enforced | 6 | /// ## NIP-09 Rules Enforced |
| 7 | /// | 7 | /// |
| @@ -13,9 +13,9 @@ | |||
| 13 | /// | 13 | /// |
| 14 | /// ## Purgatory Interaction | 14 | /// ## Purgatory Interaction |
| 15 | /// | 15 | /// |
| 16 | /// When a valid deletion request targets a kind 30617 announcement that is currently | 16 | /// - Kind 30617 (announcement) in purgatory: entry removed, bare repo deleted from disk |
| 17 | /// in purgatory (not yet promoted to the database), the purgatory entry is removed | 17 | /// - Kind 30618 (state event) in purgatory: matching state event(s) removed by event ID |
| 18 | /// and the bare repository is deleted from disk. | 18 | /// or by (author, identifier) coordinate |
| 19 | use nostr_relay_builder::prelude::{Event, WritePolicyResult}; | 19 | use nostr_relay_builder::prelude::{Event, WritePolicyResult}; |
| 20 | 20 | ||
| 21 | use super::PolicyContext; | 21 | use super::PolicyContext; |
| @@ -48,13 +48,13 @@ impl DeletionPolicy { | |||
| 48 | WritePolicyResult::Accept | 48 | WritePolicyResult::Accept |
| 49 | } | 49 | } |
| 50 | 50 | ||
| 51 | /// Remove any purgatory announcements targeted by this deletion event. | 51 | /// Remove any purgatory entries targeted by this deletion event. |
| 52 | /// | 52 | /// |
| 53 | /// Handles both reference styles from NIP-09: | 53 | /// Handles both reference styles from NIP-09: |
| 54 | /// - `e` tags: event ID references — match against purgatory entry event IDs | 54 | /// - `e` tags: event ID references — match against announcement or state event IDs |
| 55 | /// - `a` tags: addressable coordinate references — `30617:<pubkey>:<identifier>` | 55 | /// - `a` tags: addressable coordinate references — `30617:…` or `30618:…` |
| 56 | /// | 56 | /// |
| 57 | /// Only removes entries where the purgatory entry's owner matches the deletion | 57 | /// Only removes entries where the purgatory entry's author matches the deletion |
| 58 | /// event's pubkey (enforces author-only deletion). | 58 | /// event's pubkey (enforces author-only deletion). |
| 59 | fn remove_purgatory_targets(&self, event: &Event) { | 59 | fn remove_purgatory_targets(&self, event: &Event) { |
| 60 | let author = &event.pubkey; | 60 | let author = &event.pubkey; |
| @@ -81,17 +81,19 @@ impl DeletionPolicy { | |||
| 81 | } | 81 | } |
| 82 | } | 82 | } |
| 83 | 83 | ||
| 84 | /// Remove a purgatory announcement matched by event ID. | 84 | /// Remove a purgatory entry (announcement or state event) matched by event ID. |
| 85 | /// | 85 | /// |
| 86 | /// Scans all purgatory announcements owned by `author` and removes the one | 86 | /// Checks announcements first (kind 30617), then state events (kind 30618). |
| 87 | /// whose event ID hex matches `target_id_hex`. | 87 | /// Only removes entries whose author matches `author`. |
| 88 | fn remove_by_event_id(&self, author: &nostr_relay_builder::prelude::PublicKey, target_id_hex: &str, _deletion_created_at: u64) { | 88 | fn remove_by_event_id( |
| 89 | // Scan announcements owned by this author for a matching event ID | 89 | &self, |
| 90 | // We use get_announcements_by_identifier would require knowing the identifier, | 90 | author: &nostr_relay_builder::prelude::PublicKey, |
| 91 | // so instead we iterate via find_announcement after collecting all entries. | 91 | target_id_hex: &str, |
| 92 | _deletion_created_at: u64, | ||
| 93 | ) { | ||
| 94 | // --- Check announcements (kind 30617) --- | ||
| 92 | // The DashMap doesn't expose a direct "find by event ID" method, so we use | 95 | // The DashMap doesn't expose a direct "find by event ID" method, so we use |
| 93 | // the announcements_for_sync snapshot to get all (repo_id, _) pairs and then | 96 | // the announcements_for_sync snapshot to enumerate all (repo_id, _) pairs. |
| 94 | // look up each one. | ||
| 95 | let all = self.ctx.purgatory.announcements_for_sync(); | 97 | let all = self.ctx.purgatory.announcements_for_sync(); |
| 96 | for (repo_id, _) in all { | 98 | for (repo_id, _) in all { |
| 97 | // repo_id format: "30617:{pubkey_hex}:{identifier}" | 99 | // repo_id format: "30617:{pubkey_hex}:{identifier}" |
| @@ -102,7 +104,6 @@ impl DeletionPolicy { | |||
| 102 | let entry_pubkey_hex = parts[1]; | 104 | let entry_pubkey_hex = parts[1]; |
| 103 | let identifier = parts[2]; | 105 | let identifier = parts[2]; |
| 104 | 106 | ||
| 105 | // Only check entries owned by the deletion event author | ||
| 106 | if entry_pubkey_hex != author.to_hex() { | 107 | if entry_pubkey_hex != author.to_hex() { |
| 107 | continue; | 108 | continue; |
| 108 | } | 109 | } |
| @@ -116,18 +117,37 @@ impl DeletionPolicy { | |||
| 116 | "Deletion request: removing purgatory announcement by event ID" | 117 | "Deletion request: removing purgatory announcement by event ID" |
| 117 | ); | 118 | ); |
| 118 | self.evict_purgatory_entry(author, identifier); | 119 | self.evict_purgatory_entry(author, identifier); |
| 119 | return; // event IDs are unique, no need to continue | 120 | return; // event IDs are unique |
| 121 | } | ||
| 122 | } | ||
| 123 | } | ||
| 124 | |||
| 125 | // --- Check state events (kind 30618) --- | ||
| 126 | // State events are keyed by identifier; scan all identifiers for a match. | ||
| 127 | let state_identifiers = self.ctx.purgatory.get_all_identifiers(); | ||
| 128 | for identifier in state_identifiers { | ||
| 129 | let entries = self.ctx.purgatory.find_state(&identifier); | ||
| 130 | for entry in entries { | ||
| 131 | if entry.author == *author && entry.event.id.to_hex() == target_id_hex { | ||
| 132 | tracing::info!( | ||
| 133 | event_id = %target_id_hex, | ||
| 134 | identifier = %identifier, | ||
| 135 | author = %author.to_hex(), | ||
| 136 | "Deletion request: removing purgatory state event by event ID" | ||
| 137 | ); | ||
| 138 | self.ctx.purgatory.remove_state_event(&identifier, &entry.event.id); | ||
| 139 | return; // event IDs are unique | ||
| 120 | } | 140 | } |
| 121 | } | 141 | } |
| 122 | } | 142 | } |
| 123 | } | 143 | } |
| 124 | 144 | ||
| 125 | /// Remove a purgatory announcement matched by addressable coordinate. | 145 | /// Remove a purgatory entry matched by addressable coordinate. |
| 146 | /// | ||
| 147 | /// The coordinate format is `<kind>:<pubkey>:<d-identifier>`. | ||
| 148 | /// Handles kind 30617 (announcements) and kind 30618 (state events). | ||
| 126 | /// | 149 | /// |
| 127 | /// The coordinate format is `<kind>:<pubkey>:<d-identifier>`. Only kind 30617 | 150 | /// Per NIP-09, all versions up to `deletion_created_at` are considered deleted. |
| 128 | /// coordinates are relevant here. Per NIP-09, all versions up to `deletion_created_at` | ||
| 129 | /// are considered deleted — since purgatory entries are always a single event per | ||
| 130 | /// (owner, identifier), we delete if the entry's `created_at` ≤ `deletion_created_at`. | ||
| 131 | fn remove_by_coordinate( | 151 | fn remove_by_coordinate( |
| 132 | &self, | 152 | &self, |
| 133 | author: &nostr_relay_builder::prelude::PublicKey, | 153 | author: &nostr_relay_builder::prelude::PublicKey, |
| @@ -144,11 +164,6 @@ impl DeletionPolicy { | |||
| 144 | let coord_pubkey_hex = parts[1]; | 164 | let coord_pubkey_hex = parts[1]; |
| 145 | let identifier = parts[2]; | 165 | let identifier = parts[2]; |
| 146 | 166 | ||
| 147 | // Only handle kind 30617 (GitRepoAnnouncement) | ||
| 148 | if kind_str != "30617" { | ||
| 149 | return; | ||
| 150 | } | ||
| 151 | |||
| 152 | // The coordinate pubkey must match the deletion event author | 167 | // The coordinate pubkey must match the deletion event author |
| 153 | if coord_pubkey_hex != author.to_hex() { | 168 | if coord_pubkey_hex != author.to_hex() { |
| 154 | tracing::debug!( | 169 | tracing::debug!( |
| @@ -159,25 +174,50 @@ impl DeletionPolicy { | |||
| 159 | return; | 174 | return; |
| 160 | } | 175 | } |
| 161 | 176 | ||
| 162 | if let Some(entry) = self.ctx.purgatory.find_announcement(author, identifier) { | 177 | match kind_str { |
| 163 | // Per NIP-09: delete all versions up to deletion_created_at | 178 | "30617" => { |
| 164 | if entry.event.created_at.as_secs() <= deletion_created_at { | 179 | // Announcement purgatory entry |
| 165 | tracing::info!( | 180 | if let Some(entry) = self.ctx.purgatory.find_announcement(author, identifier) { |
| 166 | identifier = %identifier, | 181 | if entry.event.created_at.as_secs() <= deletion_created_at { |
| 167 | author = %author.to_hex(), | 182 | tracing::info!( |
| 168 | entry_created_at = entry.event.created_at.as_secs(), | 183 | identifier = %identifier, |
| 169 | deletion_created_at = %deletion_created_at, | 184 | author = %author.to_hex(), |
| 170 | "Deletion request: removing purgatory announcement by coordinate" | 185 | "Deletion request: removing purgatory announcement by coordinate" |
| 171 | ); | 186 | ); |
| 172 | self.evict_purgatory_entry(author, identifier); | 187 | self.evict_purgatory_entry(author, identifier); |
| 173 | } else { | 188 | } else { |
| 174 | tracing::debug!( | 189 | tracing::debug!( |
| 175 | identifier = %identifier, | 190 | identifier = %identifier, |
| 176 | author = %author.to_hex(), | 191 | author = %author.to_hex(), |
| 177 | entry_created_at = entry.event.created_at.as_secs(), | 192 | "Ignoring deletion: purgatory announcement is newer than deletion request" |
| 178 | deletion_created_at = %deletion_created_at, | 193 | ); |
| 179 | "Ignoring deletion: purgatory entry is newer than deletion request" | 194 | } |
| 180 | ); | 195 | } |
| 196 | } | ||
| 197 | "30618" => { | ||
| 198 | // State event purgatory entries for this (author, identifier). | ||
| 199 | // Remove all entries authored by `author` with created_at ≤ deletion_created_at. | ||
| 200 | let entries = self.ctx.purgatory.find_state(identifier); | ||
| 201 | let mut removed = 0usize; | ||
| 202 | for entry in entries { | ||
| 203 | if entry.author == *author | ||
| 204 | && entry.event.created_at.as_secs() <= deletion_created_at | ||
| 205 | { | ||
| 206 | self.ctx.purgatory.remove_state_event(identifier, &entry.event.id); | ||
| 207 | removed += 1; | ||
| 208 | } | ||
| 209 | } | ||
| 210 | if removed > 0 { | ||
| 211 | tracing::info!( | ||
| 212 | identifier = %identifier, | ||
| 213 | author = %author.to_hex(), | ||
| 214 | removed = %removed, | ||
| 215 | "Deletion request: removed purgatory state event(s) by coordinate" | ||
| 216 | ); | ||
| 217 | } | ||
| 218 | } | ||
| 219 | _ => { | ||
| 220 | // Other kinds not handled | ||
| 181 | } | 221 | } |
| 182 | } | 222 | } |
| 183 | } | 223 | } |