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
46pub enum ConfigGenOutcome {
52 Generated(Box<ConfigGenParams>),
53 Restored(Box<ServerConfig>, oneshot::Sender<Result<(), String>>),
54}
55
56#[derive(Debug, Clone, Default)]
58pub struct SetupState {
59 local_params: Option<LocalParams>,
61 setup_codes: BTreeSet<PeerSetupCode>,
63 restore_in_progress: bool,
65}
66
67#[derive(Clone, Debug)]
68pub struct LocalParams {
70 tls_key: Option<Arc<rustls::pki_types::PrivateKeyDer<'static>>>,
72 iroh_api_sk: Option<iroh::SecretKey>,
74 iroh_p2p_sk: Option<iroh::SecretKey>,
76 endpoints: PeerEndpoints,
78 name: String,
80 federation_name: Option<String>,
82 disable_base_fees: Option<bool>,
84 enabled_modules: Option<BTreeSet<ModuleKind>>,
87 federation_size: Option<u32>,
90 network: bitcoin::Network,
92 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#[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,
140 code_version_hash: String,
142 auth_ui: Option<ApiAuth>,
144 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
190fn 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 if let Some(private) = files.get(&private_config_json) {
253 parse_plaintext_backup(local, consensus, private).context("Reading restored config")
256 } else if let Some(private) = files.get(&private_config_encrypted) {
257 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 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}