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