upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/docs/explanation/announcements-purgatory-implementation.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/explanation/announcements-purgatory-implementation.md')
-rw-r--r--docs/explanation/announcements-purgatory-implementation.md296
1 files changed, 0 insertions, 296 deletions
diff --git a/docs/explanation/announcements-purgatory-implementation.md b/docs/explanation/announcements-purgatory-implementation.md
deleted file mode 100644
index 263c253..0000000
--- a/docs/explanation/announcements-purgatory-implementation.md
+++ /dev/null
@@ -1,296 +0,0 @@
1# Announcements Purgatory Implementation Details
2
3This document provides detailed implementation notes for the [Announcements Purgatory Design](./announcements-purgatory-design.md).
4
5## Sync Integration
6
7### Current Sync Architecture
8
9The sync system uses a two-index approach:
10
11```rust
12// What we WANT to sync - source of truth from self-subscription
13// Key: repo addressable ref (30617:pubkey:identifier)
14pub type RepoSyncIndex = Arc<RwLock<HashMap<String, RepoSyncNeeds>>>;
15
16pub struct RepoSyncNeeds {
17 pub relays: HashSet<String>, // Relay URLs from announcement
18 pub root_events: HashSet<EventId>, // 1617/1618/1621 event IDs
19}
20
21// What we have CONFIRMED syncing + connection state
22// Key: relay URL
23pub type RelaySyncIndex = Arc<RwLock<HashMap<String, RelayState>>>;
24```
25
26**Three-Layer Sync Strategy:**
271. **Layer 1:** Announcements (kinds 30617, 10317)
282. **Layer 2:** Repo-tagging events (events with `a`/`A`/`q` tags + kind 30618 by identifier)
293. **Layer 3:** Root-event-tagging events (events with `e`/`E`/`q` tags)
30
31### Adding SyncLevel
32
33Add a `sync_level` field to distinguish purgatory from promoted repos:
34
35```rust
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
37pub enum SyncLevel {
38 #[default]
39 Full, // L2 + L3 (promoted repos)
40 StateOnly, // Only state events (purgatory announcements)
41}
42
43pub struct RepoSyncNeeds {
44 pub relays: HashSet<String>,
45 pub root_events: HashSet<EventId>,
46 pub sync_level: SyncLevel, // NEW
47}
48```
49
50### Filter Building Changes
51
52In `src/sync/filters.rs`, modify filter building to respect sync level:
53
54```rust
55// For StateOnly repos, only build state event filters
56pub fn build_layer2_and_layer3_filters(
57 repos: &HashMap<String, RepoSyncNeeds>,
58 // ...
59) -> Vec<Filter> {
60 let (full_repos, state_only_repos): (Vec<_>, Vec<_>) = repos
61 .iter()
62 .partition(|(_, needs)| needs.sync_level == SyncLevel::Full);
63
64 let mut filters = Vec::new();
65
66 // Full repos get all L2/L3 filters
67 if !full_repos.is_empty() {
68 filters.extend(tagged_one_of_our_repo_event_filters(&full_repos));
69 filters.extend(state_event_filters_for_our_repos(&full_repos));
70 filters.extend(tagged_one_of_our_root_event_filters(&full_repos));
71 }
72
73 // StateOnly repos get only state event filters
74 if !state_only_repos.is_empty() {
75 filters.extend(state_event_filters_for_our_repos(&state_only_repos));
76 }
77
78 filters
79}
80```
81
82The existing `state_event_filters_for_our_repos()` function already builds kind 30618 filters with `#d` tags, which is exactly what we need.
83
84### Self-Subscriber Changes
85
86In `src/sync/self_subscriber.rs`, add purgatory announcements to the sync index:
87
88```rust
89// When announcement enters purgatory
90fn on_announcement_to_purgatory(
91 &self,
92 event: &Event,
93 identifier: &str,
94 relays: HashSet<String>,
95) {
96 let key = format!("30617:{}:{}", event.pubkey, identifier);
97 let mut index = self.repo_sync_index.write().unwrap();
98 index.insert(key, RepoSyncNeeds {
99 relays,
100 root_events: HashSet::new(),
101 sync_level: SyncLevel::StateOnly,
102 });
103}
104
105// When announcement promotes to database
106fn on_announcement_promoted(
107 &self,
108 event: &Event,
109 identifier: &str,
110) {
111 let key = format!("30617:{}:{}", event.pubkey, identifier);
112 let mut index = self.repo_sync_index.write().unwrap();
113 if let Some(needs) = index.get_mut(&key) {
114 needs.sync_level = SyncLevel::Full;
115 }
116}
117```
118
119### Algorithm Changes
120
121In `src/sync/algorithms.rs`, preserve sync level when inverting repo->relay:
122
123```rust
124pub fn derive_relay_targets(
125 repo_index: &RepoSyncIndex,
126) -> HashMap<String, RelaySyncNeeds> {
127 // ... existing inversion logic ...
128 // Ensure sync_level is preserved/aggregated per relay
129 // A relay gets Full if ANY of its repos are Full
130}
131```
132
133## Authorization Integration
134
135### Current Authorization Flow
136
137Authorization lookups happen in `src/git/authorization.rs`:
138
139| Function | Purpose | Currently Queries |
140|----------|---------|-------------------|
141| `fetch_repository_data()` | Get announcements + states by identifier | DB only |
142| `collect_authorized_maintainers()` | Build maintainer set from announcements | DB only |
143| `pubkey_authorised_for_repo_owners()` | Check if pubkey authorized | DB only |
144
145### Required Changes
146
147Modify `fetch_repository_data()` to also query purgatory:
148
149```rust
150pub async fn fetch_repository_data(
151 db: &Database,
152 purgatory: &Purgatory, // NEW parameter
153 identifier: &str,
154) -> Result<RepositoryData> {
155 // Existing DB query
156 let db_events = db.query(/* kind 30617, 30618 by identifier */).await?;
157
158 // NEW: Also check purgatory for announcements
159 let purgatory_announcements = purgatory
160 .get_announcements_by_identifier(identifier);
161
162 // Merge results
163 let mut announcements = parse_announcements(db_events);
164 announcements.extend(purgatory_announcements);
165
166 // ... rest of function
167}
168```
169
170This affects:
171- `StatePolicy::process_state_event()` - state event validation
172- `get_state_authorization_for_specific_owner_repo()` - git push authorization
173- `AnnouncementPolicy::is_maintainer_in_any_announcement()` - maintainer exception
174
175## Purgatory Store Changes
176
177### New Fields
178
179```rust
180pub struct AnnouncementPurgatoryEntry {
181 pub event: Event,
182 pub identifier: String,
183 pub owner: PublicKey,
184 pub repo_path: PathBuf,
185 pub relays: HashSet<String>, // For sync registration
186 pub created_at: Instant,
187 pub expires_at: Instant,
188 pub soft_expired: bool, // Bare repo deleted, event retained
189}
190```
191
192### New Methods
193
194```rust
195impl Purgatory {
196 /// Get announcements by identifier (for authorization)
197 pub fn get_announcements_by_identifier(
198 &self,
199 identifier: &str,
200 ) -> Vec<&AnnouncementPurgatoryEntry> {
201 self.announcement_purgatory
202 .iter()
203 .filter(|entry| entry.identifier == identifier)
204 .collect()
205 }
206
207 /// Transition to soft-expired state (protocol's 30min expiry reached)
208 pub fn soft_expire_announcement(
209 &self,
210 key: &(PublicKey, String),
211 ) -> Option<PathBuf> {
212 if let Some(mut entry) = self.announcement_purgatory.get_mut(key) {
213 entry.soft_expired = true;
214 entry.expires_at = Instant::now() + SOFT_EXPIRY_DURATION; // e.g., 24h extended retention
215 Some(entry.repo_path.clone()) // Return path for bare repo deletion
216 } else {
217 None
218 }
219 }
220
221 /// Revive soft-expired announcement when state event arrives
222 /// (caller must recreate bare repo)
223 pub fn revive_announcement(
224 &self,
225 key: &(PublicKey, String),
226 ) -> Option<PathBuf> {
227 if let Some(mut entry) = self.announcement_purgatory.get_mut(key) {
228 if entry.soft_expired {
229 entry.soft_expired = false;
230 entry.expires_at = Instant::now() + ACTIVE_EXPIRY_DURATION; // Reset 30min protocol timer
231 return Some(entry.repo_path.clone()); // Caller recreates bare repo
232 }
233 }
234 None
235 }
236}
237```
238
239## Expiry Cleanup Task
240
241The existing cleanup task needs to handle the two-phase expiry:
242
243```rust
244async fn cleanup_expired_announcements(&self) {
245 let now = Instant::now();
246
247 for entry in self.announcement_purgatory.iter() {
248 if entry.expires_at <= now {
249 let key = (entry.owner.clone(), entry.identifier.clone());
250
251 if entry.soft_expired {
252 // Fully expired - remove entirely
253 self.announcement_purgatory.remove(&key);
254 self.unregister_from_sync(&key);
255 } else {
256 // First expiry - transition to soft-expired
257 if let Some(repo_path) = self.soft_expire_announcement(&key) {
258 delete_bare_repo(&repo_path).await;
259 }
260 // Note: stays in sync index with StateOnly level
261 }
262 }
263 }
264}
265```
266
267## State Event Revival Flow
268
269When a state event arrives for a soft-expired announcement, the state policy must:
270
2711. Check purgatory for a matching announcement (in addition to DB)
2722. Validate authorization against the purgatory announcement
2733. If soft-expired, call `revive_announcement()` and recreate the bare repo
2744. Extend the announcement's expiry (reset the 30-minute protocol timer)
2755. Route the state event to state purgatory
276
277**Why revival is necessary:** Without soft expiry + revival, late-arriving state events would either be permanently rejected (if we added the announcement to `failed_events`) or cause constant re-syncing of the announcement event. Revival allows us to respect the protocol's 30-minute expiry while still handling delayed state events gracefully.
278
279The exact integration will depend on the current structure of `StatePolicy::process_state_event()` - see implementation phase for details.
280
281## File Change Summary
282
283| File | Estimated Lines | Changes |
284|------|-----------------|---------|
285| `src/sync/mod.rs` | ~10 | Add `SyncLevel` enum, field to `RepoSyncNeeds` |
286| `src/sync/filters.rs` | ~20 | Partition repos by sync level, build appropriate filters |
287| `src/sync/algorithms.rs` | ~15 | Preserve sync level in relay target derivation |
288| `src/sync/self_subscriber.rs` | ~40 | Register purgatory announcements, handle promotion |
289| `src/purgatory/mod.rs` | ~80 | Add announcement store, soft expiry methods |
290| `src/purgatory/types.rs` | ~20 | Add `AnnouncementPurgatoryEntry` |
291| `src/git/authorization.rs` | ~30 | Query purgatory in `fetch_repository_data()` |
292| `src/nostr/policy/state.rs` | ~40 | Handle soft-expired revival |
293| `src/nostr/policy/announcement.rs` | ~30 | Route to purgatory, check for replacements |
294| `src/git/receive.rs` | ~20 | Trigger promotion on git data |
295
296**Total: ~305 lines of changes**