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