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