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