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