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