devimint/
tests.rs

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