fedimint_server/config/
setup.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::iter::once;
3use std::mem::discriminant;
4use std::str::FromStr as _;
5use std::sync::Arc;
6
7use anyhow::{Context, ensure};
8use async_trait::async_trait;
9use fedimint_core::admin_client::{SetLocalParamsRequest, SetupStatus};
10use fedimint_core::base32::FEDIMINT_PREFIX;
11use fedimint_core::config::META_FEDERATION_NAME_KEY;
12use fedimint_core::core::ModuleInstanceId;
13use fedimint_core::db::Database;
14use fedimint_core::endpoint_constants::{
15    ADD_PEER_SETUP_CODE_ENDPOINT, GET_SETUP_CODE_ENDPOINT, RESET_PEER_SETUP_CODES_ENDPOINT,
16    SET_LOCAL_PARAMS_ENDPOINT, SETUP_STATUS_ENDPOINT, START_DKG_ENDPOINT,
17};
18use fedimint_core::envs::{
19    FM_DISABLE_BASE_FEES_ENV, FM_IROH_API_SECRET_KEY_OVERRIDE_ENV,
20    FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV, is_env_var_set,
21};
22use fedimint_core::module::{
23    ApiAuth, ApiEndpoint, ApiEndpointContext, ApiError, ApiRequestErased, ApiVersion, api_endpoint,
24};
25use fedimint_core::setup_code::PeerEndpoints;
26use fedimint_core::{PeerId, base32};
27use fedimint_logging::LOG_SERVER;
28use fedimint_server_core::net::check_auth;
29use fedimint_server_core::setup_ui::ISetupApi;
30use iroh::SecretKey;
31use rand::rngs::OsRng;
32use tokio::sync::Mutex;
33use tokio::sync::mpsc::Sender;
34use tokio_rustls::rustls;
35use tracing::warn;
36
37use crate::config::{ConfigGenParams, ConfigGenSettings, PeerSetupCode};
38use crate::net::api::HasApiContext;
39use crate::net::p2p_connector::gen_cert_and_key;
40
41/// State held by the API after receiving a `ConfigGenConnectionsRequest`
42#[derive(Debug, Clone, Default)]
43pub struct SetupState {
44    /// Our local connection
45    local_params: Option<LocalParams>,
46    /// Connection info received from other guardians
47    setup_codes: BTreeSet<PeerSetupCode>,
48}
49
50#[derive(Clone, Debug)]
51/// Connection information sent between peers in order to start config gen
52pub struct LocalParams {
53    /// Our auth string
54    auth: ApiAuth,
55    /// Our TLS private key
56    tls_key: Option<Arc<rustls::pki_types::PrivateKeyDer<'static>>>,
57    /// Optional secret key for our iroh api endpoint
58    iroh_api_sk: Option<iroh::SecretKey>,
59    /// Optional secret key for our iroh p2p endpoint
60    iroh_p2p_sk: Option<iroh::SecretKey>,
61    /// Our api and p2p endpoint
62    endpoints: PeerEndpoints,
63    /// Name of the peer, used in TLS auth
64    name: String,
65    /// Federation name set by the leader
66    federation_name: Option<String>,
67    /// Whether to disable base fees, set by the leader
68    disable_base_fees: Option<bool>,
69}
70
71impl LocalParams {
72    pub fn setup_code(&self) -> PeerSetupCode {
73        PeerSetupCode {
74            name: self.name.clone(),
75            endpoints: self.endpoints.clone(),
76            federation_name: self.federation_name.clone(),
77            disable_base_fees: self.disable_base_fees,
78        }
79    }
80}
81
82/// Serves the config gen API endpoints
83#[derive(Clone)]
84pub struct SetupApi {
85    /// Our config gen settings configured locally
86    settings: ConfigGenSettings,
87    /// In-memory state machine
88    state: Arc<Mutex<SetupState>>,
89    /// DB not really used
90    db: Database,
91    /// Triggers the distributed key generation
92    sender: Sender<ConfigGenParams>,
93}
94
95impl SetupApi {
96    pub fn new(settings: ConfigGenSettings, db: Database, sender: Sender<ConfigGenParams>) -> Self {
97        Self {
98            settings,
99            state: Arc::new(Mutex::new(SetupState::default())),
100            db,
101            sender,
102        }
103    }
104
105    pub async fn setup_status(&self) -> SetupStatus {
106        match self.state.lock().await.local_params {
107            Some(..) => SetupStatus::SharingConnectionCodes,
108            None => SetupStatus::AwaitingLocalParams,
109        }
110    }
111}
112
113#[async_trait]
114impl ISetupApi for SetupApi {
115    async fn setup_code(&self) -> Option<String> {
116        self.state
117            .lock()
118            .await
119            .local_params
120            .as_ref()
121            .map(|lp| base32::encode_prefixed(FEDIMINT_PREFIX, &lp.setup_code()))
122    }
123
124    async fn auth(&self) -> Option<ApiAuth> {
125        self.state
126            .lock()
127            .await
128            .local_params
129            .as_ref()
130            .map(|lp| lp.auth.clone())
131    }
132
133    async fn connected_peers(&self) -> Vec<String> {
134        self.state
135            .lock()
136            .await
137            .setup_codes
138            .clone()
139            .into_iter()
140            .map(|info| info.name)
141            .collect()
142    }
143
144    async fn reset_setup_codes(&self) {
145        self.state.lock().await.setup_codes.clear();
146    }
147
148    async fn set_local_parameters(
149        &self,
150        auth: ApiAuth,
151        name: String,
152        federation_name: Option<String>,
153        disable_base_fees: Option<bool>,
154    ) -> anyhow::Result<String> {
155        if let Some(existing_local_parameters) = self.state.lock().await.local_params.clone()
156            && existing_local_parameters.auth == auth
157            && existing_local_parameters.name == name
158            && existing_local_parameters.federation_name == federation_name
159            && existing_local_parameters.disable_base_fees == disable_base_fees
160        {
161            return Ok(base32::encode_prefixed(
162                FEDIMINT_PREFIX,
163                &existing_local_parameters.setup_code(),
164            ));
165        }
166
167        ensure!(!name.is_empty(), "The guardian name is empty");
168
169        ensure!(!auth.0.is_empty(), "The password is empty");
170
171        ensure!(
172            auth.0.trim() == auth.0,
173            "The password contains leading/trailing whitespace",
174        );
175
176        if let Some(federation_name) = federation_name.as_ref() {
177            ensure!(!federation_name.is_empty(), "The federation name is empty");
178        }
179
180        let mut state = self.state.lock().await;
181
182        ensure!(
183            state.local_params.is_none(),
184            "Local parameters have already been set"
185        );
186
187        let lp = if self.settings.enable_iroh {
188            warn!(target: LOG_SERVER, "Iroh support is experimental");
189
190            let iroh_api_sk = if let Ok(var) = std::env::var(FM_IROH_API_SECRET_KEY_OVERRIDE_ENV) {
191                SecretKey::from_str(&var)
192                    .with_context(|| format!("Parsing {FM_IROH_API_SECRET_KEY_OVERRIDE_ENV}"))?
193            } else {
194                SecretKey::generate(&mut OsRng)
195            };
196
197            let iroh_p2p_sk = if let Ok(var) = std::env::var(FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV) {
198                SecretKey::from_str(&var)
199                    .with_context(|| format!("Parsing {FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV}"))?
200            } else {
201                SecretKey::generate(&mut OsRng)
202            };
203
204            LocalParams {
205                auth,
206                tls_key: None,
207                iroh_api_sk: Some(iroh_api_sk.clone()),
208                iroh_p2p_sk: Some(iroh_p2p_sk.clone()),
209                endpoints: PeerEndpoints::Iroh {
210                    api_pk: iroh_api_sk.public(),
211                    p2p_pk: iroh_p2p_sk.public(),
212                },
213                name,
214                federation_name,
215                disable_base_fees,
216            }
217        } else {
218            let (tls_cert, tls_key) =
219                gen_cert_and_key(&name).expect("Failed to generate TLS for given guardian name");
220
221            LocalParams {
222                auth,
223                tls_key: Some(tls_key),
224                iroh_api_sk: None,
225                iroh_p2p_sk: None,
226                endpoints: PeerEndpoints::Tcp {
227                    api_url: self
228                        .settings
229                        .api_url
230                        .clone()
231                        .ok_or_else(|| anyhow::format_err!("Api URL must be configured"))?,
232                    p2p_url: self
233                        .settings
234                        .p2p_url
235                        .clone()
236                        .ok_or_else(|| anyhow::format_err!("P2P URL must be configured"))?,
237
238                    cert: tls_cert.as_ref().to_vec(),
239                },
240                name,
241                federation_name,
242                disable_base_fees,
243            }
244        };
245
246        state.local_params = Some(lp.clone());
247
248        Ok(base32::encode_prefixed(FEDIMINT_PREFIX, &lp.setup_code()))
249    }
250
251    async fn add_peer_setup_code(&self, info: String) -> anyhow::Result<String> {
252        let info = base32::decode_prefixed(FEDIMINT_PREFIX, &info)?;
253
254        let mut state = self.state.lock().await;
255
256        if state.setup_codes.contains(&info) {
257            return Ok(info.name.clone());
258        }
259
260        let local_params = state
261            .local_params
262            .clone()
263            .expect("The endpoint is authenticated.");
264
265        ensure!(
266            info != local_params.setup_code(),
267            "You cannot add you own connection info"
268        );
269
270        ensure!(
271            discriminant(&info.endpoints) == discriminant(&local_params.endpoints),
272            "Guardian has different endpoint variant (TCP/Iroh) than us.",
273        );
274
275        if let Some(federation_name) = state
276            .setup_codes
277            .iter()
278            .chain(once(&local_params.setup_code()))
279            .find_map(|info| info.federation_name.clone())
280        {
281            ensure!(
282                info.federation_name.is_none(),
283                "Federation name has already been set to {federation_name}"
284            );
285        }
286
287        if let Some(disable_base_fees) = state
288            .setup_codes
289            .iter()
290            .chain(once(&local_params.setup_code()))
291            .find_map(|info| info.disable_base_fees)
292        {
293            ensure!(
294                info.disable_base_fees.is_none(),
295                "Base fees setting has already been configured to disabled={disable_base_fees}"
296            );
297        }
298
299        state.setup_codes.insert(info.clone());
300
301        Ok(info.name)
302    }
303
304    async fn start_dkg(&self) -> anyhow::Result<()> {
305        let mut state = self.state.lock().await.clone();
306
307        let local_params = state
308            .local_params
309            .clone()
310            .expect("The endpoint is authenticated.");
311
312        let our_setup_code = local_params.setup_code();
313
314        state.setup_codes.insert(our_setup_code.clone());
315
316        ensure!(
317            state.setup_codes.len() == 1 || state.setup_codes.len() >= 4,
318            "The number of guardians is invalid"
319        );
320
321        let federation_name = state
322            .setup_codes
323            .iter()
324            .find_map(|info| info.federation_name.clone())
325            .context("We need one guardian to configure the federations name")?;
326
327        let disable_base_fees = state
328            .setup_codes
329            .iter()
330            .find_map(|info| info.disable_base_fees)
331            .unwrap_or(is_env_var_set(FM_DISABLE_BASE_FEES_ENV));
332
333        let our_id = state
334            .setup_codes
335            .iter()
336            .position(|info| info == &our_setup_code)
337            .expect("We inserted the key above.");
338
339        let params = ConfigGenParams {
340            identity: PeerId::from(our_id as u16),
341            tls_key: local_params.tls_key,
342            iroh_api_sk: local_params.iroh_api_sk,
343            iroh_p2p_sk: local_params.iroh_p2p_sk,
344            api_auth: local_params.auth,
345            peers: (0..)
346                .map(|i| PeerId::from(i as u16))
347                .zip(state.setup_codes.clone().into_iter())
348                .collect(),
349            meta: BTreeMap::from_iter(vec![(
350                META_FEDERATION_NAME_KEY.to_string(),
351                federation_name,
352            )]),
353            disable_base_fees,
354        };
355
356        self.sender
357            .send(params)
358            .await
359            .context("Failed to send config gen params")?;
360
361        Ok(())
362    }
363}
364
365#[async_trait]
366impl HasApiContext<SetupApi> for SetupApi {
367    async fn context(
368        &self,
369        request: &ApiRequestErased,
370        id: Option<ModuleInstanceId>,
371    ) -> (&SetupApi, ApiEndpointContext<'_>) {
372        assert!(id.is_none());
373
374        let db = self.db.clone();
375        let dbtx = self.db.begin_transaction().await;
376
377        let is_authenticated = match self.state.lock().await.local_params {
378            None => false,
379            Some(ref params) => match request.auth.as_ref() {
380                Some(auth) => *auth == params.auth,
381                None => false,
382            },
383        };
384
385        let context = ApiEndpointContext::new(db, dbtx, is_authenticated, request.auth.clone());
386
387        (self, context)
388    }
389}
390
391pub fn server_endpoints() -> Vec<ApiEndpoint<SetupApi>> {
392    vec![
393        api_endpoint! {
394            SETUP_STATUS_ENDPOINT,
395            ApiVersion::new(0, 0),
396            async |config: &SetupApi, _c, _v: ()| -> SetupStatus {
397                Ok(config.setup_status().await)
398            }
399        },
400        api_endpoint! {
401            SET_LOCAL_PARAMS_ENDPOINT,
402            ApiVersion::new(0, 0),
403            async |config: &SetupApi, context, request: SetLocalParamsRequest| -> String {
404                let auth = context
405                    .request_auth()
406                    .ok_or(ApiError::bad_request("Missing password".to_string()))?;
407
408                 config.set_local_parameters(auth, request.name, request.federation_name, request.disable_base_fees)
409                    .await
410                    .map_err(|e| ApiError::bad_request(e.to_string()))
411            }
412        },
413        api_endpoint! {
414            ADD_PEER_SETUP_CODE_ENDPOINT,
415            ApiVersion::new(0, 0),
416            async |config: &SetupApi, context, info: String| -> String {
417                check_auth(context)?;
418
419                config.add_peer_setup_code(info.clone())
420                    .await
421                    .map_err(|e|ApiError::bad_request(e.to_string()))
422            }
423        },
424        api_endpoint! {
425            RESET_PEER_SETUP_CODES_ENDPOINT,
426            ApiVersion::new(0, 0),
427            async |config: &SetupApi, context, _v: ()| -> () {
428                check_auth(context)?;
429
430                config.reset_setup_codes().await;
431
432                Ok(())
433            }
434        },
435        api_endpoint! {
436            GET_SETUP_CODE_ENDPOINT,
437            ApiVersion::new(0, 0),
438            async |config: &SetupApi, context, _request: ()| -> Option<String> {
439                check_auth(context)?;
440
441                Ok(config.setup_code().await)
442            }
443        },
444        api_endpoint! {
445            START_DKG_ENDPOINT,
446            ApiVersion::new(0, 0),
447            async |config: &SetupApi, context, _v: ()| -> () {
448                check_auth(context)?;
449
450                config.start_dkg().await.map_err(|e| ApiError::server_error(e.to_string()))
451            }
452        },
453    ]
454}