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