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    }
865
866    info!("Testing outgoing payment from client to LDK via LND gateway");
867    let initial_lnd_gateway_balance = gw_lnd.ecash_balance(fed_id.clone()).await?;
868    let invoice = gw_ldk.create_invoice(2_000_000).await?;
869    ln_pay(&client, invoice.to_string(), lnd_gw_id.clone()).await?;
870    let fed_id = fed.calculate_federation_id();
871    gw_ldk
872        .wait_bolt11_invoice(invoice.payment_hash().consensus_encode_to_vec())
873        .await?;
874
875    // Assert balances changed by 2_000_000 msat (amount sent) + 0 msat (fee)
876    let final_lnd_outgoing_gateway_balance = gw_lnd.ecash_balance(fed_id.clone()).await?;
877    anyhow::ensure!(
878        almost_equal(
879            final_lnd_outgoing_gateway_balance - initial_lnd_gateway_balance,
880            2_000_000,
881            3_000
882        )
883        .is_ok(),
884        "LND Gateway balance changed by {} on LND outgoing payment, expected 2_000_000",
885        (final_lnd_outgoing_gateway_balance - initial_lnd_gateway_balance)
886    );
887
888    // INCOMING: fedimint-cli receives from LDK via LND gateway
889    info!("Testing incoming payment from LDK to client via LND gateway");
890    let initial_lnd_incoming_client_balance = client.balance().await?;
891    let recv = ln_invoice(
892        &client,
893        Amount::from_msats(1_300_000),
894        "incoming-over-lnd-gw".to_string(),
895        lnd_gw_id,
896    )
897    .await?;
898    let invoice = recv.invoice;
899    gw_ldk
900        .pay_invoice(Bolt11Invoice::from_str(&invoice).expect("Could not parse invoice"))
901        .await?;
902
903    // Receive the ecash notes
904    info!("Testing receiving ecash notes");
905    let operation_id = recv.operation_id;
906    cmd!(client, "await-invoice", operation_id.fmt_full())
907        .run()
908        .await?;
909
910    // Assert balances changed by 1_300_000 msat
911    let final_lnd_incoming_client_balance = client.balance().await?;
912    let final_lnd_incoming_gateway_balance = gw_lnd.ecash_balance(fed_id.clone()).await?;
913    anyhow::ensure!(
914        almost_equal(
915            final_lnd_incoming_client_balance - initial_lnd_incoming_client_balance,
916            1_300_000,
917            2_000
918        )
919        .is_ok(),
920        "Client balance changed by {} on LND incoming payment, expected 1_300_000",
921        (final_lnd_incoming_client_balance - initial_lnd_incoming_client_balance)
922    );
923    anyhow::ensure!(
924        almost_equal(
925            final_lnd_outgoing_gateway_balance - final_lnd_incoming_gateway_balance,
926            1_300_000,
927            2_000
928        )
929        .is_ok(),
930        "LND Gateway balance changed by {} on LND incoming payment, expected 1_300_000",
931        (final_lnd_outgoing_gateway_balance - final_lnd_incoming_gateway_balance)
932    );
933
934    // TODO: test cancel/timeout
935
936    // # Wallet tests
937    // ## Deposit
938    info!("Testing client deposit");
939    let initial_walletng_balance = client.balance().await?;
940
941    fed.pegin_client(100_000, &client).await?; // deposit in sats
942
943    let post_deposit_walletng_balance = client.balance().await?;
944
945    almost_equal(
946        post_deposit_walletng_balance,
947        initial_walletng_balance + 100_000_000, // deposit in msats
948        2_000,
949    )
950    .unwrap();
951
952    // ## Withdraw
953    info!("Testing client withdraw");
954
955    let initial_walletng_balance = client.balance().await?;
956
957    let address = bitcoind.get_new_address().await?;
958    let withdraw_res = cmd!(
959        client,
960        "withdraw",
961        "--address",
962        &address,
963        "--amount",
964        "50000 sat"
965    )
966    .out_json()
967    .await?;
968
969    let txid: Txid = withdraw_res["txid"].as_str().unwrap().parse().unwrap();
970    let fees_sat = withdraw_res["fees_sat"].as_u64().unwrap();
971
972    let tx_hex = bitcoind.poll_get_transaction(txid).await?;
973
974    let tx = bitcoin::Transaction::consensus_decode_hex(&tx_hex, &ModuleRegistry::default())?;
975    assert!(
976        tx.output
977            .iter()
978            .any(|o| o.script_pubkey == address.script_pubkey() && o.value.to_sat() == 50000)
979    );
980
981    let post_withdraw_walletng_balance = client.balance().await?;
982    let expected_wallet_balance = initial_walletng_balance - 50_000_000 - (fees_sat * 1000);
983
984    almost_equal(
985        post_withdraw_walletng_balance,
986        expected_wallet_balance,
987        4_000,
988    )
989    .unwrap();
990
991    // # peer-version command
992    let peer_0_fedimintd_version = cmd!(client, "dev", "peer-version", "--peer-id", "0")
993        .out_json()
994        .await?
995        .get("version")
996        .expect("Output didn't contain version")
997        .as_str()
998        .unwrap()
999        .to_owned();
1000
1001    assert_eq!(
1002        semver::Version::parse(&peer_0_fedimintd_version)?,
1003        fedimintd_version
1004    );
1005
1006    info!("Checking initial announcements...");
1007
1008    retry(
1009        "Check initial announcements",
1010        aggressive_backoff(),
1011        || async {
1012            // Give the client some time to fetch updates
1013            cmd!(client, "dev", "wait", "1").run().await?;
1014
1015            // # API URL announcements
1016            let initial_announcements =
1017                serde_json::from_value::<BTreeMap<PeerId, SignedApiAnnouncement>>(
1018                    cmd!(client, "dev", "api-announcements",).out_json().await?,
1019                )
1020                .expect("failed to parse API announcements");
1021
1022            if initial_announcements.len() < fed.members.len() {
1023                bail!(
1024                    "Not all announcements ready; got: {}, expected: {}",
1025                    initial_announcements.len(),
1026                    fed.members.len()
1027                )
1028            }
1029
1030            if !initial_announcements
1031                .values()
1032                .all(|announcement| announcement.api_announcement.nonce == 0)
1033            {
1034                bail!("Not all announcements have their initial value");
1035            }
1036            Ok(())
1037        },
1038    )
1039    .await?;
1040
1041    const NEW_API_URL: &str = "ws://127.0.0.1:4242";
1042    let new_announcement = serde_json::from_value::<SignedApiAnnouncement>(
1043        cmd!(
1044            client,
1045            "--our-id",
1046            "0",
1047            "--password",
1048            "pass",
1049            "admin",
1050            "sign-api-announcement",
1051            NEW_API_URL
1052        )
1053        .out_json()
1054        .await?,
1055    )
1056    .expect("Couldn't parse signed announcement");
1057
1058    assert_eq!(
1059        new_announcement.api_announcement.nonce, 1,
1060        "Nonce did not increment correctly"
1061    );
1062
1063    info!("Testing if the client syncs the announcement");
1064    let announcement = poll("Waiting for the announcement to propagate", || async {
1065        cmd!(client, "dev", "wait", "1")
1066            .run()
1067            .await
1068            .map_err(ControlFlow::Break)?;
1069
1070        let new_announcements_peer2 =
1071            serde_json::from_value::<BTreeMap<PeerId, SignedApiAnnouncement>>(
1072                cmd!(client, "dev", "api-announcements",)
1073                    .out_json()
1074                    .await
1075                    .map_err(ControlFlow::Break)?,
1076            )
1077            .expect("failed to parse API announcements");
1078
1079        let announcement = new_announcements_peer2[&PeerId::from(0)]
1080            .api_announcement
1081            .clone();
1082        if announcement.nonce == 1 {
1083            Ok(announcement)
1084        } else {
1085            Err(ControlFlow::Continue(anyhow!(
1086                "Haven't received updated announcement yet; nonce: {}",
1087                announcement.nonce
1088            )))
1089        }
1090    })
1091    .await?;
1092
1093    assert_eq!(
1094        announcement.api_url,
1095        NEW_API_URL.parse().expect("valid URL")
1096    );
1097
1098    Ok(())
1099}
1100
1101pub async fn guardian_metadata_tests(dev_fed: DevFed) -> Result<()> {
1102    use fedimint_core::PeerId;
1103    use fedimint_core::net::guardian_metadata::SignedGuardianMetadata;
1104
1105    log_binary_versions().await?;
1106
1107    let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
1108    let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
1109
1110    if fedimintd_version < *VERSION_0_11_0_ALPHA || fedimint_cli_version < *VERSION_0_11_0_ALPHA {
1111        info!("Skipping test for too old versions");
1112        return Ok(());
1113    }
1114
1115    let DevFed { fed, .. } = dev_fed;
1116
1117    let client = fed.internal_client().await?;
1118
1119    info!("Checking initial guardian metadata...");
1120
1121    retry(
1122        "Check initial guardian metadata",
1123        aggressive_backoff(),
1124        || async {
1125            // Give the client some time to fetch updates
1126            cmd!(client, "dev", "wait", "1").run().await?;
1127
1128            let initial_metadata =
1129                serde_json::from_value::<BTreeMap<PeerId, SignedGuardianMetadata>>(
1130                    cmd!(client, "dev", "guardian-metadata",).out_json().await?,
1131                )
1132                .expect("failed to parse guardian metadata");
1133
1134            if initial_metadata.len() < fed.members.len() {
1135                bail!(
1136                    "Not all guardian metadata ready; got: {}, expected: {}",
1137                    initial_metadata.len(),
1138                    fed.members.len()
1139                )
1140            }
1141
1142            Ok(())
1143        },
1144    )
1145    .await?;
1146
1147    const TEST_API_URL: &str = "ws://127.0.0.1:5000/";
1148    const TEST_PKARR_ID: &str = "test_pkarr_id_z32";
1149
1150    let new_metadata = serde_json::from_value::<SignedGuardianMetadata>(
1151        cmd!(
1152            client,
1153            "--our-id",
1154            "0",
1155            "--password",
1156            "pass",
1157            "admin",
1158            "sign-guardian-metadata",
1159            "--api-urls",
1160            TEST_API_URL,
1161            "--pkarr-id",
1162            TEST_PKARR_ID
1163        )
1164        .out_json()
1165        .await?,
1166    )
1167    .expect("Couldn't parse signed guardian metadata");
1168
1169    let parsed_metadata = new_metadata.guardian_metadata();
1170
1171    assert_eq!(
1172        parsed_metadata.api_urls.first().unwrap().to_string(),
1173        TEST_API_URL,
1174        "API URL did not match"
1175    );
1176
1177    assert_eq!(
1178        parsed_metadata.pkarr_id_z32, TEST_PKARR_ID,
1179        "Pkarr ID did not match"
1180    );
1181
1182    info!("Testing if the client syncs the guardian metadata");
1183    let metadata = poll("Waiting for the guardian metadata to propagate", || async {
1184        cmd!(client, "dev", "wait", "1")
1185            .run()
1186            .await
1187            .map_err(ControlFlow::Break)?;
1188
1189        let new_metadata_peer0 =
1190            serde_json::from_value::<BTreeMap<PeerId, SignedGuardianMetadata>>(
1191                cmd!(client, "dev", "guardian-metadata",)
1192                    .out_json()
1193                    .await
1194                    .map_err(ControlFlow::Break)?,
1195            )
1196            .expect("failed to parse guardian metadata");
1197
1198        let metadata = new_metadata_peer0[&PeerId::from(0)].guardian_metadata();
1199
1200        if metadata.api_urls.first().unwrap().to_string() == TEST_API_URL {
1201            Ok(metadata.clone())
1202        } else {
1203            Err(ControlFlow::Continue(anyhow!(
1204                "Haven't received updated guardian metadata yet"
1205            )))
1206        }
1207    })
1208    .await?;
1209
1210    assert_eq!(
1211        metadata.pkarr_id_z32, TEST_PKARR_ID,
1212        "Pkarr ID did not propagate correctly"
1213    );
1214
1215    Ok(())
1216}
1217
1218pub async fn cli_load_test_tool_test(dev_fed: DevFed) -> Result<()> {
1219    log_binary_versions().await?;
1220    let data_dir = env::var(FM_DATA_DIR_ENV)?;
1221    let load_test_temp = PathBuf::from(data_dir).join("load-test-temp");
1222    dev_fed
1223        .fed
1224        .pegin_client(10_000, dev_fed.fed.internal_client().await?)
1225        .await?;
1226    let invite_code = dev_fed.fed.invite_code()?;
1227    dev_fed
1228        .gw_lnd
1229        .set_federation_routing_fee(dev_fed.fed.calculate_federation_id(), 0, 0)
1230        .await?;
1231    run_standard_load_test(&load_test_temp, &invite_code).await?;
1232    run_ln_circular_load_test(&load_test_temp, &invite_code).await?;
1233    Ok(())
1234}
1235
1236pub async fn run_standard_load_test(
1237    load_test_temp: &Path,
1238    invite_code: &str,
1239) -> anyhow::Result<()> {
1240    let output = cmd!(
1241        LoadTestTool,
1242        "--archive-dir",
1243        load_test_temp.display(),
1244        "--users",
1245        "1",
1246        "load-test",
1247        "--notes-per-user",
1248        "1",
1249        "--generate-invoice-with",
1250        "ldk-lightning-cli",
1251        "--invite-code",
1252        invite_code
1253    )
1254    .out_string()
1255    .await?;
1256    println!("{output}");
1257    anyhow::ensure!(
1258        output.contains("2 reissue_notes"),
1259        "reissued different number notes than expected"
1260    );
1261    anyhow::ensure!(
1262        output.contains("1 gateway_pay_invoice"),
1263        "paid different number of invoices than expected"
1264    );
1265    Ok(())
1266}
1267
1268pub async fn run_ln_circular_load_test(
1269    load_test_temp: &Path,
1270    invite_code: &str,
1271) -> anyhow::Result<()> {
1272    info!("Testing ln-circular-load-test with 'two-gateways' strategy");
1273    let output = cmd!(
1274        LoadTestTool,
1275        "--archive-dir",
1276        load_test_temp.display(),
1277        "--users",
1278        "1",
1279        "ln-circular-load-test",
1280        "--strategy",
1281        "two-gateways",
1282        "--test-duration-secs",
1283        "2",
1284        "--invite-code",
1285        invite_code
1286    )
1287    .out_string()
1288    .await?;
1289    println!("{output}");
1290    anyhow::ensure!(
1291        output.contains("gateway_create_invoice"),
1292        "missing invoice creation"
1293    );
1294    anyhow::ensure!(
1295        output.contains("gateway_pay_invoice_success"),
1296        "missing invoice payment"
1297    );
1298    anyhow::ensure!(
1299        output.contains("gateway_payment_received_success"),
1300        "missing received payment"
1301    );
1302
1303    info!("Testing ln-circular-load-test with 'partner-ping-pong' strategy");
1304    // Note: invite code isn't required because we already have an archive dir
1305    // Note: test-duration-secs needs to be greater than the timeout for
1306    // discover_api_version_set to work with degraded federations
1307    let output = cmd!(
1308        LoadTestTool,
1309        "--archive-dir",
1310        load_test_temp.display(),
1311        "--users",
1312        "1",
1313        "ln-circular-load-test",
1314        "--strategy",
1315        "partner-ping-pong",
1316        "--test-duration-secs",
1317        "6",
1318        "--invite-code",
1319        invite_code
1320    )
1321    .out_string()
1322    .await?;
1323    println!("{output}");
1324    anyhow::ensure!(
1325        output.contains("gateway_create_invoice"),
1326        "missing invoice creation"
1327    );
1328    anyhow::ensure!(
1329        output.contains("gateway_payment_received_success"),
1330        "missing received payment"
1331    );
1332
1333    info!("Testing ln-circular-load-test with 'self-payment' strategy");
1334    // Note invite code isn't required because we already have an archive dir
1335    let output = cmd!(
1336        LoadTestTool,
1337        "--archive-dir",
1338        load_test_temp.display(),
1339        "--users",
1340        "1",
1341        "ln-circular-load-test",
1342        "--strategy",
1343        "self-payment",
1344        "--test-duration-secs",
1345        "2",
1346        "--invite-code",
1347        invite_code
1348    )
1349    .out_string()
1350    .await?;
1351    println!("{output}");
1352    anyhow::ensure!(
1353        output.contains("gateway_create_invoice"),
1354        "missing invoice creation"
1355    );
1356    anyhow::ensure!(
1357        output.contains("gateway_payment_received_success"),
1358        "missing received payment"
1359    );
1360    Ok(())
1361}
1362
1363pub async fn lightning_gw_reconnect_test(
1364    dev_fed: DevFed,
1365    process_mgr: &ProcessManager,
1366) -> Result<()> {
1367    log_binary_versions().await?;
1368
1369    let DevFed {
1370        bitcoind,
1371        lnd,
1372        fed,
1373        mut gw_lnd,
1374        gw_ldk,
1375        ..
1376    } = dev_fed;
1377
1378    let client = fed
1379        .new_joined_client("lightning-gw-reconnect-test-client")
1380        .await?;
1381
1382    info!("Pegging-in both gateways");
1383    fed.pegin_gateways(99_999, vec![&gw_lnd]).await?;
1384
1385    // Drop other references to LND so that the test can kill it
1386    drop(lnd);
1387
1388    tracing::info!("Stopping LND");
1389    // Verify that the gateway can query the lightning node for the pubkey and alias
1390    let mut info_cmd = cmd!(gw_lnd, "info");
1391    assert!(info_cmd.run().await.is_ok());
1392
1393    // Verify that after stopping the lightning node, info no longer returns the
1394    // node public key since the lightning node is unreachable.
1395    let ln_type = gw_lnd.ln.ln_type().to_string();
1396    gw_lnd.stop_lightning_node().await?;
1397    let lightning_info = info_cmd.out_json().await?;
1398    if gw_lnd.gatewayd_version < *VERSION_0_10_0_ALPHA {
1399        let lightning_pub_key: Option<String> =
1400            serde_json::from_value(lightning_info["lightning_pub_key"].clone())?;
1401
1402        assert!(lightning_pub_key.is_none());
1403    } else {
1404        let not_connected = lightning_info["lightning_info"].clone();
1405        assert!(not_connected.as_str().expect("ln info is not a string") == "not_connected");
1406    }
1407
1408    // Restart LND
1409    tracing::info!("Restarting LND...");
1410    let new_lnd = Lnd::new(process_mgr, bitcoind.clone()).await?;
1411    gw_lnd.set_lightning_node(LightningNode::Lnd(new_lnd.clone()));
1412
1413    tracing::info!("Retrying info...");
1414    const MAX_RETRIES: usize = 30;
1415    const RETRY_INTERVAL: Duration = Duration::from_secs(1);
1416
1417    for i in 0..MAX_RETRIES {
1418        match do_try_create_and_pay_invoice(&gw_lnd, &client, &gw_ldk).await {
1419            Ok(()) => break,
1420            Err(e) => {
1421                if i == MAX_RETRIES - 1 {
1422                    return Err(e);
1423                }
1424                tracing::debug!(
1425                    "Pay invoice for gateway {} failed with {e:?}, retrying in {} seconds (try {}/{MAX_RETRIES})",
1426                    ln_type,
1427                    RETRY_INTERVAL.as_secs(),
1428                    i + 1,
1429                );
1430                fedimint_core::task::sleep_in_test(
1431                    "paying invoice for gateway failed",
1432                    RETRY_INTERVAL,
1433                )
1434                .await;
1435            }
1436        }
1437    }
1438
1439    info!(target: LOG_DEVIMINT, "lightning_reconnect_test: success");
1440    Ok(())
1441}
1442
1443pub async fn gw_reboot_test(dev_fed: DevFed, process_mgr: &ProcessManager) -> Result<()> {
1444    log_binary_versions().await?;
1445
1446    let DevFed {
1447        bitcoind,
1448        lnd,
1449        fed,
1450        gw_lnd,
1451        gw_ldk,
1452        gw_ldk_second,
1453        ..
1454    } = dev_fed;
1455
1456    let client = fed.new_joined_client("gw-reboot-test-client").await?;
1457    fed.pegin_client(10_000, &client).await?;
1458
1459    // Wait for gateways to sync to chain
1460    let block_height = bitcoind.get_block_count().await? - 1;
1461    try_join!(
1462        gw_lnd.wait_for_block_height(block_height),
1463        gw_ldk.wait_for_block_height(block_height),
1464    )?;
1465
1466    // Drop references to gateways so the test can kill them
1467    let lnd_gateway_id = gw_lnd.gateway_id.clone();
1468    let ldk_gateway_id = gw_ldk.gateway_id.clone();
1469    let gw_ldk_name = gw_ldk.gw_name.clone();
1470    let gw_ldk_port = gw_ldk.gw_port;
1471    let gw_lightning_port = gw_ldk.ldk_port;
1472    let gw_ldk_metrics_port = gw_ldk.metrics_port;
1473    drop(gw_lnd);
1474    drop(gw_ldk);
1475
1476    // Verify that making a payment while the gateways are down does not result in
1477    // funds being stuck
1478    info!("Making payment while gateway is down");
1479    let initial_client_balance = client.balance().await?;
1480    let invoice = gw_ldk_second.create_invoice(3000).await?;
1481    ln_pay(&client, invoice.to_string(), lnd_gateway_id.clone())
1482        .await
1483        .expect_err("Expected ln-pay to return error because the gateway is not online");
1484    let new_client_balance = client.balance().await?;
1485    anyhow::ensure!(initial_client_balance == new_client_balance);
1486
1487    // Reboot gateways with the same Lightning node instances
1488    info!("Rebooting gateways...");
1489    let (new_gw_lnd, new_gw_ldk) = try_join!(
1490        Gatewayd::new(process_mgr, LightningNode::Lnd(lnd.clone()), 0),
1491        Gatewayd::new(
1492            process_mgr,
1493            LightningNode::Ldk {
1494                name: gw_ldk_name,
1495                gw_port: gw_ldk_port,
1496                ldk_port: gw_lightning_port,
1497                metrics_port: gw_ldk_metrics_port,
1498            },
1499            1,
1500        )
1501    )?;
1502
1503    let lnd_gateway_id = fedimint_core::secp256k1::PublicKey::from_str(&lnd_gateway_id)?;
1504
1505    poll(
1506        "Waiting for LND Gateway Running state after reboot",
1507        || async {
1508            let mut new_lnd_cmd = cmd!(new_gw_lnd, "info");
1509            let lnd_value = new_lnd_cmd.out_json().await.map_err(ControlFlow::Continue)?;
1510            let reboot_gateway_state: String = serde_json::from_value(lnd_value["gateway_state"].clone()).context("invalid gateway state").map_err(ControlFlow::Break)?;
1511            let reboot_gateway_id = fedimint_core::secp256k1::PublicKey::from_str(&new_gw_lnd.gateway_id).expect("Could not convert public key");
1512
1513            if reboot_gateway_state == "Running" {
1514                info!(target: LOG_DEVIMINT, "LND Gateway restarted, with auto-rejoin to federation");
1515                // Assert that the gateway info is the same as before the reboot
1516                assert_eq!(lnd_gateway_id, reboot_gateway_id);
1517                return Ok(());
1518            }
1519            Err(ControlFlow::Continue(anyhow!("gateway not running")))
1520        },
1521    )
1522    .await?;
1523
1524    let ldk_gateway_id = fedimint_core::secp256k1::PublicKey::from_str(&ldk_gateway_id)?;
1525    poll(
1526        "Waiting for LDK Gateway Running state after reboot",
1527        || async {
1528            let mut new_ldk_cmd = cmd!(new_gw_ldk, "info");
1529            let ldk_value = new_ldk_cmd.out_json().await.map_err(ControlFlow::Continue)?;
1530            let reboot_gateway_state: String = serde_json::from_value(ldk_value["gateway_state"].clone()).context("invalid gateway state").map_err(ControlFlow::Break)?;
1531            let reboot_gateway_id = fedimint_core::secp256k1::PublicKey::from_str(&new_gw_ldk.gateway_id).expect("Could not convert public key");
1532
1533            if reboot_gateway_state == "Running" {
1534                info!(target: LOG_DEVIMINT, "LDK Gateway restarted, with auto-rejoin to federation");
1535                // Assert that the gateway info is the same as before the reboot
1536                assert_eq!(ldk_gateway_id, reboot_gateway_id);
1537                return Ok(());
1538            }
1539            Err(ControlFlow::Continue(anyhow!("gateway not running")))
1540        },
1541    )
1542    .await?;
1543
1544    info!(LOG_DEVIMINT, "gateway_reboot_test: success");
1545    Ok(())
1546}
1547
1548pub async fn do_try_create_and_pay_invoice(
1549    gw_lnd: &Gatewayd,
1550    client: &Client,
1551    gw_ldk: &Gatewayd,
1552) -> anyhow::Result<()> {
1553    // Verify that after the lightning node has restarted, the gateway
1554    // automatically reconnects and can query the lightning node
1555    // info again.
1556    poll("Waiting for info to succeed after restart", || async {
1557        gw_lnd
1558            .lightning_pubkey()
1559            .await
1560            .map_err(ControlFlow::Continue)?;
1561        Ok(())
1562    })
1563    .await?;
1564
1565    tracing::info!("Creating invoice....");
1566    let invoice = ln_invoice(
1567        client,
1568        Amount::from_msats(1000),
1569        "incoming-over-lnd-gw".to_string(),
1570        gw_lnd.gateway_id.clone(),
1571    )
1572    .await?
1573    .invoice;
1574
1575    match &gw_lnd.ln.ln_type() {
1576        LightningNodeType::Lnd => {
1577            // Pay the invoice using LDK
1578            gw_ldk
1579                .pay_invoice(Bolt11Invoice::from_str(&invoice).expect("Could not parse invoice"))
1580                .await?;
1581        }
1582        LightningNodeType::Ldk => {
1583            unimplemented!("do_try_create_and_pay_invoice not implemented for LDK yet");
1584        }
1585    }
1586    Ok(())
1587}
1588
1589async fn ln_pay(client: &Client, invoice: String, gw_id: String) -> anyhow::Result<String> {
1590    let value = cmd!(client, "ln-pay", invoice, "--gateway-id", gw_id,)
1591        .out_json()
1592        .await?;
1593    let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
1594    if fedimint_cli_version >= *VERSION_0_9_0_ALPHA {
1595        let outcome = serde_json::from_value::<LightningPaymentOutcome>(value)
1596            .expect("Could not deserialize Lightning payment outcome");
1597        match outcome {
1598            LightningPaymentOutcome::Success { preimage } => Ok(preimage),
1599            LightningPaymentOutcome::Failure { error_message } => {
1600                Err(anyhow!("Failed to pay lightning invoice: {error_message}"))
1601            }
1602        }
1603    } else {
1604        let operation_id = value["operation_id"]
1605            .as_str()
1606            .ok_or(anyhow!("Failed to pay invoice"))?
1607            .to_string();
1608        Ok(operation_id)
1609    }
1610}
1611
1612async fn ln_invoice(
1613    client: &Client,
1614    amount: Amount,
1615    description: String,
1616    gw_id: String,
1617) -> anyhow::Result<LnInvoiceResponse> {
1618    let ln_response_val = cmd!(
1619        client,
1620        "ln-invoice",
1621        "--amount",
1622        amount.msats,
1623        format!("--description='{description}'"),
1624        "--gateway-id",
1625        gw_id,
1626    )
1627    .out_json()
1628    .await?;
1629
1630    let ln_invoice_response: LnInvoiceResponse = serde_json::from_value(ln_response_val)?;
1631
1632    Ok(ln_invoice_response)
1633}
1634
1635async fn lnv2_receive(
1636    client: &Client,
1637    gateway: &str,
1638    amount: u64,
1639) -> anyhow::Result<(Bolt11Invoice, OperationId)> {
1640    Ok(serde_json::from_value::<(Bolt11Invoice, OperationId)>(
1641        cmd!(
1642            client,
1643            "module",
1644            "lnv2",
1645            "receive",
1646            amount,
1647            "--gateway",
1648            gateway
1649        )
1650        .out_json()
1651        .await?,
1652    )?)
1653}
1654
1655async fn lnv2_send(client: &Client, gateway: &String, invoice: &String) -> anyhow::Result<()> {
1656    let send_op = serde_json::from_value::<OperationId>(
1657        cmd!(
1658            client,
1659            "module",
1660            "lnv2",
1661            "send",
1662            invoice,
1663            "--gateway",
1664            gateway
1665        )
1666        .out_json()
1667        .await?,
1668    )?;
1669
1670    assert_eq!(
1671        cmd!(
1672            client,
1673            "module",
1674            "lnv2",
1675            "await-send",
1676            serde_json::to_string(&send_op)?.substring(1, 65)
1677        )
1678        .out_json()
1679        .await?,
1680        serde_json::to_value(FinalSendOperationState::Success).expect("JSON serialization failed"),
1681    );
1682
1683    Ok(())
1684}
1685
1686pub async fn reconnect_test(dev_fed: DevFed, process_mgr: &ProcessManager) -> Result<()> {
1687    log_binary_versions().await?;
1688
1689    let DevFed {
1690        bitcoind, mut fed, ..
1691    } = dev_fed;
1692
1693    bitcoind.mine_blocks(110).await?;
1694    fed.await_block_sync().await?;
1695    fed.await_all_peers().await?;
1696
1697    // test a peer missing out on epochs and needing to rejoin
1698    fed.terminate_server(0).await?;
1699    fed.mine_then_wait_blocks_sync(100).await?;
1700
1701    fed.start_server(process_mgr, 0).await?;
1702    fed.mine_then_wait_blocks_sync(100).await?;
1703    fed.await_all_peers().await?;
1704    info!(target: LOG_DEVIMINT, "Server 0 successfully rejoined!");
1705    fed.mine_then_wait_blocks_sync(100).await?;
1706
1707    // now test what happens if consensus needs to be restarted
1708    fed.terminate_server(1).await?;
1709    fed.mine_then_wait_blocks_sync(100).await?;
1710    fed.terminate_server(2).await?;
1711    fed.terminate_server(3).await?;
1712
1713    fed.start_server(process_mgr, 1).await?;
1714    fed.start_server(process_mgr, 2).await?;
1715    fed.start_server(process_mgr, 3).await?;
1716
1717    fed.await_all_peers().await?;
1718
1719    info!(target: LOG_DEVIMINT, "fm success: reconnect-test");
1720    Ok(())
1721}
1722
1723pub async fn recoverytool_test(dev_fed: DevFed) -> Result<()> {
1724    log_binary_versions().await?;
1725
1726    let DevFed { bitcoind, fed, .. } = dev_fed;
1727
1728    let data_dir = env::var(FM_DATA_DIR_ENV)?;
1729    let client = fed.new_joined_client("recoverytool-test-client").await?;
1730
1731    let mut fed_utxos_sats = HashSet::from([12_345_000, 23_456_000, 34_567_000]);
1732    let deposit_fees = fed.deposit_fees()?.msats / 1000;
1733    for sats in &fed_utxos_sats {
1734        // pegin_client automatically adds fees, so we need to counteract that
1735        fed.pegin_client(*sats - deposit_fees, &client).await?;
1736    }
1737
1738    async fn withdraw(
1739        client: &Client,
1740        bitcoind: &crate::external::Bitcoind,
1741        fed_utxos_sats: &mut HashSet<u64>,
1742    ) -> Result<()> {
1743        let withdrawal_address = bitcoind.get_new_address().await?;
1744        let withdraw_res = cmd!(
1745            client,
1746            "withdraw",
1747            "--address",
1748            &withdrawal_address,
1749            "--amount",
1750            "5000 sat"
1751        )
1752        .out_json()
1753        .await?;
1754
1755        let fees_sat = withdraw_res["fees_sat"]
1756            .as_u64()
1757            .expect("withdrawal should contain fees");
1758        let txid: Txid = withdraw_res["txid"]
1759            .as_str()
1760            .expect("withdrawal should contain txid string")
1761            .parse()
1762            .expect("txid should be parsable");
1763        let tx_hex = bitcoind.poll_get_transaction(txid).await?;
1764
1765        let tx = bitcoin::Transaction::consensus_decode_hex(&tx_hex, &ModuleRegistry::default())?;
1766        assert_eq!(tx.input.len(), 1);
1767        assert_eq!(tx.output.len(), 2);
1768
1769        let change_output = tx
1770            .output
1771            .iter()
1772            .find(|o| o.to_owned().script_pubkey != withdrawal_address.script_pubkey())
1773            .expect("withdrawal must have change output");
1774        assert!(fed_utxos_sats.insert(change_output.value.to_sat()));
1775
1776        // Remove the utxo consumed from the withdrawal tx
1777        let total_output_sats = tx.output.iter().map(|o| o.value.to_sat()).sum::<u64>();
1778        let input_sats = total_output_sats + fees_sat;
1779        assert!(fed_utxos_sats.remove(&input_sats));
1780
1781        Ok(())
1782    }
1783
1784    // Initiate multiple withdrawals in a session to verify the recoverytool
1785    // recognizes change outputs
1786    for _ in 0..2 {
1787        withdraw(&client, &bitcoind, &mut fed_utxos_sats).await?;
1788    }
1789
1790    let total_fed_sats = fed_utxos_sats.iter().sum::<u64>();
1791    fed.finalize_mempool_tx().await?;
1792
1793    // We are done transacting and save the current session id so we can wait for
1794    // the next session later on. We already save it here so that if in the meantime
1795    // a session is generated we don't wait for another.
1796    let last_tx_session = client.get_session_count().await?;
1797
1798    info!("Recovering using utxos method");
1799    let output = cmd!(
1800        crate::util::Recoverytool,
1801        "--cfg",
1802        "{data_dir}/fedimintd-default-0",
1803        "utxos",
1804        "--db",
1805        "{data_dir}/fedimintd-default-0/database"
1806    )
1807    .env(FM_PASSWORD_ENV, "pass")
1808    .out_json()
1809    .await?;
1810    let outputs = output.as_array().context("expected an array")?;
1811    assert_eq!(outputs.len(), fed_utxos_sats.len());
1812
1813    assert_eq!(
1814        outputs
1815            .iter()
1816            .map(|o| o["amount_sat"].as_u64().unwrap())
1817            .collect::<HashSet<_>>(),
1818        fed_utxos_sats
1819    );
1820    let utxos_descriptors = outputs
1821        .iter()
1822        .map(|o| o["descriptor"].as_str().unwrap())
1823        .collect::<HashSet<_>>();
1824
1825    debug!(target: LOG_DEVIMINT, ?utxos_descriptors, "recoverytool descriptors using UTXOs method");
1826
1827    let descriptors_json = serde_json::value::to_raw_value(&serde_json::Value::Array(vec![
1828        serde_json::Value::Array(
1829            utxos_descriptors
1830                .iter()
1831                .map(|d| {
1832                    json!({
1833                        "desc": d,
1834                        "timestamp": 0,
1835                    })
1836                })
1837                .collect(),
1838        ),
1839    ]))?;
1840    info!("Getting wallet balances before import");
1841    let bitcoin_client = bitcoind.wallet_client().await?;
1842    let balances_before = bitcoin_client.get_balances().await?;
1843    info!("Importing descriptors into bitcoin wallet");
1844    let request = bitcoin_client
1845        .get_jsonrpc_client()
1846        .build_request("importdescriptors", Some(&descriptors_json));
1847    let response = block_in_place(|| bitcoin_client.get_jsonrpc_client().send_request(request))?;
1848    response.check_error()?;
1849    info!("Getting wallet balances after import");
1850    let balances_after = bitcoin_client.get_balances().await?;
1851    let diff = balances_after.mine.immature + balances_after.mine.trusted
1852        - balances_before.mine.immature
1853        - balances_before.mine.trusted;
1854
1855    // We need to wait for a session to be generated to make sure we have the signed
1856    // session outcome in our DB. If there ever is another problem here: wait for
1857    // fedimintd-0 specifically to acknowledge the session switch. In practice this
1858    // should be sufficiently synchronous though.
1859    client.wait_session_outcome(last_tx_session).await?;
1860
1861    // Funds from descriptors should match the fed's utxos
1862    assert_eq!(diff.to_sat(), total_fed_sats);
1863    info!("Recovering using epochs method");
1864
1865    let outputs = cmd!(
1866        crate::util::Recoverytool,
1867        "--cfg",
1868        "{data_dir}/fedimintd-default-0",
1869        "epochs",
1870        "--db",
1871        "{data_dir}/fedimintd-default-0/database"
1872    )
1873    .env(FM_PASSWORD_ENV, "pass")
1874    .out_json()
1875    .await?
1876    .as_array()
1877    .context("expected an array")?
1878    .clone();
1879
1880    let epochs_descriptors = outputs
1881        .iter()
1882        .map(|o| o["descriptor"].as_str().unwrap())
1883        .collect::<HashSet<_>>();
1884
1885    // nosemgrep: use-err-formatting
1886    debug!(target: LOG_DEVIMINT, ?epochs_descriptors, "recoverytool descriptors using epochs method");
1887
1888    // Epochs method includes descriptors from spent outputs, so we only need to
1889    // verify the epochs method includes all available utxos
1890    for utxo_descriptor in utxos_descriptors {
1891        assert!(epochs_descriptors.contains(utxo_descriptor));
1892    }
1893    Ok(())
1894}
1895
1896pub async fn guardian_backup_test(dev_fed: DevFed, process_mgr: &ProcessManager) -> Result<()> {
1897    const PEER_TO_TEST: u16 = 0;
1898
1899    log_binary_versions().await?;
1900
1901    let DevFed { mut fed, .. } = dev_fed;
1902
1903    fed.await_all_peers()
1904        .await
1905        .expect("Awaiting federation coming online failed");
1906
1907    let client = fed.new_joined_client("guardian-client").await?;
1908    let old_block_count = cmd!(
1909        client,
1910        "dev",
1911        "api",
1912        "--peer-id",
1913        PEER_TO_TEST.to_string(),
1914        "--module",
1915        "wallet",
1916        "block_count",
1917    )
1918    .out_json()
1919    .await?["value"]
1920        .as_u64()
1921        .expect("No block height returned");
1922
1923    let backup_res = cmd!(
1924        client,
1925        "--our-id",
1926        PEER_TO_TEST.to_string(),
1927        "--password",
1928        "pass",
1929        "admin",
1930        "guardian-config-backup"
1931    )
1932    .out_json()
1933    .await?;
1934    let backup_hex = backup_res["tar_archive_bytes"]
1935        .as_str()
1936        .expect("expected hex string");
1937    let backup_tar = hex::decode(backup_hex).expect("invalid hex");
1938
1939    let data_dir = fed
1940        .vars
1941        .get(&PEER_TO_TEST.into())
1942        .expect("peer not found")
1943        .FM_DATA_DIR
1944        .clone();
1945
1946    fed.terminate_server(PEER_TO_TEST.into())
1947        .await
1948        .expect("could not terminate fedimintd");
1949
1950    std::fs::remove_dir_all(&data_dir).expect("error deleting old datadir");
1951    std::fs::create_dir(&data_dir).expect("error creating new datadir");
1952
1953    let write_file = |name: &str, data: &[u8]| {
1954        let mut file = std::fs::File::options()
1955            .write(true)
1956            .create(true)
1957            .truncate(true)
1958            .open(data_dir.join(name))
1959            .expect("could not open file");
1960        file.write_all(data).expect("could not write file");
1961        file.flush().expect("could not flush file");
1962    };
1963
1964    write_file("backup.tar", &backup_tar);
1965    write_file(
1966        fedimint_server::config::io::PLAINTEXT_PASSWORD,
1967        "pass".as_bytes(),
1968    );
1969
1970    assert_eq!(
1971        std::process::Command::new("tar")
1972            .arg("-xf")
1973            .arg("backup.tar")
1974            .current_dir(data_dir)
1975            .spawn()
1976            .expect("error spawning tar")
1977            .wait()
1978            .expect("error extracting archive")
1979            .code(),
1980        Some(0),
1981        "tar failed"
1982    );
1983
1984    fed.start_server(process_mgr, PEER_TO_TEST.into())
1985        .await
1986        .expect("could not restart fedimintd");
1987
1988    poll("Peer catches up again", || async {
1989        let block_counts = all_peer_block_count(&client, fed.member_ids())
1990            .await
1991            .map_err(ControlFlow::Continue)?;
1992        let block_count = block_counts[&PeerId::from(PEER_TO_TEST)];
1993
1994        info!("Caught up to block {block_count} of at least {old_block_count} (counts={block_counts:?})");
1995
1996        if block_count < old_block_count {
1997            return Err(ControlFlow::Continue(anyhow!("Block count still behind")));
1998        }
1999
2000        Ok(())
2001    })
2002    .await
2003    .expect("Peer didn't rejoin federation");
2004
2005    Ok(())
2006}
2007
2008async fn peer_block_count(client: &Client, peer: PeerId) -> Result<u64> {
2009    cmd!(
2010        client,
2011        "dev",
2012        "api",
2013        "--peer-id",
2014        peer.to_string(),
2015        "--module",
2016        "wallet",
2017        "block_count",
2018    )
2019    .out_json()
2020    .await?["value"]
2021        .as_u64()
2022        .context("No block height returned")
2023}
2024
2025async fn all_peer_block_count(
2026    client: &Client,
2027    peers: impl Iterator<Item = PeerId>,
2028) -> Result<BTreeMap<PeerId, u64>> {
2029    let mut peer_heights = BTreeMap::new();
2030    for peer in peers {
2031        peer_heights.insert(peer, peer_block_count(client, peer).await?);
2032    }
2033    Ok(peer_heights)
2034}
2035
2036pub async fn cannot_replay_tx_test(dev_fed: DevFed) -> Result<()> {
2037    log_binary_versions().await?;
2038
2039    let DevFed { fed, .. } = dev_fed;
2040
2041    let client = fed.new_joined_client("cannot-replay-client").await?;
2042
2043    const CLIENT_START_AMOUNT: u64 = 10_000_000_000;
2044    const CLIENT_SPEND_AMOUNT: u64 = 5_000_000_000;
2045
2046    let initial_client_balance = client.balance().await?;
2047    assert_eq!(initial_client_balance, 0);
2048
2049    fed.pegin_client(CLIENT_START_AMOUNT / 1000, &client)
2050        .await?;
2051
2052    // Fork client before spending ecash so we can later attempt a double spend
2053    let double_spend_client = client.new_forked("double-spender").await?;
2054
2055    // Spend and reissue all ecash from the client
2056    let notes = cmd!(client, "spend", CLIENT_SPEND_AMOUNT)
2057        .out_json()
2058        .await?
2059        .get("notes")
2060        .expect("Output didn't contain e-cash notes")
2061        .as_str()
2062        .unwrap()
2063        .to_owned();
2064
2065    let client_post_spend_balance = client.balance().await?;
2066    crate::util::almost_equal(
2067        client_post_spend_balance,
2068        CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT,
2069        10_000,
2070    )
2071    .unwrap();
2072
2073    cmd!(client, "reissue", notes).out_json().await?;
2074    let client_post_reissue_balance = client.balance().await?;
2075    crate::util::almost_equal(client_post_reissue_balance, CLIENT_START_AMOUNT, 20_000).unwrap();
2076
2077    // Attempt to spend the same ecash from the forked client
2078    let double_spend_notes = cmd!(double_spend_client, "spend", CLIENT_SPEND_AMOUNT)
2079        .out_json()
2080        .await?
2081        .get("notes")
2082        .expect("Output didn't contain e-cash notes")
2083        .as_str()
2084        .unwrap()
2085        .to_owned();
2086
2087    let double_spend_client_post_spend_balance = double_spend_client.balance().await?;
2088    crate::util::almost_equal(
2089        double_spend_client_post_spend_balance,
2090        CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT,
2091        10_000,
2092    )
2093    .unwrap();
2094
2095    cmd!(double_spend_client, "reissue", double_spend_notes)
2096        .assert_error_contains("The transaction had an invalid input")
2097        .await?;
2098
2099    let double_spend_client_post_spend_balance = double_spend_client.balance().await?;
2100    crate::util::almost_equal(
2101        double_spend_client_post_spend_balance,
2102        CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT,
2103        10_000,
2104    )
2105    .unwrap();
2106
2107    Ok(())
2108}
2109
2110/// Test that client can init even when the federation is down
2111///
2112/// See <https://github.com/fedimint/fedimint/issues/6939>
2113pub async fn test_offline_client_initialization(
2114    dev_fed: DevFed,
2115    _process_mgr: &ProcessManager,
2116) -> Result<()> {
2117    log_binary_versions().await?;
2118
2119    let DevFed { mut fed, .. } = dev_fed;
2120
2121    // Ensure federation is properly initialized and all peers are online
2122    fed.await_all_peers().await?;
2123
2124    // Create and join a client while all servers are online
2125    let client = fed.new_joined_client("offline-test-client").await?;
2126
2127    // Verify client can get info while federation is online
2128    const INFO_COMMAND_TIMEOUT: Duration = Duration::from_secs(5);
2129    let online_info =
2130        fedimint_core::runtime::timeout(INFO_COMMAND_TIMEOUT, cmd!(client, "info").out_json())
2131            .await
2132            .context("Client info command timed out while federation was online")?
2133            .context("Client info command failed while federation was online")?;
2134    info!(target: LOG_DEVIMINT, "Client info while federation online: {:?}", online_info);
2135
2136    // Shutdown all federation servers
2137    info!(target: LOG_DEVIMINT, "Shutting down all federation servers...");
2138    fed.terminate_all_servers().await?;
2139
2140    // Wait a moment to ensure servers are fully shutdown
2141    fedimint_core::task::sleep_in_test("wait for federation shutdown", Duration::from_secs(2))
2142        .await;
2143
2144    // Test that client info command still works with all servers offline
2145    // This should work because client info doesn't require server communication
2146    // for basic federation metadata and local state
2147    info!(target: LOG_DEVIMINT, "Testing client info command with all servers offline...");
2148    let offline_info =
2149        fedimint_core::runtime::timeout(INFO_COMMAND_TIMEOUT, cmd!(client, "info").out_json())
2150            .await
2151            .context("Client info command timed out while federation was offline")?
2152            .context("Client info command failed while federation was offline")?;
2153
2154    info!(target: LOG_DEVIMINT, "Client info while federation offline: {:?}", offline_info);
2155
2156    Ok(())
2157}
2158
2159/// Test that client can detect federation config changes when servers restart
2160/// with new module configurations
2161///
2162/// This test starts a fresh federation, dumps the client config, then stops all
2163/// servers and modifies their configs by adding a new meta module instance. The
2164/// client should detect this configuration change after the servers restart.
2165pub async fn test_client_config_change_detection(
2166    dev_fed: DevFed,
2167    process_mgr: &ProcessManager,
2168) -> Result<()> {
2169    log_binary_versions().await?;
2170
2171    let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
2172    let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
2173
2174    if fedimint_cli_version < *VERSION_0_9_0_ALPHA {
2175        info!(target: LOG_DEVIMINT, "Skipping the test - fedimint-cli too old");
2176        return Ok(());
2177    }
2178
2179    if fedimintd_version < *VERSION_0_9_0_ALPHA {
2180        info!(target: LOG_DEVIMINT, "Skipping the test - fedimintd too old");
2181        return Ok(());
2182    }
2183
2184    let DevFed { mut fed, .. } = dev_fed;
2185    let peer_ids: Vec<_> = fed.member_ids().collect();
2186
2187    fed.await_all_peers().await?;
2188
2189    let client = fed.new_joined_client("config-change-test-client").await?;
2190
2191    info!(target: LOG_DEVIMINT, "Getting initial client configuration...");
2192    let initial_config = cmd!(client, "config")
2193        .out_json()
2194        .await
2195        .context("Failed to get initial client config")?;
2196
2197    info!(target: LOG_DEVIMINT, "Initial config modules: {:?}", initial_config["modules"].as_object().unwrap().keys().collect::<Vec<_>>());
2198
2199    let data_dir = env::var(FM_DATA_DIR_ENV)?;
2200    let config_dir = PathBuf::from(&data_dir);
2201
2202    // Shutdown all federation servers
2203    //
2204    // In prod. one would probably use a coordinated shutdown, just to be
2205    // careful, but since the change is only adding a new module that does
2206    // not submit CIs without user/admin interaction, there is
2207    // no way for the consensus to diverge.
2208    info!(target: LOG_DEVIMINT, "Shutting down all federation servers...");
2209    fed.terminate_all_servers().await?;
2210
2211    // Wait for servers to fully shutdown
2212    fedimint_core::task::sleep_in_test("wait for federation shutdown", Duration::from_secs(2))
2213        .await;
2214
2215    info!(target: LOG_DEVIMINT, "Modifying server configurations to add new meta module...");
2216    modify_server_configs(&config_dir, &peer_ids).await?;
2217
2218    // Restart all servers with modified configs
2219    info!(target: LOG_DEVIMINT, "Restarting all servers with modified configurations...");
2220    for peer_id in peer_ids {
2221        fed.start_server(process_mgr, peer_id.to_usize()).await?;
2222    }
2223
2224    // Wait for federation to stabilize
2225    info!(target: LOG_DEVIMINT, "Wait for peers to get back up");
2226    fed.await_all_peers().await?;
2227
2228    // Use fedimint-cli dev wait to let the client read the new config in background
2229    info!(target: LOG_DEVIMINT, "Waiting for client to fetch updated configuration...");
2230    cmd!(client, "dev", "wait", "3")
2231        .run()
2232        .await
2233        .context("Failed to wait for client config update")?;
2234
2235    // Test that client switched to the new config
2236    info!(target: LOG_DEVIMINT, "Testing client detection of configuration changes...");
2237    let updated_config = cmd!(client, "config")
2238        .out_json()
2239        .await
2240        .context("Failed to get updated client config")?;
2241
2242    info!(target: LOG_DEVIMINT, "Updated config modules: {:?}", updated_config["modules"].as_object().unwrap().keys().collect::<Vec<_>>());
2243
2244    // Verify that the configuration has changed (new meta module should be present)
2245    let initial_modules = initial_config["modules"].as_object().unwrap();
2246    let updated_modules = updated_config["modules"].as_object().unwrap();
2247
2248    anyhow::ensure!(
2249        updated_modules.len() > initial_modules.len(),
2250        "Expected more modules in updated config. Initial: {}, Updated: {}",
2251        initial_modules.len(),
2252        updated_modules.len()
2253    );
2254
2255    // Check if a new meta module was added
2256    let new_meta_module = updated_modules.iter().find(|(module_id, module_config)| {
2257        module_config["kind"].as_str() == Some("meta") && !initial_modules.contains_key(*module_id)
2258    });
2259
2260    let new_meta_module_id = new_meta_module
2261        .map(|(id, _)| id)
2262        .with_context(|| "Expected to find new meta module in updated configuration")?;
2263
2264    info!(target: LOG_DEVIMINT, "Found new meta module with id: {}", new_meta_module_id);
2265
2266    // Verify client operations still work with the new configuration
2267    info!(target: LOG_DEVIMINT, "Verifying client operations work with new configuration...");
2268    let final_info = cmd!(client, "info")
2269        .out_json()
2270        .await
2271        .context("Client info command failed with updated configuration")?;
2272
2273    info!(target: LOG_DEVIMINT, "Client successfully adapted to configuration changes: {:?}", final_info["federation_id"]);
2274
2275    Ok(())
2276}
2277
2278/// Modify server configuration files to add a new meta module instance
2279async fn modify_server_configs(config_dir: &Path, peer_ids: &[PeerId]) -> Result<()> {
2280    for &peer_id in peer_ids {
2281        modify_single_peer_config(config_dir, peer_id).await?;
2282    }
2283    Ok(())
2284}
2285
2286/// Modify configuration files for a single peer to add a new meta module
2287/// instance
2288async fn modify_single_peer_config(config_dir: &Path, peer_id: PeerId) -> Result<()> {
2289    use fedimint_aead::{encrypted_write, get_encryption_key};
2290    use fedimint_core::core::ModuleInstanceId;
2291    use fedimint_server::config::io::read_server_config;
2292    use serde_json::Value;
2293
2294    info!(target: LOG_DEVIMINT, %peer_id, "Modifying config for peer");
2295    let peer_dir = config_dir.join(format!("fedimintd-default-{}", peer_id.to_usize()));
2296
2297    // Read consensus config
2298    let consensus_config_path = peer_dir.join("consensus.json");
2299    let consensus_config_content = fs::read_to_string(&consensus_config_path)
2300        .await
2301        .with_context(|| format!("Failed to read consensus config for peer {peer_id}"))?;
2302
2303    let mut consensus_config: Value = serde_json::from_str(&consensus_config_content)
2304        .with_context(|| format!("Failed to parse consensus config for peer {peer_id}"))?;
2305
2306    // Read the encrypted private config using the server config reader
2307    let password = "pass"; // Default password used in devimint
2308    let server_config = read_server_config(password, &peer_dir)
2309        .with_context(|| format!("Failed to read server config for peer {peer_id}"))?;
2310
2311    // Find existing meta module in configs to use as template
2312    let consensus_config_modules = consensus_config["modules"]
2313        .as_object()
2314        .with_context(|| format!("No modules found in consensus config for peer {peer_id}"))?;
2315
2316    // Look for existing meta module to copy its configuration
2317    let existing_meta_consensus = consensus_config_modules
2318        .values()
2319        .find(|module_config| module_config["kind"].as_str() == Some("meta"));
2320
2321    let existing_meta_consensus = existing_meta_consensus
2322        .with_context(|| {
2323            format!("No existing meta module found in consensus config for peer {peer_id}")
2324        })?
2325        .clone();
2326
2327    // Find existing meta module in private config
2328    let existing_meta_instance_id = server_config
2329        .consensus
2330        .modules
2331        .iter()
2332        .find(|(_, config)| config.kind.as_str() == "meta")
2333        .map(|(id, _)| *id)
2334        .with_context(|| {
2335            format!("No existing meta module found in private config for peer {peer_id}")
2336        })?;
2337
2338    let existing_meta_private = server_config
2339        .private
2340        .modules
2341        .get(&existing_meta_instance_id)
2342        .with_context(|| format!("Failed to get existing meta private config for peer {peer_id}"))?
2343        .clone();
2344
2345    // Find the highest existing module ID for the new module
2346    let last_existing_module_id = consensus_config_modules
2347        .keys()
2348        .filter_map(|id| id.parse::<u32>().ok())
2349        .max()
2350        .unwrap_or(0);
2351
2352    let new_module_id = (last_existing_module_id + 1).to_string();
2353    let new_module_instance_id = ModuleInstanceId::from((last_existing_module_id + 1) as u16);
2354
2355    info!(
2356        "Adding new meta module with id {} for peer {} (copying existing meta module config)",
2357        new_module_id, peer_id
2358    );
2359
2360    // Add new meta module to consensus config by copying existing meta module
2361    if let Some(modules) = consensus_config["modules"].as_object_mut() {
2362        modules.insert(new_module_id.clone(), existing_meta_consensus);
2363    }
2364
2365    // Add new meta module to private config
2366    let mut updated_private_config = server_config.private.clone();
2367    updated_private_config
2368        .modules
2369        .insert(new_module_instance_id, existing_meta_private);
2370
2371    // Write back the modified consensus and client configs
2372    let updated_consensus_content = serde_json::to_string_pretty(&consensus_config)
2373        .with_context(|| format!("Failed to serialize consensus config for peer {peer_id}"))?;
2374
2375    write_overwrite_async(&consensus_config_path, updated_consensus_content)
2376        .await
2377        .with_context(|| format!("Failed to write consensus config for peer {peer_id}"))?;
2378
2379    // Write back the modified private config using direct encryption
2380    let salt = std::fs::read_to_string(peer_dir.join("private.salt"))
2381        .with_context(|| format!("Failed to read salt file for peer {peer_id}"))?;
2382    let key = get_encryption_key(password, &salt)
2383        .with_context(|| format!("Failed to get encryption key for peer {peer_id}"))?;
2384
2385    let private_config_bytes = serde_json::to_string(&updated_private_config)
2386        .with_context(|| format!("Failed to serialize private config for peer {peer_id}"))?
2387        .into_bytes();
2388
2389    // Remove the existing encrypted file first
2390    let encrypted_private_path = peer_dir.join("private.encrypt");
2391    if encrypted_private_path.exists() {
2392        std::fs::remove_file(&encrypted_private_path)
2393            .with_context(|| format!("Failed to remove old private config for peer {peer_id}"))?;
2394    }
2395
2396    encrypted_write(private_config_bytes, &key, encrypted_private_path)
2397        .with_context(|| format!("Failed to write encrypted private config for peer {peer_id}"))?;
2398
2399    info!("Successfully modified configs for peer {}", peer_id);
2400    Ok(())
2401}
2402
2403/// Tests the `admin auth` command that stores admin credentials in the client
2404/// database, allowing subsequent admin commands to run without --our-id and
2405/// --password.
2406pub async fn admin_auth_tests(dev_fed: DevFed) -> Result<()> {
2407    log_binary_versions().await?;
2408
2409    let DevFed { fed, .. } = dev_fed;
2410
2411    // Ensure all federation peers are ready before starting the test
2412    // This prevents flaky failures due to Iroh connection timeouts
2413    fed.await_all_peers().await?;
2414
2415    let client = fed.new_joined_client("admin-auth-test-client").await?;
2416
2417    let peer_id = 0;
2418
2419    info!(target: LOG_DEVIMINT, "Testing admin auth command stores credentials");
2420
2421    // First, store the admin credentials using the auth command
2422    // Use --no-verify to skip interactive verification in tests
2423    let auth_result = cmd!(
2424        client,
2425        "--our-id",
2426        &peer_id.to_string(),
2427        "--password",
2428        "pass",
2429        "admin",
2430        "auth",
2431        "--peer-id",
2432        &peer_id.to_string(),
2433        "--password",
2434        "pass",
2435        "--no-verify",
2436        "--force"
2437    )
2438    .out_json()
2439    .await
2440    .context("Admin auth command failed")?;
2441
2442    info!(target: LOG_DEVIMINT, ?auth_result, "Admin auth command completed");
2443
2444    // Verify the response contains expected fields
2445    assert_eq!(
2446        auth_result
2447            .get("peer_id")
2448            .and_then(serde_json::Value::as_u64),
2449        Some(peer_id as u64),
2450        "peer_id in response should match"
2451    );
2452    assert_eq!(
2453        auth_result
2454            .get("status")
2455            .and_then(serde_json::Value::as_str),
2456        Some("saved"),
2457        "status should be 'saved'"
2458    );
2459
2460    info!(target: LOG_DEVIMINT, "Testing that stored credentials are used automatically");
2461
2462    // Now run an admin command WITHOUT --our-id and --password
2463    // It should use the stored credentials automatically
2464    let status_result = cmd!(client, "admin", "status")
2465        .out_json()
2466        .await
2467        .context("Admin status command should succeed with stored credentials")?;
2468
2469    info!(target: LOG_DEVIMINT, ?status_result, "Admin status with stored credentials succeeded");
2470
2471    info!(target: LOG_DEVIMINT, "Testing that --force overwrites existing credentials");
2472
2473    // Test that --force allows overwriting
2474    let auth_result_force = cmd!(
2475        client,
2476        "--our-id",
2477        &peer_id.to_string(),
2478        "--password",
2479        "pass",
2480        "admin",
2481        "auth",
2482        "--peer-id",
2483        &peer_id.to_string(),
2484        "--password",
2485        "pass",
2486        "--no-verify",
2487        "--force"
2488    )
2489    .out_json()
2490    .await
2491    .context("Admin auth force overwrite failed")?;
2492
2493    assert_eq!(
2494        auth_result_force.get("status").and_then(|v| v.as_str()),
2495        Some("saved"),
2496        "Force overwrite should succeed"
2497    );
2498
2499    info!(target: LOG_DEVIMINT, "admin_auth_tests completed successfully");
2500
2501    Ok(())
2502}
2503
2504pub async fn test_guardian_password_change(
2505    dev_fed: DevFed,
2506    process_mgr: &ProcessManager,
2507) -> Result<()> {
2508    log_binary_versions().await?;
2509
2510    let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
2511    let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
2512
2513    if fedimint_cli_version < *VERSION_0_9_0_ALPHA {
2514        info!(target: LOG_DEVIMINT, "Skipping the test - fedimint-cli too old");
2515        return Ok(());
2516    }
2517
2518    if fedimintd_version < *VERSION_0_9_0_ALPHA {
2519        info!(target: LOG_DEVIMINT, "Skipping the test - fedimintd too old");
2520        return Ok(());
2521    }
2522
2523    let DevFed { mut fed, .. } = dev_fed;
2524    fed.await_all_peers().await?;
2525
2526    let client = fed.new_joined_client("config-change-test-client").await?;
2527
2528    let peer_id = 0;
2529    let data_dir: PathBuf = fed
2530        .vars
2531        .get(&peer_id)
2532        .expect("peer not found")
2533        .FM_DATA_DIR
2534        .clone();
2535    let file_exists = |file: &str| {
2536        let path = data_dir.join(file);
2537        path.exists()
2538    };
2539    let pre_password_file_exists = file_exists("password.secret");
2540
2541    info!(target: LOG_DEVIMINT, "Changing password");
2542    cmd!(
2543        client,
2544        "--our-id",
2545        &peer_id.to_string(),
2546        "--password",
2547        "pass",
2548        "admin",
2549        "change-password",
2550        "foobar"
2551    )
2552    .run()
2553    .await
2554    .context("Failed to change guardian password")?;
2555
2556    info!(target: LOG_DEVIMINT, "Waiting for fedimintd to be shut down");
2557    timeout(
2558        Duration::from_secs(30),
2559        fed.await_server_terminated(peer_id),
2560    )
2561    .await
2562    .context("Fedimintd didn't shut down in time after password change")??;
2563
2564    info!(target: LOG_DEVIMINT, "Restarting fedimintd");
2565    fed.start_server(process_mgr, peer_id).await?;
2566
2567    info!(target: LOG_DEVIMINT, "Wait for fedimintd to come online again");
2568    fed.await_peer(peer_id).await?;
2569
2570    info!(target: LOG_DEVIMINT, "Testing password change worked");
2571    cmd!(
2572        client,
2573        "--our-id",
2574        &peer_id.to_string(),
2575        "--password",
2576        "foobar",
2577        "admin",
2578        "backup-statistics"
2579    )
2580    .run()
2581    .await
2582    .context("Failed to run guardian command with new password")?;
2583
2584    assert!(!file_exists("private.bak"));
2585    assert!(!file_exists("password.bak"));
2586    assert!(!file_exists("private.new"));
2587    assert!(!file_exists("password.new"));
2588    assert_eq!(file_exists("password.secret"), pre_password_file_exists);
2589
2590    Ok(())
2591}
2592
2593#[derive(Subcommand)]
2594pub enum LatencyTest {
2595    Reissue,
2596    LnSend,
2597    LnReceive,
2598    FmPay,
2599    Restore,
2600}
2601
2602#[derive(Subcommand)]
2603pub enum UpgradeTest {
2604    Fedimintd {
2605        #[arg(long, trailing_var_arg = true, num_args=1..)]
2606        paths: Vec<PathBuf>,
2607    },
2608    FedimintCli {
2609        #[arg(long, trailing_var_arg = true, num_args=1..)]
2610        paths: Vec<PathBuf>,
2611    },
2612    Gatewayd {
2613        #[arg(long, trailing_var_arg = true, num_args=1..)]
2614        gatewayd_paths: Vec<PathBuf>,
2615        #[arg(long, trailing_var_arg = true, num_args=1..)]
2616        gateway_cli_paths: Vec<PathBuf>,
2617    },
2618}
2619
2620#[derive(Subcommand)]
2621pub enum TestCmd {
2622    /// `devfed` then checks the average latency of reissuing ecash, LN receive,
2623    /// and LN send
2624    LatencyTests {
2625        #[clap(subcommand)]
2626        r#type: LatencyTest,
2627
2628        #[arg(long, default_value = "10")]
2629        iterations: usize,
2630    },
2631    /// `devfed` then kills and restarts most of the Guardian nodes in a 4 node
2632    /// fedimint
2633    ReconnectTest,
2634    /// `devfed` then tests a bunch of the fedimint-cli commands
2635    CliTests,
2636    /// `devfed` then tests guardian metadata functionality
2637    GuardianMetadataTests,
2638    /// `devfed` then calls binary `fedimint-load-test-tool`. See
2639    /// `LoadTestArgs`.
2640    LoadTestToolTest,
2641    /// `devfed` then pegin LND Gateway. Kill the LN node,
2642    /// restart it, rejjoin fedimint and test payments still work
2643    LightningReconnectTest,
2644    /// `devfed` then reboot gateway daemon for both LDK and LND. Test
2645    /// afterward.
2646    GatewayRebootTest,
2647    /// `devfed` then tests if the recovery tool is able to do a basic recovery
2648    RecoverytoolTests,
2649    /// `devfed` then spawns faucet for wasm tests
2650    WasmTestSetup {
2651        #[arg(long, trailing_var_arg = true, allow_hyphen_values = true, num_args=1..)]
2652        exec: Option<Vec<ffi::OsString>>,
2653    },
2654    /// Restore guardian from downloaded backup
2655    GuardianBackup,
2656    /// `devfed` then tests that spent ecash cannot be double spent
2657    CannotReplayTransaction,
2658    /// Tests that client info commands work when all federation servers are
2659    /// offline
2660    TestOfflineClientInitialization,
2661    /// Tests that client can detect federation config changes when servers
2662    /// restart with new module configurations
2663    TestClientConfigChangeDetection,
2664    /// Tests that guardian password change works and the guardian can restart
2665    /// afterwards
2666    TestGuardianPasswordChange,
2667    /// `devfed` then tests admin auth credential storage
2668    TestAdminAuth,
2669    /// Test upgrade paths for a given binary
2670    UpgradeTests {
2671        #[clap(subcommand)]
2672        binary: UpgradeTest,
2673        #[arg(long)]
2674        lnv2: String,
2675    },
2676}
2677
2678pub async fn handle_command(cmd: TestCmd, common_args: CommonArgs) -> Result<()> {
2679    match cmd {
2680        TestCmd::WasmTestSetup { exec } => {
2681            let (process_mgr, task_group) = setup(common_args).await?;
2682            let main = {
2683                let task_group = task_group.clone();
2684                async move {
2685                    let dev_fed = dev_fed(&process_mgr).await?;
2686                    let gw_lnd = dev_fed.gw_lnd.clone();
2687                    let fed = dev_fed.fed.clone();
2688                    gw_lnd
2689                        .set_federation_routing_fee(dev_fed.fed.calculate_federation_id(), 0, 0)
2690                        .await?;
2691                    task_group.spawn_cancellable("faucet", async move {
2692                        if let Err(err) = crate::faucet::run(
2693                            &dev_fed,
2694                            format!("0.0.0.0:{}", process_mgr.globals.FM_PORT_FAUCET),
2695                            process_mgr.globals.FM_PORT_GW_LND,
2696                        )
2697                        .await
2698                        {
2699                            error!("Error spawning faucet: {err}");
2700                        }
2701                    });
2702                    try_join!(fed.pegin_gateways(30_000, vec![&gw_lnd]), async {
2703                        poll("waiting for faucet startup", || async {
2704                            TcpStream::connect(format!(
2705                                "127.0.0.1:{}",
2706                                process_mgr.globals.FM_PORT_FAUCET
2707                            ))
2708                            .await
2709                            .context("connect to faucet")
2710                            .map_err(ControlFlow::Continue)
2711                        })
2712                        .await?;
2713                        Ok(())
2714                    },)?;
2715                    if let Some(exec) = exec {
2716                        exec_user_command(exec).await?;
2717                        task_group.shutdown();
2718                    }
2719                    Ok::<_, anyhow::Error>(())
2720                }
2721            };
2722            cleanup_on_exit(main, task_group).await?;
2723        }
2724        TestCmd::LatencyTests { r#type, iterations } => {
2725            let (process_mgr, _) = setup(common_args).await?;
2726            let dev_fed = dev_fed(&process_mgr).await?;
2727            latency_tests(dev_fed, r#type, None, iterations, true).await?;
2728        }
2729        TestCmd::ReconnectTest => {
2730            let (process_mgr, _) = setup(common_args).await?;
2731            let dev_fed = dev_fed(&process_mgr).await?;
2732            reconnect_test(dev_fed, &process_mgr).await?;
2733        }
2734        TestCmd::CliTests => {
2735            let (process_mgr, _) = setup(common_args).await?;
2736            let dev_fed = dev_fed(&process_mgr).await?;
2737            cli_tests(dev_fed).await?;
2738        }
2739        TestCmd::GuardianMetadataTests => {
2740            let (process_mgr, _) = setup(common_args).await?;
2741            let dev_fed = dev_fed(&process_mgr).await?;
2742            guardian_metadata_tests(dev_fed).await?;
2743        }
2744        TestCmd::LoadTestToolTest => {
2745            // For the load test tool test, explicitly disable mint base fees
2746            unsafe { std::env::set_var(FM_DISABLE_BASE_FEES_ENV, "1") };
2747
2748            let (process_mgr, _) = setup(common_args).await?;
2749            let dev_fed = dev_fed(&process_mgr).await?;
2750            cli_load_test_tool_test(dev_fed).await?;
2751        }
2752        TestCmd::LightningReconnectTest => {
2753            let (process_mgr, _) = setup(common_args).await?;
2754            let dev_fed = dev_fed(&process_mgr).await?;
2755            lightning_gw_reconnect_test(dev_fed, &process_mgr).await?;
2756        }
2757        TestCmd::GatewayRebootTest => {
2758            let (process_mgr, _) = setup(common_args).await?;
2759            let dev_fed = dev_fed(&process_mgr).await?;
2760            gw_reboot_test(dev_fed, &process_mgr).await?;
2761        }
2762        TestCmd::RecoverytoolTests => {
2763            let (process_mgr, _) = setup(common_args).await?;
2764            let dev_fed = dev_fed(&process_mgr).await?;
2765            recoverytool_test(dev_fed).await?;
2766        }
2767        TestCmd::GuardianBackup => {
2768            let (process_mgr, _) = setup(common_args).await?;
2769            let dev_fed = dev_fed(&process_mgr).await?;
2770            guardian_backup_test(dev_fed, &process_mgr).await?;
2771        }
2772        TestCmd::CannotReplayTransaction => {
2773            let (process_mgr, _) = setup(common_args).await?;
2774            let dev_fed = dev_fed(&process_mgr).await?;
2775            cannot_replay_tx_test(dev_fed).await?;
2776        }
2777        TestCmd::TestOfflineClientInitialization => {
2778            let (process_mgr, _) = setup(common_args).await?;
2779            let dev_fed = dev_fed(&process_mgr).await?;
2780            test_offline_client_initialization(dev_fed, &process_mgr).await?;
2781        }
2782        TestCmd::TestClientConfigChangeDetection => {
2783            let (process_mgr, _) = setup(common_args).await?;
2784            let dev_fed = dev_fed(&process_mgr).await?;
2785            test_client_config_change_detection(dev_fed, &process_mgr).await?;
2786        }
2787        TestCmd::TestGuardianPasswordChange => {
2788            let (process_mgr, _) = setup(common_args).await?;
2789            let dev_fed = dev_fed(&process_mgr).await?;
2790            test_guardian_password_change(dev_fed, &process_mgr).await?;
2791        }
2792        TestCmd::TestAdminAuth => {
2793            // Check versions early before starting infrastructure
2794            let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
2795            let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
2796
2797            if fedimint_cli_version < *VERSION_0_11_0_ALPHA
2798                || fedimintd_version < *VERSION_0_11_0_ALPHA
2799            {
2800                info!(target: LOG_DEVIMINT, "Skipping admin_auth_tests - requires v0.11.0-alpha or later");
2801                return Ok(());
2802            }
2803
2804            let (process_mgr, _) = setup(common_args).await?;
2805            let dev_fed = dev_fed(&process_mgr).await?;
2806            admin_auth_tests(dev_fed).await?;
2807        }
2808        TestCmd::UpgradeTests { binary, lnv2 } => {
2809            // TODO: Audit that the environment access only happens in single-threaded code.
2810            unsafe { std::env::set_var(FM_ENABLE_MODULE_LNV2_ENV, lnv2) };
2811            let (process_mgr, _) = setup(common_args).await?;
2812            Box::pin(upgrade_tests(&process_mgr, binary)).await?;
2813        }
2814    }
2815    Ok(())
2816}