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