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