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