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