1use std::collections::{BTreeMap, HashSet};
2use std::io::Write;
3use std::ops::ControlFlow;
4use std::path::{Path, PathBuf};
5use std::str::FromStr;
6use std::time::{Duration, Instant};
7use std::{env, ffi};
8
9use anyhow::{Context, Result, anyhow, bail};
10use bitcoin::Txid;
11use clap::Subcommand;
12use fedimint_core::core::LEGACY_HARDCODED_INSTANCE_ID_WALLET;
13use fedimint_core::encoding::{Decodable, Encodable};
14use fedimint_core::envs::{FM_ENABLE_MODULE_LNV2_ENV, is_env_var_set};
15use fedimint_core::module::registry::ModuleRegistry;
16use fedimint_core::net::api_announcement::SignedApiAnnouncement;
17use fedimint_core::task::block_in_place;
18use fedimint_core::util::backoff_util::aggressive_backoff;
19use fedimint_core::util::retry;
20use fedimint_core::{Amount, PeerId};
21use fedimint_ln_client::cli::LnInvoiceResponse;
22use fedimint_ln_server::common::lightning_invoice::Bolt11Invoice;
23use fedimint_logging::LOG_DEVIMINT;
24use fedimint_testing_core::node_type::LightningNodeType;
25use futures::future::try_join_all;
26use serde_json::json;
27use tokio::net::TcpStream;
28use tokio::{fs, try_join};
29use tracing::{debug, error, info};
30
31use crate::cli::{CommonArgs, cleanup_on_exit, exec_user_command, setup};
32use crate::envs::{FM_DATA_DIR_ENV, FM_DEVIMINT_RUN_DEPRECATED_TESTS_ENV, FM_PASSWORD_ENV};
33use crate::federation::Client;
34use crate::gatewayd::LdkChainSource;
35use crate::util::{LoadTestTool, ProcessManager, poll};
36use crate::version_constants::{VERSION_0_6_0_ALPHA, VERSION_0_7_0_ALPHA};
37use crate::{DevFed, Gatewayd, LightningNode, Lnd, cmd, dev_fed, poll_eq};
38
39pub struct Stats {
40 pub min: Duration,
41 pub avg: Duration,
42 pub median: Duration,
43 pub p90: Duration,
44 pub max: Duration,
45 pub sum: Duration,
46}
47
48impl std::fmt::Display for Stats {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 write!(f, "min: {:.1}s", self.min.as_secs_f32())?;
51 write!(f, ", avg: {:.1}s", self.avg.as_secs_f32())?;
52 write!(f, ", median: {:.1}s", self.median.as_secs_f32())?;
53 write!(f, ", p90: {:.1}s", self.p90.as_secs_f32())?;
54 write!(f, ", max: {:.1}s", self.max.as_secs_f32())?;
55 write!(f, ", sum: {:.1}s", self.sum.as_secs_f32())?;
56 Ok(())
57 }
58}
59
60pub fn stats_for(mut v: Vec<Duration>) -> Stats {
61 assert!(!v.is_empty());
62 v.sort();
63 let n = v.len();
64 let min = v.first().unwrap().to_owned();
65 let max = v.iter().last().unwrap().to_owned();
66 let median = v[n / 2];
67 let sum: Duration = v.iter().sum();
68 let avg = sum / n as u32;
69 let p90 = v[(n as f32 * 0.9) as usize];
70 Stats {
71 min,
72 avg,
73 median,
74 p90,
75 max,
76 sum,
77 }
78}
79
80pub async fn log_binary_versions() -> Result<()> {
81 let fedimint_cli_version = cmd!(crate::util::get_fedimint_cli_path(), "--version")
82 .out_string()
83 .await?;
84 info!(?fedimint_cli_version);
85 let fedimint_cli_version_hash = cmd!(crate::util::get_fedimint_cli_path(), "version-hash")
86 .out_string()
87 .await?;
88 info!(?fedimint_cli_version_hash);
89 let gateway_cli_version = cmd!(crate::util::get_gateway_cli_path(), "--version")
90 .out_string()
91 .await?;
92 info!(?gateway_cli_version);
93 let gateway_cli_version_hash = cmd!(crate::util::get_gateway_cli_path(), "version-hash")
94 .out_string()
95 .await?;
96 info!(?gateway_cli_version_hash);
97 let fedimintd_version_hash = cmd!(crate::util::FedimintdCmd, "version-hash")
98 .out_string()
99 .await?;
100 info!(?fedimintd_version_hash);
101 let gatewayd_version_hash = cmd!(crate::util::Gatewayd, "version-hash")
102 .out_string()
103 .await?;
104 info!(?gatewayd_version_hash);
105 Ok(())
106}
107
108pub async fn latency_tests(
109 dev_fed: DevFed,
110 r#type: LatencyTest,
111 upgrade_clients: Option<&UpgradeClients>,
112 iterations: usize,
113 assert_thresholds: bool,
114) -> Result<()> {
115 log_binary_versions().await?;
116
117 let DevFed {
118 fed,
119 gw_lnd,
120 gw_ldk,
121 ..
122 } = dev_fed;
123
124 let max_p90_factor = 5.0;
125 let p90_median_factor = 10;
126
127 let client = match upgrade_clients {
128 Some(c) => match r#type {
129 LatencyTest::Reissue => c.reissue_client.clone(),
130 LatencyTest::LnSend => c.ln_send_client.clone(),
131 LatencyTest::LnReceive => c.ln_receive_client.clone(),
132 LatencyTest::FmPay => c.fm_pay_client.clone(),
133 LatencyTest::Restore => bail!("no reusable upgrade client for restore"),
134 },
135 None => fed.new_joined_client("latency-tests-client").await?,
136 };
137
138 let initial_balance_sats = 100_000_000;
139 fed.pegin_client(initial_balance_sats, &client).await?;
140
141 let lnd_gw_id = gw_lnd.gateway_id().await?;
142
143 match r#type {
144 LatencyTest::Reissue => {
145 info!("Testing latency of reissue");
146 let mut reissues = Vec::with_capacity(iterations);
147 let amount_per_iteration_msats =
148 ((initial_balance_sats * 1000 / iterations as u64).next_power_of_two() >> 1) - 1;
150 for _ in 0..iterations {
151 let notes = cmd!(client, "spend", amount_per_iteration_msats.to_string())
152 .out_json()
153 .await?["notes"]
154 .as_str()
155 .context("note must be a string")?
156 .to_owned();
157
158 let start_time = Instant::now();
159 cmd!(client, "reissue", notes).run().await?;
160 reissues.push(start_time.elapsed());
161 }
162 let reissue_stats = stats_for(reissues);
163 println!("### LATENCY REISSUE: {reissue_stats}");
164
165 if assert_thresholds {
166 assert!(reissue_stats.median < Duration::from_secs(10));
167 assert!(reissue_stats.p90 < reissue_stats.median * p90_median_factor);
168 assert!(
169 reissue_stats.max.as_secs_f64()
170 < reissue_stats.p90.as_secs_f64() * max_p90_factor
171 );
172 }
173 }
174 LatencyTest::LnSend => {
175 info!("Testing latency of ln send");
176 let mut ln_sends = Vec::with_capacity(iterations);
177 for _ in 0..iterations {
178 let invoice = gw_ldk.create_invoice(1_000_000).await?;
179 let start_time = Instant::now();
180 ln_pay(&client, invoice.to_string(), lnd_gw_id.clone(), false).await?;
181 gw_ldk
182 .wait_bolt11_invoice(invoice.payment_hash().consensus_encode_to_vec())
183 .await?;
184 ln_sends.push(start_time.elapsed());
185 }
186 let ln_sends_stats = stats_for(ln_sends);
187 println!("### LATENCY LN SEND: {ln_sends_stats}");
188
189 if assert_thresholds {
190 assert!(ln_sends_stats.median < Duration::from_secs(10));
191 assert!(ln_sends_stats.p90 < ln_sends_stats.median * p90_median_factor);
192 assert!(
193 ln_sends_stats.max.as_secs_f64()
194 < ln_sends_stats.p90.as_secs_f64() * max_p90_factor
195 );
196 }
197 }
198 LatencyTest::LnReceive => {
199 info!("Testing latency of ln receive");
200 let mut ln_receives = Vec::with_capacity(iterations);
201
202 let invoice = gw_ldk.create_invoice(10_000_000).await?;
204 ln_pay(&client, invoice.to_string(), lnd_gw_id.clone(), false).await?;
205
206 for _ in 0..iterations {
207 let invoice = ln_invoice(
208 &client,
209 Amount::from_msats(100_000),
210 "latency-over-lnd-gw".to_string(),
211 lnd_gw_id.clone(),
212 )
213 .await?
214 .invoice;
215
216 let start_time = Instant::now();
217 gw_ldk
218 .pay_invoice(
219 Bolt11Invoice::from_str(&invoice).expect("Could not parse invoice"),
220 )
221 .await?;
222 ln_receives.push(start_time.elapsed());
223 }
224 let ln_receives_stats = stats_for(ln_receives);
225 println!("### LATENCY LN RECV: {ln_receives_stats}");
226
227 if assert_thresholds {
228 assert!(ln_receives_stats.median < Duration::from_secs(10));
229 assert!(ln_receives_stats.p90 < ln_receives_stats.median * p90_median_factor);
230 assert!(
231 ln_receives_stats.max.as_secs_f64()
232 < ln_receives_stats.p90.as_secs_f64() * max_p90_factor
233 );
234 }
235 }
236 LatencyTest::FmPay => {
237 info!("Testing latency of internal payments within a federation");
238 let mut fm_internal_pay = Vec::with_capacity(iterations);
239 let sender = fed.new_joined_client("internal-swap-sender").await?;
240 fed.pegin_client(10_000_000, &sender).await?;
241 for _ in 0..iterations {
242 let recv = cmd!(
243 client,
244 "ln-invoice",
245 "--amount=1000000msat",
246 "--description=internal-swap-invoice",
247 "--force-internal"
248 )
249 .out_json()
250 .await?;
251
252 let invoice = recv["invoice"]
253 .as_str()
254 .context("invoice must be string")?
255 .to_owned();
256 let recv_op = recv["operation_id"]
257 .as_str()
258 .context("operation id must be string")?
259 .to_owned();
260
261 let start_time = Instant::now();
262 cmd!(sender, "ln-pay", invoice, "--force-internal")
263 .run()
264 .await?;
265
266 cmd!(client, "await-invoice", recv_op).run().await?;
267 fm_internal_pay.push(start_time.elapsed());
268 }
269 let fm_pay_stats = stats_for(fm_internal_pay);
270
271 println!("### LATENCY FM PAY: {fm_pay_stats}");
272
273 if assert_thresholds {
274 assert!(fm_pay_stats.median < Duration::from_secs(15));
275 assert!(fm_pay_stats.p90 < fm_pay_stats.median * p90_median_factor);
276 assert!(
277 fm_pay_stats.max.as_secs_f64()
278 < fm_pay_stats.p90.as_secs_f64() * max_p90_factor
279 );
280 }
281 }
282 LatencyTest::Restore => {
283 info!("Testing latency of restore");
284 let backup_secret = cmd!(client, "print-secret").out_json().await?["secret"]
285 .as_str()
286 .map(ToOwned::to_owned)
287 .unwrap();
288 if !is_env_var_set(FM_DEVIMINT_RUN_DEPRECATED_TESTS_ENV) {
289 info!("Skipping tests, as in previous versions restore was very slow to test");
290 return Ok(());
291 }
292
293 let start_time = Instant::now();
294 let restore_client = Client::create("restore").await?;
295 cmd!(
296 restore_client,
297 "restore",
298 "--mnemonic",
299 &backup_secret,
300 "--invite-code",
301 fed.invite_code()?
302 )
303 .run()
304 .await?;
305 let restore_time = start_time.elapsed();
306
307 println!("### LATENCY RESTORE: {restore_time:?}");
308
309 if assert_thresholds {
310 if crate::util::is_backwards_compatibility_test() {
311 assert!(restore_time < Duration::from_secs(160));
312 } else {
313 assert!(restore_time < Duration::from_secs(30));
314 }
315 }
316 }
317 }
318
319 Ok(())
320}
321
322#[allow(clippy::struct_field_names)]
323pub struct UpgradeClients {
325 reissue_client: Client,
326 ln_send_client: Client,
327 ln_receive_client: Client,
328 fm_pay_client: Client,
329}
330
331async fn stress_test_fed(dev_fed: &DevFed, clients: Option<&UpgradeClients>) -> anyhow::Result<()> {
332 use futures::FutureExt;
333
334 let assert_thresholds = false;
337
338 let iterations = 1;
341
342 let restore_test = if clients.is_some() {
345 futures::future::ok(()).right_future()
346 } else {
347 latency_tests(
348 dev_fed.clone(),
349 LatencyTest::Restore,
350 clients,
351 iterations,
352 assert_thresholds,
353 )
354 .left_future()
355 };
356
357 latency_tests(
360 dev_fed.clone(),
361 LatencyTest::Reissue,
362 clients,
363 iterations,
364 assert_thresholds,
365 )
366 .await?;
367
368 latency_tests(
369 dev_fed.clone(),
370 LatencyTest::LnSend,
371 clients,
372 iterations,
373 assert_thresholds,
374 )
375 .await?;
376
377 latency_tests(
378 dev_fed.clone(),
379 LatencyTest::LnReceive,
380 clients,
381 iterations,
382 assert_thresholds,
383 )
384 .await?;
385
386 latency_tests(
387 dev_fed.clone(),
388 LatencyTest::FmPay,
389 clients,
390 iterations,
391 assert_thresholds,
392 )
393 .await?;
394
395 restore_test.await?;
396
397 Ok(())
398}
399
400pub async fn upgrade_tests(process_mgr: &ProcessManager, binary: UpgradeTest) -> Result<()> {
401 match binary {
402 UpgradeTest::Fedimintd { paths } => {
403 if let Some(oldest_fedimintd) = paths.first() {
404 unsafe { std::env::set_var("FM_FEDIMINTD_BASE_EXECUTABLE", oldest_fedimintd) };
406 } else {
407 bail!("Must provide at least 1 binary path");
408 }
409
410 let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
411 info!(
412 "running first stress test for fedimintd version: {}",
413 fedimintd_version
414 );
415
416 let mut dev_fed = dev_fed(process_mgr).await?;
417 let client = dev_fed.fed.new_joined_client("test-client").await?;
418 try_join!(stress_test_fed(&dev_fed, None), client.wait_session())?;
419
420 for path in paths.iter().skip(1) {
421 dev_fed.fed.restart_all_with_bin(process_mgr, path).await?;
422
423 try_join!(stress_test_fed(&dev_fed, None), client.wait_session())?;
425
426 let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
427 info!(
428 "### fedimintd passed stress test for version {}",
429 fedimintd_version
430 );
431 }
432 info!("## fedimintd upgraded all binaries successfully");
433 }
434 UpgradeTest::FedimintCli { paths } => {
435 let set_fedimint_cli_path = |path: &PathBuf| {
436 unsafe { std::env::set_var("FM_FEDIMINT_CLI_BASE_EXECUTABLE", path) };
438 let fm_mint_client: String = format!(
439 "{fedimint_cli} --data-dir {datadir}",
440 fedimint_cli = crate::util::get_fedimint_cli_path().join(" "),
441 datadir = crate::vars::utf8(&process_mgr.globals.FM_CLIENT_DIR)
442 );
443 unsafe { std::env::set_var("FM_MINT_CLIENT", fm_mint_client) };
445 };
446
447 if let Some(oldest_fedimint_cli) = paths.first() {
448 set_fedimint_cli_path(oldest_fedimint_cli);
449 } else {
450 bail!("Must provide at least 1 binary path");
451 }
452
453 let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
454 info!(
455 "running first stress test for fedimint-cli version: {}",
456 fedimint_cli_version
457 );
458
459 let dev_fed = dev_fed(process_mgr).await?;
460
461 let wait_session_client = dev_fed.fed.new_joined_client("wait-session-client").await?;
462 let reusable_upgrade_clients = UpgradeClients {
463 reissue_client: dev_fed.fed.new_joined_client("reissue-client").await?,
464 ln_send_client: dev_fed.fed.new_joined_client("ln-send-client").await?,
465 ln_receive_client: dev_fed.fed.new_joined_client("ln-receive-client").await?,
466 fm_pay_client: dev_fed.fed.new_joined_client("fm-pay-client").await?,
467 };
468
469 try_join!(
470 stress_test_fed(&dev_fed, Some(&reusable_upgrade_clients)),
471 wait_session_client.wait_session()
472 )?;
473
474 for path in paths.iter().skip(1) {
475 set_fedimint_cli_path(path);
476 let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
477 info!("upgraded fedimint-cli to version: {}", fedimint_cli_version);
478 try_join!(
479 stress_test_fed(&dev_fed, Some(&reusable_upgrade_clients)),
480 wait_session_client.wait_session()
481 )?;
482 info!(
483 "### fedimint-cli passed stress test for version {}",
484 fedimint_cli_version
485 );
486 }
487 info!("## fedimint-cli upgraded all binaries successfully");
488 }
489 UpgradeTest::Gatewayd {
490 gatewayd_paths,
491 gateway_cli_paths,
492 } => {
493 if let Some(oldest_gatewayd) = gatewayd_paths.first() {
494 unsafe { std::env::set_var("FM_GATEWAYD_BASE_EXECUTABLE", oldest_gatewayd) };
496 } else {
497 bail!("Must provide at least 1 gatewayd path");
498 }
499
500 if let Some(oldest_gateway_cli) = gateway_cli_paths.first() {
501 unsafe { std::env::set_var("FM_GATEWAY_CLI_BASE_EXECUTABLE", oldest_gateway_cli) };
503 } else {
504 bail!("Must provide at least 1 gateway-cli path");
505 }
506
507 let gatewayd_version = crate::util::Gatewayd::version_or_default().await;
508 let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
509 info!(
510 ?gatewayd_version,
511 ?gateway_cli_version,
512 "running first stress test for gateway",
513 );
514
515 let mut dev_fed = dev_fed(process_mgr).await?;
516 let client = dev_fed.fed.new_joined_client("test-client").await?;
517 try_join!(stress_test_fed(&dev_fed, None), client.wait_session())?;
518
519 for i in 1..gatewayd_paths.len() {
520 info!(
521 "running stress test with gatewayd path {:?}",
522 gatewayd_paths.get(i)
523 );
524 let new_gatewayd_path = gatewayd_paths.get(i).expect("Not enough gatewayd paths");
525 let new_gateway_cli_path = gateway_cli_paths
526 .get(i)
527 .expect("Not enough gateway-cli paths");
528
529 let gateways = vec![&mut dev_fed.gw_lnd];
530
531 try_join_all(gateways.into_iter().map(|gateway| {
532 gateway.restart_with_bin(process_mgr, new_gatewayd_path, new_gateway_cli_path)
533 }))
534 .await?;
535
536 dev_fed.fed.await_gateways_registered().await?;
537 try_join!(stress_test_fed(&dev_fed, None), client.wait_session())?;
538 let gatewayd_version = crate::util::Gatewayd::version_or_default().await;
539 let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
540 info!(
541 ?gatewayd_version,
542 ?gateway_cli_version,
543 "### gateway passed stress test for version",
544 );
545 }
546
547 info!("## gatewayd upgraded all binaries successfully");
548 }
549 }
550 Ok(())
551}
552
553pub async fn cli_tests(dev_fed: DevFed) -> Result<()> {
554 log_binary_versions().await?;
555 let data_dir = env::var(FM_DATA_DIR_ENV)?;
556
557 let DevFed {
558 bitcoind,
559 lnd,
560 fed,
561 gw_lnd,
562 gw_ldk,
563 ..
564 } = dev_fed;
565
566 let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
567
568 let client = fed.new_joined_client("cli-tests-client").await?;
569 let lnd_gw_id = gw_lnd.gateway_id().await?;
570
571 cmd!(
572 client,
573 "dev",
574 "config-decrypt",
575 "--in-file={data_dir}/fedimintd-default-0/private.encrypt",
576 "--out-file={data_dir}/fedimintd-default-0/config-plaintext.json"
577 )
578 .env(FM_PASSWORD_ENV, "pass")
579 .run()
580 .await?;
581
582 cmd!(
583 client,
584 "dev",
585 "config-encrypt",
586 "--in-file={data_dir}/fedimintd-default-0/config-plaintext.json",
587 "--out-file={data_dir}/fedimintd-default-0/config-2"
588 )
589 .env(FM_PASSWORD_ENV, "pass-foo")
590 .run()
591 .await?;
592
593 cmd!(
594 client,
595 "dev",
596 "config-decrypt",
597 "--in-file={data_dir}/fedimintd-default-0/config-2",
598 "--out-file={data_dir}/fedimintd-default-0/config-plaintext-2.json"
599 )
600 .env(FM_PASSWORD_ENV, "pass-foo")
601 .run()
602 .await?;
603
604 let plaintext_one = fs::read_to_string(format!(
605 "{data_dir}/fedimintd-default-0/config-plaintext.json"
606 ))
607 .await?;
608 let plaintext_two = fs::read_to_string(format!(
609 "{data_dir}/fedimintd-default-0/config-plaintext-2.json"
610 ))
611 .await?;
612 anyhow::ensure!(
613 plaintext_one == plaintext_two,
614 "config-decrypt/encrypt failed"
615 );
616
617 fed.pegin_gateways(10_000_000, vec![&gw_lnd]).await?;
618
619 let fed_id = fed.calculate_federation_id();
620 let invite = fed.invite_code()?;
621
622 gw_lnd
624 .set_federation_routing_fee(fed_id.clone(), 0, 0)
625 .await?;
626 cmd!(client, "list-gateways").run().await?;
627
628 let invite_code = cmd!(client, "dev", "decode", "invite-code", invite.clone())
629 .out_json()
630 .await?;
631
632 let encode_invite_output = cmd!(
633 client,
634 "dev",
635 "encode",
636 "invite-code",
637 format!("--url={}", invite_code["url"].as_str().unwrap()),
638 "--federation_id={fed_id}",
639 "--peer=0"
640 )
641 .out_json()
642 .await?;
643
644 anyhow::ensure!(
645 encode_invite_output["invite_code"]
646 .as_str()
647 .expect("invite_code must be a string")
648 == invite,
649 "failed to decode and encode the client invite code",
650 );
651
652 info!("Testing LND can pay LDK directly");
656 let invoice = gw_ldk.create_invoice(1_200_000).await?;
657 lnd.pay_bolt11_invoice(invoice.to_string()).await?;
658 gw_ldk
659 .wait_bolt11_invoice(invoice.payment_hash().consensus_encode_to_vec())
660 .await?;
661
662 info!("Testing LDK can pay LND directly");
664 let (invoice, payment_hash) = lnd.invoice(1_000_000).await?;
665 gw_ldk
666 .pay_invoice(Bolt11Invoice::from_str(&invoice).expect("Could not parse invoice"))
667 .await?;
668 gw_lnd.wait_bolt11_invoice(payment_hash).await?;
669
670 let config = cmd!(client, "config").out_json().await?;
672 let guardian_count = config["global"]["api_endpoints"].as_object().unwrap().len();
673 let descriptor = config["modules"]["2"]["peg_in_descriptor"]
674 .as_str()
675 .unwrap()
676 .to_owned();
677
678 info!("Testing generated descriptor for {guardian_count} guardian federation");
679 if guardian_count == 1 {
680 assert!(descriptor.contains("wpkh("));
681 } else {
682 assert!(descriptor.contains("wsh(sortedmulti("));
683 }
684
685 info!("Testing Client");
687 info!("Testing reissuing e-cash");
689 const CLIENT_START_AMOUNT: u64 = 5_000_000_000;
690 const CLIENT_SPEND_AMOUNT: u64 = 1_100_000;
691
692 let initial_client_balance = client.balance().await?;
693 assert_eq!(initial_client_balance, 0);
694
695 fed.pegin_client(CLIENT_START_AMOUNT / 1000, &client)
696 .await?;
697
698 info!("Testing spending from client");
700 let notes = cmd!(client, "spend", CLIENT_SPEND_AMOUNT)
701 .out_json()
702 .await?
703 .get("notes")
704 .expect("Output didn't contain e-cash notes")
705 .as_str()
706 .unwrap()
707 .to_owned();
708
709 let client_post_spend_balance = client.balance().await?;
710 assert_eq!(
711 client_post_spend_balance,
712 CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT
713 );
714
715 cmd!(client, "reissue", notes).out_json().await?;
717
718 let client_post_spend_balance = client.balance().await?;
719 assert_eq!(client_post_spend_balance, CLIENT_START_AMOUNT);
720
721 let reissue_amount: u64 = 409_600;
722
723 info!("Testing reissuing e-cash after spending");
725 let _notes = cmd!(client, "spend", CLIENT_SPEND_AMOUNT)
726 .out_json()
727 .await?
728 .as_object()
729 .unwrap()
730 .get("notes")
731 .expect("Output didn't contain e-cash notes")
732 .as_str()
733 .unwrap();
734
735 let reissue_notes = cmd!(client, "spend", reissue_amount).out_json().await?["notes"]
736 .as_str()
737 .map(ToOwned::to_owned)
738 .unwrap();
739 let client_reissue_amt = cmd!(client, "reissue", reissue_notes)
740 .out_json()
741 .await?
742 .as_u64()
743 .unwrap();
744 assert_eq!(client_reissue_amt, reissue_amount);
745
746 info!("Testing reissuing e-cash via module commands");
748 let reissue_notes = cmd!(client, "spend", reissue_amount).out_json().await?["notes"]
749 .as_str()
750 .map(ToOwned::to_owned)
751 .unwrap();
752 let client_reissue_amt = cmd!(client, "module", "mint", "reissue", reissue_notes)
753 .out_json()
754 .await?
755 .as_u64()
756 .unwrap();
757 assert_eq!(client_reissue_amt, reissue_amount);
758
759 info!("Testing LND gateway");
761
762 info!("Testing outgoing payment from client to LDK via LND gateway");
764 let initial_lnd_gateway_balance = gw_lnd.ecash_balance(fed_id.clone()).await?;
765 let invoice = gw_ldk.create_invoice(2_000_000).await?;
766 ln_pay(&client, invoice.to_string(), lnd_gw_id.clone(), false).await?;
767 let fed_id = fed.calculate_federation_id();
768 gw_ldk
769 .wait_bolt11_invoice(invoice.payment_hash().consensus_encode_to_vec())
770 .await?;
771
772 let final_lnd_outgoing_client_balance = client.balance().await?;
774 let final_lnd_outgoing_gateway_balance = gw_lnd.ecash_balance(fed_id.clone()).await?;
775 anyhow::ensure!(
776 final_lnd_outgoing_gateway_balance - initial_lnd_gateway_balance == 2_000_000,
777 "LND Gateway balance changed by {} on LND outgoing payment, expected 2_000_000",
778 (final_lnd_outgoing_gateway_balance - initial_lnd_gateway_balance)
779 );
780
781 info!("Testing incoming payment from LDK to client via LND gateway");
783 let recv = ln_invoice(
784 &client,
785 Amount::from_msats(1_300_000),
786 "incoming-over-lnd-gw".to_string(),
787 lnd_gw_id,
788 )
789 .await?;
790 let invoice = recv.invoice;
791 gw_ldk
792 .pay_invoice(Bolt11Invoice::from_str(&invoice).expect("Could not parse invoice"))
793 .await?;
794
795 info!("Testing receiving ecash notes");
797 let operation_id = recv.operation_id;
798 cmd!(client, "await-invoice", operation_id.fmt_full())
799 .run()
800 .await?;
801
802 let final_lnd_incoming_client_balance = client.balance().await?;
804 let final_lnd_incoming_gateway_balance = gw_lnd.ecash_balance(fed_id.clone()).await?;
805 anyhow::ensure!(
806 final_lnd_incoming_client_balance - final_lnd_outgoing_client_balance == 1_300_000,
807 "Client balance changed by {} on LND incoming payment, expected 1_300_000",
808 (final_lnd_incoming_client_balance - final_lnd_outgoing_client_balance)
809 );
810 anyhow::ensure!(
811 final_lnd_outgoing_gateway_balance - final_lnd_incoming_gateway_balance == 1_300_000,
812 "LND Gateway balance changed by {} on LND incoming payment, expected 1_300_000",
813 (final_lnd_outgoing_gateway_balance - final_lnd_incoming_gateway_balance)
814 );
815
816 info!("Testing client deposit");
821 let initial_walletng_balance = client.balance().await?;
822
823 fed.pegin_client(100_000, &client).await?; let post_deposit_walletng_balance = client.balance().await?;
826
827 assert_eq!(
828 post_deposit_walletng_balance,
829 initial_walletng_balance + 100_000_000 );
831
832 info!("Testing client withdraw");
834
835 let initial_walletng_balance = client.balance().await?;
836
837 let address = bitcoind.get_new_address().await?;
838 let withdraw_res = cmd!(
839 client,
840 "withdraw",
841 "--address",
842 &address,
843 "--amount",
844 "50000 sat"
845 )
846 .out_json()
847 .await?;
848
849 let txid: Txid = withdraw_res["txid"].as_str().unwrap().parse().unwrap();
850 let fees_sat = withdraw_res["fees_sat"].as_u64().unwrap();
851
852 let tx_hex = bitcoind.poll_get_transaction(txid).await?;
853
854 let tx = bitcoin::Transaction::consensus_decode_hex(&tx_hex, &ModuleRegistry::default())?;
855 assert!(
856 tx.output
857 .iter()
858 .any(|o| o.script_pubkey == address.script_pubkey() && o.value.to_sat() == 50000)
859 );
860
861 let post_withdraw_walletng_balance = client.balance().await?;
862 let expected_wallet_balance = initial_walletng_balance - 50_000_000 - (fees_sat * 1000);
863
864 assert_eq!(post_withdraw_walletng_balance, expected_wallet_balance);
865
866 let peer_0_fedimintd_version = cmd!(client, "dev", "peer-version", "--peer-id", "0")
868 .out_json()
869 .await?
870 .get("version")
871 .expect("Output didn't contain version")
872 .as_str()
873 .unwrap()
874 .to_owned();
875
876 assert_eq!(
877 semver::Version::parse(&peer_0_fedimintd_version)?,
878 fedimintd_version
879 );
880
881 info!("Checking initial announcements...");
882
883 retry(
884 "Check initial announcements",
885 aggressive_backoff(),
886 || async {
887 let initial_announcements =
889 serde_json::from_value::<BTreeMap<PeerId, SignedApiAnnouncement>>(
890 cmd!(client, "dev", "api-announcements",).out_json().await?,
891 )
892 .expect("failed to parse API announcements");
893
894 if fed.members.len() != initial_announcements.len() {
895 bail!(
896 "Not all announcements ready: {}",
897 initial_announcements.len()
898 )
899 }
900
901 cmd!(client, "dev", "wait", "3").run().await?;
903
904 if !initial_announcements
905 .values()
906 .all(|announcement| announcement.api_announcement.nonce == 0)
907 {
908 bail!("Not all announcements have their initial value");
909 }
910 Ok(())
911 },
912 )
913 .await?;
914
915 const NEW_API_URL: &str = "ws://127.0.0.1:4242";
916 let new_announcement = serde_json::from_value::<SignedApiAnnouncement>(
917 cmd!(
918 client,
919 "--our-id",
920 "0",
921 "--password",
922 "pass",
923 "admin",
924 "sign-api-announcement",
925 NEW_API_URL
926 )
927 .out_json()
928 .await?,
929 )
930 .expect("Couldn't parse signed announcement");
931
932 assert_eq!(
933 new_announcement.api_announcement.nonce, 1,
934 "Nonce did not increment correctly"
935 );
936
937 info!("Testing if the client syncs the announcement");
938 let announcement = poll("Waiting for the announcement to propagate", || async {
939 cmd!(client, "dev", "wait", "1")
940 .run()
941 .await
942 .map_err(ControlFlow::Break)?;
943
944 let new_announcements_peer2 =
945 serde_json::from_value::<BTreeMap<PeerId, SignedApiAnnouncement>>(
946 cmd!(client, "dev", "api-announcements",)
947 .out_json()
948 .await
949 .map_err(ControlFlow::Break)?,
950 )
951 .expect("failed to parse API announcements");
952
953 let announcement = new_announcements_peer2[&PeerId::from(0)]
954 .api_announcement
955 .clone();
956 if announcement.nonce == 1 {
957 Ok(announcement)
958 } else {
959 Err(ControlFlow::Continue(anyhow!(
960 "Haven't received updated announcement yet"
961 )))
962 }
963 })
964 .await?;
965
966 assert_eq!(
967 announcement.api_url,
968 NEW_API_URL.parse().expect("valid URL")
969 );
970
971 Ok(())
972}
973
974pub async fn cli_load_test_tool_test(dev_fed: DevFed) -> Result<()> {
975 log_binary_versions().await?;
976 let data_dir = env::var(FM_DATA_DIR_ENV)?;
977 let load_test_temp = PathBuf::from(data_dir).join("load-test-temp");
978 dev_fed
979 .fed
980 .pegin_client(10_000, dev_fed.fed.internal_client().await?)
981 .await?;
982 let invite_code = dev_fed.fed.invite_code()?;
983 dev_fed
984 .gw_lnd
985 .set_federation_routing_fee(dev_fed.fed.calculate_federation_id(), 0, 0)
986 .await?;
987 run_standard_load_test(&load_test_temp, &invite_code).await?;
988 run_ln_circular_load_test(&load_test_temp, &invite_code).await?;
989 Ok(())
990}
991
992pub async fn run_standard_load_test(
993 load_test_temp: &Path,
994 invite_code: &str,
995) -> anyhow::Result<()> {
996 let output = cmd!(
997 LoadTestTool,
998 "--archive-dir",
999 load_test_temp.display(),
1000 "--users",
1001 "1",
1002 "load-test",
1003 "--notes-per-user",
1004 "1",
1005 "--generate-invoice-with",
1006 "ldk-lightning-cli",
1007 "--invite-code",
1008 invite_code
1009 )
1010 .out_string()
1011 .await?;
1012 println!("{output}");
1013 anyhow::ensure!(
1014 output.contains("2 reissue_notes"),
1015 "reissued different number notes than expected"
1016 );
1017 anyhow::ensure!(
1018 output.contains("1 gateway_pay_invoice"),
1019 "paid different number of invoices than expected"
1020 );
1021 Ok(())
1022}
1023
1024pub async fn run_ln_circular_load_test(
1025 load_test_temp: &Path,
1026 invite_code: &str,
1027) -> anyhow::Result<()> {
1028 info!("Testing ln-circular-load-test with 'two-gateways' strategy");
1029 let output = cmd!(
1030 LoadTestTool,
1031 "--archive-dir",
1032 load_test_temp.display(),
1033 "--users",
1034 "1",
1035 "ln-circular-load-test",
1036 "--strategy",
1037 "two-gateways",
1038 "--test-duration-secs",
1039 "2",
1040 "--invite-code",
1041 invite_code
1042 )
1043 .out_string()
1044 .await?;
1045 println!("{output}");
1046 anyhow::ensure!(
1047 output.contains("gateway_create_invoice"),
1048 "missing invoice creation"
1049 );
1050 anyhow::ensure!(
1051 output.contains("gateway_pay_invoice_success"),
1052 "missing invoice payment"
1053 );
1054 anyhow::ensure!(
1055 output.contains("gateway_payment_received_success"),
1056 "missing received payment"
1057 );
1058
1059 info!("Testing ln-circular-load-test with 'partner-ping-pong' strategy");
1060 let output = cmd!(
1064 LoadTestTool,
1065 "--archive-dir",
1066 load_test_temp.display(),
1067 "--users",
1068 "1",
1069 "ln-circular-load-test",
1070 "--strategy",
1071 "partner-ping-pong",
1072 "--test-duration-secs",
1073 "6",
1074 "--invite-code",
1075 invite_code
1076 )
1077 .out_string()
1078 .await?;
1079 println!("{output}");
1080 anyhow::ensure!(
1081 output.contains("gateway_create_invoice"),
1082 "missing invoice creation"
1083 );
1084 anyhow::ensure!(
1085 output.contains("gateway_payment_received_success"),
1086 "missing received payment"
1087 );
1088
1089 info!("Testing ln-circular-load-test with 'self-payment' strategy");
1090 let output = cmd!(
1092 LoadTestTool,
1093 "--archive-dir",
1094 load_test_temp.display(),
1095 "--users",
1096 "1",
1097 "ln-circular-load-test",
1098 "--strategy",
1099 "self-payment",
1100 "--test-duration-secs",
1101 "2",
1102 "--invite-code",
1103 invite_code
1104 )
1105 .out_string()
1106 .await?;
1107 println!("{output}");
1108 anyhow::ensure!(
1109 output.contains("gateway_create_invoice"),
1110 "missing invoice creation"
1111 );
1112 anyhow::ensure!(
1113 output.contains("gateway_payment_received_success"),
1114 "missing received payment"
1115 );
1116 Ok(())
1117}
1118
1119pub async fn lightning_gw_reconnect_test(
1120 dev_fed: DevFed,
1121 process_mgr: &ProcessManager,
1122) -> Result<()> {
1123 log_binary_versions().await?;
1124
1125 let DevFed {
1126 bitcoind,
1127 lnd,
1128 fed,
1129 mut gw_lnd,
1130 gw_ldk,
1131 ..
1132 } = dev_fed;
1133
1134 let client = fed
1135 .new_joined_client("lightning-gw-reconnect-test-client")
1136 .await?;
1137
1138 info!("Pegging-in both gateways");
1139 fed.pegin_gateways(99_999, vec![&gw_lnd]).await?;
1140
1141 drop(lnd);
1143
1144 tracing::info!("Stopping LND");
1145 let mut info_cmd = cmd!(gw_lnd, "info");
1147 assert!(info_cmd.run().await.is_ok());
1148
1149 let ln_type = gw_lnd.ln.ln_type().to_string();
1152 gw_lnd.stop_lightning_node().await?;
1153 let lightning_info = info_cmd.out_json().await?;
1154 let lightning_pub_key: Option<String> =
1155 serde_json::from_value(lightning_info["lightning_pub_key"].clone())?;
1156
1157 assert!(lightning_pub_key.is_none());
1158
1159 tracing::info!("Restarting LND...");
1161 let new_lnd = Lnd::new(process_mgr, bitcoind.clone()).await?;
1162 gw_lnd.set_lightning_node(LightningNode::Lnd(new_lnd.clone()));
1163
1164 tracing::info!("Retrying info...");
1165 const MAX_RETRIES: usize = 30;
1166 const RETRY_INTERVAL: Duration = Duration::from_secs(1);
1167
1168 for i in 0..MAX_RETRIES {
1169 match do_try_create_and_pay_invoice(&gw_lnd, &client, &gw_ldk).await {
1170 Ok(()) => break,
1171 Err(e) => {
1172 if i == MAX_RETRIES - 1 {
1173 return Err(e);
1174 }
1175 tracing::debug!(
1176 "Pay invoice for gateway {} failed with {e:?}, retrying in {} seconds (try {}/{MAX_RETRIES})",
1177 ln_type,
1178 RETRY_INTERVAL.as_secs(),
1179 i + 1,
1180 );
1181 fedimint_core::task::sleep_in_test(
1182 "paying invoice for gateway failed",
1183 RETRY_INTERVAL,
1184 )
1185 .await;
1186 }
1187 }
1188 }
1189
1190 info!(target: LOG_DEVIMINT, "lightning_reconnect_test: success");
1191 Ok(())
1192}
1193
1194pub async fn gw_reboot_test(dev_fed: DevFed, process_mgr: &ProcessManager) -> Result<()> {
1195 log_binary_versions().await?;
1196
1197 let DevFed {
1198 bitcoind,
1199 lnd,
1200 fed,
1201 gw_lnd,
1202 gw_ldk,
1203 gw_ldk_second,
1204 ..
1205 } = dev_fed;
1206
1207 let client = fed.new_joined_client("gw-reboot-test-client").await?;
1208 fed.pegin_client(10_000, &client).await?;
1209
1210 let block_height = bitcoind.get_block_count().await? - 1;
1212 try_join!(
1213 gw_lnd.wait_for_block_height(block_height),
1214 gw_ldk.wait_for_block_height(block_height),
1215 )?;
1216
1217 let (lnd_value, ldk_value) = try_join!(gw_lnd.get_info(), gw_ldk.get_info())?;
1219
1220 let lnd_gateway_id = gw_lnd.gateway_id().await?;
1222 let gw_ldk_name = gw_ldk.gw_name.clone();
1223 let gw_ldk_port = gw_ldk.gw_port;
1224 let gw_lightning_port = gw_ldk.ldk_port;
1225 drop(gw_lnd);
1226 drop(gw_ldk);
1227
1228 info!("Making payment while gateway is down");
1231 let initial_client_balance = client.balance().await?;
1232 let invoice = gw_ldk_second.create_invoice(3000).await?;
1233 ln_pay(&client, invoice.to_string(), lnd_gateway_id, false)
1234 .await
1235 .expect_err("Expected ln-pay to return error because the gateway is not online");
1236 let new_client_balance = client.balance().await?;
1237 anyhow::ensure!(initial_client_balance == new_client_balance);
1238
1239 info!("Rebooting gateways...");
1241 let chain_source = if crate::util::Gatewayd::version_or_default().await < *VERSION_0_7_0_ALPHA {
1242 LdkChainSource::Bitcoind
1243 } else {
1244 LdkChainSource::Esplora
1246 };
1247 let (new_gw_lnd, new_gw_ldk) = try_join!(
1248 Gatewayd::new(process_mgr, LightningNode::Lnd(lnd.clone())),
1249 Gatewayd::new(
1250 process_mgr,
1251 LightningNode::Ldk {
1252 name: gw_ldk_name,
1253 gw_port: gw_ldk_port,
1254 ldk_port: gw_lightning_port,
1255 chain_source,
1256 }
1257 )
1258 )?;
1259
1260 let lnd_gateway_id: fedimint_core::secp256k1::PublicKey =
1261 serde_json::from_value(lnd_value["gateway_id"].clone())?;
1262
1263 poll(
1264 "Waiting for LND Gateway Running state after reboot",
1265 || async {
1266 let mut new_lnd_cmd = cmd!(new_gw_lnd, "info");
1267 let lnd_value = new_lnd_cmd.out_json().await.map_err(ControlFlow::Continue)?;
1268 let reboot_gateway_state: String = serde_json::from_value(lnd_value["gateway_state"].clone()).context("invalid gateway state").map_err(ControlFlow::Break)?;
1269 let reboot_gateway_id: fedimint_core::secp256k1::PublicKey =
1270 serde_json::from_value(lnd_value["gateway_id"].clone()).context("invalid gateway id").map_err(ControlFlow::Break)?;
1271
1272 if reboot_gateway_state == "Running" {
1273 info!(target: LOG_DEVIMINT, "LND Gateway restarted, with auto-rejoin to federation");
1274 assert_eq!(lnd_gateway_id, reboot_gateway_id);
1276 return Ok(());
1277 }
1278 Err(ControlFlow::Continue(anyhow!("gateway not running")))
1279 },
1280 )
1281 .await?;
1282
1283 let ldk_gateway_id: fedimint_core::secp256k1::PublicKey =
1284 serde_json::from_value(ldk_value["gateway_id"].clone())?;
1285 poll(
1286 "Waiting for LDK Gateway Running state after reboot",
1287 || async {
1288 let mut new_ldk_cmd = cmd!(new_gw_ldk, "info");
1289 let ldk_value = new_ldk_cmd.out_json().await.map_err(ControlFlow::Continue)?;
1290 let reboot_gateway_state: String = serde_json::from_value(ldk_value["gateway_state"].clone()).context("invalid gateway state").map_err(ControlFlow::Break)?;
1291 let reboot_gateway_id: fedimint_core::secp256k1::PublicKey =
1292 serde_json::from_value(ldk_value["gateway_id"].clone()).context("invalid gateway id").map_err(ControlFlow::Break)?;
1293
1294 if reboot_gateway_state == "Running" {
1295 info!(target: LOG_DEVIMINT, "LDK Gateway restarted, with auto-rejoin to federation");
1296 assert_eq!(ldk_gateway_id, reboot_gateway_id);
1298 return Ok(());
1299 }
1300 Err(ControlFlow::Continue(anyhow!("gateway not running")))
1301 },
1302 )
1303 .await?;
1304
1305 info!(LOG_DEVIMINT, "gateway_reboot_test: success");
1306 Ok(())
1307}
1308
1309pub async fn do_try_create_and_pay_invoice(
1310 gw_lnd: &Gatewayd,
1311 client: &Client,
1312 gw_ldk: &Gatewayd,
1313) -> anyhow::Result<()> {
1314 poll("Waiting for info to succeed after restart", || async {
1318 let lightning_pub_key = cmd!(gw_lnd, "info")
1319 .out_json()
1320 .await
1321 .map_err(ControlFlow::Continue)?
1322 .get("lightning_pub_key")
1323 .map(|ln_pk| {
1324 serde_json::from_value::<Option<String>>(ln_pk.clone())
1325 .expect("could not parse lightning_pub_key")
1326 })
1327 .expect("missing lightning_pub_key");
1328
1329 poll_eq!(lightning_pub_key.is_some(), true)
1330 })
1331 .await?;
1332
1333 tracing::info!("Creating invoice....");
1334 let invoice = ln_invoice(
1335 client,
1336 Amount::from_msats(1000),
1337 "incoming-over-lnd-gw".to_string(),
1338 gw_lnd.gateway_id().await?,
1339 )
1340 .await?
1341 .invoice;
1342
1343 match &gw_lnd.ln.ln_type() {
1344 LightningNodeType::Lnd => {
1345 gw_ldk
1347 .pay_invoice(Bolt11Invoice::from_str(&invoice).expect("Could not parse invoice"))
1348 .await?;
1349 }
1350 LightningNodeType::Ldk => {
1351 unimplemented!("do_try_create_and_pay_invoice not implemented for LDK yet");
1352 }
1353 }
1354 Ok(())
1355}
1356
1357async fn ln_pay(
1358 client: &Client,
1359 invoice: String,
1360 gw_id: String,
1361 finish_in_background: bool,
1362) -> anyhow::Result<String> {
1363 let value = if finish_in_background {
1364 cmd!(
1365 client,
1366 "ln-pay",
1367 invoice,
1368 "--finish-in-background",
1369 "--gateway-id",
1370 gw_id,
1371 )
1372 .out_json()
1373 .await?
1374 } else {
1375 cmd!(client, "ln-pay", invoice, "--gateway-id", gw_id,)
1376 .out_json()
1377 .await?
1378 };
1379
1380 let operation_id = value["operation_id"]
1381 .as_str()
1382 .ok_or(anyhow!("Failed to pay invoice"))?
1383 .to_string();
1384 Ok(operation_id)
1385}
1386
1387async fn ln_invoice(
1388 client: &Client,
1389 amount: Amount,
1390 description: String,
1391 gw_id: String,
1392) -> anyhow::Result<LnInvoiceResponse> {
1393 let ln_response_val = cmd!(
1394 client,
1395 "ln-invoice",
1396 "--amount",
1397 amount.msats,
1398 format!("--description='{description}'"),
1399 "--gateway-id",
1400 gw_id,
1401 )
1402 .out_json()
1403 .await?;
1404
1405 let ln_invoice_response: LnInvoiceResponse = serde_json::from_value(ln_response_val)?;
1406
1407 Ok(ln_invoice_response)
1408}
1409
1410pub async fn reconnect_test(dev_fed: DevFed, process_mgr: &ProcessManager) -> Result<()> {
1411 log_binary_versions().await?;
1412
1413 let DevFed {
1414 bitcoind, mut fed, ..
1415 } = dev_fed;
1416
1417 bitcoind.mine_blocks(110).await?;
1418 fed.await_block_sync().await?;
1419 fed.await_all_peers().await?;
1420
1421 fed.terminate_server(0).await?;
1423 fed.mine_then_wait_blocks_sync(100).await?;
1424
1425 fed.start_server(process_mgr, 0).await?;
1426 fed.mine_then_wait_blocks_sync(100).await?;
1427 fed.await_all_peers().await?;
1428 info!(target: LOG_DEVIMINT, "Server 0 successfully rejoined!");
1429 fed.mine_then_wait_blocks_sync(100).await?;
1430
1431 fed.terminate_server(1).await?;
1433 fed.mine_then_wait_blocks_sync(100).await?;
1434 fed.terminate_server(2).await?;
1435 fed.terminate_server(3).await?;
1436
1437 fed.start_server(process_mgr, 1).await?;
1438 fed.start_server(process_mgr, 2).await?;
1439 fed.start_server(process_mgr, 3).await?;
1440
1441 fed.await_all_peers().await?;
1442
1443 info!(target: LOG_DEVIMINT, "fm success: reconnect-test");
1444 Ok(())
1445}
1446
1447pub async fn recoverytool_test(dev_fed: DevFed) -> Result<()> {
1448 log_binary_versions().await?;
1449
1450 let DevFed { bitcoind, fed, .. } = dev_fed;
1451
1452 let data_dir = env::var(FM_DATA_DIR_ENV)?;
1453 let client = fed.new_joined_client("recoverytool-test-client").await?;
1454
1455 let mut fed_utxos_sats = HashSet::from([12_345_000, 23_456_000, 34_567_000]);
1456 let deposit_fees = fed.deposit_fees()?.msats / 1000;
1457 for sats in &fed_utxos_sats {
1458 fed.pegin_client(*sats - deposit_fees, &client).await?;
1460 }
1461
1462 async fn withdraw(
1463 client: &Client,
1464 bitcoind: &crate::external::Bitcoind,
1465 fed_utxos_sats: &mut HashSet<u64>,
1466 ) -> Result<()> {
1467 let withdrawal_address = bitcoind.get_new_address().await?;
1468 let withdraw_res = cmd!(
1469 client,
1470 "withdraw",
1471 "--address",
1472 &withdrawal_address,
1473 "--amount",
1474 "5000 sat"
1475 )
1476 .out_json()
1477 .await?;
1478
1479 let fees_sat = withdraw_res["fees_sat"]
1480 .as_u64()
1481 .expect("withdrawal should contain fees");
1482 let txid: Txid = withdraw_res["txid"]
1483 .as_str()
1484 .expect("withdrawal should contain txid string")
1485 .parse()
1486 .expect("txid should be parsable");
1487 let tx_hex = bitcoind.poll_get_transaction(txid).await?;
1488
1489 let tx = bitcoin::Transaction::consensus_decode_hex(&tx_hex, &ModuleRegistry::default())?;
1490 assert_eq!(tx.input.len(), 1);
1491 assert_eq!(tx.output.len(), 2);
1492
1493 let change_output = tx
1494 .output
1495 .iter()
1496 .find(|o| o.to_owned().script_pubkey != withdrawal_address.script_pubkey())
1497 .expect("withdrawal must have change output");
1498 assert!(fed_utxos_sats.insert(change_output.value.to_sat()));
1499
1500 let total_output_sats = tx.output.iter().map(|o| o.value.to_sat()).sum::<u64>();
1502 let input_sats = total_output_sats + fees_sat;
1503 assert!(fed_utxos_sats.remove(&input_sats));
1504
1505 Ok(())
1506 }
1507
1508 for _ in 0..2 {
1511 withdraw(&client, &bitcoind, &mut fed_utxos_sats).await?;
1512 }
1513
1514 let total_fed_sats = fed_utxos_sats.iter().sum::<u64>();
1515 fed.finalize_mempool_tx().await?;
1516
1517 let last_tx_session = client.get_session_count().await?;
1521
1522 info!("Recovering using utxos method");
1523 let output = cmd!(
1524 crate::util::Recoverytool,
1525 "--cfg",
1526 "{data_dir}/fedimintd-default-0",
1527 "utxos",
1528 "--db",
1529 "{data_dir}/fedimintd-default-0/database"
1530 )
1531 .env(FM_PASSWORD_ENV, "pass")
1532 .out_json()
1533 .await?;
1534 let outputs = output.as_array().context("expected an array")?;
1535 assert_eq!(outputs.len(), fed_utxos_sats.len());
1536
1537 assert_eq!(
1538 outputs
1539 .iter()
1540 .map(|o| o["amount_sat"].as_u64().unwrap())
1541 .collect::<HashSet<_>>(),
1542 fed_utxos_sats
1543 );
1544 let utxos_descriptors = outputs
1545 .iter()
1546 .map(|o| o["descriptor"].as_str().unwrap())
1547 .collect::<HashSet<_>>();
1548
1549 debug!(target: LOG_DEVIMINT, ?utxos_descriptors, "recoverytool descriptors using UTXOs method");
1550
1551 let descriptors_json = serde_json::value::to_raw_value(&serde_json::Value::Array(vec![
1552 serde_json::Value::Array(
1553 utxos_descriptors
1554 .iter()
1555 .map(|d| {
1556 json!({
1557 "desc": d,
1558 "timestamp": 0,
1559 })
1560 })
1561 .collect(),
1562 ),
1563 ]))?;
1564 info!("Getting wallet balances before import");
1565 let bitcoin_client = bitcoind.wallet_client().await?;
1566 let balances_before = bitcoin_client.get_balances().await?;
1567 info!("Importing descriptors into bitcoin wallet");
1568 let request = bitcoin_client
1569 .get_jsonrpc_client()
1570 .build_request("importdescriptors", Some(&descriptors_json));
1571 let response = block_in_place(|| bitcoin_client.get_jsonrpc_client().send_request(request))?;
1572 response.check_error()?;
1573 info!("Getting wallet balances after import");
1574 let balances_after = bitcoin_client.get_balances().await?;
1575 let diff = balances_after.mine.immature + balances_after.mine.trusted
1576 - balances_before.mine.immature
1577 - balances_before.mine.trusted;
1578
1579 client.wait_session_outcome(last_tx_session).await?;
1584
1585 assert_eq!(diff.to_sat(), total_fed_sats);
1587 info!("Recovering using epochs method");
1588
1589 let outputs = cmd!(
1590 crate::util::Recoverytool,
1591 "--cfg",
1592 "{data_dir}/fedimintd-default-0",
1593 "epochs",
1594 "--db",
1595 "{data_dir}/fedimintd-default-0/database"
1596 )
1597 .env(FM_PASSWORD_ENV, "pass")
1598 .out_json()
1599 .await?
1600 .as_array()
1601 .context("expected an array")?
1602 .clone();
1603
1604 let epochs_descriptors = outputs
1605 .iter()
1606 .map(|o| o["descriptor"].as_str().unwrap())
1607 .collect::<HashSet<_>>();
1608
1609 debug!(target: LOG_DEVIMINT, ?epochs_descriptors, "recoverytool descriptors using epochs method");
1610
1611 for utxo_descriptor in utxos_descriptors {
1614 assert!(epochs_descriptors.contains(utxo_descriptor));
1615 }
1616 Ok(())
1617}
1618
1619pub async fn guardian_backup_test(dev_fed: DevFed, process_mgr: &ProcessManager) -> Result<()> {
1620 let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
1621 const PEER_TO_TEST: u16 = 0;
1622
1623 log_binary_versions().await?;
1624
1625 let DevFed { mut fed, .. } = dev_fed;
1626
1627 fed.await_all_peers()
1628 .await
1629 .expect("Awaiting federation coming online failed");
1630
1631 let client = fed.new_joined_client("guardian-client").await?;
1632 let old_block_count = if fedimint_cli_version < *VERSION_0_6_0_ALPHA {
1633 cmd!(
1634 client,
1635 "dev",
1636 "api",
1637 "--peer-id",
1638 PEER_TO_TEST.to_string(),
1639 "module_{LEGACY_HARDCODED_INSTANCE_ID_WALLET}_block_count",
1640 )
1641 } else {
1642 cmd!(
1643 client,
1644 "dev",
1645 "api",
1646 "--peer-id",
1647 PEER_TO_TEST.to_string(),
1648 "--module",
1649 "wallet",
1650 "block_count",
1651 )
1652 }
1653 .out_json()
1654 .await?["value"]
1655 .as_u64()
1656 .expect("No block height returned");
1657
1658 let backup_res = cmd!(
1659 client,
1660 "--our-id",
1661 PEER_TO_TEST.to_string(),
1662 "--password",
1663 "pass",
1664 "admin",
1665 "guardian-config-backup"
1666 )
1667 .out_json()
1668 .await?;
1669 let backup_hex = backup_res["tar_archive_bytes"]
1670 .as_str()
1671 .expect("expected hex string");
1672 let backup_tar = hex::decode(backup_hex).expect("invalid hex");
1673
1674 let data_dir = fed
1675 .vars
1676 .get(&PEER_TO_TEST.into())
1677 .expect("peer not found")
1678 .FM_DATA_DIR
1679 .clone();
1680
1681 fed.terminate_server(PEER_TO_TEST.into())
1682 .await
1683 .expect("could not terminate fedimintd");
1684
1685 std::fs::remove_dir_all(&data_dir).expect("error deleting old datadir");
1686 std::fs::create_dir(&data_dir).expect("error creating new datadir");
1687
1688 let write_file = |name: &str, data: &[u8]| {
1689 let mut file = std::fs::File::options()
1690 .write(true)
1691 .create(true)
1692 .truncate(true)
1693 .open(data_dir.join(name))
1694 .expect("could not open file");
1695 file.write_all(data).expect("could not write file");
1696 file.flush().expect("could not flush file");
1697 };
1698
1699 write_file("backup.tar", &backup_tar);
1700 write_file(
1701 fedimint_server::config::io::PLAINTEXT_PASSWORD,
1702 "pass".as_bytes(),
1703 );
1704
1705 assert_eq!(
1706 std::process::Command::new("tar")
1707 .arg("-xf")
1708 .arg("backup.tar")
1709 .current_dir(data_dir)
1710 .spawn()
1711 .expect("error spawning tar")
1712 .wait()
1713 .expect("error extracting archive")
1714 .code(),
1715 Some(0),
1716 "tar failed"
1717 );
1718
1719 fed.start_server(process_mgr, PEER_TO_TEST.into())
1720 .await
1721 .expect("could not restart fedimintd");
1722
1723 poll("Peer catches up again", || async {
1724 let block_counts = all_peer_block_count(&client, fed.member_ids())
1725 .await
1726 .map_err(ControlFlow::Continue)?;
1727 let block_count = block_counts[&PeerId::from(PEER_TO_TEST)];
1728
1729 info!("Caught up to block {block_count} of at least {old_block_count} (counts={block_counts:?})");
1730
1731 if block_count < old_block_count {
1732 return Err(ControlFlow::Continue(anyhow!("Block count still behind")));
1733 }
1734
1735 Ok(())
1736 })
1737 .await
1738 .expect("Peer didn't rejoin federation");
1739
1740 Ok(())
1741}
1742
1743async fn peer_block_count(client: &Client, peer: PeerId) -> Result<u64> {
1744 cmd!(
1745 client,
1746 "dev",
1747 "api",
1748 "--peer-id",
1749 peer.to_string(),
1750 "module_{LEGACY_HARDCODED_INSTANCE_ID_WALLET}_block_count",
1751 )
1752 .out_json()
1753 .await?["value"]
1754 .as_u64()
1755 .context("No block height returned")
1756}
1757
1758async fn all_peer_block_count(
1759 client: &Client,
1760 peers: impl Iterator<Item = PeerId>,
1761) -> Result<BTreeMap<PeerId, u64>> {
1762 let mut peer_heights = BTreeMap::new();
1763 for peer in peers {
1764 peer_heights.insert(peer, peer_block_count(client, peer).await?);
1765 }
1766 Ok(peer_heights)
1767}
1768
1769pub async fn cannot_replay_tx_test(dev_fed: DevFed) -> Result<()> {
1770 log_binary_versions().await?;
1771
1772 let DevFed { fed, .. } = dev_fed;
1773
1774 let client = fed.new_joined_client("cannot-replay-client").await?;
1775
1776 const CLIENT_START_AMOUNT: u64 = 5_000_000_000;
1778 const CLIENT_SPEND_AMOUNT: u64 = 5_000_000_000;
1779
1780 let initial_client_balance = client.balance().await?;
1781 assert_eq!(initial_client_balance, 0);
1782
1783 fed.pegin_client(CLIENT_START_AMOUNT / 1000, &client)
1784 .await?;
1785
1786 let double_spend_client = client.new_forked("double-spender").await?;
1788
1789 let notes = cmd!(client, "spend", CLIENT_SPEND_AMOUNT)
1791 .out_json()
1792 .await?
1793 .get("notes")
1794 .expect("Output didn't contain e-cash notes")
1795 .as_str()
1796 .unwrap()
1797 .to_owned();
1798
1799 let client_post_spend_balance = client.balance().await?;
1800 assert_eq!(
1801 client_post_spend_balance,
1802 CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT
1803 );
1804
1805 cmd!(client, "reissue", notes).out_json().await?;
1806 let client_post_reissue_balance = client.balance().await?;
1807 assert_eq!(client_post_reissue_balance, CLIENT_START_AMOUNT);
1808
1809 let double_spend_notes = cmd!(double_spend_client, "spend", CLIENT_SPEND_AMOUNT)
1811 .out_json()
1812 .await?
1813 .get("notes")
1814 .expect("Output didn't contain e-cash notes")
1815 .as_str()
1816 .unwrap()
1817 .to_owned();
1818
1819 let double_spend_client_post_spend_balance = double_spend_client.balance().await?;
1820 assert_eq!(
1821 double_spend_client_post_spend_balance,
1822 CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT
1823 );
1824
1825 cmd!(double_spend_client, "reissue", double_spend_notes)
1826 .assert_error_contains("The transaction had an invalid input")
1827 .await?;
1828
1829 let double_spend_client_post_spend_balance = double_spend_client.balance().await?;
1830 assert_eq!(
1831 double_spend_client_post_spend_balance,
1832 CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT
1833 );
1834
1835 Ok(())
1836}
1837
1838#[derive(Subcommand)]
1839pub enum LatencyTest {
1840 Reissue,
1841 LnSend,
1842 LnReceive,
1843 FmPay,
1844 Restore,
1845}
1846
1847#[derive(Subcommand)]
1848pub enum UpgradeTest {
1849 Fedimintd {
1850 #[arg(long, trailing_var_arg = true, num_args=1..)]
1851 paths: Vec<PathBuf>,
1852 },
1853 FedimintCli {
1854 #[arg(long, trailing_var_arg = true, num_args=1..)]
1855 paths: Vec<PathBuf>,
1856 },
1857 Gatewayd {
1858 #[arg(long, trailing_var_arg = true, num_args=1..)]
1859 gatewayd_paths: Vec<PathBuf>,
1860 #[arg(long, trailing_var_arg = true, num_args=1..)]
1861 gateway_cli_paths: Vec<PathBuf>,
1862 },
1863}
1864
1865#[derive(Subcommand)]
1866pub enum TestCmd {
1867 LatencyTests {
1870 #[clap(subcommand)]
1871 r#type: LatencyTest,
1872
1873 #[arg(long, default_value = "10")]
1874 iterations: usize,
1875 },
1876 ReconnectTest,
1879 CliTests,
1881 LoadTestToolTest,
1884 LightningReconnectTest,
1887 GatewayRebootTest,
1890 RecoverytoolTests,
1892 WasmTestSetup {
1894 #[arg(long, trailing_var_arg = true, allow_hyphen_values = true, num_args=1..)]
1895 exec: Option<Vec<ffi::OsString>>,
1896 },
1897 GuardianBackup,
1899 CannotReplayTransaction,
1901 UpgradeTests {
1903 #[clap(subcommand)]
1904 binary: UpgradeTest,
1905 #[arg(long)]
1906 lnv2: String,
1907 },
1908}
1909
1910pub async fn handle_command(cmd: TestCmd, common_args: CommonArgs) -> Result<()> {
1911 match cmd {
1912 TestCmd::WasmTestSetup { exec } => {
1913 let (process_mgr, task_group) = setup(common_args).await?;
1914 let main = {
1915 let task_group = task_group.clone();
1916 async move {
1917 let dev_fed = dev_fed(&process_mgr).await?;
1918 let gw_lnd = dev_fed.gw_lnd.clone();
1919 let fed = dev_fed.fed.clone();
1920 gw_lnd
1921 .set_federation_routing_fee(dev_fed.fed.calculate_federation_id(), 0, 0)
1922 .await?;
1923 task_group.spawn_cancellable("faucet", async move {
1924 if let Err(err) = crate::faucet::run(
1925 &dev_fed,
1926 format!("0.0.0.0:{}", process_mgr.globals.FM_PORT_FAUCET),
1927 process_mgr.globals.FM_PORT_GW_LND,
1928 )
1929 .await
1930 {
1931 error!("Error spawning faucet: {err}");
1932 }
1933 });
1934 try_join!(fed.pegin_gateways(30_000, vec![&gw_lnd]), async {
1935 poll("waiting for faucet startup", || async {
1936 TcpStream::connect(format!(
1937 "127.0.0.1:{}",
1938 process_mgr.globals.FM_PORT_FAUCET
1939 ))
1940 .await
1941 .context("connect to faucet")
1942 .map_err(ControlFlow::Continue)
1943 })
1944 .await?;
1945 Ok(())
1946 },)?;
1947 if let Some(exec) = exec {
1948 exec_user_command(exec).await?;
1949 task_group.shutdown();
1950 }
1951 Ok::<_, anyhow::Error>(())
1952 }
1953 };
1954 cleanup_on_exit(main, task_group).await?;
1955 }
1956 TestCmd::LatencyTests { r#type, iterations } => {
1957 let (process_mgr, _) = setup(common_args).await?;
1958 let dev_fed = dev_fed(&process_mgr).await?;
1959 latency_tests(dev_fed, r#type, None, iterations, true).await?;
1960 }
1961 TestCmd::ReconnectTest => {
1962 let (process_mgr, _) = setup(common_args).await?;
1963 let dev_fed = dev_fed(&process_mgr).await?;
1964 reconnect_test(dev_fed, &process_mgr).await?;
1965 }
1966 TestCmd::CliTests => {
1967 let (process_mgr, _) = setup(common_args).await?;
1968 let dev_fed = dev_fed(&process_mgr).await?;
1969 cli_tests(dev_fed).await?;
1970 }
1971 TestCmd::LoadTestToolTest => {
1972 let (process_mgr, _) = setup(common_args).await?;
1973 let dev_fed = dev_fed(&process_mgr).await?;
1974 cli_load_test_tool_test(dev_fed).await?;
1975 }
1976 TestCmd::LightningReconnectTest => {
1977 let (process_mgr, _) = setup(common_args).await?;
1978 let dev_fed = dev_fed(&process_mgr).await?;
1979 lightning_gw_reconnect_test(dev_fed, &process_mgr).await?;
1980 }
1981 TestCmd::GatewayRebootTest => {
1982 let (process_mgr, _) = setup(common_args).await?;
1983 let dev_fed = dev_fed(&process_mgr).await?;
1984 gw_reboot_test(dev_fed, &process_mgr).await?;
1985 }
1986 TestCmd::RecoverytoolTests => {
1987 let (process_mgr, _) = setup(common_args).await?;
1988 let dev_fed = dev_fed(&process_mgr).await?;
1989 recoverytool_test(dev_fed).await?;
1990 }
1991 TestCmd::GuardianBackup => {
1992 let (process_mgr, _) = setup(common_args).await?;
1993 let dev_fed = dev_fed(&process_mgr).await?;
1994 guardian_backup_test(dev_fed, &process_mgr).await?;
1995 }
1996 TestCmd::CannotReplayTransaction => {
1997 let (process_mgr, _) = setup(common_args).await?;
1998 let dev_fed = dev_fed(&process_mgr).await?;
1999 cannot_replay_tx_test(dev_fed).await?;
2000 }
2001 TestCmd::UpgradeTests { binary, lnv2 } => {
2002 unsafe { std::env::set_var(FM_ENABLE_MODULE_LNV2_ENV, lnv2) };
2004 let (process_mgr, _) = setup(common_args).await?;
2005 Box::pin(upgrade_tests(&process_mgr, binary)).await?;
2006 }
2007 }
2008 Ok(())
2009}