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