1use std::collections::{BTreeMap, BTreeSet};
2use std::mem::discriminant;
3use std::str::FromStr as _;
4
5use anyhow::{Context, ensure};
6use async_trait::async_trait;
7use fedimint_bitcoind::create_bitcoind;
8use fedimint_core::PeerId;
9use fedimint_core::admin_client::{ServerStatus, SetLocalParamsRequest};
10use fedimint_core::core::ModuleInstanceId;
11use fedimint_core::db::Database;
12use fedimint_core::endpoint_constants::{
13 ADD_PEER_CONNECTION_INFO_ENDPOINT, AUTH_ENDPOINT, CHECK_BITCOIN_STATUS_ENDPOINT,
14 RESET_SETUP_ENDPOINT, SERVER_STATUS_ENDPOINT, SET_LOCAL_PARAMS_ENDPOINT, START_DKG_ENDPOINT,
15};
16use fedimint_core::envs::{
17 BitcoinRpcConfig, FM_IROH_API_SECRET_KEY_OVERRIDE_ENV, FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV,
18};
19use fedimint_core::module::{
20 ApiAuth, ApiEndpoint, ApiEndpointContext, ApiError, ApiRequestErased, ApiVersion, api_endpoint,
21};
22use fedimint_logging::LOG_SERVER;
23use iroh::SecretKey;
24use rand::rngs::OsRng;
25use serde::{Deserialize, Serialize};
26use tokio::sync::Mutex;
27use tokio::sync::mpsc::Sender;
28use tokio_rustls::rustls;
29use tracing::warn;
30
31use super::PeerEndpoints;
32use crate::config::{ConfigGenParams, ConfigGenSettings, NetworkingStack, PeerConnectionInfo};
33use crate::net::api::{ApiResult, HasApiContext, check_auth};
34use crate::net::p2p_connector::gen_cert_and_key;
35
36#[derive(Debug, Clone, Default)]
38pub struct ConfigGenState {
39 local_params: Option<LocalParams>,
41 connection_info: BTreeSet<PeerConnectionInfo>,
43}
44
45#[derive(Clone, Debug)]
46pub struct LocalParams {
48 auth: ApiAuth,
50 tls_key: Option<rustls::PrivateKey>,
52 iroh_api_sk: Option<iroh::SecretKey>,
54 iroh_p2p_sk: Option<iroh::SecretKey>,
56 endpoints: PeerEndpoints,
58 name: String,
60 federation_name: Option<String>,
62}
63
64pub struct ConfigGenApi {
66 settings: ConfigGenSettings,
68 state: Mutex<ConfigGenState>,
70 db: Database,
72 sender: Sender<ConfigGenParams>,
74}
75
76impl ConfigGenApi {
77 pub fn new(settings: ConfigGenSettings, db: Database, sender: Sender<ConfigGenParams>) -> Self {
78 Self {
79 settings,
80 state: Mutex::new(ConfigGenState::default()),
81 db,
82 sender,
83 }
84 }
85
86 pub async fn server_status(&self) -> ServerStatus {
87 let state = self.state.lock().await;
88
89 match state.local_params {
90 Some(..) => ServerStatus::CollectingConnectionInfo(
91 state
92 .connection_info
93 .clone()
94 .into_iter()
95 .map(|info| info.name)
96 .collect(),
97 ),
98 None => ServerStatus::AwaitingLocalParams,
99 }
100 }
101
102 pub async fn reset(&self) {
103 *self.state.lock().await = ConfigGenState::default();
104 }
105
106 pub async fn set_local_parameters(
107 &self,
108 auth: ApiAuth,
109 request: SetLocalParamsRequest,
110 ) -> anyhow::Result<PeerConnectionInfo> {
111 ensure!(
112 auth.0.trim() == auth.0,
113 "Password contains leading/trailing whitespace",
114 );
115
116 let mut state = self.state.lock().await;
117
118 if let Some(lp) = state.local_params.clone() {
119 ensure!(
120 lp.auth == auth,
121 "Local parameters have already been set with a different auth."
122 );
123
124 ensure!(
125 lp.name == request.name,
126 "Local parameters have already been set with a different name."
127 );
128
129 ensure!(
130 lp.federation_name == request.federation_name,
131 "Local parameters have already been set with a different federation name."
132 );
133
134 let info = PeerConnectionInfo {
135 name: lp.name,
136 endpoints: lp.endpoints,
137 federation_name: lp.federation_name,
138 };
139
140 return Ok(info);
141 }
142
143 let lp = match self.settings.networking {
144 NetworkingStack::Tcp => {
145 let (tls_cert, tls_key) = gen_cert_and_key(&request.name)
146 .expect("Failed to generate TLS for given guardian name");
147
148 LocalParams {
149 auth,
150 tls_key: Some(tls_key),
151 iroh_api_sk: None,
152 iroh_p2p_sk: None,
153 endpoints: PeerEndpoints::Tcp {
154 api_url: self.settings.api_url.clone(),
155 p2p_url: self.settings.p2p_url.clone(),
156 cert: tls_cert.0,
157 },
158 name: request.name,
159 federation_name: request.federation_name,
160 }
161 }
162 NetworkingStack::Iroh => {
163 warn!(target: LOG_SERVER, "Iroh support is experimental");
164 let iroh_api_sk = if let Ok(var) =
165 std::env::var(FM_IROH_API_SECRET_KEY_OVERRIDE_ENV)
166 {
167 SecretKey::from_str(&var)
168 .with_context(|| format!("Parsing {FM_IROH_API_SECRET_KEY_OVERRIDE_ENV}"))?
169 } else {
170 SecretKey::generate(&mut OsRng)
171 };
172
173 let iroh_p2p_sk = if let Ok(var) =
174 std::env::var(FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV)
175 {
176 SecretKey::from_str(&var)
177 .with_context(|| format!("Parsing {FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV}"))?
178 } else {
179 SecretKey::generate(&mut OsRng)
180 };
181
182 LocalParams {
183 auth,
184 tls_key: None,
185 iroh_api_sk: Some(iroh_api_sk.clone()),
186 iroh_p2p_sk: Some(iroh_p2p_sk.clone()),
187 endpoints: PeerEndpoints::Iroh {
188 api_pk: iroh_api_sk.public(),
189 p2p_pk: iroh_p2p_sk.public(),
190 },
191 name: request.name,
192 federation_name: request.federation_name,
193 }
194 }
195 };
196
197 state.local_params = Some(lp.clone());
198
199 let info = PeerConnectionInfo {
200 name: lp.name,
201 endpoints: lp.endpoints,
202 federation_name: lp.federation_name,
203 };
204
205 Ok(info)
206 }
207
208 pub async fn add_peer_connection_info(&self, info: PeerConnectionInfo) -> anyhow::Result<()> {
209 let mut state = self.state.lock().await;
210
211 if state.connection_info.contains(&info) {
212 return Ok(());
213 }
214
215 let local_params = state
216 .local_params
217 .clone()
218 .expect("The endpoint is authenticated.");
219
220 ensure!(
221 discriminant(&info.endpoints) == discriminant(&local_params.endpoints),
222 "Guardian has different endpoint variant (TCP/Iroh) than us.",
223 );
224
225 if let Some(federation_name) = state
226 .connection_info
227 .iter()
228 .find_map(|info| info.federation_name.clone())
229 {
230 ensure!(
231 info.federation_name.is_none(),
232 "Federation name has already been set to {federation_name}"
233 );
234 }
235
236 state.connection_info.insert(info);
237
238 Ok(())
239 }
240
241 pub async fn start_dkg(&self) -> anyhow::Result<()> {
242 let mut state = self.state.lock().await.clone();
243
244 let local_params = state
245 .local_params
246 .clone()
247 .expect("The endpoint is authenticated.");
248
249 let our_peer_info = PeerConnectionInfo {
250 name: local_params.name,
251 endpoints: local_params.endpoints,
252 federation_name: local_params.federation_name,
253 };
254
255 state.connection_info.insert(our_peer_info.clone());
256
257 let federation_name = state
258 .connection_info
259 .iter()
260 .find_map(|info| info.federation_name.clone())
261 .context("We need one leader to configure the federation name")?;
262
263 let our_id = state
264 .connection_info
265 .iter()
266 .position(|info| info == &our_peer_info)
267 .expect("We inserted the key above.");
268
269 let params = ConfigGenParams {
270 identity: PeerId::from(our_id as u16),
271 tls_key: local_params.tls_key,
272 iroh_api_sk: local_params.iroh_api_sk,
273 iroh_p2p_sk: local_params.iroh_p2p_sk,
274 api_auth: local_params.auth,
275 p2p_bind: self.settings.p2p_bind,
276 api_bind: self.settings.api_bind,
277 peers: (0..)
278 .map(|i| PeerId::from(i as u16))
279 .zip(state.connection_info.clone().into_iter())
280 .collect(),
281 meta: BTreeMap::from_iter(vec![("federation_name".to_string(), federation_name)]),
282 modules: self.settings.modules.clone(),
283 };
284
285 self.sender
286 .send(params)
287 .await
288 .context("Failed to send config gen params")?;
289
290 Ok(())
291 }
292}
293
294#[async_trait]
295impl HasApiContext<ConfigGenApi> for ConfigGenApi {
296 async fn context(
297 &self,
298 request: &ApiRequestErased,
299 id: Option<ModuleInstanceId>,
300 ) -> (&ConfigGenApi, ApiEndpointContext<'_>) {
301 assert!(id.is_none());
302
303 let db = self.db.clone();
304 let dbtx = self.db.begin_transaction().await;
305
306 let is_authenticated = match self.state.lock().await.local_params {
307 None => false,
308 Some(ref params) => match request.auth.as_ref() {
309 Some(auth) => *auth == params.auth,
310 None => false,
311 },
312 };
313
314 let context = ApiEndpointContext::new(db, dbtx, is_authenticated, request.auth.clone());
315
316 (self, context)
317 }
318}
319
320pub fn server_endpoints() -> Vec<ApiEndpoint<ConfigGenApi>> {
321 vec![
322 api_endpoint! {
323 SERVER_STATUS_ENDPOINT,
324 ApiVersion::new(0, 0),
325 async |config: &ConfigGenApi, _c, _v: ()| -> ServerStatus {
326 Ok(config.server_status().await)
327 }
328 },
329 api_endpoint! {
330 SET_LOCAL_PARAMS_ENDPOINT,
331 ApiVersion::new(0, 0),
332 async |config: &ConfigGenApi, context, request: SetLocalParamsRequest| -> String {
333 let auth = context
334 .request_auth()
335 .ok_or(ApiError::bad_request("Missing password".to_string()))?;
336
337 let info = config.set_local_parameters(auth, request)
338 .await
339 .map_err(|e| ApiError::bad_request(e.to_string()))?;
340
341 Ok(info.encode_base32())
342 }
343 },
344 api_endpoint! {
345 ADD_PEER_CONNECTION_INFO_ENDPOINT,
346 ApiVersion::new(0, 0),
347 async |config: &ConfigGenApi, context, info: String| -> String {
348 check_auth(context)?;
349
350 let info = PeerConnectionInfo::decode_base32(&info)
351 .map_err(|e|ApiError::bad_request(e.to_string()))?;
352
353 config.add_peer_connection_info(info.clone()).await
354 .map_err(|e|ApiError::bad_request(e.to_string()))?;
355
356 Ok(info.name)
357 }
358 },
359 api_endpoint! {
360 START_DKG_ENDPOINT,
361 ApiVersion::new(0, 0),
362 async |config: &ConfigGenApi, context, _v: ()| -> () {
363 check_auth(context)?;
364
365 config.start_dkg().await.map_err(|e| ApiError::server_error(e.to_string()))
366 }
367 },
368 api_endpoint! {
369 RESET_SETUP_ENDPOINT,
370 ApiVersion::new(0, 0),
371 async |config: &ConfigGenApi, context, _v: ()| -> () {
372 check_auth(context)?;
373
374 config.reset().await;
375
376 Ok(())
377 }
378 },
379 api_endpoint! {
380 AUTH_ENDPOINT,
381 ApiVersion::new(0, 0),
382 async |_config: &ConfigGenApi, context, _v: ()| -> () {
383 check_auth(context)?;
384
385 Ok(())
386 }
387 },
388 api_endpoint! {
389 CHECK_BITCOIN_STATUS_ENDPOINT,
390 ApiVersion::new(0, 0),
391 async |_config: &ConfigGenApi, context, _v: ()| -> BitcoinRpcConnectionStatus {
392 check_auth(context)?;
393
394 check_bitcoin_status().await
395 }
396 },
397 ]
398}
399
400#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
401pub struct BitcoinRpcConnectionStatus {
402 chain_tip_block_height: u64,
403 chain_tip_block_time: u32,
404 sync_percentage: Option<f64>,
405}
406
407async fn check_bitcoin_status() -> ApiResult<BitcoinRpcConnectionStatus> {
408 let bitcoin_rpc_config = BitcoinRpcConfig::get_defaults_from_env_vars()
409 .map_err(|e| ApiError::server_error(format!("Failed to get bitcoin rpc env vars: {e}")))?;
410
411 let client = create_bitcoind(&bitcoin_rpc_config)
412 .map_err(|e| ApiError::server_error(format!("Failed to connect to bitcoin rpc: {e}")))?;
413
414 let block_count = client.get_block_count().await.map_err(|e| {
415 ApiError::server_error(format!("Failed to get block count from bitcoin rpc: {e}"))
416 })?;
417
418 let chain_tip_block_height = block_count - 1;
419
420 let chain_tip_block_hash = client
421 .get_block_hash(chain_tip_block_height)
422 .await
423 .map_err(|e| {
424 ApiError::server_error(format!(
425 "Failed to get block hash for block count {block_count} from bitcoin rpc: {e}"
426 ))
427 })?;
428
429 let chain_tip_block = client.get_block(&chain_tip_block_hash).await.map_err(|e| {
430 ApiError::server_error(format!(
431 "Failed to get block for block hash {chain_tip_block_hash} from bitcoin rpc: {e}"
432 ))
433 })?;
434
435 let chain_tip_block_time = chain_tip_block.header.time;
436
437 let sync_percentage = client.get_sync_percentage().await.map_err(|e| {
438 ApiError::server_error(format!(
439 "Failed to get sync percentage from bitcoin rpc: {e}"
440 ))
441 })?;
442
443 Ok(BitcoinRpcConnectionStatus {
444 chain_tip_block_height,
445 chain_tip_block_time,
446 sync_percentage,
447 })
448}