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 = 0;
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 inbound_route: Arc<dyn BlobRoute> = Arc::new(StoreBlobRoute::new(store.clone()));
195 let transport = Arc::new(
196 TcpBlobTransport::bind_advertised_route_with_config(
197 endpoint.native_endpoint.clone(),
198 store.clone(),
199 inbound_route,
200 blob_transport_config(request_timeout),
201 DAEMON_SAME_HOST_PROVIDER_PRIORITY,
202 )
203 .await
204 .context("Failed to advertise htree's blob resolver")?,
205 );
206 let peer_identities: Vec<_> = peers
207 .iter()
208 .filter_map(|peer| PeerIdentity::from_npub(&peer.npub).ok())
209 .collect();
210 let mut routes = vec![BlobRouteEntry::new(
211 "configured-store",
212 Arc::new(StoreBlobRoute::new(store.clone())),
213 )];
214 if !peer_identities.is_empty() {
215 let max_provider_attempts = peer_identities.len().min(4);
216 let fips_route =
217 FipsBlobRoute::explicit(transport.clone(), peer_identities, max_provider_attempts)
218 .map_err(anyhow::Error::msg)
219 .context("Failed to configure the daemon FIPS blob route")?;
220 routes.push(BlobRouteEntry::new(
221 "configured-fips-peers",
222 Arc::new(fips_route),
223 ));
224 }
225 let resolver = Arc::new(
226 BlobRouter::new(
227 routes,
228 Some(store),
229 BlobRouterConfig {
230 request_timeout,
231 ..Default::default()
232 },
233 )
234 .map_err(anyhow::Error::msg)
235 .context("Failed to configure the daemon blob router")?,
236 );
237 Ok((resolver, transport))
238}
239
240fn blob_transport_config(resolver_timeout: Duration) -> TcpBlobTransportConfig {
241 TcpBlobTransportConfig {
242 idle_timeout: resolver_timeout.saturating_add(BLOB_RESOLVER_REPLY_MARGIN),
243 }
244}
245
246pub async fn start_daemon_nostr_provider(
247 config: &Config,
248 fips_handle: Option<&DaemonFipsHandle>,
249) -> Result<Option<Arc<dyn nostr_pubsub::PubsubProvider>>> {
250 match config.nostr.event_transport {
251 NostrEventTransport::Relay => {
252 let relays = config.nostr.active_relays();
253 if relays.is_empty() {
254 return Ok(None);
255 }
256 let event_bus = nostr_pubsub_relay::RelayEventBus::new(
257 relays,
258 Duration::from_millis(config.server.fips_request_timeout_ms.max(1)),
259 )
260 .await
261 .context("Failed to start Nostr relay event provider")?;
262 Ok(Some(Arc::new(event_bus)))
263 }
264 NostrEventTransport::FipsLocalOnly => fips_handle
265 .and_then(|handle| handle.nostr_provider.clone())
266 .map(Some)
267 .ok_or_else(|| {
268 anyhow::anyhow!(
269 "nostr.event_transport=fips-local-only requires the local FIPS Nostr pubsub provider"
270 )
271 }),
272 }
273}
274
275#[cfg(feature = "experimental-decentralized-pubsub")]
276pub async fn start_daemon_nostr_pubsub(
277 config: &Config,
278 fips_handle: Option<&DaemonFipsHandle>,
279 relay: Option<Arc<NostrRelay>>,
280) -> Result<Option<Arc<DaemonNostrPubsubHandle>>> {
281 if !daemon_decentralized_pubsub_ready(config, fips_handle.is_some(), relay.is_some()) {
282 return Ok(None);
283 }
284
285 let fips_handle = fips_handle.expect("checked by daemon_decentralized_pubsub_ready");
286 let relay = relay.expect("checked by daemon_decentralized_pubsub_ready");
287 let client = fips_handle.pubsub_client.clone().ok_or_else(|| {
288 anyhow::anyhow!("decentralized Nostr pubsub requires the shared FIPS pubsub client")
289 })?;
290
291 let (outbound_tx, outbound_rx) = mpsc::unbounded_channel();
292 relay.set_decentralized_pubsub_sender(Some(outbound_tx));
293 let ingest_task = spawn_daemon_nostr_pubsub_ingest(
294 Arc::clone(&client),
295 Arc::clone(&fips_handle.endpoint),
296 Arc::clone(&relay),
297 );
298 let outbound_task = spawn_daemon_nostr_pubsub_outbound(Arc::clone(&client), outbound_rx);
299
300 Ok(Some(Arc::new(DaemonNostrPubsubHandle {
301 _client: client,
302 relay,
303 ingest_task,
304 outbound_task,
305 })))
306}
307
308#[cfg(feature = "experimental-decentralized-pubsub")]
309fn daemon_decentralized_pubsub_ready(config: &Config, has_fips: bool, has_relay: bool) -> bool {
310 config.nostr.decentralized_pubsub_enabled() && has_fips && has_relay
311}
312
313#[cfg(feature = "experimental-decentralized-pubsub")]
314fn spawn_daemon_nostr_pubsub_ingest(
315 client: Arc<FipsPubsubClient>,
316 endpoint: Arc<FipsEndpoint>,
317 relay: Arc<NostrRelay>,
318) -> JoinHandle<()> {
319 tokio::spawn(async move {
320 loop {
321 let subscribed_peers = connected_fips_peer_ids(&endpoint).await;
322 if subscribed_peers.is_empty() {
323 tokio::time::sleep(Duration::from_millis(250)).await;
324 continue;
325 }
326 let mut subscription = match client.subscribe(vec![Filter::new()]).await {
327 Ok(subscription) => subscription,
328 Err(err) => {
329 tracing::debug!("waiting to subscribe to FIPS Nostr peers: {err}");
330 tokio::time::sleep(Duration::from_millis(250)).await;
331 continue;
332 }
333 };
334 loop {
335 tokio::select! {
336 delivery = subscription.recv() => {
337 let Some(delivery) = delivery else {
338 break;
339 };
340 let source = delivery.source.id.as_str().to_string();
341 let event = delivery.event.into_event();
342 let event_id = event.id.to_hex();
343 match relay.ingest_peer_event_silent(event).await {
344 Ok(true) => tracing::debug!(
345 source,
346 event_id,
347 "ingested decentralized Nostr pubsub event"
348 ),
349 Ok(false) => {}
350 Err(err) => tracing::warn!(
351 source,
352 event_id,
353 "nostr decentralized pubsub ingest failed: {err:#}"
354 ),
355 }
356 }
357 () = tokio::time::sleep(Duration::from_millis(500)) => {
358 if connected_fips_peer_ids(&endpoint).await != subscribed_peers {
359 break;
360 }
361 }
362 }
363 }
364 }
365 })
366}
367
368#[cfg(feature = "experimental-decentralized-pubsub")]
369async fn connected_fips_peer_ids(endpoint: &FipsEndpoint) -> Vec<String> {
370 let mut peers = endpoint
371 .peers()
372 .await
373 .unwrap_or_default()
374 .into_iter()
375 .filter(|peer| peer.connected)
376 .map(|peer| peer.npub)
377 .collect::<Vec<_>>();
378 peers.sort_unstable();
379 peers.dedup();
380 peers
381}
382
383#[cfg(feature = "experimental-decentralized-pubsub")]
384fn spawn_daemon_nostr_pubsub_outbound(
385 client: Arc<FipsPubsubClient>,
386 mut outbound_rx: mpsc::UnboundedReceiver<nostr::Event>,
387) -> JoinHandle<()> {
388 tokio::spawn(async move {
389 while let Some(event) = outbound_rx.recv().await {
390 let event_id = event.id.to_hex();
391 let verified = match VerifiedEvent::try_from(event) {
392 Ok(event) => event,
393 Err(err) => {
394 tracing::warn!(event_id, "invalid outbound Nostr event: {err}");
395 continue;
396 }
397 };
398 match client
399 .publish(verified, EventSource::local_index("htree-relay"))
400 .await
401 {
402 Ok(report) => tracing::debug!(
403 event_id,
404 accepted = report.accepted,
405 "published Nostr event over decentralized pubsub"
406 ),
407 Err(err) => {
408 tracing::warn!(event_id, "nostr decentralized pubsub publish failed: {err}")
409 }
410 }
411 }
412 })
413}
414
415pub fn fips_peer_ids_from_pubkeys(pubkeys: Vec<[u8; 32]>) -> Vec<String> {
416 pubkeys
417 .into_iter()
418 .filter_map(|pubkey| PublicKey::from_slice(&pubkey).ok())
419 .filter_map(|pubkey| pubkey.to_bech32().ok())
420 .collect()
421}
422
423pub fn daemon_fips_peer_configs(
424 config: &Config,
425 discovered_peer_ids: Vec<String>,
426) -> Vec<FipsPeerConfig> {
427 let mut seen = HashSet::new();
428 let mut peers = Vec::new();
429
430 for peer in &config.server.fips_peers {
431 let npub = peer.npub.trim().to_string();
432 if npub.is_empty() || !seen.insert(npub.clone()) {
433 continue;
434 }
435 let udp_addresses = peer
436 .udp_addresses
437 .iter()
438 .map(|addr| addr.trim().to_string())
439 .filter(|addr| !addr.is_empty())
440 .collect();
441 peers.push(FipsPeerConfig {
442 npub,
443 udp_addresses,
444 });
445 }
446
447 for peer_id in discovered_peer_ids {
448 let npub = peer_id.trim().to_string();
449 if npub.is_empty() || !seen.insert(npub.clone()) {
450 continue;
451 }
452 peers.push(FipsPeerConfig::new(npub));
453 }
454
455 peers
456}
457
458fn normalized_discovery_scope(scope: &str) -> String {
459 let scope = scope.trim();
460 if scope.is_empty() {
461 DEFAULT_FIPS_DISCOVERY_SCOPE.to_string()
462 } else {
463 scope.to_string()
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470 use hashtree_core::Store;
471 #[cfg(feature = "experimental-decentralized-pubsub")]
472 use nostr::{EventBuilder, Kind, Tag};
473 #[cfg(feature = "experimental-decentralized-pubsub")]
474 use nostr_pubsub::{PubsubProviderMode, QueryOptions};
475 use sha2::{Digest, Sha256};
476 use tokio::time::timeout;
477
478 #[test]
479 fn fips_peer_ids_from_pubkeys_encodes_npbus() {
480 let keys = nostr::Keys::generate();
481 let expected = keys.public_key().to_bech32().unwrap();
482
483 assert_eq!(
484 fips_peer_ids_from_pubkeys(vec![keys.public_key().to_bytes()]),
485 vec![expected]
486 );
487 }
488
489 #[test]
490 fn empty_discovery_scope_uses_hashtree_default() {
491 assert_eq!(
492 normalized_discovery_scope(" "),
493 DEFAULT_FIPS_DISCOVERY_SCOPE.to_string()
494 );
495 }
496
497 #[tokio::test]
498 async fn fips_local_only_event_transport_requires_local_provider() {
499 let mut config = Config::default();
500 config.nostr.event_transport = NostrEventTransport::FipsLocalOnly;
501
502 let error = start_daemon_nostr_provider(&config, None)
503 .await
504 .err()
505 .expect("missing local FIPS provider must fail");
506
507 assert!(error.to_string().contains("fips-local-only"));
508 assert!(error.to_string().contains("requires"));
509 }
510
511 #[cfg(feature = "experimental-decentralized-pubsub")]
512 #[tokio::test]
513 async fn one_native_endpoint_serves_roots_and_trusted_decentralized_events() {
514 let scope = format!("htree-native-pubsub-{}", uuid::Uuid::new_v4());
515 let daemon_addr = reserve_udp_addr();
516 let rendezvous_addr = reserve_udp_addr();
517 let (remote_endpoint, remote_addr) = udp_endpoint(&scope).await;
518 let daemon_keys = nostr::Keys::generate();
519 let trusted_keys = nostr::Keys::generate();
520 let untrusted_keys = nostr::Keys::generate();
521 let outbound_keys = nostr::Keys::generate();
522 let temp = tempfile::tempdir().unwrap();
523 let store = Arc::new(HashtreeStore::new(temp.path().join("blobs")).unwrap());
524 let relay = trusted_test_relay(
525 temp.path(),
526 &[trusted_keys.public_key(), outbound_keys.public_key()],
527 )
528 .await;
529
530 let mut config = Config::default();
531 config.server.fips_discovery_scope = scope;
532 config.server.fips_relays = Some(Vec::new());
533 config.server.fips_udp_bind_addr = Some(daemon_addr.clone());
534 config.server.fips_local_rendezvous_addr = Some(rendezvous_addr);
535 config.server.enable_fips_udp = true;
536 config.server.enable_fips_webrtc = false;
537 config.server.fips_request_timeout_ms = 2_000;
538 config.server.fips_peers = vec![crate::config::ConfiguredFipsPeer {
539 npub: remote_endpoint.local_peer_id.clone(),
540 udp_addresses: vec![remote_addr],
541 }];
542 config.nostr.enabled = true;
543 config.nostr.event_transport = NostrEventTransport::FipsLocalOnly;
544 config.nostr.decentralized_pubsub = true;
545
546 let daemon = start_daemon_fips_transport(&config, &daemon_keys, store, Vec::new())
547 .await
548 .unwrap()
549 .expect("daemon FIPS endpoint");
550 let provider = start_daemon_nostr_provider(&config, Some(&daemon))
551 .await
552 .unwrap()
553 .expect("FIPS root provider");
554 assert_eq!(provider.mode(), PubsubProviderMode::LocalOnly);
555 let decentralized = start_daemon_nostr_pubsub(&config, Some(&daemon), Some(relay.clone()))
556 .await
557 .unwrap()
558 .expect("decentralized pubsub");
559
560 set_fips_peer_configs(
561 remote_endpoint.native_endpoint.as_ref(),
562 vec![FipsPeerConfig {
563 npub: daemon.endpoint_npub.clone(),
564 udp_addresses: vec![daemon_addr],
565 }],
566 )
567 .await
568 .unwrap();
569 let remote_client = Arc::new(
570 FipsPubsubClient::start(
571 remote_endpoint.native_endpoint.clone(),
572 FipsPubsubClientOptions {
573 query_timeout: Duration::from_secs(2),
574 ..Default::default()
575 },
576 )
577 .await
578 .unwrap(),
579 );
580
581 wait_for_native_peer(&daemon.endpoint, &remote_endpoint.local_peer_id).await;
582 wait_for_peer(&remote_endpoint, &daemon.endpoint_npub).await;
583 timeout(Duration::from_secs(5), async {
584 while remote_client.peer_subscription_count().unwrap() == 0 {
585 tokio::time::sleep(Duration::from_millis(20)).await;
586 }
587 })
588 .await
589 .expect("daemon did not subscribe to the authenticated peer");
590
591 let published_root = root_event(&outbound_keys, "published-root", "aa");
594 let mut published_root_rx = remote_client
595 .subscribe(vec![Filter::new().id(published_root.id)])
596 .await
597 .unwrap();
598 provider
599 .publish(
600 VerifiedEvent::try_from(published_root.clone()).unwrap(),
601 EventSource::local_index("root-publisher"),
602 )
603 .await
604 .unwrap();
605 let delivered_root = timeout(Duration::from_secs(5), published_root_rx.recv())
606 .await
607 .unwrap()
608 .expect("published root delivery");
609 assert_eq!(delivered_root.event.as_event().id, published_root.id);
610
611 let resolved_root = root_event(&trusted_keys, "resolved-root", "bb");
612 remote_client
613 .publish(
614 VerifiedEvent::try_from(resolved_root.clone()).unwrap(),
615 EventSource::local_index("remote-root"),
616 )
617 .await
618 .unwrap();
619 let resolution = provider
620 .query(
621 vec![Filter::new().id(resolved_root.id)],
622 QueryOptions { limit: Some(1) },
623 )
624 .await
625 .unwrap();
626 assert_eq!(resolution.events.len(), 1);
627 assert_eq!(resolution.events[0].event.as_event().id, resolved_root.id);
628
629 let rejected = EventBuilder::new(Kind::TextNote, "untrusted")
633 .sign_with_keys(&untrusted_keys)
634 .unwrap();
635 remote_client
636 .publish(
637 VerifiedEvent::try_from(rejected.clone()).unwrap(),
638 EventSource::local_index("remote-untrusted"),
639 )
640 .await
641 .unwrap();
642 let accepted = EventBuilder::new(Kind::TextNote, "trusted")
643 .sign_with_keys(&trusted_keys)
644 .unwrap();
645 remote_client
646 .publish(
647 VerifiedEvent::try_from(accepted.clone()).unwrap(),
648 EventSource::local_index("remote-trusted"),
649 )
650 .await
651 .unwrap();
652 wait_for_relay_event(&relay, accepted.id).await;
653 assert!(relay
654 .query_events(&Filter::new().id(rejected.id), 1)
655 .await
656 .is_empty());
657
658 let outbound = EventBuilder::new(Kind::TextNote, "relay outbound")
661 .sign_with_keys(&outbound_keys)
662 .unwrap();
663 let mut outbound_rx = remote_client
664 .subscribe(vec![Filter::new().id(outbound.id)])
665 .await
666 .unwrap();
667 let (client_tx, _client_rx) = mpsc::unbounded_channel();
668 relay.register_client(1, client_tx, None).await;
669 relay
670 .handle_client_message(1, nostr::ClientMessage::event(outbound.clone()))
671 .await;
672 let delivered = timeout(Duration::from_secs(5), outbound_rx.recv())
673 .await
674 .unwrap()
675 .expect("outbound relay publication");
676 assert_eq!(delivered.event.as_event().id, outbound.id);
677
678 decentralized.shutdown();
679 daemon.shutdown().await;
680 remote_endpoint.native_endpoint.shutdown().await.unwrap();
681 }
682
683 #[tokio::test]
684 async fn daemon_blob_resolver_advertises_and_serves_storage_router() {
685 let provider_endpoint = local_only_endpoint("htree-provider-test").await;
686 let observer_endpoint = local_only_endpoint("iris-drive-observer-test").await;
687 let temp = tempfile::tempdir().unwrap();
688 let store = Arc::new(HashtreeStore::new(temp.path()).unwrap());
689 let data = b"served directly from htree StorageRouter".to_vec();
690 let hash = Sha256::digest(&data).into();
691 store.store_arc().put(hash, data.clone()).await.unwrap();
692 let (blob_resolver, provider) = bind_daemon_blob_resolver(
693 &provider_endpoint,
694 store.store_arc(),
695 &[],
696 Duration::from_secs(1),
697 )
698 .await
699 .expect("bind htree provider");
700 let handle = DaemonFipsHandle {
701 endpoint: provider_endpoint.native_endpoint.clone(),
702 endpoint_npub: provider_endpoint.local_peer_id.clone(),
703 discovery_scope: provider_endpoint.discovery_scope.clone(),
704 nostr_provider: None,
705 pubsub_client: None,
706 blob_resolver,
707 blob_transport: Mutex::new(Some(provider)),
708 relay_adapter: Mutex::new(None),
709 };
710
711 timeout(Duration::from_secs(10), async {
712 loop {
713 if provider_advertised(&observer_endpoint, &provider_endpoint.local_peer_id) {
714 break;
715 }
716 tokio::time::sleep(Duration::from_millis(20)).await;
717 }
718 })
719 .await
720 .expect("htree provider capability did not converge");
721
722 let observer_store = Arc::new(hashtree_core::MemoryStore::new());
723 let observer_transport = Arc::new(
724 TcpBlobTransport::bind_route(
725 observer_endpoint.native_endpoint.clone(),
726 observer_store.clone(),
727 Arc::new(StoreBlobRoute::new(observer_store.clone())),
728 )
729 .await
730 .unwrap(),
731 );
732 let observer_fips_route = FipsBlobRoute::discovered(
733 observer_endpoint.native_endpoint.clone(),
734 observer_transport.clone(),
735 4,
736 )
737 .unwrap();
738 let observer_router = BlobRouter::new(
739 vec![BlobRouteEntry::new(
740 "same-host-fips",
741 Arc::new(observer_fips_route),
742 )],
743 Some(observer_store),
744 BlobRouterConfig::default(),
745 )
746 .unwrap();
747 assert_eq!(observer_router.get(&hash, None).await.unwrap(), Some(data));
748
749 drop(observer_router);
750 drop(observer_transport);
751 handle.shutdown().await;
752 timeout(Duration::from_secs(10), async {
753 while provider_advertised(&observer_endpoint, &provider_endpoint.local_peer_id) {
754 tokio::time::sleep(Duration::from_millis(20)).await;
755 }
756 })
757 .await
758 .expect("htree provider capability survived daemon shutdown");
759 drop(handle);
760 observer_endpoint.native_endpoint.shutdown().await.unwrap();
761 }
762
763 fn provider_advertised(endpoint: &BoundFipsEndpoint, npub: &str) -> bool {
764 endpoint
765 .native_endpoint
766 .local_instance_advertisements()
767 .unwrap()
768 .iter()
769 .any(|advert| {
770 advert.npub == npub
771 && advert
772 .capability(hashtree_fips_transport::TCP_BLOB_CAPABILITY)
773 .and_then(|capability| capability.fsp_port)
774 == Some(hashtree_fips_transport::TCP_BLOB_SERVICE_PORT)
775 })
776 }
777
778 async fn local_only_endpoint(scope: &str) -> hashtree_fips_transport::BoundFipsEndpoint {
779 let keys = nostr::Keys::generate();
780 let mut options = FipsEndpointOptions::new(keys.secret_key().to_bech32().unwrap());
781 options.discovery_scope = scope.to_string();
782 options.enable_udp = false;
783 options.enable_webrtc = false;
784 options.enable_local_rendezvous = true;
785 options.enable_lan_discovery = false;
786 options.share_local_candidates = false;
787 bind_fips_endpoint(options).await.unwrap()
788 }
789
790 #[cfg(feature = "experimental-decentralized-pubsub")]
791 async fn udp_endpoint(scope: &str) -> (BoundFipsEndpoint, String) {
792 let addr = reserve_udp_addr();
793 let keys = nostr::Keys::generate();
794 let mut options = FipsEndpointOptions::new(keys.secret_key().to_bech32().unwrap());
795 options.discovery_scope = scope.to_string();
796 options.enable_udp = true;
797 options.udp_bind_addr = Some(addr.clone());
798 options.enable_webrtc = false;
799 options.enable_local_rendezvous = false;
800 options.enable_lan_discovery = false;
801 options.share_local_candidates = false;
802 let endpoint = bind_fips_endpoint(options).await.unwrap();
803 (endpoint, addr)
804 }
805
806 #[cfg(feature = "experimental-decentralized-pubsub")]
807 fn reserve_udp_addr() -> String {
808 let socket = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
809 socket.local_addr().unwrap().to_string()
810 }
811
812 #[cfg(feature = "experimental-decentralized-pubsub")]
813 async fn wait_for_peer(endpoint: &BoundFipsEndpoint, npub: &str) {
814 wait_for_native_peer(endpoint.native_endpoint.as_ref(), npub).await;
815 }
816
817 #[cfg(feature = "experimental-decentralized-pubsub")]
818 async fn wait_for_native_peer(endpoint: &FipsEndpoint, npub: &str) {
819 let result = timeout(Duration::from_secs(10), async {
820 loop {
821 if endpoint
822 .peers()
823 .await
824 .unwrap()
825 .iter()
826 .any(|peer| peer.npub == npub && peer.connected)
827 {
828 break;
829 }
830 tokio::time::sleep(Duration::from_millis(20)).await;
831 }
832 })
833 .await;
834 if result.is_err() {
835 panic!(
836 "FIPS peer {npub} did not connect; peers={:?}",
837 endpoint.peers().await
838 );
839 }
840 }
841
842 #[cfg(feature = "experimental-decentralized-pubsub")]
843 async fn trusted_test_relay(
844 root: &std::path::Path,
845 allowed: &[nostr::PublicKey],
846 ) -> Arc<NostrRelay> {
847 let graph_dir = root.join("graph");
848 let data_dir = root.join("nostr");
849 std::fs::create_dir_all(&data_dir).unwrap();
850 let graph = {
851 let _guard = crate::socialgraph::test_lock().await;
852 crate::socialgraph::open_test_social_graph_store_with_mapsize(
853 &graph_dir,
854 Some(128 * 1024 * 1024),
855 )
856 .unwrap()
857 };
858 let backend: Arc<dyn crate::socialgraph::SocialGraphBackend> = graph;
859 let allowed = allowed
860 .iter()
861 .map(|pubkey| pubkey.to_hex())
862 .collect::<HashSet<_>>();
863 let access = Arc::new(crate::socialgraph::SocialGraphAccessControl::new(
864 Arc::clone(&backend),
865 0,
866 allowed.clone(),
867 ));
868 Arc::new(
869 NostrRelay::new(
870 backend,
871 data_dir,
872 allowed,
873 Some(access),
874 crate::nostr_relay::NostrRelayConfig {
875 spambox_db_max_bytes: 0,
876 ..Default::default()
877 },
878 )
879 .unwrap(),
880 )
881 }
882
883 #[cfg(feature = "experimental-decentralized-pubsub")]
884 fn root_event(keys: &nostr::Keys, name: &str, hash: &str) -> nostr::Event {
885 EventBuilder::new(Kind::Custom(30_078), hash)
886 .tags([
887 Tag::parse(["d", name]).unwrap(),
888 Tag::parse(["l", "hashtree"]).unwrap(),
889 Tag::parse(["hash", hash]).unwrap(),
890 ])
891 .sign_with_keys(keys)
892 .unwrap()
893 }
894
895 #[cfg(feature = "experimental-decentralized-pubsub")]
896 async fn wait_for_relay_event(relay: &NostrRelay, id: nostr::EventId) {
897 timeout(Duration::from_secs(5), async {
898 loop {
899 if relay
900 .query_events(&Filter::new().id(id), 1)
901 .await
902 .iter()
903 .any(|event| event.id == id)
904 {
905 break;
906 }
907 tokio::time::sleep(Duration::from_millis(20)).await;
908 }
909 })
910 .await
911 .expect("trusted decentralized event was not indexed");
912 }
913
914 #[test]
915 fn daemon_fips_peer_configs_prefer_configured_peers() {
916 let mut config = Config::default();
917 config.server.fips_peers = vec![
918 crate::config::ConfiguredFipsPeer {
919 npub: " origin ".to_string(),
920 udp_addresses: vec![" udp:192.0.2.10:2121 ".to_string(), " ".to_string()],
921 },
922 crate::config::ConfiguredFipsPeer {
923 npub: "origin".to_string(),
924 udp_addresses: vec!["udp:ignored:2121".to_string()],
925 },
926 ];
927
928 let peers = daemon_fips_peer_configs(
929 &config,
930 vec![
931 "origin".to_string(),
932 " followed ".to_string(),
933 " ".to_string(),
934 ],
935 );
936
937 assert_eq!(
938 peers,
939 vec![
940 FipsPeerConfig {
941 npub: "origin".to_string(),
942 udp_addresses: vec!["udp:192.0.2.10:2121".to_string()],
943 },
944 FipsPeerConfig::new("followed"),
945 ]
946 );
947 }
948
949 #[test]
950 fn blob_carrier_deadline_outlives_the_complete_resolver_budget() {
951 for resolver_timeout in [Duration::from_millis(1), Duration::from_secs(20)] {
952 assert!(blob_transport_config(resolver_timeout).idle_timeout > resolver_timeout);
953 }
954 }
955
956 #[cfg(feature = "experimental-decentralized-pubsub")]
957 #[test]
958 fn daemon_decentralized_pubsub_requires_config_fips_and_relay() {
959 let mut config = Config::default();
960 assert!(!daemon_decentralized_pubsub_ready(&config, true, true));
961
962 config.nostr.decentralized_pubsub = true;
963 assert!(daemon_decentralized_pubsub_ready(&config, true, true));
964 assert!(!daemon_decentralized_pubsub_ready(&config, false, true));
965 assert!(!daemon_decentralized_pubsub_ready(&config, true, false));
966
967 config.nostr.enabled = false;
968 assert!(!daemon_decentralized_pubsub_ready(&config, true, true));
969 }
970}