Skip to main content

hashtree_cli/
fips_transport.rs

1//! Hashtree daemon integration for the embedded FIPS endpoint API.
2//!
3//! This starts `fips_core::FipsEndpoint` inside the htree process; it does not
4//! depend on or talk to an external FIPS daemon.
5
6use crate::config::Config;
7use crate::storage::{HashtreeStore, StorageRouter};
8use anyhow::{Context, Result};
9use hashtree_fips_transport::{
10    bind_fips_endpoint, FipsEndpointOptions, HashtreeFipsTransport, DEFAULT_FIPS_DISCOVERY_SCOPE,
11};
12use nostr::nips::nip19::ToBech32;
13use nostr::PublicKey;
14use std::sync::Arc;
15use std::time::Duration;
16use tokio::task::JoinHandle;
17
18pub type DaemonFipsTransport = HashtreeFipsTransport<StorageRouter>;
19
20pub struct DaemonFipsHandle {
21    pub transport: Arc<DaemonFipsTransport>,
22    pub endpoint_npub: String,
23    pub discovery_scope: String,
24    receiver_task: JoinHandle<()>,
25}
26
27impl DaemonFipsHandle {
28    pub fn shutdown(&self) {
29        self.receiver_task.abort();
30    }
31}
32
33pub async fn start_daemon_fips_transport(
34    config: &Config,
35    keys: &nostr::Keys,
36    store: Arc<HashtreeStore>,
37    peer_ids: Vec<String>,
38) -> Result<Option<DaemonFipsHandle>> {
39    if !config.server.enable_fips || !config.server.mode.hash_get_enabled() {
40        return Ok(None);
41    }
42
43    let active_relays = config.nostr.active_relays();
44    let relays = config.server.resolved_fips_relays(&active_relays);
45    let discovery_scope = normalized_discovery_scope(&config.server.fips_discovery_scope);
46    let identity_nsec = keys
47        .secret_key()
48        .to_bech32()
49        .context("Failed to encode daemon identity for FIPS endpoint")?;
50    let mut options = FipsEndpointOptions::new(identity_nsec);
51    options.discovery_scope = discovery_scope.clone();
52    options.relays = relays;
53    options.enable_udp = config.server.enable_fips_udp;
54    options.enable_webrtc = config.server.enable_fips_webrtc;
55    options.udp_bind_addr = config.server.fips_udp_bind_addr.clone();
56    options.udp_public = config.server.fips_udp_public;
57    options.udp_external_addr = config.server.fips_udp_external_addr.clone();
58    options.webrtc_auto_connect = false;
59    options.webrtc_max_connections = hashtree_fips_transport::DEFAULT_FIPS_WEBRTC_MAX_CONNECTIONS;
60    options.open_discovery_max_pending = 0;
61    options.packet_channel_capacity = 1024;
62    let endpoint = bind_fips_endpoint(options)
63        .await
64        .context("Failed to start FIPS endpoint")?;
65
66    let request_timeout = Duration::from_millis(config.server.fips_request_timeout_ms.max(1));
67    let transport = Arc::new(
68        HashtreeFipsTransport::new(endpoint.endpoint, store.store_arc())
69            .with_request_timeout(request_timeout)
70            .with_cache_responses(false),
71    );
72    if !peer_ids.is_empty() {
73        transport.set_peers(peer_ids).await;
74    }
75    let receiver_task = transport.start();
76
77    Ok(Some(DaemonFipsHandle {
78        transport,
79        endpoint_npub: endpoint.local_peer_id,
80        discovery_scope: endpoint.discovery_scope,
81        receiver_task,
82    }))
83}
84
85pub fn fips_peer_ids_from_pubkeys(pubkeys: Vec<[u8; 32]>) -> Vec<String> {
86    pubkeys
87        .into_iter()
88        .filter_map(|pubkey| PublicKey::from_slice(&pubkey).ok())
89        .filter_map(|pubkey| pubkey.to_bech32().ok())
90        .collect()
91}
92
93fn normalized_discovery_scope(scope: &str) -> String {
94    let scope = scope.trim();
95    if scope.is_empty() {
96        DEFAULT_FIPS_DISCOVERY_SCOPE.to_string()
97    } else {
98        scope.to_string()
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn fips_peer_ids_from_pubkeys_encodes_npbus() {
108        let keys = nostr::Keys::generate();
109        let expected = keys.public_key().to_bech32().unwrap();
110
111        assert_eq!(
112            fips_peer_ids_from_pubkeys(vec![keys.public_key().to_bytes()]),
113            vec![expected]
114        );
115    }
116
117    #[test]
118    fn empty_discovery_scope_uses_hashtree_default() {
119        assert_eq!(
120            normalized_discovery_scope("  "),
121            DEFAULT_FIPS_DISCOVERY_SCOPE.to_string()
122        );
123    }
124}