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, FipsPeerConfig, HashtreeFipsTransport,
11    DEFAULT_FIPS_DISCOVERY_SCOPE,
12};
13use nostr::nips::nip19::ToBech32;
14use nostr::PublicKey;
15use std::collections::HashSet;
16use std::sync::Arc;
17use std::time::Duration;
18use tokio::task::JoinHandle;
19
20pub type DaemonFipsTransport = HashtreeFipsTransport<StorageRouter>;
21
22pub struct DaemonFipsHandle {
23    pub transport: Arc<DaemonFipsTransport>,
24    pub endpoint_npub: String,
25    pub discovery_scope: String,
26    receiver_task: JoinHandle<()>,
27}
28
29impl DaemonFipsHandle {
30    pub fn shutdown(&self) {
31        self.receiver_task.abort();
32    }
33}
34
35pub async fn start_daemon_fips_transport(
36    config: &Config,
37    keys: &nostr::Keys,
38    store: Arc<HashtreeStore>,
39    peer_ids: Vec<String>,
40) -> Result<Option<DaemonFipsHandle>> {
41    if !config.server.enable_fips || !config.server.mode.hash_get_enabled() {
42        return Ok(None);
43    }
44
45    let active_relays = config.nostr.active_relays();
46    let relays = config.server.resolved_fips_relays(&active_relays);
47    let discovery_scope = normalized_discovery_scope(&config.server.fips_discovery_scope);
48    let identity_nsec = keys
49        .secret_key()
50        .to_bech32()
51        .context("Failed to encode daemon identity for FIPS endpoint")?;
52    let mut options = FipsEndpointOptions::new(identity_nsec);
53    options.discovery_scope = discovery_scope.clone();
54    options.relays = relays;
55    options.enable_udp = config.server.enable_fips_udp;
56    options.enable_webrtc = config.server.enable_fips_webrtc;
57    options.udp_bind_addr = config.server.fips_udp_bind_addr.clone();
58    options.udp_public = config.server.fips_udp_public;
59    options.udp_external_addr = config.server.fips_udp_external_addr.clone();
60    options.webrtc_auto_connect = false;
61    options.webrtc_max_connections = hashtree_fips_transport::DEFAULT_FIPS_WEBRTC_MAX_CONNECTIONS;
62    options.open_discovery_max_pending = 0;
63    options.packet_channel_capacity = 1024;
64    let endpoint = bind_fips_endpoint(options)
65        .await
66        .context("Failed to start FIPS endpoint")?;
67
68    let request_timeout = Duration::from_millis(config.server.fips_request_timeout_ms.max(1));
69    let transport = Arc::new(
70        HashtreeFipsTransport::new(endpoint.endpoint, store.store_arc())
71            .with_request_timeout(request_timeout)
72            .with_cache_responses(false),
73    );
74    let peer_configs = daemon_fips_peer_configs(config, peer_ids);
75    if !peer_configs.is_empty() {
76        transport.set_peer_configs(peer_configs).await;
77    }
78    let receiver_task = transport.start();
79
80    Ok(Some(DaemonFipsHandle {
81        transport,
82        endpoint_npub: endpoint.local_peer_id,
83        discovery_scope: endpoint.discovery_scope,
84        receiver_task,
85    }))
86}
87
88pub fn fips_peer_ids_from_pubkeys(pubkeys: Vec<[u8; 32]>) -> Vec<String> {
89    pubkeys
90        .into_iter()
91        .filter_map(|pubkey| PublicKey::from_slice(&pubkey).ok())
92        .filter_map(|pubkey| pubkey.to_bech32().ok())
93        .collect()
94}
95
96pub fn daemon_fips_peer_configs(
97    config: &Config,
98    discovered_peer_ids: Vec<String>,
99) -> Vec<FipsPeerConfig> {
100    let mut seen = HashSet::new();
101    let mut peers = Vec::new();
102
103    for peer in &config.server.fips_peers {
104        let npub = peer.npub.trim().to_string();
105        if npub.is_empty() || !seen.insert(npub.clone()) {
106            continue;
107        }
108        let udp_addresses = peer
109            .udp_addresses
110            .iter()
111            .map(|addr| addr.trim().to_string())
112            .filter(|addr| !addr.is_empty())
113            .collect();
114        peers.push(FipsPeerConfig {
115            npub,
116            udp_addresses,
117        });
118    }
119
120    for peer_id in discovered_peer_ids {
121        let npub = peer_id.trim().to_string();
122        if npub.is_empty() || !seen.insert(npub.clone()) {
123            continue;
124        }
125        peers.push(FipsPeerConfig::new(npub));
126    }
127
128    peers
129}
130
131fn normalized_discovery_scope(scope: &str) -> String {
132    let scope = scope.trim();
133    if scope.is_empty() {
134        DEFAULT_FIPS_DISCOVERY_SCOPE.to_string()
135    } else {
136        scope.to_string()
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn fips_peer_ids_from_pubkeys_encodes_npbus() {
146        let keys = nostr::Keys::generate();
147        let expected = keys.public_key().to_bech32().unwrap();
148
149        assert_eq!(
150            fips_peer_ids_from_pubkeys(vec![keys.public_key().to_bytes()]),
151            vec![expected]
152        );
153    }
154
155    #[test]
156    fn empty_discovery_scope_uses_hashtree_default() {
157        assert_eq!(
158            normalized_discovery_scope("  "),
159            DEFAULT_FIPS_DISCOVERY_SCOPE.to_string()
160        );
161    }
162
163    #[test]
164    fn daemon_fips_peer_configs_prefer_configured_peers() {
165        let mut config = Config::default();
166        config.server.fips_peers = vec![
167            crate::config::ConfiguredFipsPeer {
168                npub: " origin ".to_string(),
169                udp_addresses: vec![" udp:192.0.2.10:2121 ".to_string(), " ".to_string()],
170            },
171            crate::config::ConfiguredFipsPeer {
172                npub: "origin".to_string(),
173                udp_addresses: vec!["udp:ignored:2121".to_string()],
174            },
175        ];
176
177        let peers = daemon_fips_peer_configs(
178            &config,
179            vec![
180                "origin".to_string(),
181                " followed ".to_string(),
182                " ".to_string(),
183            ],
184        );
185
186        assert_eq!(
187            peers,
188            vec![
189                FipsPeerConfig {
190                    npub: "origin".to_string(),
191                    udp_addresses: vec!["udp:192.0.2.10:2121".to_string()],
192                },
193                FipsPeerConfig::new("followed"),
194            ]
195        );
196    }
197}