diff --git a/crates/nostr-sdk/CHANGELOG.md b/crates/nostr-sdk/CHANGELOG.md index f6f22460a..285bfde84 100644 --- a/crates/nostr-sdk/CHANGELOG.md +++ b/crates/nostr-sdk/CHANGELOG.md @@ -29,6 +29,10 @@ - Replace `usize` with `u8` for gossip relay limits +### Added + +- Add `GossipAllowedRelays` to `GossipOptions` to filter relays during selection (https://github.com/rust-nostr/nostr/pull/1128) + ## v0.44.1 - 2025/11/09 ### Fixed diff --git a/crates/nostr-sdk/src/client/mod.rs b/crates/nostr-sdk/src/client/mod.rs index 7cf98b2ed..aad1209cc 100644 --- a/crates/nostr-sdk/src/client/mod.rs +++ b/crates/nostr-sdk/src/client/mod.rs @@ -1652,7 +1652,12 @@ impl Client { // Broken-down filters let filters: HashMap = match gossip - .break_down_filter(filter, pattern, &self.opts.gossip.limits) + .break_down_filter( + filter, + pattern, + &self.opts.gossip.limits, + self.opts.gossip.allowed, + ) .await? { BrokenDownFilters::Filters(filters) => filters, @@ -1728,6 +1733,7 @@ impl Client { .get_relays( event.tags.public_keys(), BestRelaySelection::PrivateMessage { limit: 3 }, + self.opts.gossip.allowed, ) .await?; @@ -1758,6 +1764,7 @@ impl Client { hints: 1, most_received: 1, }, + self.opts.gossip.allowed, ) .await?; @@ -1772,6 +1779,7 @@ impl Client { hints: 1, most_received: 1, }, + self.opts.gossip.allowed, ) .await?; diff --git a/crates/nostr-sdk/src/client/options.rs b/crates/nostr-sdk/src/client/options.rs index 6c64f5e31..9be3a5302 100644 --- a/crates/nostr-sdk/src/client/options.rs +++ b/crates/nostr-sdk/src/client/options.rs @@ -10,6 +10,7 @@ use std::net::SocketAddr; use std::path::Path; use std::time::Duration; +use nostr_gossip::GossipAllowedRelays; use nostr_relay_pool::prelude::*; /// Max number of relays to use for gossip @@ -44,6 +45,8 @@ impl Default for GossipRelayLimits { pub struct GossipOptions { /// Max number of relays to use pub limits: GossipRelayLimits, + /// Allowed relay during selection + pub allowed: GossipAllowedRelays, } impl GossipOptions { @@ -53,6 +56,13 @@ impl GossipOptions { self.limits = limits; self } + + /// Set allowed + #[inline] + pub fn allowed(mut self, allowed: GossipAllowedRelays) -> Self { + self.allowed = allowed; + self + } } /// Options diff --git a/crates/nostr-sdk/src/gossip/mod.rs b/crates/nostr-sdk/src/gossip/mod.rs index f7a4d86b2..32324f698 100644 --- a/crates/nostr-sdk/src/gossip/mod.rs +++ b/crates/nostr-sdk/src/gossip/mod.rs @@ -8,7 +8,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use nostr::prelude::*; -use nostr_gossip::{BestRelaySelection, NostrGossip}; +use nostr_gossip::{BestRelaySelection, GossipAllowedRelays, NostrGossip}; use crate::client::options::GossipRelayLimits; use crate::client::Error; @@ -58,6 +58,7 @@ impl GossipWrapper { &self, public_keys: I, selection: BestRelaySelection, + allowed: GossipAllowedRelays, ) -> Result, Error> where I: IntoIterator, @@ -65,8 +66,10 @@ impl GossipWrapper { let mut urls: HashSet = HashSet::new(); for public_key in public_keys.into_iter() { - let relays: HashSet = - self.gossip.get_best_relays(public_key, selection).await?; + let relays: HashSet = self + .gossip + .get_best_relays(public_key, selection, allowed) + .await?; urls.extend(relays); } @@ -77,6 +80,7 @@ impl GossipWrapper { &self, public_keys: I, selection: BestRelaySelection, + allowed: GossipAllowedRelays, ) -> Result>, Error> where I: IntoIterator, @@ -84,8 +88,10 @@ impl GossipWrapper { let mut urls: HashMap> = HashMap::new(); for public_key in public_keys.into_iter() { - let relays: HashSet = - self.gossip.get_best_relays(public_key, selection).await?; + let relays: HashSet = self + .gossip + .get_best_relays(public_key, selection, allowed) + .await?; for url in relays.into_iter() { urls.entry(url) @@ -105,6 +111,7 @@ impl GossipWrapper { filter: Filter, pattern: GossipFilterPattern, limits: &GossipRelayLimits, + allowed: GossipAllowedRelays, ) -> Result { // Extract `p` tag from generic tags and parse public key hex let p_tag: Option> = filter.generic_tags.get(&P_TAG).map(|s| { @@ -123,6 +130,7 @@ impl GossipWrapper { BestRelaySelection::Write { limit: limits.write_relays_per_user, }, + allowed, ) .await?; @@ -133,6 +141,7 @@ impl GossipWrapper { BestRelaySelection::Hints { limit: limits.hint_relays_per_user, }, + allowed, ) .await?; @@ -143,6 +152,7 @@ impl GossipWrapper { BestRelaySelection::MostReceived { limit: limits.most_used_relays_per_user, }, + allowed, ) .await?; @@ -158,6 +168,7 @@ impl GossipWrapper { BestRelaySelection::PrivateMessage { limit: limits.nip17_relays, }, + allowed, ) .await?; @@ -191,6 +202,7 @@ impl GossipWrapper { BestRelaySelection::Read { limit: limits.read_relays_per_user, }, + allowed, ) .await?; @@ -201,6 +213,7 @@ impl GossipWrapper { BestRelaySelection::Hints { limit: limits.hint_relays_per_user, }, + allowed, ) .await?; @@ -211,6 +224,7 @@ impl GossipWrapper { BestRelaySelection::MostReceived { limit: limits.most_used_relays_per_user, }, + allowed, ) .await?; @@ -227,6 +241,7 @@ impl GossipWrapper { BestRelaySelection::PrivateMessage { limit: limits.nip17_relays, }, + allowed, ) .await?; @@ -267,6 +282,7 @@ impl GossipWrapper { hints: limits.hint_relays_per_user, most_received: limits.most_used_relays_per_user, }, + allowed, ) .await?; @@ -279,6 +295,7 @@ impl GossipWrapper { BestRelaySelection::PrivateMessage { limit: limits.nip17_relays, }, + allowed, ) .await?; @@ -410,6 +427,7 @@ mod tests { filter.clone(), GossipFilterPattern::Nip65, &GossipRelayLimits::default(), + GossipAllowedRelays::default(), ) .await .unwrap() @@ -430,6 +448,7 @@ mod tests { authors_filter.clone(), GossipFilterPattern::Nip65, &GossipRelayLimits::default(), + GossipAllowedRelays::default(), ) .await .unwrap() @@ -465,6 +484,7 @@ mod tests { search_filter.clone(), GossipFilterPattern::Nip65, &GossipRelayLimits::default(), + GossipAllowedRelays::default(), ) .await .unwrap() @@ -482,6 +502,7 @@ mod tests { p_tag_filter.clone(), GossipFilterPattern::Nip65, &GossipRelayLimits::default(), + GossipAllowedRelays::default(), ) .await .unwrap() @@ -507,6 +528,7 @@ mod tests { filter.clone(), GossipFilterPattern::Nip65, &GossipRelayLimits::default(), + GossipAllowedRelays::default(), ) .await .unwrap() @@ -531,6 +553,7 @@ mod tests { filter.clone(), GossipFilterPattern::Nip65, &GossipRelayLimits::default(), + GossipAllowedRelays::default(), ) .await .unwrap() diff --git a/gossip/nostr-gossip-memory/CHANGELOG.md b/gossip/nostr-gossip-memory/CHANGELOG.md index 75639c56d..4b973ab88 100644 --- a/gossip/nostr-gossip-memory/CHANGELOG.md +++ b/gossip/nostr-gossip-memory/CHANGELOG.md @@ -33,6 +33,10 @@ - Replace `Mutex` with `RwLock` for better concurrency (https://github.com/rust-nostr/nostr/pull/1126) +### Added + +- Add support for `GossipAllowedRelays` filtering during relay selection (https://github.com/rust-nostr/nostr/pull/1128) + ## v0.44.0 - 2025/11/06 First release. diff --git a/gossip/nostr-gossip-memory/src/store.rs b/gossip/nostr-gossip-memory/src/store.rs index a52f20e4a..7f98e11e4 100644 --- a/gossip/nostr-gossip-memory/src/store.rs +++ b/gossip/nostr-gossip-memory/src/store.rs @@ -13,7 +13,9 @@ use nostr::util::BoxedFuture; use nostr::{Event, Kind, PublicKey, RelayUrl, TagKind, TagStandard, Timestamp}; use nostr_gossip::error::GossipError; use nostr_gossip::flags::GossipFlags; -use nostr_gossip::{BestRelaySelection, GossipListKind, GossipPublicKeyStatus, NostrGossip}; +use nostr_gossip::{ + BestRelaySelection, GossipAllowedRelays, GossipListKind, GossipPublicKeyStatus, NostrGossip, +}; use tokio::sync::RwLock; use crate::constant::{MAX_NIP17_SIZE, MAX_NIP65_SIZE, PUBKEY_METADATA_OUTDATED_AFTER}; @@ -194,6 +196,7 @@ impl NostrGossipMemory { &self, public_key: &PublicKey, selection: BestRelaySelection, + allowed: GossipAllowedRelays, ) -> HashSet { let public_keys = self.public_keys.read().await; @@ -211,6 +214,7 @@ impl NostrGossipMemory { &public_keys, public_key, GossipFlags::READ, + allowed, read, )); @@ -219,6 +223,7 @@ impl NostrGossipMemory { &public_keys, public_key, GossipFlags::WRITE, + allowed, write, )); @@ -227,6 +232,7 @@ impl NostrGossipMemory { &public_keys, public_key, GossipFlags::HINT, + allowed, hints, )); @@ -235,6 +241,7 @@ impl NostrGossipMemory { &public_keys, public_key, GossipFlags::RECEIVED, + allowed, most_received, )); } @@ -243,6 +250,7 @@ impl NostrGossipMemory { &public_keys, public_key, GossipFlags::READ, + allowed, limit, )); } @@ -251,6 +259,7 @@ impl NostrGossipMemory { &public_keys, public_key, GossipFlags::WRITE, + allowed, limit, )); } @@ -259,6 +268,7 @@ impl NostrGossipMemory { &public_keys, public_key, GossipFlags::PRIVATE_MESSAGE, + allowed, limit, )); } @@ -267,6 +277,7 @@ impl NostrGossipMemory { &public_keys, public_key, GossipFlags::HINT, + allowed, limit, )); } @@ -275,6 +286,7 @@ impl NostrGossipMemory { &public_keys, public_key, GossipFlags::RECEIVED, + allowed, limit, )); } @@ -288,12 +300,25 @@ impl NostrGossipMemory { tx: &LruCache, public_key: &PublicKey, flag: GossipFlags, + allowed: GossipAllowedRelays, limit: u8, ) -> impl Iterator + '_ { let mut relays: Vec<(RelayUrl, u64, Option)> = Vec::new(); if let Some(pk_data) = tx.peek(public_key) { for (relay_url, relay_data) in pk_data.relays.iter() { + if !allowed.onion && relay_url.is_onion() { + continue; + } + + if !allowed.local && relay_url.is_local_addr() { + continue; + } + + if !allowed.without_tls && !relay_url.scheme().is_secure() { + continue; + } + // Check if the relay has the specified flag if relay_data.bitflags.has(flag) { relays.push(( @@ -374,8 +399,9 @@ impl NostrGossip for NostrGossipMemory { &'a self, public_key: &'a PublicKey, selection: BestRelaySelection, + allowed: GossipAllowedRelays, ) -> BoxedFuture<'a, Result, GossipError>> { - Box::pin(async move { Ok(self._get_best_relays(public_key, selection).await) }) + Box::pin(async move { Ok(self._get_best_relays(public_key, selection, allowed).await) }) } } diff --git a/gossip/nostr-gossip-test-suite/src/lib.rs b/gossip/nostr-gossip-test-suite/src/lib.rs index cf490ec18..c81214460 100644 --- a/gossip/nostr-gossip-test-suite/src/lib.rs +++ b/gossip/nostr-gossip-test-suite/src/lib.rs @@ -38,14 +38,22 @@ macro_rules! gossip_unit_tests { // Test Read selection let read_relays = store - .get_best_relays(&public_key, BestRelaySelection::Read { limit: 2 }) + .get_best_relays( + &public_key, + BestRelaySelection::Read { limit: 2 }, + GossipAllowedRelays::default(), + ) .await.unwrap(); assert_eq!(read_relays.len(), 2); // relay.damus.io and nos.lol // Test Write selection let write_relays = store - .get_best_relays(&public_key, BestRelaySelection::Write { limit: 2 }) + .get_best_relays( + &public_key, + BestRelaySelection::Write { limit: 2 }, + GossipAllowedRelays::default(), + ) .await.unwrap(); assert_eq!(write_relays.len(), 2); // relay.damus.io and relay.nostr.band @@ -65,7 +73,11 @@ macro_rules! gossip_unit_tests { // Test PrivateMessage selection let pm_relays = store - .get_best_relays(&public_key, BestRelaySelection::PrivateMessage { limit: 4 }) + .get_best_relays( + &public_key, + BestRelaySelection::PrivateMessage { limit: 4 }, + GossipAllowedRelays::default(), + ) .await.unwrap(); assert_eq!(pm_relays.len(), 3); // inbox.nostr.wine and relay.primal.net @@ -96,7 +108,11 @@ macro_rules! gossip_unit_tests { store.process(&event, None).await.unwrap(); let hint_relays = store - .get_best_relays(&public_key, BestRelaySelection::Hints { limit: 5 }) + .get_best_relays( + &public_key, + BestRelaySelection::Hints { limit: 5 }, + GossipAllowedRelays::default(), + ) .await.unwrap(); assert_eq!(hint_relays.len(), 1); @@ -124,6 +140,7 @@ macro_rules! gossip_unit_tests { .get_best_relays( &keys.public_key, BestRelaySelection::MostReceived { limit: 10 }, + GossipAllowedRelays::default(), ) .await.unwrap(); @@ -168,6 +185,7 @@ macro_rules! gossip_unit_tests { hints: 5, most_received: 5, }, + GossipAllowedRelays::default(), ) .await.unwrap(); @@ -187,6 +205,92 @@ macro_rules! gossip_unit_tests { .any(|r| r.as_str() == "wss://received.relay.io")); } + #[tokio::test] + async fn test_selection_with_allowed_relays() { + let store: $store_type = $setup_fn().await; + + // NIP-65 relay list event with read and write relays + let json = r#"{"id":"0a49bed4a1eb0973a68a0d43b7ca62781ffd4e052b91bbadef09e5cf756f6e68","pubkey":"68d81165918100b7da43fc28f7d1fc12554466e1115886b9e7bb326f65ec4272","created_at":1759351841,"kind":10002,"tags":[["alt","Relay list to discover the user's content"],["r","wss://relay.damus.io/"],["r","ws://192.168.1.11:7777"],["r","ws://oxtrdevav64z64yb7x6rjg4ntzqjhedm5b5zjqulugknhzr46ny2qbad.onion"]],"content":"","sig":"f5bc6c18b0013214588d018c9086358fb76a529aa10867d4d02a75feb239412ae1c94ac7c7917f6e6e2303d72f00dc4e9b03b168ef98f3c3c0dec9a457ce0304"}"#; + let event = Event::from_json(json).unwrap(); + + store.process(&event, None).await.unwrap(); + + let public_key = event.pubkey; + let damus_relay = RelayUrl::parse("wss://relay.damus.io").unwrap(); + let local_relay = RelayUrl::parse("ws://192.168.1.11:7777").unwrap(); + let oxtr_relay = + RelayUrl::parse("ws://oxtrdevav64z64yb7x6rjg4ntzqjhedm5b5zjqulugknhzr46ny2qbad.onion") + .unwrap(); + + // Test selection with all relays + let read_relays = store + .get_best_relays( + &public_key, + BestRelaySelection::Read { limit: u8::MAX }, + GossipAllowedRelays { + onion: true, + local: true, + without_tls: true, + }, + ) + .await.unwrap(); + + assert_eq!(read_relays.len(), 3); + assert!(read_relays.contains(&damus_relay)); + assert!(read_relays.contains(&local_relay)); + assert!(read_relays.contains(&oxtr_relay)); + + // Test selection without local relays + let read_relays = store + .get_best_relays( + &public_key, + BestRelaySelection::Read { limit: u8::MAX }, + GossipAllowedRelays { + onion: true, + local: false, + without_tls: true, + }, + ) + .await.unwrap(); + + assert_eq!(read_relays.len(), 2); + assert!(read_relays.contains(&damus_relay)); + assert!(read_relays.contains(&oxtr_relay)); + + // Test selection without onion and local relays + let read_relays = store + .get_best_relays( + &public_key, + BestRelaySelection::Read { limit: u8::MAX }, + GossipAllowedRelays { + onion: false, + local: true, + without_tls: true, + }, + ) + .await.unwrap(); + + assert_eq!(read_relays.len(), 2); + assert!(read_relays.contains(&damus_relay)); + assert!(read_relays.contains(&local_relay)); + + // Test selection TLS-only relays + let read_relays = store + .get_best_relays( + &public_key, + BestRelaySelection::Read { limit: u8::MAX }, + GossipAllowedRelays { + onion: true, + local: true, + without_tls: false, + }, + ) + .await.unwrap(); + + assert_eq!(read_relays.len(), 1); + assert!(read_relays.contains(&damus_relay)); + } + #[tokio::test] async fn test_status_tracking() { let store: $store_type = $setup_fn().await; @@ -225,7 +329,11 @@ macro_rules! gossip_unit_tests { // Should return empty set let relays = store - .get_best_relays(&public_key, BestRelaySelection::Read { limit: 10 }) + .get_best_relays( + &public_key, + BestRelaySelection::Read { limit: 10 }, + GossipAllowedRelays::default(), + ) .await.unwrap(); assert_eq!(relays.len(), 0); diff --git a/gossip/nostr-gossip/CHANGELOG.md b/gossip/nostr-gossip/CHANGELOG.md index d1fb74c5b..1d8a48f35 100644 --- a/gossip/nostr-gossip/CHANGELOG.md +++ b/gossip/nostr-gossip/CHANGELOG.md @@ -23,6 +23,16 @@ --> +## Unreleased + +### Breaking changes + +- Change `NostrGossip::get_best_relays` signature (https://github.com/rust-nostr/nostr/pull/1128) + +### Added + +- Add `GossipAllowedRelays` struct to allow relays filtering during selection (https://github.com/rust-nostr/nostr/pull/1128) + ## v0.44.0 - 2025/11/06 First release. diff --git a/gossip/nostr-gossip/src/lib.rs b/gossip/nostr-gossip/src/lib.rs index c57a8f79d..c6af197aa 100644 --- a/gossip/nostr-gossip/src/lib.rs +++ b/gossip/nostr-gossip/src/lib.rs @@ -53,6 +53,27 @@ pub enum GossipPublicKeyStatus { }, } +/// Allowed gossip relay types during selection +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct GossipAllowedRelays { + /// Allow tor onion relays (default: true) + pub onion: bool, + /// Allow local network relays (default: false) + pub local: bool, + /// Allow relays without SSL/TLS encryption (default: true) + pub without_tls: bool, +} + +impl Default for GossipAllowedRelays { + fn default() -> Self { + Self { + onion: true, + local: false, + without_tls: true, + } + } +} + /// Best relay selection. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum BestRelaySelection { @@ -124,6 +145,7 @@ pub trait NostrGossip: Any + Debug + Send + Sync { &'a self, public_key: &'a PublicKey, selection: BestRelaySelection, + allowed: GossipAllowedRelays, ) -> BoxedFuture<'a, Result, GossipError>>; }