Skip to main content

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, ModuleKind};
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::net::auth::check_auth;
26use fedimint_core::setup_code::PeerEndpoints;
27use fedimint_core::{PeerId, base32};
28use fedimint_server_core::setup_ui::ISetupApi;
29use iroh::SecretKey;
30use rand::rngs::OsRng;
31use tokio::sync::Mutex;
32use tokio::sync::mpsc::Sender;
33use tokio_rustls::rustls;
34
35use crate::config::{ConfigGenParams, ConfigGenSettings, PeerSetupCode};
36use crate::net::api::HasApiContext;
37use crate::net::p2p_connector::gen_cert_and_key;
38
39/// State held by the API after receiving a `ConfigGenConnectionsRequest`
40#[derive(Debug, Clone, Default)]
41pub struct SetupState {
42    /// Our local connection
43    local_params: Option<LocalParams>,
44    /// Connection info received from other guardians
45    setup_codes: BTreeSet<PeerSetupCode>,
46}
47
48#[derive(Clone, Debug)]
49/// Connection information sent between peers in order to start config gen
50pub struct LocalParams {
51    /// Our auth string
52    auth: ApiAuth,
53    /// Our TLS private key
54    tls_key: Option<Arc<rustls::pki_types::PrivateKeyDer<'static>>>,
55    /// Optional secret key for our iroh api endpoint
56    iroh_api_sk: Option<iroh::SecretKey>,
57    /// Optional secret key for our iroh p2p endpoint
58    iroh_p2p_sk: Option<iroh::SecretKey>,
59    /// Our api and p2p endpoint
60    endpoints: PeerEndpoints,
61    /// Name of the peer, used in TLS auth
62    name: String,
63    /// Federation name set by the leader
64    federation_name: Option<String>,
65    /// Whether to disable base fees, set by the leader
66    disable_base_fees: Option<bool>,
67    /// Modules enabled by the leader (if None, all available modules are
68    /// enabled)
69    enabled_modules: Option<BTreeSet<ModuleKind>>,
70    /// Total number of guardians (including the one who sets this), set by the
71    /// leader
72    federation_size: Option<u32>,
73}
74
75impl LocalParams {
76    pub fn setup_code(&self) -> PeerSetupCode {
77        PeerSetupCode {
78            name: self.name.clone(),
79            endpoints: self.endpoints.clone(),
80            federation_name: self.federation_name.clone(),
81            disable_base_fees: self.disable_base_fees,
82            enabled_modules: self.enabled_modules.clone(),
83            federation_size: self.federation_size,
84        }
85    }
86}
87
88/// Serves the config gen API endpoints
89#[derive(Clone)]
90pub struct SetupApi {
91    /// Our config gen settings configured locally
92    settings: ConfigGenSettings,
93    /// In-memory state machine
94    state: Arc<Mutex<SetupState>>,
95    /// DB not really used
96    db: Database,
97    /// Triggers the distributed key generation
98    sender: Sender<ConfigGenParams>,
99}
100
101impl SetupApi {
102    pub fn new(settings: ConfigGenSettings, db: Database, sender: Sender<ConfigGenParams>) -> Self {
103        Self {
104            settings,
105            state: Arc::new(Mutex::new(SetupState::default())),
106            db,
107            sender,
108        }
109    }
110
111    pub async fn setup_status(&self) -> SetupStatus {
112        match self.state.lock().await.local_params {
113            Some(..) => SetupStatus::SharingConnectionCodes,
114            None => SetupStatus::AwaitingLocalParams,
115        }
116    }
117}
118
119#[async_trait]
120impl ISetupApi for SetupApi {
121    async fn setup_code(&self) -> Option<String> {
122        self.state
123            .lock()
124            .await
125            .local_params
126            .as_ref()
127            .map(|lp| base32::encode_prefixed(FEDIMINT_PREFIX, &lp.setup_code()))
128    }
129
130    async fn guardian_name(&self) -> Option<String> {
131        self.state
132            .lock()
133            .await
134            .local_params
135            .as_ref()
136            .map(|lp| lp.name.clone())
137    }
138
139    async fn auth(&self) -> Option<ApiAuth> {
140        self.state
141            .lock()
142            .await
143            .local_params
144            .as_ref()
145            .map(|lp| lp.auth.clone())
146    }
147
148    async fn connected_peers(&self) -> Vec<String> {
149        self.state
150            .lock()
151            .await
152            .setup_codes
153            .clone()
154            .into_iter()
155            .map(|info| info.name)
156            .collect()
157    }
158
159    fn available_modules(&self) -> BTreeSet<ModuleKind> {
160        self.settings.available_modules.clone()
161    }
162
163    fn default_modules(&self) -> BTreeSet<ModuleKind> {
164        self.settings.default_modules.clone()
165    }
166
167    async fn reset_setup_codes(&self) {
168        self.state.lock().await.setup_codes.clear();
169    }
170
171    async fn set_local_parameters(
172        &self,
173        auth: ApiAuth,
174        name: String,
175        federation_name: Option<String>,
176        disable_base_fees: Option<bool>,
177        enabled_modules: Option<BTreeSet<ModuleKind>>,
178        federation_size: Option<u32>,
179    ) -> anyhow::Result<String> {
180        if let Some(existing_local_parameters) = self.state.lock().await.local_params.clone()
181            && existing_local_parameters.auth == auth
182            && existing_local_parameters.name == name
183            && existing_local_parameters.federation_name == federation_name
184            && existing_local_parameters.disable_base_fees == disable_base_fees
185            && existing_local_parameters.enabled_modules == enabled_modules
186            && existing_local_parameters.federation_size == federation_size
187        {
188            return Ok(base32::encode_prefixed(
189                FEDIMINT_PREFIX,
190                &existing_local_parameters.setup_code(),
191            ));
192        }
193
194        ensure!(!name.is_empty(), "The guardian name is empty");
195
196        ensure!(!auth.0.is_empty(), "The password is empty");
197
198        ensure!(
199            auth.0.trim() == auth.0,
200            "The password contains leading/trailing whitespace",
201        );
202
203        if let Some(federation_name) = federation_name.as_ref() {
204            ensure!(!federation_name.is_empty(), "The federation name is empty");
205        }
206
207        if federation_name.is_some() {
208            ensure!(
209                federation_size.is_some(),
210                "The leader must set the federation size"
211            );
212        }
213
214        if let Some(size) = federation_size {
215            ensure!(
216                size == 1 || 4 <= size,
217                "Federation size must be 1 or at least 4"
218            );
219        }
220
221        let mut state = self.state.lock().await;
222
223        ensure!(
224            state.local_params.is_none(),
225            "Local parameters have already been set"
226        );
227
228        let lp = if self.settings.enable_iroh {
229            let iroh_api_sk = if let Ok(var) = std::env::var(FM_IROH_API_SECRET_KEY_OVERRIDE_ENV) {
230                SecretKey::from_str(&var)
231                    .with_context(|| format!("Parsing {FM_IROH_API_SECRET_KEY_OVERRIDE_ENV}"))?
232            } else {
233                SecretKey::generate(&mut OsRng)
234            };
235
236            let iroh_p2p_sk = if let Ok(var) = std::env::var(FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV) {
237                SecretKey::from_str(&var)
238                    .with_context(|| format!("Parsing {FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV}"))?
239            } else {
240                SecretKey::generate(&mut OsRng)
241            };
242
243            LocalParams {
244                auth,
245                tls_key: None,
246                iroh_api_sk: Some(iroh_api_sk.clone()),
247                iroh_p2p_sk: Some(iroh_p2p_sk.clone()),
248                endpoints: PeerEndpoints::Iroh {
249                    api_pk: iroh_api_sk.public(),
250                    p2p_pk: iroh_p2p_sk.public(),
251                },
252                name,
253                federation_name,
254                disable_base_fees,
255                enabled_modules,
256                federation_size,
257            }
258        } else {
259            let (tls_cert, tls_key) =
260                gen_cert_and_key(&name).expect("Failed to generate TLS for given guardian name");
261
262            LocalParams {
263                auth,
264                tls_key: Some(tls_key),
265                iroh_api_sk: None,
266                iroh_p2p_sk: None,
267                endpoints: PeerEndpoints::Tcp {
268                    api_url: self
269                        .settings
270                        .api_url
271                        .clone()
272                        .ok_or_else(|| anyhow::format_err!("Api URL must be configured"))?,
273                    p2p_url: self
274                        .settings
275                        .p2p_url
276                        .clone()
277                        .ok_or_else(|| anyhow::format_err!("P2P URL must be configured"))?,
278
279                    cert: tls_cert.as_ref().to_vec(),
280                },
281                name,
282                federation_name,
283                disable_base_fees,
284                enabled_modules,
285                federation_size,
286            }
287        };
288
289        state.local_params = Some(lp.clone());
290
291        Ok(base32::encode_prefixed(FEDIMINT_PREFIX, &lp.setup_code()))
292    }
293
294    async fn add_peer_setup_code(&self, info: String) -> anyhow::Result<String> {
295        let info = base32::decode_prefixed(FEDIMINT_PREFIX, &info)?;
296
297        let mut state = self.state.lock().await;
298
299        if state.setup_codes.contains(&info) {
300            return Ok(info.name.clone());
301        }
302
303        let local_params = state
304            .local_params
305            .clone()
306            .expect("The endpoint is authenticated.");
307
308        ensure!(
309            info != local_params.setup_code(),
310            "You cannot add you own connection info"
311        );
312
313        ensure!(
314            discriminant(&info.endpoints) == discriminant(&local_params.endpoints),
315            "Guardian has different endpoint variant (TCP/Iroh) than us.",
316        );
317
318        if let Some(federation_name) = state
319            .setup_codes
320            .iter()
321            .chain(once(&local_params.setup_code()))
322            .find_map(|info| info.federation_name.clone())
323        {
324            ensure!(
325                info.federation_name.is_none(),
326                "Federation name has already been set to {federation_name}"
327            );
328        }
329
330        if let Some(disable_base_fees) = state
331            .setup_codes
332            .iter()
333            .chain(once(&local_params.setup_code()))
334            .find_map(|info| info.disable_base_fees)
335        {
336            ensure!(
337                info.disable_base_fees.is_none(),
338                "Base fees setting has already been configured to disabled={disable_base_fees}"
339            );
340        }
341
342        if state
343            .setup_codes
344            .iter()
345            .chain(once(&local_params.setup_code()))
346            .any(|info| info.enabled_modules.is_some())
347        {
348            ensure!(
349                info.enabled_modules.is_none(),
350                "Enabled modules have already been configured by another guardian"
351            );
352        }
353
354        if let Some(federation_size) = state
355            .setup_codes
356            .iter()
357            .chain(once(&local_params.setup_code()))
358            .find_map(|info| info.federation_size)
359        {
360            ensure!(
361                info.federation_size.is_none(),
362                "Federation size has already been set to {federation_size}"
363            );
364        }
365
366        state.setup_codes.insert(info.clone());
367
368        Ok(info.name)
369    }
370
371    async fn start_dkg(&self) -> anyhow::Result<()> {
372        let mut state = self.state.lock().await.clone();
373
374        let local_params = state
375            .local_params
376            .clone()
377            .expect("The endpoint is authenticated.");
378
379        let our_setup_code = local_params.setup_code();
380
381        state.setup_codes.insert(our_setup_code.clone());
382
383        ensure!(
384            state.setup_codes.len() == 1 || 4 <= state.setup_codes.len(),
385            "The number of guardians is invalid"
386        );
387
388        if let Some(federation_size) = state
389            .setup_codes
390            .iter()
391            .find_map(|info| info.federation_size)
392        {
393            ensure!(
394                state.setup_codes.len() == federation_size as usize,
395                "Expected {federation_size} guardians but got {}",
396                state.setup_codes.len()
397            );
398        }
399
400        let federation_name = state
401            .setup_codes
402            .iter()
403            .find_map(|info| info.federation_name.clone())
404            .context("We need one guardian to configure the federations name")?;
405
406        let disable_base_fees = state
407            .setup_codes
408            .iter()
409            .find_map(|info| info.disable_base_fees)
410            .unwrap_or(is_env_var_set(FM_DISABLE_BASE_FEES_ENV));
411
412        let enabled_modules = state
413            .setup_codes
414            .iter()
415            .find_map(|info| info.enabled_modules.clone())
416            .unwrap_or_else(|| self.settings.default_modules.clone());
417
418        let our_id = state
419            .setup_codes
420            .iter()
421            .position(|info| info == &our_setup_code)
422            .expect("We inserted the key above.");
423
424        let params = ConfigGenParams {
425            identity: PeerId::from(our_id as u16),
426            tls_key: local_params.tls_key,
427            iroh_api_sk: local_params.iroh_api_sk,
428            iroh_p2p_sk: local_params.iroh_p2p_sk,
429            api_auth: local_params.auth,
430            peers: (0..)
431                .map(|i| PeerId::from(i as u16))
432                .zip(state.setup_codes.clone().into_iter())
433                .collect(),
434            meta: BTreeMap::from_iter(vec![(
435                META_FEDERATION_NAME_KEY.to_string(),
436                federation_name,
437            )]),
438            disable_base_fees,
439            enabled_modules,
440            network: self.settings.network,
441        };
442
443        self.sender
444            .send(params)
445            .await
446            .context("Failed to send config gen params")?;
447
448        Ok(())
449    }
450
451    async fn federation_size(&self) -> Option<u32> {
452        let state = self.state.lock().await;
453        let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
454        state
455            .setup_codes
456            .iter()
457            .chain(local_setup_code.iter())
458            .find_map(|info| info.federation_size)
459    }
460
461    async fn cfg_federation_name(&self) -> Option<String> {
462        let state = self.state.lock().await;
463        let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
464        state
465            .setup_codes
466            .iter()
467            .chain(local_setup_code.iter())
468            .find_map(|info| info.federation_name.clone())
469    }
470
471    async fn cfg_base_fees_disabled(&self) -> Option<bool> {
472        let state = self.state.lock().await;
473        let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
474        state
475            .setup_codes
476            .iter()
477            .chain(local_setup_code.iter())
478            .find_map(|info| info.disable_base_fees)
479    }
480
481    async fn cfg_enabled_modules(&self) -> Option<BTreeSet<ModuleKind>> {
482        let state = self.state.lock().await;
483        let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
484        state
485            .setup_codes
486            .iter()
487            .chain(local_setup_code.iter())
488            .find_map(|info| info.enabled_modules.clone())
489    }
490}
491
492#[async_trait]
493impl HasApiContext<SetupApi> for SetupApi {
494    async fn context(
495        &self,
496        request: &ApiRequestErased,
497        id: Option<ModuleInstanceId>,
498    ) -> (&SetupApi, ApiEndpointContext) {
499        assert!(id.is_none());
500
501        let db = self.db.clone();
502
503        let is_authenticated = match self.state.lock().await.local_params {
504            None => false,
505            Some(ref params) => match request.auth.as_ref() {
506                Some(auth) => *auth == params.auth,
507                None => false,
508            },
509        };
510
511        let context = ApiEndpointContext::new(db, is_authenticated, request.auth.clone());
512
513        (self, context)
514    }
515}
516
517pub fn server_endpoints() -> Vec<ApiEndpoint<SetupApi>> {
518    vec![
519        api_endpoint! {
520            SETUP_STATUS_ENDPOINT,
521            ApiVersion::new(0, 0),
522            async |config: &SetupApi, _c, _v: ()| -> SetupStatus {
523                Ok(config.setup_status().await)
524            }
525        },
526        api_endpoint! {
527            SET_LOCAL_PARAMS_ENDPOINT,
528            ApiVersion::new(0, 0),
529            async |config: &SetupApi, context, request: SetLocalParamsRequest| -> String {
530                let auth = context
531                    .request_auth()
532                    .ok_or(ApiError::bad_request("Missing password".to_string()))?;
533
534                 config.set_local_parameters(auth, request.name, request.federation_name, request.disable_base_fees, request.enabled_modules, request.federation_size)
535                    .await
536                    .map_err(|e| ApiError::bad_request(e.to_string()))
537            }
538        },
539        api_endpoint! {
540            ADD_PEER_SETUP_CODE_ENDPOINT,
541            ApiVersion::new(0, 0),
542            async |config: &SetupApi, context, info: String| -> String {
543                check_auth(context)?;
544
545                config.add_peer_setup_code(info.clone())
546                    .await
547                    .map_err(|e|ApiError::bad_request(e.to_string()))
548            }
549        },
550        api_endpoint! {
551            RESET_PEER_SETUP_CODES_ENDPOINT,
552            ApiVersion::new(0, 0),
553            async |config: &SetupApi, context, _v: ()| -> () {
554                check_auth(context)?;
555
556                config.reset_setup_codes().await;
557
558                Ok(())
559            }
560        },
561        api_endpoint! {
562            GET_SETUP_CODE_ENDPOINT,
563            ApiVersion::new(0, 0),
564            async |config: &SetupApi, context, _request: ()| -> Option<String> {
565                check_auth(context)?;
566
567                Ok(config.setup_code().await)
568            }
569        },
570        api_endpoint! {
571            START_DKG_ENDPOINT,
572            ApiVersion::new(0, 0),
573            async |config: &SetupApi, context, _v: ()| -> () {
574                check_auth(context)?;
575
576                config.start_dkg().await.map_err(|e| ApiError::server_error(e.to_string()))
577            }
578        },
579    ]
580}