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