diff --git a/.env.example b/.env.example index b05f96a..c97e7a8 100644 --- a/.env.example +++ b/.env.example @@ -120,6 +120,11 @@ # Default: (none - relay discovery from stored announcements only) # NGIT_SYNC_BOOTSTRAP_RELAY_URL=wss://relay.example.com +# Enable GRASP-03 Sync+ mailbox discovery on top of proactive GRASP-02 sync +# CLI: --sync-plus-enabled +# Default: true +# NGIT_SYNC_PLUS_ENABLED=true + # Relays used to discover eligible accepted repository participants' NIP-65 relay lists # CLI: --user-index-relays # Default: wss://purplepag.es,wss://index.hzrd149.com,wss://indexer.coracle.social diff --git a/CHANGELOG.md b/CHANGELOG.md index f0e11bc..3b5dfee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add a default-on `NGIT_SYNC_PLUS_ENABLED` opt-out and advertise GRASP-03 in + NIP-11 only when the Sync+ overlay is effective. - Recover repository-event descendants which reference a direct thread member but omit the repository and root-event tags. When the per-connection ledger can retain complete descendant coverage while preserving control and diff --git a/docs/explanation/architecture.md b/docs/explanation/architecture.md index f7d25c8..63a3295 100644 --- a/docs/explanation/architecture.md +++ b/docs/explanation/architecture.md @@ -580,6 +580,11 @@ The ngit-grasp relay implements **Proactive Sync of Nostr Events**, which synchr For full design details, see [grasp-02-proactive-sync.md](grasp-02-proactive-sync.md). +GRASP-03 Sync+ is a default-on mailbox-discovery overlay on this manager. Set +`NGIT_SYNC_PLUS_ENABLED=false` to retain GRASP-02 sync without discovering +root-author NIP-65 inboxes; NIP-11 advertises `GRASP-03` only while the overlay +is enabled. + ### Rejected Events Index The rejected events index solves two critical problems during sync: diff --git a/docs/explanation/grasp-03-proactive-sync-plus.md b/docs/explanation/grasp-03-proactive-sync-plus.md index 692e57a..0b3ae4e 100644 --- a/docs/explanation/grasp-03-proactive-sync-plus.md +++ b/docs/explanation/grasp-03-proactive-sync-plus.md @@ -4,6 +4,11 @@ GRASP-03 extends repository-declared GRASP-02 coverage to the Nostr outbox model. An accepted issue, patch or pull request may have replies in the root author's inbox even when those events are absent from repository relays. +The overlay is enabled by default and can be disabled with +`NGIT_SYNC_PLUS_ENABLED=false`. Because it extends the proactive GRASP-02 +manager, it is effective only while that manager is running. NIP-11 advertises +`GRASP-03` only when the overlay is enabled. + ## Minimal approach The implementation adds a narrow discovery control-plane, not a second sync diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index c611c06..a207069 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -290,6 +290,25 @@ NGIT_DATABASE_BACKEND=memory These options configure the proactive sync feature that synchronizes events from other relays. +#### `NGIT_SYNC_PLUS_ENABLED` + +**Description:** Enable GRASP-03 Sync+ mailbox discovery on top of proactive GRASP-02 sync +**Type:** Boolean +**Default:** `true` +**Required:** No + +```bash +# Opt out of mailbox discovery while retaining ordinary proactive sync +NGIT_SYNC_PLUS_ENABLED=false +``` + +The corresponding NixOS option is `syncPlusEnabled`. The setting is effective +only while the relay service's proactive sync manager is running. When false, +the relay retains GRASP-02 sync but does not discover NIP-65 inboxes and omits +`GRASP-03` from its NIP-11 `supported_grasps` list. + +--- + #### `NGIT_SYNC_BOOTSTRAP_RELAY_URL` **Description:** URL of the bootstrap relay to initially sync events from diff --git a/nix/module.nix b/nix/module.nix index 4fccd93..3731ba4 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -103,6 +103,16 @@ let description = "Bootstrap relay URL to sync from on startup (optional)"; }; + syncPlusEnabled = mkOption { + type = types.bool; + default = true; + description = '' + Enable GRASP-03 Sync+ mailbox discovery on top of proactive + GRASP-02 sync. This has no effect unless the relay service, and + therefore its proactive sync manager, is enabled. + ''; + }; + userIndexRelays = mkOption { type = types.listOf types.str; default = [ @@ -486,6 +496,7 @@ let toString cfg.metricsConnectionPerIpAbuseThreshold; NGIT_METRICS_TOP_N_REPOS = toString cfg.metricsTopNRepos; NGIT_SYNC_MAX_BACKOFF_SECS = toString cfg.syncMaxBackoffSecs; + NGIT_SYNC_PLUS_ENABLED = if cfg.syncPlusEnabled then "true" else "false"; NGIT_SYNC_DISCONNECT_CHECK_INTERVAL_SECS = toString cfg.syncDisconnectCheckIntervalSecs; NGIT_SYNC_BASE_BACKOFF_SECS = toString cfg.syncBaseBackoffSecs; diff --git a/src/config.rs b/src/config.rs index 91736f2..41ac531 100644 --- a/src/config.rs +++ b/src/config.rs @@ -389,6 +389,15 @@ pub struct Config { #[arg(long, env = "NGIT_SYNC_BOOTSTRAP_RELAY_URL")] pub sync_bootstrap_relay_url: Option, + /// Enable GRASP-03 Sync+ mailbox discovery on top of proactive GRASP-02 sync. + #[arg( + long, + env = "NGIT_SYNC_PLUS_ENABLED", + default_value_t = true, + action = clap::ArgAction::Set + )] + pub sync_plus_enabled: bool, + /// Comma-separated relays used to discover eligible accepted repository participants' /// NIP-65 relay lists. #[arg( @@ -1101,6 +1110,7 @@ impl Config { metrics_connection_per_ip_abuse_threshold: 10, metrics_top_n_repos: 10, sync_bootstrap_relay_url: None, + sync_plus_enabled: true, user_index_relays: DEFAULT_USER_INDEX_RELAYS.to_string(), sync_max_backoff_secs: 3600, sync_disconnect_check_interval_secs: 60, @@ -1220,6 +1230,22 @@ mod tests { ); } + #[test] + fn sync_plus_is_enabled_by_default_and_can_be_disabled() { + let default = Config::try_parse_from(["ngit-grasp", "--domain", "example.com"]) + .expect("Sync+ default should parse"); + assert!(default.sync_plus_enabled); + + let disabled = Config::try_parse_from([ + "ngit-grasp", + "--domain", + "example.com", + "--sync-plus-enabled=false", + ]) + .expect("Sync+ opt-out should parse"); + assert!(!disabled.sync_plus_enabled); + } + #[test] fn user_index_relays_parse_cli_list_and_ignore_empty_entries() { let config = Config::try_parse_from([ diff --git a/src/http/nip11.rs b/src/http/nip11.rs index a98b57f..9bf6036 100644 --- a/src/http/nip11.rs +++ b/src/http/nip11.rs @@ -80,6 +80,9 @@ impl RelayInformationDocument { supported_grasps.push("GRASP-05".to_string()); } supported_grasps.push("GRASP-02".to_string()); + if config.sync_plus_enabled { + supported_grasps.push("GRASP-03".to_string()); + } if config.grasp06_enable { supported_grasps.push("GRASP-06".to_string()); } @@ -197,8 +200,10 @@ mod tests { // (disrespector off). assert!(doc.supported_nips.contains(&9)); assert!(doc.supported_nips.contains(&62)); - // Without archive mode, only GRASP-01 and GRASP-02 - assert_eq!(doc.supported_grasps, vec!["GRASP-01", "GRASP-02"]); + assert_eq!( + doc.supported_grasps, + vec!["GRASP-01", "GRASP-02", "GRASP-03"] + ); assert!(doc.repo_acceptance_criteria.contains("None")); assert!(doc.curation.is_none()); assert_eq!( @@ -236,6 +241,7 @@ mod tests { assert_eq!(parsed["name"], "Test Relay"); assert_eq!(parsed["supported_grasps"][0], "GRASP-01"); assert_eq!(parsed["supported_grasps"][1], "GRASP-02"); + assert_eq!(parsed["supported_grasps"][2], "GRASP-03"); assert_eq!(parsed["icon"], "https://relay.example.com/icon.png"); assert_eq!(parsed["limitation"]["max_subscriptions"], 500); assert_eq!(parsed["limitation"]["max_limit"], 500); @@ -268,7 +274,7 @@ mod tests { // Archive mode enabled: should include GRASP-05 assert_eq!( doc.supported_grasps, - vec!["GRASP-01", "GRASP-05", "GRASP-02"] + vec!["GRASP-01", "GRASP-05", "GRASP-02", "GRASP-03"] ); // Archive read-only: should have curation field assert!(doc.curation.is_some()); @@ -291,7 +297,7 @@ mod tests { // Archive whitelist enabled: should include GRASP-05 assert_eq!( doc.supported_grasps, - vec!["GRASP-01", "GRASP-05", "GRASP-02"] + vec!["GRASP-01", "GRASP-05", "GRASP-02", "GRASP-03"] ); // Archive read-only defaults to true: should have curation field assert!(doc.curation.is_some()); @@ -312,7 +318,10 @@ mod tests { let doc = RelayInformationDocument::from_config(&config); // Repository whitelist doesn't enable GRASP-05 - assert_eq!(doc.supported_grasps, vec!["GRASP-01", "GRASP-02"]); + assert_eq!( + doc.supported_grasps, + vec!["GRASP-01", "GRASP-02", "GRASP-03"] + ); // Should have curation field for repository whitelist assert!(doc.curation.is_some()); assert!(doc @@ -336,7 +345,7 @@ mod tests { // Should have GRASP-05 enabled due to archive whitelist assert_eq!( doc.supported_grasps, - vec!["GRASP-01", "GRASP-05", "GRASP-02"] + vec!["GRASP-01", "GRASP-05", "GRASP-02", "GRASP-03"] ); // Should have curation field reflecting BOTH archive and repository whitelist assert!(doc.curation.is_some()); @@ -366,10 +375,20 @@ mod tests { // to match the insertion order in from_config(). assert_eq!( doc.supported_grasps, - vec!["GRASP-01", "GRASP-02", "GRASP-06"] + vec!["GRASP-01", "GRASP-02", "GRASP-03", "GRASP-06"] ); } + #[test] + fn test_nip11_omits_grasp_03_when_sync_plus_is_disabled() { + let mut config = Config::for_testing(); + config.sync_plus_enabled = false; + + let doc = RelayInformationDocument::from_config(&config); + + assert_eq!(doc.supported_grasps, vec!["GRASP-01", "GRASP-02"]); + } + #[test] fn test_nip11_advertises_deletion_by_default() { let config = Config::for_testing(); diff --git a/src/sync/mod.rs b/src/sync/mod.rs index 0218b9b..8d38181 100644 --- a/src/sync/mod.rs +++ b/src/sync/mod.rs @@ -4816,6 +4816,13 @@ impl SyncManager { } async fn schedule_nip65_discovery(&mut self) { + // GRASP-03 is an optional overlay on the always-running GRASP-02 + // manager. When disabled, do not inventory authors, retain identity + // authority, open discovery connections, or add inbox roots. + if !self.config.sync_plus_enabled { + return; + } + let now = Instant::now(); if self .nip65_discovery