1#![deny(clippy::pedantic)]
2
3use std::collections::BTreeMap;
4use std::fs::{remove_dir_all, remove_file};
5use std::ops::ControlFlow;
6use std::path::PathBuf;
7use std::str::FromStr;
8use std::time::Duration;
9use std::{env, ffi};
10
11use clap::{Parser, Subcommand};
12use devimint::cli::cleanup_on_exit;
13use devimint::envs::FM_DATA_DIR_ENV;
14use devimint::external::{Bitcoind, Esplora};
15use devimint::federation::Federation;
16use devimint::util::{ProcessManager, almost_equal, poll, poll_with_timeout};
17use devimint::version_constants::{VERSION_0_8_2, VERSION_0_10_0_ALPHA};
18use devimint::{Gatewayd, LightningNode, cli, cmd, util};
19use fedimint_core::config::FederationId;
20use fedimint_core::time::now;
21use fedimint_core::{Amount, BitcoinAmountOrAll, bitcoin, default_esplora_server};
22use fedimint_gateway_common::{
23 FederationInfo, GatewayBalances, GatewayFedConfig, PaymentDetails, PaymentKind, PaymentStatus,
24};
25use fedimint_logging::LOG_TEST;
26use fedimint_testing_core::node_type::LightningNodeType;
27use itertools::Itertools;
28use tracing::info;
29
30#[derive(Parser)]
31struct GatewayTestOpts {
32 #[clap(subcommand)]
33 test: GatewayTest,
34}
35
36#[derive(Debug, Clone, Subcommand)]
37#[allow(clippy::enum_variant_names)]
38enum GatewayTest {
39 ConfigTest {
40 #[arg(long = "gw-type")]
41 gateway_type: LightningNodeType,
42 },
43 BackupRestoreTest,
44 LiquidityTest,
45 EsploraTest,
46}
47
48#[tokio::main]
49async fn main() -> anyhow::Result<()> {
50 let opts = GatewayTestOpts::parse();
51 match opts.test {
52 GatewayTest::ConfigTest { gateway_type } => Box::pin(config_test(gateway_type)).await,
53 GatewayTest::BackupRestoreTest => Box::pin(backup_restore_test()).await,
54 GatewayTest::LiquidityTest => Box::pin(liquidity_test()).await,
55 GatewayTest::EsploraTest => esplora_test().await,
56 }
57}
58
59async fn backup_restore_test() -> anyhow::Result<()> {
60 Box::pin(
61 devimint::run_devfed_test().call(|dev_fed, process_mgr| async move {
62 let gw = if devimint::util::supports_lnv2() {
63 dev_fed.gw_ldk_connected().await?
64 } else {
65 dev_fed.gw_lnd_registered().await?
66 };
67
68 let fed = dev_fed.fed().await?;
69 fed.pegin_gateways(10_000_000, vec![gw]).await?;
70
71 let mnemonic = gw.get_mnemonic().await?.mnemonic;
72
73 info!(target: LOG_TEST, "Wiping gateway and recovering without a backup...");
75 let ln = gw.ln.clone();
76 let new_gw = stop_and_recover_gateway(
77 process_mgr.clone(),
78 mnemonic.clone(),
79 gw.to_owned(),
80 ln.clone(),
81 fed,
82 )
83 .await?;
84
85 info!(target: LOG_TEST, "Wiping gateway and recovering with a backup...");
87 info!(target: LOG_TEST, "Creating backup...");
88 new_gw.backup_to_fed(fed).await?;
89 stop_and_recover_gateway(process_mgr, mnemonic, new_gw, ln, fed).await?;
90
91 info!(target: LOG_TEST, "backup_restore_test successful");
92 Ok(())
93 }),
94 )
95 .await
96}
97
98async fn stop_and_recover_gateway(
99 process_mgr: ProcessManager,
100 mnemonic: Vec<String>,
101 old_gw: Gatewayd,
102 new_ln: LightningNode,
103 fed: &Federation,
104) -> anyhow::Result<Gatewayd> {
105 let gateway_balances =
106 serde_json::from_value::<GatewayBalances>(cmd!(old_gw, "get-balances").out_json().await?)?;
107 let before_onchain_balance = gateway_balances.onchain_balance_sats;
108
109 let gw_type = old_gw.ln.ln_type();
111 let gw_name = old_gw.gw_name.clone();
112 let old_gw_index = old_gw.gateway_index;
113 old_gw.terminate().await?;
114 info!(target: LOG_TEST, "Terminated Gateway");
115
116 let data_dir: PathBuf = env::var(FM_DATA_DIR_ENV)
118 .expect("Data dir is not set")
119 .parse()
120 .expect("Could not parse data dir");
121 let gw_db = data_dir.join(gw_name.clone()).join("gatewayd.db");
122 if gw_db.is_file() {
123 remove_file(gw_db)?;
125 } else {
126 remove_dir_all(gw_db)?;
127 }
128 info!(target: LOG_TEST, "Deleted the Gateway's database");
129
130 if gw_type == LightningNodeType::Ldk {
131 let ldk_data_dir = data_dir.join(gw_name).join("ldk_node");
133 remove_dir_all(ldk_data_dir)?;
134 info!(target: LOG_TEST, "Deleted LDK's database");
135 }
136
137 let seed = mnemonic.join(" ");
138 unsafe { std::env::set_var("FM_GATEWAY_MNEMONIC", seed) };
140 let new_gw = Gatewayd::new(&process_mgr, new_ln, old_gw_index).await?;
141 let new_mnemonic = new_gw.get_mnemonic().await?.mnemonic;
142 assert_eq!(mnemonic, new_mnemonic);
143 info!(target: LOG_TEST, "Verified mnemonic is the same after creating new Gateway");
144
145 let federations = serde_json::from_value::<Vec<FederationInfo>>(
146 new_gw.get_info().await?["federations"].clone(),
147 )?;
148 assert_eq!(0, federations.len());
149 info!(target: LOG_TEST, "Verified new Gateway has no federations");
150
151 new_gw.recover_fed(fed).await?;
152
153 let gateway_balances =
154 serde_json::from_value::<GatewayBalances>(cmd!(new_gw, "get-balances").out_json().await?)?;
155 let ecash_balance = gateway_balances
156 .ecash_balances
157 .first()
158 .expect("Should have one joined federation");
159 almost_equal(
160 ecash_balance.ecash_balance_msats.sats_round_down(),
161 10_000_000,
162 10,
163 )
164 .unwrap();
165 let after_onchain_balance = gateway_balances.onchain_balance_sats;
166 assert_eq!(before_onchain_balance, after_onchain_balance);
167 info!(target: LOG_TEST, "Verified balances after recovery");
168
169 Ok(new_gw)
170}
171
172#[allow(clippy::too_many_lines)]
174async fn config_test(gw_type: LightningNodeType) -> anyhow::Result<()> {
175 Box::pin(
176 devimint::run_devfed_test()
177 .num_feds(2)
178 .call(|dev_fed, process_mgr| async move {
179 let gw = match gw_type {
180 LightningNodeType::Lnd => dev_fed.gw_lnd_registered().await?,
181 LightningNodeType::Ldk => dev_fed.gw_ldk_connected().await?,
182 };
183
184 let invite_code = dev_fed.fed().await?.invite_code()?;
186 let output = cmd!(gw, "connect-fed", invite_code.clone())
187 .out_json()
188 .await;
189 assert!(
190 output.is_err(),
191 "Connecting to the same federation succeeded"
192 );
193 info!(target: LOG_TEST, "Verified that gateway couldn't connect to already connected federation");
194
195 let gatewayd_version = util::Gatewayd::version_or_default().await;
196
197 let fed_id = dev_fed.fed().await?.calculate_federation_id();
199 gw.set_federation_routing_fee(fed_id.clone(), 20, 20000)
200 .await?;
201
202 let lightning_fee = gw.get_lightning_fee(fed_id.clone()).await?;
203 assert_eq!(
204 lightning_fee.base.msats, 20,
205 "Federation base msat is not 20"
206 );
207 assert_eq!(
208 lightning_fee.parts_per_million, 20000,
209 "Federation proportional millionths is not 20000"
210 );
211 info!(target: LOG_TEST, "Verified per-federation routing fees changed");
212
213 let info_value = cmd!(gw, "info").out_json().await?;
214 let federations = info_value["federations"]
215 .as_array()
216 .expect("federations is an array");
217 assert_eq!(
218 federations.len(),
219 1,
220 "Gateway did not have one connected federation"
221 );
222
223 let config_val = cmd!(gw, "cfg", "client-config", "--federation-id", fed_id)
225 .out_json()
226 .await?;
227
228 serde_json::from_value::<GatewayFedConfig>(config_val)?;
229
230 let bitcoind = dev_fed.bitcoind().await?;
232 let new_fed = Federation::new(
233 &process_mgr,
234 bitcoind.clone(),
235 false,
236 false,
237 1,
238 "config-test".to_string(),
239 )
240 .await?;
241 let new_fed_id = new_fed.calculate_federation_id();
242 info!(target: LOG_TEST, "Successfully spawned new federation");
243
244 let new_invite_code = new_fed.invite_code()?;
245 cmd!(gw, "connect-fed", new_invite_code.clone())
246 .out_json()
247 .await?;
248
249 let (default_base, default_ppm) = if gatewayd_version >= *VERSION_0_8_2 {
250 (0, 0)
251 } else {
252 (2000, 3000)
254 };
255
256 let lightning_fee = gw.get_lightning_fee(new_fed_id.clone()).await?;
257 assert_eq!(
258 lightning_fee.base.msats, default_base,
259 "Default Base msat for new federation was not correct"
260 );
261 assert_eq!(
262 lightning_fee.parts_per_million, default_ppm,
263 "Default Base msat for new federation was not correct"
264 );
265
266 info!(target: LOG_TEST, federation_id = %new_fed_id, "Verified new federation");
267
268 let pegin_amount = Amount::from_msats(10_000_000);
270 new_fed
271 .pegin_gateways(pegin_amount.sats_round_down(), vec![gw])
272 .await?;
273
274 let info_value = cmd!(gw, "info").out_json().await?;
276 let federations = info_value["federations"]
277 .as_array()
278 .expect("federations is an array");
279
280 assert_eq!(
281 federations.len(),
282 2,
283 "Gateway did not have two connected federations"
284 );
285
286 let federation_fake_scids =
287 serde_json::from_value::<Option<BTreeMap<u64, FederationId>>>(
288 info_value
289 .get("channels")
290 .or_else(|| info_value.get("federation_fake_scids"))
291 .expect("field exists")
292 .to_owned(),
293 )
294 .expect("cannot parse")
295 .expect("should have scids");
296
297 assert_eq!(
298 federation_fake_scids.keys().copied().collect::<Vec<u64>>(),
299 vec![1, 2]
300 );
301
302 let first_fed_info = federations
303 .iter()
304 .find(|i| {
305 *i["federation_id"]
306 .as_str()
307 .expect("should parse as str")
308 .to_string()
309 == fed_id
310 })
311 .expect("Could not find federation");
312
313 let second_fed_info = federations
314 .iter()
315 .find(|i| {
316 *i["federation_id"]
317 .as_str()
318 .expect("should parse as str")
319 .to_string()
320 == new_fed_id
321 })
322 .expect("Could not find federation");
323
324 let first_fed_balance_msat =
325 serde_json::from_value::<Amount>(first_fed_info["balance_msat"].clone())
326 .expect("fed should have balance");
327
328 let second_fed_balance_msat =
329 serde_json::from_value::<Amount>(second_fed_info["balance_msat"].clone())
330 .expect("fed should have balance");
331
332 assert_eq!(first_fed_balance_msat, Amount::ZERO);
333 almost_equal(second_fed_balance_msat.msats, pegin_amount.msats, 10_000).unwrap();
334
335 leave_federation(gw, fed_id, 1).await?;
336 leave_federation(gw, new_fed_id, 2).await?;
337
338 let output = cmd!(gw, "connect-fed", new_invite_code.clone())
340 .out_json()
341 .await?;
342 let rejoined_federation_balance_msat =
343 serde_json::from_value::<Amount>(output["balance_msat"].clone())
344 .expect("fed has balance");
345
346 assert_eq!(second_fed_balance_msat, rejoined_federation_balance_msat);
347
348 if gw.gatewayd_version >= *VERSION_0_10_0_ALPHA {
349 info!(target: LOG_TEST, gatewayd_version = %gw.gatewayd_version, "Getting info over iroh");
351 gw.get_info_iroh().await?;
352 }
353
354 info!(target: LOG_TEST, "Gateway configuration test successful");
355 Ok(())
356 }),
357 )
358 .await
359}
360
361#[allow(clippy::too_many_lines)]
364async fn liquidity_test() -> anyhow::Result<()> {
365 devimint::run_devfed_test()
366 .call(|dev_fed, _process_mgr| async move {
367 let federation = dev_fed.fed().await?;
368
369 if !devimint::util::supports_lnv2() {
370 info!(target: LOG_TEST, "LNv2 is not supported, which is necessary for LDK GW and liquidity test");
371 return Ok(());
372 }
373
374 let gw_lnd = dev_fed.gw_lnd_registered().await?;
375 let gw_ldk = dev_fed.gw_ldk_connected().await?;
376 let gw_ldk_second = dev_fed.gw_ldk_second_connected().await?;
377 let gateways = [gw_lnd, gw_ldk].to_vec();
378
379 let gateway_matrix = gateways
380 .iter()
381 .cartesian_product(gateways.iter())
382 .filter(|(a, b)| a.ln.ln_type() != b.ln.ln_type());
383
384 info!(target: LOG_TEST, "Pegging-in gateways...");
385 federation
386 .pegin_gateways(1_000_000, gateways.clone())
387 .await?;
388
389 info!(target: LOG_TEST, "Testing ecash payments between gateways...");
390 for (gw_send, gw_receive) in gateway_matrix.clone() {
391 info!(
392 target: LOG_TEST,
393 gw_send = %gw_send.ln.ln_type(),
394 gw_receive = %gw_receive.ln.ln_type(),
395 "Testing ecash payment",
396 );
397
398 let fed_id = federation.calculate_federation_id();
399 let prev_send_ecash_balance = gw_send.ecash_balance(fed_id.clone()).await?;
400 let prev_receive_ecash_balance = gw_receive.ecash_balance(fed_id.clone()).await?;
401 let ecash = gw_send.send_ecash(fed_id.clone(), 500_000).await?;
402 gw_receive.receive_ecash(ecash).await?;
403 let after_send_ecash_balance = gw_send.ecash_balance(fed_id.clone()).await?;
404 let after_receive_ecash_balance = gw_receive.ecash_balance(fed_id.clone()).await?;
405 assert_eq!(prev_send_ecash_balance - 500_000, after_send_ecash_balance);
406 almost_equal(
407 prev_receive_ecash_balance + 500_000,
408 after_receive_ecash_balance,
409 2_000,
410 )
411 .unwrap();
412 }
413
414 info!(target: LOG_TEST, "Testing payments between gateways...");
415 for (gw_send, gw_receive) in gateway_matrix.clone() {
416 info!(
417 target: LOG_TEST,
418 gw_send = %gw_send.ln.ln_type(),
419 gw_receive = %gw_receive.ln.ln_type(),
420 "Testing lightning payment",
421 );
422
423 let invoice = gw_receive.create_invoice(1_000_000).await?;
424 gw_send.pay_invoice(invoice).await?;
425 }
426
427 let start = now() - Duration::from_secs(5 * 60);
428 let end = now() + Duration::from_secs(5 * 60);
429 info!(target: LOG_TEST, "Verifying list of transactions");
430 let lnd_transactions = gw_lnd.list_transactions(start, end).await?;
431 assert_eq!(lnd_transactions.len(), 2);
433
434 let ldk_transactions = gw_ldk.list_transactions(start, end).await?;
435 assert_eq!(ldk_transactions.len(), 2);
436
437 let start = now() - Duration::from_secs(10 * 60);
439 let end = now() - Duration::from_secs(5 * 60);
440 let lnd_transactions = gw_lnd.list_transactions(start, end).await?;
441 assert_eq!(lnd_transactions.len(), 0);
442
443 info!(target: LOG_TEST, "Testing paying Bolt12 Offers...");
444 poll_with_timeout("First BOLT12 payment", Duration::from_secs(30), || async {
446 let offer_with_amount = gw_ldk_second.create_offer(Some(Amount::from_msats(10_000_000))).await.map_err(ControlFlow::Continue)?;
447 gw_ldk.pay_offer(offer_with_amount, None).await.map_err(ControlFlow::Continue)?;
448 assert!(get_transaction(gw_ldk_second, PaymentKind::Bolt12Offer, Amount::from_msats(10_000_000), PaymentStatus::Succeeded).await.is_some());
449 Ok(())
450 }).await?;
451
452 let offer_without_amount = gw_ldk.create_offer(None).await?;
453 gw_ldk_second.pay_offer(offer_without_amount.clone(), Some(Amount::from_msats(5_000_000))).await?;
454 assert!(get_transaction(gw_ldk, PaymentKind::Bolt12Offer, Amount::from_msats(5_000_000), PaymentStatus::Succeeded).await.is_some());
455
456 gw_ldk_second.pay_offer(offer_without_amount.clone(), None).await.expect_err("Cannot pay amountless offer without specifying an amount");
458
459 gw_ldk_second.pay_offer(offer_without_amount, Some(Amount::from_msats(3_000_000))).await?;
461 assert!(get_transaction(gw_ldk, PaymentKind::Bolt12Offer, Amount::from_msats(3_000_000), PaymentStatus::Succeeded).await.is_some());
462
463 info!(target: LOG_TEST, "Pegging-out gateways...");
464 federation
465 .pegout_gateways(500_000_000, gateways.clone())
466 .await?;
467
468 info!(target: LOG_TEST, "Testing sending onchain...");
469 let bitcoind = dev_fed.bitcoind().await?;
470 for gw in &gateways {
471 let txid = gw
472 .send_onchain(dev_fed.bitcoind().await?, BitcoinAmountOrAll::All, 10)
473 .await?;
474 bitcoind.poll_get_transaction(txid).await?;
475 }
476
477 info!(target: LOG_TEST, "Testing closing all channels...");
478
479 let gw_ldk_pubkey = gw_ldk.lightning_pubkey().await?;
481 gw_lnd.close_channel(gw_ldk_pubkey, false).await?;
482
483 gw_ldk_second.close_all_channels(true).await?;
485
486 for gw in gateways {
488 let channels = gw.list_channels().await?;
489 let active_channel = channels.into_iter().any(|chan| chan.is_active);
490 assert!(!active_channel);
491 }
492
493 Ok(())
494 })
495 .await
496}
497
498async fn esplora_test() -> anyhow::Result<()> {
499 let args = cli::CommonArgs::parse_from::<_, ffi::OsString>(vec![]);
500 let (process_mgr, task_group) = cli::setup(args).await?;
501 cleanup_on_exit(
502 async {
503 info!("Spawning bitcoind...");
504 let bitcoind = Bitcoind::new(&process_mgr, false).await?;
505 info!("Spawning esplora...");
506 let _esplora = Esplora::new(&process_mgr, bitcoind).await?;
507 let network = bitcoin::Network::from_str(&process_mgr.globals.FM_GATEWAY_NETWORK)
508 .expect("Could not parse network");
509 let esplora_port = process_mgr.globals.FM_PORT_ESPLORA.to_string();
510 let esplora = default_esplora_server(network, Some(esplora_port));
511 unsafe {
512 std::env::remove_var("FM_BITCOIND_URL");
513 std::env::set_var("FM_ESPLORA_URL", esplora.url.to_string());
514 }
515 info!("Spawning ldk gateway...");
516 let ldk = Gatewayd::new(
517 &process_mgr,
518 LightningNode::Ldk {
519 name: "gateway-ldk-esplora".to_string(),
520 gw_port: process_mgr.globals.FM_PORT_GW_LDK,
521 ldk_port: process_mgr.globals.FM_PORT_LDK,
522 metrics_port: process_mgr.globals.FM_PORT_GW_LDK_METRICS,
523 },
524 0,
525 )
526 .await?;
527
528 info!("Waiting for ldk gatewy to be ready...");
529 poll("Waiting for LDK to be ready", || async {
530 let info = ldk.get_info().await.map_err(ControlFlow::Continue)?;
531 let state: String = serde_json::from_value(info["gateway_state"].clone())
532 .expect("Could not get gateway state");
533 if state == "Running" {
534 Ok(())
535 } else {
536 Err(ControlFlow::Continue(anyhow::anyhow!(
537 "Gateway not running"
538 )))
539 }
540 })
541 .await?;
542
543 ldk.get_ln_onchain_address().await?;
544 info!(target:LOG_TEST, "ldk gateway successfully spawned and connected to esplora");
545 Ok(())
546 },
547 task_group,
548 )
549 .await?;
550 Ok(())
551}
552
553async fn get_transaction(
554 gateway: &Gatewayd,
555 kind: PaymentKind,
556 amount: Amount,
557 status: PaymentStatus,
558) -> Option<PaymentDetails> {
559 let transactions = gateway
560 .list_transactions(
561 now() - Duration::from_secs(5 * 60),
562 now() + Duration::from_secs(5 * 60),
563 )
564 .await
565 .ok()?;
566 transactions.into_iter().find(|details| {
567 details.payment_kind == kind && details.amount == amount && details.status == status
568 })
569}
570
571async fn leave_federation(gw: &Gatewayd, fed_id: String, expected_scid: u64) -> anyhow::Result<()> {
574 let leave_fed = cmd!(gw, "leave-fed", "--federation-id", fed_id.clone())
575 .out_json()
576 .await
577 .expect("Leaving the federation failed");
578
579 let federation_id: FederationId = serde_json::from_value(leave_fed["federation_id"].clone())?;
580 assert_eq!(federation_id.to_string(), fed_id);
581
582 let scid = serde_json::from_value::<u64>(leave_fed["config"]["federation_index"].clone())?;
583
584 assert_eq!(scid, expected_scid);
585
586 info!(target: LOG_TEST, federation_id = %fed_id, "Verified gateway left federation");
587 Ok(())
588}