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