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