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::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 descriptor = config["modules"]["2"]["peg_in_descriptor"]
692 .as_str()
693 .unwrap()
694 .to_owned();
695
696 info!("Testing generated descriptor for {guardian_count} guardian federation");
697 if guardian_count == 1 {
698 assert!(descriptor.contains("wpkh("));
699 } else {
700 assert!(descriptor.contains("wsh(sortedmulti("));
701 }
702
703 info!("Testing Client");
705
706 info!("Testing reissuing e-cash");
708 const CLIENT_START_AMOUNT: u64 = 5_000_000_000;
709 const CLIENT_SPEND_AMOUNT: u64 = 1_100_000;
710
711 let initial_client_balance = client.balance().await?;
712 assert_eq!(initial_client_balance, 0);
713
714 fed.pegin_client(CLIENT_START_AMOUNT / 1000, &client)
715 .await?;
716
717 info!("Testing spending from client");
719 let notes = cmd!(client, "spend", CLIENT_SPEND_AMOUNT)
720 .out_json()
721 .await?
722 .get("notes")
723 .expect("Output didn't contain e-cash notes")
724 .as_str()
725 .unwrap()
726 .to_owned();
727
728 let client_post_spend_balance = client.balance().await?;
729 almost_equal(
730 client_post_spend_balance,
731 CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT,
732 10_000,
733 )
734 .unwrap();
735
736 cmd!(client, "reissue", notes).out_json().await?;
738
739 let client_post_spend_balance = client.balance().await?;
740 almost_equal(client_post_spend_balance, CLIENT_START_AMOUNT, 10_000).unwrap();
741
742 let reissue_amount: u64 = 409_600;
743
744 info!("Testing reissuing e-cash after spending");
746 let _notes = cmd!(client, "spend", CLIENT_SPEND_AMOUNT)
747 .out_json()
748 .await?
749 .as_object()
750 .unwrap()
751 .get("notes")
752 .expect("Output didn't contain e-cash notes")
753 .as_str()
754 .unwrap();
755
756 let reissue_notes = cmd!(client, "spend", reissue_amount).out_json().await?["notes"]
757 .as_str()
758 .map(ToOwned::to_owned)
759 .unwrap();
760 let client_reissue_amt = cmd!(client, "reissue", reissue_notes)
761 .out_json()
762 .await?
763 .as_u64()
764 .unwrap();
765 assert_eq!(client_reissue_amt, reissue_amount);
766
767 info!("Testing reissuing e-cash via module commands");
769 let reissue_notes = cmd!(client, "spend", reissue_amount).out_json().await?["notes"]
770 .as_str()
771 .map(ToOwned::to_owned)
772 .unwrap();
773 let client_reissue_amt = cmd!(client, "module", "mint", "reissue", reissue_notes)
774 .out_json()
775 .await?
776 .as_u64()
777 .unwrap();
778 assert_eq!(client_reissue_amt, reissue_amount);
779
780 info!("Testing LND gateway");
782
783 let gatewayd_version = crate::util::Gatewayd::version_or_default().await;
784 if gatewayd_version < *VERSION_0_8_0_ALPHA {
788 gw_lnd
789 .set_federation_routing_fee(fed_id.clone(), 0, 0)
790 .await?;
791
792 poll("Waiting for LND GW fees to update", || async {
794 let gateways_val = cmd!(client, "list-gateways")
795 .out_json()
796 .await
797 .map_err(ControlFlow::Break)?;
798 let gateways =
799 serde_json::from_value::<Vec<LightningGatewayAnnouncement>>(gateways_val)
800 .expect("Could not deserialize");
801 let fees = gateways
802 .first()
803 .expect("No gateway was registered")
804 .info
805 .fees;
806 if fees.base_msat == 0 && fees.proportional_millionths == 0 {
807 Ok(())
808 } else {
809 Err(ControlFlow::Continue(anyhow!("Fees have not been updated")))
810 }
811 })
812 .await?;
813 }
814
815 info!("Testing outgoing payment from client to LDK via LND gateway");
817 let initial_lnd_gateway_balance = gw_lnd.ecash_balance(fed_id.clone()).await?;
818 let invoice = gw_ldk.create_invoice(2_000_000).await?;
819 ln_pay(&client, invoice.to_string(), lnd_gw_id.clone()).await?;
820 let fed_id = fed.calculate_federation_id();
821 gw_ldk
822 .wait_bolt11_invoice(invoice.payment_hash().consensus_encode_to_vec())
823 .await?;
824
825 let final_lnd_outgoing_client_balance = client.balance().await?;
827 let final_lnd_outgoing_gateway_balance = gw_lnd.ecash_balance(fed_id.clone()).await?;
828 anyhow::ensure!(
829 almost_equal(
830 final_lnd_outgoing_gateway_balance - initial_lnd_gateway_balance,
831 2_000_000,
832 1_000
833 )
834 .is_ok(),
835 "LND Gateway balance changed by {} on LND outgoing payment, expected 2_000_000",
836 (final_lnd_outgoing_gateway_balance - initial_lnd_gateway_balance)
837 );
838
839 info!("Testing incoming payment from LDK to client via LND gateway");
841 let recv = ln_invoice(
842 &client,
843 Amount::from_msats(1_300_000),
844 "incoming-over-lnd-gw".to_string(),
845 lnd_gw_id,
846 )
847 .await?;
848 let invoice = recv.invoice;
849 gw_ldk
850 .pay_invoice(Bolt11Invoice::from_str(&invoice).expect("Could not parse invoice"))
851 .await?;
852
853 info!("Testing receiving ecash notes");
855 let operation_id = recv.operation_id;
856 cmd!(client, "await-invoice", operation_id.fmt_full())
857 .run()
858 .await?;
859
860 let final_lnd_incoming_client_balance = client.balance().await?;
862 let final_lnd_incoming_gateway_balance = gw_lnd.ecash_balance(fed_id.clone()).await?;
863 anyhow::ensure!(
864 almost_equal(
865 final_lnd_incoming_client_balance - final_lnd_outgoing_client_balance,
866 1_300_000,
867 2_000
868 )
869 .is_ok(),
870 "Client balance changed by {} on LND incoming payment, expected 1_300_000",
871 (final_lnd_incoming_client_balance - final_lnd_outgoing_client_balance)
872 );
873 anyhow::ensure!(
874 almost_equal(
875 final_lnd_outgoing_gateway_balance - final_lnd_incoming_gateway_balance,
876 1_300_000,
877 2_000
878 )
879 .is_ok(),
880 "LND Gateway balance changed by {} on LND incoming payment, expected 1_300_000",
881 (final_lnd_outgoing_gateway_balance - final_lnd_incoming_gateway_balance)
882 );
883
884 info!("Testing client deposit");
889 let initial_walletng_balance = client.balance().await?;
890
891 fed.pegin_client(100_000, &client).await?; let post_deposit_walletng_balance = client.balance().await?;
894
895 almost_equal(
896 post_deposit_walletng_balance,
897 initial_walletng_balance + 100_000_000, 2_000,
899 )
900 .unwrap();
901
902 info!("Testing client withdraw");
904
905 let initial_walletng_balance = client.balance().await?;
906
907 let address = bitcoind.get_new_address().await?;
908 let withdraw_res = cmd!(
909 client,
910 "withdraw",
911 "--address",
912 &address,
913 "--amount",
914 "50000 sat"
915 )
916 .out_json()
917 .await?;
918
919 let txid: Txid = withdraw_res["txid"].as_str().unwrap().parse().unwrap();
920 let fees_sat = withdraw_res["fees_sat"].as_u64().unwrap();
921
922 let tx_hex = bitcoind.poll_get_transaction(txid).await?;
923
924 let tx = bitcoin::Transaction::consensus_decode_hex(&tx_hex, &ModuleRegistry::default())?;
925 assert!(
926 tx.output
927 .iter()
928 .any(|o| o.script_pubkey == address.script_pubkey() && o.value.to_sat() == 50000)
929 );
930
931 let post_withdraw_walletng_balance = client.balance().await?;
932 let expected_wallet_balance = initial_walletng_balance - 50_000_000 - (fees_sat * 1000);
933
934 almost_equal(
935 post_withdraw_walletng_balance,
936 expected_wallet_balance,
937 2_000,
938 )
939 .unwrap();
940
941 let peer_0_fedimintd_version = cmd!(client, "dev", "peer-version", "--peer-id", "0")
943 .out_json()
944 .await?
945 .get("version")
946 .expect("Output didn't contain version")
947 .as_str()
948 .unwrap()
949 .to_owned();
950
951 assert_eq!(
952 semver::Version::parse(&peer_0_fedimintd_version)?,
953 fedimintd_version
954 );
955
956 info!("Checking initial announcements...");
957
958 retry(
959 "Check initial announcements",
960 aggressive_backoff(),
961 || async {
962 let initial_announcements =
964 serde_json::from_value::<BTreeMap<PeerId, SignedApiAnnouncement>>(
965 cmd!(client, "dev", "api-announcements",).out_json().await?,
966 )
967 .expect("failed to parse API announcements");
968
969 if fed.members.len() != initial_announcements.len() {
970 bail!(
971 "Not all announcements ready: {}",
972 initial_announcements.len()
973 )
974 }
975
976 cmd!(client, "dev", "wait", "3").run().await?;
978
979 if !initial_announcements
980 .values()
981 .all(|announcement| announcement.api_announcement.nonce == 0)
982 {
983 bail!("Not all announcements have their initial value");
984 }
985 Ok(())
986 },
987 )
988 .await?;
989
990 const NEW_API_URL: &str = "ws://127.0.0.1:4242";
991 let new_announcement = serde_json::from_value::<SignedApiAnnouncement>(
992 cmd!(
993 client,
994 "--our-id",
995 "0",
996 "--password",
997 "pass",
998 "admin",
999 "sign-api-announcement",
1000 NEW_API_URL
1001 )
1002 .out_json()
1003 .await?,
1004 )
1005 .expect("Couldn't parse signed announcement");
1006
1007 assert_eq!(
1008 new_announcement.api_announcement.nonce, 1,
1009 "Nonce did not increment correctly"
1010 );
1011
1012 info!("Testing if the client syncs the announcement");
1013 let announcement = poll("Waiting for the announcement to propagate", || async {
1014 cmd!(client, "dev", "wait", "1")
1015 .run()
1016 .await
1017 .map_err(ControlFlow::Break)?;
1018
1019 let new_announcements_peer2 =
1020 serde_json::from_value::<BTreeMap<PeerId, SignedApiAnnouncement>>(
1021 cmd!(client, "dev", "api-announcements",)
1022 .out_json()
1023 .await
1024 .map_err(ControlFlow::Break)?,
1025 )
1026 .expect("failed to parse API announcements");
1027
1028 let announcement = new_announcements_peer2[&PeerId::from(0)]
1029 .api_announcement
1030 .clone();
1031 if announcement.nonce == 1 {
1032 Ok(announcement)
1033 } else {
1034 Err(ControlFlow::Continue(anyhow!(
1035 "Haven't received updated announcement yet; nonce: {}",
1036 announcement.nonce
1037 )))
1038 }
1039 })
1040 .await?;
1041
1042 assert_eq!(
1043 announcement.api_url,
1044 NEW_API_URL.parse().expect("valid URL")
1045 );
1046
1047 Ok(())
1048}
1049
1050pub async fn cli_load_test_tool_test(dev_fed: DevFed) -> Result<()> {
1051 log_binary_versions().await?;
1052 let data_dir = env::var(FM_DATA_DIR_ENV)?;
1053 let load_test_temp = PathBuf::from(data_dir).join("load-test-temp");
1054 dev_fed
1055 .fed
1056 .pegin_client(10_000, dev_fed.fed.internal_client().await?)
1057 .await?;
1058 let invite_code = dev_fed.fed.invite_code()?;
1059 dev_fed
1060 .gw_lnd
1061 .set_federation_routing_fee(dev_fed.fed.calculate_federation_id(), 0, 0)
1062 .await?;
1063 run_standard_load_test(&load_test_temp, &invite_code).await?;
1064 run_ln_circular_load_test(&load_test_temp, &invite_code).await?;
1065 Ok(())
1066}
1067
1068pub async fn run_standard_load_test(
1069 load_test_temp: &Path,
1070 invite_code: &str,
1071) -> anyhow::Result<()> {
1072 let output = cmd!(
1073 LoadTestTool,
1074 "--archive-dir",
1075 load_test_temp.display(),
1076 "--users",
1077 "1",
1078 "load-test",
1079 "--notes-per-user",
1080 "1",
1081 "--generate-invoice-with",
1082 "ldk-lightning-cli",
1083 "--invite-code",
1084 invite_code
1085 )
1086 .out_string()
1087 .await?;
1088 println!("{output}");
1089 anyhow::ensure!(
1090 output.contains("2 reissue_notes"),
1091 "reissued different number notes than expected"
1092 );
1093 anyhow::ensure!(
1094 output.contains("1 gateway_pay_invoice"),
1095 "paid different number of invoices than expected"
1096 );
1097 Ok(())
1098}
1099
1100pub async fn run_ln_circular_load_test(
1101 load_test_temp: &Path,
1102 invite_code: &str,
1103) -> anyhow::Result<()> {
1104 info!("Testing ln-circular-load-test with 'two-gateways' strategy");
1105 let output = cmd!(
1106 LoadTestTool,
1107 "--archive-dir",
1108 load_test_temp.display(),
1109 "--users",
1110 "1",
1111 "ln-circular-load-test",
1112 "--strategy",
1113 "two-gateways",
1114 "--test-duration-secs",
1115 "2",
1116 "--invite-code",
1117 invite_code
1118 )
1119 .out_string()
1120 .await?;
1121 println!("{output}");
1122 anyhow::ensure!(
1123 output.contains("gateway_create_invoice"),
1124 "missing invoice creation"
1125 );
1126 anyhow::ensure!(
1127 output.contains("gateway_pay_invoice_success"),
1128 "missing invoice payment"
1129 );
1130 anyhow::ensure!(
1131 output.contains("gateway_payment_received_success"),
1132 "missing received payment"
1133 );
1134
1135 info!("Testing ln-circular-load-test with 'partner-ping-pong' strategy");
1136 let output = cmd!(
1140 LoadTestTool,
1141 "--archive-dir",
1142 load_test_temp.display(),
1143 "--users",
1144 "1",
1145 "ln-circular-load-test",
1146 "--strategy",
1147 "partner-ping-pong",
1148 "--test-duration-secs",
1149 "6",
1150 "--invite-code",
1151 invite_code
1152 )
1153 .out_string()
1154 .await?;
1155 println!("{output}");
1156 anyhow::ensure!(
1157 output.contains("gateway_create_invoice"),
1158 "missing invoice creation"
1159 );
1160 anyhow::ensure!(
1161 output.contains("gateway_payment_received_success"),
1162 "missing received payment"
1163 );
1164
1165 info!("Testing ln-circular-load-test with 'self-payment' strategy");
1166 let output = cmd!(
1168 LoadTestTool,
1169 "--archive-dir",
1170 load_test_temp.display(),
1171 "--users",
1172 "1",
1173 "ln-circular-load-test",
1174 "--strategy",
1175 "self-payment",
1176 "--test-duration-secs",
1177 "2",
1178 "--invite-code",
1179 invite_code
1180 )
1181 .out_string()
1182 .await?;
1183 println!("{output}");
1184 anyhow::ensure!(
1185 output.contains("gateway_create_invoice"),
1186 "missing invoice creation"
1187 );
1188 anyhow::ensure!(
1189 output.contains("gateway_payment_received_success"),
1190 "missing received payment"
1191 );
1192 Ok(())
1193}
1194
1195pub async fn lightning_gw_reconnect_test(
1196 dev_fed: DevFed,
1197 process_mgr: &ProcessManager,
1198) -> Result<()> {
1199 log_binary_versions().await?;
1200
1201 let DevFed {
1202 bitcoind,
1203 lnd,
1204 fed,
1205 mut gw_lnd,
1206 gw_ldk,
1207 ..
1208 } = dev_fed;
1209
1210 let client = fed
1211 .new_joined_client("lightning-gw-reconnect-test-client")
1212 .await?;
1213
1214 info!("Pegging-in both gateways");
1215 fed.pegin_gateways(99_999, vec![&gw_lnd]).await?;
1216
1217 drop(lnd);
1219
1220 tracing::info!("Stopping LND");
1221 let mut info_cmd = cmd!(gw_lnd, "info");
1223 assert!(info_cmd.run().await.is_ok());
1224
1225 let ln_type = gw_lnd.ln.ln_type().to_string();
1228 gw_lnd.stop_lightning_node().await?;
1229 let lightning_info = info_cmd.out_json().await?;
1230 if gw_lnd.gatewayd_version < *VERSION_0_10_0_ALPHA {
1231 let lightning_pub_key: Option<String> =
1232 serde_json::from_value(lightning_info["lightning_pub_key"].clone())?;
1233
1234 assert!(lightning_pub_key.is_none());
1235 } else {
1236 let not_connected = lightning_info["lightning_info"].clone();
1237 assert!(not_connected.as_str().expect("ln info is not a string") == "not_connected");
1238 }
1239
1240 tracing::info!("Restarting LND...");
1242 let new_lnd = Lnd::new(process_mgr, bitcoind.clone()).await?;
1243 gw_lnd.set_lightning_node(LightningNode::Lnd(new_lnd.clone()));
1244
1245 tracing::info!("Retrying info...");
1246 const MAX_RETRIES: usize = 30;
1247 const RETRY_INTERVAL: Duration = Duration::from_secs(1);
1248
1249 for i in 0..MAX_RETRIES {
1250 match do_try_create_and_pay_invoice(&gw_lnd, &client, &gw_ldk).await {
1251 Ok(()) => break,
1252 Err(e) => {
1253 if i == MAX_RETRIES - 1 {
1254 return Err(e);
1255 }
1256 tracing::debug!(
1257 "Pay invoice for gateway {} failed with {e:?}, retrying in {} seconds (try {}/{MAX_RETRIES})",
1258 ln_type,
1259 RETRY_INTERVAL.as_secs(),
1260 i + 1,
1261 );
1262 fedimint_core::task::sleep_in_test(
1263 "paying invoice for gateway failed",
1264 RETRY_INTERVAL,
1265 )
1266 .await;
1267 }
1268 }
1269 }
1270
1271 info!(target: LOG_DEVIMINT, "lightning_reconnect_test: success");
1272 Ok(())
1273}
1274
1275pub async fn gw_reboot_test(dev_fed: DevFed, process_mgr: &ProcessManager) -> Result<()> {
1276 log_binary_versions().await?;
1277
1278 let DevFed {
1279 bitcoind,
1280 lnd,
1281 fed,
1282 gw_lnd,
1283 gw_ldk,
1284 gw_ldk_second,
1285 ..
1286 } = dev_fed;
1287
1288 let client = fed.new_joined_client("gw-reboot-test-client").await?;
1289 fed.pegin_client(10_000, &client).await?;
1290
1291 let block_height = bitcoind.get_block_count().await? - 1;
1293 try_join!(
1294 gw_lnd.wait_for_block_height(block_height),
1295 gw_ldk.wait_for_block_height(block_height),
1296 )?;
1297
1298 let (lnd_value, ldk_value) = try_join!(gw_lnd.get_info(), gw_ldk.get_info())?;
1300
1301 let lnd_gateway_id = gw_lnd.gateway_id.clone();
1303 let gw_ldk_name = gw_ldk.gw_name.clone();
1304 let gw_ldk_port = gw_ldk.gw_port;
1305 let gw_lightning_port = gw_ldk.ldk_port;
1306 drop(gw_lnd);
1307 drop(gw_ldk);
1308
1309 info!("Making payment while gateway is down");
1312 let initial_client_balance = client.balance().await?;
1313 let invoice = gw_ldk_second.create_invoice(3000).await?;
1314 ln_pay(&client, invoice.to_string(), lnd_gateway_id)
1315 .await
1316 .expect_err("Expected ln-pay to return error because the gateway is not online");
1317 let new_client_balance = client.balance().await?;
1318 anyhow::ensure!(initial_client_balance == new_client_balance);
1319
1320 info!("Rebooting gateways...");
1322 let (new_gw_lnd, new_gw_ldk) = try_join!(
1323 Gatewayd::new(process_mgr, LightningNode::Lnd(lnd.clone()), 0),
1324 Gatewayd::new(
1325 process_mgr,
1326 LightningNode::Ldk {
1327 name: gw_ldk_name,
1328 gw_port: gw_ldk_port,
1329 ldk_port: gw_lightning_port,
1330 },
1331 1,
1332 )
1333 )?;
1334
1335 let lnd_gateway_id: fedimint_core::secp256k1::PublicKey =
1336 serde_json::from_value(lnd_value["gateway_id"].clone())?;
1337
1338 poll(
1339 "Waiting for LND Gateway Running state after reboot",
1340 || async {
1341 let mut new_lnd_cmd = cmd!(new_gw_lnd, "info");
1342 let lnd_value = new_lnd_cmd.out_json().await.map_err(ControlFlow::Continue)?;
1343 let reboot_gateway_state: String = serde_json::from_value(lnd_value["gateway_state"].clone()).context("invalid gateway state").map_err(ControlFlow::Break)?;
1344 let reboot_gateway_id: fedimint_core::secp256k1::PublicKey =
1345 serde_json::from_value(lnd_value["gateway_id"].clone()).context("invalid gateway id").map_err(ControlFlow::Break)?;
1346
1347 if reboot_gateway_state == "Running" {
1348 info!(target: LOG_DEVIMINT, "LND Gateway restarted, with auto-rejoin to federation");
1349 assert_eq!(lnd_gateway_id, reboot_gateway_id);
1351 return Ok(());
1352 }
1353 Err(ControlFlow::Continue(anyhow!("gateway not running")))
1354 },
1355 )
1356 .await?;
1357
1358 let ldk_gateway_id: fedimint_core::secp256k1::PublicKey =
1359 serde_json::from_value(ldk_value["gateway_id"].clone())?;
1360 poll(
1361 "Waiting for LDK Gateway Running state after reboot",
1362 || async {
1363 let mut new_ldk_cmd = cmd!(new_gw_ldk, "info");
1364 let ldk_value = new_ldk_cmd.out_json().await.map_err(ControlFlow::Continue)?;
1365 let reboot_gateway_state: String = serde_json::from_value(ldk_value["gateway_state"].clone()).context("invalid gateway state").map_err(ControlFlow::Break)?;
1366 let reboot_gateway_id: fedimint_core::secp256k1::PublicKey =
1367 serde_json::from_value(ldk_value["gateway_id"].clone()).context("invalid gateway id").map_err(ControlFlow::Break)?;
1368
1369 if reboot_gateway_state == "Running" {
1370 info!(target: LOG_DEVIMINT, "LDK Gateway restarted, with auto-rejoin to federation");
1371 assert_eq!(ldk_gateway_id, reboot_gateway_id);
1373 return Ok(());
1374 }
1375 Err(ControlFlow::Continue(anyhow!("gateway not running")))
1376 },
1377 )
1378 .await?;
1379
1380 info!(LOG_DEVIMINT, "gateway_reboot_test: success");
1381 Ok(())
1382}
1383
1384pub async fn do_try_create_and_pay_invoice(
1385 gw_lnd: &Gatewayd,
1386 client: &Client,
1387 gw_ldk: &Gatewayd,
1388) -> anyhow::Result<()> {
1389 poll("Waiting for info to succeed after restart", || async {
1393 gw_lnd
1394 .lightning_pubkey()
1395 .await
1396 .map_err(ControlFlow::Continue)?;
1397 Ok(())
1398 })
1399 .await?;
1400
1401 tracing::info!("Creating invoice....");
1402 let invoice = ln_invoice(
1403 client,
1404 Amount::from_msats(1000),
1405 "incoming-over-lnd-gw".to_string(),
1406 gw_lnd.gateway_id.clone(),
1407 )
1408 .await?
1409 .invoice;
1410
1411 match &gw_lnd.ln.ln_type() {
1412 LightningNodeType::Lnd => {
1413 gw_ldk
1415 .pay_invoice(Bolt11Invoice::from_str(&invoice).expect("Could not parse invoice"))
1416 .await?;
1417 }
1418 LightningNodeType::Ldk => {
1419 unimplemented!("do_try_create_and_pay_invoice not implemented for LDK yet");
1420 }
1421 }
1422 Ok(())
1423}
1424
1425async fn ln_pay(client: &Client, invoice: String, gw_id: String) -> anyhow::Result<String> {
1426 let value = cmd!(client, "ln-pay", invoice, "--gateway-id", gw_id,)
1427 .out_json()
1428 .await?;
1429 let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
1430 if fedimint_cli_version >= *VERSION_0_9_0_ALPHA {
1431 let outcome = serde_json::from_value::<LightningPaymentOutcome>(value)
1432 .expect("Could not deserialize Lightning payment outcome");
1433 match outcome {
1434 LightningPaymentOutcome::Success { preimage } => Ok(preimage),
1435 LightningPaymentOutcome::Failure { error_message } => {
1436 Err(anyhow!("Failed to pay lightning invoice: {error_message}"))
1437 }
1438 }
1439 } else {
1440 let operation_id = value["operation_id"]
1441 .as_str()
1442 .ok_or(anyhow!("Failed to pay invoice"))?
1443 .to_string();
1444 Ok(operation_id)
1445 }
1446}
1447
1448async fn ln_invoice(
1449 client: &Client,
1450 amount: Amount,
1451 description: String,
1452 gw_id: String,
1453) -> anyhow::Result<LnInvoiceResponse> {
1454 let ln_response_val = cmd!(
1455 client,
1456 "ln-invoice",
1457 "--amount",
1458 amount.msats,
1459 format!("--description='{description}'"),
1460 "--gateway-id",
1461 gw_id,
1462 )
1463 .out_json()
1464 .await?;
1465
1466 let ln_invoice_response: LnInvoiceResponse = serde_json::from_value(ln_response_val)?;
1467
1468 Ok(ln_invoice_response)
1469}
1470
1471async fn lnv2_receive(
1472 client: &Client,
1473 gateway: &str,
1474 amount: u64,
1475) -> anyhow::Result<(Bolt11Invoice, OperationId)> {
1476 Ok(serde_json::from_value::<(Bolt11Invoice, OperationId)>(
1477 cmd!(
1478 client,
1479 "module",
1480 "lnv2",
1481 "receive",
1482 amount,
1483 "--gateway",
1484 gateway
1485 )
1486 .out_json()
1487 .await?,
1488 )?)
1489}
1490
1491async fn lnv2_send(client: &Client, gateway: &String, invoice: &String) -> anyhow::Result<()> {
1492 let send_op = serde_json::from_value::<OperationId>(
1493 cmd!(
1494 client,
1495 "module",
1496 "lnv2",
1497 "send",
1498 invoice,
1499 "--gateway",
1500 gateway
1501 )
1502 .out_json()
1503 .await?,
1504 )?;
1505
1506 assert_eq!(
1507 cmd!(
1508 client,
1509 "module",
1510 "lnv2",
1511 "await-send",
1512 serde_json::to_string(&send_op)?.substring(1, 65)
1513 )
1514 .out_json()
1515 .await?,
1516 serde_json::to_value(FinalSendOperationState::Success).expect("JSON serialization failed"),
1517 );
1518
1519 Ok(())
1520}
1521
1522pub async fn reconnect_test(dev_fed: DevFed, process_mgr: &ProcessManager) -> Result<()> {
1523 log_binary_versions().await?;
1524
1525 let DevFed {
1526 bitcoind, mut fed, ..
1527 } = dev_fed;
1528
1529 bitcoind.mine_blocks(110).await?;
1530 fed.await_block_sync().await?;
1531 fed.await_all_peers().await?;
1532
1533 fed.terminate_server(0).await?;
1535 fed.mine_then_wait_blocks_sync(100).await?;
1536
1537 fed.start_server(process_mgr, 0).await?;
1538 fed.mine_then_wait_blocks_sync(100).await?;
1539 fed.await_all_peers().await?;
1540 info!(target: LOG_DEVIMINT, "Server 0 successfully rejoined!");
1541 fed.mine_then_wait_blocks_sync(100).await?;
1542
1543 fed.terminate_server(1).await?;
1545 fed.mine_then_wait_blocks_sync(100).await?;
1546 fed.terminate_server(2).await?;
1547 fed.terminate_server(3).await?;
1548
1549 fed.start_server(process_mgr, 1).await?;
1550 fed.start_server(process_mgr, 2).await?;
1551 fed.start_server(process_mgr, 3).await?;
1552
1553 fed.await_all_peers().await?;
1554
1555 info!(target: LOG_DEVIMINT, "fm success: reconnect-test");
1556 Ok(())
1557}
1558
1559pub async fn recoverytool_test(dev_fed: DevFed) -> Result<()> {
1560 log_binary_versions().await?;
1561
1562 let DevFed { bitcoind, fed, .. } = dev_fed;
1563
1564 let data_dir = env::var(FM_DATA_DIR_ENV)?;
1565 let client = fed.new_joined_client("recoverytool-test-client").await?;
1566
1567 let mut fed_utxos_sats = HashSet::from([12_345_000, 23_456_000, 34_567_000]);
1568 let deposit_fees = fed.deposit_fees()?.msats / 1000;
1569 for sats in &fed_utxos_sats {
1570 fed.pegin_client(*sats - deposit_fees, &client).await?;
1572 }
1573
1574 async fn withdraw(
1575 client: &Client,
1576 bitcoind: &crate::external::Bitcoind,
1577 fed_utxos_sats: &mut HashSet<u64>,
1578 ) -> Result<()> {
1579 let withdrawal_address = bitcoind.get_new_address().await?;
1580 let withdraw_res = cmd!(
1581 client,
1582 "withdraw",
1583 "--address",
1584 &withdrawal_address,
1585 "--amount",
1586 "5000 sat"
1587 )
1588 .out_json()
1589 .await?;
1590
1591 let fees_sat = withdraw_res["fees_sat"]
1592 .as_u64()
1593 .expect("withdrawal should contain fees");
1594 let txid: Txid = withdraw_res["txid"]
1595 .as_str()
1596 .expect("withdrawal should contain txid string")
1597 .parse()
1598 .expect("txid should be parsable");
1599 let tx_hex = bitcoind.poll_get_transaction(txid).await?;
1600
1601 let tx = bitcoin::Transaction::consensus_decode_hex(&tx_hex, &ModuleRegistry::default())?;
1602 assert_eq!(tx.input.len(), 1);
1603 assert_eq!(tx.output.len(), 2);
1604
1605 let change_output = tx
1606 .output
1607 .iter()
1608 .find(|o| o.to_owned().script_pubkey != withdrawal_address.script_pubkey())
1609 .expect("withdrawal must have change output");
1610 assert!(fed_utxos_sats.insert(change_output.value.to_sat()));
1611
1612 let total_output_sats = tx.output.iter().map(|o| o.value.to_sat()).sum::<u64>();
1614 let input_sats = total_output_sats + fees_sat;
1615 assert!(fed_utxos_sats.remove(&input_sats));
1616
1617 Ok(())
1618 }
1619
1620 for _ in 0..2 {
1623 withdraw(&client, &bitcoind, &mut fed_utxos_sats).await?;
1624 }
1625
1626 let total_fed_sats = fed_utxos_sats.iter().sum::<u64>();
1627 fed.finalize_mempool_tx().await?;
1628
1629 let last_tx_session = client.get_session_count().await?;
1633
1634 info!("Recovering using utxos method");
1635 let output = cmd!(
1636 crate::util::Recoverytool,
1637 "--cfg",
1638 "{data_dir}/fedimintd-default-0",
1639 "utxos",
1640 "--db",
1641 "{data_dir}/fedimintd-default-0/database"
1642 )
1643 .env(FM_PASSWORD_ENV, "pass")
1644 .out_json()
1645 .await?;
1646 let outputs = output.as_array().context("expected an array")?;
1647 assert_eq!(outputs.len(), fed_utxos_sats.len());
1648
1649 assert_eq!(
1650 outputs
1651 .iter()
1652 .map(|o| o["amount_sat"].as_u64().unwrap())
1653 .collect::<HashSet<_>>(),
1654 fed_utxos_sats
1655 );
1656 let utxos_descriptors = outputs
1657 .iter()
1658 .map(|o| o["descriptor"].as_str().unwrap())
1659 .collect::<HashSet<_>>();
1660
1661 debug!(target: LOG_DEVIMINT, ?utxos_descriptors, "recoverytool descriptors using UTXOs method");
1662
1663 let descriptors_json = serde_json::value::to_raw_value(&serde_json::Value::Array(vec![
1664 serde_json::Value::Array(
1665 utxos_descriptors
1666 .iter()
1667 .map(|d| {
1668 json!({
1669 "desc": d,
1670 "timestamp": 0,
1671 })
1672 })
1673 .collect(),
1674 ),
1675 ]))?;
1676 info!("Getting wallet balances before import");
1677 let bitcoin_client = bitcoind.wallet_client().await?;
1678 let balances_before = bitcoin_client.get_balances().await?;
1679 info!("Importing descriptors into bitcoin wallet");
1680 let request = bitcoin_client
1681 .get_jsonrpc_client()
1682 .build_request("importdescriptors", Some(&descriptors_json));
1683 let response = block_in_place(|| bitcoin_client.get_jsonrpc_client().send_request(request))?;
1684 response.check_error()?;
1685 info!("Getting wallet balances after import");
1686 let balances_after = bitcoin_client.get_balances().await?;
1687 let diff = balances_after.mine.immature + balances_after.mine.trusted
1688 - balances_before.mine.immature
1689 - balances_before.mine.trusted;
1690
1691 client.wait_session_outcome(last_tx_session).await?;
1696
1697 assert_eq!(diff.to_sat(), total_fed_sats);
1699 info!("Recovering using epochs method");
1700
1701 let outputs = cmd!(
1702 crate::util::Recoverytool,
1703 "--cfg",
1704 "{data_dir}/fedimintd-default-0",
1705 "epochs",
1706 "--db",
1707 "{data_dir}/fedimintd-default-0/database"
1708 )
1709 .env(FM_PASSWORD_ENV, "pass")
1710 .out_json()
1711 .await?
1712 .as_array()
1713 .context("expected an array")?
1714 .clone();
1715
1716 let epochs_descriptors = outputs
1717 .iter()
1718 .map(|o| o["descriptor"].as_str().unwrap())
1719 .collect::<HashSet<_>>();
1720
1721 debug!(target: LOG_DEVIMINT, ?epochs_descriptors, "recoverytool descriptors using epochs method");
1723
1724 for utxo_descriptor in utxos_descriptors {
1727 assert!(epochs_descriptors.contains(utxo_descriptor));
1728 }
1729 Ok(())
1730}
1731
1732pub async fn guardian_backup_test(dev_fed: DevFed, process_mgr: &ProcessManager) -> Result<()> {
1733 const PEER_TO_TEST: u16 = 0;
1734
1735 log_binary_versions().await?;
1736
1737 let DevFed { mut fed, .. } = dev_fed;
1738
1739 fed.await_all_peers()
1740 .await
1741 .expect("Awaiting federation coming online failed");
1742
1743 let client = fed.new_joined_client("guardian-client").await?;
1744 let old_block_count = cmd!(
1745 client,
1746 "dev",
1747 "api",
1748 "--peer-id",
1749 PEER_TO_TEST.to_string(),
1750 "--module",
1751 "wallet",
1752 "block_count",
1753 )
1754 .out_json()
1755 .await?["value"]
1756 .as_u64()
1757 .expect("No block height returned");
1758
1759 let backup_res = cmd!(
1760 client,
1761 "--our-id",
1762 PEER_TO_TEST.to_string(),
1763 "--password",
1764 "pass",
1765 "admin",
1766 "guardian-config-backup"
1767 )
1768 .out_json()
1769 .await?;
1770 let backup_hex = backup_res["tar_archive_bytes"]
1771 .as_str()
1772 .expect("expected hex string");
1773 let backup_tar = hex::decode(backup_hex).expect("invalid hex");
1774
1775 let data_dir = fed
1776 .vars
1777 .get(&PEER_TO_TEST.into())
1778 .expect("peer not found")
1779 .FM_DATA_DIR
1780 .clone();
1781
1782 fed.terminate_server(PEER_TO_TEST.into())
1783 .await
1784 .expect("could not terminate fedimintd");
1785
1786 std::fs::remove_dir_all(&data_dir).expect("error deleting old datadir");
1787 std::fs::create_dir(&data_dir).expect("error creating new datadir");
1788
1789 let write_file = |name: &str, data: &[u8]| {
1790 let mut file = std::fs::File::options()
1791 .write(true)
1792 .create(true)
1793 .truncate(true)
1794 .open(data_dir.join(name))
1795 .expect("could not open file");
1796 file.write_all(data).expect("could not write file");
1797 file.flush().expect("could not flush file");
1798 };
1799
1800 write_file("backup.tar", &backup_tar);
1801 write_file(
1802 fedimint_server::config::io::PLAINTEXT_PASSWORD,
1803 "pass".as_bytes(),
1804 );
1805
1806 assert_eq!(
1807 std::process::Command::new("tar")
1808 .arg("-xf")
1809 .arg("backup.tar")
1810 .current_dir(data_dir)
1811 .spawn()
1812 .expect("error spawning tar")
1813 .wait()
1814 .expect("error extracting archive")
1815 .code(),
1816 Some(0),
1817 "tar failed"
1818 );
1819
1820 fed.start_server(process_mgr, PEER_TO_TEST.into())
1821 .await
1822 .expect("could not restart fedimintd");
1823
1824 poll("Peer catches up again", || async {
1825 let block_counts = all_peer_block_count(&client, fed.member_ids())
1826 .await
1827 .map_err(ControlFlow::Continue)?;
1828 let block_count = block_counts[&PeerId::from(PEER_TO_TEST)];
1829
1830 info!("Caught up to block {block_count} of at least {old_block_count} (counts={block_counts:?})");
1831
1832 if block_count < old_block_count {
1833 return Err(ControlFlow::Continue(anyhow!("Block count still behind")));
1834 }
1835
1836 Ok(())
1837 })
1838 .await
1839 .expect("Peer didn't rejoin federation");
1840
1841 Ok(())
1842}
1843
1844async fn peer_block_count(client: &Client, peer: PeerId) -> Result<u64> {
1845 cmd!(
1846 client,
1847 "dev",
1848 "api",
1849 "--peer-id",
1850 peer.to_string(),
1851 "--module",
1852 LEGACY_HARDCODED_INSTANCE_ID_WALLET.to_string(),
1853 "block_count",
1854 )
1855 .out_json()
1856 .await?["value"]
1857 .as_u64()
1858 .context("No block height returned")
1859}
1860
1861async fn all_peer_block_count(
1862 client: &Client,
1863 peers: impl Iterator<Item = PeerId>,
1864) -> Result<BTreeMap<PeerId, u64>> {
1865 let mut peer_heights = BTreeMap::new();
1866 for peer in peers {
1867 peer_heights.insert(peer, peer_block_count(client, peer).await?);
1868 }
1869 Ok(peer_heights)
1870}
1871
1872pub async fn cannot_replay_tx_test(dev_fed: DevFed) -> Result<()> {
1873 log_binary_versions().await?;
1874
1875 let DevFed { fed, .. } = dev_fed;
1876
1877 let client = fed.new_joined_client("cannot-replay-client").await?;
1878
1879 const CLIENT_START_AMOUNT: u64 = 10_000_000_000;
1880 const CLIENT_SPEND_AMOUNT: u64 = 5_000_000_000;
1881
1882 let initial_client_balance = client.balance().await?;
1883 assert_eq!(initial_client_balance, 0);
1884
1885 fed.pegin_client(CLIENT_START_AMOUNT / 1000, &client)
1886 .await?;
1887
1888 let double_spend_client = client.new_forked("double-spender").await?;
1890
1891 let notes = cmd!(client, "spend", CLIENT_SPEND_AMOUNT)
1893 .out_json()
1894 .await?
1895 .get("notes")
1896 .expect("Output didn't contain e-cash notes")
1897 .as_str()
1898 .unwrap()
1899 .to_owned();
1900
1901 let client_post_spend_balance = client.balance().await?;
1902 crate::util::almost_equal(
1903 client_post_spend_balance,
1904 CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT,
1905 10_000,
1906 )
1907 .unwrap();
1908
1909 cmd!(client, "reissue", notes).out_json().await?;
1910 let client_post_reissue_balance = client.balance().await?;
1911 crate::util::almost_equal(client_post_reissue_balance, CLIENT_START_AMOUNT, 20_000).unwrap();
1912
1913 let double_spend_notes = cmd!(double_spend_client, "spend", CLIENT_SPEND_AMOUNT)
1915 .out_json()
1916 .await?
1917 .get("notes")
1918 .expect("Output didn't contain e-cash notes")
1919 .as_str()
1920 .unwrap()
1921 .to_owned();
1922
1923 let double_spend_client_post_spend_balance = double_spend_client.balance().await?;
1924 crate::util::almost_equal(
1925 double_spend_client_post_spend_balance,
1926 CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT,
1927 10_000,
1928 )
1929 .unwrap();
1930
1931 cmd!(double_spend_client, "reissue", double_spend_notes)
1932 .assert_error_contains("The transaction had an invalid input")
1933 .await?;
1934
1935 let double_spend_client_post_spend_balance = double_spend_client.balance().await?;
1936 crate::util::almost_equal(
1937 double_spend_client_post_spend_balance,
1938 CLIENT_START_AMOUNT - CLIENT_SPEND_AMOUNT,
1939 10_000,
1940 )
1941 .unwrap();
1942
1943 Ok(())
1944}
1945
1946pub async fn test_offline_client_initialization(
1950 dev_fed: DevFed,
1951 _process_mgr: &ProcessManager,
1952) -> Result<()> {
1953 log_binary_versions().await?;
1954
1955 let DevFed { mut fed, .. } = dev_fed;
1956
1957 fed.await_all_peers().await?;
1959
1960 let client = fed.new_joined_client("offline-test-client").await?;
1962
1963 const INFO_COMMAND_TIMEOUT: Duration = Duration::from_secs(5);
1965 let online_info =
1966 fedimint_core::runtime::timeout(INFO_COMMAND_TIMEOUT, cmd!(client, "info").out_json())
1967 .await
1968 .context("Client info command timed out while federation was online")?
1969 .context("Client info command failed while federation was online")?;
1970 info!(target: LOG_DEVIMINT, "Client info while federation online: {:?}", online_info);
1971
1972 info!(target: LOG_DEVIMINT, "Shutting down all federation servers...");
1974 fed.terminate_all_servers().await?;
1975
1976 fedimint_core::task::sleep_in_test("wait for federation shutdown", Duration::from_secs(2))
1978 .await;
1979
1980 info!(target: LOG_DEVIMINT, "Testing client info command with all servers offline...");
1984 let offline_info =
1985 fedimint_core::runtime::timeout(INFO_COMMAND_TIMEOUT, cmd!(client, "info").out_json())
1986 .await
1987 .context("Client info command timed out while federation was offline")?
1988 .context("Client info command failed while federation was offline")?;
1989
1990 info!(target: LOG_DEVIMINT, "Client info while federation offline: {:?}", offline_info);
1991
1992 Ok(())
1993}
1994
1995pub async fn test_client_config_change_detection(
2002 dev_fed: DevFed,
2003 process_mgr: &ProcessManager,
2004) -> Result<()> {
2005 log_binary_versions().await?;
2006
2007 let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
2008 let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
2009
2010 if fedimint_cli_version < *VERSION_0_9_0_ALPHA {
2011 info!(target: LOG_DEVIMINT, "Skipping the test - fedimint-cli too old");
2012 return Ok(());
2013 }
2014
2015 if fedimintd_version < *VERSION_0_9_0_ALPHA {
2016 info!(target: LOG_DEVIMINT, "Skipping the test - fedimintd too old");
2017 return Ok(());
2018 }
2019
2020 let DevFed { mut fed, .. } = dev_fed;
2021 let peer_ids: Vec<_> = fed.member_ids().collect();
2022
2023 fed.await_all_peers().await?;
2024
2025 let client = fed.new_joined_client("config-change-test-client").await?;
2026
2027 info!(target: LOG_DEVIMINT, "Getting initial client configuration...");
2028 let initial_config = cmd!(client, "config")
2029 .out_json()
2030 .await
2031 .context("Failed to get initial client config")?;
2032
2033 info!(target: LOG_DEVIMINT, "Initial config modules: {:?}", initial_config["modules"].as_object().unwrap().keys().collect::<Vec<_>>());
2034
2035 let data_dir = env::var(FM_DATA_DIR_ENV)?;
2036 let config_dir = PathBuf::from(&data_dir);
2037
2038 info!(target: LOG_DEVIMINT, "Shutting down all federation servers...");
2045 fed.terminate_all_servers().await?;
2046
2047 fedimint_core::task::sleep_in_test("wait for federation shutdown", Duration::from_secs(2))
2049 .await;
2050
2051 info!(target: LOG_DEVIMINT, "Modifying server configurations to add new meta module...");
2052 modify_server_configs(&config_dir, &peer_ids).await?;
2053
2054 info!(target: LOG_DEVIMINT, "Restarting all servers with modified configurations...");
2056 for peer_id in peer_ids {
2057 fed.start_server(process_mgr, peer_id.to_usize()).await?;
2058 }
2059
2060 info!(target: LOG_DEVIMINT, "Wait for peers to get back up");
2062 fed.await_all_peers().await?;
2063
2064 info!(target: LOG_DEVIMINT, "Waiting for client to fetch updated configuration...");
2066 cmd!(client, "dev", "wait", "3")
2067 .run()
2068 .await
2069 .context("Failed to wait for client config update")?;
2070
2071 info!(target: LOG_DEVIMINT, "Testing client detection of configuration changes...");
2073 let updated_config = cmd!(client, "config")
2074 .out_json()
2075 .await
2076 .context("Failed to get updated client config")?;
2077
2078 info!(target: LOG_DEVIMINT, "Updated config modules: {:?}", updated_config["modules"].as_object().unwrap().keys().collect::<Vec<_>>());
2079
2080 let initial_modules = initial_config["modules"].as_object().unwrap();
2082 let updated_modules = updated_config["modules"].as_object().unwrap();
2083
2084 anyhow::ensure!(
2085 updated_modules.len() > initial_modules.len(),
2086 "Expected more modules in updated config. Initial: {}, Updated: {}",
2087 initial_modules.len(),
2088 updated_modules.len()
2089 );
2090
2091 let new_meta_module = updated_modules.iter().find(|(module_id, module_config)| {
2093 module_config["kind"].as_str() == Some("meta") && !initial_modules.contains_key(*module_id)
2094 });
2095
2096 let new_meta_module_id = new_meta_module
2097 .map(|(id, _)| id)
2098 .with_context(|| "Expected to find new meta module in updated configuration")?;
2099
2100 info!(target: LOG_DEVIMINT, "Found new meta module with id: {}", new_meta_module_id);
2101
2102 info!(target: LOG_DEVIMINT, "Verifying client operations work with new configuration...");
2104 let final_info = cmd!(client, "info")
2105 .out_json()
2106 .await
2107 .context("Client info command failed with updated configuration")?;
2108
2109 info!(target: LOG_DEVIMINT, "Client successfully adapted to configuration changes: {:?}", final_info["federation_id"]);
2110
2111 Ok(())
2112}
2113
2114async fn modify_server_configs(config_dir: &Path, peer_ids: &[PeerId]) -> Result<()> {
2116 for &peer_id in peer_ids {
2117 modify_single_peer_config(config_dir, peer_id).await?;
2118 }
2119 Ok(())
2120}
2121
2122async fn modify_single_peer_config(config_dir: &Path, peer_id: PeerId) -> Result<()> {
2125 use fedimint_aead::{encrypted_write, get_encryption_key};
2126 use fedimint_core::core::ModuleInstanceId;
2127 use fedimint_server::config::io::read_server_config;
2128 use serde_json::Value;
2129
2130 info!(target: LOG_DEVIMINT, %peer_id, "Modifying config for peer");
2131 let peer_dir = config_dir.join(format!("fedimintd-default-{}", peer_id.to_usize()));
2132
2133 let consensus_config_path = peer_dir.join("consensus.json");
2135 let consensus_config_content = fs::read_to_string(&consensus_config_path)
2136 .await
2137 .with_context(|| format!("Failed to read consensus config for peer {peer_id}"))?;
2138
2139 let mut consensus_config: Value = serde_json::from_str(&consensus_config_content)
2140 .with_context(|| format!("Failed to parse consensus config for peer {peer_id}"))?;
2141
2142 let password = "pass"; let server_config = read_server_config(password, &peer_dir)
2145 .with_context(|| format!("Failed to read server config for peer {peer_id}"))?;
2146
2147 let consensus_config_modules = consensus_config["modules"]
2149 .as_object()
2150 .with_context(|| format!("No modules found in consensus config for peer {peer_id}"))?;
2151
2152 let existing_meta_consensus = consensus_config_modules
2154 .values()
2155 .find(|module_config| module_config["kind"].as_str() == Some("meta"));
2156
2157 let existing_meta_consensus = existing_meta_consensus
2158 .with_context(|| {
2159 format!("No existing meta module found in consensus config for peer {peer_id}")
2160 })?
2161 .clone();
2162
2163 let existing_meta_instance_id = server_config
2165 .consensus
2166 .modules
2167 .iter()
2168 .find(|(_, config)| config.kind.as_str() == "meta")
2169 .map(|(id, _)| *id)
2170 .with_context(|| {
2171 format!("No existing meta module found in private config for peer {peer_id}")
2172 })?;
2173
2174 let existing_meta_private = server_config
2175 .private
2176 .modules
2177 .get(&existing_meta_instance_id)
2178 .with_context(|| format!("Failed to get existing meta private config for peer {peer_id}"))?
2179 .clone();
2180
2181 let last_existing_module_id = consensus_config_modules
2183 .keys()
2184 .filter_map(|id| id.parse::<u32>().ok())
2185 .max()
2186 .unwrap_or(0);
2187
2188 let new_module_id = (last_existing_module_id + 1).to_string();
2189 let new_module_instance_id = ModuleInstanceId::from((last_existing_module_id + 1) as u16);
2190
2191 info!(
2192 "Adding new meta module with id {} for peer {} (copying existing meta module config)",
2193 new_module_id, peer_id
2194 );
2195
2196 if let Some(modules) = consensus_config["modules"].as_object_mut() {
2198 modules.insert(new_module_id.clone(), existing_meta_consensus);
2199 }
2200
2201 let mut updated_private_config = server_config.private.clone();
2203 updated_private_config
2204 .modules
2205 .insert(new_module_instance_id, existing_meta_private);
2206
2207 let updated_consensus_content = serde_json::to_string_pretty(&consensus_config)
2209 .with_context(|| format!("Failed to serialize consensus config for peer {peer_id}"))?;
2210
2211 write_overwrite_async(&consensus_config_path, updated_consensus_content)
2212 .await
2213 .with_context(|| format!("Failed to write consensus config for peer {peer_id}"))?;
2214
2215 let salt = std::fs::read_to_string(peer_dir.join("private.salt"))
2217 .with_context(|| format!("Failed to read salt file for peer {peer_id}"))?;
2218 let key = get_encryption_key(password, &salt)
2219 .with_context(|| format!("Failed to get encryption key for peer {peer_id}"))?;
2220
2221 let private_config_bytes = serde_json::to_string(&updated_private_config)
2222 .with_context(|| format!("Failed to serialize private config for peer {peer_id}"))?
2223 .into_bytes();
2224
2225 let encrypted_private_path = peer_dir.join("private.encrypt");
2227 if encrypted_private_path.exists() {
2228 std::fs::remove_file(&encrypted_private_path)
2229 .with_context(|| format!("Failed to remove old private config for peer {peer_id}"))?;
2230 }
2231
2232 encrypted_write(private_config_bytes, &key, encrypted_private_path)
2233 .with_context(|| format!("Failed to write encrypted private config for peer {peer_id}"))?;
2234
2235 info!("Successfully modified configs for peer {}", peer_id);
2236 Ok(())
2237}
2238
2239pub async fn test_guardian_password_change(
2240 dev_fed: DevFed,
2241 process_mgr: &ProcessManager,
2242) -> Result<()> {
2243 log_binary_versions().await?;
2244
2245 let fedimint_cli_version = crate::util::FedimintCli::version_or_default().await;
2246 let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
2247
2248 if fedimint_cli_version < *VERSION_0_9_0_ALPHA {
2249 info!(target: LOG_DEVIMINT, "Skipping the test - fedimint-cli too old");
2250 return Ok(());
2251 }
2252
2253 if fedimintd_version < *VERSION_0_9_0_ALPHA {
2254 info!(target: LOG_DEVIMINT, "Skipping the test - fedimintd too old");
2255 return Ok(());
2256 }
2257
2258 let DevFed { mut fed, .. } = dev_fed;
2259 fed.await_all_peers().await?;
2260
2261 let client = fed.new_joined_client("config-change-test-client").await?;
2262
2263 let peer_id = 0;
2264 let data_dir: PathBuf = fed
2265 .vars
2266 .get(&peer_id)
2267 .expect("peer not found")
2268 .FM_DATA_DIR
2269 .clone();
2270 let file_exists = |file: &str| {
2271 let path = data_dir.join(file);
2272 path.exists()
2273 };
2274 let pre_password_file_exists = file_exists("password.secret");
2275
2276 info!(target: LOG_DEVIMINT, "Changing password");
2277 cmd!(
2278 client,
2279 "--our-id",
2280 &peer_id.to_string(),
2281 "--password",
2282 "pass",
2283 "admin",
2284 "change-password",
2285 "foobar"
2286 )
2287 .run()
2288 .await
2289 .context("Failed to change guardian password")?;
2290
2291 info!(target: LOG_DEVIMINT, "Waiting for fedimintd to be shut down");
2292 timeout(
2293 Duration::from_secs(30),
2294 fed.await_server_terminated(peer_id),
2295 )
2296 .await
2297 .context("Fedimintd didn't shut down in time after password change")??;
2298
2299 info!(target: LOG_DEVIMINT, "Restarting fedimintd");
2300 fed.start_server(process_mgr, peer_id).await?;
2301
2302 info!(target: LOG_DEVIMINT, "Wait for fedimintd to come online again");
2303 fed.await_peer(peer_id).await?;
2304
2305 info!(target: LOG_DEVIMINT, "Testing password change worked");
2306 cmd!(
2307 client,
2308 "--our-id",
2309 &peer_id.to_string(),
2310 "--password",
2311 "foobar",
2312 "admin",
2313 "backup-statistics"
2314 )
2315 .run()
2316 .await
2317 .context("Failed to run guardian command with new password")?;
2318
2319 assert!(!file_exists("private.bak"));
2320 assert!(!file_exists("password.bak"));
2321 assert!(!file_exists("private.new"));
2322 assert!(!file_exists("password.new"));
2323 assert_eq!(file_exists("password.secret"), pre_password_file_exists);
2324
2325 Ok(())
2326}
2327
2328#[derive(Subcommand)]
2329pub enum LatencyTest {
2330 Reissue,
2331 LnSend,
2332 LnReceive,
2333 FmPay,
2334 Restore,
2335}
2336
2337#[derive(Subcommand)]
2338pub enum UpgradeTest {
2339 Fedimintd {
2340 #[arg(long, trailing_var_arg = true, num_args=1..)]
2341 paths: Vec<PathBuf>,
2342 },
2343 FedimintCli {
2344 #[arg(long, trailing_var_arg = true, num_args=1..)]
2345 paths: Vec<PathBuf>,
2346 },
2347 Gatewayd {
2348 #[arg(long, trailing_var_arg = true, num_args=1..)]
2349 gatewayd_paths: Vec<PathBuf>,
2350 #[arg(long, trailing_var_arg = true, num_args=1..)]
2351 gateway_cli_paths: Vec<PathBuf>,
2352 },
2353}
2354
2355#[derive(Subcommand)]
2356pub enum TestCmd {
2357 LatencyTests {
2360 #[clap(subcommand)]
2361 r#type: LatencyTest,
2362
2363 #[arg(long, default_value = "10")]
2364 iterations: usize,
2365 },
2366 ReconnectTest,
2369 CliTests,
2371 LoadTestToolTest,
2374 LightningReconnectTest,
2377 GatewayRebootTest,
2380 RecoverytoolTests,
2382 WasmTestSetup {
2384 #[arg(long, trailing_var_arg = true, allow_hyphen_values = true, num_args=1..)]
2385 exec: Option<Vec<ffi::OsString>>,
2386 },
2387 GuardianBackup,
2389 CannotReplayTransaction,
2391 TestOfflineClientInitialization,
2394 TestClientConfigChangeDetection,
2397 TestGuardianPasswordChange,
2400 UpgradeTests {
2402 #[clap(subcommand)]
2403 binary: UpgradeTest,
2404 #[arg(long)]
2405 lnv2: String,
2406 },
2407}
2408
2409pub async fn handle_command(cmd: TestCmd, common_args: CommonArgs) -> Result<()> {
2410 match cmd {
2411 TestCmd::WasmTestSetup { exec } => {
2412 let (process_mgr, task_group) = setup(common_args).await?;
2413 let main = {
2414 let task_group = task_group.clone();
2415 async move {
2416 let dev_fed = dev_fed(&process_mgr).await?;
2417 let gw_lnd = dev_fed.gw_lnd.clone();
2418 let fed = dev_fed.fed.clone();
2419 gw_lnd
2420 .set_federation_routing_fee(dev_fed.fed.calculate_federation_id(), 0, 0)
2421 .await?;
2422 task_group.spawn_cancellable("faucet", async move {
2423 if let Err(err) = crate::faucet::run(
2424 &dev_fed,
2425 format!("0.0.0.0:{}", process_mgr.globals.FM_PORT_FAUCET),
2426 process_mgr.globals.FM_PORT_GW_LND,
2427 )
2428 .await
2429 {
2430 error!("Error spawning faucet: {err}");
2431 }
2432 });
2433 try_join!(fed.pegin_gateways(30_000, vec![&gw_lnd]), async {
2434 poll("waiting for faucet startup", || async {
2435 TcpStream::connect(format!(
2436 "127.0.0.1:{}",
2437 process_mgr.globals.FM_PORT_FAUCET
2438 ))
2439 .await
2440 .context("connect to faucet")
2441 .map_err(ControlFlow::Continue)
2442 })
2443 .await?;
2444 Ok(())
2445 },)?;
2446 if let Some(exec) = exec {
2447 exec_user_command(exec).await?;
2448 task_group.shutdown();
2449 }
2450 Ok::<_, anyhow::Error>(())
2451 }
2452 };
2453 cleanup_on_exit(main, task_group).await?;
2454 }
2455 TestCmd::LatencyTests { r#type, iterations } => {
2456 let (process_mgr, _) = setup(common_args).await?;
2457 let dev_fed = dev_fed(&process_mgr).await?;
2458 latency_tests(dev_fed, r#type, None, iterations, true).await?;
2459 }
2460 TestCmd::ReconnectTest => {
2461 let (process_mgr, _) = setup(common_args).await?;
2462 let dev_fed = dev_fed(&process_mgr).await?;
2463 reconnect_test(dev_fed, &process_mgr).await?;
2464 }
2465 TestCmd::CliTests => {
2466 let (process_mgr, _) = setup(common_args).await?;
2467 let dev_fed = dev_fed(&process_mgr).await?;
2468 cli_tests(dev_fed).await?;
2469 }
2470 TestCmd::LoadTestToolTest => {
2471 unsafe { std::env::set_var(FM_DISABLE_BASE_FEES_ENV, "1") };
2473
2474 let (process_mgr, _) = setup(common_args).await?;
2475 let dev_fed = dev_fed(&process_mgr).await?;
2476 cli_load_test_tool_test(dev_fed).await?;
2477 }
2478 TestCmd::LightningReconnectTest => {
2479 let (process_mgr, _) = setup(common_args).await?;
2480 let dev_fed = dev_fed(&process_mgr).await?;
2481 lightning_gw_reconnect_test(dev_fed, &process_mgr).await?;
2482 }
2483 TestCmd::GatewayRebootTest => {
2484 let (process_mgr, _) = setup(common_args).await?;
2485 let dev_fed = dev_fed(&process_mgr).await?;
2486 gw_reboot_test(dev_fed, &process_mgr).await?;
2487 }
2488 TestCmd::RecoverytoolTests => {
2489 let (process_mgr, _) = setup(common_args).await?;
2490 let dev_fed = dev_fed(&process_mgr).await?;
2491 recoverytool_test(dev_fed).await?;
2492 }
2493 TestCmd::GuardianBackup => {
2494 let (process_mgr, _) = setup(common_args).await?;
2495 let dev_fed = dev_fed(&process_mgr).await?;
2496 guardian_backup_test(dev_fed, &process_mgr).await?;
2497 }
2498 TestCmd::CannotReplayTransaction => {
2499 let (process_mgr, _) = setup(common_args).await?;
2500 let dev_fed = dev_fed(&process_mgr).await?;
2501 cannot_replay_tx_test(dev_fed).await?;
2502 }
2503 TestCmd::TestOfflineClientInitialization => {
2504 let (process_mgr, _) = setup(common_args).await?;
2505 let dev_fed = dev_fed(&process_mgr).await?;
2506 test_offline_client_initialization(dev_fed, &process_mgr).await?;
2507 }
2508 TestCmd::TestClientConfigChangeDetection => {
2509 let (process_mgr, _) = setup(common_args).await?;
2510 let dev_fed = dev_fed(&process_mgr).await?;
2511 test_client_config_change_detection(dev_fed, &process_mgr).await?;
2512 }
2513 TestCmd::TestGuardianPasswordChange => {
2514 let (process_mgr, _) = setup(common_args).await?;
2515 let dev_fed = dev_fed(&process_mgr).await?;
2516 test_guardian_password_change(dev_fed, &process_mgr).await?;
2517 }
2518 TestCmd::UpgradeTests { binary, lnv2 } => {
2519 unsafe { std::env::set_var(FM_ENABLE_MODULE_LNV2_ENV, lnv2) };
2521 let (process_mgr, _) = setup(common_args).await?;
2522 Box::pin(upgrade_tests(&process_mgr, binary)).await?;
2523 }
2524 }
2525 Ok(())
2526}