gateway_tests/
main.rs

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            // Recover without a backup
96            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            // Recover with a backup
108            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    // Stop the Gateway
132    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    // Delete the gateway's database
138    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        // Delete LDK's database as well
148        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    // TODO: Audit that the environment access only happens in single-threaded code.
155    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
186/// TODO(v0.5.0): We do not need to run the `gatewayd-mnemonic` test from v0.4.0
187/// -> v0.5.0 over and over again. Once we have verified this test passes for
188/// v0.5.0, it can safely be removed.
189async 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    // TODO: Audit that the environment access only happens in single-threaded code.
196    unsafe { std::env::set_var("FM_GATEWAYD_BASE_EXECUTABLE", old_gatewayd_path) };
197    // TODO: Audit that the environment access only happens in single-threaded code.
198    unsafe { std::env::set_var("FM_GATEWAY_CLI_BASE_EXECUTABLE", old_gateway_cli_path) };
199    // TODO: Audit that the environment access only happens in single-threaded code.
200    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            // Gateway mnemonic is only support in >= v0.5.0
221            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            // Verify that we have a legacy federation
228            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            // Leave federation
238            gw_lnd.leave_federation(federation_id).await?;
239
240            // Rejoin federation
241            gw_lnd.connect_fed(fed).await?;
242
243            // Verify that the legacy federation is recognized
244            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            // Leave federation and delete database to force migration to mnemonic
255            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            // Verify that the re-connected federation is not a legacy federation
269            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/// Test that sets and verifies configurations within the gateway
287#[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                // Try to connect to already connected federation
304                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                // Change the routing fees for a specific federation
317                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                // TODO(support:v0.4): a bug calling `gateway-cli config` was fixed in v0.5.0
343                // see: https://github.com/fedimint/fedimint/pull/5803
344                if gatewayd_version >= *VERSION_0_5_0_ALPHA
345                    && gatewayd_version < *VERSION_0_6_0_ALPHA
346                {
347                    // Get the federation's config and verify it parses correctly
348                    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                    // Get the federation's config and verify it parses correctly
354                    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                // Spawn new federation
361                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                // Peg-in sats to gw for the new fed
397                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                // Verify `info` returns multiple federations
403                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                // Rejoin new federation, verify that the balance is the same
467                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
483/// Test that verifies the various liquidity tools (onchain, lightning, ecash)
484/// work correctly.
485async 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        // Need to try to pay the invoice multiple times in case the channel graph has not been updated yet.
545        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
590/// Leaves the specified federation by issuing a `leave-fed` POST request to the
591/// gateway.
592async 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    // TODO(support:v0.4): `federation_index` was introduced in v0.5.0
603    // see: https://github.com/fedimint/fedimint/pull/5971
604    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}