devimint/
tests.rs

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