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