Skip to main content

fedimint_server/config/
setup.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::io::Read as _;
3use std::iter::once;
4use std::mem::discriminant;
5use std::path::{Component, Path, PathBuf};
6use std::str::FromStr as _;
7use std::sync::Arc;
8
9use anyhow::{Context, ensure};
10use async_trait::async_trait;
11use fedimint_core::admin_client::{SetLocalParamsRequest, SetupStatus};
12use fedimint_core::base32::FEDIMINT_PREFIX;
13use fedimint_core::config::META_FEDERATION_NAME_KEY;
14use fedimint_core::core::{ModuleInstanceId, ModuleKind};
15use fedimint_core::db::Database;
16use fedimint_core::endpoint_constants::{
17    ADD_PEER_SETUP_CODE_ENDPOINT, GET_SETUP_CODE_ENDPOINT, RESET_PEER_SETUP_CODES_ENDPOINT,
18    SET_LOCAL_PARAMS_ENDPOINT, SETUP_STATUS_ENDPOINT, START_DKG_ENDPOINT,
19};
20use fedimint_core::envs::{
21    FM_DISABLE_BASE_FEES_ENV, FM_IROH_API_SECRET_KEY_OVERRIDE_ENV,
22    FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV, is_env_var_set,
23};
24use fedimint_core::module::{
25    ApiAuth, ApiEndpoint, ApiEndpointContext, ApiError, ApiRequestErased, ApiVersion,
26    admin_api_endpoint, public_api_endpoint,
27};
28use fedimint_core::setup_code::PeerEndpoints;
29use fedimint_core::version::DkgVersion;
30use fedimint_core::{PeerId, base32, runtime};
31use fedimint_server_core::setup_ui::ISetupApi;
32use iroh::SecretKey;
33use rand::rngs::OsRng;
34use tokio::sync::mpsc::Sender;
35use tokio::sync::{Mutex, oneshot};
36use tokio_rustls::rustls;
37
38use crate::config::io::{
39    CONSENSUS_CONFIG, ENCRYPTED_EXT, JSON_EXT, LOCAL_CONFIG, PRIVATE_CONFIG, SALT_FILE,
40    parse_legacy_encrypted_backup, parse_plaintext_backup,
41};
42use crate::config::{ConfigGenParams, ConfigGenSettings, PeerSetupCode, ServerConfig};
43use crate::net::api::HasApiContext;
44use crate::net::p2p_connector::gen_cert_and_key;
45
46/// Result sent from the setup API task to the main setup driver.
47///
48/// Normal DKG sends generated params directly. Restore sends the parsed config
49/// plus a oneshot acknowledgement so the HTTP handler can report success only
50/// after the main setup driver validates and writes the restored config.
51pub enum ConfigGenOutcome {
52    Generated(Box<ConfigGenParams>),
53    Restored(Box<ServerConfig>, oneshot::Sender<Result<(), String>>),
54}
55
56/// State held by the API after receiving a `ConfigGenConnectionsRequest`
57#[derive(Debug, Clone, Default)]
58pub struct SetupState {
59    /// Our local connection
60    local_params: Option<LocalParams>,
61    /// Connection info received from other guardians
62    setup_codes: BTreeSet<PeerSetupCode>,
63    /// Set while a backup restore is being processed
64    restore_in_progress: bool,
65}
66
67#[derive(Clone, Debug)]
68/// Connection information sent between peers in order to start config gen
69pub struct LocalParams {
70    /// Our TLS private key
71    tls_key: Option<Arc<rustls::pki_types::PrivateKeyDer<'static>>>,
72    /// Optional secret key for our iroh api endpoint
73    iroh_api_sk: Option<iroh::SecretKey>,
74    /// Optional secret key for our iroh p2p endpoint
75    iroh_p2p_sk: Option<iroh::SecretKey>,
76    /// Our api and p2p endpoint
77    endpoints: PeerEndpoints,
78    /// Name of the peer, used in TLS auth
79    name: String,
80    /// Federation name set by the leader
81    federation_name: Option<String>,
82    /// Whether to disable base fees, set by the leader
83    disable_base_fees: Option<bool>,
84    /// Modules enabled by the leader (if None, all available modules are
85    /// enabled)
86    enabled_modules: Option<BTreeSet<ModuleKind>>,
87    /// Total number of guardians (including the one who sets this), set by the
88    /// leader
89    federation_size: Option<u32>,
90    /// Bitcoin network configured locally
91    network: bitcoin::Network,
92    /// Normalized Fedimint version used for setup and DKG compatibility
93    fedimint_version: DkgVersion,
94}
95
96impl LocalParams {
97    pub fn setup_code(&self) -> PeerSetupCode {
98        PeerSetupCode {
99            name: self.name.clone(),
100            endpoints: self.endpoints.clone(),
101            federation_name: self.federation_name.clone(),
102            disable_base_fees: self.disable_base_fees,
103            enabled_modules: self.enabled_modules.clone(),
104            federation_size: self.federation_size,
105            network: self.network,
106            fedimint_version: self.fedimint_version.clone(),
107        }
108    }
109}
110
111fn ensure_fedimint_version_matches(
112    peer_setup_code: &PeerSetupCode,
113    local_fedimint_version: &DkgVersion,
114) -> anyhow::Result<()> {
115    ensure!(
116        peer_setup_code.fedimint_version.compatibility_version()
117            == local_fedimint_version.compatibility_version(),
118        "Guardian uses Fedimint version {} but we use {local_fedimint_version}",
119        peer_setup_code.fedimint_version,
120    );
121
122    Ok(())
123}
124
125/// Serves the config gen API endpoints
126#[derive(Clone)]
127pub struct SetupApi {
128    /// Our config gen settings configured locally
129    settings: ConfigGenSettings,
130    /// In-memory state machine
131    state: Arc<Mutex<SetupState>>,
132    /// DB not really used
133    db: Database,
134    /// Triggers config generation or config restore
135    sender: Sender<ConfigGenOutcome>,
136    /// Exact running version exposed by the setup API and recorded after setup
137    ///
138    /// Compatibility checks parse this boundary value into [`DkgVersion`].
139    code_version_str: String,
140    /// Git hash of the running fedimintd binary
141    code_version_hash: String,
142    /// Password protecting the setup UI login form. `None` ⇒ no login.
143    auth_ui: Option<ApiAuth>,
144    /// Password protecting setup admin RPCs over WS/iroh. `None` ⇒ 401.
145    auth_api: Option<ApiAuth>,
146}
147
148impl SetupApi {
149    pub fn new(
150        settings: ConfigGenSettings,
151        db: Database,
152        sender: Sender<ConfigGenOutcome>,
153        code_version_str: String,
154        code_version_hash: String,
155        auth_ui: Option<ApiAuth>,
156        auth_api: Option<ApiAuth>,
157    ) -> Self {
158        Self {
159            settings,
160            state: Arc::new(Mutex::new(SetupState::default())),
161            db,
162            sender,
163            code_version_str,
164            code_version_hash,
165            auth_ui,
166            auth_api,
167        }
168    }
169
170    pub async fn setup_status(&self) -> SetupStatus {
171        match self.state.lock().await.local_params {
172            Some(..) => SetupStatus::SharingConnectionCodes,
173            None => SetupStatus::AwaitingLocalParams,
174        }
175    }
176}
177
178fn is_expected_backup_path(path: &Path) -> bool {
179    let expected_paths = [
180        PathBuf::from(LOCAL_CONFIG).with_extension(JSON_EXT),
181        PathBuf::from(CONSENSUS_CONFIG).with_extension(JSON_EXT),
182        PathBuf::from(PRIVATE_CONFIG).with_extension(JSON_EXT),
183        PathBuf::from(PRIVATE_CONFIG).with_extension(ENCRYPTED_EXT),
184        PathBuf::from(SALT_FILE),
185    ];
186
187    expected_paths.iter().any(|expected| expected == path)
188}
189
190/// Parse a guardian backup tar into a [`ServerConfig`] entirely in memory.
191///
192/// Validates archive paths and rejects missing, unexpected, duplicate, or
193/// non-file entries. Two backup formats are supported: the current plaintext
194/// format (containing `private.json`) and the legacy encrypted format
195/// (containing `private.encrypt` + `private.salt`), which requires the guardian
196/// password used when the backup was created. Nothing is written to disk; the
197/// caller writes the validated config into the data directory just like a
198/// freshly generated config.
199fn parse_backup(backup: &[u8], password: Option<&str>) -> anyhow::Result<ServerConfig> {
200    let mut archive = tar::Archive::new(backup);
201    let mut files: BTreeMap<PathBuf, Vec<u8>> = BTreeMap::new();
202
203    for entry in archive.entries().context("Reading backup archive")? {
204        let mut entry = entry.context("Reading backup archive entry")?;
205        let path = entry
206            .path()
207            .context("Reading backup archive entry path")?
208            .into_owned();
209        ensure!(
210            path.components()
211                .all(|component| matches!(component, Component::Normal(_))),
212            "Backup archive contains an invalid path"
213        );
214        ensure!(
215            is_expected_backup_path(&path),
216            "Backup archive contains unexpected file {}",
217            path.display()
218        );
219        ensure!(
220            entry.header().entry_type().is_file(),
221            "Backup archive contains non-file entry {}",
222            path.display()
223        );
224
225        let mut bytes = Vec::new();
226        entry
227            .read_to_end(&mut bytes)
228            .context("Reading backup archive entry contents")?;
229        ensure!(
230            files.insert(path.clone(), bytes).is_none(),
231            "Backup archive contains duplicate file {}",
232            path.display()
233        );
234    }
235
236    let local_config = PathBuf::from(LOCAL_CONFIG).with_extension(JSON_EXT);
237    let consensus_config = PathBuf::from(CONSENSUS_CONFIG).with_extension(JSON_EXT);
238    let private_config_json = PathBuf::from(PRIVATE_CONFIG).with_extension(JSON_EXT);
239    let private_config_encrypted = PathBuf::from(PRIVATE_CONFIG).with_extension(ENCRYPTED_EXT);
240    let salt_file = PathBuf::from(SALT_FILE);
241
242    let local = files
243        .get(&local_config)
244        .with_context(|| format!("Backup archive is missing {}", local_config.display()))?;
245    let consensus = files
246        .get(&consensus_config)
247        .with_context(|| format!("Backup archive is missing {}", consensus_config.display()))?;
248
249    // Both formats are parsed into a plaintext config in memory, which the
250    // caller then writes out exactly like a freshly generated config, so a
251    // restored guardian looks like a fresh post-migration setup.
252    if let Some(private) = files.get(&private_config_json) {
253        // Current plaintext format. No password is needed; any supplied
254        // password is ignored.
255        parse_plaintext_backup(local, consensus, private).context("Reading restored config")
256    } else if let Some(private) = files.get(&private_config_encrypted) {
257        // Legacy encrypted format. Requires the salt file and the password used
258        // when the backup was created.
259        let salt = files
260            .get(&salt_file)
261            .with_context(|| format!("Backup archive is missing {}", salt_file.display()))?;
262        let password = password.context(
263            "This backup is encrypted, please provide the guardian password used when it was created",
264        )?;
265        parse_legacy_encrypted_backup(local, consensus, private, salt, password)
266            .context("Reading restored config")
267    } else {
268        anyhow::bail!("Backup archive is missing the private config");
269    }
270}
271
272#[async_trait]
273impl ISetupApi for SetupApi {
274    async fn setup_code(&self) -> Option<String> {
275        self.state
276            .lock()
277            .await
278            .local_params
279            .as_ref()
280            .map(|lp| base32::encode_prefixed(FEDIMINT_PREFIX, &lp.setup_code()))
281    }
282
283    async fn guardian_name(&self) -> Option<String> {
284        self.state
285            .lock()
286            .await
287            .local_params
288            .as_ref()
289            .map(|lp| lp.name.clone())
290    }
291
292    fn auth_ui(&self) -> Option<ApiAuth> {
293        self.auth_ui.clone()
294    }
295
296    async fn connected_peers(&self) -> Vec<String> {
297        self.state
298            .lock()
299            .await
300            .setup_codes
301            .clone()
302            .into_iter()
303            .map(|info| info.name)
304            .collect()
305    }
306
307    fn available_modules(&self) -> BTreeSet<ModuleKind> {
308        self.settings.available_modules.clone()
309    }
310
311    fn default_modules(&self) -> BTreeSet<ModuleKind> {
312        self.settings.default_modules.clone()
313    }
314
315    async fn reset_setup_codes(&self) {
316        self.state.lock().await.setup_codes.clear();
317    }
318
319    async fn set_local_parameters(
320        &self,
321        name: String,
322        federation_name: Option<String>,
323        disable_base_fees: Option<bool>,
324        enabled_modules: Option<BTreeSet<ModuleKind>>,
325        federation_size: Option<u32>,
326    ) -> anyhow::Result<String> {
327        if let Some(existing_local_parameters) = self.state.lock().await.local_params.clone()
328            && existing_local_parameters.name == name
329            && existing_local_parameters.federation_name == federation_name
330            && existing_local_parameters.disable_base_fees == disable_base_fees
331            && existing_local_parameters.enabled_modules == enabled_modules
332            && existing_local_parameters.federation_size == federation_size
333        {
334            return Ok(base32::encode_prefixed(
335                FEDIMINT_PREFIX,
336                &existing_local_parameters.setup_code(),
337            ));
338        }
339
340        ensure!(!name.is_empty(), "The guardian name is empty");
341
342        if let Some(federation_name) = federation_name.as_ref() {
343            ensure!(!federation_name.is_empty(), "The federation name is empty");
344        }
345
346        if federation_name.is_some() {
347            ensure!(
348                federation_size.is_some(),
349                "The leader must set the federation size"
350            );
351        }
352
353        if let Some(size) = federation_size {
354            ensure!(
355                size == 1 || 4 <= size,
356                "Federation size must be 1 or at least 4"
357            );
358        }
359
360        let mut state = self.state.lock().await;
361
362        ensure!(
363            state.local_params.is_none(),
364            "Local parameters have already been set"
365        );
366
367        ensure!(
368            !state.restore_in_progress,
369            "A restore is already in progress"
370        );
371
372        let fedimint_version =
373            DkgVersion::parse(&self.code_version_str).context("Invalid local Fedimint version")?;
374
375        let lp = if self.settings.enable_iroh {
376            let iroh_api_sk = if let Ok(var) = std::env::var(FM_IROH_API_SECRET_KEY_OVERRIDE_ENV) {
377                SecretKey::from_str(&var)
378                    .with_context(|| format!("Parsing {FM_IROH_API_SECRET_KEY_OVERRIDE_ENV}"))?
379            } else {
380                SecretKey::generate(&mut OsRng)
381            };
382
383            let iroh_p2p_sk = if let Ok(var) = std::env::var(FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV) {
384                SecretKey::from_str(&var)
385                    .with_context(|| format!("Parsing {FM_IROH_P2P_SECRET_KEY_OVERRIDE_ENV}"))?
386            } else {
387                SecretKey::generate(&mut OsRng)
388            };
389
390            LocalParams {
391                tls_key: None,
392                iroh_api_sk: Some(iroh_api_sk.clone()),
393                iroh_p2p_sk: Some(iroh_p2p_sk.clone()),
394                endpoints: PeerEndpoints::Iroh {
395                    api_pk: iroh_api_sk.public(),
396                    p2p_pk: iroh_p2p_sk.public(),
397                },
398                name,
399                federation_name,
400                disable_base_fees,
401                enabled_modules,
402                federation_size,
403                network: self.settings.network,
404                fedimint_version: fedimint_version.clone(),
405            }
406        } else {
407            let (tls_cert, tls_key) =
408                gen_cert_and_key(&name).expect("Failed to generate TLS for given guardian name");
409
410            LocalParams {
411                tls_key: Some(tls_key),
412                iroh_api_sk: None,
413                iroh_p2p_sk: None,
414                endpoints: PeerEndpoints::Tcp {
415                    api_url: self
416                        .settings
417                        .api_url
418                        .clone()
419                        .ok_or_else(|| anyhow::format_err!("Api URL must be configured"))?,
420                    p2p_url: self
421                        .settings
422                        .p2p_url
423                        .clone()
424                        .ok_or_else(|| anyhow::format_err!("P2P URL must be configured"))?,
425
426                    cert: tls_cert.as_ref().to_vec(),
427                },
428                name,
429                federation_name,
430                disable_base_fees,
431                enabled_modules,
432                federation_size,
433                network: self.settings.network,
434                fedimint_version,
435            }
436        };
437
438        state.local_params = Some(lp.clone());
439
440        Ok(base32::encode_prefixed(FEDIMINT_PREFIX, &lp.setup_code()))
441    }
442
443    async fn add_peer_setup_code(&self, info: String) -> anyhow::Result<String> {
444        let info = base32::decode_prefixed(FEDIMINT_PREFIX, &info)?;
445
446        let mut state = self.state.lock().await;
447
448        if state.setup_codes.contains(&info) {
449            return Ok(info.name.clone());
450        }
451
452        ensure!(
453            !state.restore_in_progress,
454            "A restore is already in progress"
455        );
456
457        let local_params = state
458            .local_params
459            .clone()
460            .expect("The endpoint is authenticated.");
461
462        ensure!(
463            info != local_params.setup_code(),
464            "You cannot add your own setup code"
465        );
466
467        ensure!(
468            discriminant(&info.endpoints) == discriminant(&local_params.endpoints),
469            "Guardian has different endpoint variant (TCP/Iroh) than us.",
470        );
471
472        ensure_fedimint_version_matches(&info, &local_params.fedimint_version)?;
473
474        ensure!(
475            info.network == local_params.network,
476            "Guardian uses Bitcoin network {} but we use {}",
477            info.network,
478            local_params.network,
479        );
480
481        if let Some(federation_name) = state
482            .setup_codes
483            .iter()
484            .chain(once(&local_params.setup_code()))
485            .find_map(|info| info.federation_name.clone())
486        {
487            ensure!(
488                info.federation_name.is_none(),
489                "Federation name has already been set to {federation_name}"
490            );
491        }
492
493        if let Some(disable_base_fees) = state
494            .setup_codes
495            .iter()
496            .chain(once(&local_params.setup_code()))
497            .find_map(|info| info.disable_base_fees)
498        {
499            ensure!(
500                info.disable_base_fees.is_none(),
501                "Base fees setting has already been configured to disabled={disable_base_fees}"
502            );
503        }
504
505        if state
506            .setup_codes
507            .iter()
508            .chain(once(&local_params.setup_code()))
509            .any(|info| info.enabled_modules.is_some())
510        {
511            ensure!(
512                info.enabled_modules.is_none(),
513                "Enabled modules have already been configured by another guardian"
514            );
515        }
516
517        if let Some(federation_size) = state
518            .setup_codes
519            .iter()
520            .chain(once(&local_params.setup_code()))
521            .find_map(|info| info.federation_size)
522        {
523            ensure!(
524                info.federation_size.is_none(),
525                "Federation size has already been set to {federation_size}"
526            );
527        }
528
529        state.setup_codes.insert(info.clone());
530
531        Ok(info.name)
532    }
533
534    async fn start_dkg(&self) -> anyhow::Result<()> {
535        let mut state = self.state.lock().await.clone();
536
537        ensure!(
538            !state.restore_in_progress,
539            "A restore is already in progress"
540        );
541
542        let local_params = state
543            .local_params
544            .clone()
545            .expect("The endpoint is authenticated.");
546
547        let our_setup_code = local_params.setup_code();
548
549        state.setup_codes.insert(our_setup_code.clone());
550
551        for setup_code in &state.setup_codes {
552            ensure_fedimint_version_matches(setup_code, &local_params.fedimint_version)?;
553        }
554
555        ensure!(
556            state.setup_codes.len() == 1 || 4 <= state.setup_codes.len(),
557            "The number of guardians is invalid"
558        );
559
560        if let Some(federation_size) = state
561            .setup_codes
562            .iter()
563            .find_map(|info| info.federation_size)
564        {
565            ensure!(
566                state.setup_codes.len() == federation_size as usize,
567                "Expected {federation_size} guardians but got {}",
568                state.setup_codes.len()
569            );
570        }
571
572        let federation_name = state
573            .setup_codes
574            .iter()
575            .find_map(|info| info.federation_name.clone())
576            .context("We need one guardian to configure the federations name")?;
577
578        let disable_base_fees = state
579            .setup_codes
580            .iter()
581            .find_map(|info| info.disable_base_fees)
582            .unwrap_or(is_env_var_set(FM_DISABLE_BASE_FEES_ENV));
583
584        let enabled_modules = state
585            .setup_codes
586            .iter()
587            .find_map(|info| info.enabled_modules.clone())
588            .unwrap_or_else(|| self.settings.default_modules.clone());
589
590        let our_id = state
591            .setup_codes
592            .iter()
593            .position(|info| info == &our_setup_code)
594            .expect("We inserted the key above.");
595
596        let params = ConfigGenParams {
597            identity: PeerId::from(our_id as u16),
598            tls_key: local_params.tls_key,
599            iroh_api_sk: local_params.iroh_api_sk,
600            iroh_p2p_sk: local_params.iroh_p2p_sk,
601            peers: (0..)
602                .map(|i| PeerId::from(i as u16))
603                .zip(state.setup_codes.clone())
604                .collect(),
605            meta: BTreeMap::from_iter(vec![(
606                META_FEDERATION_NAME_KEY.to_string(),
607                federation_name,
608            )]),
609            disable_base_fees,
610            enabled_modules,
611            network: local_params.network,
612        };
613
614        self.sender
615            .send(ConfigGenOutcome::Generated(Box::new(params)))
616            .await
617            .context("Failed to send config gen params")?;
618
619        Ok(())
620    }
621
622    async fn restore_from_backup(
623        &self,
624        password: Option<String>,
625        backup: Vec<u8>,
626    ) -> anyhow::Result<()> {
627        if let Some(password) = &password {
628            ensure!(!password.is_empty(), "The password is empty");
629            ensure!(
630                password.trim() == password,
631                "The password contains leading/trailing whitespace",
632            );
633        }
634        {
635            let mut state = self.state.lock().await;
636            ensure!(
637                state.local_params.is_none(),
638                "Local parameters have already been set"
639            );
640            ensure!(
641                !state.restore_in_progress,
642                "A restore is already in progress"
643            );
644            state.restore_in_progress = true;
645        }
646
647        let state = self.state.clone();
648        let sender = self.sender.clone();
649        runtime::spawn("restore guardian backup", async move {
650            let result = async {
651                let cfg =
652                    tokio::task::spawn_blocking(move || parse_backup(&backup, password.as_deref()))
653                        .await
654                        .context("Restore backup task panicked")??;
655                let (restore_result_sender, restore_result_receiver) = oneshot::channel();
656                let restored = ConfigGenOutcome::Restored(Box::new(cfg), restore_result_sender);
657                if sender.send(restored).await.is_err() {
658                    return Err(anyhow::format_err!("Failed to send restored config"));
659                }
660                restore_result_receiver
661                    .await
662                    .context("Restore result sender dropped")?
663                    .map_err(anyhow::Error::msg)?;
664                Ok(())
665            }
666            .await;
667
668            if result.is_err() {
669                state.lock().await.restore_in_progress = false;
670            }
671            // On success, the setup task consumes the restored config and exits setup mode,
672            // so there is no setup API left that could observe or reset
673            // `restore_in_progress`.
674
675            result
676        })
677        .await
678        .context("Restore task panicked")?
679    }
680
681    async fn federation_size(&self) -> Option<u32> {
682        let state = self.state.lock().await;
683        let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
684        state
685            .setup_codes
686            .iter()
687            .chain(local_setup_code.iter())
688            .find_map(|info| info.federation_size)
689    }
690
691    async fn cfg_federation_name(&self) -> Option<String> {
692        let state = self.state.lock().await;
693        let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
694        state
695            .setup_codes
696            .iter()
697            .chain(local_setup_code.iter())
698            .find_map(|info| info.federation_name.clone())
699    }
700
701    async fn cfg_base_fees_disabled(&self) -> Option<bool> {
702        let state = self.state.lock().await;
703        let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
704        state
705            .setup_codes
706            .iter()
707            .chain(local_setup_code.iter())
708            .find_map(|info| info.disable_base_fees)
709    }
710
711    async fn cfg_enabled_modules(&self) -> Option<BTreeSet<ModuleKind>> {
712        let state = self.state.lock().await;
713        let local_setup_code = state.local_params.as_ref().map(LocalParams::setup_code);
714        state
715            .setup_codes
716            .iter()
717            .chain(local_setup_code.iter())
718            .find_map(|info| info.enabled_modules.clone())
719    }
720
721    async fn fedimintd_version(&self) -> String {
722        self.code_version_str.clone()
723    }
724
725    async fn fedimintd_version_hash(&self) -> Option<String> {
726        fedimint_core::version::non_zero_version_hash(&self.code_version_hash).map(str::to_owned)
727    }
728}
729
730#[async_trait]
731impl HasApiContext<SetupApi> for SetupApi {
732    async fn context(
733        &self,
734        request: &ApiRequestErased,
735        id: Option<ModuleInstanceId>,
736    ) -> (&SetupApi, ApiEndpointContext) {
737        assert!(id.is_none());
738
739        let db = self.db.clone();
740
741        let is_authenticated = match (&self.auth_api, &request.auth) {
742            (Some(server_auth), Some(req_auth)) => server_auth.verify(req_auth.as_str()),
743            _ => false,
744        };
745
746        let context = ApiEndpointContext::new(db, is_authenticated);
747
748        (self, context)
749    }
750}
751
752pub fn server_endpoints() -> Vec<ApiEndpoint<SetupApi>> {
753    vec![
754        public_api_endpoint! {
755            SETUP_STATUS_ENDPOINT,
756            ApiVersion::new(0, 0),
757            async |config: &SetupApi, _c, _v: ()| -> SetupStatus {
758                Ok(config.setup_status().await)
759            }
760        },
761        admin_api_endpoint! {
762            SET_LOCAL_PARAMS_ENDPOINT,
763            ApiVersion::new(0, 0),
764            async |config: &SetupApi, context, request: SetLocalParamsRequest| -> String {
765
766                 config.set_local_parameters(request.name, request.federation_name, request.disable_base_fees, request.enabled_modules, request.federation_size)
767                    .await
768                    .map_err(|e| ApiError::bad_request(e.to_string()))
769            }
770        },
771        admin_api_endpoint! {
772            ADD_PEER_SETUP_CODE_ENDPOINT,
773            ApiVersion::new(0, 0),
774            async |config: &SetupApi, context, info: String| -> String {
775
776                config.add_peer_setup_code(info.clone())
777                    .await
778                    .map_err(|e|ApiError::bad_request(e.to_string()))
779            }
780        },
781        admin_api_endpoint! {
782            RESET_PEER_SETUP_CODES_ENDPOINT,
783            ApiVersion::new(0, 0),
784            async |config: &SetupApi, context, _v: ()| -> () {
785
786                config.reset_setup_codes().await;
787
788                Ok(())
789            }
790        },
791        admin_api_endpoint! {
792            GET_SETUP_CODE_ENDPOINT,
793            ApiVersion::new(0, 0),
794            async |config: &SetupApi, context, _request: ()| -> Option<String> {
795
796                Ok(config.setup_code().await)
797            }
798        },
799        admin_api_endpoint! {
800            START_DKG_ENDPOINT,
801            ApiVersion::new(0, 0),
802            async |config: &SetupApi, context, _v: ()| -> () {
803
804                config.start_dkg().await.map_err(|e| ApiError::server_error(e.to_string()))
805            }
806        },
807    ]
808}
809
810#[cfg(test)]
811mod tests {
812    use std::collections::BTreeSet;
813    use std::net::{IpAddr, Ipv4Addr, SocketAddr};
814
815    use base64::Engine as _;
816    use bitcoin::Network;
817    use fedimint_core::db::IRawDatabaseExt;
818    use fedimint_core::db::mem_impl::MemDatabase;
819    use tokio::sync::mpsc::{self, Receiver};
820
821    use super::*;
822
823    fn setup_api(network: Network) -> SetupApi {
824        setup_api_with_version(network, "1.2.3-alpha")
825    }
826
827    fn setup_api_with_version(network: Network, version: &str) -> SetupApi {
828        setup_api_with_version_and_receiver(network, version).0
829    }
830
831    fn setup_api_with_version_and_receiver(
832        network: Network,
833        version: &str,
834    ) -> (SetupApi, Receiver<ConfigGenOutcome>) {
835        let (sender, receiver) = mpsc::channel(1);
836        let bind = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0);
837
838        (
839            SetupApi::new(
840                ConfigGenSettings {
841                    p2p_bind: bind,
842                    api_bind: bind,
843                    ui_bind: bind,
844                    p2p_url: None,
845                    api_url: None,
846                    enable_iroh: true,
847                    iroh_dns: None,
848                    iroh_relays: Vec::new(),
849                    network,
850                    available_modules: BTreeSet::new(),
851                    default_modules: BTreeSet::new(),
852                },
853                MemDatabase::new().into_database(),
854                sender,
855                version.to_owned(),
856                String::new(),
857                None,
858                None,
859            ),
860            receiver,
861        )
862    }
863
864    const INVALID_RESTORE_BACKUP_FIXTURE_B64: &str =
865        include_str!("../test_fixtures/guardian-backup-invalid-config.tar.b64");
866
867    async fn setup_code(api: &SetupApi, name: &str) -> String {
868        api.set_local_parameters(name.to_string(), None, None, None, None)
869            .await
870            .expect("setting local parameters should succeed")
871    }
872
873    fn decode_setup_code(setup_code: &str) -> PeerSetupCode {
874        base32::decode_prefixed(FEDIMINT_PREFIX, setup_code).expect("setup code should decode")
875    }
876
877    #[tokio::test]
878    async fn accepts_peer_setup_code_with_matching_network() {
879        let api = setup_api(Network::Regtest);
880        let peer_api = setup_api(Network::Regtest);
881
882        setup_code(&api, "local").await;
883        let peer_code = setup_code(&peer_api, "peer").await;
884
885        let added_peer = api
886            .add_peer_setup_code(peer_code)
887            .await
888            .expect("peer setup code with matching network should be accepted");
889
890        assert_eq!(added_peer, "peer");
891    }
892
893    #[test]
894    fn checked_in_backup_fixture_reaches_config_validation() {
895        let backup = base64::prelude::BASE64_STANDARD
896            .decode(INVALID_RESTORE_BACKUP_FIXTURE_B64.trim())
897            .expect("checked-in backup fixture base64 should decode");
898        let Err(err) = parse_backup(&backup, Some("pass")) else {
899            panic!("invalid checked-in backup fixture should not restore");
900        };
901
902        assert!(
903            err.to_string().contains("Reading restored config"),
904            "unexpected restore error: {err:#}"
905        );
906    }
907
908    #[test]
909    fn backup_restore_rejects_non_file_entries() {
910        let mut backup = Vec::new();
911        {
912            let mut archive = tar::Builder::new(&mut backup);
913            let mut header = tar::Header::new_gnu();
914            header.set_entry_type(tar::EntryType::Directory);
915            header.set_size(0);
916            header.set_cksum();
917            archive
918                .append_data(
919                    &mut header,
920                    PathBuf::from(LOCAL_CONFIG).with_extension(JSON_EXT),
921                    std::io::empty(),
922                )
923                .expect("writing tar entry should succeed");
924            archive.finish().expect("finishing tar should succeed");
925        }
926
927        let Err(err) = parse_backup(&backup, None) else {
928            panic!("non-file backup entries should be rejected");
929        };
930
931        assert!(
932            err.to_string().contains("non-file entry"),
933            "unexpected restore error: {err:#}"
934        );
935    }
936
937    #[tokio::test]
938    async fn rejects_peer_setup_code_with_different_network() {
939        let api = setup_api(Network::Regtest);
940        let peer_api = setup_api(Network::Signet);
941
942        setup_code(&api, "local").await;
943        let peer_code = setup_code(&peer_api, "peer").await;
944
945        let err = api
946            .add_peer_setup_code(peer_code)
947            .await
948            .expect_err("peer setup code with different network should be rejected");
949
950        assert!(
951            err.to_string()
952                .contains("Guardian uses Bitcoin network signet but we use regtest")
953        );
954    }
955
956    #[tokio::test]
957    async fn rejects_peer_setup_code_from_different_fedimint_minor() {
958        let api = setup_api_with_version(Network::Regtest, "1.2.3-alpha");
959        let peer_api = setup_api_with_version(Network::Regtest, "1.3.0-beta");
960
961        setup_code(&api, "local").await;
962        let peer_code = setup_code(&peer_api, "peer").await;
963
964        let err = api
965            .add_peer_setup_code(peer_code)
966            .await
967            .expect_err("peer setup code from a different Fedimint minor should be rejected");
968
969        assert!(
970            err.to_string()
971                .contains("Guardian uses Fedimint version 1.3.0 but we use 1.2.3")
972        );
973    }
974
975    #[tokio::test]
976    async fn accepts_peer_setup_code_from_same_vendor_with_patch_skew() {
977        let api = setup_api_with_version(Network::Regtest, "1.2.3-alpha+fedi");
978        let peer_api = setup_api_with_version(Network::Regtest, "1.2.4-beta+fedi");
979
980        let local_code = setup_code(&api, "local").await;
981        let peer_code = setup_code(&peer_api, "peer").await;
982        let peer_setup_code = decode_setup_code(&peer_code);
983
984        let added_peer = api
985            .add_peer_setup_code(peer_code)
986            .await
987            .expect("peer setup code from the same Fedimint minor should be accepted");
988
989        assert_eq!(added_peer, "peer");
990        assert_eq!(
991            decode_setup_code(&local_code).fedimint_version.to_string(),
992            "1.2.3+fedi"
993        );
994        assert_eq!(peer_setup_code.fedimint_version.to_string(), "1.2.4+fedi");
995    }
996
997    #[tokio::test]
998    async fn rejects_peer_setup_code_from_different_fedimint_vendor() {
999        for (local_version, peer_version) in [("1.2.3+fedi", "1.2.4"), ("1.2.3+fedi", "1.2.4+acme")]
1000        {
1001            let api = setup_api_with_version(Network::Regtest, local_version);
1002            let peer_api = setup_api_with_version(Network::Regtest, peer_version);
1003
1004            setup_code(&api, "local").await;
1005            let peer_code = setup_code(&peer_api, "peer").await;
1006
1007            let err = api
1008                .add_peer_setup_code(peer_code)
1009                .await
1010                .expect_err("peer setup code from a different vendor should be rejected");
1011
1012            assert!(
1013                err.to_string()
1014                    .contains(&format!("Guardian uses Fedimint version {peer_version}"))
1015            );
1016        }
1017    }
1018
1019    #[tokio::test]
1020    async fn rejects_malformed_local_fedimint_version() {
1021        let malformed_local = setup_api_with_version(Network::Regtest, "fedimint-code-version");
1022        let err = malformed_local
1023            .set_local_parameters("local".to_owned(), None, None, None, None)
1024            .await
1025            .expect_err("malformed local version should be rejected");
1026        assert!(err.to_string().contains("Invalid local Fedimint version"));
1027    }
1028
1029    #[tokio::test]
1030    async fn rejects_different_fedimint_minor_during_dkg() {
1031        let api = setup_api_with_version(Network::Regtest, "1.2.3-alpha");
1032        let peer_api = setup_api_with_version(Network::Regtest, "1.3.0-beta");
1033
1034        setup_code(&api, "local").await;
1035        let peer_code = setup_code(&peer_api, "peer").await;
1036        let peer_code = base32::decode_prefixed(FEDIMINT_PREFIX, &peer_code)
1037            .expect("peer setup code should decode");
1038
1039        api.state.lock().await.setup_codes.insert(peer_code);
1040
1041        let err = api
1042            .start_dkg()
1043            .await
1044            .expect_err("DKG should reject a peer from a different Fedimint minor");
1045
1046        assert!(
1047            err.to_string()
1048                .contains("Guardian uses Fedimint version 1.3.0 but we use 1.2.3")
1049        );
1050    }
1051
1052    #[tokio::test]
1053    async fn accepts_same_vendor_patch_versions_during_dkg() {
1054        let (api, mut receiver) =
1055            setup_api_with_version_and_receiver(Network::Regtest, "1.2.3-alpha+fedi");
1056        api.set_local_parameters(
1057            "local".to_owned(),
1058            Some("test federation".to_owned()),
1059            None,
1060            None,
1061            Some(4),
1062        )
1063        .await
1064        .expect("setting local parameters should succeed");
1065
1066        for (name, version) in [
1067            ("peer-1", "1.2.4-beta+fedi"),
1068            ("peer-2", "1.2.5+fedi"),
1069            ("peer-3", "1.2.6-rc.1+fedi"),
1070        ] {
1071            let peer_api = setup_api_with_version(Network::Regtest, version);
1072            let peer_code = setup_code(&peer_api, name).await;
1073            let peer_code = decode_setup_code(&peer_code);
1074            api.state.lock().await.setup_codes.insert(peer_code);
1075        }
1076
1077        api.start_dkg()
1078            .await
1079            .expect("DKG should accept peers from the same Fedimint minor");
1080        receiver
1081            .recv()
1082            .await
1083            .expect("DKG parameters should be sent");
1084    }
1085
1086    #[tokio::test]
1087    async fn rejects_different_fedimint_vendor_during_dkg() {
1088        for (local_version, peer_version) in [("1.2.3+fedi", "1.2.4"), ("1.2.3+fedi", "1.2.4+acme")]
1089        {
1090            let api = setup_api_with_version(Network::Regtest, local_version);
1091            let peer_api = setup_api_with_version(Network::Regtest, peer_version);
1092
1093            setup_code(&api, "local").await;
1094            let peer_code = decode_setup_code(&setup_code(&peer_api, "peer").await);
1095            api.state.lock().await.setup_codes.insert(peer_code);
1096
1097            let err = api
1098                .start_dkg()
1099                .await
1100                .expect_err("DKG should reject a peer from a different vendor");
1101            assert!(
1102                err.to_string()
1103                    .contains(&format!("Guardian uses Fedimint version {peer_version}"))
1104            );
1105        }
1106    }
1107}