Skip to main content

devimint/
tests.rs

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