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::JitTry;
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: JitTry<Client, anyhow::Error>,
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 = JitTry::<_, anyhow::Error>::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 Ok(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(
1116 &self,
1117 amount: u64,
1118 gateways: Vec<&super::gatewayd::Gatewayd>,
1119 ) -> Result<()> {
1120 info!(amount, "Pegging-out gateway funds");
1121 let fed_id = self.calculate_federation_id();
1122 let mut peg_outs: BTreeMap<LightningNodeType, (Amount, WithdrawResponse)> = BTreeMap::new();
1123 for gw in gateways.clone() {
1124 let prev_fed_ecash_balance = gw
1125 .client()
1126 .get_balances()
1127 .await?
1128 .ecash_balances
1129 .into_iter()
1130 .find(|fed| fed.federation_id.to_string() == fed_id)
1131 .expect("Gateway has not joined federation")
1132 .ecash_balance_msats;
1133
1134 let pegout_address = self.bitcoind.get_new_address().await?;
1135 let response = gw
1136 .client()
1137 .pegout(fed_id.clone(), amount, pegout_address)
1138 .await?;
1139 peg_outs.insert(gw.ln.ln_type(), (prev_fed_ecash_balance, response));
1140 }
1141 try_join_all(
1145 peg_outs
1146 .values()
1147 .map(|(_, pegout)| self.bitcoind.poll_get_transaction(pegout.txid)),
1148 )
1149 .await?;
1150 self.finalize_mempool_tx().await?;
1151
1152 for gw in gateways.clone() {
1153 let after_fed_ecash_balance = gw
1154 .client()
1155 .get_balances()
1156 .await?
1157 .ecash_balances
1158 .into_iter()
1159 .find(|fed| fed.federation_id.to_string() == fed_id)
1160 .expect("Gateway has not joined federation")
1161 .ecash_balance_msats;
1162
1163 let ln_type = gw.ln.ln_type();
1164 let prev_balance = peg_outs
1165 .get(&ln_type)
1166 .expect("peg out does not exist")
1167 .0
1168 .msats;
1169 let fees = peg_outs
1170 .get(&ln_type)
1171 .expect("peg out does not exist")
1172 .1
1173 .fees;
1174 let total_fee = fees.amount().to_sat() * 1000;
1175 let tolerance = if crate::util::supports_wallet_v2() {
1178 let amount_sats = amount / 1000;
1179 let module_fee_sats = 100 + amount_sats / 100;
1180 module_fee_sats * 1000 + 2000
1181 } else if crate::util::supports_mint_v2() {
1182 4000
1183 } else {
1184 2000
1185 };
1186 crate::util::almost_equal(
1187 after_fed_ecash_balance.msats,
1188 prev_balance - amount - total_fee,
1189 tolerance,
1190 )
1191 .map_err(|e| {
1192 anyhow::anyhow!(
1193 "new balance did not equal prev balance minus withdraw_amount minus fees: {e}"
1194 )
1195 })?;
1196 }
1197
1198 Ok(())
1199 }
1200
1201 pub fn calculate_federation_id(&self) -> String {
1202 self.client_config()
1203 .unwrap()
1204 .global
1205 .calculate_federation_id()
1206 .to_string()
1207 }
1208
1209 pub async fn await_block_sync(&self) -> Result<u64> {
1210 let finality_delay = self.get_finality_delay()?;
1211 let block_count = self.bitcoind.get_block_count().await?;
1212 let expected = block_count.saturating_sub(finality_delay.into());
1213
1214 if crate::util::supports_wallet_v2() {
1215 let client = self.internal_client().await?;
1217 loop {
1218 let value = cmd!(client, "module", "walletv2", "info", "block-count")
1219 .out_json()
1220 .await?;
1221 let current: u64 = serde_json::from_value(value)?;
1222 if current >= expected {
1223 break;
1224 }
1225 fedimint_core::task::sleep_in_test(
1226 format!("Waiting for consensus block count to reach {expected}"),
1227 std::time::Duration::from_secs(1),
1228 )
1229 .await;
1230 }
1231 } else {
1232 cmd!(
1233 self.internal_client().await?,
1234 "dev",
1235 "wait-block-count",
1236 expected
1237 )
1238 .run()
1239 .await?;
1240 }
1241
1242 Ok(expected)
1243 }
1244
1245 fn get_finality_delay(&self) -> Result<u32, anyhow::Error> {
1246 if crate::util::supports_wallet_v2() {
1248 return Ok(fedimint_walletv2_server::BLOCK_COUNT_VOTE_DELAY as u32);
1249 }
1250
1251 let wallet_instance_id = self.module_instance_id_by_kind(&fedimint_wallet_client::KIND)?;
1252 let client_config = &self.client_config()?;
1253 let wallet_cfg = client_config
1254 .modules
1255 .get(&wallet_instance_id)
1256 .context("wallet module not found")?
1257 .clone()
1258 .redecode_raw(&ModuleDecoderRegistry::new([(
1259 wallet_instance_id,
1260 fedimint_wallet_client::KIND,
1261 fedimint_wallet_client::WalletModuleTypes::decoder(),
1262 )]))?;
1263 let wallet_cfg: &WalletClientConfig = wallet_cfg.cast()?;
1264
1265 let finality_delay = wallet_cfg.finality_delay;
1266 Ok(finality_delay)
1267 }
1268
1269 pub async fn await_gateways_registered(&self) -> Result<()> {
1270 if !crate::util::supports_lnv1() {
1276 return Ok(());
1277 }
1278
1279 let start_time = Instant::now();
1280 debug!(target: LOG_DEVIMINT, "Awaiting LN gateways registration");
1281
1282 poll("gateways registered", || async {
1283 let num_gateways = cmd!(
1284 self.internal_client()
1285 .await
1286 .map_err(ControlFlow::Continue)?,
1287 "list-gateways"
1288 )
1289 .out_json()
1290 .await
1291 .map_err(ControlFlow::Continue)?
1292 .as_array()
1293 .context("invalid output")
1294 .map_err(ControlFlow::Break)?
1295 .len();
1296
1297 let expected_gateways =
1300 if crate::util::Gatewayd::version_or_default().await < *VERSION_0_10_0_ALPHA {
1301 1
1302 } else {
1303 2
1304 };
1305
1306 poll_eq!(num_gateways, expected_gateways)
1307 })
1308 .await?;
1309 debug!(target: LOG_DEVIMINT,
1310 elapsed_ms = %start_time.elapsed().as_millis(),
1311 "Gateways registered");
1312 Ok(())
1313 }
1314
1315 pub async fn await_all_peers(&self) -> Result<()> {
1316 let (module_name, endpoint) = if crate::util::supports_wallet_v2() {
1317 ("walletv2", "consensus_block_count")
1318 } else {
1319 ("wallet", "block_count")
1320 };
1321 poll("Waiting for all peers to be online", || async {
1322 cmd!(
1323 self.internal_client()
1324 .await
1325 .map_err(ControlFlow::Continue)?,
1326 "dev",
1327 "api",
1328 "--module",
1329 module_name,
1330 endpoint
1331 )
1332 .run()
1333 .await
1334 .map_err(ControlFlow::Continue)?;
1335 Ok(())
1336 })
1337 .await
1338 }
1339
1340 pub async fn await_peer(&self, peer_id: usize) -> Result<()> {
1341 poll("Waiting for all peers to be online", || async {
1342 cmd!(
1343 self.internal_client()
1344 .await
1345 .map_err(ControlFlow::Continue)?,
1346 "dev",
1347 "api",
1348 "--peer-id",
1349 peer_id,
1350 "--module",
1351 "wallet",
1352 "block_count"
1353 )
1354 .run()
1355 .await
1356 .map_err(ControlFlow::Continue)?;
1357 Ok(())
1358 })
1359 .await
1360 }
1361
1362 pub async fn finalize_mempool_tx(&self) -> Result<()> {
1372 let finality_delay = self.get_finality_delay()?;
1373 let blocks_to_mine = finality_delay + 1;
1374 self.bitcoind.mine_blocks(blocks_to_mine.into()).await?;
1375 self.await_block_sync().await?;
1376 Ok(())
1377 }
1378
1379 pub async fn mine_then_wait_blocks_sync(&self, blocks: u64) -> Result<()> {
1380 self.bitcoind.mine_blocks(blocks).await?;
1381 self.await_block_sync().await?;
1382 Ok(())
1383 }
1384
1385 pub fn num_members(&self) -> usize {
1386 self.members.len()
1387 }
1388
1389 pub fn member_ids(&self) -> impl Iterator<Item = PeerId> + '_ {
1390 self.members
1391 .keys()
1392 .map(|&peer_id| PeerId::from(peer_id as u16))
1393 }
1394}
1395
1396fn walletv2_receive_claimed(events: &serde_json::Value, deposit_address: &str) -> bool {
1404 let Some(events) = events.as_array() else {
1405 return false;
1406 };
1407
1408 let is_walletv2 = |event: &serde_json::Value| event["module"]["kind"] == "walletv2";
1409
1410 let receive_operations = events
1411 .iter()
1412 .filter(|event| {
1413 is_walletv2(event)
1414 && event["kind"] == "payment-receive"
1415 && event["payload"]["address"] == deposit_address
1416 })
1417 .map(|event| &event["payload"]["operation_id"])
1418 .collect::<Vec<_>>();
1419
1420 events.iter().any(|event| {
1421 is_walletv2(event)
1422 && event["kind"] == "payment-receive-update"
1423 && event["payload"]["status"] == "Success"
1424 && receive_operations.contains(&&event["payload"]["operation_id"])
1425 })
1426}
1427
1428#[derive(Clone)]
1429pub struct Fedimintd {
1430 _bitcoind: Bitcoind,
1431 process: ProcessHandle,
1432}
1433
1434impl Fedimintd {
1435 pub async fn new(
1436 process_mgr: &ProcessManager,
1437 bitcoind: Bitcoind,
1438 peer_id: usize,
1439 env: &vars::Fedimintd,
1440 fed_name: String,
1441 ) -> Result<Self> {
1442 debug!(target: LOG_DEVIMINT, "Starting fedimintd-{fed_name}-{peer_id}");
1443 let process = process_mgr
1444 .spawn_daemon(
1445 &format!("fedimintd-{fed_name}-{peer_id}"),
1446 cmd!(FedimintdCmd).envs(env.vars()),
1447 )
1448 .await?;
1449
1450 Ok(Self {
1451 _bitcoind: bitcoind,
1452 process,
1453 })
1454 }
1455
1456 pub async fn terminate(self) -> Result<()> {
1457 self.process.terminate().await
1458 }
1459
1460 pub async fn await_terminated(&self) -> Result<()> {
1461 self.process.await_terminated().await
1462 }
1463}
1464
1465pub async fn run_cli_dkg_v2(endpoints: BTreeMap<PeerId, String>) -> Result<()> {
1466 let status_futures = endpoints.values().map(|endpoint| {
1468 let endpoint = endpoint.clone();
1469 async move {
1470 let status = poll("awaiting-setup-status-awaiting-local-params", || async {
1471 crate::util::FedimintCli
1472 .setup_status(&API_AUTH, &endpoint)
1473 .await
1474 .map_err(ControlFlow::Continue)
1475 })
1476 .await
1477 .unwrap();
1478
1479 assert_eq!(status, SetupStatus::AwaitingLocalParams);
1480 }
1481 });
1482 join_all(status_futures).await;
1483
1484 debug!(target: LOG_DEVIMINT, "Setting local parameters...");
1485
1486 let federation_size =
1489 if crate::util::FedimintCli::version_or_default().await >= *VERSION_0_11_0_ALPHA {
1490 Some(endpoints.len())
1491 } else {
1492 None
1493 };
1494 let local_params_futures = endpoints.iter().map(|(peer, endpoint)| {
1495 let peer = *peer;
1496 let endpoint = endpoint.clone();
1497 async move {
1498 let info = if peer.to_usize() == 0 {
1499 crate::util::FedimintCli
1500 .set_local_params_leader(&peer, &API_AUTH, &endpoint, federation_size)
1501 .await
1502 } else {
1503 crate::util::FedimintCli
1504 .set_local_params_follower(&peer, &API_AUTH, &endpoint)
1505 .await
1506 };
1507 info.map(|i| (peer, i))
1508 }
1509 });
1510 let connection_info: BTreeMap<_, _> = try_join_all(local_params_futures)
1511 .await?
1512 .into_iter()
1513 .collect();
1514
1515 debug!(target: LOG_DEVIMINT, "Exchanging peer connection info...");
1516
1517 let add_peer_futures = connection_info.iter().flat_map(|(peer, info)| {
1520 endpoints
1521 .iter()
1522 .filter(move |(p, _)| *p != peer)
1523 .map(move |(_, endpoint)| {
1524 let endpoint = endpoint.clone();
1525 let info = info.clone();
1526 async move {
1527 crate::util::FedimintCli
1528 .add_peer(&info, &API_AUTH, &endpoint)
1529 .await
1530 }
1531 })
1532 });
1533 try_join_all(add_peer_futures).await?;
1534
1535 debug!(target: LOG_DEVIMINT, "Starting DKG...");
1536
1537 let start_dkg_futures = endpoints.values().map(|endpoint| {
1539 let endpoint = endpoint.clone();
1540 async move {
1541 crate::util::FedimintCli
1542 .start_dkg(&API_AUTH, &endpoint)
1543 .await
1544 }
1545 });
1546 try_join_all(start_dkg_futures).await?;
1547
1548 Ok(())
1549}