Skip to content

Add LND Integration Tests in CI #509

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/lnd-integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: CI Checks - LND Integration Tests

on: [push, pull_request]

jobs:
check-lnd:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Create temporary directory for LND data
id: create-temp-dir
run: echo "LND_DATA_DIR=$(mktemp -d)" >> $GITHUB_ENV

- name: Start bitcoind, electrs, and LND
run: docker compose -f docker-compose-lnd.yml up -d
env:
LND_DATA_DIR: ${{ env.LND_DATA_DIR }}

- name: Set permissions for LND data directory
# In PR 4622 (https://github.com/lightningnetwork/lnd/pull/4622),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Thanks for adding comment, but let's move it down into the item this is meant for.

# LND sets file permissions to 0700, preventing test code from accessing them.
# This step ensures the test suite has the necessary permissions.
run: sudo chmod -R 755 $LND_DATA_DIR
env:
LND_DATA_DIR: ${{ env.LND_DATA_DIR }}

- name: Run LND integration tests
run: LND_CERT_PATH=$LND_DATA_DIR/tls.cert LND_MACAROON_PATH=$LND_DATA_DIR/data/chain/bitcoin/regtest/admin.macaroon
RUSTFLAGS="--cfg lnd_test" cargo test --test integration_tests_lnd -- --exact --show-output
env:
LND_DATA_DIR: ${{ env.LND_DATA_DIR }}
5 changes: 5 additions & 0 deletions Cargo.toml
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ electrsd = { version = "0.29.0", features = ["legacy"] }
[target.'cfg(cln_test)'.dev-dependencies]
clightningrpc = { version = "0.3.0-beta.8", default-features = false }

[target.'cfg(lnd_test)'.dev-dependencies]
lnd_grpc_rust = { version = "2.10.0", default-features = false }
tokio = { version = "1.37", features = ["fs"] }

[build-dependencies]
uniffi = { version = "0.27.3", features = ["build"], optional = true }

Expand All @@ -123,4 +127,5 @@ check-cfg = [
"cfg(ldk_bench)",
"cfg(tokio_unstable)",
"cfg(cln_test)",
"cfg(lnd_test)",
]
87 changes: 87 additions & 0 deletions docker-compose-lnd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
services:
bitcoin:
image: blockstream/bitcoind:24.1
platform: linux/amd64
command:
[
"bitcoind",
"-printtoconsole",
"-regtest=1",
"-rpcallowip=0.0.0.0/0",
"-rpcbind=0.0.0.0",
"-rpcuser=user",
"-rpcpassword=pass",
"-fallbackfee=0.00001",
"-zmqpubrawblock=tcp://0.0.0.0:28332",
"-zmqpubrawtx=tcp://0.0.0.0:28333"
]
ports:
- "18443:18443" # Regtest RPC port
- "18444:18444" # Regtest P2P port
- "28332:28332" # ZMQ block port
- "28333:28333" # ZMQ tx port
networks:
- bitcoin-electrs
healthcheck:
test: ["CMD", "bitcoin-cli", "-regtest", "-rpcuser=user", "-rpcpassword=pass", "getblockchaininfo"]
interval: 5s
timeout: 10s
retries: 5

electrs:
image: blockstream/esplora:electrs-cd9f90c115751eb9d2bca9a4da89d10d048ae931
platform: linux/amd64
depends_on:
bitcoin:
condition: service_healthy
command:
[
"/app/electrs_bitcoin/bin/electrs",
"-vvvv",
"--timestamp",
"--jsonrpc-import",
"--cookie=user:pass",
"--network=regtest",
"--daemon-rpc-addr=bitcoin:18443",
"--http-addr=0.0.0.0:3002",
"--electrum-rpc-addr=0.0.0.0:50001"
]
ports:
- "3002:3002"
- "50001:50001"
networks:
- bitcoin-electrs

