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