upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/nostr/builder.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-01-08 00:41:02 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-01-08 00:41:02 +0000
commit5833c9bdf815699838a0445f750b99b26fd4a3bd (patch)
treebd148e548e5621872615627cdbd88ba577d072ce /src/nostr/builder.rs
parentac3e00a7e102d7ae341f554563646e05aed7edac (diff)
feat(purgatory): track expired events to prevent infinite re-sync loops
Adds expired event tracking to prevent proactive sync from repeatedly fetching and re-adding events that expired from purgatory without finding git data. Key features: - Track expired events for 7 days to prevent re-sync loops - Distinguish synced vs user-submitted events (via socket address) - Allow users to retry expired events (git data might now be available) - Reject synced expired events (prevents infinite loop) - Daily cleanup of expired event records older than 7 days Implementation: - Added expired_events: DashMap<EventId, Instant> to Purgatory - Updated event_ids() to include both purgatory + expired events - Added is_expired(), mark_expired(), cleanup_expired_events() - Updated cleanup() to mark expired events automatically - Added is_synced detection in WritePolicy (localhost:0 = synced) - Policy layer checks is_synced && is_expired() before rejecting Behavior: - Negentropy: Filters expired events before fetching (optimal) - REQ+EOSE: Rejects synced expired events at policy layer - User submissions: Always allowed to retry (skip expired check) Testing: - Added 5 new tests for expired event tracking - All 222 tests passing Fixes the infinite re-sync loop where events without git data would expire, get synced again, expire again, repeat forever.
Diffstat (limited to 'src/nostr/builder.rs')
-rw-r--r--src/nostr/builder.rs37
1 files changed, 31 insertions, 6 deletions
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs
index db2f59b..0e5c18a 100644
--- a/src/nostr/builder.rs
+++ b/src/nostr/builder.rs
@@ -152,13 +152,17 @@ impl Nip34WritePolicy {
152 } 152 }
153 153
154 /// Handle repository state event 154 /// Handle repository state event
155 async fn handle_state(&self, event: &Event) -> WritePolicyResult { 155 ///
156 /// # Arguments
157 /// * `event` - The state event to validate
158 /// * `is_synced` - True if this event came from proactive sync (vs user-submitted)
159 async fn handle_state(&self, event: &Event, is_synced: bool) -> WritePolicyResult {
156 let event_id_str = event.id.to_bech32().unwrap_or_else(|_| event.id.to_hex()); 160 let event_id_str = event.id.to_bech32().unwrap_or_else(|_| event.id.to_hex());
157 161
158 match self.state_policy.validate(event) { 162 match self.state_policy.validate(event) {
159 StateResult::Accept => { 163 StateResult::Accept => {
160 // Process state alignment asynchronously 164 // Process state alignment asynchronously
161 match self.state_policy.process_state_event(event).await { 165 match self.state_policy.process_state_event(event, is_synced).await {
162 Ok(poilicy_result) => poilicy_result, 166 Ok(poilicy_result) => poilicy_result,
163 Err(e) => { 167 Err(e) => {
164 tracing::warn!("Failed to process state event {}: {}", event_id_str, e); 168 tracing::warn!("Failed to process state event {}: {}", event_id_str, e);
@@ -178,7 +182,11 @@ impl Nip34WritePolicy {
178 } 182 }
179 183
180 /// Handle PR or PR Update event 184 /// Handle PR or PR Update event
181 async fn handle_pr_event(&self, event: &Event) -> WritePolicyResult { 185 ///
186 /// # Arguments
187 /// * `event` - The PR event to validate
188 /// * `is_synced` - True if this event came from proactive sync (vs user-submitted)
189 async fn handle_pr_event(&self, event: &Event, is_synced: bool) -> WritePolicyResult {
182 let event_id_str = event.id.to_bech32().unwrap_or_else(|_| event.id.to_hex()); 190 let event_id_str = event.id.to_bech32().unwrap_or_else(|_| event.id.to_hex());
183 191
184 // duplicate check in purgatory 192 // duplicate check in purgatory
@@ -230,6 +238,19 @@ impl Nip34WritePolicy {
230 // Check if git data exists (delete any incorrect commits at refs/nostr/<event-id>, copies correct data to relivant repositories) 238 // Check if git data exists (delete any incorrect commits at refs/nostr/<event-id>, copies correct data to relivant repositories)
231 match self.pr_event_policy.git_data_check(event).await { 239 match self.pr_event_policy.git_data_check(event).await {
232 Ok(false) => { 240 Ok(false) => {
241 // Only reject expired events if they're from sync (not user-submitted)
242 // User-submitted events should be allowed to retry in case git data became available
243 if is_synced && self.ctx.purgatory.is_expired(&event.id) {
244 tracing::debug!(
245 event_id = %event_id_str,
246 "PR event previously expired from purgatory (synced), rejecting to prevent re-sync loop"
247 );
248 return WritePolicyResult::Reject {
249 status: false,
250 message: "invalid: previously expired from purgatory without git data".into(),
251 };
252 }
253
233 // No git data exists - add to purgatory 254 // No git data exists - add to purgatory
234 let commit = event 255 let commit = event
235 .tags 256 .tags
@@ -344,13 +365,17 @@ impl WritePolicy for Nip34WritePolicy {
344 fn admit_event<'a>( 365 fn admit_event<'a>(
345 &'a self, 366 &'a self,
346 event: &'a nostr_relay_builder::prelude::Event, 367 event: &'a nostr_relay_builder::prelude::Event,
347 _addr: &'a SocketAddr, 368 addr: &'a SocketAddr,
348 ) -> BoxedFuture<'a, WritePolicyResult> { 369 ) -> BoxedFuture<'a, WritePolicyResult> {
349 Box::pin(async move { 370 Box::pin(async move {
371 // Detect if this is a synced event (from proactive sync) vs user-submitted
372 // Sync uses localhost:0 as a dummy address
373 let is_synced = addr.ip().is_loopback() && addr.port() == 0;
374
350 match event.kind.as_u16() { 375 match event.kind.as_u16() {
351 KIND_REPOSITORY_ANNOUNCEMENT => self.handle_announcement(event).await, 376 KIND_REPOSITORY_ANNOUNCEMENT => self.handle_announcement(event).await,
352 KIND_REPOSITORY_STATE => self.handle_state(event).await, 377 KIND_REPOSITORY_STATE => self.handle_state(event, is_synced).await,
353 KIND_PR | KIND_PR_UPDATE => self.handle_pr_event(event).await, 378 KIND_PR | KIND_PR_UPDATE => self.handle_pr_event(event, is_synced).await,
354 KIND_USER_GRASP_LIST => { 379 KIND_USER_GRASP_LIST => {
355 // Accept all kind 10317 (User Grasp List) events 380 // Accept all kind 10317 (User Grasp List) events
356 // for better GRASP repository discovery 381 // for better GRASP repository discovery