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