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