diff --git a/.github/workflows/cln-integration.yml b/.github/workflows/cln-integration.yml index 2c427cbde..b088a3686 100644 --- a/.github/workflows/cln-integration.yml +++ b/.github/workflows/cln-integration.yml @@ -9,18 +9,13 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Install dependencies - run: | - sudo apt-get update -y - sudo apt-get install -y socat + - name: Create temporary directory for CLN data + id: create-temp-dir + run: echo "CLN_DATA_DIR=$(mktemp -d)" >> $GITHUB_ENV - name: Start bitcoind, electrs, and lightningd run: docker compose -f docker-compose-cln.yml up -d - - name: Forward lightningd RPC socket - run: | - docker exec ldk-node-cln-1 sh -c "socat -d -d TCP-LISTEN:9937,fork,reuseaddr UNIX-CONNECT:/root/.lightning/regtest/lightning-rpc&" - socat -d -d UNIX-LISTEN:/tmp/lightning-rpc,reuseaddr,fork TCP:127.0.0.1:9937& - - name: Run CLN integration tests - run: RUSTFLAGS="--cfg cln_test" cargo test --test integration_tests_cln + run: | + CLN_SOCK=$CLN_DATA_DIR/lightning-rpc RUSTFLAGS="--cfg cln_test" cargo test --test integration_tests_cln diff --git a/Cargo.toml b/Cargo.toml index b639b7dc1..0b6e4cb26 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,7 +126,6 @@ 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.28.3", features = ["build"], optional = true } diff --git a/docker-compose-cln.yml b/docker-compose-cln.yml index e1fb117e5..fbd11a8f6 100644 --- a/docker-compose-cln.yml +++ b/docker-compose-cln.yml @@ -48,20 +48,24 @@ services: - bitcoin-electrs cln: - image: blockstream/lightningd:v23.08 + image: blockstream/lightningd:v25.05 platform: linux/amd64 depends_on: bitcoin: condition: service_healthy command: [ + "--network=regtest", "--bitcoin-rpcconnect=bitcoin", "--bitcoin-rpcport=18443", "--bitcoin-rpcuser=user", "--bitcoin-rpcpassword=pass", "--regtest", - "--experimental-anchors", + "--rpc-file=/tmp/lightning-rpc", + "--rpc-file-mode=0666", ] + volumes: + - ${CLN_DATA_DIR}:/tmp ports: - "19846:19846" - "9937:9937" diff --git a/tests/common/external_node.rs b/tests/common/external_node.rs new file mode 100644 index 000000000..190f81b38 --- /dev/null +++ b/tests/common/external_node.rs @@ -0,0 +1,389 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +#![cfg(any(cln_test, lnd_test))] +#![allow(dead_code)] + +use electrsd::corepc_client::client_sync::Auth; +use lightning::offers::offer; + +use ldk_node::bitcoin::secp256k1::PublicKey; +use ldk_node::{Builder, Event, Node, NodeError, UserChannelId}; + +use lightning::ln::msgs::SocketAddress; + +use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; + +use bitcoin::{Amount, OutPoint}; + +use electrsd::corepc_node::Client as BitcoindClient; +use electrum_client::Client as ElectrumClient; +use electrum_client::ElectrumApi; + +use std::str::FromStr; +use std::sync::OnceLock; + +use crate::common::{ + distribute_funds_unconfirmed, expect_channel_pending_event, expect_channel_ready_event, + expect_event, expect_payment_successful_event, generate_blocks_and_wait, premine_blocks, + random_config, wait_for_tx, +}; + +pub trait ExternalLightningNode { + fn get_node_info(&mut self) -> (PublicKey, SocketAddress); + + fn create_invoice(&mut self, amount_msat: u64, description: Option) -> String; + + fn pay_invoice(&mut self, invoice: &str); + + fn check_receive_payment(&mut self, invoice: Bolt11Invoice); + + fn close_channel(&mut self, channel_id: OutPoint, node_id: PublicKey, force: bool); + + fn create_new_address(&mut self) -> String; + + fn open_channel(&mut self, node_id: PublicKey, funding_amount_sat: u64); + + fn generate_offer(&mut self, _amount_msat: Option, _description: &str) -> String { + panic!("Not implemented") + } + + fn pay_offer(&mut self, _offer: &str, _amount_msat: Option) -> String { + panic!("Not implemented") + } +} + +static BITCOIND_CLIENT: OnceLock = OnceLock::new(); + +pub(crate) fn init_bitcoind_client() -> &'static BitcoindClient { + BITCOIND_CLIENT.get_or_init(|| { + let bitcoind_client = BitcoindClient::new_with_auth( + "http://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(); + premine_blocks(&bitcoind_client, &electrs_client); + + bitcoind_client + }) +} + +pub(crate) fn setup_test_node( + anchor_channels: bool, +) -> (Node, &'static BitcoindClient, ElectrumClient) { + let bitcoind_client = init_bitcoind_client(); + + let electrs_client = ElectrumClient::new("tcp://127.0.0.1:50001").unwrap(); + + let config = random_config(anchor_channels); + 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(); + + (node, bitcoind_client, electrs_client) +} + +fn add_onchain_funds(bitcoind: &BitcoindClient, electrs: &ElectrumClient, node: &Node) { + let addr = node.onchain_payment().new_address().unwrap(); + let amount = Amount::from_sat(5_000_000); + distribute_funds_unconfirmed(bitcoind, electrs, vec![addr], amount); + generate_blocks_and_wait(bitcoind, electrs, 1); +} + +fn open_channel( + bitcoind: &BitcoindClient, electrs: &E, node: &Node, external_node_id: PublicKey, + external_node_address: SocketAddress, funding_amount_sat: u64, push_msat: Option, +) -> (UserChannelId, OutPoint) { + node.sync_wallets().unwrap(); + + // Open the channel + node.open_announced_channel( + external_node_id, + external_node_address, + funding_amount_sat, + push_msat, + None, + ) + .unwrap(); + + let funding_txo = expect_channel_pending_event!(node, external_node_id); + wait_for_tx(electrs, funding_txo.txid); + generate_blocks_and_wait(bitcoind, electrs, 6); + node.sync_wallets().unwrap(); + let user_channel_id = expect_channel_ready_event!(node, external_node_id); + + (user_channel_id, funding_txo) +} + +fn send_payment_to_external_node( + node: &Node, external_node: &mut E, amount_msat: u64, +) -> Bolt11Invoice { + let invoice_string = external_node.create_invoice(amount_msat, None); + let invoice = Bolt11Invoice::from_str(&invoice_string).unwrap(); + node.bolt11_payment() + .send(&invoice, None) + .unwrap_or_else(|e| panic!("Failed to send payment: {:?}", e)); + invoice +} + +fn check_send_payment_succeeds( + node: &Node, external_node: &mut E, invoice: Bolt11Invoice, +) { + expect_event!(node, PaymentSuccessful); + external_node.check_receive_payment(invoice) +} + +fn receive_payment_from_external_node( + node: &Node, external_node: &mut E, amount_msat: u64, +) { + let invoice_description = Bolt11InvoiceDescription::Direct( + Description::new("test external node".to_string()).unwrap(), + ); + let ldk_invoice = + node.bolt11_payment().receive(amount_msat, &invoice_description, 3600).unwrap(); + external_node.pay_invoice(&ldk_invoice.to_string()); + + expect_event!(node, PaymentReceived); +} + +fn close_channel( + bitcoind_client: &BitcoindClient, electrs_client: &F, external_node: &mut E, node: &Node, + channel_id: OutPoint, node_id: PublicKey, user_channel_id: &UserChannelId, + external_force_close: Option, +) { + match external_force_close { + Some(force) => { + if force { + // Other Lightning implementations only force close if the counterparty is not connected; + // they always attempt a cooperative close first before forcing. + node.stop().unwrap(); + external_node.close_channel(channel_id, node.node_id(), force); + node.start().unwrap(); + } else { + external_node.close_channel(channel_id, node.node_id(), force); + } + generate_blocks_and_wait(bitcoind_client, electrs_client, 1); + }, + None => { + node.close_channel(user_channel_id, node_id).unwrap(); + }, + } + expect_event!(node, ChannelClosed); +} + +pub(crate) fn do_ldk_opens_channel_full_cycle_with_external_node( + external_node: &mut E, external_force_close: Option, +) { + // Initialize LDK node and clients + let (node, bitcoind_client, electrs_client) = setup_test_node(true); + + // setup external node info + let (external_node_id, external_node_address) = external_node.get_node_info(); + let external_node_address = external_node_address.clone(); + + // Open the channel + add_onchain_funds(&bitcoind_client, &electrs_client, &node); + let funding_amount_sat = 2_000_000; + let push_msat = Some(500_000_000); + let (user_channel_id, funding_txo) = open_channel( + &bitcoind_client, + &electrs_client, + &node, + external_node_id, + external_node_address, + funding_amount_sat, + push_msat, + ); + + // Send a payment to the external node + let invoice_amount_sat = 100_000_000; + let invoice = send_payment_to_external_node(&node, external_node, invoice_amount_sat); + check_send_payment_succeeds(&node, external_node, invoice); + + // Send a payment to LDK + let amount_msat = 9_000_000; + receive_payment_from_external_node(&node, external_node, amount_msat); + + // Test overpayment: Send more than the invoice amount to the external node + let overpaid_invoice_amount_msat = 100_000_000; + let overpaid_amount_msat = overpaid_invoice_amount_msat + 10_000; // Excesso de 10k msat + let overpaid_invoice = external_node.create_invoice(overpaid_invoice_amount_msat, None); + let overpaid_bolt11_invoice = Bolt11Invoice::from_str(&overpaid_invoice).unwrap(); + let _overpaid_payment_id = node + .bolt11_payment() + .send_using_amount(&overpaid_bolt11_invoice, overpaid_amount_msat, None) + .unwrap(); + expect_event!(node, PaymentSuccessful); + external_node.check_receive_payment(overpaid_bolt11_invoice); // Assumindo que o externo aceita overpayment + + // Test underpayment: Attempt to send less than invoice amount (should fail) + let underpaid_invoice_amount_msat = 100_000_000; + let underpaid_amount_msat = underpaid_invoice_amount_msat - 1; + let underpaid_invoice = external_node.create_invoice(underpaid_invoice_amount_msat, None); + let underpaid_bolt11_invoice = Bolt11Invoice::from_str(&underpaid_invoice).unwrap(); + assert_eq!( + Err(NodeError::InvalidAmount), + node.bolt11_payment().send_using_amount( + &underpaid_bolt11_invoice, + underpaid_amount_msat, + None + ) + ); + + // Test variable amount invoice: External node creates zero-amount invoice, LDK pays a determined amount + let variable_invoice = external_node.create_invoice(0, None); + let variable_bolt11_invoice = Bolt11Invoice::from_str(&variable_invoice).unwrap(); + let determined_amount_msat = 50_000_000; + let _variable_payment_id = node + .bolt11_payment() + .send_using_amount(&variable_bolt11_invoice, determined_amount_msat, None) + .unwrap(); + expect_event!(node, PaymentSuccessful); + + // Close the channel + close_channel( + &bitcoind_client, + &electrs_client, + external_node, + &node, + funding_txo, + external_node_id, + &user_channel_id, + external_force_close, + ); + + node.stop().unwrap(); +} + +pub(crate) fn do_external_node_opens_channel_simple_transactions_with_ldk< + E: ExternalLightningNode, +>( + external_node: &mut E, +) { + // Initialize LDK node and clients + let (node, bitcoind_client, electrs_client) = setup_test_node(false); + + // setup external node info + let (external_node_id, external_node_address) = external_node.get_node_info(); + + let addr_string = external_node.create_new_address(); + let addr = bitcoin::Address::from_str(&addr_string) + .unwrap() + .require_network(bitcoin::Network::Regtest) + .unwrap(); + let amount = Amount::from_sat(5_000_000); + distribute_funds_unconfirmed(bitcoind_client, &electrs_client, vec![addr], amount); + generate_blocks_and_wait(bitcoind_client, &electrs_client, 6); + + node.connect(external_node_id, external_node_address.clone(), true).unwrap(); + + // Open the channel + external_node.open_channel(node.node_id(), 2_000_000); + let funding_txo = expect_channel_pending_event!(node, external_node_id); + wait_for_tx(&electrs_client, funding_txo.txid); + generate_blocks_and_wait(bitcoind_client, &electrs_client, 6); + node.sync_wallets().unwrap(); + let user_channel_id = expect_channel_ready_event!(node, external_node_id); + + // Send a payment to LDK + let amount_msat = 100_000_000; + receive_payment_from_external_node(&node, external_node, amount_msat); + node.sync_wallets().unwrap(); + + // Send a payment to the external node + let invoice_amount_sat = node.list_channels().first().unwrap().next_outbound_htlc_limit_msat; + let invoice: Bolt11Invoice = + send_payment_to_external_node(&node, external_node, invoice_amount_sat); + check_send_payment_succeeds(&node, external_node, invoice); + + // Close the channel + close_channel( + &bitcoind_client, + &electrs_client, + external_node, + &node, + funding_txo, + external_node_id, + &user_channel_id, + None, + ); + + node.stop().unwrap(); +} + +pub(crate) fn do_bolt12_cycle_with_external_node(external_node: &mut E) { + // Initialize LDK node and clients + let (node, bitcoind_client, electrs_client) = setup_test_node(true); + + // setup external node info + let (external_node_id, external_node_address) = external_node.get_node_info(); + + // Open the channel + add_onchain_funds(&bitcoind_client, &electrs_client, &node); + let funding_amount_sat = 2_000_000; + let push_msat = Some(500_000_000); + let (user_channel_id, funding_txo) = open_channel( + &bitcoind_client, + &electrs_client, + &node, + external_node_id, + external_node_address, + funding_amount_sat, + push_msat, + ); + + // Send a payment to the external node, without specifying an amount + let mut payer_note = "without specifying an amount"; + let offer_string = external_node.generate_offer(None, payer_note); + let offer = offer::Offer::from_str(&offer_string).unwrap(); + let mut amount_msat = 100_000_000; + let payment_id = node + .bolt12_payment() + .send_using_amount(&offer, amount_msat, None, Some(payer_note.to_string())) + .unwrap(); + expect_payment_successful_event!(node, Some(payment_id), None); + + // Send a payment to the external node, specifying an amount + amount_msat = 100_000_000; + payer_note = "specifying an amount"; + let offer_string = external_node.generate_offer(Some(amount_msat), payer_note); + let offer = offer::Offer::from_str(&offer_string).unwrap(); + let payment_id = + node.bolt12_payment().send(&offer, None, Some(payer_note.to_string())).unwrap(); + expect_payment_successful_event!(node, Some(payment_id), None); + + // Send a payment to LDK, without specifying an amount + let offer = node.bolt12_payment().receive(0, "", None, None).unwrap(); + let offer_string: String = offer.to_string(); + let amount_msat = 9_000_000; + external_node.pay_offer(&offer_string, Some(amount_msat)); + expect_event!(node, PaymentReceived); + + // Send a payment to LDK, specifying an amount + let offer = node.bolt12_payment().receive(0, "", None, None).unwrap(); + let offer_string: String = offer.to_string(); + let amount_msat = 9_000_000; + external_node.pay_offer(&offer_string, Some(amount_msat)); + expect_event!(node, PaymentReceived); + + // Close the channel + close_channel( + &bitcoind_client, + &electrs_client, + external_node, + &node, + funding_txo, + external_node_id, + &user_channel_id, + None, + ); + node.stop().unwrap(); +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 1331fc047..0a53d8c5c 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -8,6 +8,7 @@ #![cfg(any(test, cln_test, lnd_test, vss_test))] #![allow(dead_code)] +pub(crate) mod external_node; pub(crate) mod logging; use std::collections::{HashMap, HashSet}; diff --git a/tests/integration_tests_cln.rs b/tests/integration_tests_cln.rs index 6fc72b2c2..9a827ba6c 100644 --- a/tests/integration_tests_cln.rs +++ b/tests/integration_tests_cln.rs @@ -9,118 +9,195 @@ mod common; -use std::default::Default; -use std::str::FromStr; - use clightningrpc::lightningrpc::LightningRPC; use clightningrpc::responses::NetworkAddress; -use electrsd::corepc_client::client_sync::Auth; -use electrsd::corepc_node::Client as BitcoindClient; -use electrum_client::Client as ElectrumClient; use ldk_node::bitcoin::secp256k1::PublicKey; -use ldk_node::bitcoin::Amount; use ldk_node::lightning::ln::msgs::SocketAddress; -use ldk_node::{Builder, Event}; -use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; + use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; +use std::default::Default; +use std::str::FromStr; + +use crate::common::external_node::{ + do_bolt12_cycle_with_external_node, + do_external_node_opens_channel_simple_transactions_with_ldk, + do_ldk_opens_channel_full_cycle_with_external_node, init_bitcoind_client, + ExternalLightningNode, +}; + +#[test] +fn test_ldk_initiates_channel_close() { + init_bitcoind_client(); + let mut client = ClnClient::new(); + do_ldk_opens_channel_full_cycle_with_external_node(&mut client, Some(true)); +} + +#[test] +fn test_cln_initiates_channel_close() { + init_bitcoind_client(); + let mut client = ClnClient::new(); + do_ldk_opens_channel_full_cycle_with_external_node(&mut client, Some(false)); +} + +#[test] +fn test_cln_initiates_force_channel_close() { + init_bitcoind_client(); + let mut client = ClnClient::new(); + do_ldk_opens_channel_full_cycle_with_external_node(&mut client, None); +} + +#[test] +fn test_cln_opens_channel_with_ldk() { + init_bitcoind_client(); + let mut client = ClnClient::new(); + do_external_node_opens_channel_simple_transactions_with_ldk(&mut client); +} + #[test] -fn test_cln() { - // Setup bitcoind / electrs clients - let bitcoind_client = BitcoindClient::new_with_auth( - "http://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 CLN - let sock = "/tmp/lightning-rpc"; - let cln_client = LightningRPC::new(&sock); - let cln_info = { +fn test_simple_bolt12() { + init_bitcoind_client(); + let mut client = ClnClient::new(); + do_bolt12_cycle_with_external_node(&mut client); +} + +struct ClnClient { + pub client: LightningRPC, + pub rng: rand::rngs::ThreadRng, +} + +impl ClnClient { + pub fn new() -> Self { + let sock = std::env::var("CLN_SOCK").expect("CLN_SOCK must be set"); + let cln_client = LightningRPC::new(&sock); + + use std::time::{Duration, Instant}; + let start = Instant::now(); loop { - let info = cln_client.getinfo().unwrap(); - // Wait for CLN to sync block height before channel open. - // Prevents crash due to unset blockheight (see LDK Node issue #527). - if info.blockheight > 0 { - break info; + match cln_client.getinfo() { + Ok(info) => { + if info.blockheight > 0 { + return Self { client: cln_client, rng: thread_rng() }; + } else if start.elapsed() > Duration::from_secs(30) { + panic!("Timeout waiting for cln to be ready"); + } + }, + Err(e) => { + panic!("Not able to connect to cln: {}", e); + }, } - std::thread::sleep(std::time::Duration::from_millis(250)); } - }; - let cln_node_id = PublicKey::from_str(&cln_info.id).unwrap(); - let cln_address: SocketAddress = match cln_info.binding.first().unwrap() { - NetworkAddress::Ipv4 { address, port } => { - std::net::SocketAddrV4::new(*address, *port).into() - }, - NetworkAddress::Ipv6 { address, port } => { - std::net::SocketAddrV6::new(*address, *port, 0, 0).into() - }, - _ => { - panic!() - }, - }; - - node.sync_wallets().unwrap(); - - // Open the channel - let funding_amount_sat = 1_000_000; - - node.open_channel(cln_node_id, cln_address, funding_amount_sat, Some(500_000_000), None) - .unwrap(); - - let funding_txo = common::expect_channel_pending_event!(node, cln_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, cln_node_id); - - // Send a payment to CLN - let mut rng = thread_rng(); - let rand_label: String = (0..7).map(|_| rng.sample(Alphanumeric) as char).collect(); - let cln_invoice = - cln_client.invoice(Some(10_000_000), &rand_label, &rand_label, None, None, None).unwrap(); - let parsed_invoice = Bolt11Invoice::from_str(&cln_invoice.bolt11).unwrap(); - - node.bolt11_payment().send(&parsed_invoice, None).unwrap(); - common::expect_event!(node, PaymentSuccessful); - let cln_listed_invoices = - cln_client.listinvoices(Some(&rand_label), None, None, None).unwrap().invoices; - assert_eq!(cln_listed_invoices.len(), 1); - assert_eq!(cln_listed_invoices.first().unwrap().status, "paid"); - - // Send a payment to LDK - let rand_label: String = (0..7).map(|_| rng.sample(Alphanumeric) as char).collect(); - let invoice_description = - Bolt11InvoiceDescription::Direct(Description::new(rand_label).unwrap()); - let ldk_invoice = - node.bolt11_payment().receive(10_000_000, &invoice_description, 3600).unwrap(); - cln_client.pay(&ldk_invoice.to_string(), Default::default()).unwrap(); - common::expect_event!(node, PaymentReceived); - - node.close_channel(&user_channel_id, cln_node_id).unwrap(); - common::expect_event!(node, ChannelClosed); - node.stop().unwrap(); + } +} + +impl ExternalLightningNode for ClnClient { + fn get_node_info(&mut self) -> (PublicKey, SocketAddress) { + let info = self.client.getinfo().unwrap(); + let node_id = PublicKey::from_str(&info.id).unwrap(); + let address: SocketAddress = match info.binding.first().unwrap() { + NetworkAddress::Ipv4 { address, port } => { + std::net::SocketAddrV4::new(*address, *port).into() + }, + NetworkAddress::Ipv6 { address, port } => { + std::net::SocketAddrV6::new(*address, *port, 0, 0).into() + }, + _ => { + panic!() + }, + }; + (node_id, address) + } + + fn create_invoice(&mut self, amount_msat: u64, _description: Option) -> String { + let description = _description + .unwrap_or_else(|| (0..7).map(|_| self.rng.sample(Alphanumeric) as char).collect()); + let rand_label: String = (0..7).map(|_| self.rng.sample(Alphanumeric) as char).collect(); + let amount = if amount_msat == 0 { None } else { Some(amount_msat) }; + let cln_invoice = + self.client.invoice(amount, &rand_label, &description, None, None, None).unwrap(); + + cln_invoice.bolt11 + } + + fn check_receive_payment(&mut self, invoice: lightning_invoice::Bolt11Invoice) { + let payment_hash = invoice.payment_hash().to_string(); + let invoices = + self.client.listinvoices(None, None, Some(&payment_hash), None).unwrap().invoices; + + assert_eq!(invoices.len(), 1); + assert_eq!(invoices[0].status, "paid"); + } + + fn pay_invoice(&mut self, invoice: &str) { + self.client.pay(invoice, Default::default()).unwrap(); + } + + fn close_channel(&mut self, _out_point: bitcoin::OutPoint, peer_id: PublicKey, force: bool) { + // Find the channel to close + let response_channels = + self.client.listchannels(None, None, Some(&peer_id.to_string())).unwrap(); + if response_channels.channels.is_empty() && response_channels.channels.len() > 1 { + panic!("No channels to close"); + } + let short_channel_id = &response_channels.channels[0].short_channel_id; + + let response: clightningrpc::responses::Close; + if force { + let unilateral_timeout = Some(1); + let input = serde_json::json!({ + "id": short_channel_id, + "unilateraltimeout": unilateral_timeout, + }); + response = self.client.call("close", input).unwrap(); + assert_eq!(response.type_, "unilateral"); + } else { + response = self.client.close(short_channel_id, None, None).unwrap(); + assert_eq!(response.type_, "mutual"); + } + } + fn create_new_address(&mut self) -> String { + let address_info = self.client.newaddr(None).unwrap(); + address_info.bech32.unwrap() + } + + fn open_channel(&mut self, node_id: PublicKey, funding_amount_sat: u64) { + self.client + .fundchannel( + &node_id.to_string(), + clightningrpc::requests::AmountOrAll::Amount(funding_amount_sat), + None, + ) + .unwrap(); + } + + fn generate_offer(&mut self, amount_msat: Option, description: &str) -> String { + let mut input = serde_json::json!({}); + if let Some(amt) = amount_msat { + input["amount"] = amt.to_string().into(); + } else { + input["amount"] = "any".into(); + } + input["description"] = description.into(); + let offer: serde_json::Value = self.client.call("offer", input).unwrap(); + offer["bolt12"].as_str().unwrap().to_string() + } + + fn pay_offer(&mut self, offer: &str, amount_msat: Option) -> String { + let input = if let Some(amt) = amount_msat { + serde_json::json!({ + "offer": offer, + "amount_msat": amt, + }) + } else { + serde_json::json!({ + "offer": offer, + }) + }; + + let response: serde_json::Value = self.client.call("fetchinvoice", input).unwrap(); + self.pay_invoice(response["invoice"].as_str().unwrap()); + + response["invoice"].as_str().unwrap().to_string() + } } diff --git a/tests/integration_tests_lnd.rs b/tests/integration_tests_lnd.rs index 7dfc1e4f9..f73192393 100755 --- a/tests/integration_tests_lnd.rs +++ b/tests/integration_tests_lnd.rs @@ -2,222 +2,215 @@ mod common; -use std::default::Default; -use std::str::FromStr; - -use bitcoin::hex::DisplayHex; -use electrsd::corepc_client::client_sync::Auth; -use electrsd::corepc_node::Client as BitcoindClient; -use electrum_client::Client as ElectrumClient; +use bdk_chain::miniscript::ToPublicKey; +use bitcoin::hashes::Hash; use ldk_node::bitcoin::secp256k1::PublicKey; -use ldk_node::bitcoin::Amount; use ldk_node::lightning::ln::msgs::SocketAddress; -use ldk_node::{Builder, Event}; -use lightning_invoice::{Bolt11InvoiceDescription, Description}; -use lnd_grpc_rust::lnrpc::invoice::InvoiceState::Settled as LndInvoiceStateSettled; + use lnd_grpc_rust::lnrpc::{ - GetInfoRequest as LndGetInfoRequest, GetInfoResponse as LndGetInfoResponse, - Invoice as LndInvoice, ListInvoiceRequest as LndListInvoiceRequest, - QueryRoutesRequest as LndQueryRoutesRequest, Route as LndRoute, SendRequest as LndSendRequest, + channel_point, invoice::InvoiceState as LndInvoiceState, ChannelPoint, CloseChannelRequest, + GetInfoRequest as LndGetInfoRequest, Invoice as LndInvoice, + QueryRoutesRequest as LndQueryRoutesRequest, SendRequest as LndSendRequest, }; -use lnd_grpc_rust::{connect, LndClient}; -use tokio::fs; - -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn test_lnd() { - // Setup bitcoind / electrs clients - let bitcoind_client = BitcoindClient::new_with_auth( - "http://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(); +use lnd_grpc_rust::{connect, LndClient as ApiLndClient}; + +use bitcoin::hex::DisplayHex; + +use std::default::Default; +use std::str::FromStr; + +use crate::common::external_node::{ + do_ldk_opens_channel_full_cycle_with_external_node, init_bitcoind_client, ExternalLightningNode, +}; + +#[test] +fn test_ldk_initiates_channel_close() { + init_bitcoind_client(); + let mut client = LndClient::new(); + do_ldk_opens_channel_full_cycle_with_external_node(&mut client, None); +} + +#[test] +fn test_lnd_initiates_channel_close() { + init_bitcoind_client(); + let mut client = LndClient::new(); + do_ldk_opens_channel_full_cycle_with_external_node(&mut client, Some(false)); } -struct TestLndClient { - client: LndClient, +#[test] +fn test_lnd_initiates_force_channel_close() { + init_bitcoind_client(); + let mut client = LndClient::new(); + do_ldk_opens_channel_full_cycle_with_external_node(&mut client, Some(true)); } -impl TestLndClient { - async fn new(cert_path: String, macaroon_path: String, socket: String) -> Self { +struct LndClient { + client: ApiLndClient, + runtime: tokio::runtime::Runtime, + lnd_address: SocketAddress, +} + +impl LndClient { + fn new() -> Self { + let endpoint = String::from("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"); // 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"); + let cert_bytes = std::fs::read(cert_path).expect("Failed to read tls cert file"); + let mac_bytes = std::fs::read(macaroon_path).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"); + let runtime = tokio::runtime::Runtime::new().unwrap(); + let client = runtime.block_on(connect(cert, macaroon, endpoint)).unwrap(); - TestLndClient { client } + LndClient { client, runtime, lnd_address: "127.0.0.1:9735".parse().unwrap() } } - async fn get_node_info(&mut self) -> LndGetInfoResponse { + fn can_send_payments(&mut self, invoice: &str) { + let invoice = lightning_invoice::Bolt11Invoice::from_str(invoice) + .expect("Failed to parse invoice string into Bolt11Invoice"); + + // To ensure LND has updated its routes/balance after receiving payment, we attempt to query routes to ourselves + let available_to_spend_msat = invoice.amount_milli_satoshis().unwrap() as i64; + let node_id = if let Some(key) = invoice.payee_pub_key() { + key.to_string() + } else { + invoice.recover_payee_pub_key().to_string() + }; + // Wait up to 30 seconds for LND to update routes/balance after receiving payment; panic if timeout + let timeout = std::time::Duration::from_secs(30); + let start = std::time::Instant::now(); + loop { + let found = self + .runtime + .block_on(self.client.lightning().query_routes(LndQueryRoutesRequest { + pub_key: node_id.clone(), + amt_msat: available_to_spend_msat as i64, + ..Default::default() + })) + .ok() + .and_then(|resp| Some(!resp.into_inner().routes.is_empty())) + .unwrap_or(false); + + if found { + break; + } + if start.elapsed() > timeout { + panic!( + "LND did not update balance/routes after receiving payment within {} seconds", + timeout.as_secs() + ); + } + } + } +} + +impl ExternalLightningNode for LndClient { + fn get_node_info(&mut self) -> (PublicKey, SocketAddress) { let response = self - .client - .lightning() - .get_info(LndGetInfoRequest {}) - .await + .runtime + .block_on(self.client.lightning().get_info(LndGetInfoRequest {})) .expect("Failed to fetch node info from LND") .into_inner(); - - response + let pubkey = PublicKey::from_str(&response.identity_pubkey).unwrap(); + (pubkey, self.lnd_address.clone()) } - async fn create_invoice(&mut self, amount_msat: u64) -> String { + fn create_invoice(&mut self, amount_msat: u64, _description: Option) -> String { let invoice = LndInvoice { value_msat: amount_msat as i64, ..Default::default() }; - self.client - .lightning() - .add_invoice(invoice) - .await + self.runtime + .block_on(self.client.lightning().add_invoice(invoice)) .expect("Failed to create invoice on LND") .into_inner() .payment_request } - async fn list_invoices(&mut self) -> Vec { - 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, String> { - let request = LndQueryRoutesRequest { - pub_key: pubkey.to_string(), - amt_msat: amount_msat as i64, + fn pay_invoice(&mut self, invoice: &str) { + self.can_send_payments(invoice); + let send_req = LndSendRequest { + payment_request: invoice.to_string(), + fee_limit: Some(lnd_grpc_rust::lnrpc::FeeLimit { + limit: Some(lnd_grpc_rust::lnrpc::fee_limit::Limit::Fixed(10_000)), + }), ..Default::default() }; - let response = self - .client - .lightning() - .query_routes(request) - .await - .map_err(|err| format!("Failed to query routes from LND: {:?}", err))? + .runtime + .block_on(self.client.lightning().send_payment_sync(send_req)) + .expect("Failed to pay invoice on LND") .into_inner(); + if !response.payment_error.is_empty() || response.payment_preimage.is_empty() { + panic!("Failed to pay invoice on LND: {}", response.payment_error); + } + } + + fn check_receive_payment(&mut self, invoice: lightning_invoice::Bolt11Invoice) { + let payment_hash_bytes = invoice.payment_hash().as_byte_array().to_vec(); + + let payment_hash_req = + lnd_grpc_rust::lnrpc::PaymentHash { r_hash: payment_hash_bytes, ..Default::default() }; - if response.routes.is_empty() { - return Err(format!("No routes found for pubkey: {}", pubkey)); + let invoice_response = self + .runtime + .block_on(self.client.lightning().lookup_invoice(payment_hash_req)) + .expect("Failed to lookup invoice from LND") + .into_inner(); + + // Check that the invoice is payment settled + if invoice_response.state != LndInvoiceState::Settled as i32 { + panic!("Invoice not settled: expected Settled, found {:?}", invoice_response.state); } + } - Ok(response.routes) + fn close_channel(&mut self, funding_txo: bitcoin::OutPoint, _: PublicKey, force: bool) { + let request = CloseChannelRequest { + channel_point: Some(ChannelPoint { + funding_txid: Some(channel_point::FundingTxid::FundingTxidStr( + funding_txo.txid.to_string(), + )), + output_index: funding_txo.vout, + }), + force, + no_wait: true, + ..Default::default() + }; + + self.runtime + .block_on(self.client.lightning().close_channel(request)) + .expect("Failed to initiate close channel on LND"); } - 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") + fn create_new_address(&mut self) -> String { + let address = self + .runtime + .block_on(self.client.lightning().new_address( + lnd_grpc_rust::lnrpc::NewAddressRequest { + r#type: 0, // 0 = WITNESS_PUBKEY_HASH (bech32) + account: "".to_string(), + }, + )) + .expect("Failed to create new address") .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 - } - ); - } + address.address + } + + fn open_channel(&mut self, node_id: PublicKey, funding_amount_sat: u64) { + let open_channel_request = lnd_grpc_rust::lnrpc::OpenChannelRequest { + node_pubkey: node_id.to_public_key().to_bytes().to_vec(), + local_funding_amount: funding_amount_sat as i64, + sat_per_vbyte: 1, + ..Default::default() + }; + + let _stream = self + .runtime + .block_on(self.client.lightning().open_channel_sync(open_channel_request)) + .expect("Failed to open channel on LND"); } }