Skip to main content

hashtree_resolver/
nostr.rs

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/// Configuration for NostrRootResolver
27#[derive(Clone)]
28pub struct NostrResolverConfig {
29    /// Nostr relays to connect to
30    pub relays: Vec<String>,
31    /// Timeout for one-shot resolve operations
32    pub resolve_timeout: Duration,
33    /// Secret key for publishing (optional)
34    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
51/// Tag names for hashtree events
52const 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    // NIP-01 ordering: newest created_at, then lowest event id.
103    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/// A Nostr event whose id and Schnorr signature have been checked locally.
139#[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    // NIP-01 ordering: newest created_at, then lowest event id.
192    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
269/// Subscription state
270struct 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
277/// NostrRootResolver - Maps npub/treename keys to merkle root hashes
278pub 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    /// Create a new NostrRootResolver
287    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        // Add relays
292        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        // Connect
300        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    /// Parse a key into pubkey and tree name
311    /// Tree names may contain slashes (e.g., "videos/Music")
312    fn parse_key(key: &str) -> Result<(PublicKey, String), ResolverError> {
313        // Use splitn(2) to split only on first '/' - tree names can contain '/'
314        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    /// Get current user's public key (if we have a secret key)
332    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    /// Build the exact Nostr filter for a resolver key without opening a relay
347    /// connection. Pubsub transports can use this to subscribe on behalf of
348    /// the resolver.
349    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    /// Build an unsigned public-root announcement for any event transport.
355    /// The caller signs with the owner of the resolver key.
356    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    /// Validate that a signed event is a usable root for exactly this key.
384    ///
385    /// Pubsub consumers can use this before caching an event obtained from an
386    /// untrusted transport. Private roots require resolver keys and therefore
387    /// are not considered usable by this transport-neutral check.
388    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    /// Authenticate and decode a public root without creating a relay client.
393    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    /// Ingest a signed root event obtained from any transport. Returns true
403    /// only when it advances the replaceable root for its author and tree.
404    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    /// Extract Cid from event tags
479    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        // hash is required
613        let hash = from_hex(&hash_hex?).ok()?;
614
615        // key is optional
616        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    /// Extract Cid from event with encrypted key decryption
647    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    /// Resolve a key, waiting indefinitely until found.
710    ///
711    /// Unlike `resolve()` which returns `None` after timeout, this method
712    /// subscribes and waits until a Cid is found. Caller should apply their
713    /// own timeout if needed (e.g., via `tokio::time::timeout`).
714    ///
715    /// This matches the behavior of hashtree-ts NostrRootResolver.
716    pub async fn resolve_wait(&self, key: &str) -> Result<Cid, ResolverError> {
717        // First try a quick resolve
718        if let Some(cid) = self.resolve(key).await? {
719            return Ok(cid);
720        }
721
722        // Not found, subscribe and wait
723        let mut rx = self.subscribe(key).await?;
724
725        // Wait for first non-None value
726        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    /// Publish a private root (selfEncryptedKey tag, NIP-44 to self)
736    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        // Extract Cid from event tags
825        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        // Create filter
868        let filter = Self::build_tree_filter(pubkey, &tree_name);
869
870        // Store subscription state
871        {
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        // Subscribe to events
885        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        // Spawn subscription handler
891        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            // Handle incoming events via notifications
900            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                                    // Receiver dropped, clean up
930                                    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        // Check we own this key
949        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        // Mask the key with share_secret (XOR)
1006        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        // Filter for all hashtree events from this author
1060        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        // Deduplicate by d-tag, keeping latest event
1068        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        // Convert to entries
1078        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        // Generate a valid npub for testing
1443        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            // The second lookup must observe the relay response even though the
1926            // SDK has already seen this exact event on the first subscription.
1927            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}