lnd:
image: lightninglabs/lnd:v0.18.5-beta
container_name: ldk-node-lnd
depends_on:
- bitcoin
volumes:
- ${LND_DATA_DIR}:/root/.lnd
ports:
- "8081:8081"
- "9735:9735"
command:
- "--noseedbackup"
- "--trickledelay=5000"
- "--alias=ldk-node-lnd-test"
- "--externalip=lnd:9735"
- "--bitcoin.active"
- "--bitcoin.regtest"
- "--bitcoin.node=bitcoind"
- "--bitcoind.rpchost=bitcoin:18443"
- "--bitcoind.rpcuser=user"
- "--bitcoind.rpcpass=pass"
- "--bitcoind.zmqpubrawblock=tcp://bitcoin:28332"
- "--bitcoind.zmqpubrawtx=tcp://bitcoin:28333"
- "--accept-keysend"
- "--rpclisten=0.0.0.0:8081"
- "--tlsextradomain=lnd"
- "--tlsextraip=0.0.0.0"
networks:
- bitcoin-electrs

networks:
bitcoin-electrs:
driver: bridge
2 changes: 1 addition & 1 deletion tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
// accordance with one or both of these licenses.

#![cfg(any(test, cln_test, vss_test))]
#![cfg(any(test, cln_test, lnd_test, vss_test))]
#![allow(dead_code)]

pub(crate) mod logging;
Expand Down
227 changes: 227 additions & 0 deletions tests/integration_tests_lnd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
#![cfg(lnd_test)]

mod common;

use ldk_node::bitcoin::secp256k1::PublicKey;
use ldk_node::bitcoin::Amount;
use ldk_node::lightning::ln::msgs::SocketAddress;
use ldk_node::{Builder, Event};

use lnd_grpc_rust::lnrpc::{
invoice::InvoiceState::Settled as LndInvoiceStateSettled, GetInfoRequest as LndGetInfoRequest,
GetInfoResponse as LndGetInfoResponse, Invoice as LndInvoice,
ListInvoiceRequest as LndListInvoiceRequest, QueryRoutesRequest as LndQueryRoutesRequest,
Route as LndRoute, SendRequest as LndSendRequest,
};
use lnd_grpc_rust::{connect, LndClient};

use bitcoincore_rpc::Auth;
use bitcoincore_rpc::Client as BitcoindClient;

use electrum_client::Client as ElectrumClient;
use lightning_invoice::{Bolt11InvoiceDescription, Description};

use bitcoin::hex::DisplayHex;

use std::default::Default;
use std::str::FromStr;
use tokio::fs;

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_lnd() {
// Setup bitcoind / electrs clients
let bitcoind_client = BitcoindClient::new(
"127.0.0.1:18443",
Auth::UserPass("user".to_string(), "pass".to_string()),
)
.unwrap();
let electrs_client = ElectrumClient::new("tcp://127.0.0.1:50001").unwrap();

// Give electrs a kick.
common::generate_blocks_and_wait(&bitcoind_client, &electrs_client, 1);

// Setup LDK Node
let config = common::random_config(true);
let mut builder = Builder::from_config(config.node_config);
builder.set_chain_source_esplora("http://127.0.0.1:3002".to_string(), None);

let node = builder.build().unwrap();
node.start().unwrap();

// Premine some funds and distribute
let address = node.onchain_payment().new_address().unwrap();
let premine_amount = Amount::from_sat(5_000_000);
common::premine_and_distribute_funds(
&bitcoind_client,
&electrs_client,
vec![address],
premine_amount,
);

// Setup LND
let endpoint = "127.0.0.1:8081";
let cert_path = std::env::var("LND_CERT_PATH").expect("LND_CERT_PATH not set");
let macaroon_path = std::env::var("LND_MACAROON_PATH").expect("LND_MACAROON_PATH not set");
let mut lnd = TestLndClient::new(cert_path, macaroon_path, endpoint.to_string()).await;

let lnd_node_info = lnd.get_node_info().await;
let lnd_node_id = PublicKey::from_str(&lnd_node_info.identity_pubkey).unwrap();
let lnd_address: SocketAddress = "127.0.0.1:9735".parse().unwrap();

node.sync_wallets().unwrap();

// Open the channel
let funding_amount_sat = 1_000_000;

node.open_channel(lnd_node_id, lnd_address, funding_amount_sat, Some(500_000_000), None)
.unwrap();

let funding_txo = common::expect_channel_pending_event!(node, lnd_node_id);
common::wait_for_tx(&electrs_client, funding_txo.txid);
common::generate_blocks_and_wait(&bitcoind_client, &electrs_client, 6);
node.sync_wallets().unwrap();
let user_channel_id = common::expect_channel_ready_event!(node, lnd_node_id);

// Send a payment to LND
let lnd_invoice = lnd.create_invoice(100_000_000).await;
let parsed_invoice = lightning_invoice::Bolt11Invoice::from_str(&lnd_invoice).unwrap();

node.bolt11_payment().send(&parsed_invoice, None).unwrap();
common::expect_event!(node, PaymentSuccessful);
let lnd_listed_invoices = lnd.list_invoices().await;
assert_eq!(lnd_listed_invoices.len(), 1);
assert_eq!(lnd_listed_invoices.first().unwrap().state, LndInvoiceStateSettled as i32);

// Check route LND -> LDK
let amount_msat = 9_000_000;
let max_retries = 7;
for attempt in 1..=max_retries {
match lnd.query_routes(&node.node_id().to_string(), amount_msat).await {
Ok(routes) => {
if !routes.is_empty() {
break;
}
},
Err(err) => {
if attempt == max_retries {
panic!("Failed to find route from LND to LDK: {}", err);
}
},
};
// wait for the payment process
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
}

// Send a payment to LDK
let invoice_description =
Bolt11InvoiceDescription::Direct(Description::new("lndTest".to_string()).unwrap());
let ldk_invoice =
node.bolt11_payment().receive(amount_msat, &invoice_description, 3600).unwrap();
lnd.pay_invoice(&ldk_invoice.to_string()).await;
common::expect_event!(node, PaymentReceived);

node.close_channel(&user_channel_id, lnd_node_id).unwrap();
common::expect_event!(node, ChannelClosed);
node.stop().unwrap();
}

