Skip to main content

hashtree_cli/
fips_transport.rs

1//! Hashtree daemon integration for the embedded FIPS endpoint API.
2//!
3//! This starts `fips_core::FipsEndpoint` inside the htree process; it does not
4//! depend on or talk to an external FIPS daemon.
5
6mod blob_resolver;
7pub(crate) use blob_resolver::read_daemon_blob;
8use blob_resolver::{BlockingStoreRoute, DaemonInboundBlobRoute};
9
10use crate::config::{Config, NostrEventTransport};
11#[cfg(feature = "experimental-decentralized-pubsub")]
12use crate::nostr_relay::NostrRelay;
13use crate::storage::{HashtreeStore, StorageRouter};
14use anyhow::{Context, Result};
15use hashtree_core::{BlobRoute, StoreBlobRoute};
16use hashtree_fips_transport::{
17    bind_fips_endpoint, bind_fips_endpoint_at_local_rendezvous, set_fips_peer_configs,
18    BoundFipsEndpoint, FipsBlobRoute, FipsEndpoint, FipsEndpointOptions, FipsPeerConfig,
19    PeerIdentity, TcpBlobTransport, TcpBlobTransportConfig, WebSocketConfig,
20    DEFAULT_FIPS_DISCOVERY_SCOPE,
21};
22use hashtree_network::{BlobRouteEntry, BlobRouter, BlobRouterConfig, MeshForwardingRoute};
23use hashtree_nostr_pubsub::HashtreeNostrBoundedEventCache;
24use nostr::nips::nip19::ToBech32;
25use nostr::PublicKey;
26#[cfg(feature = "experimental-decentralized-pubsub")]
27use nostr_pubsub::{EventBus, Filter, VerifiedEvent};
28use nostr_pubsub::{
29    EventPolicyContext, EventRetentionPolicy, EventSource, NostrPubsubRouter, PolicyDecision,
30    PubsubPolicy, RouterLiveSource, RouterPublishSource, RouterQuerySource, SourcePolicyContext,
31    SourceRoute,
32};
33use nostr_pubsub_fips::{FipsPubsubClient, FipsPubsubClientOptions, FipsPubsubPolicyOptions};
34use std::collections::HashSet;
35use std::sync::{Arc, Mutex};
36use std::time::Duration;
37#[cfg(feature = "experimental-decentralized-pubsub")]
38use tokio::sync::mpsc;
39#[cfg(feature = "experimental-decentralized-pubsub")]
40use tokio::task::JoinHandle;
41
42pub type DaemonBlobResolver = BlobRouter;
43type DaemonBlobTransport = TcpBlobTransport<StorageRouter>;
44pub type DaemonNostrCache = HashtreeNostrBoundedEventCache<StorageRouter>;
45
46const DAEMON_SAME_HOST_PROVIDER_PRIORITY: i16 = 100;
47const DAEMON_NOSTR_CACHE_EVENTS: usize = 4_096;
48const BLOB_RESOLVER_REPLY_MARGIN: Duration = Duration::from_secs(1);
49
50pub struct DaemonFipsHandle {
51    pub endpoint: Arc<FipsEndpoint>,
52    pub endpoint_npub: String,
53    pub discovery_scope: String,
54    pub pubsub_client: Option<Arc<FipsPubsubClient>>,
55    pub blob_resolver: Arc<DaemonBlobResolver>,
56    blob_transport: Mutex<Option<Arc<DaemonBlobTransport>>>,
57}
58
59impl DaemonFipsHandle {
60    pub async fn shutdown(&self) {
61        if let Ok(mut transport) = self.blob_transport.lock() {
62            transport.take();
63        }
64        if let Some(client) = &self.pubsub_client {
65            client.shutdown_shared().await;
66        }
67        if let Err(err) = self.endpoint.shutdown().await {
68            tracing::warn!("failed to stop embedded FIPS endpoint: {err}");
69        }
70    }
71}
72
73#[cfg(feature = "experimental-decentralized-pubsub")]
74pub struct DaemonNostrPubsubHandle {
75    relay: Arc<NostrRelay>,
76    tasks: tokio::sync::Mutex<Option<[JoinHandle<()>; 2]>>,
77}
78
79#[cfg(feature = "experimental-decentralized-pubsub")]
80impl DaemonNostrPubsubHandle {
81    pub async fn shutdown(&self) {
82        let mut tasks = self.tasks.lock().await;
83        self.relay.set_decentralized_pubsub_sender(None);
84        if let Some(tasks) = tasks.take() {
85            for task in &tasks {
86                task.abort();
87            }
88            for task in tasks {
89                let _ = task.await;
90            }
91        }
92    }
93}
94
95pub async fn start_daemon_fips_transport(
96    config: &Config,
97    keys: &nostr::Keys,
98    store: Arc<HashtreeStore>,
99    peer_ids: Vec<String>,
100) -> Result<Option<DaemonFipsHandle>> {
101    if !config.server.enable_fips || !config.server.mode.hash_get_enabled() {
102        return Ok(None);
103    }
104
105    let active_relays = config.nostr.active_relays();
106    let relays = config.server.resolved_fips_relays(&active_relays);
107    let discovery_scope = normalized_discovery_scope(&config.server.fips_discovery_scope);
108    let peer_configs = daemon_fips_peer_configs(config, peer_ids);
109    let identity_nsec = keys
110        .secret_key()
111        .to_bech32()
112        .context("Failed to encode daemon identity for FIPS endpoint")?;
113    let mut options = FipsEndpointOptions::new(identity_nsec);
114    options.discovery_scope = discovery_scope.clone();
115    options.relays = relays;
116    options.enable_udp = config.server.enable_fips_udp;
117    options.enable_webrtc = config.server.enable_fips_webrtc;
118    let websocket_seed_urls = config.server.resolved_fips_websocket_seed_urls();
119    if !websocket_seed_urls.is_empty() {
120        options.websocket = Some(WebSocketConfig {
121            seed_urls: websocket_seed_urls,
122            ..Default::default()
123        });
124    }
125    options.enable_local_rendezvous = true;
126    options.enable_lan_discovery = config.server.enable_fips_lan_discovery;
127    options.ethernet_interfaces = config.server.fips_ethernet_interfaces.clone();
128    options.udp_bind_addr = config.server.fips_udp_bind_addr.clone();
129    options.udp_public = config.server.fips_udp_public;
130    options.udp_external_addr = config.server.fips_udp_external_addr.clone();
131    // A configured/followed peer may be WebRTC-only. Let this endpoint dial
132    // discovered WebRTC adverts so connectivity does not depend on the remote
133    // side winning the offer race.
134    options.webrtc_auto_connect = options.enable_webrtc && !peer_configs.is_empty();
135    options.webrtc_max_connections = hashtree_fips_transport::DEFAULT_FIPS_WEBRTC_MAX_CONNECTIONS;
136    options.open_discovery_max_pending = config.server.fips_open_discovery_max_pending;
137    options.packet_channel_capacity = 1024;
138    let endpoint = if let Some(raw) = config.server.fips_local_rendezvous_addr.as_deref() {
139        let rendezvous_addr = raw
140            .parse()
141            .context("server.fips_local_rendezvous_addr must be an IPv4 socket address")?;
142        bind_fips_endpoint_at_local_rendezvous(options, rendezvous_addr).await
143    } else {
144        bind_fips_endpoint(options).await
145    }
146    .context("Failed to start FIPS endpoint")?;
147    let request_timeout = Duration::from_millis(config.server.fips_request_timeout_ms.max(1));
148    let pubsub_client = if daemon_fips_pubsub_required(config) {
149        let options = FipsPubsubClientOptions {
150            query_timeout: request_timeout,
151            routed_peers: config.nostr.fips_pubsub_peers.clone(),
152            max_frame_bytes: config
153                .nostr
154                .decentralized_pubsub_max_event_bytes
155                .min(nostr_pubsub_fips::FIPS_NOSTR_PUBSUB_MAX_FRAME_BYTES),
156            ..Default::default()
157        };
158        let mut policy = FipsPubsubPolicyOptions::default();
159        policy
160            .reputation
161            .trusted_raters
162            .extend(config.nostr.fips_trusted_raters.iter().cloned());
163        Some(Arc::new(
164            FipsPubsubClient::start_with_reputation(
165                endpoint.native_endpoint.clone(),
166                options,
167                policy,
168            )
169            .await
170            .context("Failed to start FIPS Nostr pubsub provider")?,
171        ))
172    } else {
173        None
174    };
175    if !peer_configs.is_empty() {
176        set_fips_peer_configs(endpoint.native_endpoint.as_ref(), peer_configs.clone())
177            .await
178            .context("Failed to configure FIPS peers")?;
179    }
180    let (blob_resolver, blob_transport) =
181        bind_daemon_blob_resolver(&endpoint, store.store_arc(), &peer_configs, request_timeout)
182            .await?;
183
184    Ok(Some(DaemonFipsHandle {
185        endpoint: endpoint.native_endpoint,
186        endpoint_npub: endpoint.local_peer_id,
187        discovery_scope: endpoint.discovery_scope,
188        pubsub_client,
189        blob_resolver,
190        blob_transport: Mutex::new(Some(blob_transport)),
191    }))
192}
193
194fn daemon_fips_pubsub_required(config: &Config) -> bool {
195    config.nostr.event_transport == NostrEventTransport::FipsLocalOnly
196        || config.nostr.decentralized_pubsub_enabled()
197}
198
199async fn bind_daemon_blob_resolver(
200    endpoint: &BoundFipsEndpoint,
201    store: Arc<StorageRouter>,
202    peers: &[FipsPeerConfig],
203    request_timeout: Duration,
204) -> Result<(Arc<DaemonBlobResolver>, Arc<DaemonBlobTransport>)> {
205    let mut routes = vec![BlobRouteEntry::new(
206        "configured-store",
207        Arc::new(BlockingStoreRoute(Arc::new(StoreBlobRoute::new(
208            store.clone(),
209        )))),
210    )];
211    let resolver = Arc::new(
212        BlobRouter::new(
213            routes.clone(),
214            Some(store.clone()),
215            BlobRouterConfig {
216                request_timeout,
217                ..Default::default()
218            },
219        )
220        .map_err(anyhow::Error::msg)
221        .context("Failed to configure the daemon blob router")?,
222    );
223    // Advertise the same resolver used by in-process daemon reads. The FIPS
224    // route added below owns only a weak transport reference, so routing an
225    // inbound request through this resolver does not create an Arc cycle.
226    let inbound_route: Arc<dyn BlobRoute> = Arc::new(DaemonInboundBlobRoute(resolver.clone()));
227    let transport = Arc::new(
228        TcpBlobTransport::bind_advertised_route_with_config(
229            endpoint.native_endpoint.clone(),
230            store.clone(),
231            inbound_route,
232            blob_transport_config(request_timeout),
233            DAEMON_SAME_HOST_PROVIDER_PRIORITY,
234        )
235        .await
236        .context("Failed to advertise htree's blob resolver")?,
237    );
238    let peer_identities: Vec<_> = peers
239        .iter()
240        .filter_map(|peer| PeerIdentity::from_npub(&peer.npub).ok())
241        .collect();
242    if !peer_identities.is_empty() {
243        let max_provider_attempts = peer_identities.len().min(4);
244        let fips_route =
245            FipsBlobRoute::explicit(transport.clone(), peer_identities, max_provider_attempts)
246                .map_err(anyhow::Error::msg)
247                .context("Failed to configure the daemon FIPS blob route")?;
248        routes.push(BlobRouteEntry::new(
249            "configured-fips-peers",
250            Arc::new(MeshForwardingRoute::new(Arc::new(fips_route))),
251        ));
252    }
253    resolver
254        .set_routes(routes)
255        .await
256        .map_err(anyhow::Error::msg)
257        .context("Failed to install daemon blob routes")?;
258    Ok((resolver, transport))
259}
260
261fn blob_transport_config(resolver_timeout: Duration) -> TcpBlobTransportConfig {
262    TcpBlobTransportConfig {
263        idle_timeout: resolver_timeout.saturating_add(BLOB_RESOLVER_REPLY_MARGIN),
264    }
265}
266
267pub fn new_daemon_nostr_cache(store: Arc<StorageRouter>) -> Arc<DaemonNostrCache> {
268    Arc::new(
269        HashtreeNostrBoundedEventCache::new(
270            store,
271            None,
272            EventSource::local_index("hashtree-nostr-cache"),
273            EventRetentionPolicy::new(DAEMON_NOSTR_CACHE_EVENTS, Vec::new()),
274        )
275        .with_priority(nostr_pubsub::SOURCE_PRIORITY_LOCAL_INDEX),
276    )
277}
278
279struct AllowDaemonPubsubRoutes;
280
281#[async_trait::async_trait]
282impl PubsubPolicy for AllowDaemonPubsubRoutes {
283    async fn check_event(
284        &self,
285        _context: EventPolicyContext<'_>,
286    ) -> nostr_pubsub::Result<PolicyDecision> {
287        Ok(PolicyDecision::allow_with_priority(0))
288    }
289
290    async fn check_source(
291        &self,
292        _context: SourcePolicyContext<'_>,
293    ) -> nostr_pubsub::Result<PolicyDecision> {
294        Ok(PolicyDecision::allow_with_priority(0))
295    }
296}
297
298pub async fn start_daemon_nostr_provider(
299    config: &Config,
300    fips_handle: Option<&DaemonFipsHandle>,
301    cache: Option<Arc<DaemonNostrCache>>,
302) -> Result<Option<Arc<dyn nostr_pubsub::PubsubProvider>>> {
303    let mut router = NostrPubsubRouter::new(Arc::new(AllowDaemonPubsubRoutes));
304    if let Some(cache) = cache {
305        let route = cache
306            .source_route("hashtree-local-cache")
307            .context("Failed to configure Hashtree Nostr cache route")?;
308        router = router.with_query_source(RouterQuerySource::new(route, cache));
309    }
310
311    let router = match config.nostr.event_transport {
312        NostrEventTransport::Relay => {
313            let relays = config.nostr.active_relays();
314            if relays.is_empty() {
315                return Ok(None);
316            }
317            let route = SourceRoute::relay(relays.join(","))
318                .with_dataset("configured-relays")
319                .context("Failed to configure Nostr relay route")?;
320            let event_bus = Arc::new(
321                nostr_pubsub_relay::RelayEventBus::new(
322                    relays.clone(),
323                    Duration::from_millis(config.server.fips_request_timeout_ms.max(1)),
324                )
325                .await
326                .context("Failed to start Nostr relay event provider")?,
327            );
328            router
329                .with_query_source(RouterQuerySource::new(
330                    route.clone(),
331                    Arc::clone(&event_bus),
332                ))
333                .with_publish_source(RouterPublishSource::new(
334                    route.clone(),
335                    Arc::clone(&event_bus),
336                ))
337                .with_live_source(RouterLiveSource::new(route, event_bus))
338        }
339        NostrEventTransport::FipsLocalOnly => {
340            let client = fips_handle
341                .and_then(|handle| handle.pubsub_client.clone())
342                .ok_or_else(|| {
343                anyhow::anyhow!(
344                    "nostr.event_transport=fips-local-only requires the local FIPS Nostr pubsub provider"
345                )
346                })?;
347            let route = SourceRoute::fips_peer_default("connected-fips-mesh")
348                .with_dataset("fips-mesh")
349                .context("Failed to configure FIPS Nostr route")?;
350            router
351                .with_query_source(RouterQuerySource::new(route.clone(), Arc::clone(&client)))
352                .with_publish_source(RouterPublishSource::new(route.clone(), Arc::clone(&client)))
353                .with_live_source(RouterLiveSource::new(route, client))
354        }
355    };
356    Ok(Some(Arc::new(router)))
357}
358
359#[cfg(feature = "experimental-decentralized-pubsub")]
360pub async fn start_daemon_nostr_pubsub(
361    config: &Config,
362    fips_handle: Option<&DaemonFipsHandle>,
363    relay: Option<Arc<NostrRelay>>,
364    cache: Arc<DaemonNostrCache>,
365) -> Result<Option<Arc<DaemonNostrPubsubHandle>>> {
366    if !daemon_decentralized_pubsub_ready(config, fips_handle.is_some(), relay.is_some()) {
367        return Ok(None);
368    }
369
370    let fips_handle = fips_handle.expect("checked by daemon_decentralized_pubsub_ready");
371    let relay = relay.expect("checked by daemon_decentralized_pubsub_ready");
372    let client = fips_handle.pubsub_client.clone().ok_or_else(|| {
373        anyhow::anyhow!("decentralized Nostr pubsub requires the shared FIPS pubsub client")
374    })?;
375
376    let (outbound_tx, outbound_rx) = mpsc::unbounded_channel();
377    relay.set_decentralized_pubsub_sender(Some(outbound_tx));
378    let ingest_task = spawn_daemon_nostr_pubsub_ingest(
379        Arc::clone(&client),
380        Arc::clone(&fips_handle.endpoint),
381        Arc::clone(&relay),
382        Arc::clone(&cache),
383    );
384    let outbound_task = spawn_daemon_nostr_pubsub_outbound(client, outbound_rx, cache);
385
386    Ok(Some(Arc::new(DaemonNostrPubsubHandle {
387        relay,
388        tasks: tokio::sync::Mutex::new(Some([ingest_task, outbound_task])),
389    })))
390}
391
392#[cfg(feature = "experimental-decentralized-pubsub")]
393fn daemon_decentralized_pubsub_ready(config: &Config, has_fips: bool, has_relay: bool) -> bool {
394    config.nostr.decentralized_pubsub_enabled() && has_fips && has_relay
395}
396
397#[cfg(feature = "experimental-decentralized-pubsub")]
398fn spawn_daemon_nostr_pubsub_ingest(
399    client: Arc<FipsPubsubClient>,
400    endpoint: Arc<FipsEndpoint>,
401    relay: Arc<NostrRelay>,
402    cache: Arc<DaemonNostrCache>,
403) -> JoinHandle<()> {
404    tokio::spawn(async move {
405        loop {
406            let subscribed_peers = connected_fips_peer_ids(&endpoint).await;
407            if subscribed_peers.is_empty() {
408                tokio::time::sleep(Duration::from_millis(250)).await;
409                continue;
410            }
411            let mut subscription = match client.subscribe(vec![Filter::new()]).await {
412                Ok(subscription) => subscription,
413                Err(err) => {
414                    tracing::debug!("waiting to subscribe to FIPS Nostr peers: {err}");
415                    tokio::time::sleep(Duration::from_millis(250)).await;
416                    continue;
417                }
418            };
419            loop {
420                tokio::select! {
421                    delivery = subscription.recv() => {
422                        let Some(delivery) = delivery else {
423                            break;
424                        };
425                        let source = delivery.source.clone();
426                        let source_id = source.id.as_str().to_string();
427                        let verified = delivery.event;
428                        let event = verified.as_event().clone();
429                        let event_id = event.id.to_hex();
430                        match relay.ingest_peer_event_silent(event).await {
431                            Ok(true) => {
432                                if let Err(error) = cache.publish(verified, source).await {
433                                    tracing::warn!(event_id, %error, "failed to cache decentralized Nostr event");
434                                }
435                                tracing::debug!(
436                                    source = source_id,
437                                    event_id,
438                                    "ingested decentralized Nostr pubsub event"
439                                );
440                            }
441                            Ok(false) => {}
442                            Err(err) => tracing::warn!(
443                                source = source_id,
444                                event_id,
445                                "nostr decentralized pubsub ingest failed: {err:#}"
446                            ),
447                        }
448                    }
449                    () = tokio::time::sleep(Duration::from_millis(500)) => {
450                        if connected_fips_peer_ids(&endpoint).await != subscribed_peers {
451                            break;
452                        }
453                    }
454                }
455            }
456        }
457    })
458}
459
460#[cfg(feature = "experimental-decentralized-pubsub")]
461async fn connected_fips_peer_ids(endpoint: &FipsEndpoint) -> Vec<String> {
462    let mut peers = endpoint
463        .peers()
464        .await
465        .unwrap_or_default()
466        .into_iter()
467        .filter(|peer| peer.connected)
468        .map(|peer| peer.npub)
469        .collect::<Vec<_>>();
470    peers.sort_unstable();
471    peers.dedup();
472    peers
473}
474
475#[cfg(feature = "experimental-decentralized-pubsub")]
476fn spawn_daemon_nostr_pubsub_outbound(
477    client: Arc<FipsPubsubClient>,
478    mut outbound_rx: mpsc::UnboundedReceiver<nostr::Event>,
479    cache: Arc<DaemonNostrCache>,
480) -> JoinHandle<()> {
481    tokio::spawn(async move {
482        while let Some(event) = outbound_rx.recv().await {
483            let event_id = event.id.to_hex();
484            let verified = match VerifiedEvent::try_from(event) {
485                Ok(event) => event,
486                Err(err) => {
487                    tracing::warn!(event_id, "invalid outbound Nostr event: {err}");
488                    continue;
489                }
490            };
491            let source = EventSource::local_index("htree-relay");
492            if let Err(error) = cache.publish(verified.clone(), source.clone()).await {
493                tracing::warn!(event_id, %error, "failed to cache outbound Nostr event");
494            }
495            match client.publish(verified, source).await {
496                Ok(report) => tracing::debug!(
497                    event_id,
498                    accepted = report.accepted,
499                    "published Nostr event over decentralized pubsub"
500                ),
501                Err(err) => {
502                    tracing::warn!(event_id, "nostr decentralized pubsub publish failed: {err}")
503                }
504            }
505        }
506    })
507}
508
509pub fn fips_peer_ids_from_pubkeys(pubkeys: Vec<[u8; 32]>) -> Vec<String> {
510    pubkeys
511        .into_iter()
512        .filter_map(|pubkey| PublicKey::from_slice(&pubkey).ok())
513        .filter_map(|pubkey| pubkey.to_bech32().ok())
514        .collect()
515}
516
517pub fn daemon_fips_peer_configs(
518    config: &Config,
519    discovered_peer_ids: Vec<String>,
520) -> Vec<FipsPeerConfig> {
521    let mut seen = HashSet::new();
522    let mut peers = Vec::new();
523
524    for peer in &config.server.fips_peers {
525        let npub = peer.npub.trim().to_string();
526        if npub.is_empty() || !seen.insert(npub.clone()) {
527            continue;
528        }
529        let udp_addresses = peer
530            .udp_addresses
531            .iter()
532            .map(|addr| addr.trim().to_string())
533            .filter(|addr| !addr.is_empty())
534            .collect();
535        peers.push(FipsPeerConfig {
536            npub,
537            udp_addresses,
538        });
539    }
540
541    for peer_id in discovered_peer_ids {
542        let npub = peer_id.trim().to_string();
543        if npub.is_empty() || !seen.insert(npub.clone()) {
544            continue;
545        }
546        peers.push(FipsPeerConfig::new(npub));
547    }
548
549    peers
550}
551
552fn normalized_discovery_scope(scope: &str) -> String {
553    let scope = scope.trim();
554    if scope.is_empty() {
555        DEFAULT_FIPS_DISCOVERY_SCOPE.to_string()
556    } else {
557        scope.to_string()
558    }
559}
560
561#[cfg(test)]
562mod tests {
563    use super::*;
564    use hashtree_core::Store;
565    #[cfg(feature = "experimental-decentralized-pubsub")]
566    use nostr::{EventBuilder, Kind, Tag};
567    #[cfg(feature = "experimental-decentralized-pubsub")]
568    use nostr_pubsub::{PubsubProviderMode, QueryOptions};
569    use sha2::{Digest, Sha256};
570    use tokio::time::timeout;
571
572    struct RecordingStoreRoute {
573        store: Arc<hashtree_core::MemoryStore>,
574        requests: Arc<std::sync::Mutex<Vec<hashtree_core::BlobRequest>>>,
575    }
576
577    #[async_trait::async_trait]
578    impl BlobRoute for RecordingStoreRoute {
579        async fn route(
580            &self,
581            request: hashtree_core::BlobRequest,
582        ) -> Result<hashtree_core::BlobReply, hashtree_core::StoreError> {
583            self.requests.lock().unwrap().push(request);
584            StoreBlobRoute::new(self.store.clone()).route(request).await
585        }
586    }
587
588    #[test]
589    fn fips_peer_ids_from_pubkeys_encodes_npbus() {
590        let keys = nostr::Keys::generate();
591        let expected = keys.public_key().to_bech32().unwrap();
592
593        assert_eq!(
594            fips_peer_ids_from_pubkeys(vec![keys.public_key().to_bytes()]),
595            vec![expected]
596        );
597    }
598
599    #[test]
600    fn empty_discovery_scope_uses_hashtree_default() {
601        assert_eq!(
602            normalized_discovery_scope("  "),
603            DEFAULT_FIPS_DISCOVERY_SCOPE.to_string()
604        );
605    }
606
607    #[tokio::test]
608    async fn fips_local_only_event_transport_requires_local_provider() {
609        let mut config = Config::default();
610        config.nostr.event_transport = NostrEventTransport::FipsLocalOnly;
611
612        let error = start_daemon_nostr_provider(&config, None, None)
613            .await
614            .err()
615            .expect("missing local FIPS provider must fail");
616
617        assert!(error.to_string().contains("fips-local-only"));
618        assert!(error.to_string().contains("requires"));
619    }
620
621    #[tokio::test]
622    async fn daemon_rejects_invalid_configured_machine_rater() {
623        let temp = tempfile::tempdir().unwrap();
624        let store = Arc::new(HashtreeStore::new(temp.path().join("blobs")).unwrap());
625        let mut config = Config::default();
626        config.server.fips_discovery_scope =
627            format!("htree-invalid-rater-{}", uuid::Uuid::new_v4());
628        config.server.fips_relays = Some(Vec::new());
629        config.server.fips_websocket_seed_urls = Some(Vec::new());
630        config.server.enable_fips_udp = false;
631        config.server.enable_fips_webrtc = false;
632        config.server.enable_fips_lan_discovery = false;
633        config.server.fips_local_rendezvous_addr =
634            Some(reserve_local_rendezvous_addr().to_string());
635        config.nostr = toml::from_str(
636            r#"event_transport = "fips-local-only"
637fips_trusted_raters = ["invalid-key"]"#,
638        )
639        .unwrap();
640        let result =
641            start_daemon_fips_transport(&config, &nostr::Keys::generate(), store, Vec::new()).await;
642        if let Ok(Some(daemon)) = &result {
643            daemon.shutdown().await;
644        }
645        let error = result
646            .err()
647            .expect("invalid configured machine raters must fail startup");
648        assert!(format!("{error:#}").contains("trusted peer reputation rater"));
649    }
650
651    #[cfg(feature = "experimental-decentralized-pubsub")]
652    #[tokio::test]
653    async fn one_native_endpoint_routes_roots_and_trusted_events_through_transit() {
654        let scope = format!("htree-native-pubsub-{}", uuid::Uuid::new_v4());
655        let daemon_addr = reserve_udp_addr();
656        let rendezvous_addr = reserve_udp_addr();
657        let (remote_endpoint, remote_addr) = udp_endpoint(&scope).await;
658        // This node only routes FIPS datagrams: it has no pubsub client,
659        // matching subscription, Nostr cache, or application authorization.
660        let (transit_endpoint, transit_addr) = udp_endpoint(&scope).await;
661        let daemon_keys = nostr::Keys::generate();
662        let trusted_keys = nostr::Keys::generate();
663        let untrusted_keys = nostr::Keys::generate();
664        let outbound_keys = nostr::Keys::generate();
665        let temp = tempfile::tempdir().unwrap();
666        let store = Arc::new(HashtreeStore::new(temp.path().join("blobs")).unwrap());
667        let relay = trusted_test_relay(
668            temp.path(),
669            &[trusted_keys.public_key(), outbound_keys.public_key()],
670        )
671        .await;
672
673        let mut config = Config::default();
674        config.server.fips_discovery_scope = scope;
675        config.server.fips_relays = Some(Vec::new());
676        config.server.fips_udp_bind_addr = Some(daemon_addr.clone());
677        config.server.fips_local_rendezvous_addr = Some(rendezvous_addr);
678        config.server.enable_fips_udp = true;
679        config.server.enable_fips_webrtc = false;
680        config.server.enable_fips_lan_discovery = false;
681        config.server.fips_websocket_seed_urls = Some(Vec::new());
682        config.server.fips_request_timeout_ms = 2_000;
683        config.server.fips_peers = vec![crate::config::ConfiguredFipsPeer {
684            npub: transit_endpoint.local_peer_id.clone(),
685            udp_addresses: vec![transit_addr.clone()],
686        }];
687        config.nostr = toml::from_str(&format!(
688            "fips_trusted_raters = [\"{}\", \"{}\"]",
689            trusted_keys.public_key().to_hex(),
690            outbound_keys.public_key().to_bech32().unwrap()
691        ))
692        .unwrap();
693        config.nostr.enabled = true;
694        config.nostr.event_transport = NostrEventTransport::FipsLocalOnly;
695        config.nostr.decentralized_pubsub = true;
696        config.nostr.fips_pubsub_peers = vec![remote_endpoint.local_peer_id.clone()];
697
698        let cache = new_daemon_nostr_cache(store.store_arc());
699        let daemon = start_daemon_fips_transport(&config, &daemon_keys, store, Vec::new())
700            .await
701            .unwrap()
702            .expect("daemon FIPS endpoint");
703        assert_eq!(
704            daemon
705                .pubsub_client
706                .as_ref()
707                .unwrap()
708                .active_subscription_count()
709                .unwrap(),
710            2,
711            "the daemon must own discovery and one bounded peer reputation subscription"
712        );
713        let provider =
714            start_daemon_nostr_provider(&config, Some(&daemon), Some(Arc::clone(&cache)))
715                .await
716                .unwrap()
717                .expect("FIPS root provider");
718        assert_eq!(provider.mode(), PubsubProviderMode::Router);
719        let decentralized =
720            start_daemon_nostr_pubsub(&config, Some(&daemon), Some(relay.clone()), cache)
721                .await
722                .unwrap()
723                .expect("decentralized pubsub");
724
725        set_fips_peer_configs(
726            remote_endpoint.native_endpoint.as_ref(),
727            vec![FipsPeerConfig {
728                npub: transit_endpoint.local_peer_id.clone(),
729                udp_addresses: vec![transit_addr],
730            }],
731        )
732        .await
733        .unwrap();
734        set_fips_peer_configs(
735            transit_endpoint.native_endpoint.as_ref(),
736            vec![
737                FipsPeerConfig {
738                    npub: daemon.endpoint_npub.clone(),
739                    udp_addresses: vec![daemon_addr],
740                },
741                FipsPeerConfig {
742                    npub: remote_endpoint.local_peer_id.clone(),
743                    udp_addresses: vec![remote_addr],
744                },
745            ],
746        )
747        .await
748        .unwrap();
749        let remote_client = Arc::new(
750            FipsPubsubClient::start(
751                remote_endpoint.native_endpoint.clone(),
752                FipsPubsubClientOptions {
753                    query_timeout: Duration::from_secs(2),
754                    routed_peers: vec![daemon.endpoint_npub.clone()],
755                    ..Default::default()
756                },
757            )
758            .await
759            .unwrap(),
760        );
761
762        wait_for_native_peer(&daemon.endpoint, &transit_endpoint.local_peer_id).await;
763        wait_for_peer(&remote_endpoint, &transit_endpoint.local_peer_id).await;
764        timeout(Duration::from_secs(5), async {
765            while remote_client.peer_subscription_count().unwrap() == 0 {
766                tokio::time::sleep(Duration::from_millis(20)).await;
767            }
768        })
769        .await
770        .expect("daemon did not subscribe to the authenticated peer");
771
772        // FipsLocalOnly root publication and resolution use the same client
773        // that the decentralized relay bridge already owns.
774        let published_root = root_event(&outbound_keys, "published-root", "aa");
775        let mut published_root_rx = remote_client
776            .subscribe(vec![Filter::new().id(published_root.id)])
777            .await
778            .unwrap();
779        provider
780            .publish(
781                VerifiedEvent::try_from(published_root.clone()).unwrap(),
782                EventSource::local_index("root-publisher"),
783            )
784            .await
785            .unwrap();
786        let delivered_root = timeout(Duration::from_secs(5), published_root_rx.recv())
787            .await
788            .unwrap()
789            .expect("published root delivery");
790        assert_eq!(delivered_root.event.as_event().id, published_root.id);
791
792        let resolved_root = root_event(&trusted_keys, "resolved-root", "bb");
793        remote_client
794            .publish(
795                VerifiedEvent::try_from(resolved_root.clone()).unwrap(),
796                EventSource::local_index("remote-root"),
797            )
798            .await
799            .unwrap();
800        let resolution = provider
801            .query(
802                vec![Filter::new().id(resolved_root.id)],
803                QueryOptions { limit: Some(1) },
804            )
805            .await
806            .unwrap();
807        assert_eq!(resolution.events.len(), 1);
808        assert_eq!(resolution.events[0].event.as_event().id, resolved_root.id);
809
810        // The normal Nostr relay policy remains authoritative after wire-level
811        // signature verification: an untrusted author is not indexed, while a
812        // later trusted event proves the receive path advanced past it.
813        let rejected = EventBuilder::new(Kind::TextNote, "untrusted")
814            .sign_with_keys(&untrusted_keys)
815            .unwrap();
816        remote_client
817            .publish(
818                VerifiedEvent::try_from(rejected.clone()).unwrap(),
819                EventSource::local_index("remote-untrusted"),
820            )
821            .await
822            .unwrap();
823        let accepted = EventBuilder::new(Kind::TextNote, "trusted")
824            .sign_with_keys(&trusted_keys)
825            .unwrap();
826        remote_client
827            .publish(
828                VerifiedEvent::try_from(accepted.clone()).unwrap(),
829                EventSource::local_index("remote-trusted"),
830            )
831            .await
832            .unwrap();
833        wait_for_relay_event(&relay, accepted.id).await;
834        assert!(relay
835            .query_events(&Filter::new().id(rejected.id), 1)
836            .await
837            .is_empty());
838
839        // A trusted local relay publication travels back over the same FIPS
840        // event bus without opening another service receiver.
841        let outbound = EventBuilder::new(Kind::TextNote, "relay outbound")
842            .sign_with_keys(&outbound_keys)
843            .unwrap();
844        let mut outbound_rx = remote_client
845            .subscribe(vec![Filter::new().id(outbound.id)])
846            .await
847            .unwrap();
848        let (client_tx, _client_rx) = mpsc::unbounded_channel();
849        relay.register_client(1, client_tx, None).await;
850        relay
851            .handle_client_message(1, nostr::ClientMessage::event(outbound.clone()))
852            .await;
853        let delivered = timeout(Duration::from_secs(5), outbound_rx.recv())
854            .await
855            .unwrap()
856            .expect("outbound relay publication");
857        assert_eq!(delivered.event.as_event().id, outbound.id);
858
859        assert!(!daemon
860            .endpoint
861            .peers()
862            .await
863            .unwrap()
864            .iter()
865            .any(|peer| { peer.npub == remote_endpoint.local_peer_id && peer.connected }));
866        assert!(!remote_endpoint
867            .native_endpoint
868            .peers()
869            .await
870            .unwrap()
871            .iter()
872            .any(|peer| { peer.npub == daemon.endpoint_npub && peer.connected }));
873
874        decentralized.shutdown().await;
875        daemon.shutdown().await;
876        remote_endpoint.native_endpoint.shutdown().await.unwrap();
877        transit_endpoint.native_endpoint.shutdown().await.unwrap();
878    }
879
880    #[tokio::test]
881    async fn daemon_blob_resolver_advertises_and_serves_storage_router() {
882        let rendezvous_addr = reserve_local_rendezvous_addr();
883        let test_id = uuid::Uuid::new_v4();
884        let provider_scope = format!("htree-provider-test-{test_id}");
885        let observer_scope = format!("iris-drive-observer-test-{test_id}");
886        let provider_endpoint = local_only_endpoint(rendezvous_addr, &provider_scope).await;
887        let observer_endpoint = local_only_endpoint(rendezvous_addr, &observer_scope).await;
888        let temp = tempfile::tempdir().unwrap();
889        let store = Arc::new(HashtreeStore::new(temp.path()).unwrap());
890        let data = b"served directly from htree StorageRouter".to_vec();
891        let hash = Sha256::digest(&data).into();
892        store.store_arc().put(hash, data.clone()).await.unwrap();
893        let (blob_resolver, provider) = bind_daemon_blob_resolver(
894            &provider_endpoint,
895            store.store_arc(),
896            &[],
897            Duration::from_secs(1),
898        )
899        .await
900        .expect("bind htree provider");
901        let handle = DaemonFipsHandle {
902            endpoint: provider_endpoint.native_endpoint.clone(),
903            endpoint_npub: provider_endpoint.local_peer_id.clone(),
904            discovery_scope: provider_endpoint.discovery_scope.clone(),
905            pubsub_client: None,
906            blob_resolver,
907            blob_transport: Mutex::new(Some(provider)),
908        };
909
910        timeout(Duration::from_secs(10), async {
911            loop {
912                if provider_advertised(&observer_endpoint, &provider_endpoint.local_peer_id) {
913                    break;
914                }
915                tokio::time::sleep(Duration::from_millis(20)).await;
916            }
917        })
918        .await
919        .expect("htree provider capability did not converge");
920
921        let observer_store = Arc::new(hashtree_core::MemoryStore::new());
922        let observer_transport = Arc::new(
923            TcpBlobTransport::bind_route(
924                observer_endpoint.native_endpoint.clone(),
925                observer_store.clone(),
926                Arc::new(StoreBlobRoute::new(observer_store.clone())),
927            )
928            .await
929            .unwrap(),
930        );
931        let observer_fips_route = FipsBlobRoute::discovered(
932            observer_endpoint.native_endpoint.clone(),
933            observer_transport.clone(),
934            4,
935        )
936        .unwrap();
937        let observer_router = BlobRouter::new(
938            vec![BlobRouteEntry::new(
939                "same-host-fips",
940                Arc::new(observer_fips_route),
941            )],
942            Some(observer_store),
943            BlobRouterConfig::default(),
944        )
945        .unwrap();
946        assert_eq!(observer_router.get(&hash, None).await.unwrap(), Some(data));
947
948        drop(observer_router);
949        drop(observer_transport);
950        handle.shutdown().await;
951        timeout(Duration::from_secs(10), async {
952            while provider_advertised(&observer_endpoint, &provider_endpoint.local_peer_id) {
953                tokio::time::sleep(Duration::from_millis(20)).await;
954            }
955        })
956        .await
957        .expect("htree provider capability survived daemon shutdown");
958        drop(handle);
959        observer_endpoint.native_endpoint.shutdown().await.unwrap();
960    }
961
962    #[tokio::test]
963    async fn daemon_mesh_forwarding_observes_two_one_zero_and_exhaustion() {
964        let scope = format!("htree-inbound-forwarding-{}", uuid::Uuid::new_v4());
965        let (upstream_endpoint, upstream_addr) = udp_endpoint(&scope).await;
966        let (provider_endpoint, provider_addr) = udp_endpoint(&scope).await;
967        let (observer_endpoint, observer_addr) = udp_endpoint(&scope).await;
968        let temp = tempfile::tempdir().unwrap();
969        let provider_store = Arc::new(HashtreeStore::new(temp.path().join("provider")).unwrap());
970        let upstream_store = Arc::new(hashtree_core::MemoryStore::new());
971        let data = vec![47; 4096];
972        let wire_exchange_bytes =
973            data.len() + hashtree_core::BLOB_REQUEST_BYTES + hashtree_core::BLOB_REPLY_HEADER_BYTES;
974        let hash = Sha256::digest(&data).into();
975        upstream_store.put(hash, data.clone()).await.unwrap();
976        let upstream_requests = Arc::new(std::sync::Mutex::new(Vec::new()));
977        let upstream_transport = Arc::new(
978            TcpBlobTransport::bind_route(
979                upstream_endpoint.native_endpoint.clone(),
980                upstream_store.clone(),
981                Arc::new(RecordingStoreRoute {
982                    store: upstream_store,
983                    requests: upstream_requests.clone(),
984                }),
985            )
986            .await
987            .unwrap(),
988        );
989        let upstream_peer = FipsPeerConfig {
990            npub: upstream_endpoint.local_peer_id.clone(),
991            udp_addresses: vec![upstream_addr],
992        };
993        set_fips_peer_configs(
994            provider_endpoint.native_endpoint.as_ref(),
995            vec![
996                upstream_peer.clone(),
997                FipsPeerConfig {
998                    npub: observer_endpoint.local_peer_id.clone(),
999                    udp_addresses: vec![observer_addr],
1000                },
1001            ],
1002        )
1003        .await
1004        .unwrap();
1005        set_fips_peer_configs(
1006            upstream_endpoint.native_endpoint.as_ref(),
1007            vec![FipsPeerConfig {
1008                npub: provider_endpoint.local_peer_id.clone(),
1009                udp_addresses: vec![provider_addr.clone()],
1010            }],
1011        )
1012        .await
1013        .unwrap();
1014        set_fips_peer_configs(
1015            observer_endpoint.native_endpoint.as_ref(),
1016            vec![FipsPeerConfig {
1017                npub: provider_endpoint.local_peer_id.clone(),
1018                udp_addresses: vec![provider_addr],
1019            }],
1020        )
1021        .await
1022        .unwrap();
1023        let (provider_resolver, provider_transport) = bind_daemon_blob_resolver(
1024            &provider_endpoint,
1025            provider_store.store_arc(),
1026            &[upstream_peer],
1027            Duration::from_secs(2),
1028        )
1029        .await
1030        .unwrap();
1031        let observer_store = Arc::new(hashtree_core::MemoryStore::new());
1032        let observer_transport = Arc::new(
1033            TcpBlobTransport::bind_route(
1034                observer_endpoint.native_endpoint.clone(),
1035                observer_store.clone(),
1036                Arc::new(StoreBlobRoute::new(observer_store)),
1037            )
1038            .await
1039            .unwrap(),
1040        );
1041        let observer_fips_route = FipsBlobRoute::explicit(
1042            observer_transport.clone(),
1043            vec![PeerIdentity::from_npub(&provider_endpoint.local_peer_id).unwrap()],
1044            1,
1045        )
1046        .unwrap();
1047        let observer_route = MeshForwardingRoute::new(Arc::new(observer_fips_route));
1048
1049        wait_for_native_peer(
1050            provider_endpoint.native_endpoint.as_ref(),
1051            &upstream_endpoint.local_peer_id,
1052        )
1053        .await;
1054        wait_for_native_peer(
1055            observer_endpoint.native_endpoint.as_ref(),
1056            &provider_endpoint.local_peer_id,
1057        )
1058        .await;
1059        assert_eq!(
1060            observer_route
1061                .route(hashtree_core::BlobRequest { hash, htl: 0 })
1062                .await
1063                .unwrap(),
1064            hashtree_core::BlobReply::NoResult,
1065        );
1066        assert!(upstream_requests.lock().unwrap().is_empty());
1067        assert_eq!(
1068            observer_route
1069                .route(hashtree_core::BlobRequest { hash, htl: 1 })
1070                .await
1071                .unwrap(),
1072            hashtree_core::BlobReply::NoResult,
1073        );
1074        assert!(
1075            upstream_requests.lock().unwrap().is_empty(),
1076            "HTL 1 reaches the intermediate daemon as 0 and cannot be forwarded"
1077        );
1078        assert_eq!(
1079            observer_route
1080                .route(hashtree_core::BlobRequest { hash, htl: 2 })
1081                .await
1082                .unwrap(),
1083            hashtree_core::BlobReply::Data(data.clone()),
1084        );
1085        assert_eq!(
1086            upstream_requests.lock().unwrap().as_slice(),
1087            &[hashtree_core::BlobRequest { hash, htl: 0 }],
1088            "three Hashtree nodes must observe the two-to-one-to-zero forwarding budget while each FIPS carrier preserves its request",
1089        );
1090
1091        assert_eq!(
1092            observer_route
1093                .route(hashtree_core::BlobRequest { hash, htl: 2 })
1094                .await
1095                .unwrap(),
1096            hashtree_core::BlobReply::Data(data),
1097        );
1098        assert_eq!(
1099            upstream_requests.lock().unwrap().len(),
1100            1,
1101            "the intermediate daemon must serve its verified cache before forwarding again"
1102        );
1103        println!("daemon warm read: cold_blob_wire_bytes={} warm_blob_wire_bytes={} upstream_repeat_bytes=0", 2 * wire_exchange_bytes, wire_exchange_bytes);
1104
1105        drop(observer_route);
1106        drop(observer_transport);
1107        drop(provider_resolver);
1108        drop(provider_transport);
1109        drop(upstream_transport);
1110        observer_endpoint.native_endpoint.shutdown().await.unwrap();
1111        provider_endpoint.native_endpoint.shutdown().await.unwrap();
1112        upstream_endpoint.native_endpoint.shutdown().await.unwrap();
1113    }
1114
1115    fn provider_advertised(endpoint: &BoundFipsEndpoint, npub: &str) -> bool {
1116        endpoint
1117            .native_endpoint
1118            .local_instance_advertisements()
1119            .unwrap()
1120            .iter()
1121            .any(|advert| {
1122                advert.npub == npub
1123                    && advert
1124                        .capability(hashtree_fips_transport::TCP_BLOB_CAPABILITY)
1125                        .and_then(|capability| capability.fsp_port)
1126                        == Some(hashtree_fips_transport::TCP_BLOB_SERVICE_PORT)
1127            })
1128    }
1129
1130    async fn local_only_endpoint(
1131        rendezvous_addr: std::net::SocketAddrV4,
1132        scope: &str,
1133    ) -> hashtree_fips_transport::BoundFipsEndpoint {
1134        let keys = nostr::Keys::generate();
1135        let mut options = FipsEndpointOptions::new(keys.secret_key().to_bech32().unwrap());
1136        options.discovery_scope = scope.to_string();
1137        options.enable_udp = false;
1138        options.enable_webrtc = false;
1139        options.enable_local_rendezvous = true;
1140        options.enable_lan_discovery = false;
1141        options.share_local_candidates = false;
1142        bind_fips_endpoint_at_local_rendezvous(options, rendezvous_addr)
1143            .await
1144            .unwrap()
1145    }
1146
1147    async fn udp_endpoint(scope: &str) -> (BoundFipsEndpoint, String) {
1148        let keys = nostr::Keys::generate();
1149        let mut options = FipsEndpointOptions::new(keys.secret_key().to_bech32().unwrap());
1150        options.discovery_scope = scope.to_string();
1151        options.enable_udp = true;
1152        options.udp_bind_addr = Some("127.0.0.1:0".to_string());
1153        options.enable_webrtc = false;
1154        options.enable_local_rendezvous = false;
1155        options.enable_lan_discovery = false;
1156        options.share_local_candidates = false;
1157        let endpoint = bind_fips_endpoint(options).await.unwrap();
1158        let addrs = endpoint
1159            .native_endpoint
1160            .bound_udp_listen_addrs()
1161            .await
1162            .unwrap();
1163        let [addr] = addrs.as_slice() else {
1164            panic!("expected exactly one bound UDP listener, got {addrs:?}");
1165        };
1166        assert!(addr.ip().is_loopback());
1167        assert_ne!(addr.port(), 0);
1168        (endpoint, addr.to_string())
1169    }
1170
1171    #[cfg(feature = "experimental-decentralized-pubsub")]
1172    fn reserve_udp_addr() -> String {
1173        let socket = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
1174        socket.local_addr().unwrap().to_string()
1175    }
1176
1177    fn reserve_local_rendezvous_addr() -> std::net::SocketAddrV4 {
1178        let socket = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
1179        match socket.local_addr().unwrap() {
1180            std::net::SocketAddr::V4(addr) => addr,
1181            std::net::SocketAddr::V6(_) => unreachable!("loopback IPv4 bind returned IPv6"),
1182        }
1183    }
1184
1185    #[cfg(feature = "experimental-decentralized-pubsub")]
1186    async fn wait_for_peer(endpoint: &BoundFipsEndpoint, npub: &str) {
1187        wait_for_native_peer(endpoint.native_endpoint.as_ref(), npub).await;
1188    }
1189
1190    async fn wait_for_native_peer(endpoint: &FipsEndpoint, npub: &str) {
1191        let result = timeout(Duration::from_secs(10), async {
1192            loop {
1193                if endpoint
1194                    .peers()
1195                    .await
1196                    .unwrap()
1197                    .iter()
1198                    .any(|peer| peer.npub == npub && peer.connected)
1199                {
1200                    break;
1201                }
1202                tokio::time::sleep(Duration::from_millis(20)).await;
1203            }
1204        })
1205        .await;
1206        if result.is_err() {
1207            panic!(
1208                "FIPS peer {npub} did not connect; peers={:?}",
1209                endpoint.peers().await
1210            );
1211        }
1212    }
1213
1214    #[cfg(feature = "experimental-decentralized-pubsub")]
1215    async fn trusted_test_relay(
1216        root: &std::path::Path,
1217        allowed: &[nostr::PublicKey],
1218    ) -> Arc<NostrRelay> {
1219        let graph_dir = root.join("graph");
1220        let data_dir = root.join("nostr");
1221        std::fs::create_dir_all(&data_dir).unwrap();
1222        let graph = {
1223            let _guard = crate::socialgraph::test_lock().await;
1224            crate::socialgraph::open_test_social_graph_store_with_mapsize(
1225                &graph_dir,
1226                Some(128 * 1024 * 1024),
1227            )
1228            .unwrap()
1229        };
1230        let backend: Arc<dyn crate::socialgraph::SocialGraphBackend> = graph;
1231        let allowed = allowed
1232            .iter()
1233            .map(|pubkey| pubkey.to_hex())
1234            .collect::<HashSet<_>>();
1235        let access = Arc::new(crate::socialgraph::SocialGraphAccessControl::new(
1236            Arc::clone(&backend),
1237            0,
1238            allowed.clone(),
1239        ));
1240        Arc::new(
1241            NostrRelay::new(
1242                backend,
1243                data_dir,
1244                allowed,
1245                Some(access),
1246                crate::nostr_relay::NostrRelayConfig {
1247                    spambox_db_max_bytes: 0,
1248                    ..Default::default()
1249                },
1250            )
1251            .unwrap(),
1252        )
1253    }
1254
1255    #[cfg(feature = "experimental-decentralized-pubsub")]
1256    fn root_event(keys: &nostr::Keys, name: &str, hash: &str) -> nostr::Event {
1257        EventBuilder::new(Kind::Custom(30_078), hash)
1258            .tags([
1259                Tag::parse(["d", name]).unwrap(),
1260                Tag::parse(["l", "hashtree"]).unwrap(),
1261                Tag::parse(["hash", hash]).unwrap(),
1262            ])
1263            .sign_with_keys(keys)
1264            .unwrap()
1265    }
1266
1267    #[cfg(feature = "experimental-decentralized-pubsub")]
1268    async fn wait_for_relay_event(relay: &NostrRelay, id: nostr::EventId) {
1269        timeout(Duration::from_secs(5), async {
1270            loop {
1271                if relay
1272                    .query_events(&Filter::new().id(id), 1)
1273                    .await
1274                    .iter()
1275                    .any(|event| event.id == id)
1276                {
1277                    break;
1278                }
1279                tokio::time::sleep(Duration::from_millis(20)).await;
1280            }
1281        })
1282        .await
1283        .expect("trusted decentralized event was not indexed");
1284    }
1285
1286    #[test]
1287    fn daemon_fips_peer_configs_prefer_configured_peers() {
1288        let mut config = Config::default();
1289        config.server.fips_peers = vec![
1290            crate::config::ConfiguredFipsPeer {
1291                npub: " origin ".to_string(),
1292                udp_addresses: vec![" udp:192.0.2.10:2121 ".to_string(), " ".to_string()],
1293            },
1294            crate::config::ConfiguredFipsPeer {
1295                npub: "origin".to_string(),
1296                udp_addresses: vec!["udp:ignored:2121".to_string()],
1297            },
1298        ];
1299
1300        let peers = daemon_fips_peer_configs(
1301            &config,
1302            vec![
1303                "origin".to_string(),
1304                " followed ".to_string(),
1305                " ".to_string(),
1306            ],
1307        );
1308
1309        assert_eq!(
1310            peers,
1311            vec![
1312                FipsPeerConfig {
1313                    npub: "origin".to_string(),
1314                    udp_addresses: vec!["udp:192.0.2.10:2121".to_string()],
1315                },
1316                FipsPeerConfig::new("followed"),
1317            ]
1318        );
1319    }
1320
1321    #[test]
1322    fn blob_carrier_deadline_outlives_the_complete_resolver_budget() {
1323        for resolver_timeout in [Duration::from_millis(1), Duration::from_secs(20)] {
1324            assert!(blob_transport_config(resolver_timeout).idle_timeout > resolver_timeout);
1325        }
1326    }
1327
1328    #[cfg(feature = "experimental-decentralized-pubsub")]
1329    #[test]
1330    fn daemon_decentralized_pubsub_requires_config_fips_and_relay() {
1331        let mut config = Config::default();
1332        assert!(!daemon_decentralized_pubsub_ready(&config, true, true));
1333
1334        config.nostr.decentralized_pubsub = true;
1335        assert!(daemon_decentralized_pubsub_ready(&config, true, true));
1336        assert!(!daemon_decentralized_pubsub_ready(&config, false, true));
1337        assert!(!daemon_decentralized_pubsub_ready(&config, true, false));
1338
1339        config.nostr.enabled = false;
1340        assert!(!daemon_decentralized_pubsub_ready(&config, true, true));
1341    }
1342}