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