Skip to main content

devimint/
tests.rs

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