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