Skip to main content

gateway_tests/
main.rs

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