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 add_lnv2_gateway(&self, gateway: &str) -> Result<()> {
673 let client = self.internal_client().await?;
674 for peer_id in self.members.keys() {
675 poll("adding lnv2 gateway", || async {
679 cmd!(
680 client,
681 "--our-id",
682 peer_id.to_string(),
683 "--password",
684 API_AUTH.as_str(),
685 "module",
686 "lnv2",
687 "gateways",
688 "add",
689 gateway
690 )
691 .run()
692 .await
693 .map_err(ControlFlow::Continue)
694 })
695 .await?;
696 }
697 Ok(())
698 }
699
700 pub async fn start_server(&mut self, process_mgr: &ProcessManager, peer: usize) -> Result<()> {
701 if self.members.contains_key(&peer) {
702 bail!("fedimintd-{peer} already running");
703 }
704 self.members.insert(
705 peer,
706 Fedimintd::new(
707 process_mgr,
708 self.bitcoind.clone(),
709 peer,
710 &self.vars[&peer],
711 "default".to_string(),
712 )
713 .await?,
714 );
715 Ok(())
716 }
717
718 pub async fn terminate_server(&mut self, peer_id: usize) -> Result<()> {
719 let Some((_, fedimintd)) = self.members.remove_entry(&peer_id) else {
720 bail!("fedimintd-{peer_id} does not exist");
721 };
722 fedimintd.terminate().await?;
723 Ok(())
724 }
725
726 pub async fn await_server_terminated(&mut self, peer_id: usize) -> Result<()> {
727 let Some(fedimintd) = self.members.get_mut(&peer_id) else {
728 bail!("fedimintd-{peer_id} does not exist");
729 };
730 fedimintd.await_terminated().await?;
731 self.members.remove(&peer_id);
732 Ok(())
733 }
734
735 pub async fn start_all_servers(&mut self, process_mgr: &ProcessManager) -> Result<()> {
737 info!("starting all servers");
738 let fed_size = process_mgr.globals.FM_FED_SIZE;
739 for peer_id in 0..fed_size {
740 if self.members.contains_key(&peer_id) {
741 continue;
742 }
743 self.start_server(process_mgr, peer_id).await?;
744 }
745 self.await_all_peers().await?;
746 Ok(())
747 }
748
749 pub async fn terminate_all_servers(&mut self) -> Result<()> {
751 info!("terminating all servers");
752 let running_peer_ids: Vec<_> = self.members.keys().copied().collect();
753 for peer_id in running_peer_ids {
754 self.terminate_server(peer_id).await?;
755 }
756 Ok(())
757 }
758
759 pub async fn restart_all_staggered_with_bin(
764 &mut self,
765 process_mgr: &ProcessManager,
766 bin_path: &PathBuf,
767 ) -> Result<()> {
768 let fed_size = process_mgr.globals.FM_FED_SIZE;
769
770 self.start_all_servers(process_mgr).await?;
772
773 while self.num_members() > 0 {
775 self.terminate_server(self.num_members() - 1).await?;
776 if self.num_members() > 0 {
777 fedimint_core::task::sleep_in_test(
778 "waiting to shutdown remaining peers",
779 Duration::from_secs(10),
780 )
781 .await;
782 }
783 }
784
785 unsafe { std::env::set_var("FM_FEDIMINTD_BASE_EXECUTABLE", bin_path) };
787
788 for peer_id in 0..fed_size {
790 self.start_server(process_mgr, peer_id).await?;
791 if peer_id < fed_size - 1 {
792 fedimint_core::task::sleep_in_test(
793 "waiting to restart remaining peers",
794 Duration::from_secs(10),
795 )
796 .await;
797 }
798 }
799
800 self.await_all_peers().await?;
801
802 let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
803 info!("upgraded fedimintd to version: {}", fedimintd_version);
804 Ok(())
805 }
806
807 pub async fn restart_all_with_bin(
808 &mut self,
809 process_mgr: &ProcessManager,
810 bin_path: &PathBuf,
811 ) -> Result<()> {
812 let current_fedimintd_path = std::env::var("FM_FEDIMINTD_BASE_EXECUTABLE")?;
814 unsafe { std::env::set_var("FM_FEDIMINTD_BASE_EXECUTABLE", bin_path) };
816 unsafe { std::env::set_var("FM_FEDIMINTD_BASE_EXECUTABLE", current_fedimintd_path) };
818
819 self.restart_all_staggered_with_bin(process_mgr, bin_path)
820 .await
821 }
822
823 pub async fn degrade_federation(&mut self, process_mgr: &ProcessManager) -> Result<()> {
824 let fed_size = process_mgr.globals.FM_FED_SIZE;
825 let offline_nodes = process_mgr.globals.FM_OFFLINE_NODES;
826 anyhow::ensure!(
827 fed_size > 3 * offline_nodes,
828 "too many offline nodes ({offline_nodes}) to reach consensus"
829 );
830
831 while self.num_members() > fed_size - offline_nodes {
832 self.terminate_server(self.num_members() - 1).await?;
833 }
834
835 if offline_nodes > 0 {
836 info!(fed_size, offline_nodes, "federation is degraded");
837 }
838 Ok(())
839 }
840
841 pub async fn send_to_address(&self, address: String, amount: u64) -> Result<()> {
842 self.bitcoind.send_to(address, amount).await?;
843
844 self.bitcoind.mine_blocks(21).await?;
845
846 Ok(())
847 }
848
849 pub async fn pegin_client_no_wait(&self, amount: u64, client: &Client) -> Result<String> {
850 let deposit_fees_msat = self.deposit_fees()?.msats;
851 assert_eq!(
852 deposit_fees_msat % 1000,
853 0,
854 "Deposit fees expected to be whole sats in test suite"
855 );
856 let deposit_fees = deposit_fees_msat / 1000;
857 info!(amount, deposit_fees, "Pegging-in client funds");
858
859 let (address, handle) = client.get_deposit_addr().await?;
862
863 self.bitcoind
864 .send_to(address, amount + deposit_fees)
865 .await?;
866 self.bitcoind.mine_blocks(21).await?;
867
868 Ok(handle)
869 }
870
871 pub async fn pegin_client(&self, amount: u64, client: &Client) -> Result<()> {
872 let walletv2_await_receive = crate::util::supports_wallet_v2()
873 && crate::util::FedimintCli::version_or_default().await >= *VERSION_0_12_0_ALPHA;
874
875 let initial_balance = if crate::util::supports_wallet_v2() && !walletv2_await_receive {
879 Some(client.balance().await?)
880 } else {
881 None
882 };
883
884 let handle = self.pegin_client_no_wait(amount, client).await?;
885
886 if walletv2_await_receive {
887 client.await_receive(&handle).await?;
890 } else if let Some(initial) = initial_balance {
891 let expected_balance = initial + (amount * 1000 * 9 / 10);
894 client.await_balance(expected_balance).await?;
895 } else {
896 client.await_deposit(&handle).await?;
897 }
898
899 Ok(())
900 }
901
902 pub async fn pegin_gateways(
905 &self,
906 amount: u64,
907 gateways: Vec<&super::gatewayd::Gatewayd>,
908 ) -> Result<()> {
909 let deposit_fees_msat = self.deposit_fees()?.msats;
910 assert_eq!(
911 deposit_fees_msat % 1000,
912 0,
913 "Deposit fees expected to be whole sats in test suite"
914 );
915 let deposit_fees = deposit_fees_msat / 1000;
916 info!(amount, deposit_fees, "Pegging-in gateway funds");
917 let fed_id = self.calculate_federation_id();
918
919 let uses_walletv2 = crate::util::supports_wallet_v2();
920
921 let walletv2_claim_wait = uses_walletv2
926 && crate::util::GatewayCli::version_or_default().await >= *VERSION_0_10_0_ALPHA;
927
928 let mut initial_balances = Vec::new();
932 if uses_walletv2 {
933 for gw in &gateways {
934 let balance = gw
935 .client()
936 .ecash_balance(fed_id.clone())
937 .await
938 .expect("failed to fetch initial gateway balance");
939 initial_balances.push(balance);
940 }
941 }
942
943 let mut gateway_deposit_addrs = Vec::new();
944 for gw in gateways.clone() {
945 let pegin_addr = gw.client().get_pegin_addr(&fed_id).await?;
946 debug!(
947 gateway = %gw.gw_name,
948 ln = %gw.ln.ln_type(),
949 address = %pegin_addr,
950 amount_sats = amount + deposit_fees,
951 uses_walletv2,
952 "Sending gateway pegin"
953 );
954 self.bitcoind
955 .send_to(pegin_addr.clone(), amount + deposit_fees)
956 .await?;
957 gateway_deposit_addrs.push(pegin_addr);
958 }
959
960 let pegin_start = Instant::now();
961 self.bitcoind.mine_blocks(21).await?;
962 let bitcoind_block_height: u64 = self.bitcoind.get_block_count().await? - 1;
963 debug!(
964 bitcoind_block_height,
965 elapsed_ms = %pegin_start.elapsed().as_millis(),
966 "Mined gateway pegin blocks"
967 );
968
969 if walletv2_claim_wait {
970 const CLAIM_TIMEOUT: Duration = Duration::from_secs(180);
975 const PAYMENT_LOG_PAGE_SIZE: usize = 100;
976
977 try_join_all(
978 gateways
979 .iter()
980 .enumerate()
981 .filter(|(_, gw)| gw.gatewayd_version >= *VERSION_0_11_0_ALPHA)
982 .map(|(i, gw)| {
983 let fed_id = fed_id.clone();
984 let gateway_name = gw.gw_name.clone();
985 let gateway_ln = gw.ln.ln_type().to_string();
986 let deposit_address = gateway_deposit_addrs[i].clone();
987 poll_with_timeout("gateway pegin claim", CLAIM_TIMEOUT, move || {
988 let fed_id = fed_id.clone();
989 let gateway_name = gateway_name.clone();
990 let gateway_ln = gateway_ln.clone();
991 let deposit_address = deposit_address.clone();
992 async move {
993 let events = gw
994 .client()
995 .payment_log(
996 &fed_id,
997 &["payment-receive", "payment-receive-update"],
998 PAYMENT_LOG_PAGE_SIZE,
999 )
1000 .await
1001 .map_err(ControlFlow::Continue)?;
1002
1003 if !walletv2_receive_claimed(&events, &deposit_address) {
1004 return Err(ControlFlow::Continue(anyhow::anyhow!(
1005 "Gateway {gateway_name} ({gateway_ln}) has not claimed the deposit to {deposit_address} yet"
1006 )));
1007 }
1008
1009 debug!(
1010 gateway = %gateway_name,
1011 ln = %gateway_ln,
1012 address = %deposit_address,
1013 elapsed_ms = %pegin_start.elapsed().as_millis(),
1014 "Walletv2 gateway pegin claimed"
1015 );
1016
1017 Ok(())
1018 }
1019 })
1020 }),
1021 )
1022 .await?;
1023 }
1024
1025 try_join_all(gateways.into_iter().enumerate().map(|(i, gw)| {
1026 let initial_balance = if uses_walletv2 {
1027 initial_balances[i]
1028 } else {
1029 0
1030 };
1031 let gateway_name = gw.gw_name.clone();
1032 let gateway_ln = gw.ln.ln_type().to_string();
1033 let gateway_id = gw.gateway_id.clone();
1034 let gateway_index = gw.gateway_index;
1035 let deposit_address = gateway_deposit_addrs[i].clone();
1036 let fed_id = fed_id.clone();
1037 poll("gateway pegin", move || {
1038 let fed_id = fed_id.clone();
1039 let gateway_name = gateway_name.clone();
1040 let gateway_ln = gateway_ln.clone();
1041 let gateway_id = gateway_id.clone();
1042 let deposit_address = deposit_address.clone();
1043 async move {
1044 let gw_info = gw
1045 .client()
1046 .get_info()
1047 .await
1048 .map_err(ControlFlow::Continue)?;
1049
1050 let block_height: u64 = if gw.gatewayd_version < *VERSION_0_10_0_ALPHA {
1051 gw_info["block_height"]
1052 .as_u64()
1053 .expect("Could not parse block height")
1054 } else {
1055 gw_info["lightning_info"]["connected"]["block_height"]
1056 .as_u64()
1057 .expect("Could not parse block height")
1058 };
1059
1060 if bitcoind_block_height != block_height {
1061 debug!(
1062 gateway = %gateway_name,
1063 ln = %gateway_ln,
1064 bitcoind_block_height,
1065 gateway_block_height = block_height,
1066 elapsed_ms = %pegin_start.elapsed().as_millis(),
1067 "Waiting for gateway pegin block sync"
1068 );
1069 return Err(std::ops::ControlFlow::Continue(anyhow::anyhow!(
1070 "Gateway {gateway_name} ({gateway_id}, index {gateway_index}) block height {block_height} has not reached bitcoind block height {bitcoind_block_height}"
1071 )));
1072 }
1073
1074 let gateway_balance = gw
1075 .client()
1076 .ecash_balance(fed_id)
1077 .await
1078 .map_err(ControlFlow::Continue)?;
1079
1080 if uses_walletv2 {
1081 let expected = initial_balance + (amount * 1000 * 9 / 10);
1085 debug!(
1086 gateway = %gateway_name,
1087 ln = %gateway_ln,
1088 gateway_balance_msats = gateway_balance,
1089 expected_msats = expected,
1090 initial_balance_msats = initial_balance,
1091 elapsed_ms = %pegin_start.elapsed().as_millis(),
1092 "Checked walletv2 gateway pegin balance"
1093 );
1094 if expected <= gateway_balance {
1095 Ok(())
1096 } else {
1097 Err(ControlFlow::Continue(anyhow::anyhow!(
1098 "Gateway {gateway_name} ({gateway_id}, index {gateway_index}) balance {gateway_balance} has not reached expected {expected} for deposit address {deposit_address} (initial: {initial_balance})"
1099 )))
1100 }
1101 } else {
1102 poll_almost_equal!(gateway_balance, amount * 1000)
1103 }
1104 }
1105 })
1106 }))
1107 .await?;
1108
1109 Ok(())
1110 }
1111
1112 pub async fn pegout_gateways(
1115 &self,
1116 amount: u64,
1117 gateways: Vec<&super::gatewayd::Gatewayd>,
1118 ) -> Result<()> {
1119 info!(amount, "Pegging-out gateway funds");
1120 let fed_id = self.calculate_federation_id();
1121 let mut peg_outs: BTreeMap<LightningNodeType, (Amount, WithdrawResponse)> = BTreeMap::new();
1122 for gw in gateways.clone() {
1123 let prev_fed_ecash_balance = gw
1124 .client()
1125 .get_balances()
1126 .await?
1127 .ecash_balances
1128 .into_iter()
1129 .find(|fed| fed.federation_id.to_string() == fed_id)
1130 .expect("Gateway has not joined federation")
1131 .ecash_balance_msats;
1132
1133 let pegout_address = self.bitcoind.get_new_address().await?;
1134 let response = gw
1135 .client()
1136 .pegout(fed_id.clone(), amount, pegout_address)
1137 .await?;
1138 peg_outs.insert(gw.ln.ln_type(), (prev_fed_ecash_balance, response));
1139 }
1140 self.bitcoind.mine_blocks(21).await?;
1141
1142 try_join_all(
1143 peg_outs
1144 .values()
1145 .map(|(_, pegout)| self.bitcoind.poll_get_transaction(pegout.txid)),
1146 )
1147 .await?;
1148
1149 for gw in gateways.clone() {
1150 let after_fed_ecash_balance = gw
1151 .client()
1152 .get_balances()
1153 .await?
1154 .ecash_balances
1155 .into_iter()
1156 .find(|fed| fed.federation_id.to_string() == fed_id)
1157 .expect("Gateway has not joined federation")
1158 .ecash_balance_msats;
1159
1160 let ln_type = gw.ln.ln_type();
1161 let prev_balance = peg_outs
1162 .get(&ln_type)
1163 .expect("peg out does not exist")
1164 .0
1165 .msats;
1166 let fees = peg_outs
1167 .get(&ln_type)
1168 .expect("peg out does not exist")
1169 .1
1170 .fees;
1171 let total_fee = fees.amount().to_sat() * 1000;
1172 let tolerance = if crate::util::supports_wallet_v2() {
1175 let amount_sats = amount / 1000;
1176 let module_fee_sats = 100 + amount_sats / 100;
1177 module_fee_sats * 1000 + 2000
1178 } else if crate::util::supports_mint_v2() {
1179 4000
1180 } else {
1181 2000
1182 };
1183 crate::util::almost_equal(
1184 after_fed_ecash_balance.msats,
1185 prev_balance - amount - total_fee,
1186 tolerance,
1187 )
1188 .map_err(|e| {
1189 anyhow::anyhow!(
1190 "new balance did not equal prev balance minus withdraw_amount minus fees: {e}"
1191 )
1192 })?;
1193 }
1194
1195 Ok(())
1196 }
1197
1198 pub fn calculate_federation_id(&self) -> String {
1199 self.client_config()
1200 .unwrap()
1201 .global
1202 .calculate_federation_id()
1203 .to_string()
1204 }
1205
1206 pub async fn await_block_sync(&self) -> Result<u64> {
1207 let finality_delay = self.get_finality_delay()?;
1208 let block_count = self.bitcoind.get_block_count().await?;
1209 let expected = block_count.saturating_sub(finality_delay.into());
1210
1211 if crate::util::supports_wallet_v2() {
1212 let client = self.internal_client().await?;
1214 loop {
1215 let value = cmd!(client, "module", "walletv2", "info", "block-count")
1216 .out_json()
1217 .await?;
1218 let current: u64 = serde_json::from_value(value)?;
1219 if current >= expected {
1220 break;
1221 }
1222 fedimint_core::task::sleep_in_test(
1223 format!("Waiting for consensus block count to reach {expected}"),
1224 std::time::Duration::from_secs(1),
1225 )
1226 .await;
1227 }
1228 } else {
1229 cmd!(
1230 self.internal_client().await?,
1231 "dev",
1232 "wait-block-count",
1233 expected
1234 )
1235 .run()
1236 .await?;
1237 }
1238
1239 Ok(expected)
1240 }
1241
1242 fn get_finality_delay(&self) -> Result<u32, anyhow::Error> {
1243 if crate::util::supports_wallet_v2() {
1245 return Ok(fedimint_walletv2_server::CONFIRMATION_FINALITY_DELAY as u32);
1246 }
1247
1248 let wallet_instance_id = self.module_instance_id_by_kind(&fedimint_wallet_client::KIND)?;
1249 let client_config = &self.client_config()?;
1250 let wallet_cfg = client_config
1251 .modules
1252 .get(&wallet_instance_id)
1253 .context("wallet module not found")?
1254 .clone()
1255 .redecode_raw(&ModuleDecoderRegistry::new([(
1256 wallet_instance_id,
1257 fedimint_wallet_client::KIND,
1258 fedimint_wallet_client::WalletModuleTypes::decoder(),
1259 )]))?;
1260 let wallet_cfg: &WalletClientConfig = wallet_cfg.cast()?;
1261
1262 let finality_delay = wallet_cfg.finality_delay;
1263 Ok(finality_delay)
1264 }
1265
1266 pub async fn await_gateways_registered(&self) -> Result<()> {
1267 if !crate::util::supports_lnv1() {
1273 return Ok(());
1274 }
1275
1276 let start_time = Instant::now();
1277 debug!(target: LOG_DEVIMINT, "Awaiting LN gateways registration");
1278
1279 poll("gateways registered", || async {
1280 let num_gateways = cmd!(
1281 self.internal_client()
1282 .await
1283 .map_err(ControlFlow::Continue)?,
1284 "list-gateways"
1285 )
1286 .out_json()
1287 .await
1288 .map_err(ControlFlow::Continue)?
1289 .as_array()
1290 .context("invalid output")
1291 .map_err(ControlFlow::Break)?
1292 .len();
1293
1294 let expected_gateways =
1297 if crate::util::Gatewayd::version_or_default().await < *VERSION_0_10_0_ALPHA {
1298 1
1299 } else {
1300 2
1301 };
1302
1303 poll_eq!(num_gateways, expected_gateways)
1304 })
1305 .await?;
1306 debug!(target: LOG_DEVIMINT,
1307 elapsed_ms = %start_time.elapsed().as_millis(),
1308 "Gateways registered");
1309 Ok(())
1310 }
1311
1312 pub async fn await_all_peers(&self) -> Result<()> {
1313 let (module_name, endpoint) = if crate::util::supports_wallet_v2() {
1314 ("walletv2", "consensus_block_count")
1315 } else {
1316 ("wallet", "block_count")
1317 };
1318 poll("Waiting for all peers to be online", || async {
1319 cmd!(
1320 self.internal_client()
1321 .await
1322 .map_err(ControlFlow::Continue)?,
1323 "dev",
1324 "api",
1325 "--module",
1326 module_name,
1327 endpoint
1328 )
1329 .run()
1330 .await
1331 .map_err(ControlFlow::Continue)?;
1332 Ok(())
1333 })
1334 .await
1335 }
1336
1337 pub async fn await_peer(&self, peer_id: usize) -> Result<()> {
1338 poll("Waiting for all peers to be online", || async {
1339 cmd!(
1340 self.internal_client()
1341 .await
1342 .map_err(ControlFlow::Continue)?,
1343 "dev",
1344 "api",
1345 "--peer-id",
1346 peer_id,
1347 "--module",
1348 "wallet",
1349 "block_count"
1350 )
1351 .run()
1352 .await
1353 .map_err(ControlFlow::Continue)?;
1354 Ok(())
1355 })
1356 .await
1357 }
1358
1359 pub async fn finalize_mempool_tx(&self) -> Result<()> {
1369 let finality_delay = self.get_finality_delay()?;
1370 let blocks_to_mine = finality_delay + 1;
1371 self.bitcoind.mine_blocks(blocks_to_mine.into()).await?;
1372 self.await_block_sync().await?;
1373 Ok(())
1374 }
1375
1376 pub async fn mine_then_wait_blocks_sync(&self, blocks: u64) -> Result<()> {
1377 self.bitcoind.mine_blocks(blocks).await?;
1378 self.await_block_sync().await?;
1379 Ok(())
1380 }
1381
1382 pub fn num_members(&self) -> usize {
1383 self.members.len()
1384 }
1385
1386 pub fn member_ids(&self) -> impl Iterator<Item = PeerId> + '_ {
1387 self.members
1388 .keys()
1389 .map(|&peer_id| PeerId::from(peer_id as u16))
1390 }
1391}
1392
1393fn walletv2_receive_claimed(events: &serde_json::Value, deposit_address: &str) -> bool {
1401 let Some(events) = events.as_array() else {
1402 return false;
1403 };
1404
1405 let is_walletv2 = |event: &serde_json::Value| event["module"]["kind"] == "walletv2";
1406
1407 let receive_operations = events
1408 .iter()
1409 .filter(|event| {
1410 is_walletv2(event)
1411 && event["kind"] == "payment-receive"
1412 && event["payload"]["address"] == deposit_address
1413 })
1414 .map(|event| &event["payload"]["operation_id"])
1415 .collect::<Vec<_>>();
1416
1417 events.iter().any(|event| {
1418 is_walletv2(event)
1419 && event["kind"] == "payment-receive-update"
1420 && event["payload"]["status"] == "Success"
1421 && receive_operations.contains(&&event["payload"]["operation_id"])
1422 })
1423}
1424
1425#[derive(Clone)]
1426pub struct Fedimintd {
1427 _bitcoind: Bitcoind,
1428 process: ProcessHandle,
1429}
1430
1431impl Fedimintd {
1432 pub async fn new(
1433 process_mgr: &ProcessManager,
1434 bitcoind: Bitcoind,
1435 peer_id: usize,
1436 env: &vars::Fedimintd,
1437 fed_name: String,
1438 ) -> Result<Self> {
1439 debug!(target: LOG_DEVIMINT, "Starting fedimintd-{fed_name}-{peer_id}");
1440 let process = process_mgr
1441 .spawn_daemon(
1442 &format!("fedimintd-{fed_name}-{peer_id}"),
1443 cmd!(FedimintdCmd).envs(env.vars()),
1444 )
1445 .await?;
1446
1447 Ok(Self {
1448 _bitcoind: bitcoind,
1449 process,
1450 })
1451 }
1452
1453 pub async fn terminate(self) -> Result<()> {
1454 self.process.terminate().await
1455 }
1456
1457 pub async fn await_terminated(&self) -> Result<()> {
1458 self.process.await_terminated().await
1459 }
1460}
1461
1462pub async fn run_cli_dkg_v2(endpoints: BTreeMap<PeerId, String>) -> Result<()> {
1463 let status_futures = endpoints.values().map(|endpoint| {
1465 let endpoint = endpoint.clone();
1466 async move {
1467 let status = poll("awaiting-setup-status-awaiting-local-params", || async {
1468 crate::util::FedimintCli
1469 .setup_status(&API_AUTH, &endpoint)
1470 .await
1471 .map_err(ControlFlow::Continue)
1472 })
1473 .await
1474 .unwrap();
1475
1476 assert_eq!(status, SetupStatus::AwaitingLocalParams);
1477 }
1478 });
1479 join_all(status_futures).await;
1480
1481 debug!(target: LOG_DEVIMINT, "Setting local parameters...");
1482
1483 let federation_size =
1486 if crate::util::FedimintCli::version_or_default().await >= *VERSION_0_11_0_ALPHA {
1487 Some(endpoints.len())
1488 } else {
1489 None
1490 };
1491 let local_params_futures = endpoints.iter().map(|(peer, endpoint)| {
1492 let peer = *peer;
1493 let endpoint = endpoint.clone();
1494 async move {
1495 let info = if peer.to_usize() == 0 {
1496 crate::util::FedimintCli
1497 .set_local_params_leader(&peer, &API_AUTH, &endpoint, federation_size)
1498 .await
1499 } else {
1500 crate::util::FedimintCli
1501 .set_local_params_follower(&peer, &API_AUTH, &endpoint)
1502 .await
1503 };
1504 info.map(|i| (peer, i))
1505 }
1506 });
1507 let connection_info: BTreeMap<_, _> = try_join_all(local_params_futures)
1508 .await?
1509 .into_iter()
1510 .collect();
1511
1512 debug!(target: LOG_DEVIMINT, "Exchanging peer connection info...");
1513
1514 let add_peer_futures = connection_info.iter().flat_map(|(peer, info)| {
1517 endpoints
1518 .iter()
1519 .filter(move |(p, _)| *p != peer)
1520 .map(move |(_, endpoint)| {
1521 let endpoint = endpoint.clone();
1522 let info = info.clone();
1523 async move {
1524 crate::util::FedimintCli
1525 .add_peer(&info, &API_AUTH, &endpoint)
1526 .await
1527 }
1528 })
1529 });
1530 try_join_all(add_peer_futures).await?;
1531
1532 debug!(target: LOG_DEVIMINT, "Starting DKG...");
1533
1534 let start_dkg_futures = endpoints.values().map(|endpoint| {
1536 let endpoint = endpoint.clone();
1537 async move {
1538 crate::util::FedimintCli
1539 .start_dkg(&API_AUTH, &endpoint)
1540 .await
1541 }
1542 });
1543 try_join_all(start_dkg_futures).await?;
1544
1545 Ok(())
1546}