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