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<rustls::PrivateKey>,
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            if 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
158        ensure!(!name.is_empty(), "The guardian name is empty");
159
160        ensure!(!auth.0.is_empty(), "The password is empty");
161
162        ensure!(
163            auth.0.trim() == auth.0,
164            "The password contains leading/trailing whitespace",
165        );
166
167        if let Some(federation_name) = federation_name.as_ref() {
168            ensure!(!federation_name.is_empty(), "The federation name is empty");
169        }
170
171        let mut state = self.state.lock().await;
172
173        ensure!(
174            state.local_params.is_none(),
175            "Local parameters have already been set"
176        );
177
178        let lp = if self.settings.enable_iroh {
179            warn!(target: LOG_SERVER, "Iroh support is experimental");
180
181            let iroh_api_sk = if let Ok(var) = std::env::var(FM_IROH_API_SECRET_KEY_OVERRIDE_ENV) {
182                SecretKey::from_str(&var)
183                    .with_context(|| format!("Parsing {FM_IROH_API_SECRET_KEY_OVERRIDE_ENV}"))?
184            } else {
185                SecretKey::generate(&mut OsRng)
186            };
187
188            let iroh_p2p_sk = if let Ok(var) = std::env::var(FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV) {
189                SecretKey::from_str(&var)
190                    .with_context(|| format!("Parsing {FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV}"))?
191            } else {
192                SecretKey::generate(&mut OsRng)
193            };
194
195            LocalParams {
196                auth,
197                tls_key: None,
198                iroh_api_sk: Some(iroh_api_sk.clone()),
199                iroh_p2p_sk: Some(iroh_p2p_sk.clone()),
200                endpoints: PeerEndpoints::Iroh {
201                    api_pk: iroh_api_sk.public(),
202                    p2p_pk: iroh_p2p_sk.public(),
203                },
204                name,
205                federation_name,
206            }
207        } else {
208            let (tls_cert, tls_key) =
209                gen_cert_and_key(&name).expect("Failed to generate TLS for given guardian name");
210
211            LocalParams {
212                auth,
213                tls_key: Some(tls_key),
214                iroh_api_sk: None,
215                iroh_p2p_sk: None,
216                endpoints: PeerEndpoints::Tcp {
217                    api_url: self
218                        .settings
219                        .api_url
220                        .clone()
221                        .ok_or_else(|| anyhow::format_err!("Api URL must be configured"))?,
222                    p2p_url: self
223                        .settings
224                        .p2p_url
225                        .clone()
226                        .ok_or_else(|| anyhow::format_err!("P2P URL must be configured"))?,
227
228                    cert: tls_cert.0,
229                },
230                name,
231                federation_name,
232            }
233        };
234
235        state.local_params = Some(lp.clone());
236
237        Ok(lp.setup_code().encode_base32())
238    }
239
240    async fn add_peer_setup_code(&self, info: String) -> anyhow::Result<String> {
241        let info = PeerSetupCode::decode_base32(&info)?;
242
243        let mut state = self.state.lock().await;
244
245        if state.setup_codes.contains(&info) {
246            return Ok(info.name.clone());
247        }
248
249        let local_params = state
250            .local_params
251            .clone()
252            .expect("The endpoint is authenticated.");
253
254        ensure!(
255            info != local_params.setup_code(),
256            "You cannot add you own connection info"
257        );
258
259        ensure!(
260            discriminant(&info.endpoints) == discriminant(&local_params.endpoints),
261            "Guardian has different endpoint variant (TCP/Iroh) than us.",
262        );
263
264        if let Some(federation_name) = state
265            .setup_codes
266            .iter()
267            .chain(once(&local_params.setup_code()))
268            .find_map(|info| info.federation_name.clone())
269        {
270            ensure!(
271                info.federation_name.is_none(),
272                "Federation name has already been set to {federation_name}"
273            );
274        }
275
276        state.setup_codes.insert(info.clone());
277
278        Ok(info.name)
279    }
280
281    async fn start_dkg(&self) -> anyhow::Result<()> {
282        let mut state = self.state.lock().await.clone();
283
284        let local_params = state
285            .local_params
286            .clone()
287            .expect("The endpoint is authenticated.");
288
289        let our_setup_code = local_params.setup_code();
290
291        state.setup_codes.insert(our_setup_code.clone());
292
293        ensure!(
294            state.setup_codes.len() == 1 || state.setup_codes.len() >= 4,
295            "The number of guardians is invalid"
296        );
297
298        let federation_name = state
299            .setup_codes
300            .iter()
301            .find_map(|info| info.federation_name.clone())
302            .context("We need one guardian to configure the federations name")?;
303
304        let our_id = state
305            .setup_codes
306            .iter()
307            .position(|info| info == &our_setup_code)
308            .expect("We inserted the key above.");
309
310        let params = ConfigGenParams {
311            identity: PeerId::from(our_id as u16),
312            tls_key: local_params.tls_key,
313            iroh_api_sk: local_params.iroh_api_sk,
314            iroh_p2p_sk: local_params.iroh_p2p_sk,
315            api_auth: local_params.auth,
316            peers: (0..)
317                .map(|i| PeerId::from(i as u16))
318                .zip(state.setup_codes.clone().into_iter())
319                .collect(),
320            meta: BTreeMap::from_iter(vec![(
321                META_FEDERATION_NAME_KEY.to_string(),
322                federation_name,
323            )]),
324        };
325
326        self.sender
327            .send(params)
328            .await
329            .context("Failed to send config gen params")?;
330
331        Ok(())
332    }
333}
334
335#[async_trait]
336impl HasApiContext<SetupApi> for SetupApi {
337    async fn context(
338        &self,
339        request: &ApiRequestErased,
340        id: Option<ModuleInstanceId>,
341    ) -> (&SetupApi, ApiEndpointContext<'_>) {
342        assert!(id.is_none());
343
344        let db = self.db.clone();
345        let dbtx = self.db.begin_transaction().await;
346
347        let is_authenticated = match self.state.lock().await.local_params {
348            None => false,
349            Some(ref params) => match request.auth.as_ref() {
350                Some(auth) => *auth == params.auth,
351                None => false,
352            },
353        };
354
355        let context = ApiEndpointContext::new(db, dbtx, is_authenticated, request.auth.clone());
356
357        (self, context)
358    }
359}
360
361pub fn server_endpoints() -> Vec<ApiEndpoint<SetupApi>> {
362    vec![
363        api_endpoint! {
364            SETUP_STATUS_ENDPOINT,
365            ApiVersion::new(0, 0),
366            async |config: &SetupApi, _c, _v: ()| -> SetupStatus {
367                Ok(config.setup_status().await)
368            }
369        },
370        api_endpoint! {
371            SET_LOCAL_PARAMS_ENDPOINT,
372            ApiVersion::new(0, 0),
373            async |config: &SetupApi, context, request: SetLocalParamsRequest| -> String {
374                let auth = context
375                    .request_auth()
376                    .ok_or(ApiError::bad_request("Missing password".to_string()))?;
377
378                 config.set_local_parameters(auth, request.name, request.federation_name)
379                    .await
380                    .map_err(|e| ApiError::bad_request(e.to_string()))
381            }
382        },
383        api_endpoint! {
384            ADD_PEER_SETUP_CODE_ENDPOINT,
385            ApiVersion::new(0, 0),
386            async |config: &SetupApi, context, info: String| -> String {
387                check_auth(context)?;
388
389                config.add_peer_setup_code(info.clone())
390                    .await
391                    .map_err(|e|ApiError::bad_request(e.to_string()))
392            }
393        },
394        api_endpoint! {
395            RESET_PEER_SETUP_CODES_ENDPOINT,
396            ApiVersion::new(0, 0),
397            async |config: &SetupApi, context, _v: ()| -> () {
398                check_auth(context)?;
399
400                config.reset_setup_codes().await;
401
402                Ok(())
403            }
404        },
405        api_endpoint! {
406            GET_SETUP_CODE_ENDPOINT,
407            ApiVersion::new(0, 0),
408            async |config: &SetupApi, context, _request: ()| -> Option<String> {
409                check_auth(context)?;
410
411                Ok(config.setup_code().await)
412            }
413        },
414        api_endpoint! {
415            START_DKG_ENDPOINT,
416            ApiVersion::new(0, 0),
417            async |config: &SetupApi, context, _v: ()| -> () {
418                check_auth(context)?;
419
420                config.start_dkg().await.map_err(|e| ApiError::server_error(e.to_string()))
421            }
422        },
423    ]
424}