1mod 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 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 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 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 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 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 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}