hashtree_cli/
fips_transport.rs1use 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}