From e22021f0b248ebcf3bd09210d59b2cdb4701032f Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 18 Feb 2026 19:41:29 +0000 Subject: fix: simplify purgatory sync - fix SelfSubscriber sync_level upgrade and negentropy fallback Three targeted fixes for purgatory announcement sync: 1. SelfSubscriber sync_level upgrade: After or_insert_with in process_batch, always set entry.sync_level = SyncLevel::Full so that when a promoted announcement is broadcast via notify_event and SelfSubscriber receives it, an existing StateOnly entry gets upgraded to Full and PR event subscriptions are triggered immediately (not delayed up to 24h). 2. Negentropy fallback filter split: In handle_eose, when falling back from negentropy to REQ+EOSE, split batch_repos by SyncLevel and call build_sync_level_aware_filters instead of build_layer2_and_layer3_filters. Prevents StateOnly (purgatory) repos from getting Layer 2 #a/#A/#q filters prematurely, which caused nostr-sdk client deduplication to permanently drop PR events after orphan rejection. 3. Recompute sync filters after announcement batch EOSE: Add recompute_new_sync_filters_for_relay calls at all three batch-completion paths in handle_eose for generic filter (announcement) batches. This triggers state-only subscriptions for any purgatory repos registered during that batch, fixing the 24h delay before state event sync starts. 4. User-submitted purgatory announcements: Add repo_sync_index field to PolicyContext with setter/getter, wire in main.rs after SyncManager creation, and register in AcceptPurgatory handler so user-submitted announcements get StateOnly sync started immediately. 5. Update archive tests: test_archive_without_state_events_does_not_sync_git updated to reflect that StateOnly subscription now proactively fetches state events from source relays. test_archive_read_only_creates_bare_repo un-ignored as it now works end-to-end. --- tests/archive_read_only.rs | 63 +++++++++++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 23 deletions(-) (limited to 'tests') diff --git a/tests/archive_read_only.rs b/tests/archive_read_only.rs index e388ae5..069b3b7 100644 --- a/tests/archive_read_only.rs +++ b/tests/archive_read_only.rs @@ -55,7 +55,6 @@ use std::time::Duration; /// 5. Verify bare repository is created and git data is synced /// 6. Verify git pushes are rejected (read-only mode) #[tokio::test] -#[ignore] // Requires SyncLevel implementation (Phase 3) - purgatory announcements don't trigger per-repo sync yet async fn test_archive_read_only_creates_bare_repo() { // 1. Start source relay let source_relay = TestRelay::start().await; @@ -264,24 +263,24 @@ async fn test_archive_read_only_creates_bare_repo() { source_relay.stop().await; } -/// Test that archive mode without state events does NOT sync git data. +/// Test that archive mode proactively syncs state events and git data +/// when the source relay has state events available. /// -/// This verifies the security model: archive mode only syncs git data -/// when there are state events to validate against. +/// With StateOnly sync now implemented, purgatory announcements subscribe +/// to state events from the relays listed in the announcement. This means +/// the archive relay will: +/// 1. Sync the announcement → purgatory → register as StateOnly in repo_sync_index +/// 2. Subscribe to state events (kind 30618) on source relay +/// 3. Receive the state event → purgatory sync triggered +/// 4. Fetch git data from source relay's clone URL /// -/// With announcement purgatory, the flow is: -/// 1. Send announcement to source relay (goes to purgatory) -/// 2. Send state event to source relay (goes to purgatory) -/// 3. Push git data to source relay (promotes announcement and state event) -/// 4. Start archive relay with sync from source -/// 5. Archive relay syncs the promoted announcement -/// 6. Verify git data is NOT synced (archive has no state event to authorize git fetch) +/// This test verifies the full sync chain works end-to-end for archive mode. #[tokio::test] -async fn test_archive_without_state_events_does_not_sync_git() { +async fn test_archive_syncs_state_events_and_git_data_via_state_only_subscription() { // 1. Start source relay let source_relay = TestRelay::start().await; let keys = Keys::generate(); - let identifier = "archive-no-state-repo"; + let identifier = "archive-state-only-sync-repo"; // Pre-allocate archive relay port let archive_port = TestRelay::find_free_port(); @@ -295,6 +294,7 @@ async fn test_archive_without_state_events_does_not_sync_git() { let npub = keys.public_key().to_bech32().expect("Failed to get npub"); // 3. Create and send announcement listing BOTH relays + // The archive relay will subscribe to state events on BOTH listed relays let announcement = create_repo_announcement( &keys, &[&source_relay.domain(), &archive_domain], @@ -337,6 +337,8 @@ async fn test_archive_without_state_events_does_not_sync_git() { ) .expect("Failed to create state event"); + let state_event_id = state_event.id; + source_client .send_event(&state_event) .await @@ -348,9 +350,12 @@ async fn test_archive_without_state_events_does_not_sync_git() { push_to_relay(temp_dir.path(), &source_relay.domain(), &npub, identifier) .expect("Push to source should succeed"); - tokio::time::sleep(Duration::from_millis(500)).await; + // Wait for state event to be promoted on source relay + wait_for_event_served(source_relay.url(), &state_event_id, Duration::from_secs(5)) + .await + .expect("State event should be served on source relay after push"); - // 6. Start archive relay (without state event - we don't send state event to archive) + // 6. Start archive relay - StateOnly subscription will proactively fetch state events let archive_relay = TestRelay::start_with_archive_and_sync( archive_port, Some(source_relay.url().to_string()), @@ -360,15 +365,28 @@ async fn test_archive_without_state_events_does_not_sync_git() { ) .await; - // Wait for sync + // Wait for sync connection wait_for_sync_connection(archive_relay.url(), 1, Duration::from_secs(5)) .await .expect("Sync connection should establish"); - // Give time for sync to fetch announcement - tokio::time::sleep(Duration::from_secs(3)).await; + // 7. Wait for state event to be served on archive relay + // The StateOnly subscription fetches the state event from source relay, + // which then triggers purgatory sync and git data fetch. + let found = wait_for_event_served( + archive_relay.url(), + &state_event_id, + Duration::from_secs(30), // Allow time for sync + git fetch + ) + .await; + + assert!( + found.is_ok(), + "State event should be served on archive after StateOnly subscription fetches it: {:?}", + found.err() + ); - // 7. Verify bare repository was created (announcement was synced and accepted to purgatory) + // 8. Verify bare repository was created let repo_path = archive_relay .git_data_path() .join(format!("{}/{}.git", npub, identifier)); @@ -378,8 +396,7 @@ async fn test_archive_without_state_events_does_not_sync_git() { "Bare repository should be created for archive announcement" ); - // 8. Verify git data was NOT synced (no state events on archive to trigger git fetch) - // Check that the commit does NOT exist in the archive relay's repo + // 9. Verify git data was synced via the state event chain let output = tokio::process::Command::new("git") .args(["cat-file", "-t", &commit_hash]) .current_dir(&repo_path) @@ -389,8 +406,8 @@ async fn test_archive_without_state_events_does_not_sync_git() { let commit_exists = output.map(|o| o.status.success()).unwrap_or(false); assert!( - !commit_exists, - "Git data should NOT be synced without state events (security: validates against Nostr state)" + commit_exists, + "Git data should be synced via StateOnly subscription → state event → git fetch chain" ); // Cleanup -- cgit v1.2.3