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, xor_keys};
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 mut key = key_hex.and_then(|k| {
617 let bytes = hex::decode(&k).ok()?;
618 if bytes.len() == 32 {
619 let mut arr = [0u8; 32];
620 arr.copy_from_slice(&bytes);
621 Some(arr)
622 } else {
623 None
624 }
625 });
626
627 if key.is_none() {
628 if let (Some(ciphertext), Some(keys)) = (self_encrypted_key, keys) {
629 if let Ok(key_hex) =
630 nip44::decrypt(keys.secret_key(), &keys.public_key(), &ciphertext)
631 {
632 if let Ok(bytes) = hex::decode(&key_hex) {
633 if bytes.len() == 32 {
634 let mut arr = [0u8; 32];
635 arr.copy_from_slice(&bytes);
636 key = Some(arr);
637 }
638 }
639 }
640 }
641 }
642
643 Some(Cid { hash, key })
644 }
645
646 fn cid_from_event_shared(event: &Event, share_secret: &[u8; 32]) -> Option<Cid> {
648 let mut hash_hex: Option<String> = None;
649 let mut key_hex: Option<String> = None;
650 let mut encrypted_key_hex: Option<String> = None;
651 let mut encrypted_key_legacy_hex: Option<String> = None;
652
653 for tag in event.tags.iter() {
654 let tag_vec = tag.as_slice();
655 if tag_vec.len() >= 2 {
656 match tag_vec[0].as_str() {
657 "hash" => hash_hex = Some(tag_vec[1].clone()),
658 "key" => key_hex = Some(tag_vec[1].clone()),
659 TAG_ENCRYPTED_KEY => encrypted_key_hex = Some(tag_vec[1].clone()),
660 TAG_ENCRYPTED_KEY_LEGACY => encrypted_key_legacy_hex = Some(tag_vec[1].clone()),
661 _ => {}
662 }
663 }
664 }
665
666 if hash_hex.is_none() {
667 if let Some((legacy_hash, _legacy_key)) = parse_legacy_content(&event.content) {
668 hash_hex = Some(legacy_hash);
669 }
670 }
671
672 let hash = from_hex(&hash_hex?).ok()?;
673
674 let key = if let Some(k_hex) = key_hex {
675 let bytes = hex::decode(&k_hex).ok()?;
676 if bytes.len() == 32 {
677 let mut arr = [0u8; 32];
678 arr.copy_from_slice(&bytes);
679 Some(arr)
680 } else {
681 None
682 }
683 } else if let Some(ek_hex) = encrypted_key_hex {
684 let masked = hex::decode(&ek_hex).ok()?;
685 if masked.len() == 32 {
686 let mut masked_arr = [0u8; 32];
687 masked_arr.copy_from_slice(&masked);
688 Some(xor_keys(&masked_arr, share_secret))
689 } else {
690 None
691 }
692 } else if let Some(ek_hex) = encrypted_key_legacy_hex {
693 let encrypted_key = hex::decode(&ek_hex).ok()?;
694 let decrypted = decrypt(&encrypted_key, share_secret).ok()?;
695 if decrypted.len() == 32 {
696 let mut arr = [0u8; 32];
697 arr.copy_from_slice(&decrypted);
698 Some(arr)
699 } else {
700 None
701 }
702 } else {
703 None
704 };
705
706 Some(Cid { hash, key })
707 }
708
709 pub async fn resolve_wait(&self, key: &str) -> Result<Cid, ResolverError> {
717 if let Some(cid) = self.resolve(key).await? {
719 return Ok(cid);
720 }
721
722 let mut rx = self.subscribe(key).await?;
724
725 while let Some(maybe_cid) = rx.recv().await {
727 if let Some(cid) = maybe_cid {
728 return Ok(cid);
729 }
730 }
731
732 Err(ResolverError::Stopped)
733 }
734
735 pub async fn publish_private(&self, key: &str, cid: &Cid) -> Result<bool, ResolverError> {
737 let (pubkey, tree_name) = Self::parse_key(key)?;
738
739 let keys = self
740 .config
741 .secret_key
742 .as_ref()
743 .ok_or(ResolverError::NotAuthorized)?;
744 if pubkey != keys.public_key() {
745 return Err(ResolverError::NotAuthorized);
746 }
747
748 let key_bytes = cid
749 .key
750 .ok_or_else(|| ResolverError::Other("Missing CHK key for private publish".into()))?;
751 let key_hex = hex::encode(key_bytes);
752
753 let encrypted = nip44::encrypt(
754 keys.secret_key(),
755 &keys.public_key(),
756 key_hex,
757 nip44::Version::V2,
758 )
759 .map_err(|e| ResolverError::Other(format!("NIP-44 encryption failed: {}", e)))?;
760
761 let tags = vec![
762 Tag::identifier(tree_name.clone()),
763 Tag::custom(
764 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::L)),
765 vec![HASHTREE_LABEL],
766 ),
767 Tag::custom(TagKind::Custom(TAG_HASH.into()), vec![to_hex(&cid.hash)]),
768 Tag::custom(
769 TagKind::Custom(TAG_SELF_ENCRYPTED_KEY.into()),
770 vec![encrypted],
771 ),
772 ];
773
774 let created_at = next_replaceable_created_at(
775 Timestamp::now(),
776 self.latest_existing_created_at(pubkey, &tree_name)
777 .await
778 .ok()
779 .flatten(),
780 );
781 let event = self
782 .client
783 .sign_event_builder(
784 EventBuilder::new(Kind::Custom(HASHTREE_KIND), "")
785 .tags(tags)
786 .custom_created_at(created_at),
787 )
788 .await
789 .map_err(|e| ResolverError::Network(e.to_string()))?;
790
791 let client = self.client.clone();
792 let event_for_send = event.clone();
793 let output =
794 await_publish_result(async move { client.send_event(&event_for_send).await }).await?;
795
796 {
797 let mut subs = self.subscriptions.write().await;
798 if let Some(sub) = subs.get_mut(key) {
799 sub.current_cid = Some(cid.clone());
800 sub.latest_created_at = event.created_at;
801 sub.latest_event_id = Some(event.id);
802 let _ = sub.tx.send(Some(cid.clone())).await;
803 }
804 }
805
806 Ok(publish_succeeded(output.success.len()))
807 }
808}
809
810#[async_trait]
811impl RootResolver for NostrRootResolver {
812 async fn resolve(&self, key: &str) -> Result<Option<Cid>, ResolverError> {
813 let (pubkey, tree_name) = Self::parse_key(key)?;
814 let events = self
815 .verified_events_for_key(key, pubkey, &tree_name)
816 .await?;
817
818 let latest_event = pick_latest_verified_event(
819 events
820 .iter()
821 .filter(|event| is_matching_tree_event(event, &tree_name)),
822 );
823
824 match latest_event {
826 Some(event) => Ok(self.cid_from_event(event.as_event())),
827 None => Ok(None),
828 }
829 }
830
831 async fn resolve_shared(
832 &self,
833 key: &str,
834 share_secret: &[u8; 32],
835 ) -> Result<Option<Cid>, ResolverError> {
836 let (pubkey, tree_name) = Self::parse_key(key)?;
837 let events = self
838 .verified_events_for_key(key, pubkey, &tree_name)
839 .await?;
840
841 let latest_event = pick_latest_verified_event(
842 events
843 .iter()
844 .filter(|event| is_matching_tree_event(event, &tree_name)),
845 );
846
847 match latest_event {
848 Some(event) => Ok(Self::cid_from_event_shared(event.as_event(), share_secret)),
849 None => Ok(None),
850 }
851 }
852
853 async fn subscribe(&self, key: &str) -> Result<mpsc::Receiver<Option<Cid>>, ResolverError> {
854 let (pubkey, tree_name) = Self::parse_key(key)?;
855
856 let (tx, rx) = mpsc::channel(16);
857 let ingested = self.ingested_event(key).await;
858 let current_cid = ingested
859 .as_ref()
860 .and_then(|event| self.cid_from_event(event.as_event()));
861 let latest_created_at = ingested
862 .as_ref()
863 .map_or(Timestamp::from(0), VerifiedEvent::created_at);
864 let latest_event_id = ingested.as_ref().map(VerifiedEvent::id);
865 let _ = tx.send(current_cid.clone()).await;
866
867 let filter = Self::build_tree_filter(pubkey, &tree_name);
869
870 {
872 let mut subs = self.subscriptions.write().await;
873 subs.insert(
874 key.to_string(),
875 Subscription {
876 tx: tx.clone(),
877 current_cid,
878 latest_created_at,
879 latest_event_id,
880 },
881 );
882 }
883
884 let subscriptions = self.subscriptions.clone();
886 let key_clone = key.to_string();
887 let tree_name_clone = tree_name.clone();
888 let secret_key = self.config.secret_key.clone();
889
890 let client = self.client.clone();
892 tokio::spawn(async move {
893 let sub_id = client.subscribe(filter, None).await;
894
895 if sub_id.is_err() {
896 return;
897 }
898
899 let mut notifications = client.notifications();
901
902 while let Ok(notification) = notifications.recv().await {
903 if let RelayPoolNotification::Event { event, .. } = notification {
904 let Ok(event) = VerifiedEvent::try_from(*event) else {
905 continue;
906 };
907
908 if !is_matching_tree_event(&event, &tree_name_clone) {
909 continue;
910 }
911
912 let mut subs = subscriptions.write().await;
913 if let Some(sub) = subs.get_mut(&key_clone) {
914 let new_cid = NostrRootResolver::cid_from_event_with_keys(
915 event.as_event(),
916 secret_key.as_ref(),
917 );
918 if is_newer_event(
919 event.as_event(),
920 sub.latest_created_at,
921 sub.latest_event_id,
922 ) {
923 sub.latest_created_at = event.created_at();
924 sub.latest_event_id = Some(event.id());
925
926 if new_cid != sub.current_cid {
927 sub.current_cid = new_cid.clone();
928 if sub.tx.send(new_cid).await.is_err() {
929 subs.remove(&key_clone);
931 break;
932 }
933 }
934 }
935 } else {
936 break;
937 }
938 }
939 }
940 });
941
942 Ok(rx)
943 }
944
945 async fn publish(&self, key: &str, cid: &Cid) -> Result<bool, ResolverError> {
946 let (pubkey, tree_name) = Self::parse_key(key)?;
947
948 let my_pubkey = self.pubkey().ok_or(ResolverError::NotAuthorized)?;
950 if pubkey != my_pubkey {
951 return Err(ResolverError::NotAuthorized);
952 }
953
954 let latest_created_at = self
955 .latest_existing_created_at(pubkey, &tree_name)
956 .await
957 .ok()
958 .flatten();
959 let event = self
960 .client
961 .sign_event_builder(Self::root_event_builder(&tree_name, cid, latest_created_at))
962 .await
963 .map_err(|e| ResolverError::Network(e.to_string()))?;
964
965 let client = self.client.clone();
966 let event_for_send = event.clone();
967 let output =
968 await_publish_result(async move { client.send_event(&event_for_send).await }).await?;
969
970 {
971 let mut subs = self.subscriptions.write().await;
972 if let Some(sub) = subs.get_mut(key) {
973 sub.current_cid = Some(cid.clone());
974 sub.latest_created_at = event.created_at;
975 sub.latest_event_id = Some(event.id);
976 let _ = sub.tx.send(Some(cid.clone())).await;
977 }
978 }
979
980 Ok(publish_succeeded(output.success.len()))
981 }
982
983 async fn publish_shared(
984 &self,
985 key: &str,
986 cid: &Cid,
987 share_secret: &[u8; 32],
988 ) -> Result<bool, ResolverError> {
989 let (pubkey, tree_name) = Self::parse_key(key)?;
990
991 let my_pubkey = self.pubkey().ok_or(ResolverError::NotAuthorized)?;
992 if pubkey != my_pubkey {
993 return Err(ResolverError::NotAuthorized);
994 }
995
996 let mut tags = vec![
997 Tag::identifier(tree_name.clone()),
998 Tag::custom(
999 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::L)),
1000 vec![HASHTREE_LABEL],
1001 ),
1002 Tag::custom(TagKind::Custom(TAG_HASH.into()), vec![to_hex(&cid.hash)]),
1003 ];
1004
1005 if let Some(key) = cid.key {
1007 let masked = xor_keys(&key, share_secret);
1008 tags.push(Tag::custom(
1009 TagKind::Custom(TAG_ENCRYPTED_KEY.into()),
1010 vec![hex::encode(masked)],
1011 ));
1012 }
1013
1014 let created_at = next_replaceable_created_at(
1015 Timestamp::now(),
1016 self.latest_existing_created_at(pubkey, &tree_name)
1017 .await
1018 .ok()
1019 .flatten(),
1020 );
1021 let event = self
1022 .client
1023 .sign_event_builder(
1024 EventBuilder::new(Kind::Custom(HASHTREE_KIND), "")
1025 .tags(tags)
1026 .custom_created_at(created_at),
1027 )
1028 .await
1029 .map_err(|e| ResolverError::Network(e.to_string()))?;
1030
1031 let client = self.client.clone();
1032 let event_for_send = event.clone();
1033 let output =
1034 await_publish_result(async move { client.send_event(&event_for_send).await }).await?;
1035
1036 {
1037 let mut subs = self.subscriptions.write().await;
1038 if let Some(sub) = subs.get_mut(key) {
1039 sub.current_cid = Some(cid.clone());
1040 sub.latest_created_at = event.created_at;
1041 sub.latest_event_id = Some(event.id);
1042 let _ = sub.tx.send(Some(cid.clone())).await;
1043 }
1044 }
1045
1046 Ok(publish_succeeded(output.success.len()))
1047 }
1048
1049 async fn list(&self, prefix: &str) -> Result<Vec<ResolverEntry>, ResolverError> {
1050 let parts: Vec<&str> = prefix.split('/').collect();
1051 if parts.is_empty() {
1052 return Ok(vec![]);
1053 }
1054
1055 let npub_str = parts[0];
1056 let pubkey = PublicKey::from_bech32(npub_str)
1057 .map_err(|_| ResolverError::InvalidKey(format!("Invalid npub: {}", npub_str)))?;
1058
1059 let filter = Filter::new()
1061 .kinds(hashtree_kinds())
1062 .author(pubkey)
1063 .custom_tag(SingleLetterTag::lowercase(Alphabet::L), HASHTREE_LABEL);
1064
1065 let events = self.fetch_verified_events_from_relays(filter).await?;
1066
1067 let mut entries_by_d_tag: HashMap<String, &VerifiedEvent> = HashMap::new();
1069
1070 for event in events.iter() {
1071 if !is_hashtree_event(event.as_event()) {
1072 continue;
1073 }
1074 upsert_latest_by_d_tag(&mut entries_by_d_tag, event);
1075 }
1076
1077 let mut result = Vec::new();
1079 for (d_tag, event) in entries_by_d_tag {
1080 if let Some(cid) = self.cid_from_event(event.as_event()) {
1081 result.push(ResolverEntry {
1082 key: format!("{}/{}", npub_str, d_tag),
1083 cid,
1084 });
1085 }
1086 }
1087
1088 Ok(result)
1089 }
1090
1091 async fn stop(&self) -> Result<(), ResolverError> {
1092 let _ = self.client.disconnect().await;
1093 Ok(())
1094 }
1095}
1096
1097#[cfg(test)]
1098mod tests {
1099 use super::*;
1100 use futures::{SinkExt, StreamExt};
1101 use std::collections::HashMap;
1102 use std::net::TcpListener;
1103 use std::sync::Mutex;
1104 use tokio::net::TcpStream;
1105 use tokio::sync::broadcast;
1106 use tokio_tungstenite::{accept_async, tungstenite::Message};
1107
1108 struct TestRelay {
1109 port: u16,
1110 events: Arc<Mutex<Vec<Value>>>,
1111 shutdown: broadcast::Sender<()>,
1112 }
1113
1114 impl TestRelay {
1115 fn with_events(events: Vec<Event>) -> Self {
1116 Self::with_events_and_delay(events, Duration::ZERO)
1117 }
1118
1119 fn with_events_and_delay(events: Vec<Event>, response_delay: Duration) -> Self {
1120 Self::with_event_values_and_delay(
1121 events
1122 .into_iter()
1123 .map(|event| serde_json::to_value(event).expect("event to value"))
1124 .collect(),
1125 response_delay,
1126 )
1127 }
1128
1129 fn with_event_values_and_delay(events: Vec<Value>, response_delay: Duration) -> Self {
1130 let stored_events = Arc::new(Mutex::new(events));
1131 let (shutdown, _) = broadcast::channel(1);
1132
1133 let std_listener = TcpListener::bind("127.0.0.1:0").expect("bind relay listener");
1134 let port = std_listener.local_addr().expect("relay local addr").port();
1135 std_listener
1136 .set_nonblocking(true)
1137 .expect("set relay listener nonblocking");
1138
1139 let relay_events = Arc::clone(&stored_events);
1140 let shutdown_tx = shutdown.clone();
1141
1142 std::thread::spawn(move || {
1143 let runtime = tokio::runtime::Builder::new_multi_thread()
1144 .worker_threads(2)
1145 .enable_all()
1146 .build()
1147 .expect("build relay runtime");
1148
1149 runtime.block_on(async move {
1150 let listener =
1151 tokio::net::TcpListener::from_std(std_listener).expect("tokio listener");
1152 let mut shutdown_rx = shutdown_tx.subscribe();
1153
1154 loop {
1155 tokio::select! {
1156 _ = shutdown_rx.recv() => break,
1157 accepted = listener.accept() => {
1158 if let Ok((stream, _)) = accepted {
1159 let relay_events = Arc::clone(&relay_events);
1160 tokio::spawn(async move {
1161 handle_test_relay_connection(
1162 stream,
1163 relay_events,
1164 response_delay,
1165 )
1166 .await;
1167 });
1168 }
1169 }
1170 }
1171 }
1172 });
1173 });
1174
1175 std::thread::sleep(Duration::from_millis(100));
1176
1177 Self {
1178 port,
1179 events: stored_events,
1180 shutdown,
1181 }
1182 }
1183
1184 fn url(&self) -> String {
1185 format!("ws://127.0.0.1:{}", self.port)
1186 }
1187
1188 fn latest_tree_created_at(&self, tree_name: &str) -> Option<u64> {
1189 self.events
1190 .lock()
1191 .expect("relay events")
1192 .iter()
1193 .filter(|event| {
1194 event.get("kind").and_then(Value::as_u64) == Some(HASHTREE_KIND as u64)
1195 && event_tag_matches(event, "d", &[tree_name.to_string()])
1196 })
1197 .max_by_key(|event| {
1198 (
1199 event
1200 .get("created_at")
1201 .and_then(Value::as_u64)
1202 .unwrap_or_default(),
1203 event
1204 .get("id")
1205 .and_then(Value::as_str)
1206 .unwrap_or_default()
1207 .to_string(),
1208 )
1209 })
1210 .and_then(|event| event.get("created_at").and_then(Value::as_u64))
1211 }
1212 }
1213
1214 impl Drop for TestRelay {
1215 fn drop(&mut self) {
1216 let _ = self.shutdown.send(());
1217 std::thread::sleep(Duration::from_millis(50));
1218 }
1219 }
1220
1221 fn event_tag_matches(event: &Value, name: &str, accepted: &[String]) -> bool {
1222 let Some(tags) = event.get("tags").and_then(Value::as_array) else {
1223 return false;
1224 };
1225
1226 tags.iter().any(|tag| {
1227 let Some(arr) = tag.as_array() else {
1228 return false;
1229 };
1230 if arr.len() < 2 {
1231 return false;
1232 }
1233 let Some(tag_name) = arr.first().and_then(Value::as_str) else {
1234 return false;
1235 };
1236 if tag_name != name {
1237 return false;
1238 }
1239 let Some(tag_value) = arr.get(1).and_then(Value::as_str) else {
1240 return false;
1241 };
1242 accepted.iter().any(|value| value == tag_value)
1243 })
1244 }
1245
1246 fn event_matches_filter(event: &Value, filter: &Value) -> bool {
1247 let Some(filter_obj) = filter.as_object() else {
1248 return true;
1249 };
1250
1251 if let Some(kinds) = filter_obj.get("kinds").and_then(Value::as_array) {
1252 let event_kind = event
1253 .get("kind")
1254 .and_then(Value::as_i64)
1255 .unwrap_or_default();
1256 if !kinds
1257 .iter()
1258 .any(|kind| kind.as_i64().is_some_and(|value| value == event_kind))
1259 {
1260 return false;
1261 }
1262 }
1263
1264 if let Some(authors) = filter_obj.get("authors").and_then(Value::as_array) {
1265 let event_author = event
1266 .get("pubkey")
1267 .and_then(Value::as_str)
1268 .unwrap_or_default();
1269 if !authors
1270 .iter()
1271 .filter_map(Value::as_str)
1272 .any(|author| author == event_author)
1273 {
1274 return false;
1275 }
1276 }
1277
1278 if let Some(d_values) = filter_obj.get("#d").and_then(Value::as_array) {
1279 let accepted: Vec<String> = d_values
1280 .iter()
1281 .filter_map(|value| value.as_str().map(ToOwned::to_owned))
1282 .collect();
1283 if !accepted.is_empty() && !event_tag_matches(event, "d", &accepted) {
1284 return false;
1285 }
1286 }
1287
1288 if let Some(l_values) = filter_obj.get("#l").and_then(Value::as_array) {
1289 let accepted: Vec<String> = l_values
1290 .iter()
1291 .filter_map(|value| value.as_str().map(ToOwned::to_owned))
1292 .collect();
1293 if !accepted.is_empty() && !event_tag_matches(event, "l", &accepted) {
1294 return false;
1295 }
1296 }
1297
1298 true
1299 }
1300
1301 #[tokio::test]
1302 async fn publish_task_panic_is_reported_as_error() {
1303 let error = await_publish_result(async move {
1304 panic!("boom");
1305 #[allow(unreachable_code)]
1306 Ok::<(), &'static str>(())
1307 })
1308 .await
1309 .expect_err("panic should be converted to resolver error");
1310
1311 match error {
1312 ResolverError::Other(message) => {
1313 assert!(message.contains("panicked"));
1314 }
1315 other => panic!("expected ResolverError::Other, got {other:?}"),
1316 }
1317 }
1318
1319 async fn handle_test_relay_connection(
1320 stream: TcpStream,
1321 events: Arc<Mutex<Vec<Value>>>,
1322 response_delay: Duration,
1323 ) {
1324 let ws_stream = match accept_async(stream).await {
1325 Ok(ws) => ws,
1326 Err(_) => return,
1327 };
1328 let (mut write, mut read) = ws_stream.split();
1329
1330 while let Some(message) = read.next().await {
1331 let text = match message {
1332 Ok(Message::Text(text)) => text,
1333 Ok(Message::Ping(data)) => {
1334 let _ = write.send(Message::Pong(data)).await;
1335 continue;
1336 }
1337 Ok(Message::Close(_)) => break,
1338 _ => continue,
1339 };
1340
1341 let parsed: Vec<Value> = match serde_json::from_str(&text) {
1342 Ok(value) => value,
1343 Err(_) => continue,
1344 };
1345
1346 let Some(message_type) = parsed.first().and_then(Value::as_str) else {
1347 continue;
1348 };
1349
1350 match message_type {
1351 "REQ" => {
1352 let Some(sub_id) = parsed.get(1).and_then(Value::as_str) else {
1353 continue;
1354 };
1355 let filters: Vec<Value> = parsed.iter().skip(2).cloned().collect();
1356 let snapshot = events.lock().expect("relay events lock").clone();
1357
1358 if !response_delay.is_zero() {
1359 tokio::time::sleep(response_delay).await;
1360 }
1361
1362 for event in snapshot {
1363 let matched = if filters.is_empty() {
1364 true
1365 } else {
1366 filters
1367 .iter()
1368 .any(|filter| event_matches_filter(&event, filter))
1369 };
1370 if matched {
1371 let message = serde_json::json!(["EVENT", sub_id, event]);
1372 let _ = write.send(Message::Text(message.to_string())).await;
1373 }
1374 }
1375
1376 let eose = serde_json::json!(["EOSE", sub_id]);
1377 let _ = write.send(Message::Text(eose.to_string())).await;
1378 }
1379 "EVENT" => {
1380 let Some(event) = parsed.get(1).cloned() else {
1381 continue;
1382 };
1383 let Some(id) = event
1384 .get("id")
1385 .and_then(Value::as_str)
1386 .map(ToOwned::to_owned)
1387 else {
1388 continue;
1389 };
1390 events.lock().expect("relay events lock").push(event);
1391 let ok = serde_json::json!(["OK", id, true, ""]);
1392 let _ = write.send(Message::Text(ok.to_string())).await;
1393 }
1394 "CLOSE" => {}
1395 _ => {}
1396 }
1397 }
1398 }
1399
1400 fn build_hashtree_event(
1401 keys: &Keys,
1402 tree_name: &str,
1403 created_at: u64,
1404 hash: &str,
1405 content: &str,
1406 ) -> Event {
1407 let tags = vec![
1408 Tag::identifier(tree_name.to_string()),
1409 Tag::custom(
1410 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::L)),
1411 vec![HASHTREE_LABEL.to_string()],
1412 ),
1413 Tag::custom(TagKind::Custom(TAG_HASH.into()), vec![hash.to_string()]),
1414 ];
1415 EventBuilder::new(Kind::Custom(HASHTREE_KIND), content)
1416 .tags(tags)
1417 .custom_created_at(Timestamp::from_secs(created_at))
1418 .sign_with_keys(keys)
1419 .unwrap()
1420 }
1421
1422 fn tamper_event_hash(event: &Event, hash: &str) -> Event {
1423 let mut value = serde_json::to_value(event).expect("event to value");
1424 let tags = value
1425 .get_mut("tags")
1426 .and_then(Value::as_array_mut)
1427 .expect("event tags");
1428 for tag in tags {
1429 let Some(tag) = tag.as_array_mut() else {
1430 continue;
1431 };
1432 if tag.first().and_then(Value::as_str) == Some(TAG_HASH) {
1433 tag[1] = Value::String(hash.to_string());
1434 break;
1435 }
1436 }
1437 serde_json::from_value(value).expect("tampered event from value")
1438 }
1439
1440 #[test]
1441 fn test_parse_key_valid() {
1442 let keys = Keys::generate();
1444 let npub = keys.public_key().to_bech32().unwrap();
1445 let key = format!("{}/mytree", npub);
1446
1447 let result = NostrRootResolver::parse_key(&key);
1448 assert!(result.is_ok());
1449 let (pubkey, tree_name) = result.unwrap();
1450 assert_eq!(pubkey, keys.public_key());
1451 assert_eq!(tree_name, "mytree");
1452 }
1453
1454 #[test]
1455 fn test_parse_key_invalid_format() {
1456 let key = "notvalid";
1457 let result = NostrRootResolver::parse_key(key);
1458 assert!(result.is_err());
1459 }
1460
1461 #[test]
1462 fn test_parse_key_invalid_npub() {
1463 let key = "notannpub/mytree";
1464 let result = NostrRootResolver::parse_key(key);
1465 assert!(result.is_err());
1466 }
1467
1468 #[test]
1469 fn test_resolve_tie_breaks_with_event_id() {
1470 let keys = Keys::generate();
1471 let created_at = 1_700_000_000;
1472
1473 let event_a = build_hashtree_event(
1474 &keys,
1475 "tree",
1476 created_at,
1477 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1478 "a",
1479 );
1480 let event_b = build_hashtree_event(
1481 &keys,
1482 "tree",
1483 created_at,
1484 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1485 "b",
1486 );
1487
1488 let picked = pick_latest_event([&event_a, &event_b]).unwrap();
1489 let expected = if event_a.id < event_b.id {
1490 event_a.id
1491 } else {
1492 event_b.id
1493 };
1494 assert_eq!(picked.id, expected);
1495 }
1496
1497 #[test]
1498 fn test_resolve_shared_tie_breaks_with_event_id() {
1499 let keys = Keys::generate();
1500 let created_at = 1_700_000_000;
1501
1502 let event_old = build_hashtree_event(
1503 &keys,
1504 "tree",
1505 created_at,
1506 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1507 "older",
1508 );
1509 let event_new = build_hashtree_event(
1510 &keys,
1511 "tree",
1512 created_at,
1513 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1514 "newer",
1515 );
1516
1517 let mut events = vec![&event_old, &event_new];
1518 events.sort_by_key(|e| e.id);
1519 let picked = pick_latest_event(events).unwrap();
1520 assert_eq!(picked.id, std::cmp::min(event_old.id, event_new.id));
1521 }
1522
1523 #[test]
1524 fn test_subscribe_tie_breaks_with_event_id() {
1525 let keys = Keys::generate();
1526 let created_at = Timestamp::from_secs(1_700_000_000);
1527
1528 let current = build_hashtree_event(
1529 &keys,
1530 "tree",
1531 created_at.as_secs(),
1532 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1533 "current",
1534 );
1535 let candidate = build_hashtree_event(
1536 &keys,
1537 "tree",
1538 created_at.as_secs(),
1539 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1540 "candidate",
1541 );
1542
1543 let should = is_newer_event(&candidate, current.created_at, Some(current.id));
1544 assert_eq!(should, candidate.id < current.id);
1545 }
1546
1547 #[test]
1548 fn test_list_dedupe_tie_breaks_with_event_id() {
1549 let keys = Keys::generate();
1550 let created_at = 1_700_000_000;
1551
1552 let first = build_hashtree_event(
1553 &keys,
1554 "videos",
1555 created_at,
1556 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1557 "first",
1558 );
1559 let second = build_hashtree_event(
1560 &keys,
1561 "videos",
1562 created_at,
1563 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1564 "second",
1565 );
1566
1567 let first = VerifiedEvent::try_from(first).expect("first event is valid");
1568 let second = VerifiedEvent::try_from(second).expect("second event is valid");
1569 let mut by_tag: HashMap<String, &VerifiedEvent> = HashMap::new();
1570 upsert_latest_by_d_tag(&mut by_tag, &first);
1571 upsert_latest_by_d_tag(&mut by_tag, &second);
1572
1573 let selected = by_tag.get("videos").unwrap();
1574 let expected = if first.id() < second.id() {
1575 first.id()
1576 } else {
1577 second.id()
1578 };
1579 assert_eq!(selected.id(), expected);
1580 }
1581
1582 #[test]
1583 fn test_verified_event_rejects_tampered_signature() {
1584 let keys = Keys::generate();
1585 let event = build_hashtree_event(
1586 &keys,
1587 "videos",
1588 1_700_000_000,
1589 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1590 "",
1591 );
1592 let tampered = tamper_event_hash(
1593 &event,
1594 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1595 );
1596
1597 assert!(VerifiedEvent::try_from(event).is_ok());
1598 assert!(VerifiedEvent::try_from(tampered).is_err());
1599 }
1600
1601 #[tokio::test]
1602 async fn ingested_root_event_resolves_and_updates_subscription_without_relays() {
1603 let keys = Keys::generate();
1604 let npub = keys.public_key().to_bech32().expect("npub");
1605 let resolver = NostrRootResolver::new(NostrResolverConfig {
1606 relays: Vec::new(),
1607 resolve_timeout: Duration::from_millis(50),
1608 secret_key: None,
1609 })
1610 .await
1611 .expect("relayless resolver");
1612 let key = format!("{npub}/releases/app");
1613 let mut subscription = resolver.subscribe(&key).await.expect("subscription");
1614
1615 assert_eq!(subscription.recv().await, Some(None));
1616 let event = build_hashtree_event(
1617 &keys,
1618 "releases/app",
1619 1_700_000_000,
1620 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1621 "",
1622 );
1623 assert!(resolver.ingest_event(event).await.expect("ingest event"));
1624
1625 let expected = Cid {
1626 hash: [0xaa; 32],
1627 key: None,
1628 };
1629 assert_eq!(
1630 resolver.resolve(&key).await.expect("resolve"),
1631 Some(expected.clone())
1632 );
1633 assert_eq!(subscription.recv().await, Some(Some(expected)));
1634 }
1635
1636 #[tokio::test]
1637 async fn ingested_root_event_rejects_tampering_and_keeps_newest_replaceable() {
1638 let keys = Keys::generate();
1639 let npub = keys.public_key().to_bech32().expect("npub");
1640 let resolver = NostrRootResolver::new(NostrResolverConfig {
1641 relays: Vec::new(),
1642 resolve_timeout: Duration::from_millis(50),
1643 secret_key: None,
1644 })
1645 .await
1646 .expect("relayless resolver");
1647 let older = build_hashtree_event(
1648 &keys,
1649 "releases/app",
1650 1_700_000_000,
1651 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1652 "",
1653 );
1654 let newer = build_hashtree_event(
1655 &keys,
1656 "releases/app",
1657 1_700_000_001,
1658 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1659 "",
1660 );
1661 let tampered = tamper_event_hash(
1662 &newer,
1663 "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
1664 );
1665
1666 assert!(resolver.ingest_event(newer).await.expect("newer"));
1667 assert!(!resolver.ingest_event(older).await.expect("older"));
1668 assert!(resolver.ingest_event(tampered).await.is_err());
1669 assert_eq!(
1670 resolver
1671 .resolve(&format!("{npub}/releases/app"))
1672 .await
1673 .expect("resolve")
1674 .expect("root")
1675 .hash,
1676 [0xbb; 32]
1677 );
1678 }
1679
1680 #[test]
1681 fn test_root_selection_ignores_invalid_newer_event() {
1682 let keys = Keys::generate();
1683 let valid_hash = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
1684 let signed_newer = build_hashtree_event(
1685 &keys,
1686 "videos",
1687 1_700_000_010,
1688 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1689 "",
1690 );
1691 let invalid_newer = tamper_event_hash(
1692 &signed_newer,
1693 "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
1694 );
1695 let valid_older = build_hashtree_event(&keys, "videos", 1_700_000_000, valid_hash, "");
1696
1697 let verified = verify_events(vec![invalid_newer, valid_older.clone()]);
1698 let selected = pick_latest_verified_event(
1699 verified
1700 .iter()
1701 .filter(|event| is_matching_tree_event(event, "videos")),
1702 )
1703 .expect("valid older event should remain selectable");
1704
1705 assert_eq!(selected.id(), valid_older.id);
1706 assert_eq!(
1707 NostrRootResolver::cid_from_event_with_keys(selected.as_event(), None),
1708 Some(Cid {
1709 hash: from_hex(valid_hash).unwrap(),
1710 key: None,
1711 })
1712 );
1713 }
1714
1715 #[test]
1716 fn test_publish_bumps_replaceable_timestamp_past_existing_event() {
1717 let runtime = tokio::runtime::Builder::new_multi_thread()
1718 .worker_threads(2)
1719 .enable_all()
1720 .build()
1721 .expect("build test runtime");
1722
1723 runtime.block_on(async {
1724 let keys = Keys::generate();
1725 let npub = keys.public_key().to_bech32().expect("npub");
1726 let key = format!("{npub}/tree");
1727 let stale_created_at = Timestamp::now().as_secs().saturating_add(30);
1728 let stale = build_hashtree_event(
1729 &keys,
1730 "tree",
1731 stale_created_at,
1732 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1733 "stale",
1734 );
1735 let relay = TestRelay::with_events(vec![stale]);
1736 let resolver = NostrRootResolver::new(NostrResolverConfig {
1737 relays: vec![relay.url()],
1738 resolve_timeout: Duration::from_secs(2),
1739 secret_key: Some(keys.clone()),
1740 })
1741 .await
1742 .expect("resolver");
1743
1744 let cid = Cid {
1745 hash: [0x22; 32],
1746 key: None,
1747 };
1748 let published = resolver.publish(&key, &cid).await.expect("publish");
1749 assert!(published);
1750
1751 let resolved = resolver
1752 .resolve(&key)
1753 .await
1754 .expect("resolve")
1755 .expect("published cid");
1756 assert_eq!(resolved, cid);
1757 assert!(
1758 relay
1759 .latest_tree_created_at("tree")
1760 .is_some_and(|created_at| created_at > stale_created_at),
1761 "publish should advance created_at beyond the existing replaceable event"
1762 );
1763 resolver.stop().await.expect("stop resolver");
1764 });
1765 }
1766
1767 #[test]
1768 fn test_resolve_succeeds_when_some_relays_fail() {
1769 let runtime = tokio::runtime::Builder::new_multi_thread()
1770 .worker_threads(2)
1771 .enable_all()
1772 .build()
1773 .expect("build test runtime");
1774
1775 runtime.block_on(async {
1776 let keys = Keys::generate();
1777 let tree_name = "video";
1778 let hash = "c94a1b5bde1d7a32b96df53086a27f4385a631e1e39a5aac97589d20c49c5022";
1779 let event = build_hashtree_event(&keys, tree_name, 1_774_517_172, hash, "");
1780 let good_relay = TestRelay::with_events(vec![event]);
1781 let bad_relay = "ws://127.0.0.1:9".to_string();
1782
1783 let resolver = NostrRootResolver::new(NostrResolverConfig {
1784 relays: vec![bad_relay, good_relay.url()],
1785 resolve_timeout: Duration::from_millis(400),
1786 secret_key: None,
1787 })
1788 .await
1789 .expect("create resolver");
1790
1791 let key = format!("{}/{}", keys.public_key().to_bech32().unwrap(), tree_name);
1792 let resolved = resolver
1793 .resolve(&key)
1794 .await
1795 .expect("resolve via healthy relay");
1796
1797 assert_eq!(
1798 resolved,
1799 Some(Cid {
1800 hash: from_hex(hash).unwrap(),
1801 key: None,
1802 })
1803 );
1804 });
1805 }
1806
1807 #[test]
1808 fn test_resolve_returns_after_quick_quorum() {
1809 let runtime = tokio::runtime::Builder::new_multi_thread()
1810 .worker_threads(2)
1811 .enable_all()
1812 .build()
1813 .expect("build test runtime");
1814
1815 runtime.block_on(async {
1816 let keys = Keys::generate();
1817 let tree_name = "video";
1818 let hash = "c94a1b5bde1d7a32b96df53086a27f4385a631e1e39a5aac97589d20c49c5022";
1819 let event = build_hashtree_event(&keys, tree_name, 1_774_517_172, hash, "");
1820 let quick_hit = TestRelay::with_events(vec![event]);
1821 let quick_empty = TestRelay::with_events(Vec::new());
1822 let slow_empty =
1823 TestRelay::with_events_and_delay(Vec::new(), Duration::from_millis(1200));
1824
1825 let resolver = NostrRootResolver::new(NostrResolverConfig {
1826 relays: vec![quick_hit.url(), quick_empty.url(), slow_empty.url()],
1827 resolve_timeout: Duration::from_millis(1500),
1828 secret_key: None,
1829 })
1830 .await
1831 .expect("create resolver");
1832
1833 let key = format!("{}/{}", keys.public_key().to_bech32().unwrap(), tree_name);
1834 let started = std::time::Instant::now();
1835 let resolved = resolver
1836 .resolve(&key)
1837 .await
1838 .expect("resolve from quick quorum");
1839
1840 assert_eq!(
1841 resolved,
1842 Some(Cid {
1843 hash: from_hex(hash).unwrap(),
1844 key: None,
1845 })
1846 );
1847 assert!(
1848 started.elapsed() < Duration::from_millis(800),
1849 "resolve waited too long: {:?}",
1850 started.elapsed()
1851 );
1852 });
1853 }
1854
1855 #[test]
1856 fn test_resolve_uses_soft_deadline_for_partial_results() {
1857 let runtime = tokio::runtime::Builder::new_multi_thread()
1858 .worker_threads(2)
1859 .enable_all()
1860 .build()
1861 .expect("build test runtime");
1862
1863 runtime.block_on(async {
1864 let keys = Keys::generate();
1865 let tree_name = "video";
1866 let hash = "c94a1b5bde1d7a32b96df53086a27f4385a631e1e39a5aac97589d20c49c5022";
1867 let event = build_hashtree_event(&keys, tree_name, 1_774_517_172, hash, "");
1868 let quick_hit = TestRelay::with_events(vec![event]);
1869 let slow_empty_a = TestRelay::with_events_and_delay(Vec::new(), Duration::from_secs(5));
1870 let slow_empty_b = TestRelay::with_events_and_delay(Vec::new(), Duration::from_secs(5));
1871
1872 let resolver = NostrRootResolver::new(NostrResolverConfig {
1873 relays: vec![quick_hit.url(), slow_empty_a.url(), slow_empty_b.url()],
1874 resolve_timeout: Duration::from_secs(6),
1875 secret_key: None,
1876 })
1877 .await
1878 .expect("create resolver");
1879
1880 let key = format!("{}/{}", keys.public_key().to_bech32().unwrap(), tree_name);
1881 let started = std::time::Instant::now();
1882 let resolved = resolver
1883 .resolve(&key)
1884 .await
1885 .expect("resolve from partial results");
1886
1887 assert_eq!(
1888 resolved,
1889 Some(Cid {
1890 hash: from_hex(hash).unwrap(),
1891 key: None,
1892 })
1893 );
1894 assert!(
1895 started.elapsed() < Duration::from_secs(4),
1896 "resolve missed the soft deadline: {:?}",
1897 started.elapsed()
1898 );
1899 });
1900 }
1901
1902 #[test]
1903 fn test_open_resolve_reobserves_root_and_removes_each_subscription() {
1904 let runtime = tokio::runtime::Builder::new_current_thread()
1905 .enable_all()
1906 .build()
1907 .unwrap();
1908 runtime.block_on(async {
1909 let keys = Keys::generate();
1910 let hash = "c94a1b5bde1d7a32b96df53086a27f4385a631e1e39a5aac97589d20c49c5022";
1911 let relay = TestRelay::with_events(vec![build_hashtree_event(
1912 &keys,
1913 "tap.git",
1914 1_774_517_172,
1915 hash,
1916 "",
1917 )]);
1918 let resolver = NostrRootResolver::new(NostrResolverConfig {
1919 relays: vec![relay.url()],
1920 ..Default::default()
1921 })
1922 .await
1923 .unwrap();
1924 let key = format!("{}/tap.git", keys.public_key().to_bech32().unwrap());
1925 for _ in 0..2 {
1928 assert_eq!(
1929 resolver
1930 .resolve_open(&key, Duration::from_secs(1))
1931 .await
1932 .unwrap(),
1933 Cid {
1934 hash: from_hex(hash).unwrap(),
1935 key: None
1936 }
1937 );
1938 assert!(resolver.client.subscriptions().await.is_empty());
1939 }
1940 resolver.stop().await.unwrap();
1941 });
1942 }
1943
1944 #[test]
1945 fn publish_succeeded_requires_at_least_one_successful_relay() {
1946 assert!(!publish_succeeded(0));
1947 assert!(publish_succeeded(1));
1948 assert!(publish_succeeded(2));
1949 }
1950}