Skip to main content

devimint/
tests.rs

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