struct TestLndClient {
client: LndClient,
}

impl TestLndClient {
async fn new(cert_path: String, macaroon_path: String, socket: String) -> Self {
// Read the contents of the file into a vector of bytes
let cert_bytes = fs::read(cert_path).await.expect("Failed to read tls cert file");
let mac_bytes = fs::read(macaroon_path).await.expect("Failed to read macaroon file");

// Convert the bytes to a hex string
let cert = cert_bytes.as_hex().to_string();
let macaroon = mac_bytes.as_hex().to_string();

let client = connect(cert, macaroon, socket).await.expect("Failed to connect to Lnd");

TestLndClient { client }
}

async fn get_node_info(&mut self) -> LndGetInfoResponse {
let response = self
.client
.lightning()
.get_info(LndGetInfoRequest {})
.await
.expect("Failed to fetch node info from LND")
.into_inner();

response
}

async fn create_invoice(&mut self, amount_msat: u64) -> String {
let invoice = LndInvoice { value_msat: amount_msat as i64, ..Default::default() };

self.client
.lightning()
.add_invoice(invoice)
.await
.expect("Failed to create invoice on LND")
.into_inner()
.payment_request
}

async fn list_invoices(&mut self) -> Vec<LndInvoice> {
self.client
.lightning()
.list_invoices(LndListInvoiceRequest { ..Default::default() })
.await
.expect("Failed to list invoices from LND")
.into_inner()
.invoices
}

async fn query_routes(
&mut self, pubkey: &str, amount_msat: u64,
) -> Result<Vec<LndRoute>, String> {
let request = LndQueryRoutesRequest {
pub_key: pubkey.to_string(),
amt_msat: amount_msat as i64,
..Default::default()
};

let response = self
.client
.lightning()
.query_routes(request)
.await
.map_err(|err| format!("Failed to query routes from LND: {:?}", err))?
.into_inner();

if response.routes.is_empty() {
return Err(format!("No routes found for pubkey: {}", pubkey));
}

Ok(response.routes)
}

async fn pay_invoice(&mut self, invoice_str: &str) {
let send_req =
LndSendRequest { payment_request: invoice_str.to_string(), ..Default::default() };
let response = self
.client
.lightning()
.send_payment_sync(send_req)
.await
.expect("Failed to pay invoice on LND")
.into_inner();

if !response.payment_error.is_empty() || response.payment_preimage.is_empty() {
panic!(
"LND payment failed: {}",
if response.payment_error.is_empty() {
"No preimage returned"
} else {
&response.payment_error
}
);
}
}
}