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
322pub struct UpgradeClients {
324 reissue_client: Client,
325 ln_send_client: Client,
326 ln_receive_client: Client,
327 fm_pay_client: Client,
328}
329
330async fn stress_test_fed(dev_fed: &DevFed, clients: Option<&UpgradeClients>) -> anyhow::Result<()> {
331 use futures::FutureExt;
332
333 let assert_thresholds = false;
336
337 let iterations = 1;
340
341 let restore_test = if clients.is_some() {
344 futures::future::ok(()).right_future()
345 } else {
346 latency_tests(
347 dev_fed.clone(),
348 LatencyTest::Restore,
349 clients,
350 iterations,
351 assert_thresholds,
352 )
353 .left_future()
354 };
355
356 latency_tests(
359 dev_fed.clone(),
360 LatencyTest::Reissue,
361 clients,
362 iterations,
363 assert_thresholds,
364 )
365 .await?;
366
367 latency_tests(
368 dev_fed.clone(),
369 LatencyTest::LnSend,
370 clients,
371 iterations,
372 assert_thresholds,
373 )
374 .await?;
375
376 latency_tests(
377 dev_fed.clone(),
378 LatencyTest::LnReceive,
379 clients,
380 iterations,
381 assert_thresholds,
382 )
383 .await?;
384
385 latency_tests(
386 dev_fed.clone(),
387 LatencyTest::FmPay,
388 clients,
389 iterations,
390 assert_thresholds,
391 )
392 .await?;
393
394 restore_test.await?;
395
396 Ok(())
397}
398
399pub async fn upgrade_tests(process_mgr: &ProcessManager, binary: UpgradeTest) -> Result<()> {
400 match binary {
401 UpgradeTest::Fedimintd { paths } => {
402 if let Some(oldest_fedimintd) = paths.first() {
403 unsafe { std::env::set_var("FM_FEDIMINTD_BASE_EXECUTABLE", oldest_fedimintd) };
405 } else {
406 bail!("Must provide at least 1 binary path");
407 }
408
409 let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
410 info!(
411 "running first stress test for fedimintd version: {}",
412 fedimintd_version
413 );
414
415 let mut dev_fed = dev_fed(process_mgr).await?;
416 let client = dev_fed.fed.new_joined_client("test-client").await?;
417 try_join!(stress_test_fed(&dev_fed, None), client.wait_session())?;
418
419 for path in paths.iter().skip(1) {
420 dev_fed.fed.restart_all_with_bin(process_mgr, path).await?;
421
422 try_join!(stress_test_fed(&dev_fed, None), client.wait_session())?;
424
425 let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
426 info!(
427 "### fedimintd passed stress test for version {}",
428 fedimintd_version
429 );
430 }
431 info!("## fedimintd upgraded all binaries successfully");
432 }
433 UpgradeTest::FedimintCli { paths } => {
434 let set_fedimint_cli_path = |path: &PathBuf| {
435 unsafe { std::env::set_var("FM_FEDIMINT_CLI_BASE_EXECUTABLE", path) };
437 let fm_mint_client: String = format!(
438 "{fedimint_cli} --data-dir {datadir}",
439 fedimint_cli = crate::util::get_fedimint_cli_path().join(" "),
440 datadir = crate::vars::utf8(&process_mgr.globals.FM_CLIENT_DIR)
441 );
442 unsafe { std::env::set_var("FM_MINT_CLIENT", fm_mint_client) };
444 };
445
446 if let Some(oldest_fedimint_cli) = paths.first() {
447 set_fedimint_cli_path(oldest_fedimint_cli);
448 } else {
449 bail!("Must provide at least 1 binary path");
450 }
451
452 let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
453 info!(
454 "running first stress test for fedimint-cli version: {}",
455 fedimint_cli_version
456 );
457
458 let dev_fed = dev_fed(process_mgr).await?;
459
460 let wait_session_client = dev_fed.fed.new_joined_client("wait-session-client").await?;
461 let reusable_upgrade_clients = UpgradeClients {
462 reissue_client: dev_fed.fed.new_joined_client("reissue-client").await?,
463 ln_send_client: dev_fed.fed.new_joined_client("ln-send-client").await?,
464 ln_receive_client: dev_fed.fed.new_joined_client("ln-receive-client").await?,
465 fm_pay_client: dev_fed.fed.new_joined_client("fm-pay-client").await?,
466 };
467
468 try_join!(
469 stress_test_fed(&dev_fed, Some(&reusable_upgrade_clients)),
470 wait_session_client.wait_session()
471 )?;
472
473 for path in paths.iter().skip(1) {
474 set_fedimint_cli_path(path);
475 let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
476 info!("upgraded fedimint-cli to version: {}", fedimint_cli_version);
477 try_join!(
478 stress_test_fed(&dev_fed, Some(&reusable_upgrade_clients)),
479 wait_session_client.wait_session()
480 )?;
481 info!(
482 "### fedimint-cli passed stress test for version {}",
483 fedimint_cli_version
484 );
485 }
486 info!("## fedimint-cli upgraded all binaries successfully");
487 }
488 UpgradeTest::Gatewayd {
489 gatewayd_paths,
490 gateway_cli_paths,
491 } => {
492 if let Some(oldest_gatewayd) = gatewayd_paths.first() {
493 unsafe { std::env::set_var("FM_GATEWAYD_BASE_EXECUTABLE", oldest_gatewayd) };
495 } else {
496 bail!("Must provide at least 1 gatewayd path");
497 }
498
499 if let Some(oldest_gateway_cli) = gateway_cli_paths.first() {
500 unsafe { std::env::set_var("FM_GATEWAY_CLI_BASE_EXECUTABLE", oldest_gateway_cli) };
502 } else {
503 bail!("Must provide at least 1 gateway-cli path");
504 }
505
506 let gatewayd_version = crate::util::Gatewayd::version_or_default().await;
507 let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
508 info!(
509 ?gatewayd_version,
510 ?gateway_cli_version,
511 "running first stress test for gateway",
512 );
513
514 let mut dev_fed = dev_fed(process_mgr).await?;
515 let client = dev_fed.fed.new_joined_client("test-client").await?;
516 try_join!(stress_test_fed(&dev_fed, None), client.wait_session())?;
517
518 for i in 1..gatewayd_paths.len() {
519 info!(
520 "running stress test with gatewayd path {:?}",
521 gatewayd_paths.get(i)
522 );
523 let new_gatewayd_path = gatewayd_paths.get(i).expect("Not enough gatewayd paths");
524 let new_gateway_cli_path = gateway_cli_paths
525 .get(i)
526 .expect("Not enough gateway-cli paths");
527
528 let gateways = vec![&mut dev_fed.gw_lnd];
529
530 try_join_all(gateways.into_iter().map(|gateway| {
531 gateway.restart_with_bin(process_mgr, new_gatewayd_path, new_gateway_cli_path)
532 }))
533 .await?;
534
535 dev_fed.fed.await_gateways_registered().await?;
536 try_join!(stress_test_fed(&dev_fed, None), client.wait_session())?;
537 let gatewayd_version = crate::util::Gatewayd::version_or_default().await;
538 let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
539 info!(
540 ?gatewayd_version,
541 ?gateway_cli_version,
542 "### gateway passed stress test for version",
543 );
544 }
545
546 info!("## gatewayd upgraded all binaries successfully");
547 }
548 }
549 Ok(())
550}
551
552pub async fn cli_tests(dev_fed: DevFed) -> Result<()> {
553 log_binary_versions().await?;
554 let data_dir = env::var(FM_DATA_DIR_ENV)?;
555
556 let DevFed {
557 bitcoind,
558 lnd,
559 fed,
560 gw_lnd,
561 gw_ldk,
562 ..
563 } = dev_fed;
564
565 let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
566
567 let client = fed.new_joined_client("cli-tests-client").await?;
568 let lnd_gw_id = gw_lnd.gateway_id().await?;
569
570 cmd!(
571 client,
572 "dev",
573 "config-decrypt",
574 "--in-file={data_dir}/fedimintd-default-0/private.encrypt",
575 "--out-file={data_dir}/fedimintd-default-0/config-plaintext.json"
576 )
577 .env(FM_PASSWORD_ENV, "pass")
578 .run()
579 .await?;
580
581 cmd!(
582 client,
583 "dev",
584 "config-encrypt",
585 "--in-file={data_dir}/fedimintd-default-0/config-plaintext.json",
586 "--out-file={data_dir}/fedimintd-default-0/config-2"
587 )
588 .env(FM_PASSWORD_ENV, "pass-foo")
589 .run()
590 .await?;
591
592 cmd!(
593 client,
594 "dev",
595 "config-decrypt",
596 "--in-file={data_dir}/fedimintd-default-0/config-2",
597 "--out-file={data_dir}/fedimintd-default-0/config-plaintext-2.json"
598 )
599 .env(FM_PASSWORD_ENV, "pass-foo")
600 .run()
601 .await?;
602
603 let plaintext_one = fs::read_to_string(format!(
604 "{data_dir}/fedimintd-default-0/config-plaintext.json"
605 ))
606 .await?;
607 let plaintext_two = fs::read_to_string(format!(
608 "{data_dir}/fedimintd-default-0/config-plaintext-2.json"
609 ))
610 .await?;
611 anyhow::ensure!(
612 plaintext_one == plaintext_two,
613 "config-decrypt/encrypt failed"
614 );
615
616 fed.pegin_gateways(10_000_000, vec![&gw_lnd]).await?;
617
618 let fed_id = fed.calculate_federation_id();
619 let invite = fed.invite_code()?;
620
621 gw_lnd
623 .set_federation_routing_fee(fed_id.clone(), 0, 0)
624 .await?;
625 cmd!(client, "list-gateways").run().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 let peer_0_fedimintd_version = cmd!(client, "dev", "peer-version", "--peer-id", "0")
867 .out_json()
868 .await?
869 .get("version")
870 .expect("Output didn't contain version")
871 .as_str()
872 .unwrap()
873 .to_owned();
874
875 assert_eq!(
876 semver::Version::parse(&peer_0_fedimintd_version)?,
877 fedimintd_version
878 );
879
880 retry(
881 "Check initial announcements",
882 aggressive_backoff(),
883 || async {
884 let initial_announcements =
886 serde_json::from_value::<BTreeMap<PeerId, SignedApiAnnouncement>>(
887 cmd!(client, "dev", "api-announcements",).out_json().await?,
888 )
889 .expect("failed to parse API announcements");
890
891 if fed.members.len() != initial_announcements.len() {
892 bail!(
893 "Not all announcements ready: {}",
894 initial_announcements.len()
895 )
896 }
897
898 if !initial_announcements
899 .values()
900 .all(|announcement| announcement.api_announcement.nonce == 0)
901 {
902 bail!("Not all announcements have their initial value");
903 }
904 Ok(())
905 },
906 )
907 .await?;
908
909 const NEW_API_URL: &str = "ws://127.0.0.1:4242";
910 let new_announcement = serde_json::from_value::<SignedApiAnnouncement>(
911 cmd!(
912 client,
913 "--our-id",
914 "0",
915 "--password",
916 "pass",
917 "admin",
918 "sign-api-announcement",
919 NEW_API_URL
920 )
921 .out_json()
922 .await?,
923 )
924 .expect("Couldn't parse signed announcement");
925
926 assert_eq!(
927 new_announcement.api_announcement.nonce, 1,
928 "Nonce did not increment correctly"
929 );
930
931 info!("Testing if the client syncs the announcement");
932 let announcement = poll("Waiting for the announcement to propagate", || async {
933 cmd!(client, "dev", "wait", "1")
934 .run()
935 .await
936 .map_err(ControlFlow::Break)?;
937
938 let new_announcements_peer2 =
939 serde_json::from_value::<BTreeMap<PeerId, SignedApiAnnouncement>>(
940 cmd!(client, "dev", "api-announcements",)
941 .out_json()
942 .await
943 .map_err(ControlFlow::Break)?,
944 )
945 .expect("failed to parse API announcements");
946
947 let announcement = new_announcements_peer2[&PeerId::from(0)]
948 .api_announcement
949 .clone();
950 if announcement.nonce == 1 {
951 Ok(announcement)
952 } else {
953 Err(ControlFlow::Continue(anyhow!(
954 "Haven't received updated announcement yet"
955 )))
956 }
957 })
958 .await?;
959
960 assert_eq!(
961 announcement.api_url,
962 NEW_API_URL.parse().expect("valid URL")
963 );
964
965 Ok(())
966}
967
968pub async fn cli_load_test_tool_test(dev_fed: DevFed) -> Result<()> {
969 log_binary_versions().await?;
970 let data_dir = env::var(FM_DATA_DIR_ENV)?;
971 let load_test_temp = PathBuf::from(data_dir).join("load-test-temp");
972 dev_fed
973 .fed
974 .pegin_client(10_000, dev_fed.fed.internal_client().await?)
975 .await?;
976 let invite_code = dev_fed.fed.invite_code()?;
977 dev_fed
978 .gw_lnd
979 .set_federation_routing_fee(dev_fed.fed.calculate_federation_id(), 0, 0)
980 .await?;
981 run_standard_load_test(&load_test_temp, &invite_code).await?;
982 run_ln_circular_load_test(&load_test_temp, &invite_code).await?;
983 Ok(())
984}
985
986pub async fn run_standard_load_test(
987 load_test_temp: &Path,
988 invite_code: &str,
989) -> anyhow::Result<()> {
990 let output = cmd!(
991 LoadTestTool,
992 "--archive-dir",
993 load_test_temp.display(),
994 "--users",
995 "1",
996 "load-test",
997 "--notes-per-user",
998 "1",
999 "--generate-invoice-with",
1000 "ldk-lightning-cli",
1001 "--invite-code",
1002 invite_code
1003 )
1004 .out_string()
1005 .await?;
1006 println!("{output}");
1007 anyhow::ensure!(
1008 output.contains("2 reissue_notes"),
1009 "reissued different number notes than expected"
1010 );
1011 anyhow::ensure!(
1012 output.contains("1 gateway_pay_invoice"),
1013 "paid different number of invoices than expected"
1014 );
1015 Ok(())
1016}
1017
1018pub async fn run_ln_circular_load_test(
1019 load_test_temp: &Path,
1020 invite_code: &str,
1021) -> anyhow::Result<()> {
1022 info!("Testing ln-circular-load-test with 'two-gateways' strategy");
1023 let output = cmd!(
1024 LoadTestTool,
1025 "--archive-dir",
1026 load_test_temp.display(),
1027 "--users",
1028 "1",
1029 "ln-circular-load-test",
1030 "--strategy",
1031 "two-gateways",
1032 "--test-duration-secs",
1033 "2",
1034 "--invite-code",
1035 invite_code
1036 )
1037 .out_string()
1038 .await?;
1039 println!("{output}");
1040 anyhow::ensure!(
1041 output.contains("gateway_create_invoice"),
1042 "missing invoice creation"
1043 );
1044 anyhow::ensure!(
1045 output.contains("gateway_pay_invoice_success"),
1046 "missing invoice payment"
1047 );
1048 anyhow::ensure!(
1049 output.contains("gateway_payment_received_success"),
1050 "missing received payment"
1051 );
1052
1053 info!("Testing ln-circular-load-test with 'partner-ping-pong' strategy");
1054 let output = cmd!(
1058 LoadTestTool,
1059 "--archive-dir",
1060 load_test_temp.display(),
1061 "--users",
1062 "1",
1063 "ln-circular-load-test",
1064 "--strategy",
1065 "partner-ping-pong",
1066 "--test-duration-secs",
1067 "6",
1068 "--invite-code",
1069 invite_code
1070 )
1071 .out_string()
1072 .await?;
1073 println!("{output}");
1074 anyhow::ensure!(
1075 output.contains("gateway_create_invoice"),
1076 "missing invoice creation"
1077 );
1078 anyhow::ensure!(
1079 output.contains("gateway_payment_received_success"),
1080 "missing received payment"
1081 );
1082
1083 info!("Testing ln-circular-load-test with 'self-payment' strategy");
1084 let output = cmd!(
1086 LoadTestTool,
1087 "--archive-dir",
1088 load_test_temp.display(),
1089 "--users",
1090 "1",
1091 "ln-circular-load-test",
1092 "--strategy",
1093 "self-payment",
1094 "--test-duration-secs",
1095 "2",
1096 "--invite-code",
1097 invite_code
1098 )
1099 .out_string()
1100 .await?;
1101 println!("{output}");
1102 anyhow::ensure!(
1103 output.contains("gateway_create_invoice"),
1104 "missing invoice creation"
1105 );
1106 anyhow::ensure!(
1107 output.contains("gateway_payment_received_success"),
1108 "missing received payment"
1109 );
1110 Ok(())
1111}
1112
1113pub async fn lightning_gw_reconnect_test(
1114 dev_fed: DevFed,
1115 process_mgr: &ProcessManager,
1116) -> Result<()> {
1117 log_binary_versions().await?;
1118
1119 let DevFed {
1120 bitcoind,
1121 lnd,
1122 fed,
1123 mut gw_lnd,
1124 gw_ldk,
1125 ..
1126 } = dev_fed;
1127
1128 let client = fed
1129 .new_joined_client("lightning-gw-reconnect-test-client")
1130 .await?;
1131
1132 info!("Pegging-in both gateways");
1133 fed.pegin_gateways(99_999, vec![&gw_lnd]).await?;
1134
1135 drop(lnd);
1137
1138 tracing::info!("Stopping LND");
1139 let mut info_cmd = cmd!(gw_lnd, "info");
1141 assert!(info_cmd.run().await.is_ok());
1142
1143 let ln_type = gw_lnd.ln.ln_type().to_string();
1146 gw_lnd.stop_lightning_node().await?;
1147 let lightning_info = info_cmd.out_json().await?;
1148 let lightning_pub_key: Option<String> =
1149 serde_json::from_value(lightning_info["lightning_pub_key"].clone())?;
1150
1151 assert!(lightning_pub_key.is_none());
1152
1153 tracing::info!("Restarting LND...");
1155 let new_lnd = Lnd::new(process_mgr, bitcoind.clone()).await?;
1156 gw_lnd.set_lightning_node(LightningNode::Lnd(new_lnd.clone()));
1157
1158 tracing::info!("Retrying info...");
1159 const MAX_RETRIES: usize = 30;
1160 const RETRY_INTERVAL: Duration = Duration::from_secs(1);
1161
1162 for i in 0..MAX_RETRIES {
1163 match do_try_create_and_pay_invoice(&gw_lnd, &client, &gw_ldk).await {
1164 Ok(()) => break,
1165 Err(e) => {
1166 if i == MAX_RETRIES - 1 {
1167 return Err(e);
1168 }
1169 tracing::debug!(
1170 "Pay invoice for gateway {} failed with {e:?}, retrying in {} seconds (try {}/{MAX_RETRIES})",
1171 ln_type,
1172 RETRY_INTERVAL.as_secs(),
1173 i + 1,
1174 );
1175 fedimint_core::task::sleep_in_test(
1176 "paying invoice for gateway failed",
1177 RETRY_INTERVAL,
1178 )
1179 .await;
1180 }
1181 }
1182 }
1183
1184 info!(target: LOG_DEVIMINT, "lightning_reconnect_test: success");
1185 Ok(())
1186}
1187
1188pub async fn gw_reboot_test(dev_fed: DevFed, process_mgr: &ProcessManager) -> Result<()> {
1189 log_binary_versions().await?;
1190
1191 let DevFed {
1192 bitcoind,
1193 lnd,
1194 fed,
1195 gw_lnd,
1196 gw_ldk,
1197 gw_ldk_second,
1198 ..
1199 } = dev_fed;
1200
1201 let client = fed.new_joined_client("gw-reboot-test-client").await?;
1202 fed.pegin_client(10_000, &client).await?;
1203
1204 let block_height = bitcoind.get_block_count().await? - 1;
1206 try_join!(
1207 gw_lnd.wait_for_block_height(block_height),
1208 gw_ldk.wait_for_block_height(block_height),
1209 )?;
1210
1211 let (lnd_value, ldk_value) = try_join!(gw_lnd.get_info(), gw_ldk.get_info())?;
1213
1214 let lnd_gateway_id = gw_lnd.gateway_id().await?;
1216 let gw_ldk_name = gw_ldk.gw_name.clone();
1217 let gw_ldk_port = gw_ldk.gw_port;
1218 let gw_lightning_port = gw_ldk.ldk_port;
1219 drop(gw_lnd);
1220 drop(gw_ldk);
1221
1222 info!("Making payment while gateway is down");
1225 let initial_client_balance = client.balance().await?;
1226 let invoice = gw_ldk_second.create_invoice(3000).await?;
1227 ln_pay(&client, invoice.to_string(), lnd_gateway_id, false)
1228 .await
1229 .expect_err("Expected ln-pay to return error because the gateway is not online");
1230 let new_client_balance = client.balance().await?;
1231 anyhow::ensure!(initial_client_balance == new_client_balance);
1232
1233 info!("Rebooting gateways...");
1235 let chain_source = if crate::util::Gatewayd::version_or_default().await < *VERSION_0_7_0_ALPHA {
1236 LdkChainSource::Bitcoind
1237 } else {
1238 LdkChainSource::Esplora
1240 };
1241 let (new_gw_lnd, new_gw_ldk) = try_join!(
1242 Gatewayd::new(process_mgr, LightningNode::Lnd(lnd.clone())),
1243 Gatewayd::new(
1244 process_mgr,
1245 LightningNode::Ldk {
1246 name: gw_ldk_name,
1247 gw_port: gw_ldk_port,
1248 ldk_port: gw_lightning_port,
1249 chain_source,
1250 }
1251 )
1252 )?;
1253
1254 let lnd_gateway_id: fedimint_core::secp256k1::PublicKey =
1255 serde_json::from_value(lnd_value["gateway_id"].clone())?;
1256
1257 poll(
1258 "Waiting for LND Gateway Running state after reboot",
1259 || async {
1260 let mut new_lnd_cmd = cmd!(new_gw_lnd, "info");
1261 let lnd_value = new_lnd_cmd.out_json().await.map_err(ControlFlow::Continue)?;
1262 let reboot_gateway_state: String = serde_json::from_value(lnd_value["gateway_state"].clone()).context("invalid gateway state").map_err(ControlFlow::Break)?;
1263 let reboot_gateway_id: fedimint_core::secp256k1::PublicKey =
1264 serde_json::from_value(lnd_value["gateway_id"].clone()).context("invalid gateway id").map_err(ControlFlow::Break)?;
1265
1266 if reboot_gateway_state == "Running" {
1267 info!(target: LOG_DEVIMINT, "LND Gateway restarted, with auto-rejoin to federation");
1268 assert_eq!(lnd_gateway_id, reboot_gateway_id);
1270 return Ok(());
1271 }
1272 Err(ControlFlow::Continue(anyhow!("gateway not running")))
1273 },
1274 )
1275 .await?;
1276
1277 let ldk_gateway_id: fedimint_core::secp256k1::PublicKey =
1278 serde_json::from_value(ldk_value["gateway_id"].clone())?;
1279 poll(
1280 "Waiting for LDK Gateway Running state after reboot",
1281 || async {
1282 let mut new_ldk_cmd = cmd!(new_gw_ldk, "info");
1283 let ldk_value = new_ldk_cmd.out_json().await.map_err(ControlFlow::Continue)?;
1284 let reboot_gateway_state: String = serde_json::from_value(ldk_value["gateway_state"].clone()).context("invalid gateway state").map_err(ControlFlow::Break)?;
1285 let reboot_gateway_id: fedimint_core::secp256k1::PublicKey =
1286 serde_json::from_value(ldk_value["gateway_id"].clone()).context("invalid gateway id").map_err(ControlFlow::Break)?;
1287
1288 if reboot_gateway_state == "Running" {
1289 info!(target: LOG_DEVIMINT, "LDK Gateway restarted, with auto-rejoin to federation");
1290 assert_eq!(ldk_gateway_id, reboot_gateway_id);
1292 return Ok(());
1293 }
1294 Err(ControlFlow::Continue(anyhow!("gateway not running")))
1295 },
1296 )
1297 .await?;
1298
1299 info!(LOG_DEVIMINT, "gateway_reboot_test: success");
1300 Ok(())
1301}
1302
1303pub async fn do_try_create_and_pay_invoice(
1304 gw_lnd: &Gatewayd,
1305 client: &Client,
1306 gw_ldk: &Gatewayd,
1307) -> anyhow::Result<()> {
1308 poll("Waiting for info to succeed after restart", || async {
1312 let lightning_pub_key = cmd!(gw_lnd, "info")
1313 .out_json()
1314 .await
1315 .map_err(ControlFlow::Continue)?
1316 .get("lightning_pub_key")
1317 .map(|ln_pk| {
1318 serde_json::from_value::<Option<String>>(ln_pk.clone())
1319 .expect("could not parse lightning_pub_key")
1320 })
1321 .expect("missing lightning_pub_key");
1322
1323 poll_eq!(lightning_pub_key.is_some(), true)
1324 })
1325 .await?;
1326
1327 tracing::info!("Creating invoice....");
1328 let invoice = ln_invoice(
1329 client,
1330 Amount::from_msats(1000),
1331 "incoming-over-lnd-gw".to_string(),
1332 gw_lnd.gateway_id().await?,
1333 )
1334 .await?
1335 .invoice;
1336
1337 match &gw_lnd.ln.ln_type() {
1338 LightningNodeType::Lnd => {
1339 gw_ldk
1341 .pay_invoice(Bolt11Invoice::from_str(&invoice).expect("Could not parse invoice"))
1342 .await?;
1343 }
1344 LightningNodeType::Ldk => {
1345 unimplemented!("do_try_create_and_pay_invoice not implemented for LDK yet");
1346 }
1347 }
1348 Ok(())
1349}
1350
1351async fn ln_pay(
1352 client: &Client,
1353 invoice: String,
1354 gw_id: String,
1355 finish_in_background: bool,
1356) -> anyhow::Result<String> {
1357 let value = if finish_in_background {
1358 cmd!(
1359 client,
1360 "ln-pay",
1361 invoice,
1362 "--finish-in-background",
1363 "--gateway-id",
1364 gw_id,
1365 )
1366 .out_json()
1367 .await?
1368 } else {
1369 cmd!(client, "ln-pay", invoice, "--gateway-id", gw_id,)
1370 .out_json()
1371 .await?
1372 };
1373
1374 let operation_id = value["operation_id"]
1375 .as_str()
1376 .ok_or(anyhow!("Failed to pay invoice"))?
1377 .to_string();
1378 Ok(operation_id)
1379}
1380
1381async fn ln_invoice(
1382 client: &Client,
1383 amount: Amount,
1384 description: String,
1385 gw_id: String,
1386) -> anyhow::Result<LnInvoiceResponse> {
1387 let ln_response_val = cmd!(
1388 client,
1389 "ln-invoice",
1390 "--amount",
1391 amount.msats,
1392 format!("--description='{description}'"),
1393 "--gateway-id",
1394 gw_id,
1395 )
1396 .out_json()
1397 .await?;
1398
1399 let ln_invoice_response: LnInvoiceResponse = serde_json::from_value(ln_response_val)?;
1400
1401 Ok(ln_invoice_response)
1402}
1403
1404pub async fn reconnect_test(dev_fed: DevFed, process_mgr: &ProcessManager) -> Result<()> {
1405 log_binary_versions().await?;
1406
1407 let DevFed {
1408 bitcoind, mut fed, ..
1409 } = dev_fed;
1410
1411 bitcoind.mine_blocks(110).await?;
1412 fed.await_block_sync().await?;
1413 fed.await_all_peers().await?;
1414
1415 fed.terminate_server(0).await?;
1417 fed.mine_then_wait_blocks_sync(100).await?;
1418
1419 fed.start_server(process_mgr, 0).await?;
1420 fed.mine_then_wait_blocks_sync(100).await?;
1421 fed.await_all_peers().await?;
1422 info!(target: LOG_DEVIMINT, "Server 0 successfully rejoined!");
1423 fed.mine_then_wait_blocks_sync(100).await?;
1424
1425 fed.terminate_server(1).await?;
1427 fed.mine_then_wait_blocks_sync(100).await?;
1428 fed.terminate_server(2).await?;
1429 fed.terminate_server(3).await?;
1430
1431 fed.start_server(process_mgr, 1).await?;
1432 fed.start_server(process_mgr, 2).await?;
1433 fed.start_server(process_mgr, 3).await?;
1434
1435 fed.await_all_peers().await?;
1436
1437 info!(target: LOG_DEVIMINT, "fm success: reconnect-test");
1438 Ok(())
1439}
1440
1441pub async fn recoverytool_test(dev_fed: DevFed) -> Result<()> {
1442 log_binary_versions().await?;
1443
1444 let DevFed { bitcoind, fed, .. } = dev_fed;
1445
1446 let data_dir = env::var(FM_DATA_DIR_ENV)?;
1447 let client = fed.new_joined_client("recoverytool-test-client").await?;
1448
1449 let mut fed_utxos_sats = HashSet::from([12_345_000, 23_456_000, 34_567_000]);
1450 let deposit_fees = fed.deposit_fees()?.msats / 1000;
1451 for sats in &fed_utxos_sats {
1452 fed.pegin_client(*sats - deposit_fees, &client).await?;
1454 }
1455
1456 async fn withdraw(
1457 client: &Client,
1458 bitcoind: &crate::external::Bitcoind,
1459 fed_utxos_sats: &mut HashSet<u64>,
1460 ) -> Result<()> {
1461 let withdrawal_address = bitcoind.get_new_address().await?;
1462 let withdraw_res = cmd!(
1463 client,
1464 "withdraw",
1465 "--address",
1466 &withdrawal_address,
1467 "--amount",
1468 "5000 sat"
1469 )
1470 .out_json()
1471 .await?;
1472
1473 let fees_sat = withdraw_res["fees_sat"]
1474 .as_u64()
1475 .expect("withdrawal should contain fees");
1476 let txid: Txid = withdraw_res["txid"]
1477 .as_str()
1478 .expect("withdrawal should contain txid string")
1479 .parse()
1480 .expect("txid should be parsable");
1481 let tx_hex = bitcoind.poll_get_transaction(txid).await?;
1482
1483 let tx = bitcoin::Transaction::consensus_decode_hex(&tx_hex, &ModuleRegistry::default())?;
1484 assert_eq!(tx.input.len(), 1);
1485 assert_eq!(tx.output.len(), 2);
1486
1487 let change_output = tx
1488 .output
1489 .iter()
1490 .find(|o| o.to_owned().script_pubkey != withdrawal_address.script_pubkey())
1491 .expect("withdrawal must have change output");
1492 assert!(fed_utxos_sats.insert(change_output.value.to_sat()));
1493
1494 let total_output_sats = tx.output.iter().map(|o| o.value.to_sat()).sum::<u64>();
1496 let input_sats = total_output_sats + fees_sat;
1497 assert!(fed_utxos_sats.remove(&input_sats));
1498
1499 Ok(())
1500 }
1501
1502 for _ in 0..2 {
1505 withdraw(&client, &bitcoind, &mut fed_utxos_sats).await?;
1506 }
1507
1508 let total_fed_sats = fed_utxos_sats.iter().sum::<u64>();
1509 fed.finalize_mempool_tx().await?;
1510
1511 let last_tx_session = client.get_session_count().await?;
1515
1516 info!("Recovering using utxos method");
1517 let output = cmd!(
1518 crate::util::Recoverytool,
1519 "--cfg",
1520 "{data_dir}/fedimintd-default-0",
1521 "utxos",
1522 "--db",
1523 "{data_dir}/fedimintd-default-0/database"
1524 )
1525 .env(FM_PASSWORD_ENV, "pass")
1526 .out_json()
1527 .await?;
1528 let outputs = output.as_array().context("expected an array")?;
1529 assert_eq!(outputs.len(), fed_utxos_sats.len());
1530
1531 assert_eq!(
1532 outputs
1533 .iter()
1534 .map(|o| o["amount_sat"].as_u64().unwrap())
1535 .collect::<HashSet<_>>(),
1536 fed_utxos_sats
1537 );
1538 let utxos_descriptors = outputs
1539 .iter()
1540 .map(|o| o["descriptor"].as_str().unwrap())
1541 .collect::<HashSet<_>>();
1542
1543 debug!(target: LOG_DEVIMINT, ?utxos_descriptors, "recoverytool descriptors using UTXOs method");
1544
1545 let descriptors_json = serde_json::value::to_raw_value(&serde_json::Value::Array(vec![
1546 serde_json::Value::Array(
1547 utxos_descriptors
1548 .iter()
1549 .map(|d| {
1550 json!({
1551 "desc": d,
1552 "timestamp": 0,
1553 })
1554 })
1555 .collect(),
1556 ),
1557 ]))?;
1558 info!("Getting wallet balances before import");
1559 let bitcoin_client = bitcoind.wallet_client().await?;
1560 let balances_before = bitcoin_client.get_balances().await?;
1561 info!("Importing descriptors into bitcoin wallet");
1562 let request = bitcoin_client
1563 .get_jsonrpc_client()
1564 .build_request("importdescriptors", Some(&descriptors_json));
1565 let response = block_in_place(|| bitcoin_client.get_jsonrpc_client().send_request(request))?;
1566 response.check_error()?;
1567 info!("Getting wallet balances after import");
1568 let balances_after = bitcoin_client.get_balances().await?;
1569 let diff = balances_after.mine.immature + balances_after.mine.trusted
1570 - balances_before.mine.immature
1571 - balances_before.mine.trusted;
1572
1573 client.wait_session_outcome(last_tx_session).await?;
1578
1579 assert_eq!(diff.to_sat(), total_fed_sats);
1581 info!("Recovering using epochs method");
1582
1583 let outputs = cmd!(
1584 crate::util::Recoverytool,
1585 "--cfg",
1586 "{data_dir}/fedimintd-default-0",
1587 "epochs",
1588 "--db",
1589 "{data_dir}/fedimintd-default-0/database"
1590 )
1591 .env(FM_PASSWORD_ENV, "pass")
1592 .out_json()
1593 .await?
1594 .as_array()
1595 .context("expected an array")?
1596 .clone();
1597
1598 let epochs_descriptors = outputs
1599 .iter()
1600 .map(|o| o["descriptor"].as_str().unwrap())
1601 .collect::<HashSet<_>>();
1602
1603 debug!(target: LOG_DEVIMINT, ?epochs_descriptors, "recoverytool descriptors using epochs method");
1604
1605 for utxo_descriptor in utxos_descriptors {
1608 assert!(epochs_descriptors.contains(utxo_descriptor));
1609 }
1610 Ok(())
1611}
1612
1613pub async fn guardian_backup_test(dev_fed: DevFed, process_mgr: &ProcessManager) -> Result<()> {
1614 let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
1615 const PEER_TO_TEST: u16 = 0;
1616
1617 log_binary_versions().await?;
1618
1619 let DevFed { mut fed, .. } = dev_fed;
1620
1621 fed.await_all_peers()
1622 .await
1623 .expect("Awaiting federation coming online failed");
1624
1625 let client = fed.new_joined_client("guardian-client").await?;
1626 let old_block_count = if fedimint_cli_version < *VERSION_0_6_0_ALPHA {
1627 cmd!(
1628 client,
1629 "dev",
1630 "api",
1631 "--peer-id",
1632 PEER_TO_TEST.to_string(),
1633 "module_{LEGACY_HARDCODED_INSTANCE_ID_WALLET}_block_count",
1634 )
1635 } else {
1636 cmd!(
1637 client,
1638 "dev",
1639 "api",
1640 "--peer-id",
1641 PEER_TO_TEST.to_string(),
1642 "--module",
1643 "wallet",
1644 "block_count",
1645 )
1646 }
1647 .out_json()
1648 .await?["value"]
1649 .as_u64()
1650 .expect("No block height returned");
1651
1652 let backup_res = cmd!(
1653 client,
1654 "--our-id",
1655 PEER_TO_TEST.to_string(),
1656 "--password",
1657 "pass",
1658 "admin",
1659 "guardian-config-backup"
1660 )
1661 .out_json()
1662 .await?;
1663 let backup_hex = backup_res["tar_archive_bytes"]
1664 .as_str()
1665 .expect("expected hex string");
1666 let backup_tar = hex::decode(backup_hex).expect("invalid hex");
1667
1668 let data_dir = fed
1669 .vars
1670 .get(&PEER_TO_TEST.into())
1671 .expect("peer not found")
1672 .FM_DATA_DIR
1673 .clone();
1674
1675 fed.terminate_server(PEER_TO_TEST.into())
1676 .await
1677 .expect("could not terminate fedimintd");
1678
1679 std::fs::remove_dir_all(&data_dir).expect("error deleting old datadir");
1680 std::fs::create_dir(&data_dir).expect("error creating new datadir");
1681
1682 let write_file = |name: &str, data: &[u8]| {
1683 let mut file = std::fs::File::options()
1684 .write(true)
1685 .create(true)
1686 .truncate(true)
1687 .open(data_dir.join(name))
1688 .expect("could not open file");
1689 file.write_all(data).expect("could not write file");
1690 file.flush().expect("could not flush file");
1691 };
1692
1693 write_file("backup.tar", &backup_tar);
1694 write_file(
1695 fedimint_server::config::io::PLAINTEXT_PASSWORD,
1696 "pass".as_bytes(),
1697 );
1698
1699 assert_eq!(
1700 std::process::Command::new("tar")
1701 .arg("-xf")
1702 .arg("backup.tar")
1703 .current_dir(data_dir)
1704 .spawn()
1705 .expect("error spawning tar")
1706 .wait()
1707 .expect("error extracting archive")
1708 .code(),
1709 Some(0),
1710 "tar failed"
1711 );
1712
1713 fed.start_server(process_mgr, PEER_TO_TEST.into())
1714 .await
1715 .expect("could not restart fedimintd");
1716
1717 poll("Peer catches up again", || async {
1718 let block_counts = all_peer_block_count(&client, fed.member_ids())
1719 .await
1720 .map_err(ControlFlow::Continue)?;
1721 let block_count = block_counts[&PeerId::from(PEER_TO_TEST)];
1722
1723 info!("Caught up to block {block_count} of at least {old_block_count} (counts={block_counts:?})");
1724
1725 if block_count < old_block_count {
1726 return Err(ControlFlow::Continue(anyhow!("Block count still behind")));
1727 }
1728
1729 Ok(())
1730 })
1731 .await
1732 .expect("Peer didn't rejoin federation");
1733
1734 Ok(())
1735}
1736
1737async fn peer_block_count(client: &Client, peer: PeerId) -> Result<u64> {
1738 cmd!(
1739 client,
1740 "dev",
1741 "api",
1742 "--peer-id",
1743 peer.to_string(),
1744 "module_{LEGACY_HARDCODED_INSTANCE_ID_WALLET}_block_count",
1745 )
1746 .out_json()
1747 .await?["value"]
1748 .as_u64()
1749 .context("No block height returned")
1750}
1751
1752async fn all_peer_block_count(
1753 client: &Client,
1754 peers: impl Iterator<Item = PeerId>,
1755) -> Result<BTreeMap<PeerId, u64>> {
1756 let mut peer_heights = BTreeMap::new();
1757 for peer in peers {
1758 peer_heights.insert(peer, peer_block_count(client, peer).await?);
1759 }
1760 Ok(peer_heights)
1761}
1762
1763pub async fn cannot_replay_tx_test(dev_fed: DevFed) -> Result<()> {
1764 log_binary_versions().await?;
1765
1766 let DevFed { fed, .. } = dev_fed;
1767
1768 let client = fed.new_joined_client("cannot-replay-client").await?;
1769
1770 const CLIENT_START_AMOUNT: u64 = 5_000_000_000;
1772 const CLIENT_SPEND_AMOUNT: u64 = 5_000_000_000;
1773
1774 let initial_client_balance = client.balance().await?;
1775 assert_eq!(initial_client_balance, 0);
1776
1777 fed.pegin_client(CLIENT_START_AMOUNT / 1000, &client)
1778 .await?;
1779
1780 let double_spend_client = client.new_forked("double-spender").await?;
1782
1783 let notes = cmd!(client, "spend", CLIENT_SPEND_AMOUNT)
1785 .out_json()
1786 .await?
1787 .get("notes")
1788 .expect("Output didn't contain e-cash notes")
1789 .as_str()
1790 .unwrap()
1791 .to_owned();
1792
1793 let client_post_spend_balance = client.balance().await?;
1794 assert_eq!(
1795 client_post_spend_balance,
1796 CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT
1797 );
1798
1799 cmd!(client, "reissue", notes).out_json().await?;
1800 let client_post_reissue_balance = client.balance().await?;
1801 assert_eq!(client_post_reissue_balance, CLIENT_START_AMOUNT);
1802
1803 let double_spend_notes = cmd!(double_spend_client, "spend", CLIENT_SPEND_AMOUNT)
1805 .out_json()
1806 .await?
1807 .get("notes")
1808 .expect("Output didn't contain e-cash notes")
1809 .as_str()
1810 .unwrap()
1811 .to_owned();
1812
1813 let double_spend_client_post_spend_balance = double_spend_client.balance().await?;
1814 assert_eq!(
1815 double_spend_client_post_spend_balance,
1816 CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT
1817 );
1818
1819 cmd!(double_spend_client, "reissue", double_spend_notes)
1820 .assert_error_contains("The transaction had an invalid input")
1821 .await?;
1822
1823 let double_spend_client_post_spend_balance = double_spend_client.balance().await?;
1824 assert_eq!(
1825 double_spend_client_post_spend_balance,
1826 CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT
1827 );
1828
1829 Ok(())
1830}
1831
1832#[derive(Subcommand)]
1833pub enum LatencyTest {
1834 Reissue,
1835 LnSend,
1836 LnReceive,
1837 FmPay,
1838 Restore,
1839}
1840
1841#[derive(Subcommand)]
1842pub enum UpgradeTest {
1843 Fedimintd {
1844 #[arg(long, trailing_var_arg = true, num_args=1..)]
1845 paths: Vec<PathBuf>,
1846 },
1847 FedimintCli {
1848 #[arg(long, trailing_var_arg = true, num_args=1..)]
1849 paths: Vec<PathBuf>,
1850 },
1851 Gatewayd {
1852 #[arg(long, trailing_var_arg = true, num_args=1..)]
1853 gatewayd_paths: Vec<PathBuf>,
1854 #[arg(long, trailing_var_arg = true, num_args=1..)]
1855 gateway_cli_paths: Vec<PathBuf>,
1856 },
1857}
1858
1859#[derive(Subcommand)]
1860pub enum TestCmd {
1861 LatencyTests {
1864 #[clap(subcommand)]
1865 r#type: LatencyTest,
1866
1867 #[arg(long, default_value = "10")]
1868 iterations: usize,
1869 },
1870 ReconnectTest,
1873 CliTests,
1875 LoadTestToolTest,
1878 LightningReconnectTest,
1881 GatewayRebootTest,
1884 RecoverytoolTests,
1886 WasmTestSetup {
1888 #[arg(long, trailing_var_arg = true, allow_hyphen_values = true, num_args=1..)]
1889 exec: Option<Vec<ffi::OsString>>,
1890 },
1891 GuardianBackup,
1893 CannotReplayTransaction,
1895 UpgradeTests {
1897 #[clap(subcommand)]
1898 binary: UpgradeTest,
1899 #[arg(long)]
1900 lnv2: String,
1901 },
1902}
1903
1904pub async fn handle_command(cmd: TestCmd, common_args: CommonArgs) -> Result<()> {
1905 match cmd {
1906 TestCmd::WasmTestSetup { exec } => {
1907 let (process_mgr, task_group) = setup(common_args).await?;
1908 let main = {
1909 let task_group = task_group.clone();
1910 async move {
1911 let dev_fed = dev_fed(&process_mgr).await?;
1912 let gw_lnd = dev_fed.gw_lnd.clone();
1913 let fed = dev_fed.fed.clone();
1914 gw_lnd
1915 .set_federation_routing_fee(dev_fed.fed.calculate_federation_id(), 0, 0)
1916 .await?;
1917 task_group.spawn_cancellable("faucet", async move {
1918 if let Err(err) = crate::faucet::run(
1919 &dev_fed,
1920 format!("0.0.0.0:{}", process_mgr.globals.FM_PORT_FAUCET),
1921 process_mgr.globals.FM_PORT_GW_LND,
1922 )
1923 .await
1924 {
1925 error!("Error spawning faucet: {err}");
1926 }
1927 });
1928 try_join!(fed.pegin_gateways(30_000, vec![&gw_lnd]), async {
1929 poll("waiting for faucet startup", || async {
1930 TcpStream::connect(format!(
1931 "127.0.0.1:{}",
1932 process_mgr.globals.FM_PORT_FAUCET
1933 ))
1934 .await
1935 .context("connect to faucet")
1936 .map_err(ControlFlow::Continue)
1937 })
1938 .await?;
1939 Ok(())
1940 },)?;
1941 if let Some(exec) = exec {
1942 exec_user_command(exec).await?;
1943 task_group.shutdown();
1944 }
1945 Ok::<_, anyhow::Error>(())
1946 }
1947 };
1948 cleanup_on_exit(main, task_group).await?;
1949 }
1950 TestCmd::LatencyTests { r#type, iterations } => {
1951 let (process_mgr, _) = setup(common_args).await?;
1952 let dev_fed = dev_fed(&process_mgr).await?;
1953 latency_tests(dev_fed, r#type, None, iterations, true).await?;
1954 }
1955 TestCmd::ReconnectTest => {
1956 let (process_mgr, _) = setup(common_args).await?;
1957 let dev_fed = dev_fed(&process_mgr).await?;
1958 reconnect_test(dev_fed, &process_mgr).await?;
1959 }
1960 TestCmd::CliTests => {
1961 let (process_mgr, _) = setup(common_args).await?;
1962 let dev_fed = dev_fed(&process_mgr).await?;
1963 cli_tests(dev_fed).await?;
1964 }
1965 TestCmd::LoadTestToolTest => {
1966 let (process_mgr, _) = setup(common_args).await?;
1967 let dev_fed = dev_fed(&process_mgr).await?;
1968 cli_load_test_tool_test(dev_fed).await?;
1969 }
1970 TestCmd::LightningReconnectTest => {
1971 let (process_mgr, _) = setup(common_args).await?;
1972 let dev_fed = dev_fed(&process_mgr).await?;
1973 lightning_gw_reconnect_test(dev_fed, &process_mgr).await?;
1974 }
1975 TestCmd::GatewayRebootTest => {
1976 let (process_mgr, _) = setup(common_args).await?;
1977 let dev_fed = dev_fed(&process_mgr).await?;
1978 gw_reboot_test(dev_fed, &process_mgr).await?;
1979 }
1980 TestCmd::RecoverytoolTests => {
1981 let (process_mgr, _) = setup(common_args).await?;
1982 let dev_fed = dev_fed(&process_mgr).await?;
1983 recoverytool_test(dev_fed).await?;
1984 }
1985 TestCmd::GuardianBackup => {
1986 let (process_mgr, _) = setup(common_args).await?;
1987 let dev_fed = dev_fed(&process_mgr).await?;
1988 guardian_backup_test(dev_fed, &process_mgr).await?;
1989 }
1990 TestCmd::CannotReplayTransaction => {
1991 let (process_mgr, _) = setup(common_args).await?;
1992 let dev_fed = dev_fed(&process_mgr).await?;
1993 cannot_replay_tx_test(dev_fed).await?;
1994 }
1995 TestCmd::UpgradeTests { binary, lnv2 } => {
1996 unsafe { std::env::set_var(FM_ENABLE_MODULE_LNV2_ENV, lnv2) };
1998 let (process_mgr, _) = setup(common_args).await?;
1999 Box::pin(upgrade_tests(&process_mgr, binary)).await?;
2000 }
2001 }
2002 Ok(())
2003}