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