devimint/
tests.rs

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