Skip to main content

devimint/
tests.rs

1use std::collections::{BTreeMap, HashSet};
2use std::io::Write;
3use std::ops::ControlFlow;
4use std::path::{Path, PathBuf};
5use std::str::FromStr;
6use std::time::{Duration, Instant};
7use std::{env, ffi};
8
9use anyhow::{Context, Result, anyhow, bail};
10use bitcoin::Txid;
11use clap::Subcommand;
12use fedimint_core::core::OperationId;
13use fedimint_core::encoding::{Decodable, Encodable};
14use fedimint_core::envs::{FM_DISABLE_BASE_FEES_ENV, FM_ENABLE_MODULE_LNV2_ENV, is_env_var_set};
15use fedimint_core::module::registry::ModuleRegistry;
16use fedimint_core::net::api_announcement::SignedApiAnnouncement;
17use fedimint_core::task::block_in_place;
18use fedimint_core::util::backoff_util::aggressive_backoff;
19use fedimint_core::util::{retry, write_overwrite_async};
20use fedimint_core::{Amount, PeerId};
21use fedimint_ln_client::LightningPaymentOutcome;
22use fedimint_ln_client::cli::LnInvoiceResponse;
23use fedimint_ln_server::common::LightningGatewayAnnouncement;
24use fedimint_ln_server::common::lightning_invoice::Bolt11Invoice;
25use fedimint_lnv2_client::FinalSendOperationState;
26use fedimint_logging::LOG_DEVIMINT;
27use fedimint_testing_core::node_type::LightningNodeType;
28use futures::future::try_join_all;
29use serde_json::json;
30use substring::Substring;
31use tokio::net::TcpStream;
32use tokio::time::timeout;
33use tokio::{fs, try_join};
34use tracing::{debug, error, info};
35
36use crate::cli::{CommonArgs, cleanup_on_exit, exec_user_command, setup};
37use crate::envs::{FM_DATA_DIR_ENV, FM_DEVIMINT_RUN_DEPRECATED_TESTS_ENV, FM_PASSWORD_ENV};
38use crate::federation::Client;
39use crate::util::{LoadTestTool, ProcessManager, almost_equal, poll};
40use crate::version_constants::{
41    VERSION_0_8_2, VERSION_0_9_0_ALPHA, VERSION_0_10_0_ALPHA, VERSION_0_11_0_ALPHA,
42};
43use crate::{DevFed, Gatewayd, LightningNode, Lnd, cmd, dev_fed};
44
45pub struct Stats {
46    pub min: Duration,
47    pub avg: Duration,
48    pub median: Duration,
49    pub p90: Duration,
50    pub max: Duration,
51    pub sum: Duration,
52}
53
54impl std::fmt::Display for Stats {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        write!(f, "min: {:.1}s", self.min.as_secs_f32())?;
57        write!(f, ", avg: {:.1}s", self.avg.as_secs_f32())?;
58        write!(f, ", median: {:.1}s", self.median.as_secs_f32())?;
59        write!(f, ", p90: {:.1}s", self.p90.as_secs_f32())?;
60        write!(f, ", max: {:.1}s", self.max.as_secs_f32())?;
61        write!(f, ", sum: {:.1}s", self.sum.as_secs_f32())?;
62        Ok(())
63    }
64}
65
66pub fn stats_for(mut v: Vec<Duration>) -> Stats {
67    assert!(!v.is_empty());
68    v.sort();
69    let n = v.len();
70    let min = v.first().unwrap().to_owned();
71    let max = v.iter().last().unwrap().to_owned();
72    let median = v[n / 2];
73    let sum: Duration = v.iter().sum();
74    let avg = sum / n as u32;
75    let p90 = v[(n as f32 * 0.9) as usize];
76    Stats {
77        min,
78        avg,
79        median,
80        p90,
81        max,
82        sum,
83    }
84}
85
86pub async fn log_binary_versions() -> Result<()> {
87    let fedimint_cli_version = cmd!(crate::util::get_fedimint_cli_path(), "--version")
88        .out_string()
89        .await?;
90    info!(?fedimint_cli_version);
91    let fedimint_cli_version_hash = cmd!(crate::util::get_fedimint_cli_path(), "version-hash")
92        .out_string()
93        .await?;
94    info!(?fedimint_cli_version_hash);
95    let gateway_cli_version = cmd!(crate::util::get_gateway_cli_path(), "--version")
96        .out_string()
97        .await?;
98    info!(?gateway_cli_version);
99    let gateway_cli_version_hash = cmd!(crate::util::get_gateway_cli_path(), "version-hash")
100        .out_string()
101        .await?;
102    info!(?gateway_cli_version_hash);
103    let fedimintd_version_hash = cmd!(crate::util::FedimintdCmd, "version-hash")
104        .out_string()
105        .await?;
106    info!(?fedimintd_version_hash);
107    let gatewayd_version_hash = cmd!(crate::util::Gatewayd, "version-hash")
108        .out_string()
109        .await?;
110    info!(?gatewayd_version_hash);
111    Ok(())
112}
113
114pub async fn latency_tests(
115    dev_fed: DevFed,
116    r#type: LatencyTest,
117    upgrade_clients: Option<&UpgradeClients>,
118    iterations: usize,
119    assert_thresholds: bool,
120) -> Result<()> {
121    log_binary_versions().await?;
122
123    let DevFed {
124        fed,
125        gw_lnd,
126        gw_ldk,
127        ..
128    } = dev_fed;
129
130    let max_p90_factor = 10.0;
131    let p90_median_factor = 10;
132
133    let client = match upgrade_clients {
134        Some(c) => match r#type {
135            LatencyTest::Reissue => c.reissue_client.clone(),
136            LatencyTest::LnSend => c.ln_send_client.clone(),
137            LatencyTest::LnReceive => c.ln_receive_client.clone(),
138            LatencyTest::FmPay => c.fm_pay_client.clone(),
139            LatencyTest::Restore => bail!("no reusable upgrade client for restore"),
140        },
141        None => fed.new_joined_client("latency-tests-client").await?,
142    };
143
144    let initial_balance_sats = 100_000_000;
145    fed.pegin_client(initial_balance_sats, &client).await?;
146
147    let lnd_gw_id = gw_lnd.gateway_id.clone();
148
149    match r#type {
150        LatencyTest::Reissue => {
151            info!("Testing latency of reissue");
152            let mut reissues = Vec::with_capacity(iterations);
153            let amount_per_iteration_msats =
154                // use a highest 2^-1 amount that fits, to try to use as many notes as possible
155                ((initial_balance_sats * 1000 / iterations as u64).next_power_of_two() >> 1) - 1;
156            for _ in 0..iterations {
157                let notes = cmd!(client, "spend", amount_per_iteration_msats.to_string())
158                    .out_json()
159                    .await?["notes"]
160                    .as_str()
161                    .context("note must be a string")?
162                    .to_owned();
163
164                let start_time = Instant::now();
165                cmd!(client, "reissue", notes).run().await?;
166                reissues.push(start_time.elapsed());
167            }
168            let reissue_stats = stats_for(reissues);
169            println!("### LATENCY REISSUE: {reissue_stats}");
170
171            if assert_thresholds {
172                assert!(reissue_stats.median < Duration::from_secs(10));
173                assert!(reissue_stats.p90 < reissue_stats.median * p90_median_factor);
174                assert!(
175                    reissue_stats.max.as_secs_f64()
176                        < reissue_stats.p90.as_secs_f64() * max_p90_factor
177                );
178            }
179        }
180        LatencyTest::LnSend => {
181            info!("Testing latency of ln send");
182            let mut ln_sends = Vec::with_capacity(iterations);
183            for _ in 0..iterations {
184                let invoice = gw_ldk.create_invoice(1_000_000).await?;
185                let start_time = Instant::now();
186                ln_pay(&client, invoice.to_string(), lnd_gw_id.clone()).await?;
187                gw_ldk
188                    .wait_bolt11_invoice(invoice.payment_hash().consensus_encode_to_vec())
189                    .await?;
190                ln_sends.push(start_time.elapsed());
191
192                if crate::util::supports_lnv2() {
193                    let invoice = gw_lnd.create_invoice(1_000_000).await?;
194
195                    let start_time = Instant::now();
196
197                    lnv2_send(&client, &gw_ldk.addr, &invoice.to_string()).await?;
198
199                    ln_sends.push(start_time.elapsed());
200                }
201            }
202            let ln_sends_stats = stats_for(ln_sends);
203            println!("### LATENCY LN SEND: {ln_sends_stats}");
204
205            if assert_thresholds {
206                assert!(ln_sends_stats.median < Duration::from_secs(10));
207                assert!(ln_sends_stats.p90 < ln_sends_stats.median * p90_median_factor);
208                assert!(
209                    ln_sends_stats.max.as_secs_f64()
210                        < ln_sends_stats.p90.as_secs_f64() * max_p90_factor
211                );
212            }
213        }
214        LatencyTest::LnReceive => {
215            info!("Testing latency of ln receive");
216            let mut ln_receives = Vec::with_capacity(iterations);
217
218            // give lnd some funds
219            let invoice = gw_ldk.create_invoice(10_000_000).await?;
220            ln_pay(&client, invoice.to_string(), lnd_gw_id.clone()).await?;
221
222            for _ in 0..iterations {
223                let invoice = ln_invoice(
224                    &client,
225                    Amount::from_msats(100_000),
226                    "latency-over-lnd-gw".to_string(),
227                    lnd_gw_id.clone(),
228                )
229                .await?
230                .invoice;
231
232                let start_time = Instant::now();
233                gw_ldk
234                    .pay_invoice(
235                        Bolt11Invoice::from_str(&invoice).expect("Could not parse invoice"),
236                    )
237                    .await?;
238                ln_receives.push(start_time.elapsed());
239
240                if crate::util::supports_lnv2() {
241                    let invoice = lnv2_receive(&client, &gw_lnd.addr, 100_000).await?.0;
242
243                    let start_time = Instant::now();
244
245                    gw_ldk.pay_invoice(invoice).await?;
246
247                    ln_receives.push(start_time.elapsed());
248                }
249            }
250            let ln_receives_stats = stats_for(ln_receives);
251            println!("### LATENCY LN RECV: {ln_receives_stats}");
252
253            if assert_thresholds {
254                assert!(ln_receives_stats.median < Duration::from_secs(10));
255                assert!(ln_receives_stats.p90 < ln_receives_stats.median * p90_median_factor);
256                assert!(
257                    ln_receives_stats.max.as_secs_f64()
258                        < ln_receives_stats.p90.as_secs_f64() * max_p90_factor
259                );
260            }
261        }
262        LatencyTest::FmPay => {
263            info!("Testing latency of internal payments within a federation");
264            let mut fm_internal_pay = Vec::with_capacity(iterations);
265            let sender = fed.new_joined_client("internal-swap-sender").await?;
266            fed.pegin_client(10_000_000, &sender).await?;
267            for _ in 0..iterations {
268                let recv = cmd!(
269                    client,
270                    "ln-invoice",
271                    "--amount=1000000msat",
272                    "--description=internal-swap-invoice",
273                    "--force-internal"
274                )
275                .out_json()
276                .await?;
277
278                let invoice = recv["invoice"]
279                    .as_str()
280                    .context("invoice must be string")?
281                    .to_owned();
282                let recv_op = recv["operation_id"]
283                    .as_str()
284                    .context("operation id must be string")?
285                    .to_owned();
286
287                let start_time = Instant::now();
288                cmd!(sender, "ln-pay", invoice, "--force-internal")
289                    .run()
290                    .await?;
291
292                cmd!(client, "await-invoice", recv_op).run().await?;
293                fm_internal_pay.push(start_time.elapsed());
294            }
295            let fm_pay_stats = stats_for(fm_internal_pay);
296
297            println!("### LATENCY FM PAY: {fm_pay_stats}");
298
299            if assert_thresholds {
300                assert!(fm_pay_stats.median < Duration::from_secs(15));
301                assert!(fm_pay_stats.p90 < fm_pay_stats.median * p90_median_factor);
302                assert!(
303                    fm_pay_stats.max.as_secs_f64()
304                        < fm_pay_stats.p90.as_secs_f64() * max_p90_factor
305                );
306            }
307        }
308        LatencyTest::Restore => {
309            info!("Testing latency of restore");
310            let backup_secret = cmd!(client, "print-secret").out_json().await?["secret"]
311                .as_str()
312                .map(ToOwned::to_owned)
313                .unwrap();
314            if !is_env_var_set(FM_DEVIMINT_RUN_DEPRECATED_TESTS_ENV) {
315                info!("Skipping tests, as in previous versions restore was very slow to test");
316                return Ok(());
317            }
318
319            let start_time = Instant::now();
320            let restore_client = Client::create("restore").await?;
321            cmd!(
322                restore_client,
323                "restore",
324                "--mnemonic",
325                &backup_secret,
326                "--invite-code",
327                fed.invite_code()?
328            )
329            .run()
330            .await?;
331            let restore_time = start_time.elapsed();
332
333            println!("### LATENCY RESTORE: {restore_time:?}");
334
335            if assert_thresholds {
336                if crate::util::is_backwards_compatibility_test() {
337                    assert!(restore_time < Duration::from_secs(160));
338                } else {
339                    assert!(restore_time < Duration::from_secs(30));
340                }
341            }
342        }
343    }
344
345    Ok(())
346}
347
348#[allow(clippy::struct_field_names)]
349/// Clients reused for upgrade tests
350pub struct UpgradeClients {
351    reissue_client: Client,
352    ln_send_client: Client,
353    ln_receive_client: Client,
354    fm_pay_client: Client,
355}
356
357async fn stress_test_fed(dev_fed: &DevFed, clients: Option<&UpgradeClients>) -> anyhow::Result<()> {
358    use futures::FutureExt;
359
360    // local environments can fail due to latency thresholds, however this shouldn't
361    // cause the upgrade test to fail
362    let assert_thresholds = false;
363
364    // running only one iteration greatly improves the total test time while still
365    // testing the same types of database entries
366    let iterations = 1;
367
368    // skip restore test for client upgrades, since restoring a client doesn't
369    // require a persistent data dir
370    let restore_test = if clients.is_some() {
371        futures::future::ok(()).right_future()
372    } else {
373        latency_tests(
374            dev_fed.clone(),
375            LatencyTest::Restore,
376            clients,
377            iterations,
378            assert_thresholds,
379        )
380        .left_future()
381    };
382
383    // tests are run in sequence so parallelism is controlled using gnu `parallel`
384    // in `upgrade-test.sh`
385    latency_tests(
386        dev_fed.clone(),
387        LatencyTest::Reissue,
388        clients,
389        iterations,
390        assert_thresholds,
391    )
392    .await?;
393
394    latency_tests(
395        dev_fed.clone(),
396        LatencyTest::LnSend,
397        clients,
398        iterations,
399        assert_thresholds,
400    )
401    .await?;
402
403    latency_tests(
404        dev_fed.clone(),
405        LatencyTest::LnReceive,
406        clients,
407        iterations,
408        assert_thresholds,
409    )
410    .await?;
411
412    latency_tests(
413        dev_fed.clone(),
414        LatencyTest::FmPay,
415        clients,
416        iterations,
417        assert_thresholds,
418    )
419    .await?;
420
421    restore_test.await?;
422
423    Ok(())
424}
425
426pub async fn upgrade_tests(process_mgr: &ProcessManager, binary: UpgradeTest) -> Result<()> {
427    match binary {
428        UpgradeTest::Fedimintd { paths } => {
429            if let Some(oldest_fedimintd) = paths.first() {
430                // TODO: Audit that the environment access only happens in single-threaded code.
431                unsafe { std::env::set_var("FM_FEDIMINTD_BASE_EXECUTABLE", oldest_fedimintd) };
432            } else {
433                bail!("Must provide at least 1 binary path");
434            }
435
436            let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
437            info!(
438                "running first stress test for fedimintd version: {}",
439                fedimintd_version
440            );
441
442            let mut dev_fed = dev_fed(process_mgr).await?;
443            let client = dev_fed.fed.new_joined_client("test-client").await?;
444            try_join!(stress_test_fed(&dev_fed, None), client.wait_session())?;
445
446            for path in paths.iter().skip(1) {
447                dev_fed.fed.restart_all_with_bin(process_mgr, path).await?;
448
449                // stress test with all peers online
450                try_join!(stress_test_fed(&dev_fed, None), client.wait_session())?;
451
452                let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
453                info!(
454                    "### fedimintd passed stress test for version {}",
455                    fedimintd_version
456                );
457            }
458            info!("## fedimintd upgraded all binaries successfully");
459        }
460        UpgradeTest::FedimintCli { paths } => {
461            let set_fedimint_cli_path = |path: &PathBuf| {
462                // TODO: Audit that the environment access only happens in single-threaded code.
463                unsafe { std::env::set_var("FM_FEDIMINT_CLI_BASE_EXECUTABLE", path) };
464                let fm_mint_client: String = format!(
465                    "{fedimint_cli} --data-dir {datadir}",
466                    fedimint_cli = crate::util::get_fedimint_cli_path().join(" "),
467                    datadir = crate::vars::utf8(&process_mgr.globals.FM_CLIENT_DIR)
468                );
469                // TODO: Audit that the environment access only happens in single-threaded code.
470                unsafe { std::env::set_var("FM_MINT_CLIENT", fm_mint_client) };
471            };
472
473            if let Some(oldest_fedimint_cli) = paths.first() {
474                set_fedimint_cli_path(oldest_fedimint_cli);
475            } else {
476                bail!("Must provide at least 1 binary path");
477            }
478
479            let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
480            info!(
481                "running first stress test for fedimint-cli version: {}",
482                fedimint_cli_version
483            );
484
485            let dev_fed = dev_fed(process_mgr).await?;
486
487            let wait_session_client = dev_fed.fed.new_joined_client("wait-session-client").await?;
488            let reusable_upgrade_clients = UpgradeClients {
489                reissue_client: dev_fed.fed.new_joined_client("reissue-client").await?,
490                ln_send_client: dev_fed.fed.new_joined_client("ln-send-client").await?,
491                ln_receive_client: dev_fed.fed.new_joined_client("ln-receive-client").await?,
492                fm_pay_client: dev_fed.fed.new_joined_client("fm-pay-client").await?,
493            };
494
495            try_join!(
496                stress_test_fed(&dev_fed, Some(&reusable_upgrade_clients)),
497                wait_session_client.wait_session()
498            )?;
499
500            for path in paths.iter().skip(1) {
501                set_fedimint_cli_path(path);
502                let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
503                info!("upgraded fedimint-cli to version: {}", fedimint_cli_version);
504                try_join!(
505                    stress_test_fed(&dev_fed, Some(&reusable_upgrade_clients)),
506                    wait_session_client.wait_session()
507                )?;
508                info!(
509                    "### fedimint-cli passed stress test for version {}",
510                    fedimint_cli_version
511                );
512            }
513            info!("## fedimint-cli upgraded all binaries successfully");
514        }
515        UpgradeTest::Gatewayd {
516            gatewayd_paths,
517            gateway_cli_paths,
518        } => {
519            if let Some(oldest_gatewayd) = gatewayd_paths.first() {
520                // TODO: Audit that the environment access only happens in single-threaded code.
521                unsafe { std::env::set_var("FM_GATEWAYD_BASE_EXECUTABLE", oldest_gatewayd) };
522            } else {
523                bail!("Must provide at least 1 gatewayd path");
524            }
525
526            if let Some(oldest_gateway_cli) = gateway_cli_paths.first() {
527                // TODO: Audit that the environment access only happens in single-threaded code.
528                unsafe { std::env::set_var("FM_GATEWAY_CLI_BASE_EXECUTABLE", oldest_gateway_cli) };
529            } else {
530                bail!("Must provide at least 1 gateway-cli path");
531            }
532
533            let gatewayd_version = crate::util::Gatewayd::version_or_default().await;
534            let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
535            info!(
536                ?gatewayd_version,
537                ?gateway_cli_version,
538                "running first stress test for gateway",
539            );
540
541            let mut dev_fed = dev_fed(process_mgr).await?;
542            let client = dev_fed.fed.new_joined_client("test-client").await?;
543            try_join!(stress_test_fed(&dev_fed, None), client.wait_session())?;
544
545            for i in 1..gatewayd_paths.len() {
546                info!(
547                    "running stress test with gatewayd path {:?}",
548                    gatewayd_paths.get(i)
549                );
550                let new_gatewayd_path = gatewayd_paths.get(i).expect("Not enough gatewayd paths");
551                let new_gateway_cli_path = gateway_cli_paths
552                    .get(i)
553                    .expect("Not enough gateway-cli paths");
554
555                let gateways = vec![&mut dev_fed.gw_lnd];
556
557                try_join_all(gateways.into_iter().map(|gateway| {
558                    gateway.restart_with_bin(process_mgr, new_gatewayd_path, new_gateway_cli_path)
559                }))
560                .await?;
561
562                dev_fed.fed.await_gateways_registered().await?;
563                try_join!(stress_test_fed(&dev_fed, None), client.wait_session())?;
564                let gatewayd_version = crate::util::Gatewayd::version_or_default().await;
565                let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
566                info!(
567                    ?gatewayd_version,
568                    ?gateway_cli_version,
569                    "### gateway passed stress test for version",
570                );
571            }
572
573            info!("## gatewayd upgraded all binaries successfully");
574        }
575    }
576    Ok(())
577}
578
579pub async fn cli_tests(dev_fed: DevFed) -> Result<()> {
580    log_binary_versions().await?;
581    let data_dir = env::var(FM_DATA_DIR_ENV)?;
582
583    let DevFed {
584        bitcoind,
585        lnd,
586        fed,
587        gw_lnd,
588        gw_ldk,
589        ..
590    } = dev_fed;
591
592    let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
593
594    let client = fed.new_joined_client("cli-tests-client").await?;
595    let lnd_gw_id = gw_lnd.gateway_id.clone();
596
597    cmd!(
598        client,
599        "dev",
600        "config-decrypt",
601        "--in-file={data_dir}/fedimintd-default-0/private.encrypt",
602        "--out-file={data_dir}/fedimintd-default-0/config-plaintext.json"
603    )
604    .env(FM_PASSWORD_ENV, "pass")
605    .run()
606    .await?;
607
608    cmd!(
609        client,
610        "dev",
611        "config-encrypt",
612        "--in-file={data_dir}/fedimintd-default-0/config-plaintext.json",
613        "--out-file={data_dir}/fedimintd-default-0/config-2"
614    )
615    .env(FM_PASSWORD_ENV, "pass-foo")
616    .run()
617    .await?;
618
619    cmd!(
620        client,
621        "dev",
622        "config-decrypt",
623        "--in-file={data_dir}/fedimintd-default-0/config-2",
624        "--out-file={data_dir}/fedimintd-default-0/config-plaintext-2.json"
625    )
626    .env(FM_PASSWORD_ENV, "pass-foo")
627    .run()
628    .await?;
629
630    let plaintext_one = fs::read_to_string(format!(
631        "{data_dir}/fedimintd-default-0/config-plaintext.json"
632    ))
633    .await?;
634    let plaintext_two = fs::read_to_string(format!(
635        "{data_dir}/fedimintd-default-0/config-plaintext-2.json"
636    ))
637    .await?;
638    anyhow::ensure!(
639        plaintext_one == plaintext_two,
640        "config-decrypt/encrypt failed"
641    );
642
643    fed.pegin_gateways(10_000_000, vec![&gw_lnd]).await?;
644
645    let fed_id = fed.calculate_federation_id();
646    let invite = fed.invite_code()?;
647
648    let invite_code = cmd!(client, "dev", "decode", "invite-code", invite.clone())
649        .out_json()
650        .await?;
651
652    let encode_invite_output = cmd!(
653        client,
654        "dev",
655        "encode",
656        "invite-code",
657        format!("--url={}", invite_code["url"].as_str().unwrap()),
658        "--federation_id={fed_id}",
659        "--peer=0"
660    )
661    .out_json()
662    .await?;
663
664    anyhow::ensure!(
665        encode_invite_output["invite_code"]
666            .as_str()
667            .expect("invite_code must be a string")
668            == invite,
669        "failed to decode and encode the client invite code",
670    );
671
672    // Test that LND and LDK can still send directly to each other
673
674    // LND can pay LDK directly
675    info!("Testing LND can pay LDK directly");
676    let invoice = gw_ldk.create_invoice(1_200_000).await?;
677    lnd.pay_bolt11_invoice(invoice.to_string()).await?;
678    gw_ldk
679        .wait_bolt11_invoice(invoice.payment_hash().consensus_encode_to_vec())
680        .await?;
681
682    // LDK can pay LND directly
683    info!("Testing LDK can pay LND directly");
684    let (invoice, payment_hash) = lnd.invoice(1_000_000).await?;
685    gw_ldk
686        .pay_invoice(Bolt11Invoice::from_str(&invoice).expect("Could not parse invoice"))
687        .await?;
688    gw_lnd.wait_bolt11_invoice(payment_hash).await?;
689
690    // # Test the correct descriptor is used
691    let config = cmd!(client, "config").out_json().await?;
692    let guardian_count = config["global"]["api_endpoints"].as_object().unwrap().len();
693    let wallet_module = config["modules"]
694        .as_object()
695        .unwrap()
696        .values()
697        .find(|m| m["kind"].as_str() == Some("wallet"))
698        .expect("wallet module not found");
699    let descriptor = wallet_module["peg_in_descriptor"]
700        .as_str()
701        .unwrap()
702        .to_owned();
703
704    info!("Testing generated descriptor for {guardian_count} guardian federation");
705    if guardian_count == 1 {
706        assert!(descriptor.contains("wpkh("));
707    } else {
708        assert!(descriptor.contains("wsh(sortedmulti("));
709    }
710
711    // # Client tests
712    info!("Testing Client");
713
714    // ## reissue e-cash
715    info!("Testing reissuing e-cash");
716    const CLIENT_START_AMOUNT: u64 = 5_000_000_000;
717    const CLIENT_SPEND_AMOUNT: u64 = 1_100_000;
718
719    let initial_client_balance = client.balance().await?;
720    assert_eq!(initial_client_balance, 0);
721
722    fed.pegin_client(CLIENT_START_AMOUNT / 1000, &client)
723        .await?;
724
725    // # Spend from client
726    info!("Testing spending from client");
727    let notes = cmd!(client, "spend", CLIENT_SPEND_AMOUNT)
728        .out_json()
729        .await?
730        .get("notes")
731        .expect("Output didn't contain e-cash notes")
732        .as_str()
733        .unwrap()
734        .to_owned();
735
736    let client_post_spend_balance = client.balance().await?;
737    almost_equal(
738        client_post_spend_balance,
739        CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT,
740        10_000,
741    )
742    .unwrap();
743
744    // Test we can reissue our own notes
745    cmd!(client, "reissue", notes).out_json().await?;
746
747    let client_post_spend_balance = client.balance().await?;
748    almost_equal(client_post_spend_balance, CLIENT_START_AMOUNT, 10_000).unwrap();
749
750    let reissue_amount: u64 = 409_600;
751
752    // Ensure that client can reissue after spending
753    info!("Testing reissuing e-cash after spending");
754    let _notes = cmd!(client, "spend", CLIENT_SPEND_AMOUNT)
755        .out_json()
756        .await?
757        .as_object()
758        .unwrap()
759        .get("notes")
760        .expect("Output didn't contain e-cash notes")
761        .as_str()
762        .unwrap();
763
764    let reissue_notes = cmd!(client, "spend", reissue_amount).out_json().await?["notes"]
765        .as_str()
766        .map(ToOwned::to_owned)
767        .unwrap();
768    let client_reissue_amt = cmd!(client, "reissue", reissue_notes)
769        .out_json()
770        .await?
771        .as_u64()
772        .unwrap();
773    assert_eq!(client_reissue_amt, reissue_amount);
774
775    // Ensure that client can reissue via module commands
776    info!("Testing reissuing e-cash via module commands");
777    let reissue_notes = cmd!(client, "spend", reissue_amount).out_json().await?["notes"]
778        .as_str()
779        .map(ToOwned::to_owned)
780        .unwrap();
781    let client_reissue_amt = cmd!(client, "module", "mint", "reissue", reissue_notes)
782        .out_json()
783        .await?
784        .as_u64()
785        .unwrap();
786    assert_eq!(client_reissue_amt, reissue_amount);
787
788    // LND gateway tests
789    info!("Testing LND gateway");
790
791    let gatewayd_version = crate::util::Gatewayd::version_or_default().await;
792    // Gatewayd did not support default fees before v0.8.2
793    // In order for the amount tests to pass, we need to reliably set the fees to
794    // 0,0.
795    if gatewayd_version < *VERSION_0_8_2 {
796        gw_lnd
797            .set_federation_routing_fee(fed_id.clone(), 0, 0)
798            .await?;
799
800        // Poll until the client has heard about the updated fees
801        poll("Waiting for LND GW fees to update", || async {
802            let gateways_val = cmd!(client, "list-gateways")
803                .out_json()
804                .await
805                .map_err(ControlFlow::Break)?;
806            let gateways =
807                serde_json::from_value::<Vec<LightningGatewayAnnouncement>>(gateways_val)
808                    .expect("Could not deserialize");
809            let fees = gateways
810                .first()
811                .expect("No gateway was registered")
812                .info
813                .fees;
814            if fees.base_msat == 0 && fees.proportional_millionths == 0 {
815                Ok(())
816            } else {
817                Err(ControlFlow::Continue(anyhow!("Fees have not been updated")))
818            }
819        })
820        .await?;
821    }
822
823    // OUTGOING: fedimint-cli pays LDK via LND gateway
824    if let Some(iroh_gw_id) = &gw_lnd.iroh_gateway_id
825        && crate::util::FedimintCli::version_or_default().await >= *VERSION_0_10_0_ALPHA
826    {
827        info!("Testing outgoing payment from client to LDK via IROH LND Gateway");
828
829        let initial_lnd_gateway_balance = gw_lnd.ecash_balance(fed_id.clone()).await?;
830        let invoice = gw_ldk.create_invoice(2_000_000).await?;
831        ln_pay(&client, invoice.to_string(), iroh_gw_id.clone()).await?;
832        gw_ldk
833            .wait_bolt11_invoice(invoice.payment_hash().consensus_encode_to_vec())
834            .await?;
835
836        // Assert balances changed by 2_000_000 msat (amount sent) + 0 msat (fee)
837        let final_lnd_outgoing_gateway_balance = gw_lnd.ecash_balance(fed_id.clone()).await?;
838        info!(
839            ?final_lnd_outgoing_gateway_balance,
840            "Final LND ecash balance after iroh payment"
841        );
842        anyhow::ensure!(
843            almost_equal(
844                final_lnd_outgoing_gateway_balance - initial_lnd_gateway_balance,
845                2_000_000,
846                1_000
847            )
848            .is_ok(),
849            "LND Gateway balance changed by {} on LND outgoing IROH payment, expected 2_000_000",
850            (final_lnd_outgoing_gateway_balance - initial_lnd_gateway_balance)
851        );
852
853        // Send the funds back over iroh
854        let recv = ln_invoice(
855            &client,
856            Amount::from_msats(2_000_000),
857            "iroh receive payment".to_string(),
858            iroh_gw_id.clone(),
859        )
860        .await?;
861        gw_ldk
862            .pay_invoice(Bolt11Invoice::from_str(&recv.invoice).expect("Could not parse invoice"))
863            .await?;
864        let operation_id = recv.operation_id;
865        cmd!(client, "await-invoice", operation_id.fmt_full())
866            .run()
867            .await?;
868    }
869
870    info!("Testing outgoing payment from client to LDK via LND gateway");
871    let initial_lnd_gateway_balance = gw_lnd.ecash_balance(fed_id.clone()).await?;
872    let invoice = gw_ldk.create_invoice(2_000_000).await?;
873    ln_pay(&client, invoice.to_string(), lnd_gw_id.clone()).await?;
874    let fed_id = fed.calculate_federation_id();
875    gw_ldk
876        .wait_bolt11_invoice(invoice.payment_hash().consensus_encode_to_vec())
877        .await?;
878
879    // Assert balances changed by 2_000_000 msat (amount sent) + 0 msat (fee)
880    let final_lnd_outgoing_gateway_balance = gw_lnd.ecash_balance(fed_id.clone()).await?;
881    anyhow::ensure!(
882        almost_equal(
883            final_lnd_outgoing_gateway_balance - initial_lnd_gateway_balance,
884            2_000_000,
885            3_000
886        )
887        .is_ok(),
888        "LND Gateway balance changed by {} on LND outgoing payment, expected 2_000_000",
889        (final_lnd_outgoing_gateway_balance - initial_lnd_gateway_balance)
890    );
891
892    // INCOMING: fedimint-cli receives from LDK via LND gateway
893    info!("Testing incoming payment from LDK to client via LND gateway");
894    let initial_lnd_incoming_client_balance = client.balance().await?;
895    let recv = ln_invoice(
896        &client,
897        Amount::from_msats(1_300_000),
898        "incoming-over-lnd-gw".to_string(),
899        lnd_gw_id,
900    )
901    .await?;
902    let invoice = recv.invoice;
903    gw_ldk
904        .pay_invoice(Bolt11Invoice::from_str(&invoice).expect("Could not parse invoice"))
905        .await?;
906
907    // Receive the ecash notes
908    info!("Testing receiving ecash notes");
909    let operation_id = recv.operation_id;
910    cmd!(client, "await-invoice", operation_id.fmt_full())
911        .run()
912        .await?;
913
914    // Only validate balance changes on v0.11 and above because there is a race
915    // condition in older versions that causes the balances to not match.
916    let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
917    if fedimint_cli_version >= *VERSION_0_11_0_ALPHA {
918        // Assert balances changed by 1_300_000 msat
919        let final_lnd_incoming_client_balance = client.balance().await?;
920        let final_lnd_incoming_gateway_balance = gw_lnd.ecash_balance(fed_id.clone()).await?;
921        anyhow::ensure!(
922            almost_equal(
923                final_lnd_incoming_client_balance - initial_lnd_incoming_client_balance,
924                1_300_000,
925                2_000
926            )
927            .is_ok(),
928            "Client balance changed by {} on LND incoming payment, expected 1_300_000",
929            (final_lnd_incoming_client_balance - initial_lnd_incoming_client_balance)
930        );
931        anyhow::ensure!(
932            almost_equal(
933                final_lnd_outgoing_gateway_balance - final_lnd_incoming_gateway_balance,
934                1_300_000,
935                2_000
936            )
937            .is_ok(),
938            "LND Gateway balance changed by {} on LND incoming payment, expected 1_300_000",
939            (final_lnd_outgoing_gateway_balance - final_lnd_incoming_gateway_balance)
940        );
941    }
942
943    // # Wallet tests
944    // ## Deposit
945    info!("Testing client deposit");
946    let initial_walletng_balance = client.balance().await?;
947
948    fed.pegin_client(100_000, &client).await?; // deposit in sats
949
950    let post_deposit_walletng_balance = client.balance().await?;
951
952    almost_equal(
953        post_deposit_walletng_balance,
954        initial_walletng_balance + 100_000_000, // deposit in msats
955        2_000,
956    )
957    .unwrap();
958
959    // ## Withdraw
960    info!("Testing client withdraw");
961
962    let initial_walletng_balance = client.balance().await?;
963
964    let address = bitcoind.get_new_address().await?;
965    let withdraw_res = cmd!(
966        client,
967        "withdraw",
968        "--address",
969        &address,
970        "--amount",
971        "50000 sat"
972    )
973    .out_json()
974    .await?;
975
976    let txid: Txid = withdraw_res["txid"].as_str().unwrap().parse().unwrap();
977    let fees_sat = withdraw_res["fees_sat"].as_u64().unwrap();
978
979    let tx_hex = bitcoind.poll_get_transaction(txid).await?;
980
981    let tx = bitcoin::Transaction::consensus_decode_hex(&tx_hex, &ModuleRegistry::default())?;
982    assert!(
983        tx.output
984            .iter()
985            .any(|o| o.script_pubkey == address.script_pubkey() && o.value.to_sat() == 50000)
986    );
987
988    let post_withdraw_walletng_balance = client.balance().await?;
989    let expected_wallet_balance = initial_walletng_balance - 50_000_000 - (fees_sat * 1000);
990
991    almost_equal(
992        post_withdraw_walletng_balance,
993        expected_wallet_balance,
994        4_000,
995    )
996    .unwrap();
997
998    // # peer-version command
999    let peer_0_fedimintd_version = cmd!(client, "dev", "peer-version", "--peer-id", "0")
1000        .out_json()
1001        .await?
1002        .get("version")
1003        .expect("Output didn't contain version")
1004        .as_str()
1005        .unwrap()
1006        .to_owned();
1007
1008    assert_eq!(
1009        semver::Version::parse(&peer_0_fedimintd_version)?,
1010        fedimintd_version
1011    );
1012
1013    info!("Checking initial announcements...");
1014
1015    retry(
1016        "Check initial announcements",
1017        aggressive_backoff(),
1018        || async {
1019            // Give the client some time to fetch updates
1020            cmd!(client, "dev", "wait", "1").run().await?;
1021
1022            // # API URL announcements
1023            let initial_announcements =
1024                serde_json::from_value::<BTreeMap<PeerId, SignedApiAnnouncement>>(
1025                    cmd!(client, "dev", "api-announcements",).out_json().await?,
1026                )
1027                .expect("failed to parse API announcements");
1028
1029            if initial_announcements.len() < fed.members.len() {
1030                bail!(
1031                    "Not all announcements ready; got: {}, expected: {}",
1032                    initial_announcements.len(),
1033                    fed.members.len()
1034                )
1035            }
1036
1037            if !initial_announcements
1038                .values()
1039                .all(|announcement| announcement.api_announcement.nonce == 0)
1040            {
1041                bail!("Not all announcements have their initial value");
1042            }
1043            Ok(())
1044        },
1045    )
1046    .await?;
1047
1048    const NEW_API_URL: &str = "ws://127.0.0.1:4242";
1049    let new_announcement = serde_json::from_value::<SignedApiAnnouncement>(
1050        cmd!(
1051            client,
1052            "--our-id",
1053            "0",
1054            "--password",
1055            "pass",
1056            "admin",
1057            "sign-api-announcement",
1058            NEW_API_URL
1059        )
1060        .out_json()
1061        .await?,
1062    )
1063    .expect("Couldn't parse signed announcement");
1064
1065    assert_eq!(
1066        new_announcement.api_announcement.nonce, 1,
1067        "Nonce did not increment correctly"
1068    );
1069
1070    info!("Testing if the client syncs the announcement");
1071    let announcement = poll("Waiting for the announcement to propagate", || async {
1072        cmd!(client, "dev", "wait", "1")
1073            .run()
1074            .await
1075            .map_err(ControlFlow::Break)?;
1076
1077        let new_announcements_peer2 =
1078            serde_json::from_value::<BTreeMap<PeerId, SignedApiAnnouncement>>(
1079                cmd!(client, "dev", "api-announcements",)
1080                    .out_json()
1081                    .await
1082                    .map_err(ControlFlow::Break)?,
1083            )
1084            .expect("failed to parse API announcements");
1085
1086        let announcement = new_announcements_peer2[&PeerId::from(0)]
1087            .api_announcement
1088            .clone();
1089        if announcement.nonce == 1 {
1090            Ok(announcement)
1091        } else {
1092            Err(ControlFlow::Continue(anyhow!(
1093                "Haven't received updated announcement yet; nonce: {}",
1094                announcement.nonce
1095            )))
1096        }
1097    })
1098    .await?;
1099
1100    assert_eq!(
1101        announcement.api_url,
1102        NEW_API_URL.parse().expect("valid URL")
1103    );
1104
1105    Ok(())
1106}
1107
1108pub async fn guardian_metadata_tests(dev_fed: DevFed) -> Result<()> {
1109    use fedimint_core::PeerId;
1110    use fedimint_core::net::guardian_metadata::SignedGuardianMetadata;
1111
1112    log_binary_versions().await?;
1113
1114    let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
1115    let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
1116
1117    if fedimintd_version < *VERSION_0_11_0_ALPHA || fedimint_cli_version < *VERSION_0_11_0_ALPHA {
1118        info!("Skipping test for too old versions");
1119        return Ok(());
1120    }
1121
1122    let DevFed { fed, .. } = dev_fed;
1123
1124    let client = fed.internal_client().await?;
1125
1126    info!("Checking initial guardian metadata...");
1127
1128    retry(
1129        "Check initial guardian metadata",
1130        aggressive_backoff(),
1131        || async {
1132            // Give the client some time to fetch updates
1133            cmd!(client, "dev", "wait", "1").run().await?;
1134
1135            let initial_metadata =
1136                serde_json::from_value::<BTreeMap<PeerId, SignedGuardianMetadata>>(
1137                    cmd!(client, "dev", "guardian-metadata",).out_json().await?,
1138                )
1139                .expect("failed to parse guardian metadata");
1140
1141            if initial_metadata.len() < fed.members.len() {
1142                bail!(
1143                    "Not all guardian metadata ready; got: {}, expected: {}",
1144                    initial_metadata.len(),
1145                    fed.members.len()
1146                )
1147            }
1148
1149            Ok(())
1150        },
1151    )
1152    .await?;
1153
1154    const TEST_API_URL: &str = "ws://127.0.0.1:5000/";
1155    const TEST_PKARR_ID: &str = "test_pkarr_id_z32";
1156
1157    let new_metadata = serde_json::from_value::<SignedGuardianMetadata>(
1158        cmd!(
1159            client,
1160            "--our-id",
1161            "0",
1162            "--password",
1163            "pass",
1164            "admin",
1165            "sign-guardian-metadata",
1166            "--api-urls",
1167            TEST_API_URL,
1168            "--pkarr-id",
1169            TEST_PKARR_ID
1170        )
1171        .out_json()
1172        .await?,
1173    )
1174    .expect("Couldn't parse signed guardian metadata");
1175
1176    let parsed_metadata = new_metadata.guardian_metadata();
1177
1178    assert_eq!(
1179        parsed_metadata.api_urls.first().unwrap().to_string(),
1180        TEST_API_URL,
1181        "API URL did not match"
1182    );
1183
1184    assert_eq!(
1185        parsed_metadata.pkarr_id_z32, TEST_PKARR_ID,
1186        "Pkarr ID did not match"
1187    );
1188
1189    info!("Testing if the client syncs the guardian metadata");
1190    let metadata = poll("Waiting for the guardian metadata to propagate", || async {
1191        cmd!(client, "dev", "wait", "1")
1192            .run()
1193            .await
1194            .map_err(ControlFlow::Break)?;
1195
1196        let new_metadata_peer0 =
1197            serde_json::from_value::<BTreeMap<PeerId, SignedGuardianMetadata>>(
1198                cmd!(client, "dev", "guardian-metadata",)
1199                    .out_json()
1200                    .await
1201                    .map_err(ControlFlow::Break)?,
1202            )
1203            .expect("failed to parse guardian metadata");
1204
1205        let metadata = new_metadata_peer0[&PeerId::from(0)].guardian_metadata();
1206
1207        if metadata.api_urls.first().unwrap().to_string() == TEST_API_URL {
1208            Ok(metadata.clone())
1209        } else {
1210            Err(ControlFlow::Continue(anyhow!(
1211                "Haven't received updated guardian metadata yet"
1212            )))
1213        }
1214    })
1215    .await?;
1216
1217    assert_eq!(
1218        metadata.pkarr_id_z32, TEST_PKARR_ID,
1219        "Pkarr ID did not propagate correctly"
1220    );
1221
1222    Ok(())
1223}
1224
1225pub async fn cli_load_test_tool_test(dev_fed: DevFed) -> Result<()> {
1226    log_binary_versions().await?;
1227    let data_dir = env::var(FM_DATA_DIR_ENV)?;
1228    let load_test_temp = PathBuf::from(data_dir).join("load-test-temp");
1229    dev_fed
1230        .fed
1231        .pegin_client(10_000, dev_fed.fed.internal_client().await?)
1232        .await?;
1233    let invite_code = dev_fed.fed.invite_code()?;
1234    dev_fed
1235        .gw_lnd
1236        .set_federation_routing_fee(dev_fed.fed.calculate_federation_id(), 0, 0)
1237        .await?;
1238    run_standard_load_test(&load_test_temp, &invite_code).await?;
1239    run_ln_circular_load_test(&load_test_temp, &invite_code).await?;
1240    Ok(())
1241}
1242
1243pub async fn run_standard_load_test(
1244    load_test_temp: &Path,
1245    invite_code: &str,
1246) -> anyhow::Result<()> {
1247    let output = cmd!(
1248        LoadTestTool,
1249        "--archive-dir",
1250        load_test_temp.display(),
1251        "--users",
1252        "1",
1253        "load-test",
1254        "--notes-per-user",
1255        "1",
1256        "--generate-invoice-with",
1257        "ldk-lightning-cli",
1258        "--invite-code",
1259        invite_code
1260    )
1261    .out_string()
1262    .await?;
1263    println!("{output}");
1264    anyhow::ensure!(
1265        output.contains("2 reissue_notes"),
1266        "reissued different number notes than expected"
1267    );
1268    anyhow::ensure!(
1269        output.contains("1 gateway_pay_invoice"),
1270        "paid different number of invoices than expected"
1271    );
1272    Ok(())
1273}
1274
1275pub async fn run_ln_circular_load_test(
1276    load_test_temp: &Path,
1277    invite_code: &str,
1278) -> anyhow::Result<()> {
1279    info!("Testing ln-circular-load-test with 'two-gateways' strategy");
1280    let output = cmd!(
1281        LoadTestTool,
1282        "--archive-dir",
1283        load_test_temp.display(),
1284        "--users",
1285        "1",
1286        "ln-circular-load-test",
1287        "--strategy",
1288        "two-gateways",
1289        "--test-duration-secs",
1290        "2",
1291        "--invite-code",
1292        invite_code
1293    )
1294    .out_string()
1295    .await?;
1296    println!("{output}");
1297    anyhow::ensure!(
1298        output.contains("gateway_create_invoice"),
1299        "missing invoice creation"
1300    );
1301    anyhow::ensure!(
1302        output.contains("gateway_pay_invoice_success"),
1303        "missing invoice payment"
1304    );
1305    anyhow::ensure!(
1306        output.contains("gateway_payment_received_success"),
1307        "missing received payment"
1308    );
1309
1310    info!("Testing ln-circular-load-test with 'partner-ping-pong' strategy");
1311    // Note: invite code isn't required because we already have an archive dir
1312    // Note: test-duration-secs needs to be greater than the timeout for
1313    // discover_api_version_set to work with degraded federations
1314    let output = cmd!(
1315        LoadTestTool,
1316        "--archive-dir",
1317        load_test_temp.display(),
1318        "--users",
1319        "1",
1320        "ln-circular-load-test",
1321        "--strategy",
1322        "partner-ping-pong",
1323        "--test-duration-secs",
1324        "6",
1325        "--invite-code",
1326        invite_code
1327    )
1328    .out_string()
1329    .await?;
1330    println!("{output}");
1331    anyhow::ensure!(
1332        output.contains("gateway_create_invoice"),
1333        "missing invoice creation"
1334    );
1335    anyhow::ensure!(
1336        output.contains("gateway_payment_received_success"),
1337        "missing received payment"
1338    );
1339
1340    info!("Testing ln-circular-load-test with 'self-payment' strategy");
1341    // Note invite code isn't required because we already have an archive dir
1342    let output = cmd!(
1343        LoadTestTool,
1344        "--archive-dir",
1345        load_test_temp.display(),
1346        "--users",
1347        "1",
1348        "ln-circular-load-test",
1349        "--strategy",
1350        "self-payment",
1351        "--test-duration-secs",
1352        "2",
1353        "--invite-code",
1354        invite_code
1355    )
1356    .out_string()
1357    .await?;
1358    println!("{output}");
1359    anyhow::ensure!(
1360        output.contains("gateway_create_invoice"),
1361        "missing invoice creation"
1362    );
1363    anyhow::ensure!(
1364        output.contains("gateway_payment_received_success"),
1365        "missing received payment"
1366    );
1367    Ok(())
1368}
1369
1370pub async fn lightning_gw_reconnect_test(
1371    dev_fed: DevFed,
1372    process_mgr: &ProcessManager,
1373) -> Result<()> {
1374    log_binary_versions().await?;
1375
1376    let DevFed {
1377        bitcoind,
1378        lnd,
1379        fed,
1380        mut gw_lnd,
1381        gw_ldk,
1382        ..
1383    } = dev_fed;
1384
1385    let client = fed
1386        .new_joined_client("lightning-gw-reconnect-test-client")
1387        .await?;
1388
1389    info!("Pegging-in both gateways");
1390    fed.pegin_gateways(99_999, vec![&gw_lnd]).await?;
1391
1392    // Drop other references to LND so that the test can kill it
1393    drop(lnd);
1394
1395    tracing::info!("Stopping LND");
1396    // Verify that the gateway can query the lightning node for the pubkey and alias
1397    let mut info_cmd = cmd!(gw_lnd, "info");
1398    assert!(info_cmd.run().await.is_ok());
1399
1400    // Verify that after stopping the lightning node, info no longer returns the
1401    // node public key since the lightning node is unreachable.
1402    let ln_type = gw_lnd.ln.ln_type().to_string();
1403    gw_lnd.stop_lightning_node().await?;
1404    let lightning_info = info_cmd.out_json().await?;
1405    if gw_lnd.gatewayd_version < *VERSION_0_10_0_ALPHA {
1406        let lightning_pub_key: Option<String> =
1407            serde_json::from_value(lightning_info["lightning_pub_key"].clone())?;
1408
1409        assert!(lightning_pub_key.is_none());
1410    } else {
1411        let not_connected = lightning_info["lightning_info"].clone();
1412        assert!(not_connected.as_str().expect("ln info is not a string") == "not_connected");
1413    }
1414
1415    // Restart LND
1416    tracing::info!("Restarting LND...");
1417    let new_lnd = Lnd::new(process_mgr, bitcoind.clone()).await?;
1418    gw_lnd.set_lightning_node(LightningNode::Lnd(new_lnd.clone()));
1419
1420    tracing::info!("Retrying info...");
1421    const MAX_RETRIES: usize = 30;
1422    const RETRY_INTERVAL: Duration = Duration::from_secs(1);
1423
1424    for i in 0..MAX_RETRIES {
1425        match do_try_create_and_pay_invoice(&gw_lnd, &client, &gw_ldk).await {
1426            Ok(()) => break,
1427            Err(e) => {
1428                if i == MAX_RETRIES - 1 {
1429                    return Err(e);
1430                }
1431                tracing::debug!(
1432                    "Pay invoice for gateway {} failed with {e:?}, retrying in {} seconds (try {}/{MAX_RETRIES})",
1433                    ln_type,
1434                    RETRY_INTERVAL.as_secs(),
1435                    i + 1,
1436                );
1437                fedimint_core::task::sleep_in_test(
1438                    "paying invoice for gateway failed",
1439                    RETRY_INTERVAL,
1440                )
1441                .await;
1442            }
1443        }
1444    }
1445
1446    info!(target: LOG_DEVIMINT, "lightning_reconnect_test: success");
1447    Ok(())
1448}
1449
1450pub async fn gw_reboot_test(dev_fed: DevFed, process_mgr: &ProcessManager) -> Result<()> {
1451    log_binary_versions().await?;
1452
1453    let DevFed {
1454        bitcoind,
1455        lnd,
1456        fed,
1457        gw_lnd,
1458        gw_ldk,
1459        gw_ldk_second,
1460        ..
1461    } = dev_fed;
1462
1463    let client = fed.new_joined_client("gw-reboot-test-client").await?;
1464    fed.pegin_client(10_000, &client).await?;
1465
1466    // Wait for gateways to sync to chain
1467    let block_height = bitcoind.get_block_count().await? - 1;
1468    try_join!(
1469        gw_lnd.wait_for_block_height(block_height),
1470        gw_ldk.wait_for_block_height(block_height),
1471    )?;
1472
1473    // Drop references to gateways so the test can kill them
1474    let lnd_gateway_id = gw_lnd.gateway_id.clone();
1475    let ldk_gateway_id = gw_ldk.gateway_id.clone();
1476    let gw_ldk_name = gw_ldk.gw_name.clone();
1477    let gw_ldk_port = gw_ldk.gw_port;
1478    let gw_lightning_port = gw_ldk.ldk_port;
1479    let gw_ldk_metrics_port = gw_ldk.metrics_port;
1480    drop(gw_lnd);
1481    drop(gw_ldk);
1482
1483    // Verify that making a payment while the gateways are down does not result in
1484    // funds being stuck
1485    info!("Making payment while gateway is down");
1486    let initial_client_balance = client.balance().await?;
1487    let invoice = gw_ldk_second.create_invoice(3000).await?;
1488    ln_pay(&client, invoice.to_string(), lnd_gateway_id.clone())
1489        .await
1490        .expect_err("Expected ln-pay to return error because the gateway is not online");
1491    let new_client_balance = client.balance().await?;
1492    anyhow::ensure!(initial_client_balance == new_client_balance);
1493
1494    // Reboot gateways with the same Lightning node instances
1495    info!("Rebooting gateways...");
1496    let (new_gw_lnd, new_gw_ldk) = try_join!(
1497        Gatewayd::new(process_mgr, LightningNode::Lnd(lnd.clone()), 0),
1498        Gatewayd::new(
1499            process_mgr,
1500            LightningNode::Ldk {
1501                name: gw_ldk_name,
1502                gw_port: gw_ldk_port,
1503                ldk_port: gw_lightning_port,
1504                metrics_port: gw_ldk_metrics_port,
1505            },
1506            1,
1507        )
1508    )?;
1509
1510    let lnd_gateway_id = fedimint_core::secp256k1::PublicKey::from_str(&lnd_gateway_id)?;
1511
1512    poll(
1513        "Waiting for LND Gateway Running state after reboot",
1514        || async {
1515            let mut new_lnd_cmd = cmd!(new_gw_lnd, "info");
1516            let lnd_value = new_lnd_cmd.out_json().await.map_err(ControlFlow::Continue)?;
1517            let reboot_gateway_state: String = serde_json::from_value(lnd_value["gateway_state"].clone()).context("invalid gateway state").map_err(ControlFlow::Break)?;
1518            let reboot_gateway_id = fedimint_core::secp256k1::PublicKey::from_str(&new_gw_lnd.gateway_id).expect("Could not convert public key");
1519
1520            if reboot_gateway_state == "Running" {
1521                info!(target: LOG_DEVIMINT, "LND Gateway restarted, with auto-rejoin to federation");
1522                // Assert that the gateway info is the same as before the reboot
1523                assert_eq!(lnd_gateway_id, reboot_gateway_id);
1524                return Ok(());
1525            }
1526            Err(ControlFlow::Continue(anyhow!("gateway not running")))
1527        },
1528    )
1529    .await?;
1530
1531    let ldk_gateway_id = fedimint_core::secp256k1::PublicKey::from_str(&ldk_gateway_id)?;
1532    poll(
1533        "Waiting for LDK Gateway Running state after reboot",
1534        || async {
1535            let mut new_ldk_cmd = cmd!(new_gw_ldk, "info");
1536            let ldk_value = new_ldk_cmd.out_json().await.map_err(ControlFlow::Continue)?;
1537            let reboot_gateway_state: String = serde_json::from_value(ldk_value["gateway_state"].clone()).context("invalid gateway state").map_err(ControlFlow::Break)?;
1538            let reboot_gateway_id = fedimint_core::secp256k1::PublicKey::from_str(&new_gw_ldk.gateway_id).expect("Could not convert public key");
1539
1540            if reboot_gateway_state == "Running" {
1541                info!(target: LOG_DEVIMINT, "LDK Gateway restarted, with auto-rejoin to federation");
1542                // Assert that the gateway info is the same as before the reboot
1543                assert_eq!(ldk_gateway_id, reboot_gateway_id);
1544                return Ok(());
1545            }
1546            Err(ControlFlow::Continue(anyhow!("gateway not running")))
1547        },
1548    )
1549    .await?;
1550
1551    info!(LOG_DEVIMINT, "gateway_reboot_test: success");
1552    Ok(())
1553}
1554
1555pub async fn do_try_create_and_pay_invoice(
1556    gw_lnd: &Gatewayd,
1557    client: &Client,
1558    gw_ldk: &Gatewayd,
1559) -> anyhow::Result<()> {
1560    // Verify that after the lightning node has restarted, the gateway
1561    // automatically reconnects and can query the lightning node
1562    // info again.
1563    poll("Waiting for info to succeed after restart", || async {
1564        gw_lnd
1565            .lightning_pubkey()
1566            .await
1567            .map_err(ControlFlow::Continue)?;
1568        Ok(())
1569    })
1570    .await?;
1571
1572    tracing::info!("Creating invoice....");
1573    let invoice = ln_invoice(
1574        client,
1575        Amount::from_msats(1000),
1576        "incoming-over-lnd-gw".to_string(),
1577        gw_lnd.gateway_id.clone(),
1578    )
1579    .await?
1580    .invoice;
1581
1582    match &gw_lnd.ln.ln_type() {
1583        LightningNodeType::Lnd => {
1584            // Pay the invoice using LDK
1585            gw_ldk
1586                .pay_invoice(Bolt11Invoice::from_str(&invoice).expect("Could not parse invoice"))
1587                .await?;
1588        }
1589        LightningNodeType::Ldk => {
1590            unimplemented!("do_try_create_and_pay_invoice not implemented for LDK yet");
1591        }
1592    }
1593    Ok(())
1594}
1595
1596async fn ln_pay(client: &Client, invoice: String, gw_id: String) -> anyhow::Result<String> {
1597    let value = cmd!(client, "ln-pay", invoice, "--gateway-id", gw_id,)
1598        .out_json()
1599        .await?;
1600    let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
1601    if fedimint_cli_version >= *VERSION_0_9_0_ALPHA {
1602        let outcome = serde_json::from_value::<LightningPaymentOutcome>(value)
1603            .expect("Could not deserialize Lightning payment outcome");
1604        match outcome {
1605            LightningPaymentOutcome::Success { preimage } => Ok(preimage),
1606            LightningPaymentOutcome::Failure { error_message } => {
1607                Err(anyhow!("Failed to pay lightning invoice: {error_message}"))
1608            }
1609        }
1610    } else {
1611        let operation_id = value["operation_id"]
1612            .as_str()
1613            .ok_or(anyhow!("Failed to pay invoice"))?
1614            .to_string();
1615        Ok(operation_id)
1616    }
1617}
1618
1619async fn ln_invoice(
1620    client: &Client,
1621    amount: Amount,
1622    description: String,
1623    gw_id: String,
1624) -> anyhow::Result<LnInvoiceResponse> {
1625    let ln_response_val = cmd!(
1626        client,
1627        "ln-invoice",
1628        "--amount",
1629        amount.msats,
1630        format!("--description='{description}'"),
1631        "--gateway-id",
1632        gw_id,
1633    )
1634    .out_json()
1635    .await?;
1636
1637    let ln_invoice_response: LnInvoiceResponse = serde_json::from_value(ln_response_val)?;
1638
1639    Ok(ln_invoice_response)
1640}
1641
1642async fn lnv2_receive(
1643    client: &Client,
1644    gateway: &str,
1645    amount: u64,
1646) -> anyhow::Result<(Bolt11Invoice, OperationId)> {
1647    Ok(serde_json::from_value::<(Bolt11Invoice, OperationId)>(
1648        cmd!(
1649            client,
1650            "module",
1651            "lnv2",
1652            "receive",
1653            amount,
1654            "--gateway",
1655            gateway
1656        )
1657        .out_json()
1658        .await?,
1659    )?)
1660}
1661
1662async fn lnv2_send(client: &Client, gateway: &String, invoice: &String) -> anyhow::Result<()> {
1663    let send_op = serde_json::from_value::<OperationId>(
1664        cmd!(
1665            client,
1666            "module",
1667            "lnv2",
1668            "send",
1669            invoice,
1670            "--gateway",
1671            gateway
1672        )
1673        .out_json()
1674        .await?,
1675    )?;
1676
1677    assert_eq!(
1678        cmd!(
1679            client,
1680            "module",
1681            "lnv2",
1682            "await-send",
1683            serde_json::to_string(&send_op)?.substring(1, 65)
1684        )
1685        .out_json()
1686        .await?,
1687        serde_json::to_value(FinalSendOperationState::Success).expect("JSON serialization failed"),
1688    );
1689
1690    Ok(())
1691}
1692
1693pub async fn reconnect_test(dev_fed: DevFed, process_mgr: &ProcessManager) -> Result<()> {
1694    log_binary_versions().await?;
1695
1696    let DevFed {
1697        bitcoind, mut fed, ..
1698    } = dev_fed;
1699
1700    bitcoind.mine_blocks(110).await?;
1701    fed.await_block_sync().await?;
1702    fed.await_all_peers().await?;
1703
1704    // test a peer missing out on epochs and needing to rejoin
1705    fed.terminate_server(0).await?;
1706    fed.mine_then_wait_blocks_sync(100).await?;
1707
1708    fed.start_server(process_mgr, 0).await?;
1709    fed.mine_then_wait_blocks_sync(100).await?;
1710    fed.await_all_peers().await?;
1711    info!(target: LOG_DEVIMINT, "Server 0 successfully rejoined!");
1712    fed.mine_then_wait_blocks_sync(100).await?;
1713
1714    // now test what happens if consensus needs to be restarted
1715    fed.terminate_server(1).await?;
1716    fed.mine_then_wait_blocks_sync(100).await?;
1717    fed.terminate_server(2).await?;
1718    fed.terminate_server(3).await?;
1719
1720    fed.start_server(process_mgr, 1).await?;
1721    fed.start_server(process_mgr, 2).await?;
1722    fed.start_server(process_mgr, 3).await?;
1723
1724    fed.await_all_peers().await?;
1725
1726    info!(target: LOG_DEVIMINT, "fm success: reconnect-test");
1727    Ok(())
1728}
1729
1730pub async fn recoverytool_test(dev_fed: DevFed) -> Result<()> {
1731    log_binary_versions().await?;
1732
1733    let DevFed { bitcoind, fed, .. } = dev_fed;
1734
1735    let data_dir = env::var(FM_DATA_DIR_ENV)?;
1736    let client = fed.new_joined_client("recoverytool-test-client").await?;
1737
1738    let mut fed_utxos_sats = HashSet::from([12_345_000, 23_456_000, 34_567_000]);
1739    let deposit_fees = fed.deposit_fees()?.msats / 1000;
1740    for sats in &fed_utxos_sats {
1741        // pegin_client automatically adds fees, so we need to counteract that
1742        fed.pegin_client(*sats - deposit_fees, &client).await?;
1743    }
1744
1745    async fn withdraw(
1746        client: &Client,
1747        bitcoind: &crate::external::Bitcoind,
1748        fed_utxos_sats: &mut HashSet<u64>,
1749    ) -> Result<()> {
1750        let withdrawal_address = bitcoind.get_new_address().await?;
1751        let withdraw_res = cmd!(
1752            client,
1753            "withdraw",
1754            "--address",
1755            &withdrawal_address,
1756            "--amount",
1757            "5000 sat"
1758        )
1759        .out_json()
1760        .await?;
1761
1762        let fees_sat = withdraw_res["fees_sat"]
1763            .as_u64()
1764            .expect("withdrawal should contain fees");
1765        let txid: Txid = withdraw_res["txid"]
1766            .as_str()
1767            .expect("withdrawal should contain txid string")
1768            .parse()
1769            .expect("txid should be parsable");
1770        let tx_hex = bitcoind.poll_get_transaction(txid).await?;
1771
1772        let tx = bitcoin::Transaction::consensus_decode_hex(&tx_hex, &ModuleRegistry::default())?;
1773        assert_eq!(tx.input.len(), 1);
1774        assert_eq!(tx.output.len(), 2);
1775
1776        let change_output = tx
1777            .output
1778            .iter()
1779            .find(|o| o.to_owned().script_pubkey != withdrawal_address.script_pubkey())
1780            .expect("withdrawal must have change output");
1781        assert!(fed_utxos_sats.insert(change_output.value.to_sat()));
1782
1783        // Remove the utxo consumed from the withdrawal tx
1784        let total_output_sats = tx.output.iter().map(|o| o.value.to_sat()).sum::<u64>();
1785        let input_sats = total_output_sats + fees_sat;
1786        assert!(fed_utxos_sats.remove(&input_sats));
1787
1788        Ok(())
1789    }
1790
1791    // Initiate multiple withdrawals in a session to verify the recoverytool
1792    // recognizes change outputs
1793    for _ in 0..2 {
1794        withdraw(&client, &bitcoind, &mut fed_utxos_sats).await?;
1795    }
1796
1797    let total_fed_sats = fed_utxos_sats.iter().sum::<u64>();
1798    fed.finalize_mempool_tx().await?;
1799
1800    // We are done transacting and save the current session id so we can wait for
1801    // the next session later on. We already save it here so that if in the meantime
1802    // a session is generated we don't wait for another.
1803    let last_tx_session = client.get_session_count().await?;
1804
1805    info!("Recovering using utxos method");
1806    let output = cmd!(
1807        crate::util::Recoverytool,
1808        "--cfg",
1809        "{data_dir}/fedimintd-default-0",
1810        "utxos",
1811        "--db",
1812        "{data_dir}/fedimintd-default-0/database"
1813    )
1814    .env(FM_PASSWORD_ENV, "pass")
1815    .out_json()
1816    .await?;
1817    let outputs = output.as_array().context("expected an array")?;
1818    assert_eq!(outputs.len(), fed_utxos_sats.len());
1819
1820    assert_eq!(
1821        outputs
1822            .iter()
1823            .map(|o| o["amount_sat"].as_u64().unwrap())
1824            .collect::<HashSet<_>>(),
1825        fed_utxos_sats
1826    );
1827    let utxos_descriptors = outputs
1828        .iter()
1829        .map(|o| o["descriptor"].as_str().unwrap())
1830        .collect::<HashSet<_>>();
1831
1832    debug!(target: LOG_DEVIMINT, ?utxos_descriptors, "recoverytool descriptors using UTXOs method");
1833
1834    let descriptors_json = serde_json::value::to_raw_value(&serde_json::Value::Array(vec![
1835        serde_json::Value::Array(
1836            utxos_descriptors
1837                .iter()
1838                .map(|d| {
1839                    json!({
1840                        "desc": d,
1841                        "timestamp": 0,
1842                    })
1843                })
1844                .collect(),
1845        ),
1846    ]))?;
1847    info!("Getting wallet balances before import");
1848    let bitcoin_client = bitcoind.wallet_client().await?;
1849    let balances_before = bitcoin_client.get_balances().await?;
1850    info!("Importing descriptors into bitcoin wallet");
1851    let request = bitcoin_client
1852        .get_jsonrpc_client()
1853        .build_request("importdescriptors", Some(&descriptors_json));
1854    let response = block_in_place(|| bitcoin_client.get_jsonrpc_client().send_request(request))?;
1855    response.check_error()?;
1856    info!("Getting wallet balances after import");
1857    let balances_after = bitcoin_client.get_balances().await?;
1858    let diff = balances_after.mine.immature + balances_after.mine.trusted
1859        - balances_before.mine.immature
1860        - balances_before.mine.trusted;
1861
1862    // We need to wait for a session to be generated to make sure we have the signed
1863    // session outcome in our DB. If there ever is another problem here: wait for
1864    // fedimintd-0 specifically to acknowledge the session switch. In practice this
1865    // should be sufficiently synchronous though.
1866    client.wait_session_outcome(last_tx_session).await?;
1867
1868    // Funds from descriptors should match the fed's utxos
1869    assert_eq!(diff.to_sat(), total_fed_sats);
1870    info!("Recovering using epochs method");
1871
1872    let outputs = cmd!(
1873        crate::util::Recoverytool,
1874        "--cfg",
1875        "{data_dir}/fedimintd-default-0",
1876        "epochs",
1877        "--db",
1878        "{data_dir}/fedimintd-default-0/database"
1879    )
1880    .env(FM_PASSWORD_ENV, "pass")
1881    .out_json()
1882    .await?
1883    .as_array()
1884    .context("expected an array")?
1885    .clone();
1886
1887    let epochs_descriptors = outputs
1888        .iter()
1889        .map(|o| o["descriptor"].as_str().unwrap())
1890        .collect::<HashSet<_>>();
1891
1892    // nosemgrep: use-err-formatting
1893    debug!(target: LOG_DEVIMINT, ?epochs_descriptors, "recoverytool descriptors using epochs method");
1894
1895    // Epochs method includes descriptors from spent outputs, so we only need to
1896    // verify the epochs method includes all available utxos
1897    for utxo_descriptor in utxos_descriptors {
1898        assert!(epochs_descriptors.contains(utxo_descriptor));
1899    }
1900    Ok(())
1901}
1902
1903pub async fn guardian_backup_test(dev_fed: DevFed, process_mgr: &ProcessManager) -> Result<()> {
1904    const PEER_TO_TEST: u16 = 0;
1905
1906    log_binary_versions().await?;
1907
1908    let DevFed { mut fed, .. } = dev_fed;
1909
1910    fed.await_all_peers()
1911        .await
1912        .expect("Awaiting federation coming online failed");
1913
1914    let client = fed.new_joined_client("guardian-client").await?;
1915    let old_block_count = cmd!(
1916        client,
1917        "dev",
1918        "api",
1919        "--peer-id",
1920        PEER_TO_TEST.to_string(),
1921        "--module",
1922        "wallet",
1923        "block_count",
1924    )
1925    .out_json()
1926    .await?["value"]
1927        .as_u64()
1928        .expect("No block height returned");
1929
1930    let backup_res = cmd!(
1931        client,
1932        "--our-id",
1933        PEER_TO_TEST.to_string(),
1934        "--password",
1935        "pass",
1936        "admin",
1937        "guardian-config-backup"
1938    )
1939    .out_json()
1940    .await?;
1941    let backup_hex = backup_res["tar_archive_bytes"]
1942        .as_str()
1943        .expect("expected hex string");
1944    let backup_tar = hex::decode(backup_hex).expect("invalid hex");
1945
1946    let data_dir = fed
1947        .vars
1948        .get(&PEER_TO_TEST.into())
1949        .expect("peer not found")
1950        .FM_DATA_DIR
1951        .clone();
1952
1953    fed.terminate_server(PEER_TO_TEST.into())
1954        .await
1955        .expect("could not terminate fedimintd");
1956
1957    std::fs::remove_dir_all(&data_dir).expect("error deleting old datadir");
1958    std::fs::create_dir(&data_dir).expect("error creating new datadir");
1959
1960    let write_file = |name: &str, data: &[u8]| {
1961        let mut file = std::fs::File::options()
1962            .write(true)
1963            .create(true)
1964            .truncate(true)
1965            .open(data_dir.join(name))
1966            .expect("could not open file");
1967        file.write_all(data).expect("could not write file");
1968        file.flush().expect("could not flush file");
1969    };
1970
1971    write_file("backup.tar", &backup_tar);
1972    write_file(
1973        fedimint_server::config::io::PLAINTEXT_PASSWORD,
1974        "pass".as_bytes(),
1975    );
1976
1977    assert_eq!(
1978        std::process::Command::new("tar")
1979            .arg("-xf")
1980            .arg("backup.tar")
1981            .current_dir(data_dir)
1982            .spawn()
1983            .expect("error spawning tar")
1984            .wait()
1985            .expect("error extracting archive")
1986            .code(),
1987        Some(0),
1988        "tar failed"
1989    );
1990
1991    fed.start_server(process_mgr, PEER_TO_TEST.into())
1992        .await
1993        .expect("could not restart fedimintd");
1994
1995    poll("Peer catches up again", || async {
1996        let block_counts = all_peer_block_count(&client, fed.member_ids())
1997            .await
1998            .map_err(ControlFlow::Continue)?;
1999        let block_count = block_counts[&PeerId::from(PEER_TO_TEST)];
2000
2001        info!("Caught up to block {block_count} of at least {old_block_count} (counts={block_counts:?})");
2002
2003        if block_count < old_block_count {
2004            return Err(ControlFlow::Continue(anyhow!("Block count still behind")));
2005        }
2006
2007        Ok(())
2008    })
2009    .await
2010    .expect("Peer didn't rejoin federation");
2011
2012    Ok(())
2013}
2014
2015async fn peer_block_count(client: &Client, peer: PeerId) -> Result<u64> {
2016    cmd!(
2017        client,
2018        "dev",
2019        "api",
2020        "--peer-id",
2021        peer.to_string(),
2022        "--module",
2023        "wallet",
2024        "block_count",
2025    )
2026    .out_json()
2027    .await?["value"]
2028        .as_u64()
2029        .context("No block height returned")
2030}
2031
2032async fn all_peer_block_count(
2033    client: &Client,
2034    peers: impl Iterator<Item = PeerId>,
2035) -> Result<BTreeMap<PeerId, u64>> {
2036    let mut peer_heights = BTreeMap::new();
2037    for peer in peers {
2038        peer_heights.insert(peer, peer_block_count(client, peer).await?);
2039    }
2040    Ok(peer_heights)
2041}
2042
2043pub async fn cannot_replay_tx_test(dev_fed: DevFed) -> Result<()> {
2044    log_binary_versions().await?;
2045
2046    let DevFed { fed, .. } = dev_fed;
2047
2048    let client = fed.new_joined_client("cannot-replay-client").await?;
2049
2050    const CLIENT_START_AMOUNT: u64 = 10_000_000_000;
2051    const CLIENT_SPEND_AMOUNT: u64 = 5_000_000_000;
2052
2053    let initial_client_balance = client.balance().await?;
2054    assert_eq!(initial_client_balance, 0);
2055
2056    fed.pegin_client(CLIENT_START_AMOUNT / 1000, &client)
2057        .await?;
2058
2059    // Fork client before spending ecash so we can later attempt a double spend
2060    let double_spend_client = client.new_forked("double-spender").await?;
2061
2062    // Spend and reissue all ecash from the client
2063    let notes = cmd!(client, "spend", CLIENT_SPEND_AMOUNT)
2064        .out_json()
2065        .await?
2066        .get("notes")
2067        .expect("Output didn't contain e-cash notes")
2068        .as_str()
2069        .unwrap()
2070        .to_owned();
2071
2072    let client_post_spend_balance = client.balance().await?;
2073    crate::util::almost_equal(
2074        client_post_spend_balance,
2075        CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT,
2076        10_000,
2077    )
2078    .unwrap();
2079
2080    cmd!(client, "reissue", notes).out_json().await?;
2081    let client_post_reissue_balance = client.balance().await?;
2082    crate::util::almost_equal(client_post_reissue_balance, CLIENT_START_AMOUNT, 20_000).unwrap();
2083
2084    // Attempt to spend the same ecash from the forked client
2085    let double_spend_notes = cmd!(double_spend_client, "spend", CLIENT_SPEND_AMOUNT)
2086        .out_json()
2087        .await?
2088        .get("notes")
2089        .expect("Output didn't contain e-cash notes")
2090        .as_str()
2091        .unwrap()
2092        .to_owned();
2093
2094    let double_spend_client_post_spend_balance = double_spend_client.balance().await?;
2095    crate::util::almost_equal(
2096        double_spend_client_post_spend_balance,
2097        CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT,
2098        10_000,
2099    )
2100    .unwrap();
2101
2102    cmd!(double_spend_client, "reissue", double_spend_notes)
2103        .assert_error_contains("The transaction had an invalid input")
2104        .await?;
2105
2106    let double_spend_client_post_spend_balance = double_spend_client.balance().await?;
2107    crate::util::almost_equal(
2108        double_spend_client_post_spend_balance,
2109        CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT,
2110        10_000,
2111    )
2112    .unwrap();
2113
2114    Ok(())
2115}
2116
2117/// Test that client can init even when the federation is down
2118///
2119/// See <https://github.com/fedimint/fedimint/issues/6939>
2120pub async fn test_offline_client_initialization(
2121    dev_fed: DevFed,
2122    _process_mgr: &ProcessManager,
2123) -> Result<()> {
2124    log_binary_versions().await?;
2125
2126    let DevFed { mut fed, .. } = dev_fed;
2127
2128    // Ensure federation is properly initialized and all peers are online
2129    fed.await_all_peers().await?;
2130
2131    // Create and join a client while all servers are online
2132    let client = fed.new_joined_client("offline-test-client").await?;
2133
2134    // Verify client can get info while federation is online
2135    const INFO_COMMAND_TIMEOUT: Duration = Duration::from_secs(5);
2136    let online_info =
2137        fedimint_core::runtime::timeout(INFO_COMMAND_TIMEOUT, cmd!(client, "info").out_json())
2138            .await
2139            .context("Client info command timed out while federation was online")?
2140            .context("Client info command failed while federation was online")?;
2141    info!(target: LOG_DEVIMINT, "Client info while federation online: {:?}", online_info);
2142
2143    // Shutdown all federation servers
2144    info!(target: LOG_DEVIMINT, "Shutting down all federation servers...");
2145    fed.terminate_all_servers().await?;
2146
2147    // Wait a moment to ensure servers are fully shutdown
2148    fedimint_core::task::sleep_in_test("wait for federation shutdown", Duration::from_secs(2))
2149        .await;
2150
2151    // Test that client info command still works with all servers offline
2152    // This should work because client info doesn't require server communication
2153    // for basic federation metadata and local state
2154    info!(target: LOG_DEVIMINT, "Testing client info command with all servers offline...");
2155    let offline_info =
2156        fedimint_core::runtime::timeout(INFO_COMMAND_TIMEOUT, cmd!(client, "info").out_json())
2157            .await
2158            .context("Client info command timed out while federation was offline")?
2159            .context("Client info command failed while federation was offline")?;
2160
2161    info!(target: LOG_DEVIMINT, "Client info while federation offline: {:?}", offline_info);
2162
2163    Ok(())
2164}
2165
2166/// Test that client can detect federation config changes when servers restart
2167/// with new module configurations
2168///
2169/// This test starts a fresh federation, dumps the client config, then stops all
2170/// servers and modifies their configs by adding a new meta module instance. The
2171/// client should detect this configuration change after the servers restart.
2172pub async fn test_client_config_change_detection(
2173    dev_fed: DevFed,
2174    process_mgr: &ProcessManager,
2175) -> Result<()> {
2176    log_binary_versions().await?;
2177
2178    let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
2179    let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
2180
2181    if fedimint_cli_version < *VERSION_0_9_0_ALPHA {
2182        info!(target: LOG_DEVIMINT, "Skipping the test - fedimint-cli too old");
2183        return Ok(());
2184    }
2185
2186    if fedimintd_version < *VERSION_0_9_0_ALPHA {
2187        info!(target: LOG_DEVIMINT, "Skipping the test - fedimintd too old");
2188        return Ok(());
2189    }
2190
2191    let DevFed { mut fed, .. } = dev_fed;
2192    let peer_ids: Vec<_> = fed.member_ids().collect();
2193
2194    fed.await_all_peers().await?;
2195
2196    let client = fed.new_joined_client("config-change-test-client").await?;
2197
2198    info!(target: LOG_DEVIMINT, "Getting initial client configuration...");
2199    let initial_config = cmd!(client, "config")
2200        .out_json()
2201        .await
2202        .context("Failed to get initial client config")?;
2203
2204    info!(target: LOG_DEVIMINT, "Initial config modules: {:?}", initial_config["modules"].as_object().unwrap().keys().collect::<Vec<_>>());
2205
2206    let data_dir = env::var(FM_DATA_DIR_ENV)?;
2207    let config_dir = PathBuf::from(&data_dir);
2208
2209    // Shutdown all federation servers
2210    //
2211    // In prod. one would probably use a coordinated shutdown, just to be
2212    // careful, but since the change is only adding a new module that does
2213    // not submit CIs without user/admin interaction, there is
2214    // no way for the consensus to diverge.
2215    info!(target: LOG_DEVIMINT, "Shutting down all federation servers...");
2216    fed.terminate_all_servers().await?;
2217
2218    // Wait for servers to fully shutdown
2219    fedimint_core::task::sleep_in_test("wait for federation shutdown", Duration::from_secs(2))
2220        .await;
2221
2222    info!(target: LOG_DEVIMINT, "Modifying server configurations to add new meta module...");
2223    modify_server_configs(&config_dir, &peer_ids).await?;
2224
2225    // Restart all servers with modified configs
2226    info!(target: LOG_DEVIMINT, "Restarting all servers with modified configurations...");
2227    for peer_id in peer_ids {
2228        fed.start_server(process_mgr, peer_id.to_usize()).await?;
2229    }
2230
2231    // Wait for federation to stabilize
2232    info!(target: LOG_DEVIMINT, "Wait for peers to get back up");
2233    fed.await_all_peers().await?;
2234
2235    // Use fedimint-cli dev wait to let the client read the new config in background
2236    info!(target: LOG_DEVIMINT, "Waiting for client to fetch updated configuration...");
2237    cmd!(client, "dev", "wait", "3")
2238        .run()
2239        .await
2240        .context("Failed to wait for client config update")?;
2241
2242    // Test that client switched to the new config
2243    info!(target: LOG_DEVIMINT, "Testing client detection of configuration changes...");
2244    let updated_config = cmd!(client, "config")
2245        .out_json()
2246        .await
2247        .context("Failed to get updated client config")?;
2248
2249    info!(target: LOG_DEVIMINT, "Updated config modules: {:?}", updated_config["modules"].as_object().unwrap().keys().collect::<Vec<_>>());
2250
2251    // Verify that the configuration has changed (new meta module should be present)
2252    let initial_modules = initial_config["modules"].as_object().unwrap();
2253    let updated_modules = updated_config["modules"].as_object().unwrap();
2254
2255    anyhow::ensure!(
2256        updated_modules.len() > initial_modules.len(),
2257        "Expected more modules in updated config. Initial: {}, Updated: {}",
2258        initial_modules.len(),
2259        updated_modules.len()
2260    );
2261
2262    // Check if a new meta module was added
2263    let new_meta_module = updated_modules.iter().find(|(module_id, module_config)| {
2264        module_config["kind"].as_str() == Some("meta") && !initial_modules.contains_key(*module_id)
2265    });
2266
2267    let new_meta_module_id = new_meta_module
2268        .map(|(id, _)| id)
2269        .with_context(|| "Expected to find new meta module in updated configuration")?;
2270
2271    info!(target: LOG_DEVIMINT, "Found new meta module with id: {}", new_meta_module_id);
2272
2273    // Verify client operations still work with the new configuration
2274    info!(target: LOG_DEVIMINT, "Verifying client operations work with new configuration...");
2275    let final_info = cmd!(client, "info")
2276        .out_json()
2277        .await
2278        .context("Client info command failed with updated configuration")?;
2279
2280    info!(target: LOG_DEVIMINT, "Client successfully adapted to configuration changes: {:?}", final_info["federation_id"]);
2281
2282    Ok(())
2283}
2284
2285/// Modify server configuration files to add a new meta module instance
2286async fn modify_server_configs(config_dir: &Path, peer_ids: &[PeerId]) -> Result<()> {
2287    for &peer_id in peer_ids {
2288        modify_single_peer_config(config_dir, peer_id).await?;
2289    }
2290    Ok(())
2291}
2292
2293/// Modify configuration files for a single peer to add a new meta module
2294/// instance
2295async fn modify_single_peer_config(config_dir: &Path, peer_id: PeerId) -> Result<()> {
2296    use fedimint_aead::{encrypted_write, get_encryption_key};
2297    use fedimint_core::core::ModuleInstanceId;
2298    use fedimint_server::config::io::read_server_config;
2299    use serde_json::Value;
2300
2301    info!(target: LOG_DEVIMINT, %peer_id, "Modifying config for peer");
2302    let peer_dir = config_dir.join(format!("fedimintd-default-{}", peer_id.to_usize()));
2303
2304    // Read consensus config
2305    let consensus_config_path = peer_dir.join("consensus.json");
2306    let consensus_config_content = fs::read_to_string(&consensus_config_path)
2307        .await
2308        .with_context(|| format!("Failed to read consensus config for peer {peer_id}"))?;
2309
2310    let mut consensus_config: Value = serde_json::from_str(&consensus_config_content)
2311        .with_context(|| format!("Failed to parse consensus config for peer {peer_id}"))?;
2312
2313    // Read the encrypted private config using the server config reader
2314    let password = "pass"; // Default password used in devimint
2315    let server_config = read_server_config(password, &peer_dir)
2316        .with_context(|| format!("Failed to read server config for peer {peer_id}"))?;
2317
2318    // Find existing meta module in configs to use as template
2319    let consensus_config_modules = consensus_config["modules"]
2320        .as_object()
2321        .with_context(|| format!("No modules found in consensus config for peer {peer_id}"))?;
2322
2323    // Look for existing meta module to copy its configuration
2324    let existing_meta_consensus = consensus_config_modules
2325        .values()
2326        .find(|module_config| module_config["kind"].as_str() == Some("meta"));
2327
2328    let existing_meta_consensus = existing_meta_consensus
2329        .with_context(|| {
2330            format!("No existing meta module found in consensus config for peer {peer_id}")
2331        })?
2332        .clone();
2333
2334    // Find existing meta module in private config
2335    let existing_meta_instance_id = server_config
2336        .consensus
2337        .modules
2338        .iter()
2339        .find(|(_, config)| config.kind.as_str() == "meta")
2340        .map(|(id, _)| *id)
2341        .with_context(|| {
2342            format!("No existing meta module found in private config for peer {peer_id}")
2343        })?;
2344
2345    let existing_meta_private = server_config
2346        .private
2347        .modules
2348        .get(&existing_meta_instance_id)
2349        .with_context(|| format!("Failed to get existing meta private config for peer {peer_id}"))?
2350        .clone();
2351
2352    // Find the highest existing module ID for the new module
2353    let last_existing_module_id = consensus_config_modules
2354        .keys()
2355        .filter_map(|id| id.parse::<u32>().ok())
2356        .max()
2357        .unwrap_or(0);
2358
2359    let new_module_id = (last_existing_module_id + 1).to_string();
2360    let new_module_instance_id = ModuleInstanceId::from((last_existing_module_id + 1) as u16);
2361
2362    info!(
2363        "Adding new meta module with id {} for peer {} (copying existing meta module config)",
2364        new_module_id, peer_id
2365    );
2366
2367    // Add new meta module to consensus config by copying existing meta module
2368    if let Some(modules) = consensus_config["modules"].as_object_mut() {
2369        modules.insert(new_module_id.clone(), existing_meta_consensus);
2370    }
2371
2372    // Add new meta module to private config
2373    let mut updated_private_config = server_config.private.clone();
2374    updated_private_config
2375        .modules
2376        .insert(new_module_instance_id, existing_meta_private);
2377
2378    // Write back the modified consensus and client configs
2379    let updated_consensus_content = serde_json::to_string_pretty(&consensus_config)
2380        .with_context(|| format!("Failed to serialize consensus config for peer {peer_id}"))?;
2381
2382    write_overwrite_async(&consensus_config_path, updated_consensus_content)
2383        .await
2384        .with_context(|| format!("Failed to write consensus config for peer {peer_id}"))?;
2385
2386    // Write back the modified private config using direct encryption
2387    let salt = std::fs::read_to_string(peer_dir.join("private.salt"))
2388        .with_context(|| format!("Failed to read salt file for peer {peer_id}"))?;
2389    let key = get_encryption_key(password, &salt)
2390        .with_context(|| format!("Failed to get encryption key for peer {peer_id}"))?;
2391
2392    let private_config_bytes = serde_json::to_string(&updated_private_config)
2393        .with_context(|| format!("Failed to serialize private config for peer {peer_id}"))?
2394        .into_bytes();
2395
2396    // Remove the existing encrypted file first
2397    let encrypted_private_path = peer_dir.join("private.encrypt");
2398    if encrypted_private_path.exists() {
2399        std::fs::remove_file(&encrypted_private_path)
2400            .with_context(|| format!("Failed to remove old private config for peer {peer_id}"))?;
2401    }
2402
2403    encrypted_write(private_config_bytes, &key, encrypted_private_path)
2404        .with_context(|| format!("Failed to write encrypted private config for peer {peer_id}"))?;
2405
2406    info!("Successfully modified configs for peer {}", peer_id);
2407    Ok(())
2408}
2409
2410/// Tests the `admin auth` command that stores admin credentials in the client
2411/// database, allowing subsequent admin commands to run without --our-id and
2412/// --password.
2413pub async fn admin_auth_tests(dev_fed: DevFed) -> Result<()> {
2414    log_binary_versions().await?;
2415
2416    let DevFed { fed, .. } = dev_fed;
2417
2418    // Ensure all federation peers are ready before starting the test
2419    // This prevents flaky failures due to Iroh connection timeouts
2420    fed.await_all_peers().await?;
2421
2422    let client = fed.new_joined_client("admin-auth-test-client").await?;
2423
2424    let peer_id = 0;
2425
2426    info!(target: LOG_DEVIMINT, "Testing admin auth command stores credentials");
2427
2428    // First, store the admin credentials using the auth command
2429    // Use --no-verify to skip interactive verification in tests
2430    let auth_result = cmd!(
2431        client,
2432        "--our-id",
2433        &peer_id.to_string(),
2434        "--password",
2435        "pass",
2436        "admin",
2437        "auth",
2438        "--peer-id",
2439        &peer_id.to_string(),
2440        "--password",
2441        "pass",
2442        "--no-verify",
2443        "--force"
2444    )
2445    .out_json()
2446    .await
2447    .context("Admin auth command failed")?;
2448
2449    info!(target: LOG_DEVIMINT, ?auth_result, "Admin auth command completed");
2450
2451    // Verify the response contains expected fields
2452    assert_eq!(
2453        auth_result
2454            .get("peer_id")
2455            .and_then(serde_json::Value::as_u64),
2456        Some(peer_id as u64),
2457        "peer_id in response should match"
2458    );
2459    assert_eq!(
2460        auth_result
2461            .get("status")
2462            .and_then(serde_json::Value::as_str),
2463        Some("saved"),
2464        "status should be 'saved'"
2465    );
2466
2467    info!(target: LOG_DEVIMINT, "Testing that stored credentials are used automatically");
2468
2469    // Now run an admin command WITHOUT --our-id and --password
2470    // It should use the stored credentials automatically
2471    let status_result = cmd!(client, "admin", "status")
2472        .out_json()
2473        .await
2474        .context("Admin status command should succeed with stored credentials")?;
2475
2476    info!(target: LOG_DEVIMINT, ?status_result, "Admin status with stored credentials succeeded");
2477
2478    info!(target: LOG_DEVIMINT, "Testing that --force overwrites existing credentials");
2479
2480    // Test that --force allows overwriting
2481    let auth_result_force = cmd!(
2482        client,
2483        "--our-id",
2484        &peer_id.to_string(),
2485        "--password",
2486        "pass",
2487        "admin",
2488        "auth",
2489        "--peer-id",
2490        &peer_id.to_string(),
2491        "--password",
2492        "pass",
2493        "--no-verify",
2494        "--force"
2495    )
2496    .out_json()
2497    .await
2498    .context("Admin auth force overwrite failed")?;
2499
2500    assert_eq!(
2501        auth_result_force.get("status").and_then(|v| v.as_str()),
2502        Some("saved"),
2503        "Force overwrite should succeed"
2504    );
2505
2506    info!(target: LOG_DEVIMINT, "admin_auth_tests completed successfully");
2507
2508    Ok(())
2509}
2510
2511pub async fn test_guardian_password_change(
2512    dev_fed: DevFed,
2513    process_mgr: &ProcessManager,
2514) -> Result<()> {
2515    log_binary_versions().await?;
2516
2517    let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
2518    let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
2519
2520    if fedimint_cli_version < *VERSION_0_9_0_ALPHA {
2521        info!(target: LOG_DEVIMINT, "Skipping the test - fedimint-cli too old");
2522        return Ok(());
2523    }
2524
2525    if fedimintd_version < *VERSION_0_9_0_ALPHA {
2526        info!(target: LOG_DEVIMINT, "Skipping the test - fedimintd too old");
2527        return Ok(());
2528    }
2529
2530    let DevFed { mut fed, .. } = dev_fed;
2531    fed.await_all_peers().await?;
2532
2533    let client = fed.new_joined_client("config-change-test-client").await?;
2534
2535    let peer_id = 0;
2536    let data_dir: PathBuf = fed
2537        .vars
2538        .get(&peer_id)
2539        .expect("peer not found")
2540        .FM_DATA_DIR
2541        .clone();
2542    let file_exists = |file: &str| {
2543        let path = data_dir.join(file);
2544        path.exists()
2545    };
2546    let pre_password_file_exists = file_exists("password.secret");
2547
2548    info!(target: LOG_DEVIMINT, "Changing password");
2549    cmd!(
2550        client,
2551        "--our-id",
2552        &peer_id.to_string(),
2553        "--password",
2554        "pass",
2555        "admin",
2556        "change-password",
2557        "foobar"
2558    )
2559    .run()
2560    .await
2561    .context("Failed to change guardian password")?;
2562
2563    info!(target: LOG_DEVIMINT, "Waiting for fedimintd to be shut down");
2564    timeout(
2565        Duration::from_secs(30),
2566        fed.await_server_terminated(peer_id),
2567    )
2568    .await
2569    .context("Fedimintd didn't shut down in time after password change")??;
2570
2571    info!(target: LOG_DEVIMINT, "Restarting fedimintd");
2572    fed.start_server(process_mgr, peer_id).await?;
2573
2574    info!(target: LOG_DEVIMINT, "Wait for fedimintd to come online again");
2575    fed.await_peer(peer_id).await?;
2576
2577    info!(target: LOG_DEVIMINT, "Testing password change worked");
2578    cmd!(
2579        client,
2580        "--our-id",
2581        &peer_id.to_string(),
2582        "--password",
2583        "foobar",
2584        "admin",
2585        "backup-statistics"
2586    )
2587    .run()
2588    .await
2589    .context("Failed to run guardian command with new password")?;
2590
2591    assert!(!file_exists("private.bak"));
2592    assert!(!file_exists("password.bak"));
2593    assert!(!file_exists("private.new"));
2594    assert!(!file_exists("password.new"));
2595    assert_eq!(file_exists("password.secret"), pre_password_file_exists);
2596
2597    Ok(())
2598}
2599
2600#[derive(Subcommand)]
2601pub enum LatencyTest {
2602    Reissue,
2603    LnSend,
2604    LnReceive,
2605    FmPay,
2606    Restore,
2607}
2608
2609#[derive(Subcommand)]
2610pub enum UpgradeTest {
2611    Fedimintd {
2612        #[arg(long, trailing_var_arg = true, num_args=1..)]
2613        paths: Vec<PathBuf>,
2614    },
2615    FedimintCli {
2616        #[arg(long, trailing_var_arg = true, num_args=1..)]
2617        paths: Vec<PathBuf>,
2618    },
2619    Gatewayd {
2620        #[arg(long, trailing_var_arg = true, num_args=1..)]
2621        gatewayd_paths: Vec<PathBuf>,
2622        #[arg(long, trailing_var_arg = true, num_args=1..)]
2623        gateway_cli_paths: Vec<PathBuf>,
2624    },
2625}
2626
2627#[derive(Subcommand)]
2628pub enum TestCmd {
2629    /// `devfed` then checks the average latency of reissuing ecash, LN receive,
2630    /// and LN send
2631    LatencyTests {
2632        #[clap(subcommand)]
2633        r#type: LatencyTest,
2634
2635        #[arg(long, default_value = "10")]
2636        iterations: usize,
2637    },
2638    /// `devfed` then kills and restarts most of the Guardian nodes in a 4 node
2639    /// fedimint
2640    ReconnectTest,
2641    /// `devfed` then tests a bunch of the fedimint-cli commands
2642    CliTests,
2643    /// `devfed` then tests guardian metadata functionality
2644    GuardianMetadataTests,
2645    /// `devfed` then calls binary `fedimint-load-test-tool`. See
2646    /// `LoadTestArgs`.
2647    LoadTestToolTest,
2648    /// `devfed` then pegin LND Gateway. Kill the LN node,
2649    /// restart it, rejjoin fedimint and test payments still work
2650    LightningReconnectTest,
2651    /// `devfed` then reboot gateway daemon for both LDK and LND. Test
2652    /// afterward.
2653    GatewayRebootTest,
2654    /// `devfed` then tests if the recovery tool is able to do a basic recovery
2655    RecoverytoolTests,
2656    /// `devfed` then spawns faucet for wasm tests
2657    WasmTestSetup {
2658        #[arg(long, trailing_var_arg = true, allow_hyphen_values = true, num_args=1..)]
2659        exec: Option<Vec<ffi::OsString>>,
2660    },
2661    /// Restore guardian from downloaded backup
2662    GuardianBackup,
2663    /// `devfed` then tests that spent ecash cannot be double spent
2664    CannotReplayTransaction,
2665    /// Tests that client info commands work when all federation servers are
2666    /// offline
2667    TestOfflineClientInitialization,
2668    /// Tests that client can detect federation config changes when servers
2669    /// restart with new module configurations
2670    TestClientConfigChangeDetection,
2671    /// Tests that guardian password change works and the guardian can restart
2672    /// afterwards
2673    TestGuardianPasswordChange,
2674    /// `devfed` then tests admin auth credential storage
2675    TestAdminAuth,
2676    /// Test upgrade paths for a given binary
2677    UpgradeTests {
2678        #[clap(subcommand)]
2679        binary: UpgradeTest,
2680        #[arg(long)]
2681        lnv2: String,
2682    },
2683}
2684
2685pub async fn handle_command(cmd: TestCmd, common_args: CommonArgs) -> Result<()> {
2686    match cmd {
2687        TestCmd::WasmTestSetup { exec } => {
2688            let (process_mgr, task_group) = setup(common_args).await?;
2689            let main = {
2690                let task_group = task_group.clone();
2691                async move {
2692                    let dev_fed = dev_fed(&process_mgr).await?;
2693                    let gw_lnd = dev_fed.gw_lnd.clone();
2694                    let fed = dev_fed.fed.clone();
2695                    gw_lnd
2696                        .set_federation_routing_fee(dev_fed.fed.calculate_federation_id(), 0, 0)
2697                        .await?;
2698                    task_group.spawn_cancellable("faucet", async move {
2699                        if let Err(err) = crate::faucet::run(
2700                            &dev_fed,
2701                            format!("0.0.0.0:{}", process_mgr.globals.FM_PORT_FAUCET),
2702                            process_mgr.globals.FM_PORT_GW_LND,
2703                        )
2704                        .await
2705                        {
2706                            error!("Error spawning faucet: {err}");
2707                        }
2708                    });
2709                    try_join!(fed.pegin_gateways(30_000, vec![&gw_lnd]), async {
2710                        poll("waiting for faucet startup", || async {
2711                            TcpStream::connect(format!(
2712                                "127.0.0.1:{}",
2713                                process_mgr.globals.FM_PORT_FAUCET
2714                            ))
2715                            .await
2716                            .context("connect to faucet")
2717                            .map_err(ControlFlow::Continue)
2718                        })
2719                        .await?;
2720                        Ok(())
2721                    },)?;
2722                    if let Some(exec) = exec {
2723                        exec_user_command(exec).await?;
2724                        task_group.shutdown();
2725                    }
2726                    Ok::<_, anyhow::Error>(())
2727                }
2728            };
2729            cleanup_on_exit(main, task_group).await?;
2730        }
2731        TestCmd::LatencyTests { r#type, iterations } => {
2732            let (process_mgr, _) = setup(common_args).await?;
2733            let dev_fed = dev_fed(&process_mgr).await?;
2734            latency_tests(dev_fed, r#type, None, iterations, true).await?;
2735        }
2736        TestCmd::ReconnectTest => {
2737            let (process_mgr, _) = setup(common_args).await?;
2738            let dev_fed = dev_fed(&process_mgr).await?;
2739            reconnect_test(dev_fed, &process_mgr).await?;
2740        }
2741        TestCmd::CliTests => {
2742            let (process_mgr, _) = setup(common_args).await?;
2743            let dev_fed = dev_fed(&process_mgr).await?;
2744            cli_tests(dev_fed).await?;
2745        }
2746        TestCmd::GuardianMetadataTests => {
2747            let (process_mgr, _) = setup(common_args).await?;
2748            let dev_fed = dev_fed(&process_mgr).await?;
2749            guardian_metadata_tests(dev_fed).await?;
2750        }
2751        TestCmd::LoadTestToolTest => {
2752            // For the load test tool test, explicitly disable mint base fees
2753            unsafe { std::env::set_var(FM_DISABLE_BASE_FEES_ENV, "1") };
2754
2755            let (process_mgr, _) = setup(common_args).await?;
2756            let dev_fed = dev_fed(&process_mgr).await?;
2757            cli_load_test_tool_test(dev_fed).await?;
2758        }
2759        TestCmd::LightningReconnectTest => {
2760            let (process_mgr, _) = setup(common_args).await?;
2761            let dev_fed = dev_fed(&process_mgr).await?;
2762            lightning_gw_reconnect_test(dev_fed, &process_mgr).await?;
2763        }
2764        TestCmd::GatewayRebootTest => {
2765            let (process_mgr, _) = setup(common_args).await?;
2766            let dev_fed = dev_fed(&process_mgr).await?;
2767            gw_reboot_test(dev_fed, &process_mgr).await?;
2768        }
2769        TestCmd::RecoverytoolTests => {
2770            let (process_mgr, _) = setup(common_args).await?;
2771            let dev_fed = dev_fed(&process_mgr).await?;
2772            recoverytool_test(dev_fed).await?;
2773        }
2774        TestCmd::GuardianBackup => {
2775            let (process_mgr, _) = setup(common_args).await?;
2776            let dev_fed = dev_fed(&process_mgr).await?;
2777            guardian_backup_test(dev_fed, &process_mgr).await?;
2778        }
2779        TestCmd::CannotReplayTransaction => {
2780            let (process_mgr, _) = setup(common_args).await?;
2781            let dev_fed = dev_fed(&process_mgr).await?;
2782            cannot_replay_tx_test(dev_fed).await?;
2783        }
2784        TestCmd::TestOfflineClientInitialization => {
2785            let (process_mgr, _) = setup(common_args).await?;
2786            let dev_fed = dev_fed(&process_mgr).await?;
2787            test_offline_client_initialization(dev_fed, &process_mgr).await?;
2788        }
2789        TestCmd::TestClientConfigChangeDetection => {
2790            let (process_mgr, _) = setup(common_args).await?;
2791            let dev_fed = dev_fed(&process_mgr).await?;
2792            test_client_config_change_detection(dev_fed, &process_mgr).await?;
2793        }
2794        TestCmd::TestGuardianPasswordChange => {
2795            let (process_mgr, _) = setup(common_args).await?;
2796            let dev_fed = dev_fed(&process_mgr).await?;
2797            test_guardian_password_change(dev_fed, &process_mgr).await?;
2798        }
2799        TestCmd::TestAdminAuth => {
2800            // Check versions early before starting infrastructure
2801            let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
2802            let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
2803
2804            if fedimint_cli_version < *VERSION_0_11_0_ALPHA
2805                || fedimintd_version < *VERSION_0_11_0_ALPHA
2806            {
2807                info!(target: LOG_DEVIMINT, "Skipping admin_auth_tests - requires v0.11.0-alpha or later");
2808                return Ok(());
2809            }
2810
2811            let (process_mgr, _) = setup(common_args).await?;
2812            let dev_fed = dev_fed(&process_mgr).await?;
2813            admin_auth_tests(dev_fed).await?;
2814        }
2815        TestCmd::UpgradeTests { binary, lnv2 } => {
2816            // TODO: Audit that the environment access only happens in single-threaded code.
2817            unsafe { std::env::set_var(FM_ENABLE_MODULE_LNV2_ENV, lnv2) };
2818            let (process_mgr, _) = setup(common_args).await?;
2819            Box::pin(upgrade_tests(&process_mgr, binary)).await?;
2820        }
2821    }
2822    Ok(())
2823}