1#![doc = include_str!("../README.md")]
2
3use crate::{ResolverEntry, ResolverError, RootResolver};
4use async_trait::async_trait;
5use hashtree_core::{from_hex, to_hex, Cid};
6use nostr_sdk::prelude::nip44;
7use nostr_sdk::prelude::*;
8use serde_json::Value;
9use std::collections::HashMap;
10use std::future::Future;
11use std::sync::Arc;
12use std::time::Duration;
13use tokio::sync::{mpsc, RwLock};
14use tokio::task::JoinSet;
15
16mod open;
17
18use hashtree_core::{decrypt, mask_object_key, xor_key_bytes, ObjectKey};
19
20pub const HASHTREE_KIND: u16 = 30064;
21pub const HASHTREE_LEGACY_KIND: u16 = 30078;
22const HASHTREE_LABEL: &str = "hashtree";
23const DEFAULT_SUCCESSFUL_RELAY_QUORUM: usize = 2;
24const DEFAULT_SOFT_RESOLVE_TIMEOUT: Duration = Duration::from_secs(3);
25
26#[derive(Clone)]
28pub struct NostrResolverConfig {
29 pub relays: Vec<String>,
31 pub resolve_timeout: Duration,
33 pub secret_key: Option<Keys>,
35}
36
37impl Default for NostrResolverConfig {
38 fn default() -> Self {
39 Self {
40 relays: vec![
41 "wss://relay.damus.io".into(),
42 "wss://relay.primal.net".into(),
43 "wss://relay.snort.social".into(),
44 ],
45 resolve_timeout: Duration::from_millis(500),
46 secret_key: None,
47 }
48 }
49}
50
51const TAG_HASH: &str = "hash";
53const TAG_KEY: &str = "key";
54const TAG_ENCRYPTED_KEY: &str = "encryptedKey";
55const TAG_SELF_ENCRYPTED_KEY: &str = "selfEncryptedKey";
56const TAG_ENCRYPTED_KEY_LEGACY: &str = "encrypted_key";
57
58fn has_label(event: &Event, label: &str) -> bool {
59 event.tags.iter().any(|tag| {
60 let tag_vec = tag.as_slice();
61 tag_vec.len() >= 2 && tag_vec[0].as_str() == "l" && tag_vec[1].as_str() == label
62 })
63}
64
65fn has_any_label(event: &Event) -> bool {
66 event.tags.iter().any(|tag| {
67 let tag_vec = tag.as_slice();
68 !tag_vec.is_empty() && tag_vec[0].as_str() == "l"
69 })
70}
71
72fn is_hashtree_event(event: &Event) -> bool {
73 has_label(event, HASHTREE_LABEL) || !has_any_label(event)
74}
75
76fn hashtree_kinds() -> Vec<Kind> {
77 vec![
78 Kind::Custom(HASHTREE_KIND),
79 Kind::Custom(HASHTREE_LEGACY_KIND),
80 ]
81}
82
83fn is_hashtree_kind(kind: Kind) -> bool {
84 kind == Kind::Custom(HASHTREE_KIND) || kind == Kind::Custom(HASHTREE_LEGACY_KIND)
85}
86
87fn event_identifier(event: &Event) -> Option<String> {
88 event.tags.iter().find_map(|tag| {
89 if let Some(TagStandard::Identifier(id)) = tag.as_standardized() {
90 Some(id.clone())
91 } else {
92 None
93 }
94 })
95}
96
97#[cfg(test)]
98fn pick_latest_event<'a, I>(events: I) -> Option<&'a Event>
99where
100 I: IntoIterator<Item = &'a Event>,
101{
102 events.into_iter().max_by(|left, right| {
104 left.created_at
105 .cmp(&right.created_at)
106 .then_with(|| right.id.cmp(&left.id))
107 })
108}
109
110fn next_replaceable_created_at(now: Timestamp, latest_existing: Option<Timestamp>) -> Timestamp {
111 match latest_existing {
112 Some(latest) if latest >= now => Timestamp::from_secs(latest.as_secs().saturating_add(1)),
113 _ => now,
114 }
115}
116
117fn is_newer_event(
118 event: &Event,
119 current_created_at: Timestamp,
120 current_event_id: Option<EventId>,
121) -> bool {
122 if event.created_at > current_created_at {
123 return true;
124 }
125 if event.created_at < current_created_at {
126 return false;
127 }
128 match current_event_id {
129 Some(current_id) => event.id < current_id,
130 None => true,
131 }
132}
133
134fn publish_succeeded(success_count: usize) -> bool {
135 success_count > 0
136}
137
138#[derive(Debug, Clone)]
140pub struct VerifiedEvent(Event);
141
142impl VerifiedEvent {
143 pub fn as_event(&self) -> &Event {
144 &self.0
145 }
146
147 pub fn into_event(self) -> Event {
148 self.0
149 }
150
151 fn id(&self) -> EventId {
152 self.0.id
153 }
154
155 fn created_at(&self) -> Timestamp {
156 self.0.created_at
157 }
158}
159
160impl TryFrom<Event> for VerifiedEvent {
161 type Error = String;
162
163 fn try_from(event: Event) -> Result<Self, Self::Error> {
164 event
165 .verify()
166 .map_err(|err| format!("Nostr event verification failed: {err}"))?;
167 Ok(Self(event))
168 }
169}
170
171impl AsRef<Event> for VerifiedEvent {
172 fn as_ref(&self) -> &Event {
173 self.as_event()
174 }
175}
176
177fn verify_events<I>(events: I) -> Vec<VerifiedEvent>
178where
179 I: IntoIterator<Item = Event>,
180{
181 events
182 .into_iter()
183 .filter_map(|event| VerifiedEvent::try_from(event).ok())
184 .collect()
185}
186
187fn pick_latest_verified_event<'a, I>(events: I) -> Option<&'a VerifiedEvent>
188where
189 I: IntoIterator<Item = &'a VerifiedEvent>,
190{
191 events.into_iter().max_by(|left, right| {
193 left.created_at()
194 .cmp(&right.created_at())
195 .then_with(|| right.id().cmp(&left.id()))
196 })
197}
198
199fn is_matching_tree_event(event: &VerifiedEvent, tree_name: &str) -> bool {
200 let event = event.as_event();
201 is_hashtree_kind(event.kind)
202 && event_identifier(event).as_deref() == Some(tree_name)
203 && is_hashtree_event(event)
204}
205
206async fn await_publish_result<F, T, E>(future: F) -> Result<T, ResolverError>
207where
208 F: Future<Output = Result<T, E>> + Send + 'static,
209 T: Send + 'static,
210 E: ToString + Send + 'static,
211{
212 let handle = tokio::spawn(future);
213 match handle.await {
214 Ok(Ok(value)) => Ok(value),
215 Ok(Err(error)) => Err(ResolverError::Network(error.to_string())),
216 Err(error) if error.is_panic() => {
217 Err(ResolverError::Other("Nostr publish task panicked".into()))
218 }
219 Err(error) => Err(ResolverError::Other(format!(
220 "Nostr publish task failed: {}",
221 error
222 ))),
223 }
224}
225
226fn upsert_latest_by_d_tag<'a>(
227 entries_by_d_tag: &mut HashMap<String, &'a VerifiedEvent>,
228 event: &'a VerifiedEvent,
229) {
230 let Some(d_tag) = event_identifier(event.as_event()) else {
231 return;
232 };
233
234 let should_replace = match entries_by_d_tag.get(&d_tag) {
235 Some(existing) => {
236 is_newer_event(event.as_event(), existing.created_at(), Some(existing.id()))
237 }
238 None => true,
239 };
240
241 if should_replace {
242 entries_by_d_tag.insert(d_tag, event);
243 }
244}
245
246fn parse_legacy_content(content: &str) -> Option<(String, Option<String>)> {
247 let trimmed = content.trim();
248 if trimmed.is_empty() {
249 return None;
250 }
251
252 if let Ok(value) = serde_json::from_str::<Value>(trimmed) {
253 if let Some(hash) = value.get("hash").and_then(|v| v.as_str()) {
254 let key = value
255 .get("key")
256 .and_then(|v| v.as_str())
257 .map(|s| s.to_string());
258 return Some((hash.to_string(), key));
259 }
260 }
261
262 if trimmed.len() == 64 && trimmed.chars().all(|c| c.is_ascii_hexdigit()) {
263 return Some((trimmed.to_string(), None));
264 }
265
266 None
267}
268
269struct Subscription {
271 tx: mpsc::Sender<Option<Cid>>,
272 current_cid: Option<Cid>,
273 latest_created_at: Timestamp,
274 latest_event_id: Option<EventId>,
275}
276
277pub struct NostrRootResolver {
279 client: Client,
280 config: NostrResolverConfig,
281 ingested_events: Arc<RwLock<HashMap<String, VerifiedEvent>>>,
282 subscriptions: Arc<RwLock<HashMap<String, Subscription>>>,
283}
284
285impl NostrRootResolver {
286 pub async fn new(config: NostrResolverConfig) -> Result<Self, ResolverError> {
288 let keys = config.secret_key.clone().unwrap_or_else(Keys::generate);
289 let client = Client::new(keys);
290
291 for relay in &config.relays {
293 client
294 .add_relay(relay)
295 .await
296 .map_err(|e| ResolverError::Network(e.to_string()))?;
297 }
298
299 client.connect().await;
301
302 Ok(Self {
303 client,
304 config,
305 ingested_events: Arc::new(RwLock::new(HashMap::new())),
306 subscriptions: Arc::new(RwLock::new(HashMap::new())),
307 })
308 }
309
310 fn parse_key(key: &str) -> Result<(PublicKey, String), ResolverError> {
313 let parts: Vec<&str> = key.splitn(2, '/').collect();
315 if parts.len() != 2 || parts[1].is_empty() {
316 return Err(ResolverError::InvalidKey(format!(
317 "Key must be in format 'npub.../treename', got: {}",
318 key
319 )));
320 }
321
322 let npub_str = parts[0];
323 let tree_name = parts[1].to_string();
324
325 let pubkey = PublicKey::from_bech32(npub_str)
326 .map_err(|_| ResolverError::InvalidKey(format!("Invalid npub: {}", npub_str)))?;
327
328 Ok((pubkey, tree_name))
329 }
330
331 pub fn pubkey(&self) -> Option<PublicKey> {
333 self.config.secret_key.as_ref().map(|k| k.public_key())
334 }
335
336 fn build_tree_filter(pubkey: PublicKey, tree_name: &str) -> Filter {
337 Filter::new()
338 .kinds(hashtree_kinds())
339 .author(pubkey)
340 .custom_tag(
341 SingleLetterTag::lowercase(Alphabet::D),
342 tree_name.to_string(),
343 )
344 }
345
346 pub fn filter_for_key(key: &str) -> Result<Filter, ResolverError> {
350 let (pubkey, tree_name) = Self::parse_key(key)?;
351 Ok(Self::build_tree_filter(pubkey, &tree_name))
352 }
353
354 pub fn root_event_builder(
357 tree_name: &str,
358 cid: &Cid,
359 latest_created_at: Option<Timestamp>,
360 ) -> EventBuilder {
361 let mut tags = vec![
362 Tag::identifier(tree_name),
363 Tag::custom(
364 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::L)),
365 vec![HASHTREE_LABEL],
366 ),
367 Tag::custom(TagKind::Custom(TAG_HASH.into()), vec![to_hex(&cid.hash)]),
368 ];
369 if let Some(key) = cid.key {
370 tags.push(Tag::custom(
371 TagKind::Custom(TAG_KEY.into()),
372 vec![hex::encode(key)],
373 ));
374 }
375 EventBuilder::new(Kind::Custom(HASHTREE_KIND), "")
376 .tags(tags)
377 .custom_created_at(next_replaceable_created_at(
378 Timestamp::now(),
379 latest_created_at,
380 ))
381 }
382
383 pub fn event_matches_key(key: &str, event: &Event) -> Result<bool, ResolverError> {
389 Ok(Self::root_from_event(key, event)?.is_some())
390 }
391
392 pub fn root_from_event(key: &str, event: &Event) -> Result<Option<Cid>, ResolverError> {
394 let (pubkey, tree_name) = Self::parse_key(key)?;
395 let event = VerifiedEvent::try_from(event.clone()).map_err(ResolverError::Other)?;
396 if event.as_event().pubkey != pubkey || !is_matching_tree_event(&event, &tree_name) {
397 return Ok(None);
398 }
399 Ok(Self::cid_from_event_with_keys(event.as_event(), None))
400 }
401
402 pub async fn ingest_event(&self, event: Event) -> Result<bool, ResolverError> {
405 let event = VerifiedEvent::try_from(event).map_err(ResolverError::Other)?;
406 let tree_name = event_identifier(event.as_event())
407 .ok_or_else(|| ResolverError::Other("Nostr root event has no d-tag".to_string()))?;
408 if !is_matching_tree_event(&event, &tree_name) {
409 return Err(ResolverError::Other(
410 "event is not a supported hashtree root event".to_string(),
411 ));
412 }
413 let npub = event
414 .as_event()
415 .pubkey
416 .to_bech32()
417 .map_err(|error| ResolverError::Other(error.to_string()))?;
418 let key = format!("{npub}/{tree_name}");
419 let cid = self.cid_from_event(event.as_event()).ok_or_else(|| {
420 ResolverError::Other("Nostr root event has no usable CID".to_string())
421 })?;
422
423 {
424 let mut events = self.ingested_events.write().await;
425 if events.get(&key).is_some_and(|current| {
426 !is_newer_event(event.as_event(), current.created_at(), Some(current.id()))
427 }) {
428 return Ok(false);
429 }
430 events.insert(key.clone(), event.clone());
431 }
432
433 let mut subscriptions = self.subscriptions.write().await;
434 if let Some(subscription) = subscriptions.get_mut(&key) {
435 if is_newer_event(
436 event.as_event(),
437 subscription.latest_created_at,
438 subscription.latest_event_id,
439 ) {
440 subscription.latest_created_at = event.created_at();
441 subscription.latest_event_id = Some(event.id());
442 if subscription.current_cid.as_ref() != Some(&cid) {
443 subscription.current_cid = Some(cid.clone());
444 let _ = subscription.tx.send(Some(cid)).await;
445 }
446 }
447 }
448 Ok(true)
449 }
450
451 async fn ingested_event(&self, key: &str) -> Option<VerifiedEvent> {
452 self.ingested_events.read().await.get(key).cloned()
453 }
454
455 async fn verified_events_for_key(
456 &self,
457 key: &str,
458 pubkey: PublicKey,
459 tree_name: &str,
460 ) -> Result<Vec<VerifiedEvent>, ResolverError> {
461 let ingested = self.ingested_event(key).await;
462 let mut events = match self
463 .fetch_verified_events_from_relays(Self::build_tree_filter(pubkey, tree_name))
464 .await
465 {
466 Ok(events) => events,
467 Err(_) if ingested.is_some() => Vec::new(),
468 Err(error) => return Err(error),
469 };
470 if let Some(event) = ingested {
471 if !events.iter().any(|candidate| candidate.id() == event.id()) {
472 events.push(event);
473 }
474 }
475 Ok(events)
476 }
477
478 fn cid_from_event(&self, event: &Event) -> Option<Cid> {
480 Self::cid_from_event_with_keys(event, self.config.secret_key.as_ref())
481 }
482
483 async fn fetch_verified_events_from_relays(
484 &self,
485 filter: Filter,
486 ) -> Result<Vec<VerifiedEvent>, ResolverError> {
487 if self.config.relays.is_empty() {
488 return Ok(Vec::new());
489 }
490
491 let relay_count = self.config.relays.len();
492 let successful_relay_quorum = relay_count.clamp(1, DEFAULT_SUCCESSFUL_RELAY_QUORUM);
493 let soft_timeout = self
494 .config
495 .resolve_timeout
496 .min(DEFAULT_SOFT_RESOLVE_TIMEOUT);
497 let mut join_set = JoinSet::new();
498 for relay in self.config.relays.iter().cloned() {
499 let client = self.client.clone();
500 let filter = filter.clone();
501 let timeout = self.config.resolve_timeout;
502 join_set.spawn(async move {
503 let result = client
504 .fetch_events_from(vec![relay.clone()], filter, timeout)
505 .await
506 .map(|events| events.to_vec());
507 (relay, result)
508 });
509 }
510
511 let mut events_by_id: HashMap<EventId, VerifiedEvent> = HashMap::new();
512 let mut successful_relays = 0usize;
513 let mut errors = Vec::new();
514 let mut soft_timeout_elapsed = false;
515 let soft_timeout_sleep = tokio::time::sleep(soft_timeout);
516 tokio::pin!(soft_timeout_sleep);
517
518 while !join_set.is_empty() {
519 tokio::select! {
520 joined = join_set.join_next() => {
521 let Some(joined) = joined else {
522 break;
523 };
524 match joined {
525 Ok((relay, Ok(events))) => {
526 successful_relays += 1;
527 for event in verify_events(events) {
528 events_by_id.entry(event.id()).or_insert(event);
529 }
530 let _ = relay;
531 }
532 Ok((relay, Err(err))) => {
533 errors.push(format!("{relay}: {err}"));
534 }
535 Err(err) => {
536 errors.push(format!("join error: {err}"));
537 }
538 }
539
540 if successful_relays >= successful_relay_quorum && !events_by_id.is_empty() {
541 return Ok(events_by_id.values().cloned().collect());
542 }
543 if soft_timeout_elapsed && successful_relays > 0 {
544 return Ok(events_by_id.values().cloned().collect());
545 }
546 }
547 _ = &mut soft_timeout_sleep, if !soft_timeout_elapsed => {
548 soft_timeout_elapsed = true;
549 if successful_relays > 0 {
550 return Ok(events_by_id.values().cloned().collect());
551 }
552 }
553 }
554 }
555
556 if successful_relays == 0 {
557 let detail = if errors.is_empty() {
558 "no relays succeeded".to_string()
559 } else {
560 errors.join("; ")
561 };
562 return Err(ResolverError::Network(format!(
563 "Failed to get events from configured relays: {detail}"
564 )));
565 }
566
567 Ok(events_by_id.into_values().collect())
568 }
569
570 async fn latest_existing_created_at(
571 &self,
572 pubkey: PublicKey,
573 tree_name: &str,
574 ) -> Result<Option<Timestamp>, ResolverError> {
575 let events = self
576 .fetch_verified_events_from_relays(Self::build_tree_filter(pubkey, tree_name))
577 .await?;
578 Ok(pick_latest_verified_event(
579 events
580 .iter()
581 .filter(|event| is_matching_tree_event(event, tree_name)),
582 )
583 .map(|event| event.created_at()))
584 }
585
586 fn cid_from_event_with_keys(event: &Event, keys: Option<&Keys>) -> Option<Cid> {
587 let mut hash_hex: Option<String> = None;
588 let mut key_hex: Option<String> = None;
589 let mut self_encrypted_key: Option<String> = None;
590
591 for tag in event.tags.iter() {
592 let tag_vec = tag.as_slice();
593 if tag_vec.len() >= 2 {
594 match tag_vec[0].as_str() {
595 "hash" => hash_hex = Some(tag_vec[1].clone()),
596 "key" => key_hex = Some(tag_vec[1].clone()),
597 TAG_SELF_ENCRYPTED_KEY => self_encrypted_key = Some(tag_vec[1].clone()),
598 _ => {}
599 }
600 }
601 }
602
603 if hash_hex.is_none() {
604 if let Some((legacy_hash, legacy_key)) = parse_legacy_content(&event.content) {
605 hash_hex = Some(legacy_hash);
606 if key_hex.is_none() {
607 key_hex = legacy_key;
608 }
609 }
610 }
611
612 let hash = from_hex(&hash_hex?).ok()?;
614
615 let key_hex = match (key_hex, self_encrypted_key, keys) {
617 (Some(key), _, _) => Some(key),
618 (None, Some(ciphertext), Some(keys)) => {
619 Some(nip44::decrypt(keys.secret_key(), &keys.public_key(), &ciphertext).ok()?)
620 }
621 _ => None,
622 };
623 let key = key_hex
624 .map(|value| ObjectKey::from_hex(&value))
625 .transpose()
626 .ok()?;
627
628 Some(Cid { hash, key })
629 }
630
631 fn cid_from_event_shared(event: &Event, share_secret: &[u8]) -> Option<Cid> {
633 let mut hash_hex: Option<String> = None;
634 let mut key_hex: Option<String> = None;
635 let mut encrypted_key_hex: Option<String> = None;
636 let mut encrypted_key_legacy_hex: Option<String> = None;
637
638 for tag in event.tags.iter() {
639 let tag_vec = tag.as_slice();
640 if tag_vec.len() >= 2 {
641 match tag_vec[0].as_str() {
642 "hash" => hash_hex = Some(tag_vec[1].clone()),
643 "key" => key_hex = Some(tag_vec[1].clone()),
644 TAG_ENCRYPTED_KEY => encrypted_key_hex = Some(tag_vec[1].clone()),
645 TAG_ENCRYPTED_KEY_LEGACY => encrypted_key_legacy_hex = Some(tag_vec[1].clone()),
646 _ => {}
647 }
648 }
649 }
650
651 if hash_hex.is_none() {
652 if let Some((legacy_hash, _legacy_key)) = parse_legacy_content(&event.content) {
653 hash_hex = Some(legacy_hash);
654 }
655 }
656
657 let hash = from_hex(&hash_hex?).ok()?;
658
659 let key = if let Some(hex) = key_hex {
660 Some(ObjectKey::from_hex(&hex).ok()?)
661 } else if let Some(hex) = encrypted_key_hex {
662 let masked = hex::decode(hex).ok()?;
663 Some(ObjectKey::from_bytes(&xor_key_bytes(&masked, share_secret).ok()?).ok()?)
664 } else if let Some(hex) = encrypted_key_legacy_hex {
665 let bytes = decrypt(&hex::decode(hex).ok()?, share_secret.try_into().ok()?).ok()?;
666 Some(ObjectKey::from_bytes(&bytes).ok()?)
667 } else {
668 None
669 };
670
671 Some(Cid { hash, key })
672 }
673
674 pub async fn resolve_wait(&self, key: &str) -> Result<Cid, ResolverError> {
682 if let Some(cid) = self.resolve(key).await? {
684 return Ok(cid);
685 }
686
687 let mut rx = self.subscribe(key).await?;
689
690 while let Some(maybe_cid) = rx.recv().await {
692 if let Some(cid) = maybe_cid {
693 return Ok(cid);
694 }
695 }
696
697 Err(ResolverError::Stopped)
698 }
699
700 pub async fn publish_private(&self, key: &str, cid: &Cid) -> Result<bool, ResolverError> {
702 let (pubkey, tree_name) = Self::parse_key(key)?;
703
704 let keys = self
705 .config
706 .secret_key
707 .as_ref()
708 .ok_or(ResolverError::NotAuthorized)?;
709 if pubkey != keys.public_key() {
710 return Err(ResolverError::NotAuthorized);
711 }
712
713 let key_bytes = cid
714 .key
715 .ok_or_else(|| ResolverError::Other("Missing CHK key for private publish".into()))?;
716 let key_hex = hex::encode(key_bytes);
717
718 let encrypted = nip44::encrypt(
719 keys.secret_key(),
720 &keys.public_key(),
721 key_hex,
722 nip44::Version::V2,
723 )
724 .map_err(|e| ResolverError::Other(format!("NIP-44 encryption failed: {}", e)))?;
725
726 let tags = vec![
727 Tag::identifier(tree_name.clone()),
728 Tag::custom(
729 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::L)),
730 vec![HASHTREE_LABEL],
731 ),
732 Tag::custom(TagKind::Custom(TAG_HASH.into()), vec![to_hex(&cid.hash)]),
733 Tag::custom(
734 TagKind::Custom(TAG_SELF_ENCRYPTED_KEY.into()),
735 vec![encrypted],
736 ),
737 ];
738
739 let created_at = next_replaceable_created_at(
740 Timestamp::now(),
741 self.latest_existing_created_at(pubkey, &tree_name)
742 .await
743 .ok()
744 .flatten(),
745 );
746 let event = self
747 .client
748 .sign_event_builder(
749 EventBuilder::new(Kind::Custom(HASHTREE_KIND), "")
750 .tags(tags)
751 .custom_created_at(created_at),
752 )
753 .await
754 .map_err(|e| ResolverError::Network(e.to_string()))?;
755
756 let client = self.client.clone();
757 let event_for_send = event.clone();
758 let output =
759 await_publish_result(async move { client.send_event(&event_for_send).await }).await?;
760
761 {
762 let mut subs = self.subscriptions.write().await;
763 if let Some(sub) = subs.get_mut(key) {
764 sub.current_cid = Some(cid.clone());
765 sub.latest_created_at = event.created_at;
766 sub.latest_event_id = Some(event.id);
767 let _ = sub.tx.send(Some(cid.clone())).await;
768 }
769 }
770
771 Ok(publish_succeeded(output.success.len()))
772 }
773}
774
775#[async_trait]
776impl RootResolver for NostrRootResolver {
777 async fn resolve(&self, key: &str) -> Result<Option<Cid>, ResolverError> {
778 let (pubkey, tree_name) = Self::parse_key(key)?;
779 let events = self
780 .verified_events_for_key(key, pubkey, &tree_name)
781 .await?;
782
783 let latest_event = pick_latest_verified_event(
784 events
785 .iter()
786 .filter(|event| is_matching_tree_event(event, &tree_name)),
787 );
788
789 match latest_event {
791 Some(event) => Ok(self.cid_from_event(event.as_event())),
792 None => Ok(None),
793 }
794 }
795
796 async fn resolve_shared(
797 &self,
798 key: &str,
799 share_secret: &[u8],
800 ) -> Result<Option<Cid>, ResolverError> {
801 let (pubkey, tree_name) = Self::parse_key(key)?;
802 let events = self
803 .verified_events_for_key(key, pubkey, &tree_name)
804 .await?;
805
806 let latest_event = pick_latest_verified_event(
807 events
808 .iter()
809 .filter(|event| is_matching_tree_event(event, &tree_name)),
810 );
811
812 match latest_event {
813 Some(event) => Ok(Self::cid_from_event_shared(event.as_event(), share_secret)),
814 None => Ok(None),
815 }
816 }
817
818 async fn subscribe(&self, key: &str) -> Result<mpsc::Receiver<Option<Cid>>, ResolverError> {
819 let (pubkey, tree_name) = Self::parse_key(key)?;
820
821 let (tx, rx) = mpsc::channel(16);
822 let ingested = self.ingested_event(key).await;
823 let current_cid = ingested
824 .as_ref()
825 .and_then(|event| self.cid_from_event(event.as_event()));
826 let latest_created_at = ingested
827 .as_ref()
828 .map_or(Timestamp::from(0), VerifiedEvent::created_at);
829 let latest_event_id = ingested.as_ref().map(VerifiedEvent::id);
830 let _ = tx.send(current_cid.clone()).await;
831
832 let filter = Self::build_tree_filter(pubkey, &tree_name);
834
835 {
837 let mut subs = self.subscriptions.write().await;
838 subs.insert(
839 key.to_string(),
840 Subscription {
841 tx: tx.clone(),
842 current_cid,
843 latest_created_at,
844 latest_event_id,
845 },
846 );
847 }
848
849 let subscriptions = self.subscriptions.clone();
851 let key_clone = key.to_string();
852 let tree_name_clone = tree_name.clone();
853 let secret_key = self.config.secret_key.clone();
854
855 let client = self.client.clone();
857 tokio::spawn(async move {
858 let sub_id = client.subscribe(filter, None).await;
859
860 if sub_id.is_err() {
861 return;
862 }
863
864 let mut notifications = client.notifications();
866
867 while let Ok(notification) = notifications.recv().await {
868 if let RelayPoolNotification::Event { event, .. } = notification {
869 let Ok(event) = VerifiedEvent::try_from(*event) else {
870 continue;
871 };
872
873 if !is_matching_tree_event(&event, &tree_name_clone) {
874 continue;
875 }
876
877 let mut subs = subscriptions.write().await;
878 if let Some(sub) = subs.get_mut(&key_clone) {
879 let new_cid = NostrRootResolver::cid_from_event_with_keys(
880 event.as_event(),
881 secret_key.as_ref(),
882 );
883 if is_newer_event(
884 event.as_event(),
885 sub.latest_created_at,
886 sub.latest_event_id,
887 ) {
888 sub.latest_created_at = event.created_at();
889 sub.latest_event_id = Some(event.id());
890
891 if new_cid != sub.current_cid {
892 sub.current_cid = new_cid.clone();
893 if sub.tx.send(new_cid).await.is_err() {
894 subs.remove(&key_clone);
896 break;
897 }
898 }
899 }
900 } else {
901 break;
902 }
903 }
904 }
905 });
906
907 Ok(rx)
908 }
909
910 async fn publish(&self, key: &str, cid: &Cid) -> Result<bool, ResolverError> {
911 let (pubkey, tree_name) = Self::parse_key(key)?;
912
913 let my_pubkey = self.pubkey().ok_or(ResolverError::NotAuthorized)?;
915 if pubkey != my_pubkey {
916 return Err(ResolverError::NotAuthorized);
917 }
918
919 let latest_created_at = self
920 .latest_existing_created_at(pubkey, &tree_name)
921 .await
922 .ok()
923 .flatten();
924 let event = self
925 .client
926 .sign_event_builder(Self::root_event_builder(&tree_name, cid, latest_created_at))
927 .await
928 .map_err(|e| ResolverError::Network(e.to_string()))?;
929
930 let client = self.client.clone();
931 let event_for_send = event.clone();
932 let output =
933 await_publish_result(async move { client.send_event(&event_for_send).await }).await?;
934
935 {
936 let mut subs = self.subscriptions.write().await;
937 if let Some(sub) = subs.get_mut(key) {
938 sub.current_cid = Some(cid.clone());
939 sub.latest_created_at = event.created_at;
940 sub.latest_event_id = Some(event.id);
941 let _ = sub.tx.send(Some(cid.clone())).await;
942 }
943 }
944
945 Ok(publish_succeeded(output.success.len()))
946 }
947
948 async fn publish_shared(
949 &self,
950 key: &str,
951 cid: &Cid,
952 share_secret: &[u8],
953 ) -> Result<bool, ResolverError> {
954 let (pubkey, tree_name) = Self::parse_key(key)?;
955
956 let my_pubkey = self.pubkey().ok_or(ResolverError::NotAuthorized)?;
957 if pubkey != my_pubkey {
958 return Err(ResolverError::NotAuthorized);
959 }
960
961 let mut tags = vec![
962 Tag::identifier(tree_name.clone()),
963 Tag::custom(
964 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::L)),
965 vec![HASHTREE_LABEL],
966 ),
967 Tag::custom(TagKind::Custom(TAG_HASH.into()), vec![to_hex(&cid.hash)]),
968 ];
969
970 if let Some(key) = cid.key {
972 let masked = mask_object_key(&key, share_secret)
973 .map_err(|e| ResolverError::InvalidKey(e.to_string()))?;
974 tags.push(Tag::custom(
975 TagKind::Custom(TAG_ENCRYPTED_KEY.into()),
976 vec![hex::encode(masked)],
977 ));
978 }
979
980 let created_at = next_replaceable_created_at(
981 Timestamp::now(),
982 self.latest_existing_created_at(pubkey, &tree_name)
983 .await
984 .ok()
985 .flatten(),
986 );
987 let event = self
988 .client
989 .sign_event_builder(
990 EventBuilder::new(Kind::Custom(HASHTREE_KIND), "")
991 .tags(tags)
992 .custom_created_at(created_at),
993 )
994 .await
995 .map_err(|e| ResolverError::Network(e.to_string()))?;
996
997 let client = self.client.clone();
998 let event_for_send = event.clone();
999 let output =
1000 await_publish_result(async move { client.send_event(&event_for_send).await }).await?;
1001
1002 {
1003 let mut subs = self.subscriptions.write().await;
1004 if let Some(sub) = subs.get_mut(key) {
1005 sub.current_cid = Some(cid.clone());
1006 sub.latest_created_at = event.created_at;
1007 sub.latest_event_id = Some(event.id);
1008 let _ = sub.tx.send(Some(cid.clone())).await;
1009 }
1010 }
1011
1012 Ok(publish_succeeded(output.success.len()))
1013 }
1014
1015 async fn list(&self, prefix: &str) -> Result<Vec<ResolverEntry>, ResolverError> {
1016 let parts: Vec<&str> = prefix.split('/').collect();
1017 if parts.is_empty() {
1018 return Ok(vec![]);
1019 }
1020
1021 let npub_str = parts[0];
1022 let pubkey = PublicKey::from_bech32(npub_str)
1023 .map_err(|_| ResolverError::InvalidKey(format!("Invalid npub: {}", npub_str)))?;
1024
1025 let filter = Filter::new()
1027 .kinds(hashtree_kinds())
1028 .author(pubkey)
1029 .custom_tag(SingleLetterTag::lowercase(Alphabet::L), HASHTREE_LABEL);
1030
1031 let events = self.fetch_verified_events_from_relays(filter).await?;
1032
1033 let mut entries_by_d_tag: HashMap<String, &VerifiedEvent> = HashMap::new();
1035
1036 for event in events.iter() {
1037 if !is_hashtree_event(event.as_event()) {
1038 continue;
1039 }
1040 upsert_latest_by_d_tag(&mut entries_by_d_tag, event);
1041 }
1042
1043 let mut result = Vec::new();
1045 for (d_tag, event) in entries_by_d_tag {
1046 if let Some(cid) = self.cid_from_event(event.as_event()) {
1047 result.push(ResolverEntry {
1048 key: format!("{}/{}", npub_str, d_tag),
1049 cid,
1050 });
1051 }
1052 }
1053
1054 Ok(result)
1055 }
1056
1057 async fn stop(&self) -> Result<(), ResolverError> {
1058 let _ = self.client.disconnect().await;
1059 Ok(())
1060 }
1061}
1062
1063#[cfg(test)]
1064mod tests {
1065 use super::*;
1066 use futures::{SinkExt, StreamExt};
1067 use std::collections::HashMap;
1068 use std::net::TcpListener;
1069 use std::sync::Mutex;
1070 use tokio::net::TcpStream;
1071 use tokio::sync::broadcast;
1072 use tokio_tungstenite::{accept_async, tungstenite::Message};
1073
1074 struct TestRelay {
1075 port: u16,
1076 events: Arc<Mutex<Vec<Value>>>,
1077 shutdown: broadcast::Sender<()>,
1078 }
1079
1080 impl TestRelay {
1081 fn with_events(events: Vec<Event>) -> Self {
1082 Self::with_events_and_delay(events, Duration::ZERO)
1083 }
1084
1085 fn with_events_and_delay(events: Vec<Event>, response_delay: Duration) -> Self {
1086 Self::with_event_values_and_delay(
1087 events
1088 .into_iter()
1089 .map(|event| serde_json::to_value(event).expect("event to value"))
1090 .collect(),
1091 response_delay,
1092 )
1093 }
1094
1095 fn with_event_values_and_delay(events: Vec<Value>, response_delay: Duration) -> Self {
1096 let stored_events = Arc::new(Mutex::new(events));
1097 let (shutdown, _) = broadcast::channel(1);
1098
1099 let std_listener = TcpListener::bind("127.0.0.1:0").expect("bind relay listener");
1100 let port = std_listener.local_addr().expect("relay local addr").port();
1101 std_listener
1102 .set_nonblocking(true)
1103 .expect("set relay listener nonblocking");
1104
1105 let relay_events = Arc::clone(&stored_events);
1106 let shutdown_tx = shutdown.clone();
1107
1108 std::thread::spawn(move || {
1109 let runtime = tokio::runtime::Builder::new_multi_thread()
1110 .worker_threads(2)
1111 .enable_all()
1112 .build()
1113 .expect("build relay runtime");
1114
1115 runtime.block_on(async move {
1116 let listener =
1117 tokio::net::TcpListener::from_std(std_listener).expect("tokio listener");
1118 let mut shutdown_rx = shutdown_tx.subscribe();
1119
1120 loop {
1121 tokio::select! {
1122 _ = shutdown_rx.recv() => break,
1123 accepted = listener.accept() => {
1124 if let Ok((stream, _)) = accepted {
1125 let relay_events = Arc::clone(&relay_events);
1126 tokio::spawn(async move {
1127 handle_test_relay_connection(
1128 stream,
1129 relay_events,
1130 response_delay,
1131 )
1132 .await;
1133 });
1134 }
1135 }
1136 }
1137 }
1138 });
1139 });
1140
1141 std::thread::sleep(Duration::from_millis(100));
1142
1143 Self {
1144 port,
1145 events: stored_events,
1146 shutdown,
1147 }
1148 }
1149
1150 fn url(&self) -> String {
1151 format!("ws://127.0.0.1:{}", self.port)
1152 }
1153
1154 fn latest_tree_created_at(&self, tree_name: &str) -> Option<u64> {
1155 self.events
1156 .lock()
1157 .expect("relay events")
1158 .iter()
1159 .filter(|event| {
1160 event.get("kind").and_then(Value::as_u64) == Some(HASHTREE_KIND as u64)
1161 && event_tag_matches(event, "d", &[tree_name.to_string()])
1162 })
1163 .max_by_key(|event| {
1164 (
1165 event
1166 .get("created_at")
1167 .and_then(Value::as_u64)
1168 .unwrap_or_default(),
1169 event
1170 .get("id")
1171 .and_then(Value::as_str)
1172 .unwrap_or_default()
1173 .to_string(),
1174 )
1175 })
1176 .and_then(|event| event.get("created_at").and_then(Value::as_u64))
1177 }
1178 }
1179
1180 impl Drop for TestRelay {
1181 fn drop(&mut self) {
1182 let _ = self.shutdown.send(());
1183 std::thread::sleep(Duration::from_millis(50));
1184 }
1185 }
1186
1187 fn event_tag_matches(event: &Value, name: &str, accepted: &[String]) -> bool {
1188 let Some(tags) = event.get("tags").and_then(Value::as_array) else {
1189 return false;
1190 };
1191
1192 tags.iter().any(|tag| {
1193 let Some(arr) = tag.as_array() else {
1194 return false;
1195 };
1196 if arr.len() < 2 {
1197 return false;
1198 }
1199 let Some(tag_name) = arr.first().and_then(Value::as_str) else {
1200 return false;
1201 };
1202 if tag_name != name {
1203 return false;
1204 }
1205 let Some(tag_value) = arr.get(1).and_then(Value::as_str) else {
1206 return false;
1207 };
1208 accepted.iter().any(|value| value == tag_value)
1209 })
1210 }
1211
1212 fn event_matches_filter(event: &Value, filter: &Value) -> bool {
1213 let Some(filter_obj) = filter.as_object() else {
1214 return true;
1215 };
1216
1217 if let Some(kinds) = filter_obj.get("kinds").and_then(Value::as_array) {
1218 let event_kind = event
1219 .get("kind")
1220 .and_then(Value::as_i64)
1221 .unwrap_or_default();
1222 if !kinds
1223 .iter()
1224 .any(|kind| kind.as_i64().is_some_and(|value| value == event_kind))
1225 {
1226 return false;
1227 }
1228 }
1229
1230 if let Some(authors) = filter_obj.get("authors").and_then(Value::as_array) {
1231 let event_author = event
1232 .get("pubkey")
1233 .and_then(Value::as_str)
1234 .unwrap_or_default();
1235 if !authors
1236 .iter()
1237 .filter_map(Value::as_str)
1238 .any(|author| author == event_author)
1239 {
1240 return false;
1241 }
1242 }
1243
1244 if let Some(d_values) = filter_obj.get("#d").and_then(Value::as_array) {
1245 let accepted: Vec<String> = d_values
1246 .iter()
1247 .filter_map(|value| value.as_str().map(ToOwned::to_owned))
1248 .collect();
1249 if !accepted.is_empty() && !event_tag_matches(event, "d", &accepted) {
1250 return false;
1251 }
1252 }
1253
1254 if let Some(l_values) = filter_obj.get("#l").and_then(Value::as_array) {
1255 let accepted: Vec<String> = l_values
1256 .iter()
1257 .filter_map(|value| value.as_str().map(ToOwned::to_owned))
1258 .collect();
1259 if !accepted.is_empty() && !event_tag_matches(event, "l", &accepted) {
1260 return false;
1261 }
1262 }
1263
1264 true
1265 }
1266
1267 #[tokio::test]
1268 async fn publish_task_panic_is_reported_as_error() {
1269 let error = await_publish_result(async move {
1270 panic!("boom");
1271 #[allow(unreachable_code)]
1272 Ok::<(), &'static str>(())
1273 })
1274 .await
1275 .expect_err("panic should be converted to resolver error");
1276
1277 match error {
1278 ResolverError::Other(message) => {
1279 assert!(message.contains("panicked"));
1280 }
1281 other => panic!("expected ResolverError::Other, got {other:?}"),
1282 }
1283 }
1284
1285 async fn handle_test_relay_connection(
1286 stream: TcpStream,
1287 events: Arc<Mutex<Vec<Value>>>,
1288 response_delay: Duration,
1289 ) {
1290 let ws_stream = match accept_async(stream).await {
1291 Ok(ws) => ws,
1292 Err(_) => return,
1293 };
1294 let (mut write, mut read) = ws_stream.split();
1295
1296 while let Some(message) = read.next().await {
1297 let text = match message {
1298 Ok(Message::Text(text)) => text,
1299 Ok(Message::Ping(data)) => {
1300 let _ = write.send(Message::Pong(data)).await;
1301 continue;
1302 }
1303 Ok(Message::Close(_)) => break,
1304 _ => continue,
1305 };
1306
1307 let parsed: Vec<Value> = match serde_json::from_str(&text) {
1308 Ok(value) => value,
1309 Err(_) => continue,
1310 };
1311
1312 let Some(message_type) = parsed.first().and_then(Value::as_str) else {
1313 continue;
1314 };
1315
1316 match message_type {
1317 "REQ" => {
1318 let Some(sub_id) = parsed.get(1).and_then(Value::as_str) else {
1319 continue;
1320 };
1321 let filters: Vec<Value> = parsed.iter().skip(2).cloned().collect();
1322 let snapshot = events.lock().expect("relay events lock").clone();
1323
1324 if !response_delay.is_zero() {
1325 tokio::time::sleep(response_delay).await;
1326 }
1327
1328 for event in snapshot {
1329 let matched = if filters.is_empty() {
1330 true
1331 } else {
1332 filters
1333 .iter()
1334 .any(|filter| event_matches_filter(&event, filter))
1335 };
1336 if matched {
1337 let message = serde_json::json!(["EVENT", sub_id, event]);
1338 let _ = write.send(Message::Text(message.to_string())).await;
1339 }
1340 }
1341
1342 let eose = serde_json::json!(["EOSE", sub_id]);
1343 let _ = write.send(Message::Text(eose.to_string())).await;
1344 }
1345 "EVENT" => {
1346 let Some(event) = parsed.get(1).cloned() else {
1347 continue;
1348 };
1349 let Some(id) = event
1350 .get("id")
1351 .and_then(Value::as_str)
1352 .map(ToOwned::to_owned)
1353 else {
1354 continue;
1355 };
1356 events.lock().expect("relay events lock").push(event);
1357 let ok = serde_json::json!(["OK", id, true, ""]);
1358 let _ = write.send(Message::Text(ok.to_string())).await;
1359 }
1360 "CLOSE" => {}
1361 _ => {}
1362 }
1363 }
1364 }
1365
1366 fn build_hashtree_event(
1367 keys: &Keys,
1368 tree_name: &str,
1369 created_at: u64,
1370 hash: &str,
1371 content: &str,
1372 ) -> Event {
1373 let tags = vec![
1374 Tag::identifier(tree_name.to_string()),
1375 Tag::custom(
1376 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::L)),
1377 vec![HASHTREE_LABEL.to_string()],
1378 ),
1379 Tag::custom(TagKind::Custom(TAG_HASH.into()), vec![hash.to_string()]),
1380 ];
1381 EventBuilder::new(Kind::Custom(HASHTREE_KIND), content)
1382 .tags(tags)
1383 .custom_created_at(Timestamp::from_secs(created_at))
1384 .sign_with_keys(keys)
1385 .unwrap()
1386 }
1387
1388 fn tamper_event_hash(event: &Event, hash: &str) -> Event {
1389 let mut value = serde_json::to_value(event).expect("event to value");
1390 let tags = value
1391 .get_mut("tags")
1392 .and_then(Value::as_array_mut)
1393 .expect("event tags");
1394 for tag in tags {
1395 let Some(tag) = tag.as_array_mut() else {
1396 continue;
1397 };
1398 if tag.first().and_then(Value::as_str) == Some(TAG_HASH) {
1399 tag[1] = Value::String(hash.to_string());
1400 break;
1401 }
1402 }
1403 serde_json::from_value(value).expect("tampered event from value")
1404 }
1405
1406 #[test]
1407 fn test_parse_key_valid() {
1408 let keys = Keys::generate();
1410 let npub = keys.public_key().to_bech32().unwrap();
1411 let key = format!("{}/mytree", npub);
1412
1413 let result = NostrRootResolver::parse_key(&key);
1414 assert!(result.is_ok());
1415 let (pubkey, tree_name) = result.unwrap();
1416 assert_eq!(pubkey, keys.public_key());
1417 assert_eq!(tree_name, "mytree");
1418 }
1419
1420 #[test]
1421 fn test_parse_key_invalid_format() {
1422 let key = "notvalid";
1423 let result = NostrRootResolver::parse_key(key);
1424 assert!(result.is_err());
1425 }
1426
1427 #[test]
1428 fn test_parse_key_invalid_npub() {
1429 let key = "notannpub/mytree";
1430 let result = NostrRootResolver::parse_key(key);
1431 assert!(result.is_err());
1432 }
1433
1434 #[test]
1435 fn test_resolve_tie_breaks_with_event_id() {
1436 let keys = Keys::generate();
1437 let created_at = 1_700_000_000;
1438
1439 let event_a = build_hashtree_event(
1440 &keys,
1441 "tree",
1442 created_at,
1443 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1444 "a",
1445 );
1446 let event_b = build_hashtree_event(
1447 &keys,
1448 "tree",
1449 created_at,
1450 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1451 "b",
1452 );
1453
1454 let picked = pick_latest_event([&event_a, &event_b]).unwrap();
1455 let expected = if event_a.id < event_b.id {
1456 event_a.id
1457 } else {
1458 event_b.id
1459 };
1460 assert_eq!(picked.id, expected);
1461 }
1462
1463 #[test]
1464 fn test_resolve_shared_tie_breaks_with_event_id() {
1465 let keys = Keys::generate();
1466 let created_at = 1_700_000_000;
1467
1468 let event_old = build_hashtree_event(
1469 &keys,
1470 "tree",
1471 created_at,
1472 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1473 "older",
1474 );
1475 let event_new = build_hashtree_event(
1476 &keys,
1477 "tree",
1478 created_at,
1479 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1480 "newer",
1481 );
1482
1483 let mut events = vec![&event_old, &event_new];
1484 events.sort_by_key(|e| e.id);
1485 let picked = pick_latest_event(events).unwrap();
1486 assert_eq!(picked.id, std::cmp::min(event_old.id, event_new.id));
1487 }
1488
1489 #[test]
1490 fn test_subscribe_tie_breaks_with_event_id() {
1491 let keys = Keys::generate();
1492 let created_at = Timestamp::from_secs(1_700_000_000);
1493
1494 let current = build_hashtree_event(
1495 &keys,
1496 "tree",
1497 created_at.as_secs(),
1498 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1499 "current",
1500 );
1501 let candidate = build_hashtree_event(
1502 &keys,
1503 "tree",
1504 created_at.as_secs(),
1505 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1506 "candidate",
1507 );
1508
1509 let should = is_newer_event(&candidate, current.created_at, Some(current.id));
1510 assert_eq!(should, candidate.id < current.id);
1511 }
1512
1513 #[test]
1514 fn test_list_dedupe_tie_breaks_with_event_id() {
1515 let keys = Keys::generate();
1516 let created_at = 1_700_000_000;
1517
1518 let first = build_hashtree_event(
1519 &keys,
1520 "videos",
1521 created_at,
1522 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1523 "first",
1524 );
1525 let second = build_hashtree_event(
1526 &keys,
1527 "videos",
1528 created_at,
1529 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1530 "second",
1531 );
1532
1533 let first = VerifiedEvent::try_from(first).expect("first event is valid");
1534 let second = VerifiedEvent::try_from(second).expect("second event is valid");
1535 let mut by_tag: HashMap<String, &VerifiedEvent> = HashMap::new();
1536 upsert_latest_by_d_tag(&mut by_tag, &first);
1537 upsert_latest_by_d_tag(&mut by_tag, &second);
1538
1539 let selected = by_tag.get("videos").unwrap();
1540 let expected = if first.id() < second.id() {
1541 first.id()
1542 } else {
1543 second.id()
1544 };
1545 assert_eq!(selected.id(), expected);
1546 }
1547
1548 #[test]
1549 fn test_verified_event_rejects_tampered_signature() {
1550 let keys = Keys::generate();
1551 let event = build_hashtree_event(
1552 &keys,
1553 "videos",
1554 1_700_000_000,
1555 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1556 "",
1557 );
1558 let tampered = tamper_event_hash(
1559 &event,
1560 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1561 );
1562
1563 assert!(VerifiedEvent::try_from(event).is_ok());
1564 assert!(VerifiedEvent::try_from(tampered).is_err());
1565 }
1566
1567 #[tokio::test]
1568 async fn ingested_root_event_resolves_and_updates_subscription_without_relays() {
1569 let keys = Keys::generate();
1570 let npub = keys.public_key().to_bech32().expect("npub");
1571 let resolver = NostrRootResolver::new(NostrResolverConfig {
1572 relays: Vec::new(),
1573 resolve_timeout: Duration::from_millis(50),
1574 secret_key: None,
1575 })
1576 .await
1577 .expect("relayless resolver");
1578 let key = format!("{npub}/releases/app");
1579 let mut subscription = resolver.subscribe(&key).await.expect("subscription");
1580
1581 assert_eq!(subscription.recv().await, Some(None));
1582 let event = build_hashtree_event(
1583 &keys,
1584 "releases/app",
1585 1_700_000_000,
1586 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1587 "",
1588 );
1589 assert!(resolver.ingest_event(event).await.expect("ingest event"));
1590
1591 let expected = Cid {
1592 hash: [0xaa; 32],
1593 key: None,
1594 };
1595 assert_eq!(
1596 resolver.resolve(&key).await.expect("resolve"),
1597 Some(expected.clone())
1598 );
1599 assert_eq!(subscription.recv().await, Some(Some(expected)));
1600 }
1601
1602 #[tokio::test]
1603 async fn ingested_root_event_rejects_tampering_and_keeps_newest_replaceable() {
1604 let keys = Keys::generate();
1605 let npub = keys.public_key().to_bech32().expect("npub");
1606 let resolver = NostrRootResolver::new(NostrResolverConfig {
1607 relays: Vec::new(),
1608 resolve_timeout: Duration::from_millis(50),
1609 secret_key: None,
1610 })
1611 .await
1612 .expect("relayless resolver");
1613 let older = build_hashtree_event(
1614 &keys,
1615 "releases/app",
1616 1_700_000_000,
1617 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1618 "",
1619 );
1620 let newer = build_hashtree_event(
1621 &keys,
1622 "releases/app",
1623 1_700_000_001,
1624 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1625 "",
1626 );
1627 let tampered = tamper_event_hash(
1628 &newer,
1629 "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
1630 );
1631
1632 assert!(resolver.ingest_event(newer).await.expect("newer"));
1633 assert!(!resolver.ingest_event(older).await.expect("older"));
1634 assert!(resolver.ingest_event(tampered).await.is_err());
1635 assert_eq!(
1636 resolver
1637 .resolve(&format!("{npub}/releases/app"))
1638 .await
1639 .expect("resolve")
1640 .expect("root")
1641 .hash,
1642 [0xbb; 32]
1643 );
1644 }
1645
1646 #[test]
1647 fn test_root_selection_ignores_invalid_newer_event() {
1648 let keys = Keys::generate();
1649 let valid_hash = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
1650 let signed_newer = build_hashtree_event(
1651 &keys,
1652 "videos",
1653 1_700_000_010,
1654 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1655 "",
1656 );
1657 let invalid_newer = tamper_event_hash(
1658 &signed_newer,
1659 "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
1660 );
1661 let valid_older = build_hashtree_event(&keys, "videos", 1_700_000_000, valid_hash, "");
1662
1663 let verified = verify_events(vec![invalid_newer, valid_older.clone()]);
1664 let selected = pick_latest_verified_event(
1665 verified
1666 .iter()
1667 .filter(|event| is_matching_tree_event(event, "videos")),
1668 )
1669 .expect("valid older event should remain selectable");
1670
1671 assert_eq!(selected.id(), valid_older.id);
1672 assert_eq!(
1673 NostrRootResolver::cid_from_event_with_keys(selected.as_event(), None),
1674 Some(Cid {
1675 hash: from_hex(valid_hash).unwrap(),
1676 key: None,
1677 })
1678 );
1679 }
1680
1681 #[test]
1682 fn test_publish_bumps_replaceable_timestamp_past_existing_event() {
1683 let runtime = tokio::runtime::Builder::new_multi_thread()
1684 .worker_threads(2)
1685 .enable_all()
1686 .build()
1687 .expect("build test runtime");
1688
1689 runtime.block_on(async {
1690 let keys = Keys::generate();
1691 let npub = keys.public_key().to_bech32().expect("npub");
1692 let key = format!("{npub}/tree");
1693 let stale_created_at = Timestamp::now().as_secs().saturating_add(30);
1694 let stale = build_hashtree_event(
1695 &keys,
1696 "tree",
1697 stale_created_at,
1698 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1699 "stale",
1700 );
1701 let relay = TestRelay::with_events(vec![stale]);
1702 let resolver = NostrRootResolver::new(NostrResolverConfig {
1703 relays: vec![relay.url()],
1704 resolve_timeout: Duration::from_secs(2),
1705 secret_key: Some(keys.clone()),
1706 })
1707 .await
1708 .expect("resolver");
1709
1710 let cid = Cid {
1711 hash: [0x22; 32],
1712 key: None,
1713 };
1714 let published = resolver.publish(&key, &cid).await.expect("publish");
1715 assert!(published);
1716
1717 let resolved = resolver
1718 .resolve(&key)
1719 .await
1720 .expect("resolve")
1721 .expect("published cid");
1722 assert_eq!(resolved, cid);
1723 assert!(
1724 relay
1725 .latest_tree_created_at("tree")
1726 .is_some_and(|created_at| created_at > stale_created_at),
1727 "publish should advance created_at beyond the existing replaceable event"
1728 );
1729 resolver.stop().await.expect("stop resolver");
1730 });
1731 }
1732
1733 #[test]
1734 fn test_resolve_succeeds_when_some_relays_fail() {
1735 let runtime = tokio::runtime::Builder::new_multi_thread()
1736 .worker_threads(2)
1737 .enable_all()
1738 .build()
1739 .expect("build test runtime");
1740
1741 runtime.block_on(async {
1742 let keys = Keys::generate();
1743 let tree_name = "video";
1744 let hash = "c94a1b5bde1d7a32b96df53086a27f4385a631e1e39a5aac97589d20c49c5022";
1745 let event = build_hashtree_event(&keys, tree_name, 1_774_517_172, hash, "");
1746 let good_relay = TestRelay::with_events(vec![event]);
1747 let bad_relay = "ws://127.0.0.1:9".to_string();
1748
1749 let resolver = NostrRootResolver::new(NostrResolverConfig {
1750 relays: vec![bad_relay, good_relay.url()],
1751 resolve_timeout: Duration::from_millis(400),
1752 secret_key: None,
1753 })
1754 .await
1755 .expect("create resolver");
1756
1757 let key = format!("{}/{}", keys.public_key().to_bech32().unwrap(), tree_name);
1758 let resolved = resolver
1759 .resolve(&key)
1760 .await
1761 .expect("resolve via healthy relay");
1762
1763 assert_eq!(
1764 resolved,
1765 Some(Cid {
1766 hash: from_hex(hash).unwrap(),
1767 key: None,
1768 })
1769 );
1770 });
1771 }
1772
1773 #[test]
1774 fn test_resolve_returns_after_quick_quorum() {
1775 let runtime = tokio::runtime::Builder::new_multi_thread()
1776 .worker_threads(2)
1777 .enable_all()
1778 .build()
1779 .expect("build test runtime");
1780
1781 runtime.block_on(async {
1782 let keys = Keys::generate();
1783 let tree_name = "video";
1784 let hash = "c94a1b5bde1d7a32b96df53086a27f4385a631e1e39a5aac97589d20c49c5022";
1785 let event = build_hashtree_event(&keys, tree_name, 1_774_517_172, hash, "");
1786 let quick_hit = TestRelay::with_events(vec![event]);
1787 let quick_empty = TestRelay::with_events(Vec::new());
1788 let slow_empty =
1789 TestRelay::with_events_and_delay(Vec::new(), Duration::from_millis(1200));
1790
1791 let resolver = NostrRootResolver::new(NostrResolverConfig {
1792 relays: vec![quick_hit.url(), quick_empty.url(), slow_empty.url()],
1793 resolve_timeout: Duration::from_millis(1500),
1794 secret_key: None,
1795 })
1796 .await
1797 .expect("create resolver");
1798
1799 let key = format!("{}/{}", keys.public_key().to_bech32().unwrap(), tree_name);
1800 let started = std::time::Instant::now();
1801 let resolved = resolver
1802 .resolve(&key)
1803 .await
1804 .expect("resolve from quick quorum");
1805
1806 assert_eq!(
1807 resolved,
1808 Some(Cid {
1809 hash: from_hex(hash).unwrap(),
1810 key: None,
1811 })
1812 );
1813 assert!(
1814 started.elapsed() < Duration::from_millis(800),
1815 "resolve waited too long: {:?}",
1816 started.elapsed()
1817 );
1818 });
1819 }
1820
1821 #[test]
1822 fn test_resolve_uses_soft_deadline_for_partial_results() {
1823 let runtime = tokio::runtime::Builder::new_multi_thread()
1824 .worker_threads(2)
1825 .enable_all()
1826 .build()
1827 .expect("build test runtime");
1828
1829 runtime.block_on(async {
1830 let keys = Keys::generate();
1831 let tree_name = "video";
1832 let hash = "c94a1b5bde1d7a32b96df53086a27f4385a631e1e39a5aac97589d20c49c5022";
1833 let event = build_hashtree_event(&keys, tree_name, 1_774_517_172, hash, "");
1834 let quick_hit = TestRelay::with_events(vec![event]);
1835 let slow_empty_a = TestRelay::with_events_and_delay(Vec::new(), Duration::from_secs(5));
1836 let slow_empty_b = TestRelay::with_events_and_delay(Vec::new(), Duration::from_secs(5));
1837
1838 let resolver = NostrRootResolver::new(NostrResolverConfig {
1839 relays: vec![quick_hit.url(), slow_empty_a.url(), slow_empty_b.url()],
1840 resolve_timeout: Duration::from_secs(6),
1841 secret_key: None,
1842 })
1843 .await
1844 .expect("create resolver");
1845
1846 let key = format!("{}/{}", keys.public_key().to_bech32().unwrap(), tree_name);
1847 let started = std::time::Instant::now();
1848 let resolved = resolver
1849 .resolve(&key)
1850 .await
1851 .expect("resolve from partial results");
1852
1853 assert_eq!(
1854 resolved,
1855 Some(Cid {
1856 hash: from_hex(hash).unwrap(),
1857 key: None,
1858 })
1859 );
1860 assert!(
1861 started.elapsed() < Duration::from_secs(4),
1862 "resolve missed the soft deadline: {:?}",
1863 started.elapsed()
1864 );
1865 });
1866 }
1867
1868 #[test]
1869 fn test_open_resolve_reobserves_root_and_removes_each_subscription() {
1870 let runtime = tokio::runtime::Builder::new_current_thread()
1871 .enable_all()
1872 .build()
1873 .unwrap();
1874 runtime.block_on(async {
1875 let keys = Keys::generate();
1876 let hash = "c94a1b5bde1d7a32b96df53086a27f4385a631e1e39a5aac97589d20c49c5022";
1877 let relay = TestRelay::with_events(vec![build_hashtree_event(
1878 &keys,
1879 "tap.git",
1880 1_774_517_172,
1881 hash,
1882 "",
1883 )]);
1884 let resolver = NostrRootResolver::new(NostrResolverConfig {
1885 relays: vec![relay.url()],
1886 ..Default::default()
1887 })
1888 .await
1889 .unwrap();
1890 let key = format!("{}/tap.git", keys.public_key().to_bech32().unwrap());
1891 for _ in 0..2 {
1894 assert_eq!(
1895 resolver
1896 .resolve_open(&key, Duration::from_secs(1))
1897 .await
1898 .unwrap(),
1899 Cid {
1900 hash: from_hex(hash).unwrap(),
1901 key: None
1902 }
1903 );
1904 assert!(resolver.client.subscriptions().await.is_empty());
1905 }
1906 resolver.stop().await.unwrap();
1907 });
1908 }
1909
1910 #[tokio::test]
1911 async fn test_ready_resolve_reobserves_and_removes_its_subscription() {
1912 let keys = Keys::generate();
1913 let root = Cid::public([6; 32]);
1914 let relay = TestRelay::with_events(vec![NostrRootResolver::root_event_builder(
1915 "ready", &root, None,
1916 )
1917 .sign_with_keys(&keys)
1918 .unwrap()]);
1919 let resolver = NostrRootResolver::new(NostrResolverConfig {
1920 relays: vec![relay.url()],
1921 ..Default::default()
1922 })
1923 .await
1924 .unwrap();
1925 let key = format!("{}/ready", keys.public_key().to_bech32().unwrap());
1926 for _ in 0..2 {
1927 let result = tokio::time::timeout(
1928 Duration::from_secs(2),
1929 resolver.resolve_ready(&key, Duration::from_secs(10)),
1930 )
1931 .await
1932 .unwrap()
1933 .unwrap();
1934 assert_eq!(result, root);
1935 assert!(resolver.client.subscriptions().await.is_empty());
1936 }
1937 let missing = format!("{}/missing", keys.public_key().to_bech32().unwrap());
1938 assert!(resolver
1939 .resolve_ready(&missing, Duration::from_millis(100))
1940 .await
1941 .is_err());
1942 assert!(resolver.client.subscriptions().await.is_empty());
1943 resolver.stop().await.unwrap();
1944 }
1945
1946 #[test]
1947 fn publish_succeeded_requires_at_least_one_successful_relay() {
1948 assert!(!publish_succeeded(0));
1949 assert!(publish_succeeded(1));
1950 assert!(publish_succeeded(2));
1951 }
1952}