From 1d09e4bdea7e328cf2740818df9df660c5532a99 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 13 Feb 2026 13:24:46 +0000 Subject: feat: implement announcement purgatory core (breaks archive sync test) Route new announcements to purgatory instead of accepting immediately. Announcements are promoted to the database when git data arrives, ensuring we only serve announcements for repos with actual content. Implemented: - AnnouncementPurgatoryEntry type and DashMap store - Route new announcements to purgatory (replacement announcements skip) - Promote announcements on git data arrival (process_purgatory_announcements) - Authorization checks purgatory announcements (fetch_repository_data_with_purgatory) - State policy uses purgatory announcements for maintainer validation - Cleanup task handles announcement expiry - Updated count()/cleanup() to 3-tuples Known broken: - test_archive_read_only_creates_bare_repo fails: sync module does not treat purgatory announcements as confirmed repos, so per-repo sync (state events, PRs) is never triggered for purgatory announcements - Announcement persistence (save/restore) not implemented - SyncLevel (StateOnly vs Full) not implemented - Soft expiry two-phase not implemented - Expiry extension on state event / git auth not wired up --- src/nostr/policy/state.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'src/nostr/policy/state.rs') diff --git a/src/nostr/policy/state.rs b/src/nostr/policy/state.rs index f94f004..4bfb513 100644 --- a/src/nostr/policy/state.rs +++ b/src/nostr/policy/state.rs @@ -10,7 +10,7 @@ use nostr_relay_builder::prelude::Event; use super::PolicyContext; use crate::git; -use crate::git::authorization::fetch_repository_data; +use crate::git::authorization::fetch_repository_data_with_purgatory; use crate::nostr::events::{validate_state, RepositoryAnnouncement, RepositoryState}; /// Result of state policy evaluation @@ -76,7 +76,13 @@ impl StatePolicy { } // Get all repositories and state events from db with identifier - let db_repo_data = fetch_repository_data(&self.ctx.database, &state.identifier).await?; + // Include purgatory announcements for authorization + let db_repo_data = fetch_repository_data_with_purgatory( + &self.ctx.database, + &self.ctx.purgatory, + &state.identifier, + ) + .await?; // CRITICAL: Check if author is authorized via maintainer set // State events MUST be rejected if author is not in maintainer set of any accepted announcement -- cgit v1.2.3 From 49401286ea7413f834197e6a5b221649e10e2ad8 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Mon, 23 Feb 2026 11:36:45 +0000 Subject: fix: promote purgatory announcements after git sync copy path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a state event arrives and the required commits already exist in another maintainer's repo on the same relay, process_state_with_git_data copies the OIDs across and aligns refs — but never called process_purgatory_announcements for the target repos. Any announcement waiting in purgatory for that repo stayed there indefinitely. Fix: after process_state_with_git_data, call process_newly_available_git_data for each target repo (those that received copied OIDs) so purgatory announcements are promoted immediately. --- src/nostr/policy/state.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) (limited to 'src/nostr/policy/state.rs') diff --git a/src/nostr/policy/state.rs b/src/nostr/policy/state.rs index 4bfb513..9ad72c2 100644 --- a/src/nostr/policy/state.rs +++ b/src/nostr/policy/state.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; @@ -192,6 +193,42 @@ impl StatePolicy { } } + // After copying OIDs to other owner repos, promote any purgatory announcements + // for those repos. This handles the case where two maintainers push to the same + // identifier on the same relay with identical commit hashes: the second maintainer's + // announcement sits in purgatory, and when their state event arrives the relay copies + // commits from the first maintainer's repo — but without this call the announcement + // would stay in purgatory indefinitely. + let local_relay = self.ctx.get_local_relay(); + let empty_oids: HashSet = HashSet::new(); + for announcement in &db_repo_data.announcements { + let target_repo_path = self.ctx.git_data_path.join(announcement.repo_path()); + if target_repo_path != repo_with_git_data { + // OIDs were copied to this repo by process_state_with_git_data; + // check if there's a purgatory announcement waiting for it. + if let Err(e) = crate::git::sync::process_newly_available_git_data( + &target_repo_path, + &empty_oids, + &self.ctx.database, + local_relay.as_ref(), + &self.ctx.purgatory, + &self.ctx.git_data_path, + None, + None, + ) + .await + { + tracing::warn!( + identifier = %state.identifier, + event_id = %event.id, + repo_path = %target_repo_path.display(), + error = %e, + "Failed to process purgatory announcements for target repo after git sync copy" + ); + } + } + } + // Event will be saved and broadcast by relay builder Ok(WritePolicyResult::Accept) } else { -- cgit v1.2.3 From 84c9003323162f166552d1dea15ee9ed1b1a025a Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Mon, 23 Feb 2026 12:54:05 +0000 Subject: feat: extend purgatory announcement expiry when state event arrives Per design doc decision #4: state event arrival resets the 30-minute protocol timer for purgatory announcements. This prevents premature expiry during slow sync operations where the repo is actively receiving metadata but git data hasn't arrived yet. Extends expiry for all owners whose announcement authorized the state event, and triggers revival if the announcement was soft-expired. --- src/nostr/policy/state.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) (limited to 'src/nostr/policy/state.rs') diff --git a/src/nostr/policy/state.rs b/src/nostr/policy/state.rs index 9ad72c2..e6de54e 100644 --- a/src/nostr/policy/state.rs +++ b/src/nostr/policy/state.rs @@ -146,6 +146,34 @@ impl StatePolicy { "State event author authorized via maintainer set" ); + // Extend expiry for any purgatory announcements for this identifier. + // + // Per design doc decision #4: state event arrival extends the purgatory + // announcement's expiry (reset the 30-minute protocol timer). This prevents + // premature expiry during slow sync operations — the repo is actively receiving + // metadata so it should stay alive. + // + // We extend for all owners that authorized this state event, since the state + // event proves the repo is active regardless of which owner's announcement + // authorized it. + for owner_hex in &authorized_owners { + if let Ok(owner_pk) = nostr_sdk::PublicKey::from_hex(owner_hex) { + if self.ctx.purgatory.has_purgatory_announcement(&owner_pk, &state.identifier) { + self.ctx.purgatory.extend_announcement_expiry( + &owner_pk, + &state.identifier, + std::time::Duration::from_secs(1800), + ); + tracing::debug!( + event_id = %event.id, + identifier = %state.identifier, + owner = %owner_hex, + "Extended purgatory announcement expiry due to state event arrival" + ); + } + } + } + // Duplicate check in db if db_repo_data.states.iter().any(|e| e.event.id.eq(&event.id)) { tracing::debug!("processed state event duplicate (in db): {}", event.id); -- cgit v1.2.3