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}
71
72impl LocalParams {
73    pub fn setup_code(&self) -> PeerSetupCode {
74        PeerSetupCode {
75            name: self.name.clone(),
76            endpoints: self.endpoints.clone(),
77            federation_name: self.federation_name.clone(),
78            disable_base_fees: self.disable_base_fees,
79            enabled_modules: self.enabled_modules.clone(),
80        }
81    }
82}
83
84/// Serves the config gen API endpoints
85#[derive(Clone)]
86pub struct SetupApi {
87    /// Our config gen settings configured locally
88    settings: ConfigGenSettings,
89    /// In-memory state machine
90    state: Arc<Mutex<SetupState>>,
91    /// DB not really used
92    db: Database,
93    /// Triggers the distributed key generation
94    sender: Sender<ConfigGenParams>,
95}
96
97impl SetupApi {
98    pub fn new(settings: ConfigGenSettings, db: Database, sender: Sender<ConfigGenParams>) -> Self {
99        Self {
100            settings,
101            state: Arc::new(Mutex::new(SetupState::default())),
102            db,
103            sender,
104        }
105    }
106
107    pub async fn setup_status(&self) -> SetupStatus {
108        match self.state.lock().await.local_params {
109            Some(..) => SetupStatus::SharingConnectionCodes,
110            None => SetupStatus::AwaitingLocalParams,
111        }
112    }
113}
114
115#[async_trait]
116impl ISetupApi for SetupApi {
117    async fn setup_code(&self) -> Option<String> {
118        self.state
119            .lock()
120            .await
121            .local_params
122            .as_ref()
123            .map(|lp| base32::encode_prefixed(FEDIMINT_PREFIX, &lp.setup_code()))
124    }
125
126    async fn auth(&self) -> Option<ApiAuth> {
127        self.state
128            .lock()
129            .await
130            .local_params
131            .as_ref()
132            .map(|lp| lp.auth.clone())
133    }
134
135    async fn connected_peers(&self) -> Vec<String> {
136        self.state
137            .lock()
138            .await
139            .setup_codes
140            .clone()
141            .into_iter()
142            .map(|info| info.name)
143            .collect()
144    }
145
146    fn available_modules(&self) -> BTreeSet<ModuleKind> {
147        self.settings.available_modules.clone()
148    }
149
150    async fn reset_setup_codes(&self) {
151        self.state.lock().await.setup_codes.clear();
152    }
153
154    async fn set_local_parameters(
155        &self,
156        auth: ApiAuth,
157        name: String,
158        federation_name: Option<String>,
159        disable_base_fees: Option<bool>,
160        enabled_modules: Option<BTreeSet<ModuleKind>>,
161    ) -> anyhow::Result<String> {
162        if let Some(existing_local_parameters) = self.state.lock().await.local_params.clone()
163            && existing_local_parameters.auth == auth
164            && existing_local_parameters.name == name
165            && existing_local_parameters.federation_name == federation_name
166            && existing_local_parameters.disable_base_fees == disable_base_fees
167            && existing_local_parameters.enabled_modules == enabled_modules
168        {
169            return Ok(base32::encode_prefixed(
170                FEDIMINT_PREFIX,
171                &existing_local_parameters.setup_code(),
172            ));
173        }
174
175        ensure!(!name.is_empty(), "The guardian name is empty");
176
177        ensure!(!auth.0.is_empty(), "The password is empty");
178
179        ensure!(
180            auth.0.trim() == auth.0,
181            "The password contains leading/trailing whitespace",
182        );
183
184        if let Some(federation_name) = federation_name.as_ref() {
185            ensure!(!federation_name.is_empty(), "The federation name is empty");
186        }
187
188        let mut state = self.state.lock().await;
189
190        ensure!(
191            state.local_params.is_none(),
192            "Local parameters have already been set"
193        );
194
195        let lp = if self.settings.enable_iroh {
196            let iroh_api_sk = if let Ok(var) = std::env::var(FM_IROH_API_SECRET_KEY_OVERRIDE_ENV) {
197                SecretKey::from_str(&var)
198                    .with_context(|| format!("Parsing {FM_IROH_API_SECRET_KEY_OVERRIDE_ENV}"))?
199            } else {
200                SecretKey::generate(&mut OsRng)
201            };
202
203            let iroh_p2p_sk = if let Ok(var) = std::env::var(FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV) {
204                SecretKey::from_str(&var)
205                    .with_context(|| format!("Parsing {FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV}"))?
206            } else {
207                SecretKey::generate(&mut OsRng)
208            };
209
210            LocalParams {
211                auth,
212                tls_key: None,
213                iroh_api_sk: Some(iroh_api_sk.clone()),
214                iroh_p2p_sk: Some(iroh_p2p_sk.clone()),
215                endpoints: PeerEndpoints::Iroh {
216                    api_pk: iroh_api_sk.public(),
217                    p2p_pk: iroh_p2p_sk.public(),
218                },
219                name,
220                federation_name,
221                disable_base_fees,
222                enabled_modules,
223            }
224        } else {
225            let (tls_cert, tls_key) =
226                gen_cert_and_key(&name).expect("Failed to generate TLS for given guardian name");
227
228            LocalParams {
229                auth,
230                tls_key: Some(tls_key),
231                iroh_api_sk: None,
232                iroh_p2p_sk: None,
233                endpoints: PeerEndpoints::Tcp {
234                    api_url: self
235                        .settings
236                        .api_url
237                        .clone()
238                        .ok_or_else(|| anyhow::format_err!("Api URL must be configured"))?,
239                    p2p_url: self
240                        .settings
241                        .p2p_url
242                        .clone()
243                        .ok_or_else(|| anyhow::format_err!("P2P URL must be configured"))?,
244
245                    cert: tls_cert.as_ref().to_vec(),
246                },
247                name,
248                federation_name,
249                disable_base_fees,
250                enabled_modules,
251            }
252        };
253
254        state.local_params = Some(lp.clone());
255
256        Ok(base32::encode_prefixed(FEDIMINT_PREFIX, &lp.setup_code()))
257    }
258
259    async fn add_peer_setup_code(&self, info: String) -> anyhow::Result<String> {
260        let info = base32::decode_prefixed(FEDIMINT_PREFIX, &info)?;
261
262        let mut state = self.state.lock().await;
263
264        if state.setup_codes.contains(&info) {
265            return Ok(info.name.clone());
266        }
267
268        let local_params = state
269            .local_params
270            .clone()
271            .expect("The endpoint is authenticated.");
272
273        ensure!(
274            info != local_params.setup_code(),
275            "You cannot add you own connection info"
276        );
277
278        ensure!(
279            discriminant(&info.endpoints) == discriminant(&local_params.endpoints),
280            "Guardian has different endpoint variant (TCP/Iroh) than us.",
281        );
282
283        if let Some(federation_name) = state
284            .setup_codes
285            .iter()
286            .chain(once(&local_params.setup_code()))
287            .find_map(|info| info.federation_name.clone())
288        {
289            ensure!(
290                info.federation_name.is_none(),
291                "Federation name has already been set to {federation_name}"
292            );
293        }
294
295        if let Some(disable_base_fees) = state
296            .setup_codes
297            .iter()
298            .chain(once(&local_params.setup_code()))
299            .find_map(|info| info.disable_base_fees)
300        {
301            ensure!(
302                info.disable_base_fees.is_none(),
303                "Base fees setting has already been configured to disabled={disable_base_fees}"
304            );
305        }
306
307        if state
308            .setup_codes
309            .iter()
310            .chain(once(&local_params.setup_code()))
311            .any(|info| info.enabled_modules.is_some())
312        {
313            ensure!(
314                info.enabled_modules.is_none(),
315                "Enabled modules have already been configured by another guardian"
316            );
317        }
318
319        state.setup_codes.insert(info.clone());
320
321        Ok(info.name)
322    }
323
324    async fn start_dkg(&self) -> anyhow::Result<()> {
325        let mut state = self.state.lock().await.clone();
326
327        let local_params = state
328            .local_params
329            .clone()
330            .expect("The endpoint is authenticated.");
331
332        let our_setup_code = local_params.setup_code();
333
334        state.setup_codes.insert(our_setup_code.clone());
335
336        ensure!(
337            state.setup_codes.len() == 1 || state.setup_codes.len() >= 4,
338            "The number of guardians is invalid"
339        );
340
341        let federation_name = state
342            .setup_codes
343            .iter()
344            .find_map(|info| info.federation_name.clone())
345            .context("We need one guardian to configure the federations name")?;
346
347        let disable_base_fees = state
348            .setup_codes
349            .iter()
350            .find_map(|info| info.disable_base_fees)
351            .unwrap_or(is_env_var_set(FM_DISABLE_BASE_FEES_ENV));
352
353        let enabled_modules = state
354            .setup_codes
355            .iter()
356            .find_map(|info| info.enabled_modules.clone())
357            .unwrap_or_else(|| self.settings.available_modules.clone());
358
359        let our_id = state
360            .setup_codes
361            .iter()
362            .position(|info| info == &our_setup_code)
363            .expect("We inserted the key above.");
364
365        let params = ConfigGenParams {
366            identity: PeerId::from(our_id as u16),
367            tls_key: local_params.tls_key,
368            iroh_api_sk: local_params.iroh_api_sk,
369            iroh_p2p_sk: local_params.iroh_p2p_sk,
370            api_auth: local_params.auth,
371            peers: (0..)
372                .map(|i| PeerId::from(i as u16))
373                .zip(state.setup_codes.clone().into_iter())
374                .collect(),
375            meta: BTreeMap::from_iter(vec![(
376                META_FEDERATION_NAME_KEY.to_string(),
377                federation_name,
378            )]),
379            disable_base_fees,
380            enabled_modules,
381            network: self.settings.network,
382        };
383
384        self.sender
385            .send(params)
386            .await
387            .context("Failed to send config gen params")?;
388
389        Ok(())
390    }
391}
392
393#[async_trait]
394impl HasApiContext<SetupApi> for SetupApi {
395    async fn context(
396        &self,
397        request: &ApiRequestErased,
398        id: Option<ModuleInstanceId>,
399    ) -> (&SetupApi, ApiEndpointContext) {
400        assert!(id.is_none());
401
402        let db = self.db.clone();
403
404        let is_authenticated = match self.state.lock().await.local_params {
405            None => false,
406            Some(ref params) => match request.auth.as_ref() {
407                Some(auth) => *auth == params.auth,
408                None => false,
409            },
410        };
411
412        let context = ApiEndpointContext::new(db, is_authenticated, request.auth.clone());
413
414        (self, context)
415    }
416}
417
418pub fn server_endpoints() -> Vec<ApiEndpoint<SetupApi>> {
419    vec![
420        api_endpoint! {
421            SETUP_STATUS_ENDPOINT,
422            ApiVersion::new(0, 0),
423            async |config: &SetupApi, _c, _v: ()| -> SetupStatus {
424                Ok(config.setup_status().await)
425            }
426        },
427        api_endpoint! {
428            SET_LOCAL_PARAMS_ENDPOINT,
429            ApiVersion::new(0, 0),
430            async |config: &SetupApi, context, request: SetLocalParamsRequest| -> String {
431                let auth = context
432                    .request_auth()
433                    .ok_or(ApiError::bad_request("Missing password".to_string()))?;
434
435                 config.set_local_parameters(auth, request.name, request.federation_name, request.disable_base_fees, request.enabled_modules)
436                    .await
437                    .map_err(|e| ApiError::bad_request(e.to_string()))
438            }
439        },
440        api_endpoint! {
441            ADD_PEER_SETUP_CODE_ENDPOINT,
442            ApiVersion::new(0, 0),
443            async |config: &SetupApi, context, info: String| -> String {
444                check_auth(context)?;
445
446                config.add_peer_setup_code(info.clone())
447                    .await
448                    .map_err(|e|ApiError::bad_request(e.to_string()))
449            }
450        },
451        api_endpoint! {
452            RESET_PEER_SETUP_CODES_ENDPOINT,
453            ApiVersion::new(0, 0),
454            async |config: &SetupApi, context, _v: ()| -> () {
455                check_auth(context)?;
456
457                config.reset_setup_codes().await;
458
459                Ok(())
460            }
461        },
462        api_endpoint! {
463            GET_SETUP_CODE_ENDPOINT,
464            ApiVersion::new(0, 0),
465            async |config: &SetupApi, context, _request: ()| -> Option<String> {
466                check_auth(context)?;
467
468                Ok(config.setup_code().await)
469            }
470        },
471        api_endpoint! {
472            START_DKG_ENDPOINT,
473            ApiVersion::new(0, 0),
474            async |config: &SetupApi, context, _v: ()| -> () {
475                check_auth(context)?;
476
477                config.start_dkg().await.map_err(|e| ApiError::server_error(e.to_string()))
478            }
479        },
480    ]
481}