Skip to main content

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::admin_client::{SetLocalParamsRequest, SetupStatus};
10use fedimint_core::base32::FEDIMINT_PREFIX;
11use fedimint_core::config::META_FEDERATION_NAME_KEY;
12use fedimint_core::core::{ModuleInstanceId, ModuleKind};
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::net::auth::check_auth;
26use fedimint_core::setup_code::PeerEndpoints;
27use fedimint_core::{PeerId, base32};
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;
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    /// Whether to disable base fees, set by the leader
66    disable_base_fees: Option<bool>,
67    /// Modules enabled by the leader (if None, all available modules are
68    /// enabled)
69    enabled_modules: Option<BTreeSet<ModuleKind>>,
70    /// Total number of guardians (including the one who sets this), set by the
71    /// leader
72    federation_size: Option<u32>,
73    /// Bitcoin network configured locally
74    network: bitcoin::Network,
75}
76
77impl LocalParams {
78    pub fn setup_code(&self) -> PeerSetupCode {
79        PeerSetupCode {
80            name: self.name.clone(),
81            endpoints: self.endpoints.clone(),
82            federation_name: self.federation_name.clone(),
83            disable_base_fees: self.disable_base_fees,
84            enabled_modules: self.enabled_modules.clone(),
85            federation_size: self.federation_size,
86            network: self.network,
87        }
88    }
89}
90
91/// Serves the config gen API endpoints
92#[derive(Clone)]
93pub struct SetupApi {
94    /// Our config gen settings configured locally
95    settings: ConfigGenSettings,
96    /// In-memory state machine
97    state: Arc<Mutex<SetupState>>,
98    /// DB not really used
99    db: Database,
100    /// Triggers the distributed key generation
101    sender: Sender<ConfigGenParams>,
102}
103
104impl SetupApi {
105    pub fn new(settings: ConfigGenSettings, db: Database, sender: Sender<ConfigGenParams>) -> Self {
106        Self {
107            settings,
108            state: Arc::new(Mutex::new(SetupState::default())),
109            db,
110            sender,
111        }
112    }
113
114    pub async fn setup_status(&self) -> SetupStatus {
115        match self.state.lock().await.local_params {
116            Some(..) => SetupStatus::SharingConnectionCodes,
117            None => SetupStatus::AwaitingLocalParams,
118        }
119    }
120}
121
122#[async_trait]
123impl ISetupApi for SetupApi {
124    async fn setup_code(&self) -> Option<String> {
125        self.state
126            .lock()
127            .await
128            .local_params
129            .as_ref()
130            .map(|lp| base32::encode_prefixed(FEDIMINT_PREFIX, &lp.setup_code()))
131    }
132
133    async fn guardian_name(&self) -> Option<String> {
134        self.state
135            .lock()
136            .await
137            .local_params
138            .as_ref()
139            .map(|lp| lp.name.clone())
140    }
141
142    async fn auth(&self) -> Option<ApiAuth> {
143        self.state
144            .lock()
145            .await
146            .local_params
147            .as_ref()
148            .map(|lp| lp.auth.clone())
149    }
150
151    async fn connected_peers(&self) -> Vec<String> {
152        self.state
153            .lock()
154            .await
155            .setup_codes
156            .clone()
157            .into_iter()
158            .map(|info| info.name)
159            .collect()
160    }
161
162    fn available_modules(&self) -> BTreeSet<ModuleKind> {
163        self.settings.available_modules.clone()
164    }
165
166    fn default_modules(&self) -> BTreeSet<ModuleKind> {
167        self.settings.default_modules.clone()
168    }
169
170    async fn reset_setup_codes(&self) {
171        self.state.lock().await.setup_codes.clear();
172    }
173
174    async fn set_local_parameters(
175        &self,
176        auth: ApiAuth,
177        name: String,
178        federation_name: Option<String>,
179        disable_base_fees: Option<bool>,
180        enabled_modules: Option<BTreeSet<ModuleKind>>,
181        federation_size: Option<u32>,
182    ) -> anyhow::Result<String> {
183        if let Some(existing_local_parameters) = self.state.lock().await.local_params.clone()
184            && existing_local_parameters.auth.as_str() == auth.as_str()
185            && existing_local_parameters.name == name
186            && existing_local_parameters.federation_name == federation_name
187            && existing_local_parameters.disable_base_fees == disable_base_fees
188            && existing_local_parameters.enabled_modules == enabled_modules
189            && existing_local_parameters.federation_size == federation_size
190        {
191            return Ok(base32::encode_prefixed(
192                FEDIMINT_PREFIX,
193                &existing_local_parameters.setup_code(),
194            ));
195        }
196
197        ensure!(!name.is_empty(), "The guardian name is empty");
198
199        ensure!(!auth.as_str().is_empty(), "The password is empty");
200
201        ensure!(
202            auth.as_str().trim() == auth.as_str(),
203            "The password contains leading/trailing whitespace",
204        );
205
206        if let Some(federation_name) = federation_name.as_ref() {
207            ensure!(!federation_name.is_empty(), "The federation name is empty");
208        }
209
210        if federation_name.is_some() {
211            ensure!(
212                federation_size.is_some(),
213                "The leader must set the federation size"
214            );
215        }
216
217        if let Some(size) = federation_size {
218            ensure!(
219                size == 1 || 4 <= size,
220                "Federation size must be 1 or at least 4"
221            );
222        }
223
224        let mut state = self.state.lock().await;
225
226        ensure!(
227            state.local_params.is_none(),
228            "Local parameters have already been set"
229        );
230
231        let lp = if self.settings.enable_iroh {
232            let iroh_api_sk = if let Ok(var) = std::env::var(FM_IROH_API_SECRET_KEY_OVERRIDE_ENV) {
233                SecretKey::from_str(&var)
234                    .with_context(|| format!("Parsing {FM_IROH_API_SECRET_KEY_OVERRIDE_ENV}"))?
235            } else {
236                SecretKey::generate(&mut OsRng)
237            };
238
239            let iroh_p2p_sk = if let Ok(var) = std::env::var(FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV) {
240                SecretKey::from_str(&var)
241                    .with_context(|| format!("Parsing {FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV}"))?
242            } else {
243                SecretKey::generate(&mut OsRng)
244            };
245
246            LocalParams {
247                auth,
248                tls_key: None,
249                iroh_api_sk: Some(iroh_api_sk.clone()),
250                iroh_p2p_sk: Some(iroh_p2p_sk.clone()),
251                endpoints: PeerEndpoints::Iroh {
252                    api_pk: iroh_api_sk.public(),
253                    p2p_pk: iroh_p2p_sk.public(),
254                },
255                name,
256                federation_name,
257                disable_base_fees,
258                enabled_modules,
259                federation_size,
260                network: self.settings.network,
261            }
262        } else {
263            let (tls_cert, tls_key) =
264                gen_cert_and_key(&name).expect("Failed to generate TLS for given guardian name");
265
266            LocalParams {
267                auth,
268                tls_key: Some(tls_key),
269                iroh_api_sk: None,
270                iroh_p2p_sk: None,
271                endpoints: PeerEndpoints::Tcp {
272                    api_url: self
273                        .settings
274                        .api_url
275                        .clone()
276                        .ok_or_else(|| anyhow::format_err!("Api URL must be configured"))?,
277                    p2p_url: self
278                        .settings
279                        .p2p_url
280                        .clone()
281                        .ok_or_else(|| anyhow::format_err!("P2P URL must be configured"))?,
282
283                    cert: tls_cert.as_ref().to_vec(),
284                },
285                name,
286                federation_name,
287                disable_base_fees,
288                enabled_modules,
289                federation_size,
290                network: self.settings.network,
291            }
292        };
293
294        state.local_params = Some(lp.clone());
295
296        Ok(base32::encode_prefixed(FEDIMINT_PREFIX, &lp.setup_code()))
297    }
298
299    async fn add_peer_setup_code(&self, info: String) -> anyhow::Result<String> {
300        let info = base32::decode_prefixed(FEDIMINT_PREFIX, &info)?;
301
302        let mut state = self.state.lock().await;
303
304        if state.setup_codes.contains(&info) {
305            return Ok(info.name.clone());
306        }
307
308        let local_params = state
309            .local_params
310            .clone()
311            .expect("The endpoint is authenticated.");
312
313        ensure!(
314            info != local_params.setup_code(),
315            "You cannot add your own setup code"
316        );
317
318        ensure!(
319            discriminant(&info.endpoints) == discriminant(&local_params.endpoints),
320            "Guardian has different endpoint variant (TCP/Iroh) than us.",
321        );
322
323        ensure!(
324            info.network == local_params.network,
325            "Guardian uses Bitcoin network {} but we use {}",
326            info.network,
327            local_params.network,
328        );
329
330        if let Some(federation_name) = state
331            .setup_codes
332            .iter()
333            .chain(once(&local_params.setup_code()))
334            .find_map(|info| info.federation_name.clone())
335        {
336            ensure!(
337                info.federation_name.is_none(),
338                "Federation name has already been set to {federation_name}"
339            );
340        }
341
342        if let Some(disable_base_fees) = state
343            .setup_codes
344            .iter()
345            .chain(once(&local_params.setup_code()))
346            .find_map(|info| info.disable_base_fees)
347        {
348            ensure!(
349                info.disable_base_fees.is_none(),
350                "Base fees setting has already been configured to disabled={disable_base_fees}"
351            );
352        }
353
354        if state
355            .setup_codes
356            .iter()
357            .chain(once(&local_params.setup_code()))
358            .any(|info| info.enabled_modules.is_some())
359        {
360            ensure!(
361                info.enabled_modules.is_none(),
362                "Enabled modules have already been configured by another guardian"
363            );
364        }
365
366        if let Some(federation_size) = state
367            .setup_codes
368            .iter()
369            .chain(once(&local_params.setup_code()))
370            .find_map(|info| info.federation_size)
371        {
372            ensure!(
373                info.federation_size.is_none(),
374                "Federation size has already been set to {federation_size}"
375            );
376        }
377
378        state.setup_codes.insert(info.clone());
379
380        Ok(info.name)
381    }
382
383    async fn start_dkg(&self) -> anyhow::Result<()> {
384        let mut state = self.state.lock().await.clone();
385
386        let local_params = state
387            .local_params
388            .clone()
389            .expect("The endpoint is authenticated.");
390
391        let our_setup_code = local_params.setup_code();
392
393        state.setup_codes.insert(our_setup_code.clone());
394
395        ensure!(
396            state.setup_codes.len() == 1 || 4 <= state.setup_codes.len(),
397            "The number of guardians is invalid"
398        );
399
400        if let Some(federation_size) = state
401            .setup_codes
402            .iter()
403            .find_map(|info| info.federation_size)
404        {
405            ensure!(
406                state.setup_codes.len() == federation_size as usize,
407                "Expected {federation_size} guardians but got {}",
408                state.setup_codes.len()
409            );
410        }
411
412        let federation_name = state
413            .setup_codes
414            .iter()
415            .find_map(|info| info.federation_name.clone())
416            .context("We need one guardian to configure the federations name")?;
417
418        let disable_base_fees = state
419            .setup_codes
420            .iter()
421            .find_map(|info| info.disable_base_fees)
422            .unwrap_or(is_env_var_set(FM_DISABLE_BASE_FEES_ENV));
423
424        let enabled_modules = state
425            .setup_codes
426            .iter()
427            .find_map(|info| info.enabled_modules.clone())
428            .unwrap_or_else(|| self.settings.default_modules.clone());
429
430        let our_id = state
431            .setup_codes
432            .iter()
433            .position(|info| info == &our_setup_code)
434            .expect("We inserted the key above.");
435
436        let params = ConfigGenParams {
437            identity: PeerId::from(our_id as u16),
438            tls_key: local_params.tls_key,
439            iroh_api_sk: local_params.iroh_api_sk,
440            iroh_p2p_sk: local_params.iroh_p2p_sk,
441            api_auth: local_params.auth,
442            peers: (0..)
443                .map(|i| PeerId::from(i as u16))
444                .zip(state.setup_codes.clone())
445                .collect(),
446            meta: BTreeMap::from_iter(vec![(
447                META_FEDERATION_NAME_KEY.to_string(),
448                federation_name,
449            )]),
450            disable_base_fees,
451            enabled_modules,
452            network: local_params.network,
453        };
454
455        self.sender
456            .send(params)
457            .await
458            .context("Failed to send config gen params")?;
459
460        Ok(())
461    }
462
463    async fn federation_size(&self) -> Option<u32> {
464        let state = self.state.lock().await;
465        let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
466        state
467            .setup_codes
468            .iter()
469            .chain(local_setup_code.iter())
470            .find_map(|info| info.federation_size)
471    }
472
473    async fn cfg_federation_name(&self) -> Option<String> {
474        let state = self.state.lock().await;
475        let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
476        state
477            .setup_codes
478            .iter()
479            .chain(local_setup_code.iter())
480            .find_map(|info| info.federation_name.clone())
481    }
482
483    async fn cfg_base_fees_disabled(&self) -> Option<bool> {
484        let state = self.state.lock().await;
485        let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
486        state
487            .setup_codes
488            .iter()
489            .chain(local_setup_code.iter())
490            .find_map(|info| info.disable_base_fees)
491    }
492
493    async fn cfg_enabled_modules(&self) -> Option<BTreeSet<ModuleKind>> {
494        let state = self.state.lock().await;
495        let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
496        state
497            .setup_codes
498            .iter()
499            .chain(local_setup_code.iter())
500            .find_map(|info| info.enabled_modules.clone())
501    }
502}
503
504#[async_trait]
505impl HasApiContext<SetupApi> for SetupApi {
506    async fn context(
507        &self,
508        request: &ApiRequestErased,
509        id: Option<ModuleInstanceId>,
510    ) -> (&SetupApi, ApiEndpointContext) {
511        assert!(id.is_none());
512
513        let db = self.db.clone();
514
515        let is_authenticated = match self.state.lock().await.local_params {
516            None => false,
517            Some(ref params) => match request.auth.as_ref() {
518                Some(auth) => params.auth.verify(auth.as_str()),
519                None => false,
520            },
521        };
522
523        let context = ApiEndpointContext::new(db, is_authenticated, request.auth.clone());
524
525        (self, context)
526    }
527}
528
529pub fn server_endpoints() -> Vec<ApiEndpoint<SetupApi>> {
530    vec![
531        api_endpoint! {
532            SETUP_STATUS_ENDPOINT,
533            ApiVersion::new(0, 0),
534            async |config: &SetupApi, _c, _v: ()| -> SetupStatus {
535                Ok(config.setup_status().await)
536            }
537        },
538        api_endpoint! {
539            SET_LOCAL_PARAMS_ENDPOINT,
540            ApiVersion::new(0, 0),
541            async |config: &SetupApi, context, request: SetLocalParamsRequest| -> String {
542                let auth = context
543                    .request_auth()
544                    .ok_or(ApiError::bad_request("Missing password".to_string()))?;
545
546                 config.set_local_parameters(auth, request.name, request.federation_name, request.disable_base_fees, request.enabled_modules, request.federation_size)
547                    .await
548                    .map_err(|e| ApiError::bad_request(e.to_string()))
549            }
550        },
551        api_endpoint! {
552            ADD_PEER_SETUP_CODE_ENDPOINT,
553            ApiVersion::new(0, 0),
554            async |config: &SetupApi, context, info: String| -> String {
555                check_auth(context)?;
556
557                config.add_peer_setup_code(info.clone())
558                    .await
559                    .map_err(|e|ApiError::bad_request(e.to_string()))
560            }
561        },
562        api_endpoint! {
563            RESET_PEER_SETUP_CODES_ENDPOINT,
564            ApiVersion::new(0, 0),
565            async |config: &SetupApi, context, _v: ()| -> () {
566                check_auth(context)?;
567
568                config.reset_setup_codes().await;
569
570                Ok(())
571            }
572        },
573        api_endpoint! {
574            GET_SETUP_CODE_ENDPOINT,
575            ApiVersion::new(0, 0),
576            async |config: &SetupApi, context, _request: ()| -> Option<String> {
577                check_auth(context)?;
578
579                Ok(config.setup_code().await)
580            }
581        },
582        api_endpoint! {
583            START_DKG_ENDPOINT,
584            ApiVersion::new(0, 0),
585            async |config: &SetupApi, context, _v: ()| -> () {
586                check_auth(context)?;
587
588                config.start_dkg().await.map_err(|e| ApiError::server_error(e.to_string()))
589            }
590        },
591    ]
592}
593
594#[cfg(test)]
595mod tests {
596    use std::collections::BTreeSet;
597    use std::net::{IpAddr, Ipv4Addr, SocketAddr};
598
599    use bitcoin::Network;
600    use fedimint_core::db::IRawDatabaseExt;
601    use fedimint_core::db::mem_impl::MemDatabase;
602    use fedimint_core::module::ApiAuth;
603    use tokio::sync::mpsc;
604
605    use super::*;
606
607    fn setup_api(network: Network) -> SetupApi {
608        let (sender, _receiver) = mpsc::channel(1);
609        let bind = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
610
611        SetupApi::new(
612            ConfigGenSettings {
613                p2p_bind: bind,
614                api_bind: bind,
615                ui_bind: bind,
616                p2p_url: None,
617                api_url: None,
618                enable_iroh: true,
619                iroh_dns: None,
620                iroh_relays: Vec::new(),
621                network,
622                available_modules: BTreeSet::new(),
623                default_modules: BTreeSet::new(),
624            },
625            MemDatabase::new().into_database(),
626            sender,
627        )
628    }
629
630    async fn setup_code(api: &SetupApi, name: &str) -> String {
631        api.set_local_parameters(
632            ApiAuth::new(format!("{name}-password")),
633            name.to_string(),
634            None,
635            None,
636            None,
637            None,
638        )
639        .await
640        .expect("setting local parameters should succeed")
641    }
642
643    #[tokio::test]
644    async fn accepts_peer_setup_code_with_matching_network() {
645        let api = setup_api(Network::Regtest);
646        let peer_api = setup_api(Network::Regtest);
647
648        setup_code(&api, "local").await;
649        let peer_code = setup_code(&peer_api, "peer").await;
650
651        let added_peer = api
652            .add_peer_setup_code(peer_code)
653            .await
654            .expect("peer setup code with matching network should be accepted");
655
656        assert_eq!(added_peer, "peer");
657    }
658
659    #[tokio::test]
660    async fn rejects_peer_setup_code_with_different_network() {
661        let api = setup_api(Network::Regtest);
662        let peer_api = setup_api(Network::Signet);
663
664        setup_code(&api, "local").await;
665        let peer_code = setup_code(&peer_api, "peer").await;
666
667        let err = api
668            .add_peer_setup_code(peer_code)
669            .await
670            .expect_err("peer setup code with different network should be rejected");
671
672        assert!(
673            err.to_string()
674                .contains("Guardian uses Bitcoin network signet but we use regtest")
675        );
676    }
677}