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 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.addr, &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.addr, 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 fed_id = fed.calculate_federation_id();
646 let invite = fed.invite_code()?;
647
648 let invite_code = cmd!(client, "dev", "decode", "invite-code", invite.clone())
649 .out_json()
650 .await?;
651
652 let encode_invite_output = cmd!(
653 client,
654 "dev",
655 "encode",
656 "invite-code",
657 format!("--url={}", invite_code["url"].as_str().unwrap()),
658 "--federation_id={fed_id}",
659 "--peer=0"
660 )
661 .out_json()
662 .await?;
663
664 anyhow::ensure!(
665 encode_invite_output["invite_code"]
666 .as_str()
667 .expect("invite_code must be a string")
668 == invite,
669 "failed to decode and encode the client invite code",
670 );
671
672 info!("Testing LND can pay LDK directly");
676 let invoice = gw_ldk.create_invoice(1_200_000).await?;
677 lnd.pay_bolt11_invoice(invoice.to_string()).await?;
678 gw_ldk
679 .wait_bolt11_invoice(invoice.payment_hash().consensus_encode_to_vec())
680 .await?;
681
682 info!("Testing LDK can pay LND directly");
684 let (invoice, payment_hash) = lnd.invoice(1_000_000).await?;
685 gw_ldk
686 .pay_invoice(Bolt11Invoice::from_str(&invoice).expect("Could not parse invoice"))
687 .await?;
688 gw_lnd.wait_bolt11_invoice(payment_hash).await?;
689
690 let config = cmd!(client, "config").out_json().await?;
692 let guardian_count = config["global"]["api_endpoints"].as_object().unwrap().len();
693 let wallet_module = config["modules"]
694 .as_object()
695 .unwrap()
696 .values()
697 .find(|m| m["kind"].as_str() == Some("wallet"))
698 .expect("wallet module not found");
699 let descriptor = wallet_module["peg_in_descriptor"]
700 .as_str()
701 .unwrap()
702 .to_owned();
703
704 info!("Testing generated descriptor for {guardian_count} guardian federation");
705 if guardian_count == 1 {
706 assert!(descriptor.contains("wpkh("));
707 } else {
708 assert!(descriptor.contains("wsh(sortedmulti("));
709 }
710
711 info!("Testing Client");
713
714 info!("Testing reissuing e-cash");
716 const CLIENT_START_AMOUNT: u64 = 5_000_000_000;
717 const CLIENT_SPEND_AMOUNT: u64 = 1_100_000;
718
719 let initial_client_balance = client.balance().await?;
720 assert_eq!(initial_client_balance, 0);
721
722 fed.pegin_client(CLIENT_START_AMOUNT / 1000, &client)
723 .await?;
724
725 info!("Testing spending from client");
727 let notes = cmd!(client, "spend", CLIENT_SPEND_AMOUNT)
728 .out_json()
729 .await?
730 .get("notes")
731 .expect("Output didn't contain e-cash notes")
732 .as_str()
733 .unwrap()
734 .to_owned();
735
736 let client_post_spend_balance = client.balance().await?;
737 almost_equal(
738 client_post_spend_balance,
739 CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT,
740 10_000,
741 )
742 .unwrap();
743
744 cmd!(client, "reissue", notes).out_json().await?;
746
747 let client_post_spend_balance = client.balance().await?;
748 almost_equal(client_post_spend_balance, CLIENT_START_AMOUNT, 10_000).unwrap();
749
750 let reissue_amount: u64 = 409_600;
751
752 info!("Testing reissuing e-cash after spending");
754 let _notes = cmd!(client, "spend", CLIENT_SPEND_AMOUNT)
755 .out_json()
756 .await?
757 .as_object()
758 .unwrap()
759 .get("notes")
760 .expect("Output didn't contain e-cash notes")
761 .as_str()
762 .unwrap();
763
764 let reissue_notes = cmd!(client, "spend", reissue_amount).out_json().await?["notes"]
765 .as_str()
766 .map(ToOwned::to_owned)
767 .unwrap();
768 let client_reissue_amt = cmd!(client, "reissue", reissue_notes)
769 .out_json()
770 .await?
771 .as_u64()
772 .unwrap();
773 assert_eq!(client_reissue_amt, reissue_amount);
774
775 info!("Testing reissuing e-cash via module commands");
777 let reissue_notes = cmd!(client, "spend", reissue_amount).out_json().await?["notes"]
778 .as_str()
779 .map(ToOwned::to_owned)
780 .unwrap();
781 let client_reissue_amt = cmd!(client, "module", "mint", "reissue", reissue_notes)
782 .out_json()
783 .await?
784 .as_u64()
785 .unwrap();
786 assert_eq!(client_reissue_amt, reissue_amount);
787
788 info!("Testing LND gateway");
790
791 let gatewayd_version = crate::util::Gatewayd::version_or_default().await;
792 if gatewayd_version < *VERSION_0_8_2 {
796 gw_lnd
797 .set_federation_routing_fee(fed_id.clone(), 0, 0)
798 .await?;
799
800 poll("Waiting for LND GW fees to update", || async {
802 let gateways_val = cmd!(client, "list-gateways")
803 .out_json()
804 .await
805 .map_err(ControlFlow::Break)?;
806 let gateways =
807 serde_json::from_value::<Vec<LightningGatewayAnnouncement>>(gateways_val)
808 .expect("Could not deserialize");
809 let fees = gateways
810 .first()
811 .expect("No gateway was registered")
812 .info
813 .fees;
814 if fees.base_msat == 0 && fees.proportional_millionths == 0 {
815 Ok(())
816 } else {
817 Err(ControlFlow::Continue(anyhow!("Fees have not been updated")))
818 }
819 })
820 .await?;
821 }
822
823 if let Some(iroh_gw_id) = &gw_lnd.iroh_gateway_id
825 && crate::util::FedimintCli::version_or_default().await >= *VERSION_0_10_0_ALPHA
826 {
827 info!("Testing outgoing payment from client to LDK via IROH LND Gateway");
828
829 let initial_lnd_gateway_balance = gw_lnd.ecash_balance(fed_id.clone()).await?;
830 let invoice = gw_ldk.create_invoice(2_000_000).await?;
831 ln_pay(&client, invoice.to_string(), iroh_gw_id.clone()).await?;
832 gw_ldk
833 .wait_bolt11_invoice(invoice.payment_hash().consensus_encode_to_vec())
834 .await?;
835
836 let final_lnd_outgoing_gateway_balance = gw_lnd.ecash_balance(fed_id.clone()).await?;
838 info!(
839 ?final_lnd_outgoing_gateway_balance,
840 "Final LND ecash balance after iroh payment"
841 );
842 anyhow::ensure!(
843 almost_equal(
844 final_lnd_outgoing_gateway_balance - initial_lnd_gateway_balance,
845 2_000_000,
846 1_000
847 )
848 .is_ok(),
849 "LND Gateway balance changed by {} on LND outgoing IROH payment, expected 2_000_000",
850 (final_lnd_outgoing_gateway_balance - initial_lnd_gateway_balance)
851 );
852
853 let recv = ln_invoice(
855 &client,
856 Amount::from_msats(2_000_000),
857 "iroh receive payment".to_string(),
858 iroh_gw_id.clone(),
859 )
860 .await?;
861 gw_ldk
862 .pay_invoice(Bolt11Invoice::from_str(&recv.invoice).expect("Could not parse invoice"))
863 .await?;
864 }
865
866 info!("Testing outgoing payment from client to LDK via LND gateway");
867 let initial_lnd_gateway_balance = gw_lnd.ecash_balance(fed_id.clone()).await?;
868 let invoice = gw_ldk.create_invoice(2_000_000).await?;
869 ln_pay(&client, invoice.to_string(), lnd_gw_id.clone()).await?;
870 let fed_id = fed.calculate_federation_id();
871 gw_ldk
872 .wait_bolt11_invoice(invoice.payment_hash().consensus_encode_to_vec())
873 .await?;
874
875 let final_lnd_outgoing_gateway_balance = gw_lnd.ecash_balance(fed_id.clone()).await?;
877 anyhow::ensure!(
878 almost_equal(
879 final_lnd_outgoing_gateway_balance - initial_lnd_gateway_balance,
880 2_000_000,
881 3_000
882 )
883 .is_ok(),
884 "LND Gateway balance changed by {} on LND outgoing payment, expected 2_000_000",
885 (final_lnd_outgoing_gateway_balance - initial_lnd_gateway_balance)
886 );
887
888 info!("Testing incoming payment from LDK to client via LND gateway");
890 let initial_lnd_incoming_client_balance = client.balance().await?;
891 let recv = ln_invoice(
892 &client,
893 Amount::from_msats(1_300_000),
894 "incoming-over-lnd-gw".to_string(),
895 lnd_gw_id,
896 )
897 .await?;
898 let invoice = recv.invoice;
899 gw_ldk
900 .pay_invoice(Bolt11Invoice::from_str(&invoice).expect("Could not parse invoice"))
901 .await?;
902
903 info!("Testing receiving ecash notes");
905 let operation_id = recv.operation_id;
906 cmd!(client, "await-invoice", operation_id.fmt_full())
907 .run()
908 .await?;
909
910 let final_lnd_incoming_client_balance = client.balance().await?;
912 let final_lnd_incoming_gateway_balance = gw_lnd.ecash_balance(fed_id.clone()).await?;
913 anyhow::ensure!(
914 almost_equal(
915 final_lnd_incoming_client_balance - initial_lnd_incoming_client_balance,
916 1_300_000,
917 2_000
918 )
919 .is_ok(),
920 "Client balance changed by {} on LND incoming payment, expected 1_300_000",
921 (final_lnd_incoming_client_balance - initial_lnd_incoming_client_balance)
922 );
923 anyhow::ensure!(
924 almost_equal(
925 final_lnd_outgoing_gateway_balance - final_lnd_incoming_gateway_balance,
926 1_300_000,
927 2_000
928 )
929 .is_ok(),
930 "LND Gateway balance changed by {} on LND incoming payment, expected 1_300_000",
931 (final_lnd_outgoing_gateway_balance - final_lnd_incoming_gateway_balance)
932 );
933
934 info!("Testing client deposit");
939 let initial_walletng_balance = client.balance().await?;
940
941 fed.pegin_client(100_000, &client).await?; let post_deposit_walletng_balance = client.balance().await?;
944
945 almost_equal(
946 post_deposit_walletng_balance,
947 initial_walletng_balance + 100_000_000, 2_000,
949 )
950 .unwrap();
951
952 info!("Testing client withdraw");
954
955 let initial_walletng_balance = client.balance().await?;
956
957 let address = bitcoind.get_new_address().await?;
958 let withdraw_res = cmd!(
959 client,
960 "withdraw",
961 "--address",
962 &address,
963 "--amount",
964 "50000 sat"
965 )
966 .out_json()
967 .await?;
968
969 let txid: Txid = withdraw_res["txid"].as_str().unwrap().parse().unwrap();
970 let fees_sat = withdraw_res["fees_sat"].as_u64().unwrap();
971
972 let tx_hex = bitcoind.poll_get_transaction(txid).await?;
973
974 let tx = bitcoin::Transaction::consensus_decode_hex(&tx_hex, &ModuleRegistry::default())?;
975 assert!(
976 tx.output
977 .iter()
978 .any(|o| o.script_pubkey == address.script_pubkey() && o.value.to_sat() == 50000)
979 );
980
981 let post_withdraw_walletng_balance = client.balance().await?;
982 let expected_wallet_balance = initial_walletng_balance - 50_000_000 - (fees_sat * 1000);
983
984 almost_equal(
985 post_withdraw_walletng_balance,
986 expected_wallet_balance,
987 4_000,
988 )
989 .unwrap();
990
991 let peer_0_fedimintd_version = cmd!(client, "dev", "peer-version", "--peer-id", "0")
993 .out_json()
994 .await?
995 .get("version")
996 .expect("Output didn't contain version")
997 .as_str()
998 .unwrap()
999 .to_owned();
1000
1001 assert_eq!(
1002 semver::Version::parse(&peer_0_fedimintd_version)?,
1003 fedimintd_version
1004 );
1005
1006 info!("Checking initial announcements...");
1007
1008 retry(
1009 "Check initial announcements",
1010 aggressive_backoff(),
1011 || async {
1012 cmd!(client, "dev", "wait", "1").run().await?;
1014
1015 let initial_announcements =
1017 serde_json::from_value::<BTreeMap<PeerId, SignedApiAnnouncement>>(
1018 cmd!(client, "dev", "api-announcements",).out_json().await?,
1019 )
1020 .expect("failed to parse API announcements");
1021
1022 if initial_announcements.len() < fed.members.len() {
1023 bail!(
1024 "Not all announcements ready; got: {}, expected: {}",
1025 initial_announcements.len(),
1026 fed.members.len()
1027 )
1028 }
1029
1030 if !initial_announcements
1031 .values()
1032 .all(|announcement| announcement.api_announcement.nonce == 0)
1033 {
1034 bail!("Not all announcements have their initial value");
1035 }
1036 Ok(())
1037 },
1038 )
1039 .await?;
1040
1041 const NEW_API_URL: &str = "ws://127.0.0.1:4242";
1042 let new_announcement = serde_json::from_value::<SignedApiAnnouncement>(
1043 cmd!(
1044 client,
1045 "--our-id",
1046 "0",
1047 "--password",
1048 "pass",
1049 "admin",
1050 "sign-api-announcement",
1051 NEW_API_URL
1052 )
1053 .out_json()
1054 .await?,
1055 )
1056 .expect("Couldn't parse signed announcement");
1057
1058 assert_eq!(
1059 new_announcement.api_announcement.nonce, 1,
1060 "Nonce did not increment correctly"
1061 );
1062
1063 info!("Testing if the client syncs the announcement");
1064 let announcement = poll("Waiting for the announcement to propagate", || async {
1065 cmd!(client, "dev", "wait", "1")
1066 .run()
1067 .await
1068 .map_err(ControlFlow::Break)?;
1069
1070 let new_announcements_peer2 =
1071 serde_json::from_value::<BTreeMap<PeerId, SignedApiAnnouncement>>(
1072 cmd!(client, "dev", "api-announcements",)
1073 .out_json()
1074 .await
1075 .map_err(ControlFlow::Break)?,
1076 )
1077 .expect("failed to parse API announcements");
1078
1079 let announcement = new_announcements_peer2[&PeerId::from(0)]
1080 .api_announcement
1081 .clone();
1082 if announcement.nonce == 1 {
1083 Ok(announcement)
1084 } else {
1085 Err(ControlFlow::Continue(anyhow!(
1086 "Haven't received updated announcement yet; nonce: {}",
1087 announcement.nonce
1088 )))
1089 }
1090 })
1091 .await?;
1092
1093 assert_eq!(
1094 announcement.api_url,
1095 NEW_API_URL.parse().expect("valid URL")
1096 );
1097
1098 Ok(())
1099}
1100
1101pub async fn guardian_metadata_tests(dev_fed: DevFed) -> Result<()> {
1102 use fedimint_core::PeerId;
1103 use fedimint_core::net::guardian_metadata::SignedGuardianMetadata;
1104
1105 log_binary_versions().await?;
1106
1107 let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
1108 let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
1109
1110 if fedimintd_version < *VERSION_0_11_0_ALPHA || fedimint_cli_version < *VERSION_0_11_0_ALPHA {
1111 info!("Skipping test for too old versions");
1112 return Ok(());
1113 }
1114
1115 let DevFed { fed, .. } = dev_fed;
1116
1117 let client = fed.internal_client().await?;
1118
1119 info!("Checking initial guardian metadata...");
1120
1121 retry(
1122 "Check initial guardian metadata",
1123 aggressive_backoff(),
1124 || async {
1125 cmd!(client, "dev", "wait", "1").run().await?;
1127
1128 let initial_metadata =
1129 serde_json::from_value::<BTreeMap<PeerId, SignedGuardianMetadata>>(
1130 cmd!(client, "dev", "guardian-metadata",).out_json().await?,
1131 )
1132 .expect("failed to parse guardian metadata");
1133
1134 if initial_metadata.len() < fed.members.len() {
1135 bail!(
1136 "Not all guardian metadata ready; got: {}, expected: {}",
1137 initial_metadata.len(),
1138 fed.members.len()
1139 )
1140 }
1141
1142 Ok(())
1143 },
1144 )
1145 .await?;
1146
1147 const TEST_API_URL: &str = "ws://127.0.0.1:5000/";
1148 const TEST_PKARR_ID: &str = "test_pkarr_id_z32";
1149
1150 let new_metadata = serde_json::from_value::<SignedGuardianMetadata>(
1151 cmd!(
1152 client,
1153 "--our-id",
1154 "0",
1155 "--password",
1156 "pass",
1157 "admin",
1158 "sign-guardian-metadata",
1159 "--api-urls",
1160 TEST_API_URL,
1161 "--pkarr-id",
1162 TEST_PKARR_ID
1163 )
1164 .out_json()
1165 .await?,
1166 )
1167 .expect("Couldn't parse signed guardian metadata");
1168
1169 let parsed_metadata = new_metadata.guardian_metadata();
1170
1171 assert_eq!(
1172 parsed_metadata.api_urls.first().unwrap().to_string(),
1173 TEST_API_URL,
1174 "API URL did not match"
1175 );
1176
1177 assert_eq!(
1178 parsed_metadata.pkarr_id_z32, TEST_PKARR_ID,
1179 "Pkarr ID did not match"
1180 );
1181
1182 info!("Testing if the client syncs the guardian metadata");
1183 let metadata = poll("Waiting for the guardian metadata to propagate", || async {
1184 cmd!(client, "dev", "wait", "1")
1185 .run()
1186 .await
1187 .map_err(ControlFlow::Break)?;
1188
1189 let new_metadata_peer0 =
1190 serde_json::from_value::<BTreeMap<PeerId, SignedGuardianMetadata>>(
1191 cmd!(client, "dev", "guardian-metadata",)
1192 .out_json()
1193 .await
1194 .map_err(ControlFlow::Break)?,
1195 )
1196 .expect("failed to parse guardian metadata");
1197
1198 let metadata = new_metadata_peer0[&PeerId::from(0)].guardian_metadata();
1199
1200 if metadata.api_urls.first().unwrap().to_string() == TEST_API_URL {
1201 Ok(metadata.clone())
1202 } else {
1203 Err(ControlFlow::Continue(anyhow!(
1204 "Haven't received updated guardian metadata yet"
1205 )))
1206 }
1207 })
1208 .await?;
1209
1210 assert_eq!(
1211 metadata.pkarr_id_z32, TEST_PKARR_ID,
1212 "Pkarr ID did not propagate correctly"
1213 );
1214
1215 Ok(())
1216}
1217
1218pub async fn cli_load_test_tool_test(dev_fed: DevFed) -> Result<()> {
1219 log_binary_versions().await?;
1220 let data_dir = env::var(FM_DATA_DIR_ENV)?;
1221 let load_test_temp = PathBuf::from(data_dir).join("load-test-temp");
1222 dev_fed
1223 .fed
1224 .pegin_client(10_000, dev_fed.fed.internal_client().await?)
1225 .await?;
1226 let invite_code = dev_fed.fed.invite_code()?;
1227 dev_fed
1228 .gw_lnd
1229 .set_federation_routing_fee(dev_fed.fed.calculate_federation_id(), 0, 0)
1230 .await?;
1231 run_standard_load_test(&load_test_temp, &invite_code).await?;
1232 run_ln_circular_load_test(&load_test_temp, &invite_code).await?;
1233 Ok(())
1234}
1235
1236pub async fn run_standard_load_test(
1237 load_test_temp: &Path,
1238 invite_code: &str,
1239) -> anyhow::Result<()> {
1240 let output = cmd!(
1241 LoadTestTool,
1242 "--archive-dir",
1243 load_test_temp.display(),
1244 "--users",
1245 "1",
1246 "load-test",
1247 "--notes-per-user",
1248 "1",
1249 "--generate-invoice-with",
1250 "ldk-lightning-cli",
1251 "--invite-code",
1252 invite_code
1253 )
1254 .out_string()
1255 .await?;
1256 println!("{output}");
1257 anyhow::ensure!(
1258 output.contains("2 reissue_notes"),
1259 "reissued different number notes than expected"
1260 );
1261 anyhow::ensure!(
1262 output.contains("1 gateway_pay_invoice"),
1263 "paid different number of invoices than expected"
1264 );
1265 Ok(())
1266}
1267
1268pub async fn run_ln_circular_load_test(
1269 load_test_temp: &Path,
1270 invite_code: &str,
1271) -> anyhow::Result<()> {
1272 info!("Testing ln-circular-load-test with 'two-gateways' strategy");
1273 let output = cmd!(
1274 LoadTestTool,
1275 "--archive-dir",
1276 load_test_temp.display(),
1277 "--users",
1278 "1",
1279 "ln-circular-load-test",
1280 "--strategy",
1281 "two-gateways",
1282 "--test-duration-secs",
1283 "2",
1284 "--invite-code",
1285 invite_code
1286 )
1287 .out_string()
1288 .await?;
1289 println!("{output}");
1290 anyhow::ensure!(
1291 output.contains("gateway_create_invoice"),
1292 "missing invoice creation"
1293 );
1294 anyhow::ensure!(
1295 output.contains("gateway_pay_invoice_success"),
1296 "missing invoice payment"
1297 );
1298 anyhow::ensure!(
1299 output.contains("gateway_payment_received_success"),
1300 "missing received payment"
1301 );
1302
1303 info!("Testing ln-circular-load-test with 'partner-ping-pong' strategy");
1304 let output = cmd!(
1308 LoadTestTool,
1309 "--archive-dir",
1310 load_test_temp.display(),
1311 "--users",
1312 "1",
1313 "ln-circular-load-test",
1314 "--strategy",
1315 "partner-ping-pong",
1316 "--test-duration-secs",
1317 "6",
1318 "--invite-code",
1319 invite_code
1320 )
1321 .out_string()
1322 .await?;
1323 println!("{output}");
1324 anyhow::ensure!(
1325 output.contains("gateway_create_invoice"),
1326 "missing invoice creation"
1327 );
1328 anyhow::ensure!(
1329 output.contains("gateway_payment_received_success"),
1330 "missing received payment"
1331 );
1332
1333 info!("Testing ln-circular-load-test with 'self-payment' strategy");
1334 let output = cmd!(
1336 LoadTestTool,
1337 "--archive-dir",
1338 load_test_temp.display(),
1339 "--users",
1340 "1",
1341 "ln-circular-load-test",
1342 "--strategy",
1343 "self-payment",
1344 "--test-duration-secs",
1345 "2",
1346 "--invite-code",
1347 invite_code
1348 )
1349 .out_string()
1350 .await?;
1351 println!("{output}");
1352 anyhow::ensure!(
1353 output.contains("gateway_create_invoice"),
1354 "missing invoice creation"
1355 );
1356 anyhow::ensure!(
1357 output.contains("gateway_payment_received_success"),
1358 "missing received payment"
1359 );
1360 Ok(())
1361}
1362
1363pub async fn lightning_gw_reconnect_test(
1364 dev_fed: DevFed,
1365 process_mgr: &ProcessManager,
1366) -> Result<()> {
1367 log_binary_versions().await?;
1368
1369 let DevFed {
1370 bitcoind,
1371 lnd,
1372 fed,
1373 mut gw_lnd,
1374 gw_ldk,
1375 ..
1376 } = dev_fed;
1377
1378 let client = fed
1379 .new_joined_client("lightning-gw-reconnect-test-client")
1380 .await?;
1381
1382 info!("Pegging-in both gateways");
1383 fed.pegin_gateways(99_999, vec![&gw_lnd]).await?;
1384
1385 drop(lnd);
1387
1388 tracing::info!("Stopping LND");
1389 let mut info_cmd = cmd!(gw_lnd, "info");
1391 assert!(info_cmd.run().await.is_ok());
1392
1393 let ln_type = gw_lnd.ln.ln_type().to_string();
1396 gw_lnd.stop_lightning_node().await?;
1397 let lightning_info = info_cmd.out_json().await?;
1398 if gw_lnd.gatewayd_version < *VERSION_0_10_0_ALPHA {
1399 let lightning_pub_key: Option<String> =
1400 serde_json::from_value(lightning_info["lightning_pub_key"].clone())?;
1401
1402 assert!(lightning_pub_key.is_none());
1403 } else {
1404 let not_connected = lightning_info["lightning_info"].clone();
1405 assert!(not_connected.as_str().expect("ln info is not a string") == "not_connected");
1406 }
1407
1408 tracing::info!("Restarting LND...");
1410 let new_lnd = Lnd::new(process_mgr, bitcoind.clone()).await?;
1411 gw_lnd.set_lightning_node(LightningNode::Lnd(new_lnd.clone()));
1412
1413 tracing::info!("Retrying info...");
1414 const MAX_RETRIES: usize = 30;
1415 const RETRY_INTERVAL: Duration = Duration::from_secs(1);
1416
1417 for i in 0..MAX_RETRIES {
1418 match do_try_create_and_pay_invoice(&gw_lnd, &client, &gw_ldk).await {
1419 Ok(()) => break,
1420 Err(e) => {
1421 if i == MAX_RETRIES - 1 {
1422 return Err(e);
1423 }
1424 tracing::debug!(
1425 "Pay invoice for gateway {} failed with {e:?}, retrying in {} seconds (try {}/{MAX_RETRIES})",
1426 ln_type,
1427 RETRY_INTERVAL.as_secs(),
1428 i + 1,
1429 );
1430 fedimint_core::task::sleep_in_test(
1431 "paying invoice for gateway failed",
1432 RETRY_INTERVAL,
1433 )
1434 .await;
1435 }
1436 }
1437 }
1438
1439 info!(target: LOG_DEVIMINT, "lightning_reconnect_test: success");
1440 Ok(())
1441}
1442
1443pub async fn gw_reboot_test(dev_fed: DevFed, process_mgr: &ProcessManager) -> Result<()> {
1444 log_binary_versions().await?;
1445
1446 let DevFed {
1447 bitcoind,
1448 lnd,
1449 fed,
1450 gw_lnd,
1451 gw_ldk,
1452 gw_ldk_second,
1453 ..
1454 } = dev_fed;
1455
1456 let client = fed.new_joined_client("gw-reboot-test-client").await?;
1457 fed.pegin_client(10_000, &client).await?;
1458
1459 let block_height = bitcoind.get_block_count().await? - 1;
1461 try_join!(
1462 gw_lnd.wait_for_block_height(block_height),
1463 gw_ldk.wait_for_block_height(block_height),
1464 )?;
1465
1466 let lnd_gateway_id = gw_lnd.gateway_id.clone();
1468 let ldk_gateway_id = gw_ldk.gateway_id.clone();
1469 let gw_ldk_name = gw_ldk.gw_name.clone();
1470 let gw_ldk_port = gw_ldk.gw_port;
1471 let gw_lightning_port = gw_ldk.ldk_port;
1472 let gw_ldk_metrics_port = gw_ldk.metrics_port;
1473 drop(gw_lnd);
1474 drop(gw_ldk);
1475
1476 info!("Making payment while gateway is down");
1479 let initial_client_balance = client.balance().await?;
1480 let invoice = gw_ldk_second.create_invoice(3000).await?;
1481 ln_pay(&client, invoice.to_string(), lnd_gateway_id.clone())
1482 .await
1483 .expect_err("Expected ln-pay to return error because the gateway is not online");
1484 let new_client_balance = client.balance().await?;
1485 anyhow::ensure!(initial_client_balance == new_client_balance);
1486
1487 info!("Rebooting gateways...");
1489 let (new_gw_lnd, new_gw_ldk) = try_join!(
1490 Gatewayd::new(process_mgr, LightningNode::Lnd(lnd.clone()), 0),
1491 Gatewayd::new(
1492 process_mgr,
1493 LightningNode::Ldk {
1494 name: gw_ldk_name,
1495 gw_port: gw_ldk_port,
1496 ldk_port: gw_lightning_port,
1497 metrics_port: gw_ldk_metrics_port,
1498 },
1499 1,
1500 )
1501 )?;
1502
1503 let lnd_gateway_id = fedimint_core::secp256k1::PublicKey::from_str(&lnd_gateway_id)?;
1504
1505 poll(
1506 "Waiting for LND Gateway Running state after reboot",
1507 || async {
1508 let mut new_lnd_cmd = cmd!(new_gw_lnd, "info");
1509 let lnd_value = new_lnd_cmd.out_json().await.map_err(ControlFlow::Continue)?;
1510 let reboot_gateway_state: String = serde_json::from_value(lnd_value["gateway_state"].clone()).context("invalid gateway state").map_err(ControlFlow::Break)?;
1511 let reboot_gateway_id = fedimint_core::secp256k1::PublicKey::from_str(&new_gw_lnd.gateway_id).expect("Could not convert public key");
1512
1513 if reboot_gateway_state == "Running" {
1514 info!(target: LOG_DEVIMINT, "LND Gateway restarted, with auto-rejoin to federation");
1515 assert_eq!(lnd_gateway_id, reboot_gateway_id);
1517 return Ok(());
1518 }
1519 Err(ControlFlow::Continue(anyhow!("gateway not running")))
1520 },
1521 )
1522 .await?;
1523
1524 let ldk_gateway_id = fedimint_core::secp256k1::PublicKey::from_str(&ldk_gateway_id)?;
1525 poll(
1526 "Waiting for LDK Gateway Running state after reboot",
1527 || async {
1528 let mut new_ldk_cmd = cmd!(new_gw_ldk, "info");
1529 let ldk_value = new_ldk_cmd.out_json().await.map_err(ControlFlow::Continue)?;
1530 let reboot_gateway_state: String = serde_json::from_value(ldk_value["gateway_state"].clone()).context("invalid gateway state").map_err(ControlFlow::Break)?;
1531 let reboot_gateway_id = fedimint_core::secp256k1::PublicKey::from_str(&new_gw_ldk.gateway_id).expect("Could not convert public key");
1532
1533 if reboot_gateway_state == "Running" {
1534 info!(target: LOG_DEVIMINT, "LDK Gateway restarted, with auto-rejoin to federation");
1535 assert_eq!(ldk_gateway_id, reboot_gateway_id);
1537 return Ok(());
1538 }
1539 Err(ControlFlow::Continue(anyhow!("gateway not running")))
1540 },
1541 )
1542 .await?;
1543
1544 info!(LOG_DEVIMINT, "gateway_reboot_test: success");
1545 Ok(())
1546}
1547
1548pub async fn do_try_create_and_pay_invoice(
1549 gw_lnd: &Gatewayd,
1550 client: &Client,
1551 gw_ldk: &Gatewayd,
1552) -> anyhow::Result<()> {
1553 poll("Waiting for info to succeed after restart", || async {
1557 gw_lnd
1558 .lightning_pubkey()
1559 .await
1560 .map_err(ControlFlow::Continue)?;
1561 Ok(())
1562 })
1563 .await?;
1564
1565 tracing::info!("Creating invoice....");
1566 let invoice = ln_invoice(
1567 client,
1568 Amount::from_msats(1000),
1569 "incoming-over-lnd-gw".to_string(),
1570 gw_lnd.gateway_id.clone(),
1571 )
1572 .await?
1573 .invoice;
1574
1575 match &gw_lnd.ln.ln_type() {
1576 LightningNodeType::Lnd => {
1577 gw_ldk
1579 .pay_invoice(Bolt11Invoice::from_str(&invoice).expect("Could not parse invoice"))
1580 .await?;
1581 }
1582 LightningNodeType::Ldk => {
1583 unimplemented!("do_try_create_and_pay_invoice not implemented for LDK yet");
1584 }
1585 }
1586 Ok(())
1587}
1588
1589async fn ln_pay(client: &Client, invoice: String, gw_id: String) -> anyhow::Result<String> {
1590 let value = cmd!(client, "ln-pay", invoice, "--gateway-id", gw_id,)
1591 .out_json()
1592 .await?;
1593 let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
1594 if fedimint_cli_version >= *VERSION_0_9_0_ALPHA {
1595 let outcome = serde_json::from_value::<LightningPaymentOutcome>(value)
1596 .expect("Could not deserialize Lightning payment outcome");
1597 match outcome {
1598 LightningPaymentOutcome::Success { preimage } => Ok(preimage),
1599 LightningPaymentOutcome::Failure { error_message } => {
1600 Err(anyhow!("Failed to pay lightning invoice: {error_message}"))
1601 }
1602 }
1603 } else {
1604 let operation_id = value["operation_id"]
1605 .as_str()
1606 .ok_or(anyhow!("Failed to pay invoice"))?
1607 .to_string();
1608 Ok(operation_id)
1609 }
1610}
1611
1612async fn ln_invoice(
1613 client: &Client,
1614 amount: Amount,
1615 description: String,
1616 gw_id: String,
1617) -> anyhow::Result<LnInvoiceResponse> {
1618 let ln_response_val = cmd!(
1619 client,
1620 "ln-invoice",
1621 "--amount",
1622 amount.msats,
1623 format!("--description='{description}'"),
1624 "--gateway-id",
1625 gw_id,
1626 )
1627 .out_json()
1628 .await?;
1629
1630 let ln_invoice_response: LnInvoiceResponse = serde_json::from_value(ln_response_val)?;
1631
1632 Ok(ln_invoice_response)
1633}
1634
1635async fn lnv2_receive(
1636 client: &Client,
1637 gateway: &str,
1638 amount: u64,
1639) -> anyhow::Result<(Bolt11Invoice, OperationId)> {
1640 Ok(serde_json::from_value::<(Bolt11Invoice, OperationId)>(
1641 cmd!(
1642 client,
1643 "module",
1644 "lnv2",
1645 "receive",
1646 amount,
1647 "--gateway",
1648 gateway
1649 )
1650 .out_json()
1651 .await?,
1652 )?)
1653}
1654
1655async fn lnv2_send(client: &Client, gateway: &String, invoice: &String) -> anyhow::Result<()> {
1656 let send_op = serde_json::from_value::<OperationId>(
1657 cmd!(
1658 client,
1659 "module",
1660 "lnv2",
1661 "send",
1662 invoice,
1663 "--gateway",
1664 gateway
1665 )
1666 .out_json()
1667 .await?,
1668 )?;
1669
1670 assert_eq!(
1671 cmd!(
1672 client,
1673 "module",
1674 "lnv2",
1675 "await-send",
1676 serde_json::to_string(&send_op)?.substring(1, 65)
1677 )
1678 .out_json()
1679 .await?,
1680 serde_json::to_value(FinalSendOperationState::Success).expect("JSON serialization failed"),
1681 );
1682
1683 Ok(())
1684}
1685
1686pub async fn reconnect_test(dev_fed: DevFed, process_mgr: &ProcessManager) -> Result<()> {
1687 log_binary_versions().await?;
1688
1689 let DevFed {
1690 bitcoind, mut fed, ..
1691 } = dev_fed;
1692
1693 bitcoind.mine_blocks(110).await?;
1694 fed.await_block_sync().await?;
1695 fed.await_all_peers().await?;
1696
1697 fed.terminate_server(0).await?;
1699 fed.mine_then_wait_blocks_sync(100).await?;
1700
1701 fed.start_server(process_mgr, 0).await?;
1702 fed.mine_then_wait_blocks_sync(100).await?;
1703 fed.await_all_peers().await?;
1704 info!(target: LOG_DEVIMINT, "Server 0 successfully rejoined!");
1705 fed.mine_then_wait_blocks_sync(100).await?;
1706
1707 fed.terminate_server(1).await?;
1709 fed.mine_then_wait_blocks_sync(100).await?;
1710 fed.terminate_server(2).await?;
1711 fed.terminate_server(3).await?;
1712
1713 fed.start_server(process_mgr, 1).await?;
1714 fed.start_server(process_mgr, 2).await?;
1715 fed.start_server(process_mgr, 3).await?;
1716
1717 fed.await_all_peers().await?;
1718
1719 info!(target: LOG_DEVIMINT, "fm success: reconnect-test");
1720 Ok(())
1721}
1722
1723pub async fn recoverytool_test(dev_fed: DevFed) -> Result<()> {
1724 log_binary_versions().await?;
1725
1726 let DevFed { bitcoind, fed, .. } = dev_fed;
1727
1728 let data_dir = env::var(FM_DATA_DIR_ENV)?;
1729 let client = fed.new_joined_client("recoverytool-test-client").await?;
1730
1731 let mut fed_utxos_sats = HashSet::from([12_345_000, 23_456_000, 34_567_000]);
1732 let deposit_fees = fed.deposit_fees()?.msats / 1000;
1733 for sats in &fed_utxos_sats {
1734 fed.pegin_client(*sats - deposit_fees, &client).await?;
1736 }
1737
1738 async fn withdraw(
1739 client: &Client,
1740 bitcoind: &crate::external::Bitcoind,
1741 fed_utxos_sats: &mut HashSet<u64>,
1742 ) -> Result<()> {
1743 let withdrawal_address = bitcoind.get_new_address().await?;
1744 let withdraw_res = cmd!(
1745 client,
1746 "withdraw",
1747 "--address",
1748 &withdrawal_address,
1749 "--amount",
1750 "5000 sat"
1751 )
1752 .out_json()
1753 .await?;
1754
1755 let fees_sat = withdraw_res["fees_sat"]
1756 .as_u64()
1757 .expect("withdrawal should contain fees");
1758 let txid: Txid = withdraw_res["txid"]
1759 .as_str()
1760 .expect("withdrawal should contain txid string")
1761 .parse()
1762 .expect("txid should be parsable");
1763 let tx_hex = bitcoind.poll_get_transaction(txid).await?;
1764
1765 let tx = bitcoin::Transaction::consensus_decode_hex(&tx_hex, &ModuleRegistry::default())?;
1766 assert_eq!(tx.input.len(), 1);
1767 assert_eq!(tx.output.len(), 2);
1768
1769 let change_output = tx
1770 .output
1771 .iter()
1772 .find(|o| o.to_owned().script_pubkey != withdrawal_address.script_pubkey())
1773 .expect("withdrawal must have change output");
1774 assert!(fed_utxos_sats.insert(change_output.value.to_sat()));
1775
1776 let total_output_sats = tx.output.iter().map(|o| o.value.to_sat()).sum::<u64>();
1778 let input_sats = total_output_sats + fees_sat;
1779 assert!(fed_utxos_sats.remove(&input_sats));
1780
1781 Ok(())
1782 }
1783
1784 for _ in 0..2 {
1787 withdraw(&client, &bitcoind, &mut fed_utxos_sats).await?;
1788 }
1789
1790 let total_fed_sats = fed_utxos_sats.iter().sum::<u64>();
1791 fed.finalize_mempool_tx().await?;
1792
1793 let last_tx_session = client.get_session_count().await?;
1797
1798 info!("Recovering using utxos method");
1799 let output = cmd!(
1800 crate::util::Recoverytool,
1801 "--cfg",
1802 "{data_dir}/fedimintd-default-0",
1803 "utxos",
1804 "--db",
1805 "{data_dir}/fedimintd-default-0/database"
1806 )
1807 .env(FM_PASSWORD_ENV, "pass")
1808 .out_json()
1809 .await?;
1810 let outputs = output.as_array().context("expected an array")?;
1811 assert_eq!(outputs.len(), fed_utxos_sats.len());
1812
1813 assert_eq!(
1814 outputs
1815 .iter()
1816 .map(|o| o["amount_sat"].as_u64().unwrap())
1817 .collect::<HashSet<_>>(),
1818 fed_utxos_sats
1819 );
1820 let utxos_descriptors = outputs
1821 .iter()
1822 .map(|o| o["descriptor"].as_str().unwrap())
1823 .collect::<HashSet<_>>();
1824
1825 debug!(target: LOG_DEVIMINT, ?utxos_descriptors, "recoverytool descriptors using UTXOs method");
1826
1827 let descriptors_json = serde_json::value::to_raw_value(&serde_json::Value::Array(vec![
1828 serde_json::Value::Array(
1829 utxos_descriptors
1830 .iter()
1831 .map(|d| {
1832 json!({
1833 "desc": d,
1834 "timestamp": 0,
1835 })
1836 })
1837 .collect(),
1838 ),
1839 ]))?;
1840 info!("Getting wallet balances before import");
1841 let bitcoin_client = bitcoind.wallet_client().await?;
1842 let balances_before = bitcoin_client.get_balances().await?;
1843 info!("Importing descriptors into bitcoin wallet");
1844 let request = bitcoin_client
1845 .get_jsonrpc_client()
1846 .build_request("importdescriptors", Some(&descriptors_json));
1847 let response = block_in_place(|| bitcoin_client.get_jsonrpc_client().send_request(request))?;
1848 response.check_error()?;
1849 info!("Getting wallet balances after import");
1850 let balances_after = bitcoin_client.get_balances().await?;
1851 let diff = balances_after.mine.immature + balances_after.mine.trusted
1852 - balances_before.mine.immature
1853 - balances_before.mine.trusted;
1854
1855 client.wait_session_outcome(last_tx_session).await?;
1860
1861 assert_eq!(diff.to_sat(), total_fed_sats);
1863 info!("Recovering using epochs method");
1864
1865 let outputs = cmd!(
1866 crate::util::Recoverytool,
1867 "--cfg",
1868 "{data_dir}/fedimintd-default-0",
1869 "epochs",
1870 "--db",
1871 "{data_dir}/fedimintd-default-0/database"
1872 )
1873 .env(FM_PASSWORD_ENV, "pass")
1874 .out_json()
1875 .await?
1876 .as_array()
1877 .context("expected an array")?
1878 .clone();
1879
1880 let epochs_descriptors = outputs
1881 .iter()
1882 .map(|o| o["descriptor"].as_str().unwrap())
1883 .collect::<HashSet<_>>();
1884
1885 debug!(target: LOG_DEVIMINT, ?epochs_descriptors, "recoverytool descriptors using epochs method");
1887
1888 for utxo_descriptor in utxos_descriptors {
1891 assert!(epochs_descriptors.contains(utxo_descriptor));
1892 }
1893 Ok(())
1894}
1895
1896pub async fn guardian_backup_test(dev_fed: DevFed, process_mgr: &ProcessManager) -> Result<()> {
1897 const PEER_TO_TEST: u16 = 0;
1898
1899 log_binary_versions().await?;
1900
1901 let DevFed { mut fed, .. } = dev_fed;
1902
1903 fed.await_all_peers()
1904 .await
1905 .expect("Awaiting federation coming online failed");
1906
1907 let client = fed.new_joined_client("guardian-client").await?;
1908 let old_block_count = cmd!(
1909 client,
1910 "dev",
1911 "api",
1912 "--peer-id",
1913 PEER_TO_TEST.to_string(),
1914 "--module",
1915 "wallet",
1916 "block_count",
1917 )
1918 .out_json()
1919 .await?["value"]
1920 .as_u64()
1921 .expect("No block height returned");
1922
1923 let backup_res = cmd!(
1924 client,
1925 "--our-id",
1926 PEER_TO_TEST.to_string(),
1927 "--password",
1928 "pass",
1929 "admin",
1930 "guardian-config-backup"
1931 )
1932 .out_json()
1933 .await?;
1934 let backup_hex = backup_res["tar_archive_bytes"]
1935 .as_str()
1936 .expect("expected hex string");
1937 let backup_tar = hex::decode(backup_hex).expect("invalid hex");
1938
1939 let data_dir = fed
1940 .vars
1941 .get(&PEER_TO_TEST.into())
1942 .expect("peer not found")
1943 .FM_DATA_DIR
1944 .clone();
1945
1946 fed.terminate_server(PEER_TO_TEST.into())
1947 .await
1948 .expect("could not terminate fedimintd");
1949
1950 std::fs::remove_dir_all(&data_dir).expect("error deleting old datadir");
1951 std::fs::create_dir(&data_dir).expect("error creating new datadir");
1952
1953 let write_file = |name: &str, data: &[u8]| {
1954 let mut file = std::fs::File::options()
1955 .write(true)
1956 .create(true)
1957 .truncate(true)
1958 .open(data_dir.join(name))
1959 .expect("could not open file");
1960 file.write_all(data).expect("could not write file");
1961 file.flush().expect("could not flush file");
1962 };
1963
1964 write_file("backup.tar", &backup_tar);
1965 write_file(
1966 fedimint_server::config::io::PLAINTEXT_PASSWORD,
1967 "pass".as_bytes(),
1968 );
1969
1970 assert_eq!(
1971 std::process::Command::new("tar")
1972 .arg("-xf")
1973 .arg("backup.tar")
1974 .current_dir(data_dir)
1975 .spawn()
1976 .expect("error spawning tar")
1977 .wait()
1978 .expect("error extracting archive")
1979 .code(),
1980 Some(0),
1981 "tar failed"
1982 );
1983
1984 fed.start_server(process_mgr, PEER_TO_TEST.into())
1985 .await
1986 .expect("could not restart fedimintd");
1987
1988 poll("Peer catches up again", || async {
1989 let block_counts = all_peer_block_count(&client, fed.member_ids())
1990 .await
1991 .map_err(ControlFlow::Continue)?;
1992 let block_count = block_counts[&PeerId::from(PEER_TO_TEST)];
1993
1994 info!("Caught up to block {block_count} of at least {old_block_count} (counts={block_counts:?})");
1995
1996 if block_count < old_block_count {
1997 return Err(ControlFlow::Continue(anyhow!("Block count still behind")));
1998 }
1999
2000 Ok(())
2001 })
2002 .await
2003 .expect("Peer didn't rejoin federation");
2004
2005 Ok(())
2006}
2007
2008async fn peer_block_count(client: &Client, peer: PeerId) -> Result<u64> {
2009 cmd!(
2010 client,
2011 "dev",
2012 "api",
2013 "--peer-id",
2014 peer.to_string(),
2015 "--module",
2016 "wallet",
2017 "block_count",
2018 )
2019 .out_json()
2020 .await?["value"]
2021 .as_u64()
2022 .context("No block height returned")
2023}
2024
2025async fn all_peer_block_count(
2026 client: &Client,
2027 peers: impl Iterator<Item = PeerId>,
2028) -> Result<BTreeMap<PeerId, u64>> {
2029 let mut peer_heights = BTreeMap::new();
2030 for peer in peers {
2031 peer_heights.insert(peer, peer_block_count(client, peer).await?);
2032 }
2033 Ok(peer_heights)
2034}
2035
2036pub async fn cannot_replay_tx_test(dev_fed: DevFed) -> Result<()> {
2037 log_binary_versions().await?;
2038
2039 let DevFed { fed, .. } = dev_fed;
2040
2041 let client = fed.new_joined_client("cannot-replay-client").await?;
2042
2043 const CLIENT_START_AMOUNT: u64 = 10_000_000_000;
2044 const CLIENT_SPEND_AMOUNT: u64 = 5_000_000_000;
2045
2046 let initial_client_balance = client.balance().await?;
2047 assert_eq!(initial_client_balance, 0);
2048
2049 fed.pegin_client(CLIENT_START_AMOUNT / 1000, &client)
2050 .await?;
2051
2052 let double_spend_client = client.new_forked("double-spender").await?;
2054
2055 let notes = cmd!(client, "spend", CLIENT_SPEND_AMOUNT)
2057 .out_json()
2058 .await?
2059 .get("notes")
2060 .expect("Output didn't contain e-cash notes")
2061 .as_str()
2062 .unwrap()
2063 .to_owned();
2064
2065 let client_post_spend_balance = client.balance().await?;
2066 crate::util::almost_equal(
2067 client_post_spend_balance,
2068 CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT,
2069 10_000,
2070 )
2071 .unwrap();
2072
2073 cmd!(client, "reissue", notes).out_json().await?;
2074 let client_post_reissue_balance = client.balance().await?;
2075 crate::util::almost_equal(client_post_reissue_balance, CLIENT_START_AMOUNT, 20_000).unwrap();
2076
2077 let double_spend_notes = cmd!(double_spend_client, "spend", CLIENT_SPEND_AMOUNT)
2079 .out_json()
2080 .await?
2081 .get("notes")
2082 .expect("Output didn't contain e-cash notes")
2083 .as_str()
2084 .unwrap()
2085 .to_owned();
2086
2087 let double_spend_client_post_spend_balance = double_spend_client.balance().await?;
2088 crate::util::almost_equal(
2089 double_spend_client_post_spend_balance,
2090 CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT,
2091 10_000,
2092 )
2093 .unwrap();
2094
2095 cmd!(double_spend_client, "reissue", double_spend_notes)
2096 .assert_error_contains("The transaction had an invalid input")
2097 .await?;
2098
2099 let double_spend_client_post_spend_balance = double_spend_client.balance().await?;
2100 crate::util::almost_equal(
2101 double_spend_client_post_spend_balance,
2102 CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT,
2103 10_000,
2104 )
2105 .unwrap();
2106
2107 Ok(())
2108}
2109
2110pub async fn test_offline_client_initialization(
2114 dev_fed: DevFed,
2115 _process_mgr: &ProcessManager,
2116) -> Result<()> {
2117 log_binary_versions().await?;
2118
2119 let DevFed { mut fed, .. } = dev_fed;
2120
2121 fed.await_all_peers().await?;
2123
2124 let client = fed.new_joined_client("offline-test-client").await?;
2126
2127 const INFO_COMMAND_TIMEOUT: Duration = Duration::from_secs(5);
2129 let online_info =
2130 fedimint_core::runtime::timeout(INFO_COMMAND_TIMEOUT, cmd!(client, "info").out_json())
2131 .await
2132 .context("Client info command timed out while federation was online")?
2133 .context("Client info command failed while federation was online")?;
2134 info!(target: LOG_DEVIMINT, "Client info while federation online: {:?}", online_info);
2135
2136 info!(target: LOG_DEVIMINT, "Shutting down all federation servers...");
2138 fed.terminate_all_servers().await?;
2139
2140 fedimint_core::task::sleep_in_test("wait for federation shutdown", Duration::from_secs(2))
2142 .await;
2143
2144 info!(target: LOG_DEVIMINT, "Testing client info command with all servers offline...");
2148 let offline_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 offline")?
2152 .context("Client info command failed while federation was offline")?;
2153
2154 info!(target: LOG_DEVIMINT, "Client info while federation offline: {:?}", offline_info);
2155
2156 Ok(())
2157}
2158
2159pub async fn test_client_config_change_detection(
2166 dev_fed: DevFed,
2167 process_mgr: &ProcessManager,
2168) -> Result<()> {
2169 log_binary_versions().await?;
2170
2171 let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
2172 let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
2173
2174 if fedimint_cli_version < *VERSION_0_9_0_ALPHA {
2175 info!(target: LOG_DEVIMINT, "Skipping the test - fedimint-cli too old");
2176 return Ok(());
2177 }
2178
2179 if fedimintd_version < *VERSION_0_9_0_ALPHA {
2180 info!(target: LOG_DEVIMINT, "Skipping the test - fedimintd too old");
2181 return Ok(());
2182 }
2183
2184 let DevFed { mut fed, .. } = dev_fed;
2185 let peer_ids: Vec<_> = fed.member_ids().collect();
2186
2187 fed.await_all_peers().await?;
2188
2189 let client = fed.new_joined_client("config-change-test-client").await?;
2190
2191 info!(target: LOG_DEVIMINT, "Getting initial client configuration...");
2192 let initial_config = cmd!(client, "config")
2193 .out_json()
2194 .await
2195 .context("Failed to get initial client config")?;
2196
2197 info!(target: LOG_DEVIMINT, "Initial config modules: {:?}", initial_config["modules"].as_object().unwrap().keys().collect::<Vec<_>>());
2198
2199 let data_dir = env::var(FM_DATA_DIR_ENV)?;
2200 let config_dir = PathBuf::from(&data_dir);
2201
2202 info!(target: LOG_DEVIMINT, "Shutting down all federation servers...");
2209 fed.terminate_all_servers().await?;
2210
2211 fedimint_core::task::sleep_in_test("wait for federation shutdown", Duration::from_secs(2))
2213 .await;
2214
2215 info!(target: LOG_DEVIMINT, "Modifying server configurations to add new meta module...");
2216 modify_server_configs(&config_dir, &peer_ids).await?;
2217
2218 info!(target: LOG_DEVIMINT, "Restarting all servers with modified configurations...");
2220 for peer_id in peer_ids {
2221 fed.start_server(process_mgr, peer_id.to_usize()).await?;
2222 }
2223
2224 info!(target: LOG_DEVIMINT, "Wait for peers to get back up");
2226 fed.await_all_peers().await?;
2227
2228 info!(target: LOG_DEVIMINT, "Waiting for client to fetch updated configuration...");
2230 cmd!(client, "dev", "wait", "3")
2231 .run()
2232 .await
2233 .context("Failed to wait for client config update")?;
2234
2235 info!(target: LOG_DEVIMINT, "Testing client detection of configuration changes...");
2237 let updated_config = cmd!(client, "config")
2238 .out_json()
2239 .await
2240 .context("Failed to get updated client config")?;
2241
2242 info!(target: LOG_DEVIMINT, "Updated config modules: {:?}", updated_config["modules"].as_object().unwrap().keys().collect::<Vec<_>>());
2243
2244 let initial_modules = initial_config["modules"].as_object().unwrap();
2246 let updated_modules = updated_config["modules"].as_object().unwrap();
2247
2248 anyhow::ensure!(
2249 updated_modules.len() > initial_modules.len(),
2250 "Expected more modules in updated config. Initial: {}, Updated: {}",
2251 initial_modules.len(),
2252 updated_modules.len()
2253 );
2254
2255 let new_meta_module = updated_modules.iter().find(|(module_id, module_config)| {
2257 module_config["kind"].as_str() == Some("meta") && !initial_modules.contains_key(*module_id)
2258 });
2259
2260 let new_meta_module_id = new_meta_module
2261 .map(|(id, _)| id)
2262 .with_context(|| "Expected to find new meta module in updated configuration")?;
2263
2264 info!(target: LOG_DEVIMINT, "Found new meta module with id: {}", new_meta_module_id);
2265
2266 info!(target: LOG_DEVIMINT, "Verifying client operations work with new configuration...");
2268 let final_info = cmd!(client, "info")
2269 .out_json()
2270 .await
2271 .context("Client info command failed with updated configuration")?;
2272
2273 info!(target: LOG_DEVIMINT, "Client successfully adapted to configuration changes: {:?}", final_info["federation_id"]);
2274
2275 Ok(())
2276}
2277
2278async fn modify_server_configs(config_dir: &Path, peer_ids: &[PeerId]) -> Result<()> {
2280 for &peer_id in peer_ids {
2281 modify_single_peer_config(config_dir, peer_id).await?;
2282 }
2283 Ok(())
2284}
2285
2286async fn modify_single_peer_config(config_dir: &Path, peer_id: PeerId) -> Result<()> {
2289 use fedimint_aead::{encrypted_write, get_encryption_key};
2290 use fedimint_core::core::ModuleInstanceId;
2291 use fedimint_server::config::io::read_server_config;
2292 use serde_json::Value;
2293
2294 info!(target: LOG_DEVIMINT, %peer_id, "Modifying config for peer");
2295 let peer_dir = config_dir.join(format!("fedimintd-default-{}", peer_id.to_usize()));
2296
2297 let consensus_config_path = peer_dir.join("consensus.json");
2299 let consensus_config_content = fs::read_to_string(&consensus_config_path)
2300 .await
2301 .with_context(|| format!("Failed to read consensus config for peer {peer_id}"))?;
2302
2303 let mut consensus_config: Value = serde_json::from_str(&consensus_config_content)
2304 .with_context(|| format!("Failed to parse consensus config for peer {peer_id}"))?;
2305
2306 let password = "pass"; let server_config = read_server_config(password, &peer_dir)
2309 .with_context(|| format!("Failed to read server config for peer {peer_id}"))?;
2310
2311 let consensus_config_modules = consensus_config["modules"]
2313 .as_object()
2314 .with_context(|| format!("No modules found in consensus config for peer {peer_id}"))?;
2315
2316 let existing_meta_consensus = consensus_config_modules
2318 .values()
2319 .find(|module_config| module_config["kind"].as_str() == Some("meta"));
2320
2321 let existing_meta_consensus = existing_meta_consensus
2322 .with_context(|| {
2323 format!("No existing meta module found in consensus config for peer {peer_id}")
2324 })?
2325 .clone();
2326
2327 let existing_meta_instance_id = server_config
2329 .consensus
2330 .modules
2331 .iter()
2332 .find(|(_, config)| config.kind.as_str() == "meta")
2333 .map(|(id, _)| *id)
2334 .with_context(|| {
2335 format!("No existing meta module found in private config for peer {peer_id}")
2336 })?;
2337
2338 let existing_meta_private = server_config
2339 .private
2340 .modules
2341 .get(&existing_meta_instance_id)
2342 .with_context(|| format!("Failed to get existing meta private config for peer {peer_id}"))?
2343 .clone();
2344
2345 let last_existing_module_id = consensus_config_modules
2347 .keys()
2348 .filter_map(|id| id.parse::<u32>().ok())
2349 .max()
2350 .unwrap_or(0);
2351
2352 let new_module_id = (last_existing_module_id + 1).to_string();
2353 let new_module_instance_id = ModuleInstanceId::from((last_existing_module_id + 1) as u16);
2354
2355 info!(
2356 "Adding new meta module with id {} for peer {} (copying existing meta module config)",
2357 new_module_id, peer_id
2358 );
2359
2360 if let Some(modules) = consensus_config["modules"].as_object_mut() {
2362 modules.insert(new_module_id.clone(), existing_meta_consensus);
2363 }
2364
2365 let mut updated_private_config = server_config.private.clone();
2367 updated_private_config
2368 .modules
2369 .insert(new_module_instance_id, existing_meta_private);
2370
2371 let updated_consensus_content = serde_json::to_string_pretty(&consensus_config)
2373 .with_context(|| format!("Failed to serialize consensus config for peer {peer_id}"))?;
2374
2375 write_overwrite_async(&consensus_config_path, updated_consensus_content)
2376 .await
2377 .with_context(|| format!("Failed to write consensus config for peer {peer_id}"))?;
2378
2379 let salt = std::fs::read_to_string(peer_dir.join("private.salt"))
2381 .with_context(|| format!("Failed to read salt file for peer {peer_id}"))?;
2382 let key = get_encryption_key(password, &salt)
2383 .with_context(|| format!("Failed to get encryption key for peer {peer_id}"))?;
2384
2385 let private_config_bytes = serde_json::to_string(&updated_private_config)
2386 .with_context(|| format!("Failed to serialize private config for peer {peer_id}"))?
2387 .into_bytes();
2388
2389 let encrypted_private_path = peer_dir.join("private.encrypt");
2391 if encrypted_private_path.exists() {
2392 std::fs::remove_file(&encrypted_private_path)
2393 .with_context(|| format!("Failed to remove old private config for peer {peer_id}"))?;
2394 }
2395
2396 encrypted_write(private_config_bytes, &key, encrypted_private_path)
2397 .with_context(|| format!("Failed to write encrypted private config for peer {peer_id}"))?;
2398
2399 info!("Successfully modified configs for peer {}", peer_id);
2400 Ok(())
2401}
2402
2403pub async fn admin_auth_tests(dev_fed: DevFed) -> Result<()> {
2407 log_binary_versions().await?;
2408
2409 let DevFed { fed, .. } = dev_fed;
2410
2411 fed.await_all_peers().await?;
2414
2415 let client = fed.new_joined_client("admin-auth-test-client").await?;
2416
2417 let peer_id = 0;
2418
2419 info!(target: LOG_DEVIMINT, "Testing admin auth command stores credentials");
2420
2421 let auth_result = cmd!(
2424 client,
2425 "--our-id",
2426 &peer_id.to_string(),
2427 "--password",
2428 "pass",
2429 "admin",
2430 "auth",
2431 "--peer-id",
2432 &peer_id.to_string(),
2433 "--password",
2434 "pass",
2435 "--no-verify",
2436 "--force"
2437 )
2438 .out_json()
2439 .await
2440 .context("Admin auth command failed")?;
2441
2442 info!(target: LOG_DEVIMINT, ?auth_result, "Admin auth command completed");
2443
2444 assert_eq!(
2446 auth_result
2447 .get("peer_id")
2448 .and_then(serde_json::Value::as_u64),
2449 Some(peer_id as u64),
2450 "peer_id in response should match"
2451 );
2452 assert_eq!(
2453 auth_result
2454 .get("status")
2455 .and_then(serde_json::Value::as_str),
2456 Some("saved"),
2457 "status should be 'saved'"
2458 );
2459
2460 info!(target: LOG_DEVIMINT, "Testing that stored credentials are used automatically");
2461
2462 let status_result = cmd!(client, "admin", "status")
2465 .out_json()
2466 .await
2467 .context("Admin status command should succeed with stored credentials")?;
2468
2469 info!(target: LOG_DEVIMINT, ?status_result, "Admin status with stored credentials succeeded");
2470
2471 info!(target: LOG_DEVIMINT, "Testing that --force overwrites existing credentials");
2472
2473 let auth_result_force = cmd!(
2475 client,
2476 "--our-id",
2477 &peer_id.to_string(),
2478 "--password",
2479 "pass",
2480 "admin",
2481 "auth",
2482 "--peer-id",
2483 &peer_id.to_string(),
2484 "--password",
2485 "pass",
2486 "--no-verify",
2487 "--force"
2488 )
2489 .out_json()
2490 .await
2491 .context("Admin auth force overwrite failed")?;
2492
2493 assert_eq!(
2494 auth_result_force.get("status").and_then(|v| v.as_str()),
2495 Some("saved"),
2496 "Force overwrite should succeed"
2497 );
2498
2499 info!(target: LOG_DEVIMINT, "admin_auth_tests completed successfully");
2500
2501 Ok(())
2502}
2503
2504pub async fn test_guardian_password_change(
2505 dev_fed: DevFed,
2506 process_mgr: &ProcessManager,
2507) -> Result<()> {
2508 log_binary_versions().await?;
2509
2510 let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
2511 let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
2512
2513 if fedimint_cli_version < *VERSION_0_9_0_ALPHA {
2514 info!(target: LOG_DEVIMINT, "Skipping the test - fedimint-cli too old");
2515 return Ok(());
2516 }
2517
2518 if fedimintd_version < *VERSION_0_9_0_ALPHA {
2519 info!(target: LOG_DEVIMINT, "Skipping the test - fedimintd too old");
2520 return Ok(());
2521 }
2522
2523 let DevFed { mut fed, .. } = dev_fed;
2524 fed.await_all_peers().await?;
2525
2526 let client = fed.new_joined_client("config-change-test-client").await?;
2527
2528 let peer_id = 0;
2529 let data_dir: PathBuf = fed
2530 .vars
2531 .get(&peer_id)
2532 .expect("peer not found")
2533 .FM_DATA_DIR
2534 .clone();
2535 let file_exists = |file: &str| {
2536 let path = data_dir.join(file);
2537 path.exists()
2538 };
2539 let pre_password_file_exists = file_exists("password.secret");
2540
2541 info!(target: LOG_DEVIMINT, "Changing password");
2542 cmd!(
2543 client,
2544 "--our-id",
2545 &peer_id.to_string(),
2546 "--password",
2547 "pass",
2548 "admin",
2549 "change-password",
2550 "foobar"
2551 )
2552 .run()
2553 .await
2554 .context("Failed to change guardian password")?;
2555
2556 info!(target: LOG_DEVIMINT, "Waiting for fedimintd to be shut down");
2557 timeout(
2558 Duration::from_secs(30),
2559 fed.await_server_terminated(peer_id),
2560 )
2561 .await
2562 .context("Fedimintd didn't shut down in time after password change")??;
2563
2564 info!(target: LOG_DEVIMINT, "Restarting fedimintd");
2565 fed.start_server(process_mgr, peer_id).await?;
2566
2567 info!(target: LOG_DEVIMINT, "Wait for fedimintd to come online again");
2568 fed.await_peer(peer_id).await?;
2569
2570 info!(target: LOG_DEVIMINT, "Testing password change worked");
2571 cmd!(
2572 client,
2573 "--our-id",
2574 &peer_id.to_string(),
2575 "--password",
2576 "foobar",
2577 "admin",
2578 "backup-statistics"
2579 )
2580 .run()
2581 .await
2582 .context("Failed to run guardian command with new password")?;
2583
2584 assert!(!file_exists("private.bak"));
2585 assert!(!file_exists("password.bak"));
2586 assert!(!file_exists("private.new"));
2587 assert!(!file_exists("password.new"));
2588 assert_eq!(file_exists("password.secret"), pre_password_file_exists);
2589
2590 Ok(())
2591}
2592
2593#[derive(Subcommand)]
2594pub enum LatencyTest {
2595 Reissue,
2596 LnSend,
2597 LnReceive,
2598 FmPay,
2599 Restore,
2600}
2601
2602#[derive(Subcommand)]
2603pub enum UpgradeTest {
2604 Fedimintd {
2605 #[arg(long, trailing_var_arg = true, num_args=1..)]
2606 paths: Vec<PathBuf>,
2607 },
2608 FedimintCli {
2609 #[arg(long, trailing_var_arg = true, num_args=1..)]
2610 paths: Vec<PathBuf>,
2611 },
2612 Gatewayd {
2613 #[arg(long, trailing_var_arg = true, num_args=1..)]
2614 gatewayd_paths: Vec<PathBuf>,
2615 #[arg(long, trailing_var_arg = true, num_args=1..)]
2616 gateway_cli_paths: Vec<PathBuf>,
2617 },
2618}
2619
2620#[derive(Subcommand)]
2621pub enum TestCmd {
2622 LatencyTests {
2625 #[clap(subcommand)]
2626 r#type: LatencyTest,
2627
2628 #[arg(long, default_value = "10")]
2629 iterations: usize,
2630 },
2631 ReconnectTest,
2634 CliTests,
2636 GuardianMetadataTests,
2638 LoadTestToolTest,
2641 LightningReconnectTest,
2644 GatewayRebootTest,
2647 RecoverytoolTests,
2649 WasmTestSetup {
2651 #[arg(long, trailing_var_arg = true, allow_hyphen_values = true, num_args=1..)]
2652 exec: Option<Vec<ffi::OsString>>,
2653 },
2654 GuardianBackup,
2656 CannotReplayTransaction,
2658 TestOfflineClientInitialization,
2661 TestClientConfigChangeDetection,
2664 TestGuardianPasswordChange,
2667 TestAdminAuth,
2669 UpgradeTests {
2671 #[clap(subcommand)]
2672 binary: UpgradeTest,
2673 #[arg(long)]
2674 lnv2: String,
2675 },
2676}
2677
2678pub async fn handle_command(cmd: TestCmd, common_args: CommonArgs) -> Result<()> {
2679 match cmd {
2680 TestCmd::WasmTestSetup { exec } => {
2681 let (process_mgr, task_group) = setup(common_args).await?;
2682 let main = {
2683 let task_group = task_group.clone();
2684 async move {
2685 let dev_fed = dev_fed(&process_mgr).await?;
2686 let gw_lnd = dev_fed.gw_lnd.clone();
2687 let fed = dev_fed.fed.clone();
2688 gw_lnd
2689 .set_federation_routing_fee(dev_fed.fed.calculate_federation_id(), 0, 0)
2690 .await?;
2691 task_group.spawn_cancellable("faucet", async move {
2692 if let Err(err) = crate::faucet::run(
2693 &dev_fed,
2694 format!("0.0.0.0:{}", process_mgr.globals.FM_PORT_FAUCET),
2695 process_mgr.globals.FM_PORT_GW_LND,
2696 )
2697 .await
2698 {
2699 error!("Error spawning faucet: {err}");
2700 }
2701 });
2702 try_join!(fed.pegin_gateways(30_000, vec![&gw_lnd]), async {
2703 poll("waiting for faucet startup", || async {
2704 TcpStream::connect(format!(
2705 "127.0.0.1:{}",
2706 process_mgr.globals.FM_PORT_FAUCET
2707 ))
2708 .await
2709 .context("connect to faucet")
2710 .map_err(ControlFlow::Continue)
2711 })
2712 .await?;
2713 Ok(())
2714 },)?;
2715 if let Some(exec) = exec {
2716 exec_user_command(exec).await?;
2717 task_group.shutdown();
2718 }
2719 Ok::<_, anyhow::Error>(())
2720 }
2721 };
2722 cleanup_on_exit(main, task_group).await?;
2723 }
2724 TestCmd::LatencyTests { r#type, iterations } => {
2725 let (process_mgr, _) = setup(common_args).await?;
2726 let dev_fed = dev_fed(&process_mgr).await?;
2727 latency_tests(dev_fed, r#type, None, iterations, true).await?;
2728 }
2729 TestCmd::ReconnectTest => {
2730 let (process_mgr, _) = setup(common_args).await?;
2731 let dev_fed = dev_fed(&process_mgr).await?;
2732 reconnect_test(dev_fed, &process_mgr).await?;
2733 }
2734 TestCmd::CliTests => {
2735 let (process_mgr, _) = setup(common_args).await?;
2736 let dev_fed = dev_fed(&process_mgr).await?;
2737 cli_tests(dev_fed).await?;
2738 }
2739 TestCmd::GuardianMetadataTests => {
2740 let (process_mgr, _) = setup(common_args).await?;
2741 let dev_fed = dev_fed(&process_mgr).await?;
2742 guardian_metadata_tests(dev_fed).await?;
2743 }
2744 TestCmd::LoadTestToolTest => {
2745 unsafe { std::env::set_var(FM_DISABLE_BASE_FEES_ENV, "1") };
2747
2748 let (process_mgr, _) = setup(common_args).await?;
2749 let dev_fed = dev_fed(&process_mgr).await?;
2750 cli_load_test_tool_test(dev_fed).await?;
2751 }
2752 TestCmd::LightningReconnectTest => {
2753 let (process_mgr, _) = setup(common_args).await?;
2754 let dev_fed = dev_fed(&process_mgr).await?;
2755 lightning_gw_reconnect_test(dev_fed, &process_mgr).await?;
2756 }
2757 TestCmd::GatewayRebootTest => {
2758 let (process_mgr, _) = setup(common_args).await?;
2759 let dev_fed = dev_fed(&process_mgr).await?;
2760 gw_reboot_test(dev_fed, &process_mgr).await?;
2761 }
2762 TestCmd::RecoverytoolTests => {
2763 let (process_mgr, _) = setup(common_args).await?;
2764 let dev_fed = dev_fed(&process_mgr).await?;
2765 recoverytool_test(dev_fed).await?;
2766 }
2767 TestCmd::GuardianBackup => {
2768 let (process_mgr, _) = setup(common_args).await?;
2769 let dev_fed = dev_fed(&process_mgr).await?;
2770 guardian_backup_test(dev_fed, &process_mgr).await?;
2771 }
2772 TestCmd::CannotReplayTransaction => {
2773 let (process_mgr, _) = setup(common_args).await?;
2774 let dev_fed = dev_fed(&process_mgr).await?;
2775 cannot_replay_tx_test(dev_fed).await?;
2776 }
2777 TestCmd::TestOfflineClientInitialization => {
2778 let (process_mgr, _) = setup(common_args).await?;
2779 let dev_fed = dev_fed(&process_mgr).await?;
2780 test_offline_client_initialization(dev_fed, &process_mgr).await?;
2781 }
2782 TestCmd::TestClientConfigChangeDetection => {
2783 let (process_mgr, _) = setup(common_args).await?;
2784 let dev_fed = dev_fed(&process_mgr).await?;
2785 test_client_config_change_detection(dev_fed, &process_mgr).await?;
2786 }
2787 TestCmd::TestGuardianPasswordChange => {
2788 let (process_mgr, _) = setup(common_args).await?;
2789 let dev_fed = dev_fed(&process_mgr).await?;
2790 test_guardian_password_change(dev_fed, &process_mgr).await?;
2791 }
2792 TestCmd::TestAdminAuth => {
2793 let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
2795 let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
2796
2797 if fedimint_cli_version < *VERSION_0_11_0_ALPHA
2798 || fedimintd_version < *VERSION_0_11_0_ALPHA
2799 {
2800 info!(target: LOG_DEVIMINT, "Skipping admin_auth_tests - requires v0.11.0-alpha or later");
2801 return Ok(());
2802 }
2803
2804 let (process_mgr, _) = setup(common_args).await?;
2805 let dev_fed = dev_fed(&process_mgr).await?;
2806 admin_auth_tests(dev_fed).await?;
2807 }
2808 TestCmd::UpgradeTests { binary, lnv2 } => {
2809 unsafe { std::env::set_var(FM_ENABLE_MODULE_LNV2_ENV, lnv2) };
2811 let (process_mgr, _) = setup(common_args).await?;
2812 Box::pin(upgrade_tests(&process_mgr, binary)).await?;
2813 }
2814 }
2815 Ok(())
2816}