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