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