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