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