fedimint_server/config/
api.rs

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/// State held by the API after receiving a `ConfigGenConnectionsRequest`
37#[derive(Debug, Clone, Default)]
38pub struct ConfigGenState {
39    /// Our local connection
40    local_params: Option<LocalParams>,
41    /// Connection info received from other guardians
42    connection_info: BTreeSet<PeerConnectionInfo>,
43}
44
45#[derive(Clone, Debug)]
46/// Connection information sent between peers in order to start config gen
47pub struct LocalParams {
48    /// Our auth string
49    auth: ApiAuth,
50    /// Our TLS private key
51    tls_key: Option<rustls::PrivateKey>,
52    /// Optional secret key for our iroh api endpoint
53    iroh_api_sk: Option<iroh::SecretKey>,
54    /// Optional secret key for our iroh p2p endpoint
55    iroh_p2p_sk: Option<iroh::SecretKey>,
56    /// Our api and p2p endpoint
57    endpoints: PeerEndpoints,
58    /// Name of the peer, used in TLS auth
59    name: String,
60    /// Federation name set by the leader
61    federation_name: Option<String>,
62}
63
64/// Serves the config gen API endpoints
65pub struct ConfigGenApi {
66    /// Our config gen settings configured locally
67    settings: ConfigGenSettings,
68    /// In-memory state machine
69    state: Mutex<ConfigGenState>,
70    /// DB not really used
71    db: Database,
72    /// Triggers the distributed key generation
73    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}