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
45pub enum ConfigGenOutcome {
51 Generated(Box<ConfigGenParams>),
52 Restored(Box<ServerConfig>, oneshot::Sender<Result<(), String>>),
53}
54
55#[derive(Debug, Clone, Default)]
57pub struct SetupState {
58 local_params: Option<LocalParams>,
60 setup_codes: BTreeSet<PeerSetupCode>,
62 restore_in_progress: bool,
64}
65
66#[derive(Clone, Debug)]
67pub struct LocalParams {
69 tls_key: Option<Arc<rustls::pki_types::PrivateKeyDer<'static>>>,
71 iroh_api_sk: Option<iroh::SecretKey>,
73 iroh_p2p_sk: Option<iroh::SecretKey>,
75 endpoints: PeerEndpoints,
77 name: String,
79 federation_name: Option<String>,
81 disable_base_fees: Option<bool>,
83 enabled_modules: Option<BTreeSet<ModuleKind>>,
86 federation_size: Option<u32>,
89 network: bitcoin::Network,
91 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#[derive(Clone)]
127pub struct SetupApi {
128 settings: ConfigGenSettings,
130 state: Arc<Mutex<SetupState>>,
132 db: Database,
134 sender: Sender<ConfigGenOutcome>,
136 code_version_str: String,
138 code_version_hash: String,
140 auth_ui: Option<ApiAuth>,
142 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
188fn 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 if let Some(private) = files.get(&private_config_json) {
251 parse_plaintext_backup(local, consensus, private).context("Reading restored config")
254 } else if let Some(private) = files.get(&private_config_encrypted) {
255 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 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}