Skip to content

Commit f2bf31e

Browse files
committed
sdk: fetch gossip NIP-17 list only if really needed
Closes https://gitworkshop.dev/yukikishimoto.com/nostr/issues/note15mhvsqdntu8w3njkh7urf92jlvak5rxdq7z9snfjtf2678kd9qvsgut0hz Signed-off-by: Yuki Kishimoto <[email protected]>
1 parent 506cb68 commit f2bf31e

File tree

3 files changed

+160
-38
lines changed

3 files changed

+160
-38
lines changed

crates/nostr-sdk/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
- Remove `lmdb`, `ndb` and `indexeddb` features (https://github.com/rust-nostr/nostr/pull/1083)
3131
- Replace `ReceiverStream` with `BoxStream` (https://github.com/rust-nostr/nostr/pull/1087)
3232

33+
### Changed
34+
35+
- Fetch gossip NIP-17 list only if really needed (https://github.com/rust-nostr/nostr/pull/1090)
36+
3337
### Added
3438

3539
- `Client::public_key` function to retrieve the public key (https://github.com/rust-nostr/nostr/pull/1028)

crates/nostr-sdk/src/client/mod.rs

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub use self::error::Error;
2525
pub use self::options::{ClientOptions, SleepWhenIdle};
2626
#[cfg(not(target_arch = "wasm32"))]
2727
pub use self::options::{Connection, ConnectionTarget};
28-
use crate::gossip::{BrokenDownFilters, Gossip};
28+
use crate::gossip::{self, BrokenDownFilters, Gossip, GossipFilterPattern, GossipKind};
2929

3030
/// Nostr client
3131
#[derive(Debug, Clone)]
@@ -1323,22 +1323,29 @@ impl Client {
13231323
// Gossip
13241324
impl Client {
13251325
/// Check if there are outdated public keys and update them
1326-
async fn check_and_update_gossip<I>(&self, public_keys: I) -> Result<(), Error>
1326+
async fn check_and_update_gossip<I>(
1327+
&self,
1328+
public_keys: I,
1329+
kind: GossipKind,
1330+
) -> Result<(), Error>
13271331
where
13281332
I: IntoIterator<Item = PublicKey>,
13291333
{
13301334
let outdated_public_keys: HashSet<PublicKey> =
1331-
self.gossip.check_outdated(public_keys).await;
1335+
self.gossip.check_outdated(public_keys, &kind).await;
13321336

13331337
// No outdated public keys, immediately return.
13341338
if outdated_public_keys.is_empty() {
13351339
return Ok(());
13361340
}
13371341

1342+
// Get kind
1343+
let kind: Kind = kind.to_event_kind();
1344+
13381345
// Compose filters
13391346
let filter: Filter = Filter::default()
13401347
.authors(outdated_public_keys.clone())
1341-
.kinds([Kind::RelayList, Kind::InboxRelays]);
1348+
.kind(kind);
13421349

13431350
// Query from database
13441351
let stored_events: Events = self.database().query(filter.clone()).await?;
@@ -1374,23 +1381,38 @@ impl Client {
13741381
// Extract all public keys from filters
13751382
let public_keys = filter.extract_public_keys();
13761383

1377-
// Check and update outdated public keys
1378-
self.check_and_update_gossip(public_keys).await?;
1384+
// Find pattern to decide what list to update
1385+
let pattern: GossipFilterPattern = gossip::find_filter_pattern(&filter);
1386+
1387+
// Update outdated public keys
1388+
match &pattern {
1389+
GossipFilterPattern::Nip65 => {
1390+
self.check_and_update_gossip(public_keys, GossipKind::Nip65)
1391+
.await?;
1392+
}
1393+
GossipFilterPattern::Nip65AndNip17 => {
1394+
self.check_and_update_gossip(public_keys.iter().copied(), GossipKind::Nip65)
1395+
.await?;
1396+
self.check_and_update_gossip(public_keys, GossipKind::Nip17)
1397+
.await?;
1398+
}
1399+
}
13791400

13801401
// Broken-down filters
1381-
let filters: HashMap<RelayUrl, Filter> = match self.gossip.break_down_filter(filter).await {
1382-
BrokenDownFilters::Filters(filters) => filters,
1383-
BrokenDownFilters::Orphan(filter) | BrokenDownFilters::Other(filter) => {
1384-
// Get read relays
1385-
let read_relays: Vec<RelayUrl> = self.pool.__read_relay_urls().await;
1386-
1387-
let mut map = HashMap::with_capacity(read_relays.len());
1388-
for url in read_relays.into_iter() {
1389-
map.insert(url, filter.clone());
1402+
let filters: HashMap<RelayUrl, Filter> =
1403+
match self.gossip.break_down_filter(filter, pattern).await {
1404+
BrokenDownFilters::Filters(filters) => filters,
1405+
BrokenDownFilters::Orphan(filter) | BrokenDownFilters::Other(filter) => {
1406+
// Get read relays
1407+
let read_relays: Vec<RelayUrl> = self.pool.__read_relay_urls().await;
1408+
1409+
let mut map = HashMap::with_capacity(read_relays.len());
1410+
for url in read_relays.into_iter() {
1411+
map.insert(url, filter.clone());
1412+
}
1413+
map
13901414
}
1391-
map
1392-
}
1393-
};
1415+
};
13941416

13951417
// Add gossip (outbox and inbox) relays
13961418
for url in filters.keys() {
@@ -1417,17 +1439,24 @@ impl Client {
14171439

14181440
// Get involved public keys and check what are up to date in the gossip graph and which ones require an update.
14191441
if is_gift_wrap {
1442+
let kind: GossipKind = if is_nip17 {
1443+
GossipKind::Nip17
1444+
} else {
1445+
GossipKind::Nip65
1446+
};
1447+
14201448
// Get only p tags since the author of a gift wrap is randomized
14211449
let public_keys = event.tags.public_keys().copied();
1422-
self.check_and_update_gossip(public_keys).await?;
1450+
self.check_and_update_gossip(public_keys, kind).await?;
14231451
} else {
14241452
// Get all public keys involved in the event: author + p tags
14251453
let public_keys = event
14261454
.tags
14271455
.public_keys()
14281456
.copied()
14291457
.chain(iter::once(event.pubkey));
1430-
self.check_and_update_gossip(public_keys).await?;
1458+
self.check_and_update_gossip(public_keys, GossipKind::Nip65)
1459+
.await?;
14311460
};
14321461

14331462
// Check if NIP17 or NIP65

crates/nostr-sdk/src/gossip/mod.rs

Lines changed: 107 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,21 @@ use self::constant::{
1818

1919
const P_TAG: SingleLetterTag = SingleLetterTag::lowercase(Alphabet::P);
2020

21+
pub(crate) enum GossipKind {
22+
Nip17,
23+
Nip65,
24+
}
25+
26+
impl GossipKind {
27+
#[allow(clippy::wrong_self_convention)]
28+
pub(crate) fn to_event_kind(self) -> Kind {
29+
match self {
30+
Self::Nip17 => Kind::InboxRelays,
31+
Self::Nip65 => Kind::RelayList,
32+
}
33+
}
34+
}
35+
2136
#[derive(Debug)]
2237
pub enum BrokenDownFilters {
2338
/// Filters by url
@@ -145,7 +160,7 @@ impl Gossip {
145160
}
146161

147162
/// Check for what public keys the metadata are outdated or not existent (both for NIP17 and NIP65)
148-
pub async fn check_outdated<I>(&self, public_keys: I) -> HashSet<PublicKey>
163+
pub async fn check_outdated<I>(&self, public_keys: I, kind: &GossipKind) -> HashSet<PublicKey>
149164
where
150165
I: IntoIterator<Item = PublicKey>,
151166
{
@@ -161,14 +176,28 @@ impl Gossip {
161176
continue;
162177
}
163178

164-
// Check if collections are empty
165-
let empty: bool =
166-
lists.nip17.collection.is_empty() || lists.nip65.collection.is_empty();
179+
let (empty, expired) = match kind {
180+
GossipKind::Nip17 => {
181+
// Check if the collection is empty
182+
let empty: bool = lists.nip17.collection.is_empty();
167183

168-
// Check if expired
169-
let expired: bool = lists.nip17.last_update + PUBKEY_METADATA_OUTDATED_AFTER
170-
< now
171-
|| lists.nip65.last_update + PUBKEY_METADATA_OUTDATED_AFTER < now;
184+
// Check if expired
185+
let expired: bool =
186+
lists.nip17.last_update + PUBKEY_METADATA_OUTDATED_AFTER < now;
187+
188+
(empty, expired)
189+
}
190+
GossipKind::Nip65 => {
191+
// Check if the collection is empty
192+
let empty: bool = lists.nip65.collection.is_empty();
193+
194+
// Check if expired
195+
let expired: bool =
196+
lists.nip65.last_update + PUBKEY_METADATA_OUTDATED_AFTER < now;
197+
198+
(empty, expired)
199+
}
200+
};
172201

173202
if empty || expired {
174203
outdated.insert(public_key);
@@ -372,7 +401,11 @@ impl Gossip {
372401
self.map_nip65_relays(txn, public_keys, RelayMetadata::Read)
373402
}
374403

375-
pub async fn break_down_filter(&self, filter: Filter) -> BrokenDownFilters {
404+
pub async fn break_down_filter(
405+
&self,
406+
filter: Filter,
407+
pattern: GossipFilterPattern,
408+
) -> BrokenDownFilters {
376409
let txn = self.public_keys.read().await;
377410

378411
// Extract `p` tag from generic tags and parse public key hex
@@ -390,7 +423,9 @@ impl Gossip {
390423
self.map_nip65_outbox_relays(&txn, authors);
391424

392425
// Extend with NIP17 relays
393-
outbox.extend(self.map_nip17_relays(&txn, authors));
426+
if pattern.has_nip17() {
427+
outbox.extend(self.map_nip17_relays(&txn, authors));
428+
}
394429

395430
// No relay available for the authors
396431
if outbox.is_empty() {
@@ -417,7 +452,9 @@ impl Gossip {
417452
self.map_nip65_inbox_relays(&txn, p_public_keys);
418453

419454
// Extend with NIP17 relays
420-
inbox.extend(self.map_nip17_relays(&txn, p_public_keys));
455+
if pattern.has_nip17() {
456+
inbox.extend(self.map_nip17_relays(&txn, p_public_keys));
457+
}
421458

422459
// No relay available for the p tags
423460
if inbox.is_empty() {
@@ -446,7 +483,9 @@ impl Gossip {
446483
self.get_nip65_relays(&txn, authors.union(p_public_keys), None);
447484

448485
// Extend with NIP17 relays
449-
relays.extend(self.get_nip17_relays(&txn, authors.union(p_public_keys)));
486+
if pattern.has_nip17() {
487+
relays.extend(self.get_nip17_relays(&txn, authors.union(p_public_keys)));
488+
}
450489

451490
// No relay available for the authors and p tags
452491
if relays.is_empty() {
@@ -561,6 +600,38 @@ fn extract_nip65_relay_list(
561600
map
562601
}
563602

603+
pub(crate) enum GossipFilterPattern {
604+
Nip65,
605+
Nip65AndNip17,
606+
}
607+
608+
impl GossipFilterPattern {
609+
#[inline]
610+
fn has_nip17(&self) -> bool {
611+
matches!(self, Self::Nip65AndNip17)
612+
}
613+
}
614+
615+
/// Use both NIP-65 and NIP-17 if:
616+
/// - the `kinds` field contains the [`Kind::GiftWrap`];
617+
/// - if it's set a `#p` tag and no kind is specified
618+
pub(crate) fn find_filter_pattern(filter: &Filter) -> GossipFilterPattern {
619+
let (are_kinds_empty, has_gift_wrap_kind): (bool, bool) = match &filter.kinds {
620+
Some(kinds) if kinds.is_empty() => (true, false),
621+
Some(kinds) => (false, kinds.contains(&Kind::GiftWrap)),
622+
None => (true, false),
623+
};
624+
let has_p_tags: bool = filter.generic_tags.contains_key(&P_TAG);
625+
626+
// TODO: use both also if there are only IDs?
627+
628+
if has_gift_wrap_kind || (has_p_tags && are_kinds_empty) {
629+
return GossipFilterPattern::Nip65AndNip17;
630+
}
631+
632+
GossipFilterPattern::Nip65
633+
}
634+
564635
#[cfg(test)]
565636
mod tests {
566637
use super::*;
@@ -736,7 +807,10 @@ mod tests {
736807

737808
// Single author
738809
let filter = Filter::new().author(keys_a.public_key);
739-
match graph.break_down_filter(filter.clone()).await {
810+
match graph
811+
.break_down_filter(filter.clone(), GossipFilterPattern::Nip65)
812+
.await
813+
{
740814
BrokenDownFilters::Filters(map) => {
741815
assert_eq!(map.get(&damus_url).unwrap(), &filter);
742816
assert_eq!(map.get(&nostr_bg_url).unwrap(), &filter);
@@ -748,7 +822,10 @@ mod tests {
748822

749823
// Multiple authors
750824
let authors_filter = Filter::new().authors([keys_a.public_key, keys_b.public_key]);
751-
match graph.break_down_filter(authors_filter.clone()).await {
825+
match graph
826+
.break_down_filter(authors_filter.clone(), GossipFilterPattern::Nip65)
827+
.await
828+
{
752829
BrokenDownFilters::Filters(map) => {
753830
assert_eq!(map.get(&damus_url).unwrap(), &authors_filter);
754831
assert_eq!(
@@ -775,7 +852,10 @@ mod tests {
775852

776853
// Other filter
777854
let search_filter = Filter::new().search("Test").limit(10);
778-
match graph.break_down_filter(search_filter.clone()).await {
855+
match graph
856+
.break_down_filter(search_filter.clone(), GossipFilterPattern::Nip65)
857+
.await
858+
{
779859
BrokenDownFilters::Other(filter) => {
780860
assert_eq!(filter, search_filter);
781861
}
@@ -784,7 +864,10 @@ mod tests {
784864

785865
// Single p tags
786866
let p_tag_filter = Filter::new().pubkey(keys_a.public_key);
787-
match graph.break_down_filter(p_tag_filter.clone()).await {
867+
match graph
868+
.break_down_filter(p_tag_filter.clone(), GossipFilterPattern::Nip65)
869+
.await
870+
{
788871
BrokenDownFilters::Filters(map) => {
789872
assert_eq!(map.get(&damus_url).unwrap(), &p_tag_filter);
790873
assert_eq!(map.get(&nostr_bg_url).unwrap(), &p_tag_filter);
@@ -801,7 +884,10 @@ mod tests {
801884
let filter = Filter::new()
802885
.author(keys_a.public_key)
803886
.pubkey(keys_b.public_key);
804-
match graph.break_down_filter(filter.clone()).await {
887+
match graph
888+
.break_down_filter(filter.clone(), GossipFilterPattern::Nip65)
889+
.await
890+
{
805891
BrokenDownFilters::Filters(map) => {
806892
assert_eq!(map.get(&damus_url).unwrap(), &filter);
807893
assert_eq!(map.get(&nostr_bg_url).unwrap(), &filter);
@@ -817,7 +903,10 @@ mod tests {
817903
// test orphan filters
818904
let random_keys = Keys::generate();
819905
let filter = Filter::new().author(random_keys.public_key);
820-
match graph.break_down_filter(filter.clone()).await {
906+
match graph
907+
.break_down_filter(filter.clone(), GossipFilterPattern::Nip65)
908+
.await
909+
{
821910
BrokenDownFilters::Orphan(f) => {
822911
assert_eq!(f, filter);
823912
}

0 commit comments

Comments
 (0)