From 52489d3b1a7d79e164b4cc901b53fd06c05ce1b1 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Thu, 11 Dec 2025 16:45:34 +0000 Subject: sync: test sync works without negentropy and add disable option in sync --- src/config.rs | 7 ++++ src/http/nip11.rs | 2 + src/sync/mod.rs | 11 +++-- tests/common/relay.rs | 55 ++++++++++++++++++++++-- tests/sync/bootstrap.rs | 108 +++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 175 insertions(+), 8 deletions(-) diff --git a/src/config.rs b/src/config.rs index f3a3e2a..8c6de05 100644 --- a/src/config.rs +++ b/src/config.rs @@ -104,6 +104,12 @@ pub struct Config { /// Note: The connection timeout is capped at this value #[arg(long, env = "NGIT_SYNC_BASE_BACKOFF_SECS", default_value_t = 5)] pub sync_base_backoff_secs: u64, + + /// Disable NIP-77 negentropy sync (default: false) + /// When enabled, sync will use REQ+EOSE instead of negentropy for history sync. + /// Primarily useful for testing that sync works without negentropy support. + #[arg(long, env = "NGIT_SYNC_DISABLE_NEGENTROPY", default_value_t = false)] + pub sync_disable_negentropy: bool, } impl Config { @@ -163,6 +169,7 @@ impl Config { sync_max_backoff_secs: 3600, sync_disconnect_check_interval_secs: 60, sync_base_backoff_secs: 5, + sync_disable_negentropy: false, } } } diff --git a/src/http/nip11.rs b/src/http/nip11.rs index 8c18dde..7df8306 100644 --- a/src/http/nip11.rs +++ b/src/http/nip11.rs @@ -109,6 +109,7 @@ mod tests { sync_max_backoff_secs: 3600, sync_disconnect_check_interval_secs: 60, sync_base_backoff_secs: 5, + sync_disable_negentropy: false, }; let doc = RelayInformationDocument::from_config(&config); @@ -147,6 +148,7 @@ mod tests { sync_max_backoff_secs: 3600, sync_disconnect_check_interval_secs: 60, sync_base_backoff_secs: 5, + sync_disable_negentropy: false, }; let doc = RelayInformationDocument::from_config(&config); diff --git a/src/sync/mod.rs b/src/sync/mod.rs index 3f3966a..c4c3c7f 100644 --- a/src/sync/mod.rs +++ b/src/sync/mod.rs @@ -511,8 +511,9 @@ impl SyncManager { } }; - // Check if relay supports NIP-77 negentropy - let use_negentropy = connection.supports_negentropy().await; + // Check if relay supports NIP-77 negentropy AND negentropy is not disabled + let use_negentropy = !self.config.sync_disable_negentropy + && connection.supports_negentropy().await; // Unsubscribe all current subscriptions connection.unsubscribe_all().await; @@ -948,7 +949,11 @@ impl SyncManager { ); // Check if relay supports NIP-77 negentropy for efficient sync - let use_negentropy = if let Some(connection) = self.connections.get(relay_url) { + // Respect the sync_disable_negentropy config option + let use_negentropy = if self.config.sync_disable_negentropy { + tracing::debug!(relay = %relay_url, "Negentropy disabled via config"); + false + } else if let Some(connection) = self.connections.get(relay_url) { connection.supports_negentropy().await } else { false diff --git a/tests/common/relay.rs b/tests/common/relay.rs index 0b3926e..2dd526b 100644 --- a/tests/common/relay.rs +++ b/tests/common/relay.rs @@ -38,7 +38,21 @@ impl TestRelay { /// Start relay on a specific port pub async fn start_with_port(port: u16) -> Self { - Self::start_with_options(port, None).await + Self::start_with_full_options(port, None, false).await + } + + /// Start relay on a specific port with full options + /// + /// This is useful for testing history sync where we need to: + /// 1. Start relay_b (first instance) to get its domain + /// 2. Stop relay_b + /// 3. Start relay_b (second instance) on SAME port with different options + pub async fn start_on_port_with_options( + port: u16, + bootstrap_relay_url: Option, + disable_negentropy: bool, + ) -> Self { + Self::start_with_full_options(port, bootstrap_relay_url, disable_negentropy).await } /// Start relay with sync from another relay (bootstrap relay) @@ -58,11 +72,39 @@ impl TestRelay { /// } /// ``` pub async fn start_with_sync(bootstrap_relay_url: Option) -> Self { - Self::start_with_options(Self::find_free_port(), bootstrap_relay_url).await + Self::start_with_full_options(Self::find_free_port(), bootstrap_relay_url, false).await + } + + /// Start relay with sync and negentropy disabled + /// + /// This is useful for testing that sync works without NIP-77 negentropy. + /// History sync will use REQ+EOSE instead of the more efficient negentropy protocol. + /// + /// # Example + /// + /// ```no_run + /// use common::TestRelay; + /// + /// #[tokio::test] + /// async fn test_sync_without_negentropy() { + /// let source = TestRelay::start().await; + /// let syncing = TestRelay::start_with_sync_no_negentropy(Some(source.url().into())).await; + /// // ... test sync behavior without negentropy ... + /// syncing.stop().await; + /// source.stop().await; + /// } + /// ``` + pub async fn start_with_sync_no_negentropy(bootstrap_relay_url: Option) -> Self { + Self::start_with_full_options(Self::find_free_port(), bootstrap_relay_url, true).await } - /// Start relay with options + /// Start relay with options (internal, maintains backward compatibility) async fn start_with_options(port: u16, bootstrap_relay_url: Option) -> Self { + Self::start_with_full_options(port, bootstrap_relay_url, false).await + } + + /// Start relay with full options + async fn start_with_full_options(port: u16, bootstrap_relay_url: Option, disable_negentropy: bool) -> Self { let bind_address = format!("127.0.0.1:{}", port); let url = format!("ws://127.0.0.1:{}", port); @@ -108,6 +150,11 @@ impl TestRelay { cmd.env("NGIT_SYNC_BOOTSTRAP_RELAY_URL", bootstrap_url); } + // Add negentropy disable flag if requested + if disable_negentropy { + cmd.env("NGIT_SYNC_DISABLE_NEGENTROPY", "true"); + } + let process = cmd.spawn().expect("Failed to start relay process"); let relay = Self { process, url, port }; @@ -166,7 +213,7 @@ impl TestRelay { } /// Find a free port to use for testing - fn find_free_port() -> u16 { + pub fn find_free_port() -> u16 { use std::net::TcpListener; // Bind to port 0 to get a random free port diff --git a/tests/sync/bootstrap.rs b/tests/sync/bootstrap.rs index 0d68609..8a181c9 100644 --- a/tests/sync/bootstrap.rs +++ b/tests/sync/bootstrap.rs @@ -305,4 +305,110 @@ async fn test_announcement_not_listing_relay_is_not_synced() { announcement_id ); println!("SUCCESS: Announcement was correctly rejected by relay_b (not synced)"); -} \ No newline at end of file +} + +/// Test: History sync (bootstrap) works without NIP-77 negentropy +/// +/// This tests that HISTORY sync works when negentropy is disabled. +/// History sync means: events that existed on the source relay BEFORE +/// the syncing relay connected. +/// +/// Scenario: +/// 1. Start relay_b temporarily to get its domain (then stop it) +/// 2. Start relay_a (source) +/// 3. Create announcement listing both relay domains +/// 4. Send announcement to relay_a (event exists BEFORE relay_b connects) +/// 5. Start relay_b AGAIN on same port, with negentropy DISABLED +/// 6. relay_b should sync the pre-existing event via REQ+EOSE (history sync) +/// 7. Verify relay_b has the event +/// +/// This is different from "live sync" where events arrive after connection. +#[tokio::test] +async fn test_history_sync_without_negentropy() { + // 1. First, start relay_b temporarily just to reserve a port and get its domain + let relay_b_port = TestRelay::find_free_port(); + let relay_b_domain = format!("127.0.0.1:{}", relay_b_port); + println!( + "Reserved port {} for relay_b (domain: {})", + relay_b_port, relay_b_domain + ); + + // 2. Start relay_a (source relay) + let relay_a = TestRelay::start().await; + println!( + "relay_a started at {} (domain: {})", + relay_a.url(), + relay_a.domain() + ); + + // 3. Create test keys + let keys = Keys::generate(); + + // 4. Create announcement listing BOTH relay domains + // This event will exist on relay_a BEFORE relay_b ever connects + let announcement = create_repo_announcement( + &keys, + &[&relay_a.domain(), &relay_b_domain], + "test-repo-history-no-negentropy", + ); + let announcement_id = announcement.id; + + println!( + "Created announcement {} (kind {})", + announcement_id, + announcement.kind.as_u16() + ); + for tag in announcement.tags.iter() { + println!(" Tag: {:?}", tag.as_slice()); + } + + // 5. Send announcement to relay_a (event now exists BEFORE relay_b connects) + let client_a = TestClient::new(relay_a.url(), keys.clone()) + .await + .expect("Failed to connect to relay_a"); + + client_a + .send_event(&announcement) + .await + .expect("Failed to send announcement to relay_a"); + println!("Announcement sent to relay_a (event exists BEFORE relay_b connects)"); + + client_a.disconnect().await; + + // 6. Wait a moment to ensure the event is stored + tokio::time::sleep(Duration::from_millis(500)).await; + + // 7. NOW start relay_b on the reserved port, with negentropy DISABLED + // This relay_b has never connected before - it needs to do HISTORY sync + let relay_b = TestRelay::start_on_port_with_options( + relay_b_port, + Some(relay_a.url().into()), + true, // disable_negentropy = true + ).await; + println!( + "relay_b started at {} (domain: {}) - negentropy DISABLED, will do HISTORY sync", + relay_b.url(), + relay_b.domain() + ); + + // 8. Wait for history sync to complete (using REQ+EOSE, not negentropy) + tokio::time::sleep(Duration::from_secs(3)).await; + + // 9. Verify announcement synced to relay_b via HISTORY sync + let filter = Filter::new() + .kind(Kind::Custom(KIND_REPOSITORY_STATE)) + .author(keys.public_key()); + + let synced = wait_for_event_on_relay(relay_b.url(), filter, Duration::from_secs(5)).await; + + // 10. Cleanup + relay_b.stop().await; + relay_a.stop().await; + + assert!( + synced, + "Announcement {} should have synced from relay_a to relay_b via HISTORY sync (REQ+EOSE, negentropy disabled)", + announcement_id + ); + println!("SUCCESS: History sync works without negentropy (using REQ+EOSE fallback)"); +} -- cgit v1.2.3