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