1use std::collections::BTreeMap;
2use std::ops::ControlFlow;
3use std::path::{Path, PathBuf};
4use std::str::FromStr;
5use std::time::Duration;
6use std::{env, fs};
7
8use anyhow::{Context, Result, bail};
9use fedimint_api_client::api::DynGlobalApi;
10use fedimint_api_client::download_from_invite_code;
11use fedimint_client_module::module::ClientModule;
12use fedimint_connectors::ConnectorRegistry;
13use fedimint_core::admin_client::SetupStatus;
14use fedimint_core::config::{ClientConfig, load_from_file};
15use fedimint_core::core::{ModuleInstanceId, ModuleKind};
16use fedimint_core::invite_code::InviteCode;
17use fedimint_core::module::ModuleCommon;
18use fedimint_core::module::registry::ModuleDecoderRegistry;
19use fedimint_core::runtime::block_in_place;
20use fedimint_core::task::block_on;
21use fedimint_core::task::jit::JitTryAnyhow;
22use fedimint_core::util::SafeUrl;
23use fedimint_core::{Amount, NumPeers, PeerId};
24use fedimint_gateway_common::WithdrawResponse;
25use fedimint_logging::LOG_DEVIMINT;
26use fedimint_server::config::io::{CONSENSUS_CONFIG, JSON_EXT, LOCAL_CONFIG, PRIVATE_CONFIG};
27use fedimint_testing_core::config::API_AUTH;
28use fedimint_testing_core::node_type::LightningNodeType;
29use fedimint_wallet_client::WalletClientModule;
30use fedimint_wallet_client::config::WalletClientConfig;
31use fs_lock::FileLock;
32use futures::future::{join_all, try_join_all};
33use tokio::task::{JoinSet, spawn_blocking};
34use tokio::time::Instant;
35use tracing::{debug, info};
36
37use super::external::Bitcoind;
38use super::util::{Command, ProcessHandle, ProcessManager, cmd};
39use super::vars::utf8;
40use crate::envs::{FM_CLIENT_DIR_ENV, FM_DATA_DIR_ENV};
41use crate::util::{FedimintdCmd, poll, poll_simple, poll_with_timeout};
42use crate::version_constants::{VERSION_0_10_0_ALPHA, VERSION_0_11_0_ALPHA, VERSION_0_12_0_ALPHA};
43use crate::{poll_almost_equal, poll_eq, vars};
44
45pub const PORTS_PER_FEDIMINTD: u16 = 4;
48pub const FEDIMINTD_P2P_PORT_OFFSET: u16 = 0;
50pub const FEDIMINTD_API_PORT_OFFSET: u16 = 1;
52pub const FEDIMINTD_UI_PORT_OFFSET: u16 = 2;
54pub const FEDIMINTD_METRICS_PORT_OFFSET: u16 = 3;
56
57#[derive(Clone)]
58pub struct Federation {
59 pub members: BTreeMap<usize, Fedimintd>,
61 pub vars: BTreeMap<usize, vars::Fedimintd>,
62 pub bitcoind: Bitcoind,
63
64 client: JitTryAnyhow<Client>,
66 #[allow(dead_code)] connectors: ConnectorRegistry,
68}
69
70impl Drop for Federation {
71 fn drop(&mut self) {
72 block_in_place(|| {
73 block_on(async {
74 let mut set = JoinSet::new();
75
76 while let Some((_id, fedimintd)) = self.members.pop_first() {
77 set.spawn(async { drop(fedimintd) });
78 }
79 while (set.join_next().await).is_some() {}
80 });
81 });
82 }
83}
84#[derive(Clone)]
86pub struct Client {
87 name: String,
88}
89
90impl Client {
91 fn clients_dir() -> PathBuf {
92 let data_dir: PathBuf = env::var(FM_DATA_DIR_ENV)
93 .expect("FM_DATA_DIR_ENV not set")
94 .parse()
95 .expect("FM_DATA_DIR_ENV invalid");
96 data_dir.join("clients")
97 }
98
99 fn client_dir(&self) -> PathBuf {
100 Self::clients_dir().join(&self.name)
101 }
102
103 pub fn client_name_lock(name: &str) -> Result<FileLock> {
104 let lock_path = Self::clients_dir().join(format!(".{name}.lock"));
105 let file_lock = std::fs::OpenOptions::new()
106 .write(true)
107 .create(true)
108 .truncate(true)
109 .open(&lock_path)
110 .with_context(|| format!("Failed to open {}", lock_path.display()))?;
111
112 fs_lock::FileLock::new_exclusive(file_lock)
113 .with_context(|| format!("Failed to lock {}", lock_path.display()))
114 }
115
116 pub async fn create(name: impl ToString) -> Result<Client> {
118 let name = name.to_string();
119 spawn_blocking(move || {
120 let _lock = Self::client_name_lock(&name);
121 for i in 0u64.. {
122 let client = Self {
123 name: format!("{name}-{i}"),
124 };
125
126 if !client.client_dir().exists() {
127 std::fs::create_dir_all(client.client_dir())?;
128 return Ok(client);
129 }
130 }
131 unreachable!()
132 })
133 .await?
134 }
135
136 pub fn open_or_create(name: &str) -> Result<Client> {
138 block_in_place(|| {
139 let _lock = Self::client_name_lock(name);
140 let client = Self {
141 name: format!("{name}-0"),
142 };
143 if !client.client_dir().exists() {
144 std::fs::create_dir_all(client.client_dir())?;
145 }
146 Ok(client)
147 })
148 }
149
150 pub async fn join_federation(&self, invite_code: String) -> Result<()> {
152 debug!(target: LOG_DEVIMINT, "Joining federation with the main client");
153 cmd!(self, "join-federation", invite_code).run().await?;
154
155 Ok(())
156 }
157
158 pub async fn restore_federation(&self, invite_code: String, mnemonic: String) -> Result<()> {
160 debug!(target: LOG_DEVIMINT, "Joining federation with restore procedure");
161 cmd!(
162 self,
163 "restore",
164 "--invite-code",
165 invite_code,
166 "--mnemonic",
167 mnemonic
168 )
169 .run()
170 .await?;
171
172 Ok(())
173 }
174
175 pub async fn new_restored(&self, name: &str, invite_code: String) -> Result<Self> {
177 let restored = Self::open_or_create(name)?;
178
179 let mnemonic = cmd!(self, "print-secret").out_json().await?["secret"]
180 .as_str()
181 .unwrap()
182 .to_owned();
183
184 debug!(target: LOG_DEVIMINT, name, "Restoring from mnemonic");
185 cmd!(
186 restored,
187 "restore",
188 "--invite-code",
189 invite_code,
190 "--mnemonic",
191 mnemonic
192 )
193 .run()
194 .await?;
195
196 Ok(restored)
197 }
198
199 pub async fn new_forked(&self, name: impl ToString) -> Result<Client> {
202 let new = Client::create(name).await?;
203
204 cmd!(
205 "cp",
206 "-R",
207 self.client_dir().join("client.db").display(),
208 new.client_dir().display()
209 )
210 .run()
211 .await?;
212
213 Ok(new)
214 }
215
216 pub async fn balance(&self) -> Result<u64> {
217 Ok(cmd!(self, "info").out_json().await?["total_amount_msat"]
218 .as_u64()
219 .unwrap())
220 }
221
222 pub async fn await_balance(&self, min_balance_msat: u64) -> Result<()> {
224 loop {
225 cmd!(self, "dev", "wait", "3").out_json().await?;
226
227 let balance = self.balance().await?;
228 if balance >= min_balance_msat {
229 return Ok(());
230 }
231
232 info!(
233 target: LOG_DEVIMINT,
234 balance,
235 min_balance_msat,
236 "Waiting for client balance to reach minimum"
237 );
238 }
239 }
240
241 pub async fn await_receive(&self, position: &str) -> Result<String> {
246 let output = cmd!(self, "module", "walletv2", "await-receive", position)
247 .out_json()
248 .await?;
249
250 Ok(output[1].to_string())
252 }
253
254 pub async fn get_deposit_addr(&self) -> Result<(String, String)> {
255 if crate::util::supports_wallet_v2() {
256 if crate::util::FedimintCli::version_or_default().await >= *VERSION_0_12_0_ALPHA {
257 let position = cmd!(self, "dev", "next-event-log-id").out_json().await?;
261
262 let address = cmd!(self, "module", "walletv2", "receive")
263 .out_json()
264 .await?;
265
266 Ok((address.as_str().unwrap().to_string(), position.to_string()))
267 } else {
268 let address = cmd!(self, "module", "walletv2", "receive")
272 .out_json()
273 .await?;
274
275 Ok((address.as_str().unwrap().to_string(), String::new()))
276 }
277 } else {
278 let deposit = cmd!(self, "deposit-address").out_json().await?;
279 Ok((
280 deposit["address"].as_str().unwrap().to_string(),
281 deposit["operation_id"].as_str().unwrap().to_string(),
282 ))
283 }
284 }
285
286 pub async fn await_deposit(&self, operation_id: &str) -> Result<()> {
287 cmd!(self, "await-deposit", operation_id).run().await
288 }
289
290 pub fn cmd(&self) -> Command {
291 cmd!(
292 crate::util::get_fedimint_cli_path(),
293 format!("--data-dir={}", self.client_dir().display())
294 )
295 }
296
297 pub fn get_name(&self) -> &str {
298 &self.name
299 }
300
301 pub async fn get_session_count(&self) -> Result<u64> {
303 cmd!(self, "dev", "session-count").out_json().await?["count"]
304 .as_u64()
305 .context("count field wasn't a number")
306 }
307
308 pub async fn wait_complete(&self) -> Result<()> {
310 cmd!(self, "dev", "wait-complete").run().await
311 }
312
313 pub async fn wait_session(&self) -> anyhow::Result<()> {
315 info!("Waiting for a new session");
316 let session_count = self.get_session_count().await?;
317 self.wait_session_outcome(session_count).await?;
318 Ok(())
319 }
320
321 pub async fn wait_session_outcome(&self, session_count: u64) -> anyhow::Result<()> {
323 let timeout = {
324 let current_session_count = self.get_session_count().await?;
325 let sessions_to_wait = session_count.saturating_sub(current_session_count) + 1;
326 let session_duration_seconds = 180;
327 Duration::from_secs(sessions_to_wait * session_duration_seconds)
328 };
329
330 let start = Instant::now();
331 poll_with_timeout("Waiting for a new session", timeout, || async {
332 info!("Awaiting session outcome {session_count}");
333 match cmd!(self, "dev", "api", "await_session_outcome", session_count)
334 .run()
335 .await
336 {
337 Err(e) => Err(ControlFlow::Continue(e)),
338 Ok(()) => Ok(()),
339 }
340 })
341 .await?;
342
343 let session_found_in = start.elapsed();
344 info!("session found in {session_found_in:?}");
345 Ok(())
346 }
347}
348
349impl Federation {
350 pub async fn new(
351 process_mgr: &ProcessManager,
352 bitcoind: Bitcoind,
353 skip_setup: bool,
354 pre_dkg: bool,
355 pre_restore: bool,
356 fed_index: usize,
358 federation_name: String,
359 ) -> Result<Self> {
360 let num_peers = NumPeers::from(process_mgr.globals.FM_FED_SIZE);
361 let mut members = BTreeMap::new();
362 let mut peer_to_env_vars_map = BTreeMap::new();
363
364 let mut admin_clients: BTreeMap<PeerId, DynGlobalApi> = BTreeMap::new();
365 let mut api_endpoints: BTreeMap<PeerId, _> = BTreeMap::new();
366
367 let connectors = ConnectorRegistry::build_from_testing_env()?.bind().await?;
368 for peer_id in num_peers.peer_ids() {
369 let peer_env_vars = vars::Fedimintd::init(
370 &process_mgr.globals,
371 federation_name.clone(),
372 peer_id,
373 process_mgr
374 .globals
375 .fedimintd_overrides
376 .peer_expect(fed_index, peer_id),
377 )
378 .await?;
379 members.insert(
380 peer_id.to_usize(),
381 Fedimintd::new(
382 process_mgr,
383 bitcoind.clone(),
384 peer_id.to_usize(),
385 &peer_env_vars,
386 federation_name.clone(),
387 )
388 .await?,
389 );
390 let admin_client = DynGlobalApi::new_admin_setup(
391 connectors.clone(),
392 SafeUrl::parse(&peer_env_vars.FM_API_URL)?,
393 )?;
396 api_endpoints.insert(peer_id, peer_env_vars.FM_API_URL.clone());
397 admin_clients.insert(peer_id, admin_client);
398 peer_to_env_vars_map.insert(peer_id.to_usize(), peer_env_vars);
399 }
400
401 if !skip_setup && !pre_dkg {
402 let (original_fedimint_cli_path, original_fm_mint_client) =
405 crate::util::use_matching_fedimint_cli_for_dkg().await?;
406
407 run_cli_dkg_v2(api_endpoints).await?;
408
409 crate::util::use_fedimint_cli(original_fedimint_cli_path, original_fm_mint_client);
411
412 let client_dir = utf8(&process_mgr.globals.FM_CLIENT_DIR);
414 let invite_code_filename_original = "invite-code";
415
416 for peer_env_vars in peer_to_env_vars_map.values() {
417 let peer_data_dir = utf8(&peer_env_vars.FM_DATA_DIR);
418
419 let invite_code = poll_simple("awaiting-invite-code", || async {
420 let path = format!("{peer_data_dir}/{invite_code_filename_original}");
421 tokio::fs::read_to_string(&path)
422 .await
423 .with_context(|| format!("Awaiting invite code file: {path}"))
424 })
425 .await
426 .context("Awaiting invite code file")?;
427
428 download_from_invite_code(&connectors, &InviteCode::from_str(&invite_code)?)
429 .await?;
430 }
431
432 let peer_data_dir = utf8(&peer_to_env_vars_map[&0].FM_DATA_DIR);
434
435 tokio::fs::copy(
436 format!("{peer_data_dir}/{invite_code_filename_original}"),
437 format!("{client_dir}/{invite_code_filename_original}"),
438 )
439 .await
440 .context("copying invite-code file")?;
441
442 for (index, peer_env_vars) in &peer_to_env_vars_map {
445 let peer_data_dir = utf8(&peer_env_vars.FM_DATA_DIR);
446
447 let invite_code_filename_indexed =
448 format!("{invite_code_filename_original}-{index}");
449 tokio::fs::rename(
450 format!("{peer_data_dir}/{invite_code_filename_original}"),
451 format!("{client_dir}/{invite_code_filename_indexed}"),
452 )
453 .await
454 .context("moving invite-code file")?;
455 }
456
457 debug!("Moved invite-code files to client data directory");
458
459 if pre_restore {
460 Self::restart_guardian_for_manual_restore(
461 process_mgr,
462 &mut members,
463 &peer_to_env_vars_map,
464 &bitcoind,
465 )
466 .await?;
467 }
468 }
469
470 let client = JitTryAnyhow::new_try({
471 move || async move {
472 let client = Client::open_or_create(federation_name.as_str())?;
473 let invite_code = Self::invite_code_static()?;
474 if !skip_setup && !pre_dkg {
475 cmd!(client, "join-federation", invite_code).run().await?;
476 }
477 Ok(client)
478 }
479 });
480
481 Ok(Self {
482 members,
483 vars: peer_to_env_vars_map,
484 bitcoind,
485 client,
486 connectors,
487 })
488 }
489
490 async fn restart_guardian_for_manual_restore(
491 process_mgr: &ProcessManager,
492 members: &mut BTreeMap<usize, Fedimintd>,
493 peer_to_env_vars_map: &BTreeMap<usize, vars::Fedimintd>,
494 bitcoind: &Bitcoind,
495 ) -> Result<()> {
496 const RESTORE_PEER: usize = 0;
497
498 let peer_env_vars = &peer_to_env_vars_map[&RESTORE_PEER];
499 let backup_dir = process_mgr.globals.FM_TEST_DIR.join("fedimintd-backups");
500 tokio::fs::create_dir_all(&backup_dir)
501 .await
502 .context("Creating fedimintd backup directory")?;
503 let backup_path = backup_dir.join("fedimint-0-guardian-backup.tar");
504
505 Self::write_guardian_backup_tar(&peer_env_vars.FM_DATA_DIR, &backup_path).await?;
506 info!(
507 target: LOG_DEVIMINT,
508 path = %backup_path.display(),
509 "Wrote guardian backup for manual restore"
510 );
511
512 let fedimintd = members
513 .remove(&RESTORE_PEER)
514 .context("Missing fedimint-0 process")?;
515 fedimintd.terminate().await?;
516 tokio::fs::remove_dir_all(&peer_env_vars.FM_DATA_DIR)
517 .await
518 .with_context(|| format!("Removing {}", peer_env_vars.FM_DATA_DIR.display()))?;
519 tokio::fs::create_dir_all(&peer_env_vars.FM_DATA_DIR)
520 .await
521 .with_context(|| format!("Creating {}", peer_env_vars.FM_DATA_DIR.display()))?;
522
523 let fedimintd = Fedimintd::new(
524 process_mgr,
525 bitcoind.clone(),
526 RESTORE_PEER,
527 peer_env_vars,
528 "default".to_string(),
529 )
530 .await?;
531 members.insert(RESTORE_PEER, fedimintd);
532
533 info!(
534 target: LOG_DEVIMINT,
535 ui = %peer_env_vars.FM_BIND_UI,
536 backup = %backup_path.display(),
537 "fedimint-0 restarted in setup mode for manual restore"
538 );
539
540 Ok(())
541 }
542
543 async fn write_guardian_backup_tar(data_dir: &Path, backup_path: &Path) -> Result<()> {
544 let data_dir = data_dir.to_path_buf();
545 let backup_path = backup_path.to_path_buf();
546 spawn_blocking(move || {
547 let file = fs::File::options()
548 .write(true)
549 .create_new(true)
550 .open(&backup_path)
551 .with_context(|| format!("Creating {}", backup_path.display()))?;
552 let mut archive = tar::Builder::new(file);
553 for path in [
554 PathBuf::from(LOCAL_CONFIG).with_extension(JSON_EXT),
555 PathBuf::from(CONSENSUS_CONFIG).with_extension(JSON_EXT),
556 PathBuf::from(PRIVATE_CONFIG).with_extension(JSON_EXT),
557 ] {
558 archive
559 .append_path_with_name(data_dir.join(&path), &path)
560 .with_context(|| format!("Adding {} to backup", path.display()))?;
561 }
562 archive.finish().context("Finishing guardian backup tar")?;
563 Ok::<_, anyhow::Error>(())
564 })
565 .await?
566 }
567
568 pub fn client_config(&self) -> Result<ClientConfig> {
569 let cfg_path = self.vars[&0].FM_DATA_DIR.join("client.json");
570 load_from_file(&cfg_path)
571 }
572
573 pub fn module_instance_id_by_kind(&self, kind: &ModuleKind) -> Result<ModuleInstanceId> {
575 self.client_config()?
576 .modules
577 .iter()
578 .find_map(|(id, cfg)| if &cfg.kind == kind { Some(*id) } else { None })
579 .with_context(|| format!("Module kind {kind} not found"))
580 }
581
582 pub fn module_client_config<M: ClientModule>(
583 &self,
584 ) -> Result<Option<<M::Common as ModuleCommon>::ClientConfig>> {
585 self.client_config()?
586 .modules
587 .iter()
588 .find_map(|(module_instance_id, module_cfg)| {
589 if module_cfg.kind == M::kind() {
590 let decoders = ModuleDecoderRegistry::new(vec![(
591 *module_instance_id,
592 M::kind(),
593 M::decoder(),
594 )]);
595 Some(
596 module_cfg
597 .config
598 .clone()
599 .redecode_raw(&decoders)
600 .expect("Decoding client cfg failed")
601 .expect_decoded_ref()
602 .as_any()
603 .downcast_ref::<<M::Common as ModuleCommon>::ClientConfig>()
604 .cloned()
605 .context("Cast to module config failed"),
606 )
607 } else {
608 None
609 }
610 })
611 .transpose()
612 }
613
614 pub fn deposit_fees(&self) -> Result<Amount> {
615 if crate::util::supports_wallet_v2() {
616 Ok(self
617 .module_client_config::<fedimint_walletv2_client::WalletClientModule>()?
618 .context("No walletv2 module found")?
619 .fee_consensus
620 .base)
621 } else {
622 Ok(self
623 .module_client_config::<WalletClientModule>()?
624 .context("No wallet module found")?
625 .fee_consensus
626 .peg_in_abs)
627 }
628 }
629
630 pub fn invite_code(&self) -> Result<String> {
632 let data_dir: PathBuf = env::var(FM_CLIENT_DIR_ENV)?.parse()?;
633 let invite_code = fs::read_to_string(data_dir.join("invite-code"))?;
634 Ok(invite_code)
635 }
636
637 pub fn invite_code_static() -> Result<String> {
638 let data_dir: PathBuf = env::var(FM_CLIENT_DIR_ENV)?.parse()?;
639 let invite_code = fs::read_to_string(data_dir.join("invite-code"))?;
640 Ok(invite_code)
641 }
642 pub fn invite_code_for(peer_id: PeerId) -> Result<String> {
643 let data_dir: PathBuf = env::var(FM_CLIENT_DIR_ENV)?.parse()?;
644 let name = format!("invite-code-{peer_id}");
645 let invite_code = fs::read_to_string(data_dir.join(name))?;
646 Ok(invite_code)
647 }
648
649 pub async fn internal_client(&self) -> Result<&Client> {
653 self.client
654 .get_try()
655 .await
656 .context("Internal client joining Federation")
657 }
658
659 pub async fn new_joined_client(&self, name: impl ToString) -> Result<Client> {
661 let client = Client::create(name).await?;
662 client.join_federation(self.invite_code()?).await?;
663 Ok(client)
664 }
665
666 pub async fn start_server(&mut self, process_mgr: &ProcessManager, peer: usize) -> Result<()> {
667 if self.members.contains_key(&peer) {
668 bail!("fedimintd-{peer} already running");
669 }
670 self.members.insert(
671 peer,
672 Fedimintd::new(
673 process_mgr,
674 self.bitcoind.clone(),
675 peer,
676 &self.vars[&peer],
677 "default".to_string(),
678 )
679 .await?,
680 );
681 Ok(())
682 }
683
684 pub async fn terminate_server(&mut self, peer_id: usize) -> Result<()> {
685 let Some((_, fedimintd)) = self.members.remove_entry(&peer_id) else {
686 bail!("fedimintd-{peer_id} does not exist");
687 };
688 fedimintd.terminate().await?;
689 Ok(())
690 }
691
692 pub async fn await_server_terminated(&mut self, peer_id: usize) -> Result<()> {
693 let Some(fedimintd) = self.members.get_mut(&peer_id) else {
694 bail!("fedimintd-{peer_id} does not exist");
695 };
696 fedimintd.await_terminated().await?;
697 self.members.remove(&peer_id);
698 Ok(())
699 }
700
701 pub async fn start_all_servers(&mut self, process_mgr: &ProcessManager) -> Result<()> {
703 info!("starting all servers");
704 let fed_size = process_mgr.globals.FM_FED_SIZE;
705 for peer_id in 0..fed_size {
706 if self.members.contains_key(&peer_id) {
707 continue;
708 }
709 self.start_server(process_mgr, peer_id).await?;
710 }
711 self.await_all_peers().await?;
712 Ok(())
713 }
714
715 pub async fn terminate_all_servers(&mut self) -> Result<()> {
717 info!("terminating all servers");
718 let running_peer_ids: Vec<_> = self.members.keys().copied().collect();
719 for peer_id in running_peer_ids {
720 self.terminate_server(peer_id).await?;
721 }
722 Ok(())
723 }
724
725 pub async fn restart_all_staggered_with_bin(
730 &mut self,
731 process_mgr: &ProcessManager,
732 bin_path: &PathBuf,
733 ) -> Result<()> {
734 let fed_size = process_mgr.globals.FM_FED_SIZE;
735
736 self.start_all_servers(process_mgr).await?;
738
739 while self.num_members() > 0 {
741 self.terminate_server(self.num_members() - 1).await?;
742 if self.num_members() > 0 {
743 fedimint_core::task::sleep_in_test(
744 "waiting to shutdown remaining peers",
745 Duration::from_secs(10),
746 )
747 .await;
748 }
749 }
750
751 unsafe { std::env::set_var("FM_FEDIMINTD_BASE_EXECUTABLE", bin_path) };
753
754 for peer_id in 0..fed_size {
756 self.start_server(process_mgr, peer_id).await?;
757 if peer_id < fed_size - 1 {
758 fedimint_core::task::sleep_in_test(
759 "waiting to restart remaining peers",
760 Duration::from_secs(10),
761 )
762 .await;
763 }
764 }
765
766 self.await_all_peers().await?;
767
768 let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
769 info!("upgraded fedimintd to version: {}", fedimintd_version);
770 Ok(())
771 }
772
773 pub async fn restart_all_with_bin(
774 &mut self,
775 process_mgr: &ProcessManager,
776 bin_path: &PathBuf,
777 ) -> Result<()> {
778 let current_fedimintd_path = std::env::var("FM_FEDIMINTD_BASE_EXECUTABLE")?;
780 unsafe { std::env::set_var("FM_FEDIMINTD_BASE_EXECUTABLE", bin_path) };
782 unsafe { std::env::set_var("FM_FEDIMINTD_BASE_EXECUTABLE", current_fedimintd_path) };
784
785 self.restart_all_staggered_with_bin(process_mgr, bin_path)
786 .await
787 }
788
789 pub async fn degrade_federation(&mut self, process_mgr: &ProcessManager) -> Result<()> {
790 let fed_size = process_mgr.globals.FM_FED_SIZE;
791 let offline_nodes = process_mgr.globals.FM_OFFLINE_NODES;
792 anyhow::ensure!(
793 fed_size > 3 * offline_nodes,
794 "too many offline nodes ({offline_nodes}) to reach consensus"
795 );
796
797 while self.num_members() > fed_size - offline_nodes {
798 self.terminate_server(self.num_members() - 1).await?;
799 }
800
801 if offline_nodes > 0 {
802 info!(fed_size, offline_nodes, "federation is degraded");
803 }
804 Ok(())
805 }
806
807 pub async fn send_to_address(&self, address: String, amount: u64) -> Result<()> {
808 self.bitcoind.send_to(address, amount).await?;
809
810 self.bitcoind.mine_blocks(21).await?;
811
812 Ok(())
813 }
814
815 pub async fn pegin_client_no_wait(&self, amount: u64, client: &Client) -> Result<String> {
816 let deposit_fees_msat = self.deposit_fees()?.msats;
817 assert_eq!(
818 deposit_fees_msat % 1000,
819 0,
820 "Deposit fees expected to be whole sats in test suite"
821 );
822 let deposit_fees = deposit_fees_msat / 1000;
823 info!(amount, deposit_fees, "Pegging-in client funds");
824
825 let (address, handle) = client.get_deposit_addr().await?;
828
829 self.bitcoind
830 .send_to(address, amount + deposit_fees)
831 .await?;
832 self.bitcoind.mine_blocks(21).await?;
833
834 Ok(handle)
835 }
836
837 pub async fn pegin_client(&self, amount: u64, client: &Client) -> Result<()> {
838 let walletv2_await_receive = crate::util::supports_wallet_v2()
839 && crate::util::FedimintCli::version_or_default().await >= *VERSION_0_12_0_ALPHA;
840
841 let initial_balance = if crate::util::supports_wallet_v2() && !walletv2_await_receive {
845 Some(client.balance().await?)
846 } else {
847 None
848 };
849
850 let handle = self.pegin_client_no_wait(amount, client).await?;
851
852 if walletv2_await_receive {
853 client.await_receive(&handle).await?;
856 } else if let Some(initial) = initial_balance {
857 let expected_balance = initial + (amount * 1000 * 9 / 10);
860 client.await_balance(expected_balance).await?;
861 } else {
862 client.await_deposit(&handle).await?;
863 }
864
865 Ok(())
866 }
867
868 pub async fn pegin_gateways(
871 &self,
872 amount: u64,
873 gateways: Vec<&super::gatewayd::Gatewayd>,
874 ) -> Result<()> {
875 let deposit_fees_msat = self.deposit_fees()?.msats;
876 assert_eq!(
877 deposit_fees_msat % 1000,
878 0,
879 "Deposit fees expected to be whole sats in test suite"
880 );
881 let deposit_fees = deposit_fees_msat / 1000;
882 info!(amount, deposit_fees, "Pegging-in gateway funds");
883 let fed_id = self.calculate_federation_id();
884
885 let uses_walletv2 = crate::util::supports_wallet_v2();
889 let mut initial_balances = Vec::new();
890 if uses_walletv2 {
891 for gw in &gateways {
892 let balance = gw
893 .client()
894 .ecash_balance(fed_id.clone())
895 .await
896 .expect("failed to fetch initial gateway balance");
897 initial_balances.push(balance);
898 }
899 }
900
901 let mut gateway_deposit_addrs = Vec::new();
902 for gw in gateways.clone() {
903 let pegin_addr = gw.client().get_pegin_addr(&fed_id).await?;
904 debug!(
905 gateway = %gw.gw_name,
906 ln = %gw.ln.ln_type(),
907 address = %pegin_addr,
908 amount_sats = amount + deposit_fees,
909 uses_walletv2,
910 "Sending gateway pegin"
911 );
912 self.bitcoind
913 .send_to(pegin_addr.clone(), amount + deposit_fees)
914 .await?;
915 gateway_deposit_addrs.push(pegin_addr);
916 }
917
918 let pegin_start = Instant::now();
919 self.bitcoind.mine_blocks(21).await?;
920 let bitcoind_block_height: u64 = self.bitcoind.get_block_count().await? - 1;
921 debug!(
922 bitcoind_block_height,
923 elapsed_ms = %pegin_start.elapsed().as_millis(),
924 "Mined gateway pegin blocks"
925 );
926
927 try_join_all(gateways.into_iter().enumerate().map(|(i, gw)| {
928 let initial_balance = if uses_walletv2 {
929 initial_balances[i]
930 } else {
931 0
932 };
933 let gateway_name = gw.gw_name.clone();
934 let gateway_ln = gw.ln.ln_type().to_string();
935 let gateway_id = gw.gateway_id.clone();
936 let gateway_index = gw.gateway_index;
937 let deposit_address = gateway_deposit_addrs[i].clone();
938 let fed_id = fed_id.clone();
939 poll("gateway pegin", move || {
940 let fed_id = fed_id.clone();
941 let gateway_name = gateway_name.clone();
942 let gateway_ln = gateway_ln.clone();
943 let gateway_id = gateway_id.clone();
944 let deposit_address = deposit_address.clone();
945 async move {
946 let gw_info = gw
947 .client()
948 .get_info()
949 .await
950 .map_err(ControlFlow::Continue)?;
951
952 let block_height: u64 = if gw.gatewayd_version < *VERSION_0_10_0_ALPHA {
953 gw_info["block_height"]
954 .as_u64()
955 .expect("Could not parse block height")
956 } else {
957 gw_info["lightning_info"]["connected"]["block_height"]
958 .as_u64()
959 .expect("Could not parse block height")
960 };
961
962 if bitcoind_block_height != block_height {
963 debug!(
964 gateway = %gateway_name,
965 ln = %gateway_ln,
966 bitcoind_block_height,
967 gateway_block_height = block_height,
968 elapsed_ms = %pegin_start.elapsed().as_millis(),
969 "Waiting for gateway pegin block sync"
970 );
971 return Err(std::ops::ControlFlow::Continue(anyhow::anyhow!(
972 "Gateway {gateway_name} ({gateway_id}, index {gateway_index}) block height {block_height} has not reached bitcoind block height {bitcoind_block_height}"
973 )));
974 }
975
976 let gateway_balance = gw
977 .client()
978 .ecash_balance(fed_id)
979 .await
980 .map_err(ControlFlow::Continue)?;
981
982 if uses_walletv2 {
983 let expected = initial_balance + (amount * 1000 * 9 / 10);
987 debug!(
988 gateway = %gateway_name,
989 ln = %gateway_ln,
990 gateway_balance_msats = gateway_balance,
991 expected_msats = expected,
992 initial_balance_msats = initial_balance,
993 elapsed_ms = %pegin_start.elapsed().as_millis(),
994 "Checked walletv2 gateway pegin balance"
995 );
996 if expected <= gateway_balance {
997 Ok(())
998 } else {
999 Err(ControlFlow::Continue(anyhow::anyhow!(
1000 "Gateway {gateway_name} ({gateway_id}, index {gateway_index}) balance {gateway_balance} has not reached expected {expected} for deposit address {deposit_address} (initial: {initial_balance})"
1001 )))
1002 }
1003 } else {
1004 poll_almost_equal!(gateway_balance, amount * 1000)
1005 }
1006 }
1007 })
1008 }))
1009 .await?;
1010
1011 Ok(())
1012 }
1013
1014 pub async fn pegout_gateways(
1017 &self,
1018 amount: u64,
1019 gateways: Vec<&super::gatewayd::Gatewayd>,
1020 ) -> Result<()> {
1021 info!(amount, "Pegging-out gateway funds");
1022 let fed_id = self.calculate_federation_id();
1023 let mut peg_outs: BTreeMap<LightningNodeType, (Amount, WithdrawResponse)> = BTreeMap::new();
1024 for gw in gateways.clone() {
1025 let prev_fed_ecash_balance = gw
1026 .client()
1027 .get_balances()
1028 .await?
1029 .ecash_balances
1030 .into_iter()
1031 .find(|fed| fed.federation_id.to_string() == fed_id)
1032 .expect("Gateway has not joined federation")
1033 .ecash_balance_msats;
1034
1035 let pegout_address = self.bitcoind.get_new_address().await?;
1036 let response = gw
1037 .client()
1038 .pegout(fed_id.clone(), amount, pegout_address)
1039 .await?;
1040 peg_outs.insert(gw.ln.ln_type(), (prev_fed_ecash_balance, response));
1041 }
1042 self.bitcoind.mine_blocks(21).await?;
1043
1044 try_join_all(
1045 peg_outs
1046 .values()
1047 .map(|(_, pegout)| self.bitcoind.poll_get_transaction(pegout.txid)),
1048 )
1049 .await?;
1050
1051 for gw in gateways.clone() {
1052 let after_fed_ecash_balance = gw
1053 .client()
1054 .get_balances()
1055 .await?
1056 .ecash_balances
1057 .into_iter()
1058 .find(|fed| fed.federation_id.to_string() == fed_id)
1059 .expect("Gateway has not joined federation")
1060 .ecash_balance_msats;
1061
1062 let ln_type = gw.ln.ln_type();
1063 let prev_balance = peg_outs
1064 .get(&ln_type)
1065 .expect("peg out does not exist")
1066 .0
1067 .msats;
1068 let fees = peg_outs
1069 .get(&ln_type)
1070 .expect("peg out does not exist")
1071 .1
1072 .fees;
1073 let total_fee = fees.amount().to_sat() * 1000;
1074 let tolerance = if crate::util::supports_wallet_v2() {
1077 let amount_sats = amount / 1000;
1078 let module_fee_sats = 100 + amount_sats / 100;
1079 module_fee_sats * 1000 + 2000
1080 } else if crate::util::supports_mint_v2() {
1081 4000
1082 } else {
1083 2000
1084 };
1085 crate::util::almost_equal(
1086 after_fed_ecash_balance.msats,
1087 prev_balance - amount - total_fee,
1088 tolerance,
1089 )
1090 .map_err(|e| {
1091 anyhow::anyhow!(
1092 "new balance did not equal prev balance minus withdraw_amount minus fees: {e}"
1093 )
1094 })?;
1095 }
1096
1097 Ok(())
1098 }
1099
1100 pub fn calculate_federation_id(&self) -> String {
1101 self.client_config()
1102 .unwrap()
1103 .global
1104 .calculate_federation_id()
1105 .to_string()
1106 }
1107
1108 pub async fn await_block_sync(&self) -> Result<u64> {
1109 let finality_delay = self.get_finality_delay()?;
1110 let block_count = self.bitcoind.get_block_count().await?;
1111 let expected = block_count.saturating_sub(finality_delay.into());
1112
1113 if crate::util::supports_wallet_v2() {
1114 let client = self.internal_client().await?;
1116 loop {
1117 let value = cmd!(client, "module", "walletv2", "info", "block-count")
1118 .out_json()
1119 .await?;
1120 let current: u64 = serde_json::from_value(value)?;
1121 if current >= expected {
1122 break;
1123 }
1124 fedimint_core::task::sleep_in_test(
1125 format!("Waiting for consensus block count to reach {expected}"),
1126 std::time::Duration::from_secs(1),
1127 )
1128 .await;
1129 }
1130 } else {
1131 cmd!(
1132 self.internal_client().await?,
1133 "dev",
1134 "wait-block-count",
1135 expected
1136 )
1137 .run()
1138 .await?;
1139 }
1140
1141 Ok(expected)
1142 }
1143
1144 fn get_finality_delay(&self) -> Result<u32, anyhow::Error> {
1145 if crate::util::supports_wallet_v2() {
1147 return Ok(fedimint_walletv2_server::CONFIRMATION_FINALITY_DELAY as u32);
1148 }
1149
1150 let wallet_instance_id = self.module_instance_id_by_kind(&fedimint_wallet_client::KIND)?;
1151 let client_config = &self.client_config()?;
1152 let wallet_cfg = client_config
1153 .modules
1154 .get(&wallet_instance_id)
1155 .context("wallet module not found")?
1156 .clone()
1157 .redecode_raw(&ModuleDecoderRegistry::new([(
1158 wallet_instance_id,
1159 fedimint_wallet_client::KIND,
1160 fedimint_wallet_client::WalletModuleTypes::decoder(),
1161 )]))?;
1162 let wallet_cfg: &WalletClientConfig = wallet_cfg.cast()?;
1163
1164 let finality_delay = wallet_cfg.finality_delay;
1165 Ok(finality_delay)
1166 }
1167
1168 pub async fn await_gateways_registered(&self) -> Result<()> {
1169 let start_time = Instant::now();
1170 debug!(target: LOG_DEVIMINT, "Awaiting LN gateways registration");
1171
1172 poll("gateways registered", || async {
1173 let num_gateways = cmd!(
1174 self.internal_client()
1175 .await
1176 .map_err(ControlFlow::Continue)?,
1177 "list-gateways"
1178 )
1179 .out_json()
1180 .await
1181 .map_err(ControlFlow::Continue)?
1182 .as_array()
1183 .context("invalid output")
1184 .map_err(ControlFlow::Break)?
1185 .len();
1186
1187 let expected_gateways =
1190 if crate::util::Gatewayd::version_or_default().await < *VERSION_0_10_0_ALPHA {
1191 1
1192 } else {
1193 2
1194 };
1195
1196 poll_eq!(num_gateways, expected_gateways)
1197 })
1198 .await?;
1199 debug!(target: LOG_DEVIMINT,
1200 elapsed_ms = %start_time.elapsed().as_millis(),
1201 "Gateways registered");
1202 Ok(())
1203 }
1204
1205 pub async fn await_all_peers(&self) -> Result<()> {
1206 let (module_name, endpoint) = if crate::util::supports_wallet_v2() {
1207 ("walletv2", "consensus_block_count")
1208 } else {
1209 ("wallet", "block_count")
1210 };
1211 poll("Waiting for all peers to be online", || async {
1212 cmd!(
1213 self.internal_client()
1214 .await
1215 .map_err(ControlFlow::Continue)?,
1216 "dev",
1217 "api",
1218 "--module",
1219 module_name,
1220 endpoint
1221 )
1222 .run()
1223 .await
1224 .map_err(ControlFlow::Continue)?;
1225 Ok(())
1226 })
1227 .await
1228 }
1229
1230 pub async fn await_peer(&self, peer_id: usize) -> Result<()> {
1231 poll("Waiting for all peers to be online", || async {
1232 cmd!(
1233 self.internal_client()
1234 .await
1235 .map_err(ControlFlow::Continue)?,
1236 "dev",
1237 "api",
1238 "--peer-id",
1239 peer_id,
1240 "--module",
1241 "wallet",
1242 "block_count"
1243 )
1244 .run()
1245 .await
1246 .map_err(ControlFlow::Continue)?;
1247 Ok(())
1248 })
1249 .await
1250 }
1251
1252 pub async fn finalize_mempool_tx(&self) -> Result<()> {
1262 let finality_delay = self.get_finality_delay()?;
1263 let blocks_to_mine = finality_delay + 1;
1264 self.bitcoind.mine_blocks(blocks_to_mine.into()).await?;
1265 self.await_block_sync().await?;
1266 Ok(())
1267 }
1268
1269 pub async fn mine_then_wait_blocks_sync(&self, blocks: u64) -> Result<()> {
1270 self.bitcoind.mine_blocks(blocks).await?;
1271 self.await_block_sync().await?;
1272 Ok(())
1273 }
1274
1275 pub fn num_members(&self) -> usize {
1276 self.members.len()
1277 }
1278
1279 pub fn member_ids(&self) -> impl Iterator<Item = PeerId> + '_ {
1280 self.members
1281 .keys()
1282 .map(|&peer_id| PeerId::from(peer_id as u16))
1283 }
1284}
1285
1286#[derive(Clone)]
1287pub struct Fedimintd {
1288 _bitcoind: Bitcoind,
1289 process: ProcessHandle,
1290}
1291
1292impl Fedimintd {
1293 pub async fn new(
1294 process_mgr: &ProcessManager,
1295 bitcoind: Bitcoind,
1296 peer_id: usize,
1297 env: &vars::Fedimintd,
1298 fed_name: String,
1299 ) -> Result<Self> {
1300 debug!(target: LOG_DEVIMINT, "Starting fedimintd-{fed_name}-{peer_id}");
1301 let process = process_mgr
1302 .spawn_daemon(
1303 &format!("fedimintd-{fed_name}-{peer_id}"),
1304 cmd!(FedimintdCmd).envs(env.vars()),
1305 )
1306 .await?;
1307
1308 Ok(Self {
1309 _bitcoind: bitcoind,
1310 process,
1311 })
1312 }
1313
1314 pub async fn terminate(self) -> Result<()> {
1315 self.process.terminate().await
1316 }
1317
1318 pub async fn await_terminated(&self) -> Result<()> {
1319 self.process.await_terminated().await
1320 }
1321}
1322
1323pub async fn run_cli_dkg_v2(endpoints: BTreeMap<PeerId, String>) -> Result<()> {
1324 let status_futures = endpoints.values().map(|endpoint| {
1326 let endpoint = endpoint.clone();
1327 async move {
1328 let status = poll("awaiting-setup-status-awaiting-local-params", || async {
1329 crate::util::FedimintCli
1330 .setup_status(&API_AUTH, &endpoint)
1331 .await
1332 .map_err(ControlFlow::Continue)
1333 })
1334 .await
1335 .unwrap();
1336
1337 assert_eq!(status, SetupStatus::AwaitingLocalParams);
1338 }
1339 });
1340 join_all(status_futures).await;
1341
1342 debug!(target: LOG_DEVIMINT, "Setting local parameters...");
1343
1344 let federation_size =
1347 if crate::util::FedimintCli::version_or_default().await >= *VERSION_0_11_0_ALPHA {
1348 Some(endpoints.len())
1349 } else {
1350 None
1351 };
1352 let local_params_futures = endpoints.iter().map(|(peer, endpoint)| {
1353 let peer = *peer;
1354 let endpoint = endpoint.clone();
1355 async move {
1356 let info = if peer.to_usize() == 0 {
1357 crate::util::FedimintCli
1358 .set_local_params_leader(&peer, &API_AUTH, &endpoint, federation_size)
1359 .await
1360 } else {
1361 crate::util::FedimintCli
1362 .set_local_params_follower(&peer, &API_AUTH, &endpoint)
1363 .await
1364 };
1365 info.map(|i| (peer, i))
1366 }
1367 });
1368 let connection_info: BTreeMap<_, _> = try_join_all(local_params_futures)
1369 .await?
1370 .into_iter()
1371 .collect();
1372
1373 debug!(target: LOG_DEVIMINT, "Exchanging peer connection info...");
1374
1375 let add_peer_futures = connection_info.iter().flat_map(|(peer, info)| {
1378 endpoints
1379 .iter()
1380 .filter(move |(p, _)| *p != peer)
1381 .map(move |(_, endpoint)| {
1382 let endpoint = endpoint.clone();
1383 let info = info.clone();
1384 async move {
1385 crate::util::FedimintCli
1386 .add_peer(&info, &API_AUTH, &endpoint)
1387 .await
1388 }
1389 })
1390 });
1391 try_join_all(add_peer_futures).await?;
1392
1393 debug!(target: LOG_DEVIMINT, "Starting DKG...");
1394
1395 let start_dkg_futures = endpoints.values().map(|endpoint| {
1397 let endpoint = endpoint.clone();
1398 async move {
1399 crate::util::FedimintCli
1400 .start_dkg(&API_AUTH, &endpoint)
1401 .await
1402 }
1403 });
1404 try_join_all(start_dkg_futures).await?;
1405
1406 Ok(())
1407}