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    drop(gw_lnd);
1275    drop(gw_ldk);
1276
1277    // Verify that making a payment while the gateways are down does not result in
1278    // funds being stuck
1279    info!("Making payment while gateway is down");
1280    let initial_client_balance = client.balance().await?;
1281    let invoice = gw_ldk_second.create_invoice(3000).await?;
1282    ln_pay(&client, invoice.to_string(), lnd_gateway_id)
1283        .await
1284        .expect_err("Expected ln-pay to return error because the gateway is not online");
1285    let new_client_balance = client.balance().await?;
1286    anyhow::ensure!(initial_client_balance == new_client_balance);
1287
1288    // Reboot gateways with the same Lightning node instances
1289    info!("Rebooting gateways...");
1290    let (new_gw_lnd, new_gw_ldk) = try_join!(
1291        Gatewayd::new(process_mgr, LightningNode::Lnd(lnd.clone())),
1292        Gatewayd::new(
1293            process_mgr,
1294            LightningNode::Ldk {
1295                name: gw_ldk_name,
1296                gw_port: gw_ldk_port,
1297                ldk_port: gw_lightning_port,
1298            }
1299        )
1300    )?;
1301
1302    let lnd_gateway_id: fedimint_core::secp256k1::PublicKey =
1303        serde_json::from_value(lnd_value["gateway_id"].clone())?;
1304
1305    poll(
1306        "Waiting for LND Gateway Running state after reboot",
1307        || async {
1308            let mut new_lnd_cmd = cmd!(new_gw_lnd, "info");
1309            let lnd_value = new_lnd_cmd.out_json().await.map_err(ControlFlow::Continue)?;
1310            let reboot_gateway_state: String = serde_json::from_value(lnd_value["gateway_state"].clone()).context("invalid gateway state").map_err(ControlFlow::Break)?;
1311            let reboot_gateway_id: fedimint_core::secp256k1::PublicKey =
1312        serde_json::from_value(lnd_value["gateway_id"].clone()).context("invalid gateway id").map_err(ControlFlow::Break)?;
1313
1314            if reboot_gateway_state == "Running" {
1315                info!(target: LOG_DEVIMINT, "LND Gateway restarted, with auto-rejoin to federation");
1316                // Assert that the gateway info is the same as before the reboot
1317                assert_eq!(lnd_gateway_id, reboot_gateway_id);
1318                return Ok(());
1319            }
1320            Err(ControlFlow::Continue(anyhow!("gateway not running")))
1321        },
1322    )
1323    .await?;
1324
1325    let ldk_gateway_id: fedimint_core::secp256k1::PublicKey =
1326        serde_json::from_value(ldk_value["gateway_id"].clone())?;
1327    poll(
1328        "Waiting for LDK Gateway Running state after reboot",
1329        || async {
1330            let mut new_ldk_cmd = cmd!(new_gw_ldk, "info");
1331            let ldk_value = new_ldk_cmd.out_json().await.map_err(ControlFlow::Continue)?;
1332            let reboot_gateway_state: String = serde_json::from_value(ldk_value["gateway_state"].clone()).context("invalid gateway state").map_err(ControlFlow::Break)?;
1333            let reboot_gateway_id: fedimint_core::secp256k1::PublicKey =
1334        serde_json::from_value(ldk_value["gateway_id"].clone()).context("invalid gateway id").map_err(ControlFlow::Break)?;
1335
1336            if reboot_gateway_state == "Running" {
1337                info!(target: LOG_DEVIMINT, "LDK Gateway restarted, with auto-rejoin to federation");
1338                // Assert that the gateway info is the same as before the reboot
1339                assert_eq!(ldk_gateway_id, reboot_gateway_id);
1340                return Ok(());
1341            }
1342            Err(ControlFlow::Continue(anyhow!("gateway not running")))
1343        },
1344    )
1345    .await?;
1346
1347    info!(LOG_DEVIMINT, "gateway_reboot_test: success");
1348    Ok(())
1349}
1350
1351pub async fn do_try_create_and_pay_invoice(
1352    gw_lnd: &Gatewayd,
1353    client: &Client,
1354    gw_ldk: &Gatewayd,
1355) -> anyhow::Result<()> {
1356    // Verify that after the lightning node has restarted, the gateway
1357    // automatically reconnects and can query the lightning node
1358    // info again.
1359    poll("Waiting for info to succeed after restart", || async {
1360        let lightning_pub_key = cmd!(gw_lnd, "info")
1361            .out_json()
1362            .await
1363            .map_err(ControlFlow::Continue)?
1364            .get("lightning_pub_key")
1365            .map(|ln_pk| {
1366                serde_json::from_value::<Option<String>>(ln_pk.clone())
1367                    .expect("could not parse lightning_pub_key")
1368            })
1369            .expect("missing lightning_pub_key");
1370
1371        poll_eq!(lightning_pub_key.is_some(), true)
1372    })
1373    .await?;
1374
1375    tracing::info!("Creating invoice....");
1376    let invoice = ln_invoice(
1377        client,
1378        Amount::from_msats(1000),
1379        "incoming-over-lnd-gw".to_string(),
1380        gw_lnd.gateway_id.clone(),
1381    )
1382    .await?
1383    .invoice;
1384
1385    match &gw_lnd.ln.ln_type() {
1386        LightningNodeType::Lnd => {
1387            // Pay the invoice using LDK
1388            gw_ldk
1389                .pay_invoice(Bolt11Invoice::from_str(&invoice).expect("Could not parse invoice"))
1390                .await?;
1391        }
1392        LightningNodeType::Ldk => {
1393            unimplemented!("do_try_create_and_pay_invoice not implemented for LDK yet");
1394        }
1395    }
1396    Ok(())
1397}
1398
1399async fn ln_pay(client: &Client, invoice: String, gw_id: String) -> anyhow::Result<String> {
1400    let value = cmd!(client, "ln-pay", invoice, "--gateway-id", gw_id,)
1401        .out_json()
1402        .await?;
1403    let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
1404    if fedimint_cli_version >= *VERSION_0_9_0_ALPHA {
1405        let outcome = serde_json::from_value::<LightningPaymentOutcome>(value)
1406            .expect("Could not deserialize Lightning payment outcome");
1407        match outcome {
1408            LightningPaymentOutcome::Success { preimage } => Ok(preimage),
1409            LightningPaymentOutcome::Failure { error_message } => {
1410                Err(anyhow!("Failed to pay lightning invoice: {error_message}"))
1411            }
1412        }
1413    } else {
1414        let operation_id = value["operation_id"]
1415            .as_str()
1416            .ok_or(anyhow!("Failed to pay invoice"))?
1417            .to_string();
1418        Ok(operation_id)
1419    }
1420}
1421
1422async fn ln_invoice(
1423    client: &Client,
1424    amount: Amount,
1425    description: String,
1426    gw_id: String,
1427) -> anyhow::Result<LnInvoiceResponse> {
1428    let ln_response_val = cmd!(
1429        client,
1430        "ln-invoice",
1431        "--amount",
1432        amount.msats,
1433        format!("--description='{description}'"),
1434        "--gateway-id",
1435        gw_id,
1436    )
1437    .out_json()
1438    .await?;
1439
1440    let ln_invoice_response: LnInvoiceResponse = serde_json::from_value(ln_response_val)?;
1441
1442    Ok(ln_invoice_response)
1443}
1444
1445async fn lnv2_receive(
1446    client: &Client,
1447    gateway: &str,
1448    amount: u64,
1449) -> anyhow::Result<(Bolt11Invoice, OperationId)> {
1450    Ok(serde_json::from_value::<(Bolt11Invoice, OperationId)>(
1451        cmd!(
1452            client,
1453            "module",
1454            "lnv2",
1455            "receive",
1456            amount,
1457            "--gateway",
1458            gateway
1459        )
1460        .out_json()
1461        .await?,
1462    )?)
1463}
1464
1465async fn lnv2_send(client: &Client, gateway: &String, invoice: &String) -> anyhow::Result<()> {
1466    let send_op = serde_json::from_value::<OperationId>(
1467        cmd!(
1468            client,
1469            "module",
1470            "lnv2",
1471            "send",
1472            invoice,
1473            "--gateway",
1474            gateway
1475        )
1476        .out_json()
1477        .await?,
1478    )?;
1479
1480    assert_eq!(
1481        cmd!(
1482            client,
1483            "module",
1484            "lnv2",
1485            "await-send",
1486            serde_json::to_string(&send_op)?.substring(1, 65)
1487        )
1488        .out_json()
1489        .await?,
1490        serde_json::to_value(FinalSendOperationState::Success).expect("JSON serialization failed"),
1491    );
1492
1493    Ok(())
1494}
1495
1496pub async fn reconnect_test(dev_fed: DevFed, process_mgr: &ProcessManager) -> Result<()> {
1497    log_binary_versions().await?;
1498
1499    let DevFed {
1500        bitcoind, mut fed, ..
1501    } = dev_fed;
1502
1503    bitcoind.mine_blocks(110).await?;
1504    fed.await_block_sync().await?;
1505    fed.await_all_peers().await?;
1506
1507    // test a peer missing out on epochs and needing to rejoin
1508    fed.terminate_server(0).await?;
1509    fed.mine_then_wait_blocks_sync(100).await?;
1510
1511    fed.start_server(process_mgr, 0).await?;
1512    fed.mine_then_wait_blocks_sync(100).await?;
1513    fed.await_all_peers().await?;
1514    info!(target: LOG_DEVIMINT, "Server 0 successfully rejoined!");
1515    fed.mine_then_wait_blocks_sync(100).await?;
1516
1517    // now test what happens if consensus needs to be restarted
1518    fed.terminate_server(1).await?;
1519    fed.mine_then_wait_blocks_sync(100).await?;
1520    fed.terminate_server(2).await?;
1521    fed.terminate_server(3).await?;
1522
1523    fed.start_server(process_mgr, 1).await?;
1524    fed.start_server(process_mgr, 2).await?;
1525    fed.start_server(process_mgr, 3).await?;
1526
1527    fed.await_all_peers().await?;
1528
1529    info!(target: LOG_DEVIMINT, "fm success: reconnect-test");
1530    Ok(())
1531}
1532
1533pub async fn recoverytool_test(dev_fed: DevFed) -> Result<()> {
1534    log_binary_versions().await?;
1535
1536    let DevFed { bitcoind, fed, .. } = dev_fed;
1537
1538    let data_dir = env::var(FM_DATA_DIR_ENV)?;
1539    let client = fed.new_joined_client("recoverytool-test-client").await?;
1540
1541    let mut fed_utxos_sats = HashSet::from([12_345_000, 23_456_000, 34_567_000]);
1542    let deposit_fees = fed.deposit_fees()?.msats / 1000;
1543    for sats in &fed_utxos_sats {
1544        // pegin_client automatically adds fees, so we need to counteract that
1545        fed.pegin_client(*sats - deposit_fees, &client).await?;
1546    }
1547
1548    async fn withdraw(
1549        client: &Client,
1550        bitcoind: &crate::external::Bitcoind,
1551        fed_utxos_sats: &mut HashSet<u64>,
1552    ) -> Result<()> {
1553        let withdrawal_address = bitcoind.get_new_address().await?;
1554        let withdraw_res = cmd!(
1555            client,
1556            "withdraw",
1557            "--address",
1558            &withdrawal_address,
1559            "--amount",
1560            "5000 sat"
1561        )
1562        .out_json()
1563        .await?;
1564
1565        let fees_sat = withdraw_res["fees_sat"]
1566            .as_u64()
1567            .expect("withdrawal should contain fees");
1568        let txid: Txid = withdraw_res["txid"]
1569            .as_str()
1570            .expect("withdrawal should contain txid string")
1571            .parse()
1572            .expect("txid should be parsable");
1573        let tx_hex = bitcoind.poll_get_transaction(txid).await?;
1574
1575        let tx = bitcoin::Transaction::consensus_decode_hex(&tx_hex, &ModuleRegistry::default())?;
1576        assert_eq!(tx.input.len(), 1);
1577        assert_eq!(tx.output.len(), 2);
1578
1579        let change_output = tx
1580            .output
1581            .iter()
1582            .find(|o| o.to_owned().script_pubkey != withdrawal_address.script_pubkey())
1583            .expect("withdrawal must have change output");
1584        assert!(fed_utxos_sats.insert(change_output.value.to_sat()));
1585
1586        // Remove the utxo consumed from the withdrawal tx
1587        let total_output_sats = tx.output.iter().map(|o| o.value.to_sat()).sum::<u64>();
1588        let input_sats = total_output_sats + fees_sat;
1589        assert!(fed_utxos_sats.remove(&input_sats));
1590
1591        Ok(())
1592    }
1593
1594    // Initiate multiple withdrawals in a session to verify the recoverytool
1595    // recognizes change outputs
1596    for _ in 0..2 {
1597        withdraw(&client, &bitcoind, &mut fed_utxos_sats).await?;
1598    }
1599
1600    let total_fed_sats = fed_utxos_sats.iter().sum::<u64>();
1601    fed.finalize_mempool_tx().await?;
1602
1603    // We are done transacting and save the current session id so we can wait for
1604    // the next session later on. We already save it here so that if in the meantime
1605    // a session is generated we don't wait for another.
1606    let last_tx_session = client.get_session_count().await?;
1607
1608    info!("Recovering using utxos method");
1609    let output = cmd!(
1610        crate::util::Recoverytool,
1611        "--cfg",
1612        "{data_dir}/fedimintd-default-0",
1613        "utxos",
1614        "--db",
1615        "{data_dir}/fedimintd-default-0/database"
1616    )
1617    .env(FM_PASSWORD_ENV, "pass")
1618    .out_json()
1619    .await?;
1620    let outputs = output.as_array().context("expected an array")?;
1621    assert_eq!(outputs.len(), fed_utxos_sats.len());
1622
1623    assert_eq!(
1624        outputs
1625            .iter()
1626            .map(|o| o["amount_sat"].as_u64().unwrap())
1627            .collect::<HashSet<_>>(),
1628        fed_utxos_sats
1629    );
1630    let utxos_descriptors = outputs
1631        .iter()
1632        .map(|o| o["descriptor"].as_str().unwrap())
1633        .collect::<HashSet<_>>();
1634
1635    debug!(target: LOG_DEVIMINT, ?utxos_descriptors, "recoverytool descriptors using UTXOs method");
1636
1637    let descriptors_json = serde_json::value::to_raw_value(&serde_json::Value::Array(vec![
1638        serde_json::Value::Array(
1639            utxos_descriptors
1640                .iter()
1641                .map(|d| {
1642                    json!({
1643                        "desc": d,
1644                        "timestamp": 0,
1645                    })
1646                })
1647                .collect(),
1648        ),
1649    ]))?;
1650    info!("Getting wallet balances before import");
1651    let bitcoin_client = bitcoind.wallet_client().await?;
1652    let balances_before = bitcoin_client.get_balances().await?;
1653    info!("Importing descriptors into bitcoin wallet");
1654    let request = bitcoin_client
1655        .get_jsonrpc_client()
1656        .build_request("importdescriptors", Some(&descriptors_json));
1657    let response = block_in_place(|| bitcoin_client.get_jsonrpc_client().send_request(request))?;
1658    response.check_error()?;
1659    info!("Getting wallet balances after import");
1660    let balances_after = bitcoin_client.get_balances().await?;
1661    let diff = balances_after.mine.immature + balances_after.mine.trusted
1662        - balances_before.mine.immature
1663        - balances_before.mine.trusted;
1664
1665    // We need to wait for a session to be generated to make sure we have the signed
1666    // session outcome in our DB. If there ever is another problem here: wait for
1667    // fedimintd-0 specifically to acknowledge the session switch. In practice this
1668    // should be sufficiently synchronous though.
1669    client.wait_session_outcome(last_tx_session).await?;
1670
1671    // Funds from descriptors should match the fed's utxos
1672    assert_eq!(diff.to_sat(), total_fed_sats);
1673    info!("Recovering using epochs method");
1674
1675    let outputs = cmd!(
1676        crate::util::Recoverytool,
1677        "--cfg",
1678        "{data_dir}/fedimintd-default-0",
1679        "epochs",
1680        "--db",
1681        "{data_dir}/fedimintd-default-0/database"
1682    )
1683    .env(FM_PASSWORD_ENV, "pass")
1684    .out_json()
1685    .await?
1686    .as_array()
1687    .context("expected an array")?
1688    .clone();
1689
1690    let epochs_descriptors = outputs
1691        .iter()
1692        .map(|o| o["descriptor"].as_str().unwrap())
1693        .collect::<HashSet<_>>();
1694
1695    // nosemgrep: use-err-formatting
1696    debug!(target: LOG_DEVIMINT, ?epochs_descriptors, "recoverytool descriptors using epochs method");
1697
1698    // Epochs method includes descriptors from spent outputs, so we only need to
1699    // verify the epochs method includes all available utxos
1700    for utxo_descriptor in utxos_descriptors {
1701        assert!(epochs_descriptors.contains(utxo_descriptor));
1702    }
1703    Ok(())
1704}
1705
1706pub async fn guardian_backup_test(dev_fed: DevFed, process_mgr: &ProcessManager) -> Result<()> {
1707    const PEER_TO_TEST: u16 = 0;
1708
1709    log_binary_versions().await?;
1710
1711    let DevFed { mut fed, .. } = dev_fed;
1712
1713    fed.await_all_peers()
1714        .await
1715        .expect("Awaiting federation coming online failed");
1716
1717    let client = fed.new_joined_client("guardian-client").await?;
1718    let old_block_count = cmd!(
1719        client,
1720        "dev",
1721        "api",
1722        "--peer-id",
1723        PEER_TO_TEST.to_string(),
1724        "--module",
1725        "wallet",
1726        "block_count",
1727    )
1728    .out_json()
1729    .await?["value"]
1730        .as_u64()
1731        .expect("No block height returned");
1732
1733    let backup_res = cmd!(
1734        client,
1735        "--our-id",
1736        PEER_TO_TEST.to_string(),
1737        "--password",
1738        "pass",
1739        "admin",
1740        "guardian-config-backup"
1741    )
1742    .out_json()
1743    .await?;
1744    let backup_hex = backup_res["tar_archive_bytes"]
1745        .as_str()
1746        .expect("expected hex string");
1747    let backup_tar = hex::decode(backup_hex).expect("invalid hex");
1748
1749    let data_dir = fed
1750        .vars
1751        .get(&PEER_TO_TEST.into())
1752        .expect("peer not found")
1753        .FM_DATA_DIR
1754        .clone();
1755
1756    fed.terminate_server(PEER_TO_TEST.into())
1757        .await
1758        .expect("could not terminate fedimintd");
1759
1760    std::fs::remove_dir_all(&data_dir).expect("error deleting old datadir");
1761    std::fs::create_dir(&data_dir).expect("error creating new datadir");
1762
1763    let write_file = |name: &str, data: &[u8]| {
1764        let mut file = std::fs::File::options()
1765            .write(true)
1766            .create(true)
1767            .truncate(true)
1768            .open(data_dir.join(name))
1769            .expect("could not open file");
1770        file.write_all(data).expect("could not write file");
1771        file.flush().expect("could not flush file");
1772    };
1773
1774    write_file("backup.tar", &backup_tar);
1775    write_file(
1776        fedimint_server::config::io::PLAINTEXT_PASSWORD,
1777        "pass".as_bytes(),
1778    );
1779
1780    assert_eq!(
1781        std::process::Command::new("tar")
1782            .arg("-xf")
1783            .arg("backup.tar")
1784            .current_dir(data_dir)
1785            .spawn()
1786            .expect("error spawning tar")
1787            .wait()
1788            .expect("error extracting archive")
1789            .code(),
1790        Some(0),
1791        "tar failed"
1792    );
1793
1794    fed.start_server(process_mgr, PEER_TO_TEST.into())
1795        .await
1796        .expect("could not restart fedimintd");
1797
1798    poll("Peer catches up again", || async {
1799        let block_counts = all_peer_block_count(&client, fed.member_ids())
1800            .await
1801            .map_err(ControlFlow::Continue)?;
1802        let block_count = block_counts[&PeerId::from(PEER_TO_TEST)];
1803
1804        info!("Caught up to block {block_count} of at least {old_block_count} (counts={block_counts:?})");
1805
1806        if block_count < old_block_count {
1807            return Err(ControlFlow::Continue(anyhow!("Block count still behind")));
1808        }
1809
1810        Ok(())
1811    })
1812    .await
1813    .expect("Peer didn't rejoin federation");
1814
1815    Ok(())
1816}
1817
1818async fn peer_block_count(client: &Client, peer: PeerId) -> Result<u64> {
1819    cmd!(
1820        client,
1821        "dev",
1822        "api",
1823        "--peer-id",
1824        peer.to_string(),
1825        "module_{LEGACY_HARDCODED_INSTANCE_ID_WALLET}_block_count",
1826    )
1827    .out_json()
1828    .await?["value"]
1829        .as_u64()
1830        .context("No block height returned")
1831}
1832
1833async fn all_peer_block_count(
1834    client: &Client,
1835    peers: impl Iterator<Item = PeerId>,
1836) -> Result<BTreeMap<PeerId, u64>> {
1837    let mut peer_heights = BTreeMap::new();
1838    for peer in peers {
1839        peer_heights.insert(peer, peer_block_count(client, peer).await?);
1840    }
1841    Ok(peer_heights)
1842}
1843
1844pub async fn cannot_replay_tx_test(dev_fed: DevFed) -> Result<()> {
1845    log_binary_versions().await?;
1846
1847    let DevFed { fed, .. } = dev_fed;
1848
1849    let client = fed.new_joined_client("cannot-replay-client").await?;
1850
1851    const CLIENT_START_AMOUNT: u64 = 10_000_000_000;
1852    const CLIENT_SPEND_AMOUNT: u64 = 5_000_000_000;
1853
1854    let initial_client_balance = client.balance().await?;
1855    assert_eq!(initial_client_balance, 0);
1856
1857    fed.pegin_client(CLIENT_START_AMOUNT / 1000, &client)
1858        .await?;
1859
1860    // Fork client before spending ecash so we can later attempt a double spend
1861    let double_spend_client = client.new_forked("double-spender").await?;
1862
1863    // Spend and reissue all ecash from the client
1864    let notes = cmd!(client, "spend", CLIENT_SPEND_AMOUNT)
1865        .out_json()
1866        .await?
1867        .get("notes")
1868        .expect("Output didn't contain e-cash notes")
1869        .as_str()
1870        .unwrap()
1871        .to_owned();
1872
1873    let client_post_spend_balance = client.balance().await?;
1874    crate::util::almost_equal(
1875        client_post_spend_balance,
1876        CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT,
1877        10_000,
1878    )
1879    .unwrap();
1880
1881    cmd!(client, "reissue", notes).out_json().await?;
1882    let client_post_reissue_balance = client.balance().await?;
1883    crate::util::almost_equal(client_post_reissue_balance, CLIENT_START_AMOUNT, 20_000).unwrap();
1884
1885    // Attempt to spend the same ecash from the forked client
1886    let double_spend_notes = cmd!(double_spend_client, "spend", CLIENT_SPEND_AMOUNT)
1887        .out_json()
1888        .await?
1889        .get("notes")
1890        .expect("Output didn't contain e-cash notes")
1891        .as_str()
1892        .unwrap()
1893        .to_owned();
1894
1895    let double_spend_client_post_spend_balance = double_spend_client.balance().await?;
1896    crate::util::almost_equal(
1897        double_spend_client_post_spend_balance,
1898        CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT,
1899        10_000,
1900    )
1901    .unwrap();
1902
1903    cmd!(double_spend_client, "reissue", double_spend_notes)
1904        .assert_error_contains("The transaction had an invalid input")
1905        .await?;
1906
1907    let double_spend_client_post_spend_balance = double_spend_client.balance().await?;
1908    crate::util::almost_equal(
1909        double_spend_client_post_spend_balance,
1910        CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT,
1911        10_000,
1912    )
1913    .unwrap();
1914
1915    Ok(())
1916}
1917
1918/// Test that client can init even when the federation is down
1919///
1920/// See <https://github.com/fedimint/fedimint/issues/6939>
1921pub async fn test_offline_client_initialization(
1922    dev_fed: DevFed,
1923    _process_mgr: &ProcessManager,
1924) -> Result<()> {
1925    log_binary_versions().await?;
1926
1927    let DevFed { mut fed, .. } = dev_fed;
1928
1929    // Ensure federation is properly initialized and all peers are online
1930    fed.await_all_peers().await?;
1931
1932    // Create and join a client while all servers are online
1933    let client = fed.new_joined_client("offline-test-client").await?;
1934
1935    // Verify client can get info while federation is online
1936    const INFO_COMMAND_TIMEOUT: Duration = Duration::from_secs(5);
1937    let online_info =
1938        fedimint_core::runtime::timeout(INFO_COMMAND_TIMEOUT, cmd!(client, "info").out_json())
1939            .await
1940            .context("Client info command timed out while federation was online")?
1941            .context("Client info command failed while federation was online")?;
1942    info!(target: LOG_DEVIMINT, "Client info while federation online: {:?}", online_info);
1943
1944    // Shutdown all federation servers
1945    info!(target: LOG_DEVIMINT, "Shutting down all federation servers...");
1946    fed.terminate_all_servers().await?;
1947
1948    // Wait a moment to ensure servers are fully shutdown
1949    fedimint_core::task::sleep_in_test("wait for federation shutdown", Duration::from_secs(2))
1950        .await;
1951
1952    // Test that client info command still works with all servers offline
1953    // This should work because client info doesn't require server communication
1954    // for basic federation metadata and local state
1955    info!(target: LOG_DEVIMINT, "Testing client info command with all servers offline...");
1956    let offline_info =
1957        fedimint_core::runtime::timeout(INFO_COMMAND_TIMEOUT, cmd!(client, "info").out_json())
1958            .await
1959            .context("Client info command timed out while federation was offline")?
1960            .context("Client info command failed while federation was offline")?;
1961
1962    info!(target: LOG_DEVIMINT, "Client info while federation offline: {:?}", offline_info);
1963
1964    Ok(())
1965}
1966
1967/// Test that client can detect federation config changes when servers restart
1968/// with new module configurations
1969///
1970/// This test starts a fresh federation, dumps the client config, then stops all
1971/// servers and modifies their configs by adding a new meta module instance. The
1972/// client should detect this configuration change after the servers restart.
1973pub async fn test_client_config_change_detection(
1974    dev_fed: DevFed,
1975    process_mgr: &ProcessManager,
1976) -> Result<()> {
1977    log_binary_versions().await?;
1978
1979    let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
1980    let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
1981
1982    if fedimint_cli_version < *VERSION_0_9_0_ALPHA {
1983        info!(target: LOG_DEVIMINT, "Skipping the test - fedimint-cli too old");
1984        return Ok(());
1985    }
1986
1987    if fedimintd_version < *VERSION_0_9_0_ALPHA {
1988        info!(target: LOG_DEVIMINT, "Skipping the test - fedimintd too old");
1989        return Ok(());
1990    }
1991
1992    let DevFed { mut fed, .. } = dev_fed;
1993    let peer_ids: Vec<_> = fed.member_ids().collect();
1994
1995    fed.await_all_peers().await?;
1996
1997    let client = fed.new_joined_client("config-change-test-client").await?;
1998
1999    info!(target: LOG_DEVIMINT, "Getting initial client configuration...");
2000    let initial_config = cmd!(client, "config")
2001        .out_json()
2002        .await
2003        .context("Failed to get initial client config")?;
2004
2005    info!(target: LOG_DEVIMINT, "Initial config modules: {:?}", initial_config["modules"].as_object().unwrap().keys().collect::<Vec<_>>());
2006
2007    let data_dir = env::var(FM_DATA_DIR_ENV)?;
2008    let config_dir = PathBuf::from(&data_dir);
2009
2010    // Shutdown all federation servers
2011    //
2012    // In prod. one would probably use a coordinated shutdown, just to be
2013    // careful, but since the change is only adding a new module that does
2014    // not submit CIs without user/admin interaction, there is
2015    // no way for the consensus to diverge.
2016    info!(target: LOG_DEVIMINT, "Shutting down all federation servers...");
2017    fed.terminate_all_servers().await?;
2018
2019    // Wait for servers to fully shutdown
2020    fedimint_core::task::sleep_in_test("wait for federation shutdown", Duration::from_secs(2))
2021        .await;
2022
2023    info!(target: LOG_DEVIMINT, "Modifying server configurations to add new meta module...");
2024    modify_server_configs(&config_dir, &peer_ids).await?;
2025
2026    // Restart all servers with modified configs
2027    info!(target: LOG_DEVIMINT, "Restarting all servers with modified configurations...");
2028    for peer_id in peer_ids {
2029        fed.start_server(process_mgr, peer_id.to_usize()).await?;
2030    }
2031
2032    // Wait for federation to stabilize
2033    info!(target: LOG_DEVIMINT, "Wait for peers to get back up");
2034    fed.await_all_peers().await?;
2035
2036    // Use fedimint-cli dev wait to let the client read the new config in background
2037    info!(target: LOG_DEVIMINT, "Waiting for client to fetch updated configuration...");
2038    cmd!(client, "dev", "wait", "3")
2039        .run()
2040        .await
2041        .context("Failed to wait for client config update")?;
2042
2043    // Test that client switched to the new config
2044    info!(target: LOG_DEVIMINT, "Testing client detection of configuration changes...");
2045    let updated_config = cmd!(client, "config")
2046        .out_json()
2047        .await
2048        .context("Failed to get updated client config")?;
2049
2050    info!(target: LOG_DEVIMINT, "Updated config modules: {:?}", updated_config["modules"].as_object().unwrap().keys().collect::<Vec<_>>());
2051
2052    // Verify that the configuration has changed (new meta module should be present)
2053    let initial_modules = initial_config["modules"].as_object().unwrap();
2054    let updated_modules = updated_config["modules"].as_object().unwrap();
2055
2056    anyhow::ensure!(
2057        updated_modules.len() > initial_modules.len(),
2058        "Expected more modules in updated config. Initial: {}, Updated: {}",
2059        initial_modules.len(),
2060        updated_modules.len()
2061    );
2062
2063    // Check if a new meta module was added
2064    let new_meta_module = updated_modules.iter().find(|(module_id, module_config)| {
2065        module_config["kind"].as_str() == Some("meta") && !initial_modules.contains_key(*module_id)
2066    });
2067
2068    let new_meta_module_id = new_meta_module
2069        .map(|(id, _)| id)
2070        .with_context(|| "Expected to find new meta module in updated configuration")?;
2071
2072    info!(target: LOG_DEVIMINT, "Found new meta module with id: {}", new_meta_module_id);
2073
2074    // Verify client operations still work with the new configuration
2075    info!(target: LOG_DEVIMINT, "Verifying client operations work with new configuration...");
2076    let final_info = cmd!(client, "info")
2077        .out_json()
2078        .await
2079        .context("Client info command failed with updated configuration")?;
2080
2081    info!(target: LOG_DEVIMINT, "Client successfully adapted to configuration changes: {:?}", final_info["federation_id"]);
2082
2083    Ok(())
2084}
2085
2086/// Modify server configuration files to add a new meta module instance
2087async fn modify_server_configs(config_dir: &Path, peer_ids: &[PeerId]) -> Result<()> {
2088    for &peer_id in peer_ids {
2089        modify_single_peer_config(config_dir, peer_id).await?;
2090    }
2091    Ok(())
2092}
2093
2094/// Modify configuration files for a single peer to add a new meta module
2095/// instance
2096async fn modify_single_peer_config(config_dir: &Path, peer_id: PeerId) -> Result<()> {
2097    use fedimint_aead::{encrypted_write, get_encryption_key};
2098    use fedimint_core::core::ModuleInstanceId;
2099    use fedimint_server::config::io::read_server_config;
2100    use serde_json::Value;
2101
2102    info!(target: LOG_DEVIMINT, %peer_id, "Modifying config for peer");
2103    let peer_dir = config_dir.join(format!("fedimintd-default-{}", peer_id.to_usize()));
2104
2105    // Read consensus config
2106    let consensus_config_path = peer_dir.join("consensus.json");
2107    let consensus_config_content = fs::read_to_string(&consensus_config_path)
2108        .await
2109        .with_context(|| format!("Failed to read consensus config for peer {peer_id}"))?;
2110
2111    let mut consensus_config: Value = serde_json::from_str(&consensus_config_content)
2112        .with_context(|| format!("Failed to parse consensus config for peer {peer_id}"))?;
2113
2114    // Read the encrypted private config using the server config reader
2115    let password = "pass"; // Default password used in devimint
2116    let server_config = read_server_config(password, &peer_dir)
2117        .with_context(|| format!("Failed to read server config for peer {peer_id}"))?;
2118
2119    // Find existing meta module in configs to use as template
2120    let consensus_config_modules = consensus_config["modules"]
2121        .as_object()
2122        .with_context(|| format!("No modules found in consensus config for peer {peer_id}"))?;
2123
2124    // Look for existing meta module to copy its configuration
2125    let existing_meta_consensus = consensus_config_modules
2126        .values()
2127        .find(|module_config| module_config["kind"].as_str() == Some("meta"));
2128
2129    let existing_meta_consensus = existing_meta_consensus
2130        .with_context(|| {
2131            format!("No existing meta module found in consensus config for peer {peer_id}")
2132        })?
2133        .clone();
2134
2135    // Find existing meta module in private config
2136    let existing_meta_instance_id = server_config
2137        .consensus
2138        .modules
2139        .iter()
2140        .find(|(_, config)| config.kind.as_str() == "meta")
2141        .map(|(id, _)| *id)
2142        .with_context(|| {
2143            format!("No existing meta module found in private config for peer {peer_id}")
2144        })?;
2145
2146    let existing_meta_private = server_config
2147        .private
2148        .modules
2149        .get(&existing_meta_instance_id)
2150        .with_context(|| format!("Failed to get existing meta private config for peer {peer_id}"))?
2151        .clone();
2152
2153    // Find the highest existing module ID for the new module
2154    let last_existing_module_id = consensus_config_modules
2155        .keys()
2156        .filter_map(|id| id.parse::<u32>().ok())
2157        .max()
2158        .unwrap_or(0);
2159
2160    let new_module_id = (last_existing_module_id + 1).to_string();
2161    let new_module_instance_id = ModuleInstanceId::from((last_existing_module_id + 1) as u16);
2162
2163    info!(
2164        "Adding new meta module with id {} for peer {} (copying existing meta module config)",
2165        new_module_id, peer_id
2166    );
2167
2168    // Add new meta module to consensus config by copying existing meta module
2169    if let Some(modules) = consensus_config["modules"].as_object_mut() {
2170        modules.insert(new_module_id.clone(), existing_meta_consensus);
2171    }
2172
2173    // Add new meta module to private config
2174    let mut updated_private_config = server_config.private.clone();
2175    updated_private_config
2176        .modules
2177        .insert(new_module_instance_id, existing_meta_private);
2178
2179    // Write back the modified consensus and client configs
2180    let updated_consensus_content = serde_json::to_string_pretty(&consensus_config)
2181        .with_context(|| format!("Failed to serialize consensus config for peer {peer_id}"))?;
2182
2183    write_overwrite_async(&consensus_config_path, updated_consensus_content)
2184        .await
2185        .with_context(|| format!("Failed to write consensus config for peer {peer_id}"))?;
2186
2187    // Write back the modified private config using direct encryption
2188    let salt = std::fs::read_to_string(peer_dir.join("private.salt"))
2189        .with_context(|| format!("Failed to read salt file for peer {peer_id}"))?;
2190    let key = get_encryption_key(password, &salt)
2191        .with_context(|| format!("Failed to get encryption key for peer {peer_id}"))?;
2192
2193    let private_config_bytes = serde_json::to_string(&updated_private_config)
2194        .with_context(|| format!("Failed to serialize private config for peer {peer_id}"))?
2195        .into_bytes();
2196
2197    // Remove the existing encrypted file first
2198    let encrypted_private_path = peer_dir.join("private.encrypt");
2199    if encrypted_private_path.exists() {
2200        std::fs::remove_file(&encrypted_private_path)
2201            .with_context(|| format!("Failed to remove old private config for peer {peer_id}"))?;
2202    }
2203
2204    encrypted_write(private_config_bytes, &key, encrypted_private_path)
2205        .with_context(|| format!("Failed to write encrypted private config for peer {peer_id}"))?;
2206
2207    info!("Successfully modified configs for peer {}", peer_id);
2208    Ok(())
2209}
2210
2211pub async fn test_guardian_password_change(
2212    dev_fed: DevFed,
2213    process_mgr: &ProcessManager,
2214) -> Result<()> {
2215    log_binary_versions().await?;
2216
2217    let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
2218    let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
2219
2220    if fedimint_cli_version < *VERSION_0_9_0_ALPHA {
2221        info!(target: LOG_DEVIMINT, "Skipping the test - fedimint-cli too old");
2222        return Ok(());
2223    }
2224
2225    if fedimintd_version < *VERSION_0_9_0_ALPHA {
2226        info!(target: LOG_DEVIMINT, "Skipping the test - fedimintd too old");
2227        return Ok(());
2228    }
2229
2230    let DevFed { mut fed, .. } = dev_fed;
2231    fed.await_all_peers().await?;
2232
2233    let client = fed.new_joined_client("config-change-test-client").await?;
2234
2235    let data_dir: PathBuf = fed
2236        .vars
2237        .get(&2)
2238        .expect("peer not found")
2239        .FM_DATA_DIR
2240        .clone();
2241    let file_exists = |file: &str| {
2242        let path = data_dir.join(file);
2243        path.exists()
2244    };
2245    let pre_password_file_exists = file_exists("password.secret");
2246
2247    info!(target: LOG_DEVIMINT, "Changing password");
2248    cmd!(
2249        client,
2250        "--our-id",
2251        "2",
2252        "--password",
2253        "pass",
2254        "admin",
2255        "change-password",
2256        "foobar"
2257    )
2258    .run()
2259    .await
2260    .context("Failed to change guardian password")?;
2261
2262    info!(target: LOG_DEVIMINT, "Waiting for fedimintd 2 to be shut down");
2263    timeout(Duration::from_secs(30), fed.await_server_terminated(2))
2264        .await
2265        .context("Fedimintd didn't shut down in time after password change")??;
2266
2267    info!(target: LOG_DEVIMINT, "Restarting fedimintd 2");
2268    fed.start_server(process_mgr, 2).await?;
2269
2270    info!(target: LOG_DEVIMINT, "Wait for fedimintd 2 to come online again");
2271    fed.await_peer(2).await?;
2272
2273    info!(target: LOG_DEVIMINT, "Testing password change worked");
2274    cmd!(
2275        client,
2276        "--our-id",
2277        "2",
2278        "--password",
2279        "foobar",
2280        "admin",
2281        "backup-statistics"
2282    )
2283    .run()
2284    .await
2285    .context("Failed to run guardian command with new password")?;
2286
2287    assert!(!file_exists("private.bak"));
2288    assert!(!file_exists("password.bak"));
2289    assert!(!file_exists("private.new"));
2290    assert!(!file_exists("password.new"));
2291    assert_eq!(file_exists("password.secret"), pre_password_file_exists);
2292
2293    Ok(())
2294}
2295
2296#[derive(Subcommand)]
2297pub enum LatencyTest {
2298    Reissue,
2299    LnSend,
2300    LnReceive,
2301    FmPay,
2302    Restore,
2303}
2304
2305#[derive(Subcommand)]
2306pub enum UpgradeTest {
2307    Fedimintd {
2308        #[arg(long, trailing_var_arg = true, num_args=1..)]
2309        paths: Vec<PathBuf>,
2310    },
2311    FedimintCli {
2312        #[arg(long, trailing_var_arg = true, num_args=1..)]
2313        paths: Vec<PathBuf>,
2314    },
2315    Gatewayd {
2316        #[arg(long, trailing_var_arg = true, num_args=1..)]
2317        gatewayd_paths: Vec<PathBuf>,
2318        #[arg(long, trailing_var_arg = true, num_args=1..)]
2319        gateway_cli_paths: Vec<PathBuf>,
2320    },
2321}
2322
2323#[derive(Subcommand)]
2324pub enum TestCmd {
2325    /// `devfed` then checks the average latency of reissuing ecash, LN receive,
2326    /// and LN send
2327    LatencyTests {
2328        #[clap(subcommand)]
2329        r#type: LatencyTest,
2330
2331        #[arg(long, default_value = "10")]
2332        iterations: usize,
2333    },
2334    /// `devfed` then kills and restarts most of the Guardian nodes in a 4 node
2335    /// fedimint
2336    ReconnectTest,
2337    /// `devfed` then tests a bunch of the fedimint-cli commands
2338    CliTests,
2339    /// `devfed` then calls binary `fedimint-load-test-tool`. See
2340    /// `LoadTestArgs`.
2341    LoadTestToolTest,
2342    /// `devfed` then pegin LND Gateway. Kill the LN node,
2343    /// restart it, rejjoin fedimint and test payments still work
2344    LightningReconnectTest,
2345    /// `devfed` then reboot gateway daemon for both LDK and LND. Test
2346    /// afterward.
2347    GatewayRebootTest,
2348    /// `devfed` then tests if the recovery tool is able to do a basic recovery
2349    RecoverytoolTests,
2350    /// `devfed` then spawns faucet for wasm tests
2351    WasmTestSetup {
2352        #[arg(long, trailing_var_arg = true, allow_hyphen_values = true, num_args=1..)]
2353        exec: Option<Vec<ffi::OsString>>,
2354    },
2355    /// Restore guardian from downloaded backup
2356    GuardianBackup,
2357    /// `devfed` then tests that spent ecash cannot be double spent
2358    CannotReplayTransaction,
2359    /// Tests that client info commands work when all federation servers are
2360    /// offline
2361    TestOfflineClientInitialization,
2362    /// Tests that client can detect federation config changes when servers
2363    /// restart with new module configurations
2364    TestClientConfigChangeDetection,
2365    /// Tests that guardian password change works and the guardian can restart
2366    /// afterwards
2367    TestGuardianPasswordChange,
2368    /// Test upgrade paths for a given binary
2369    UpgradeTests {
2370        #[clap(subcommand)]
2371        binary: UpgradeTest,
2372        #[arg(long)]
2373        lnv2: String,
2374    },
2375}
2376
2377pub async fn handle_command(cmd: TestCmd, common_args: CommonArgs) -> Result<()> {
2378    match cmd {
2379        TestCmd::WasmTestSetup { exec } => {
2380            let (process_mgr, task_group) = setup(common_args).await?;
2381            let main = {
2382                let task_group = task_group.clone();
2383                async move {
2384                    let dev_fed = dev_fed(&process_mgr).await?;
2385                    let gw_lnd = dev_fed.gw_lnd.clone();
2386                    let fed = dev_fed.fed.clone();
2387                    gw_lnd
2388                        .set_federation_routing_fee(dev_fed.fed.calculate_federation_id(), 0, 0)
2389                        .await?;
2390                    task_group.spawn_cancellable("faucet", async move {
2391                        if let Err(err) = crate::faucet::run(
2392                            &dev_fed,
2393                            format!("0.0.0.0:{}", process_mgr.globals.FM_PORT_FAUCET),
2394                            process_mgr.globals.FM_PORT_GW_LND,
2395                        )
2396                        .await
2397                        {
2398                            error!("Error spawning faucet: {err}");
2399                        }
2400                    });
2401                    try_join!(fed.pegin_gateways(30_000, vec![&gw_lnd]), async {
2402                        poll("waiting for faucet startup", || async {
2403                            TcpStream::connect(format!(
2404                                "127.0.0.1:{}",
2405                                process_mgr.globals.FM_PORT_FAUCET
2406                            ))
2407                            .await
2408                            .context("connect to faucet")
2409                            .map_err(ControlFlow::Continue)
2410                        })
2411                        .await?;
2412                        Ok(())
2413                    },)?;
2414                    if let Some(exec) = exec {
2415                        exec_user_command(exec).await?;
2416                        task_group.shutdown();
2417                    }
2418                    Ok::<_, anyhow::Error>(())
2419                }
2420            };
2421            cleanup_on_exit(main, task_group).await?;
2422        }
2423        TestCmd::LatencyTests { r#type, iterations } => {
2424            let (process_mgr, _) = setup(common_args).await?;
2425            let dev_fed = dev_fed(&process_mgr).await?;
2426            latency_tests(dev_fed, r#type, None, iterations, true).await?;
2427        }
2428        TestCmd::ReconnectTest => {
2429            let (process_mgr, _) = setup(common_args).await?;
2430            let dev_fed = dev_fed(&process_mgr).await?;
2431            reconnect_test(dev_fed, &process_mgr).await?;
2432        }
2433        TestCmd::CliTests => {
2434            let (process_mgr, _) = setup(common_args).await?;
2435            let dev_fed = dev_fed(&process_mgr).await?;
2436            cli_tests(dev_fed).await?;
2437        }
2438        TestCmd::LoadTestToolTest => {
2439            // For the load test tool test, explicitly disable mint base fees
2440            unsafe { std::env::set_var(FM_DISABLE_BASE_FEES_ENV, "1") };
2441
2442            let (process_mgr, _) = setup(common_args).await?;
2443            let dev_fed = dev_fed(&process_mgr).await?;
2444            cli_load_test_tool_test(dev_fed).await?;
2445        }
2446        TestCmd::LightningReconnectTest => {
2447            let (process_mgr, _) = setup(common_args).await?;
2448            let dev_fed = dev_fed(&process_mgr).await?;
2449            lightning_gw_reconnect_test(dev_fed, &process_mgr).await?;
2450        }
2451        TestCmd::GatewayRebootTest => {
2452            let (process_mgr, _) = setup(common_args).await?;
2453            let dev_fed = dev_fed(&process_mgr).await?;
2454            gw_reboot_test(dev_fed, &process_mgr).await?;
2455        }
2456        TestCmd::RecoverytoolTests => {
2457            let (process_mgr, _) = setup(common_args).await?;
2458            let dev_fed = dev_fed(&process_mgr).await?;
2459            recoverytool_test(dev_fed).await?;
2460        }
2461        TestCmd::GuardianBackup => {
2462            let (process_mgr, _) = setup(common_args).await?;
2463            let dev_fed = dev_fed(&process_mgr).await?;
2464            guardian_backup_test(dev_fed, &process_mgr).await?;
2465        }
2466        TestCmd::CannotReplayTransaction => {
2467            let (process_mgr, _) = setup(common_args).await?;
2468            let dev_fed = dev_fed(&process_mgr).await?;
2469            cannot_replay_tx_test(dev_fed).await?;
2470        }
2471        TestCmd::TestOfflineClientInitialization => {
2472            let (process_mgr, _) = setup(common_args).await?;
2473            let dev_fed = dev_fed(&process_mgr).await?;
2474            test_offline_client_initialization(dev_fed, &process_mgr).await?;
2475        }
2476        TestCmd::TestClientConfigChangeDetection => {
2477            let (process_mgr, _) = setup(common_args).await?;
2478            let dev_fed = dev_fed(&process_mgr).await?;
2479            test_client_config_change_detection(dev_fed, &process_mgr).await?;
2480        }
2481        TestCmd::TestGuardianPasswordChange => {
2482            let (process_mgr, _) = setup(common_args).await?;
2483            let dev_fed = dev_fed(&process_mgr).await?;
2484            test_guardian_password_change(dev_fed, &process_mgr).await?;
2485        }
2486        TestCmd::UpgradeTests { binary, lnv2 } => {
2487            // TODO: Audit that the environment access only happens in single-threaded code.
2488            unsafe { std::env::set_var(FM_ENABLE_MODULE_LNV2_ENV, lnv2) };
2489            let (process_mgr, _) = setup(common_args).await?;
2490            Box::pin(upgrade_tests(&process_mgr, binary)).await?;
2491        }
2492    }
2493    Ok(())
2494}