1#![deny(clippy::pedantic)]
2
3use std::collections::BTreeMap;
4use std::env;
5use std::fs::remove_dir_all;
6use std::path::PathBuf;
7use std::str::FromStr;
8
9use anyhow::ensure;
10use clap::{Parser, Subcommand};
11use devimint::envs::FM_DATA_DIR_ENV;
12use devimint::federation::Federation;
13use devimint::util::ProcessManager;
14use devimint::version_constants::{VERSION_0_5_0_ALPHA, VERSION_0_6_0_ALPHA};
15use devimint::{Gatewayd, LightningNode, cmd, util};
16use fedimint_core::config::FederationId;
17use fedimint_core::util::backoff_util::aggressive_backoff_long;
18use fedimint_core::util::retry;
19use fedimint_core::{Amount, BitcoinAmountOrAll};
20use fedimint_gateway_common::{FederationInfo, GatewayBalances, GatewayFedConfig};
21use fedimint_testing::ln::LightningNodeType;
22use itertools::Itertools;
23use tracing::{debug, info, warn};
24
25#[derive(Parser)]
26struct GatewayTestOpts {
27 #[clap(subcommand)]
28 test: GatewayTest,
29}
30
31#[derive(Debug, Clone, Subcommand)]
32enum GatewayTest {
33 ConfigTest {
34 #[arg(long = "gw-type")]
35 gateway_type: LightningNodeType,
36 },
37 GatewaydMnemonic {
38 #[arg(long)]
39 old_gatewayd_path: PathBuf,
40 #[arg(long)]
41 new_gatewayd_path: PathBuf,
42 #[arg(long)]
43 old_gateway_cli_path: PathBuf,
44 #[arg(long)]
45 new_gateway_cli_path: PathBuf,
46 },
47 BackupRestoreTest,
48 LiquidityTest,
49}
50
51#[tokio::main]
52async fn main() -> anyhow::Result<()> {
53 let opts = GatewayTestOpts::parse();
54 match opts.test {
55 GatewayTest::ConfigTest { gateway_type } => Box::pin(config_test(gateway_type)).await,
56 GatewayTest::GatewaydMnemonic {
57 old_gatewayd_path,
58 new_gatewayd_path,
59 old_gateway_cli_path,
60 new_gateway_cli_path,
61 } => {
62 mnemonic_upgrade_test(
63 old_gatewayd_path,
64 new_gatewayd_path,
65 old_gateway_cli_path,
66 new_gateway_cli_path,
67 )
68 .await
69 }
70 GatewayTest::BackupRestoreTest => Box::pin(backup_restore_test()).await,
71 GatewayTest::LiquidityTest => Box::pin(liquidity_test()).await,
72 }
73}
74
75async fn backup_restore_test() -> anyhow::Result<()> {
76 Box::pin(
77 devimint::run_devfed_test().call(|dev_fed, process_mgr| async move {
78 let gatewayd_version = util::Gatewayd::version_or_default().await;
79 if gatewayd_version < *VERSION_0_5_0_ALPHA {
80 warn!("Gateway backup-restore is not supported below v0.5.0");
81 return Ok(());
82 }
83
84 let gw = if devimint::util::supports_lnv2() {
85 dev_fed.gw_ldk_connected().await?
86 } else {
87 dev_fed.gw_lnd_registered().await?
88 };
89
90 let fed = dev_fed.fed().await?;
91 fed.pegin_gateways(10_000_000, vec![gw]).await?;
92
93 let mnemonic = gw.get_mnemonic().await?.mnemonic;
94
95 info!("Wiping gateway and recovering without a backup...");
97 let ln = gw.ln.clone();
98 let new_gw = stop_and_recover_gateway(
99 process_mgr.clone(),
100 mnemonic.clone(),
101 gw.to_owned(),
102 ln.clone(),
103 fed,
104 )
105 .await?;
106
107 info!("Wiping gateway and recovering with a backup...");
109 info!("Creating backup...");
110 new_gw.backup_to_fed(fed).await?;
111 stop_and_recover_gateway(process_mgr, mnemonic, new_gw, ln, fed).await?;
112
113 info!("backup_restore_test successful");
114 Ok(())
115 }),
116 )
117 .await
118}
119
120async fn stop_and_recover_gateway(
121 process_mgr: ProcessManager,
122 mnemonic: Vec<String>,
123 old_gw: Gatewayd,
124 new_ln: LightningNode,
125 fed: &Federation,
126) -> anyhow::Result<Gatewayd> {
127 let gateway_balances =
128 serde_json::from_value::<GatewayBalances>(cmd!(old_gw, "get-balances").out_json().await?)?;
129 let before_onchain_balance = gateway_balances.onchain_balance_sats;
130
131 let gw_type = old_gw.ln.ln_type();
133 let gw_name = old_gw.gw_name.clone();
134 old_gw.terminate().await?;
135 info!("Terminated Gateway");
136
137 let data_dir: PathBuf = env::var(FM_DATA_DIR_ENV)
139 .expect("Data dir is not set")
140 .parse()
141 .expect("Could not parse data dir");
142 let gw_db = data_dir.join(gw_name.clone()).join("gatewayd.db");
143 remove_dir_all(gw_db)?;
144 info!("Deleted the Gateway's database");
145
146 if gw_type == LightningNodeType::Ldk {
147 let ldk_data_dir = data_dir.join(gw_name).join("ldk_node");
149 remove_dir_all(ldk_data_dir)?;
150 info!("Deleted LDK's database");
151 }
152
153 let seed = mnemonic.join(" ");
154 unsafe { std::env::set_var("FM_GATEWAY_MNEMONIC", seed) };
156 let new_gw = Gatewayd::new(&process_mgr, new_ln).await?;
157 let new_mnemonic = new_gw.get_mnemonic().await?.mnemonic;
158 assert_eq!(mnemonic, new_mnemonic);
159 info!("Verified mnemonic is the same after creating new Gateway");
160
161 let federations = serde_json::from_value::<Vec<FederationInfo>>(
162 new_gw.get_info().await?["federations"].clone(),
163 )?;
164 assert_eq!(0, federations.len());
165 info!("Verified new Gateway has no federations");
166
167 new_gw.recover_fed(fed).await?;
168
169 let gateway_balances =
170 serde_json::from_value::<GatewayBalances>(cmd!(new_gw, "get-balances").out_json().await?)?;
171 let ecash_balance = gateway_balances
172 .ecash_balances
173 .first()
174 .expect("Should have one joined federation");
175 assert_eq!(
176 10_000_000,
177 ecash_balance.ecash_balance_msats.sats_round_down()
178 );
179 let after_onchain_balance = gateway_balances.onchain_balance_sats;
180 assert_eq!(before_onchain_balance, after_onchain_balance);
181 info!("Verified balances after recovery");
182
183 Ok(new_gw)
184}
185
186async fn mnemonic_upgrade_test(
190 old_gatewayd_path: PathBuf,
191 new_gatewayd_path: PathBuf,
192 old_gateway_cli_path: PathBuf,
193 new_gateway_cli_path: PathBuf,
194) -> anyhow::Result<()> {
195 unsafe { std::env::set_var("FM_GATEWAYD_BASE_EXECUTABLE", old_gatewayd_path) };
197 unsafe { std::env::set_var("FM_GATEWAY_CLI_BASE_EXECUTABLE", old_gateway_cli_path) };
199 unsafe { std::env::set_var("FM_ENABLE_MODULE_LNV2", "0") };
201
202 devimint::run_devfed_test()
203 .call(|dev_fed, process_mgr| async move {
204 let gatewayd_version = util::Gatewayd::version_or_default().await;
205 let gateway_cli_version = util::GatewayCli::version_or_default().await;
206 info!(
207 ?gatewayd_version,
208 ?gateway_cli_version,
209 "Running gatewayd mnemonic test"
210 );
211
212 let mut gw_lnd = dev_fed.gw_lnd_registered().await?.to_owned();
213 let fed = dev_fed.fed().await?;
214 let federation_id = FederationId::from_str(fed.calculate_federation_id().as_str())?;
215
216 gw_lnd
217 .restart_with_bin(&process_mgr, &new_gatewayd_path, &new_gateway_cli_path)
218 .await?;
219
220 let new_gatewayd_version = util::Gatewayd::version_or_default().await;
222 if new_gatewayd_version < *VERSION_0_5_0_ALPHA {
223 warn!("Gateway mnemonic test is not supported below v0.5.0");
224 return Ok(());
225 }
226
227 let mnemonic_response = gw_lnd.get_mnemonic().await?;
229 assert!(
230 mnemonic_response
231 .legacy_federations
232 .contains(&federation_id)
233 );
234
235 info!("Verified a legacy federation exists");
236
237 gw_lnd.leave_federation(federation_id).await?;
239
240 gw_lnd.connect_fed(fed).await?;
242
243 let mnemonic_response = gw_lnd.get_mnemonic().await?;
245 assert!(
246 mnemonic_response
247 .legacy_federations
248 .contains(&federation_id)
249 );
250 assert_eq!(mnemonic_response.legacy_federations.len(), 1);
251
252 info!("Verified leaving and re-joining preservers legacy federation");
253
254 gw_lnd.leave_federation(federation_id).await?;
256
257 let data_dir: PathBuf = env::var(FM_DATA_DIR_ENV)
258 .expect("Data dir is not set")
259 .parse()
260 .expect("Could not parse data dir");
261 let gw_fed_db = data_dir
262 .join(gw_lnd.gw_name.clone())
263 .join(format!("{federation_id}.db"));
264 remove_dir_all(gw_fed_db)?;
265
266 gw_lnd.connect_fed(fed).await?;
267
268 let mnemonic_response = gw_lnd.get_mnemonic().await?;
270 assert!(
271 !mnemonic_response
272 .legacy_federations
273 .contains(&federation_id)
274 );
275 assert_eq!(mnemonic_response.legacy_federations.len(), 0);
276
277 info!("Verified deleting database will migrate the federation to use mnemonic");
278
279 info!("Successfully completed mnemonic upgrade test");
280
281 Ok(())
282 })
283 .await
284}
285
286#[allow(clippy::too_many_lines)]
288async fn config_test(gw_type: LightningNodeType) -> anyhow::Result<()> {
289 Box::pin(
290 devimint::run_devfed_test()
291 .num_feds(2)
292 .call(|dev_fed, process_mgr| async move {
293 let gatewayd_version = util::Gatewayd::version_or_default().await;
294 if gatewayd_version < *VERSION_0_5_0_ALPHA && gw_type == LightningNodeType::Ldk {
295 return Ok(());
296 }
297
298 let gw = match gw_type {
299 LightningNodeType::Lnd => dev_fed.gw_lnd_registered().await?,
300 LightningNodeType::Ldk => dev_fed.gw_ldk_connected().await?,
301 };
302
303 let invite_code = dev_fed.fed().await?.invite_code()?;
305 let output = cmd!(gw, "connect-fed", invite_code.clone())
306 .out_json()
307 .await;
308 assert!(
309 output.is_err(),
310 "Connecting to the same federation succeeded"
311 );
312 info!("Verified that gateway couldn't connect to already connected federation");
313
314 let gatewayd_version = util::Gatewayd::version_or_default().await;
315
316 let fed_id = dev_fed.fed().await?.calculate_federation_id();
318 gw.set_federation_routing_fee(fed_id.clone(), 20, 20000)
319 .await?;
320
321 let lightning_fee = gw.get_lightning_fee(fed_id.clone()).await?;
322 assert_eq!(
323 lightning_fee.base.msats, 20,
324 "Federation base msat is not 20"
325 );
326 assert_eq!(
327 lightning_fee.parts_per_million, 20000,
328 "Federation proportional millionths is not 20000"
329 );
330 info!("Verified per-federation routing fees changed");
331
332 let info_value = cmd!(gw, "info").out_json().await?;
333 let federations = info_value["federations"]
334 .as_array()
335 .expect("federations is an array");
336 assert_eq!(
337 federations.len(),
338 1,
339 "Gateway did not have one connected federation"
340 );
341
342 if gatewayd_version >= *VERSION_0_5_0_ALPHA
345 && gatewayd_version < *VERSION_0_6_0_ALPHA
346 {
347 let config_val = cmd!(gw, "config", "--federation-id", fed_id)
349 .out_json()
350 .await?;
351 serde_json::from_value::<GatewayFedConfig>(config_val)?;
352 } else if gatewayd_version >= *VERSION_0_6_0_ALPHA {
353 let config_val = cmd!(gw, "cfg", "client-config", "--federation-id", fed_id)
355 .out_json()
356 .await?;
357 serde_json::from_value::<GatewayFedConfig>(config_val)?;
358 }
359
360 let bitcoind = dev_fed.bitcoind().await?;
362 let new_fed = Federation::new(
363 &process_mgr,
364 bitcoind.clone(),
365 false,
366 1,
367 "config-test".to_string(),
368 )
369 .await?;
370 let new_fed_id = new_fed.calculate_federation_id();
371 info!("Successfully spawned new federation");
372
373 let new_invite_code = new_fed.invite_code()?;
374 cmd!(gw, "connect-fed", new_invite_code.clone())
375 .out_json()
376 .await?;
377
378 let (default_base, default_ppm) = if gatewayd_version >= *VERSION_0_6_0_ALPHA {
379 (50000, 5000)
380 } else {
381 (0, 10000)
382 };
383
384 let lightning_fee = gw.get_lightning_fee(new_fed_id.clone()).await?;
385 assert_eq!(
386 lightning_fee.base.msats, default_base,
387 "Default Base msat for new federation was not correct"
388 );
389 assert_eq!(
390 lightning_fee.parts_per_million, default_ppm,
391 "Default Base msat for new federation was not correct"
392 );
393
394 info!(?new_fed_id, "Verified new federation");
395
396 let pegin_amount = Amount::from_msats(10_000_000);
398 new_fed
399 .pegin_gateways(pegin_amount.sats_round_down(), vec![gw])
400 .await?;
401
402 let info_value = cmd!(gw, "info").out_json().await?;
404 let federations = info_value["federations"]
405 .as_array()
406 .expect("federations is an array");
407
408 assert_eq!(
409 federations.len(),
410 2,
411 "Gateway did not have two connected federations"
412 );
413
414 let federation_fake_scids =
415 serde_json::from_value::<Option<BTreeMap<u64, FederationId>>>(
416 info_value
417 .get("channels")
418 .or_else(|| info_value.get("federation_fake_scids"))
419 .expect("field exists")
420 .to_owned(),
421 )
422 .expect("cannot parse")
423 .expect("should have scids");
424
425 assert_eq!(
426 federation_fake_scids.keys().copied().collect::<Vec<u64>>(),
427 vec![1, 2]
428 );
429
430 let first_fed_info = federations
431 .iter()
432 .find(|i| {
433 *i["federation_id"]
434 .as_str()
435 .expect("should parse as str")
436 .to_string()
437 == fed_id
438 })
439 .expect("Could not find federation");
440
441 let second_fed_info = federations
442 .iter()
443 .find(|i| {
444 *i["federation_id"]
445 .as_str()
446 .expect("should parse as str")
447 .to_string()
448 == new_fed_id
449 })
450 .expect("Could not find federation");
451
452 let first_fed_balance_msat =
453 serde_json::from_value::<Amount>(first_fed_info["balance_msat"].clone())
454 .expect("fed should have balance");
455
456 let second_fed_balance_msat =
457 serde_json::from_value::<Amount>(second_fed_info["balance_msat"].clone())
458 .expect("fed should have balance");
459
460 assert_eq!(first_fed_balance_msat, Amount::ZERO);
461 assert_eq!(second_fed_balance_msat, pegin_amount);
462
463 leave_federation(gw, fed_id, 1).await?;
464 leave_federation(gw, new_fed_id, 2).await?;
465
466 let output = cmd!(gw, "connect-fed", new_invite_code.clone())
468 .out_json()
469 .await?;
470 let rejoined_federation_balance_msat =
471 serde_json::from_value::<Amount>(output["balance_msat"].clone())
472 .expect("fed has balance");
473
474 assert_eq!(second_fed_balance_msat, rejoined_federation_balance_msat);
475
476 info!("Gateway configuration test successful");
477 Ok(())
478 }),
479 )
480 .await
481}
482
483async fn liquidity_test() -> anyhow::Result<()> {
486 devimint::run_devfed_test().call(|dev_fed, _process_mgr| async move {
487 let federation = dev_fed.fed().await?;
488
489 if !devimint::util::supports_lnv2() {
490 info!("LNv2 is not supported, which is necessary for LDK GW and liquidity test");
491 return Ok(());
492 }
493
494 let gw_lnd = dev_fed.gw_lnd_registered().await?;
495 let gw_ldk = dev_fed.gw_ldk_connected().await?;
496 let gateways = [gw_lnd, gw_ldk].to_vec();
497
498 let gateway_matrix = gateways
499 .iter()
500 .cartesian_product(gateways.iter())
501 .filter(|(a, b)| a.ln.ln_type() != b.ln.ln_type());
502
503 info!("Pegging-in gateways...");
504
505 federation
506 .pegin_gateways(1_000_000, gateways.clone())
507 .await?;
508
509 info!("Testing ecash payments between gateways...");
510 for (gw_send, gw_receive) in gateway_matrix.clone() {
511 info!(
512 "Testing ecash payment: {} -> {}",
513 gw_send.ln.ln_type(),
514 gw_receive.ln.ln_type()
515 );
516
517 let fed_id = federation.calculate_federation_id();
518 let prev_send_ecash_balance = gw_send.ecash_balance(fed_id.clone()).await?;
519 let prev_receive_ecash_balance = gw_receive.ecash_balance(fed_id.clone()).await?;
520 let ecash = gw_send.send_ecash(fed_id.clone(), 500_000).await?;
521 gw_receive.receive_ecash(ecash).await?;
522 let after_send_ecash_balance = gw_send.ecash_balance(fed_id.clone()).await?;
523 let after_receive_ecash_balance = gw_receive.ecash_balance(fed_id.clone()).await?;
524 assert_eq!(prev_send_ecash_balance - 500_000, after_send_ecash_balance);
525 assert_eq!(prev_receive_ecash_balance + 500_000, after_receive_ecash_balance);
526 }
527
528 info!("Testing payments between gateways...");
529
530 for (gw_send, gw_receive) in gateway_matrix.clone() {
531 info!(
532 "Testing lightning payment: {} -> {}",
533 gw_send.ln.ln_type(),
534 gw_receive.ln.ln_type()
535 );
536
537 let invoice = gw_receive.create_invoice(1_000_000).await?;
538 gw_send.pay_invoice(invoice).await?;
539 }
540
541 info!("Testing paying through LND Gateway...");
542 let invoice = gw_ldk.create_invoice(1_550_000).await?;
543 let cln = dev_fed.cln().await?;
544 retry("CLN pay LDK", aggressive_backoff_long(), || async {
546 debug!("Trying CLN -> LND -> LDK...");
547 cln.pay_bolt11_invoice(invoice.to_string()).await?;
548 Ok(())
549 }).await?;
550
551 info!("Pegging-out gateways...");
552 federation.pegout_gateways(500_000_000, gateways.clone()).await?;
553
554 info!("Testing closing all channels...");
555 for gw in gateways.clone() {
556 gw.close_all_channels(dev_fed.bitcoind().await?.clone()).await?;
557
558 retry(
559 "Wait for balance update after sweeping all lightning funds",
560 aggressive_backoff_long(),
561 || async {
562 let balances = gw.get_balances().await?;
563 let curr_lightning_balance = balances.lightning_balance_msats;
564 ensure!(curr_lightning_balance == 0, "Close channels did not sweep all lightning funds");
565 let inbound_lightning_balance = balances.inbound_lightning_liquidity_msats;
566 ensure!(inbound_lightning_balance == 0, "Close channels did not sweep all lightning funds");
567 Ok(())
568 }
569 ).await?;
570 }
571
572 info!("Testing sending onchain...");
573 for gw in gateways {
574 gw.send_onchain(dev_fed.bitcoind().await?, BitcoinAmountOrAll::All, 10).await?;
575 retry(
576 "Wait for balance update after sending on chain funds",
577 aggressive_backoff_long(),
578 || async {
579 let curr_balance = gw.get_balances().await?.onchain_balance_sats;
580 ensure!(curr_balance == 0, "Gateway onchain balance did not match previous balance minus withdraw amount");
581 Ok(())
582 }
583 ).await?;
584 }
585
586 Ok(())
587 }).await
588}
589
590async fn leave_federation(gw: &Gatewayd, fed_id: String, expected_scid: u64) -> anyhow::Result<()> {
593 let gatewayd_version = util::Gatewayd::version_or_default().await;
594 let leave_fed = cmd!(gw, "leave-fed", "--federation-id", fed_id.clone())
595 .out_json()
596 .await
597 .expect("Leaving the federation failed");
598
599 let federation_id: FederationId = serde_json::from_value(leave_fed["federation_id"].clone())?;
600 assert_eq!(federation_id.to_string(), fed_id);
601
602 let scid = if gatewayd_version < *VERSION_0_5_0_ALPHA {
605 let channel_id: Option<u64> = serde_json::from_value(leave_fed["channel_id"].clone())?;
606 channel_id.expect("must have channel id")
607 } else if gatewayd_version >= *VERSION_0_5_0_ALPHA && gatewayd_version < *VERSION_0_6_0_ALPHA {
608 serde_json::from_value::<u64>(leave_fed["federation_index"].clone())?
609 } else {
610 serde_json::from_value::<u64>(leave_fed["config"]["federation_index"].clone())?
611 };
612
613 assert_eq!(scid, expected_scid);
614
615 info!("Verified gateway left federation {fed_id}");
616 Ok(())
617}