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