diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d3352fb1..d346bc80 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,8 +6,11 @@ on: pull_request: branches: ['*'] +env: + GO_VERSION: 1.21.0 + jobs: - build_and_test: + build_and_format: name: LNDK Rust Build runs-on: ubuntu-latest steps: @@ -20,11 +23,6 @@ jobs: with: command: build args: --release --all-features - - uses: actions-rs/cargo@v1 - name: cargo test - with: - command: test - args: --all-targets --benches -- --test-threads=1 - uses: actions-rs/cargo@v1 name: cargo fmt with: @@ -35,6 +33,29 @@ jobs: with: command: clippy args: -- --deny warnings + + ######################## + # run ubuntu integration tests + ######################## + ubuntu-integration-test: + name: run ubuntu unit and integration tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: 'true' + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + - name: setup go ${{ env.GO_VERSION }} + uses: lightningnetwork/lnd/.github/actions/setup-go@v0-16-4-branch + with: + go-version: '${{ env.GO_VERSION }}' + - name: run unit tests + run: cargo test --bin lndk + - name: run integration tests + run: make itest + coverage: name: LNDK Code Coverage runs-on: ubuntu-latest @@ -48,7 +69,7 @@ jobs: - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - name: Generate code coverage - run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info + run: cargo llvm-cov --bin lndk --workspace --lcov --output-path lcov.info - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..d4c6c614 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lnd"] + path = lnd + url = https://github.com/lightningnetwork/lnd diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fea97da7..3d22c68b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,8 +46,13 @@ cargo clippy 2. Error String: Lower case, no `.` terminating. 3. Display imps: Lower case, no `.` terminating. -### Integration tests +### Running tests -Note that the way the tests (both unit and integration tests) run now, it's best to run them sequentially rather than concurrently (which is Rust's default) to avoid port conflicts. You can run them sequentially with this command: +To run just the unit tests use the command: + +`cargo test --bin lndk` + +The integration tests require a Makefile to create an lnd binary. You'll need to [install Go](https://go.dev/doc/install) and then run them with this command: + +`make itest` -`cargo test -- --test-threads=1` diff --git a/Cargo.lock b/Cargo.lock index 4fd81a13..0378b8b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -328,7 +328,9 @@ checksum = "defd4e7873dbddba6c7c91e199c7fcb946abc4a6a4ac3195400bcfb01b5de877" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", "windows-targets", ] @@ -852,7 +854,7 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "ldk-sample" version = "0.1.0" -source = "git+https://github.com/lndk-org/ldk-sample?branch=pub-interface#07b56fd162b3f9907f805b6b38f06d529e4978bf" +source = "git+https://github.com/lndk-org/ldk-sample?branch=log-dir-tweak#614a87801b2d11326ac483dd65d2a47ab09614b9" dependencies = [ "base64 0.13.1", "bech32 0.8.1", @@ -973,6 +975,7 @@ dependencies = [ "bitcoin 0.29.2", "bitcoind", "bytes", + "chrono", "configure_me", "configure_me_codegen", "core-rpc", @@ -1975,8 +1978,7 @@ dependencies = [ [[package]] name = "tonic_lnd" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a207832efa21cc12bd0d520ce36554af91f5bbcc8873273bc1bab238b3365dbb" +source = "git+https://github.com/Kixunil/tonic_lnd?rev=fac4a67a8d4951d62fc020d61d38628c0064e6df#fac4a67a8d4951d62fc020d61d38628c0064e6df" dependencies = [ "hex 0.4.3", "prost 0.9.0", diff --git a/Cargo.toml b/Cargo.toml index d5895f9b..f51455ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ log = "0.4.17" simple_logger = "4.0.0" tokio = { version = "1.25.0", features = ["rt", "rt-multi-thread"] } tonic = "0.8.3" -tonic_lnd = "0.5.1" +tonic_lnd = { git = "https://github.com/Kixunil/tonic_lnd", rev = "fac4a67a8d4951d62fc020d61d38628c0064e6df" } hex = "0.4.3" configure_me = "0.4.0" bytes = "1.4.0" @@ -27,7 +27,8 @@ bytes = "1.4.0" [dev-dependencies] bitcoincore-rpc = { package="core-rpc", version = "0.17.0" } bitcoind = { version = "0.30.0", features = [ "22_0" ] } -ldk-sample = { git = "https://github.com/lndk-org/ldk-sample", branch = "pub-interface" } +chrono = { version = "0.4.26" } +ldk-sample = { git = "https://github.com/lndk-org/ldk-sample", branch = "log-dir-tweak" } mockall = "0.11.3" tempfile = "3.5.0" diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..11ca02b3 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +# This Makefile is used to compile an LND binary for the integration tests. + +GO_BUILD := go build +CARGO_TEST := cargo test +LND_PKG := github.com/lightningnetwork/lnd + +TMP_DIR := "/tmp" +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Darwin) + TMP_DIR=${TMPDIR} +endif + +itest: + @$(call print, "Building lnd for itests.") + git submodule update --init --recursive + cd lnd/cmd/lnd; $(GO_BUILD) -tags="peersrpc signrpc dev" -o $(TMP_DIR)/lndk-tests/bin/lnd-itest$(EXEC_SUFFIX) + $(CARGO_TEST) -- -- test '*' --test-threads=1 --nocapture + diff --git a/lnd b/lnd new file mode 160000 index 00000000..2fb150c8 --- /dev/null +++ b/lnd @@ -0,0 +1 @@ +Subproject commit 2fb150c8fe827df9df0520ef9916b3afb7b03a8d diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 67b36a41..22f18ee3 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,18 +1,45 @@ +mod test_utils; + use bitcoin::network::constants::Network; +use bitcoin::secp256k1::PublicKey; use bitcoincore_rpc::{bitcoin::Network as BitcoindNetwork, json, RpcApi}; -use bitcoind::{get_available_port, BitcoinD, Conf}; +use bitcoind::{get_available_port, BitcoinD, Conf, ConnectParams}; +use chrono::Utc; use ldk_sample::config::LdkUserInfo; use ldk_sample::node_api::Node as LdkNode; -use tempfile::{tempdir, TempDir}; +use std::net::SocketAddr; +use std::path::PathBuf; +use std::process::{Child, Command, Stdio}; +use std::thread; +use std::{env, fs}; +use tempfile::{tempdir, Builder, TempDir}; +use tokio::time::Duration; +use tonic_lnd::lnrpc::GetInfoRequest; +use tonic_lnd::Client; + +const LNDK_TESTS_FOLDER: &str = "lndk-tests"; + +pub async fn setup_test_infrastructure( + test_name: &str, +) -> (BitcoindNode, LndNode, LdkNode, LdkNode) { + let bitcoind = setup_bitcoind().await; + let (ldk_test_dir, lnd_test_dir) = setup_test_dirs(test_name); + let mut lnd = LndNode::new( + bitcoind.node.params.clone(), + bitcoind.zmq_block_port, + bitcoind.zmq_tx_port, + lnd_test_dir, + ); + lnd.setup_client().await; + + let connect_params = bitcoind.node.params.get_cookie_values().unwrap(); -pub async fn setup_test_infrastructure(test_name: &str) -> (BitcoinD, TempDir, LdkNode, LdkNode) { - let (bitcoind, bitcoind_dir) = setup_bitcoind().await; - let connect_params = bitcoind.params.get_cookie_values().unwrap(); let ldk1_config = LdkUserInfo { bitcoind_rpc_username: connect_params.0.clone().unwrap(), bitcoind_rpc_password: connect_params.1.clone().unwrap(), bitcoind_rpc_host: String::from("localhost"), - bitcoind_rpc_port: bitcoind.params.rpc_socket.port(), + bitcoind_rpc_port: bitcoind.node.params.rpc_socket.port(), + ldk_data_dir: ldk_test_dir.clone(), ldk_announced_listen_addr: Vec::new(), ldk_peer_listening_port: get_available_port().unwrap(), ldk_announced_node_name: [0; 32], @@ -23,7 +50,8 @@ pub async fn setup_test_infrastructure(test_name: &str) -> (BitcoinD, TempDir, L bitcoind_rpc_username: connect_params.0.unwrap(), bitcoind_rpc_password: connect_params.1.unwrap(), bitcoind_rpc_host: String::from("localhost"), - bitcoind_rpc_port: bitcoind.params.rpc_socket.port(), + bitcoind_rpc_port: bitcoind.node.params.rpc_socket.port(), + ldk_data_dir: ldk_test_dir, ldk_announced_listen_addr: Vec::new(), ldk_peer_listening_port: get_available_port().unwrap(), ldk_announced_node_name: [0; 32], @@ -33,18 +61,60 @@ pub async fn setup_test_infrastructure(test_name: &str) -> (BitcoinD, TempDir, L let ldk1 = ldk_sample::start_ldk(ldk1_config, test_name).await; let ldk2 = ldk_sample::start_ldk(ldk2_config, test_name).await; - (bitcoind, bitcoind_dir, ldk1, ldk2) + (bitcoind, lnd, ldk1, ldk2) +} + +// Sets up /tmp/lndk-tests folder where we'll store the bins, data directories, and logs needed +// for our tests. +// +// The file tree structure looks like: +// +// /tmp/lndk-tests +// | +// +-- /bin (compiled lnd binary is stored here) +// | +// +-- /test-{test_name}-{time-run} (each time you run a test a new folder will be +// | created with the data within) +// | +// +-- /lnd-data (lnd data and logs are stored here) +// | +// +-- /ldk-data (ldk data and logs are stored here) +// +fn setup_test_dirs(test_name: &str) -> (PathBuf, PathBuf) { + let lndk_tests_dir = env::temp_dir().join(LNDK_TESTS_FOLDER); + let bin_dir = lndk_tests_dir.join("bin"); + let now_timestamp = Utc::now(); + let timestamp = now_timestamp.format("%d-%m-%Y-%H%M"); + let itest_dir = lndk_tests_dir.join(format!("test-{test_name}-{timestamp}")); + let ldk_data_dir = itest_dir.join("ldk-data"); + let lnd_data_dir = itest_dir.join("lnd-data"); + + fs::create_dir_all(lndk_tests_dir.clone()).unwrap(); + fs::create_dir_all(bin_dir.clone()).unwrap(); + fs::create_dir_all(itest_dir.clone()).unwrap(); + fs::create_dir_all(ldk_data_dir.clone()).unwrap(); + fs::create_dir_all(lnd_data_dir.clone()).unwrap(); + + (ldk_data_dir, lnd_data_dir) } -pub async fn setup_bitcoind() -> (BitcoinD, TempDir) { - let bitcoind_dir = tempdir().unwrap(); - let bitcoind_dir_path = bitcoind_dir.path().clone().to_path_buf(); +// BitcoindNode holds the tools we need to interact with a Bitcoind node. +pub struct BitcoindNode { + node: BitcoinD, + _data_dir: TempDir, + zmq_block_port: u16, + zmq_tx_port: u16, +} + +pub async fn setup_bitcoind() -> BitcoindNode { + let data_dir = tempdir().unwrap(); + let data_dir_path = data_dir.path().to_path_buf(); let mut conf = Conf::default(); let zmq_block_port = get_available_port().unwrap(); let zmq_tx_port = get_available_port().unwrap(); let zmq_block_port_arg = &format!("-zmqpubrawblock=tcp://127.0.0.1:{zmq_block_port}"); let zmq_tx_port_arg = &format!("-zmqpubrawtx=tcp://127.0.0.1:{zmq_tx_port}"); - conf.tmpdir = Some(bitcoind_dir_path); + conf.tmpdir = Some(data_dir_path); conf.args = vec!["-regtest", zmq_block_port_arg, zmq_tx_port_arg]; let bitcoind = BitcoinD::from_downloaded_with_conf(&conf).unwrap(); @@ -57,5 +127,191 @@ pub async fn setup_bitcoind() -> (BitcoinD, TempDir) { let address = address.require_network(BitcoindNetwork::Regtest).unwrap(); bitcoind.client.generate_to_address(101, &address).unwrap(); - (bitcoind, bitcoind_dir) + BitcoindNode { + node: bitcoind, + _data_dir: data_dir, + zmq_block_port, + zmq_tx_port, + } +} + +// LndNode holds the tools we need to interact with a Lightning node. +pub struct LndNode { + address: String, + _lnd_dir_tmp: TempDir, + cert_path: String, + macaroon_path: String, + _handle: Child, + client: Option, +} + +impl LndNode { + fn new( + bitcoind_connect_params: ConnectParams, + zmq_block_port: u16, + zmq_tx_port: u16, + lnd_data_dir: PathBuf, + ) -> LndNode { + let lnd_exe_dir = env::temp_dir().join(LNDK_TESTS_FOLDER).join("bin"); + env::set_current_dir(lnd_exe_dir).expect("couldn't set current directory"); + + let lnd_dir_binding = Builder::new() + .prefix("lnd-data-") + .tempdir_in(lnd_data_dir.clone()) + .unwrap(); + let lnd_dir = lnd_dir_binding.path(); + + let macaroon_path = lnd_dir + .join("data/chain/bitcoin/regtest/admin.macaroon") + .to_str() + .unwrap() + .to_string(); + + let connect_params = bitcoind_connect_params.get_cookie_values().unwrap(); + let log_dir_path_buf = lnd_data_dir.join(format!("lnd-logs")); + let log_dir = log_dir_path_buf.as_path(); + let data_dir = lnd_dir.join("data").to_str().unwrap().to_string(); + let cert_path = lnd_dir.to_str().unwrap().to_string() + "/tls.cert"; + let key_path = lnd_dir.to_str().unwrap().to_string() + "/tls.key"; + + // Have node run on a randomly assigned grpc port. That way, if we run more than one lnd node, they won't + // clash. + let port = bitcoind::get_available_port().unwrap(); + let rpc_addr = format!("localhost:{}", port); + let lnd_port = bitcoind::get_available_port().unwrap(); + let lnd_addr = format!("localhost:{}", lnd_port); + let args = [ + format!("--listen={}", lnd_addr), + format!("--rpclisten={}", rpc_addr), + format!("--norest"), + // With this flag, we don't have to unlock the wallet on startup. + format!("--noseedbackup"), + format!("--bitcoin.active"), + format!("--bitcoin.node=bitcoind"), + format!("--bitcoin.regtest"), + format!("--datadir={}", data_dir), + format!("--tlscertpath={}", cert_path), + format!("--tlskeypath={}", key_path), + format!("--logdir={}", log_dir.display()), + format!("--bitcoind.rpcuser={}", connect_params.0.unwrap()), + format!("--bitcoind.rpcpass={}", connect_params.1.unwrap()), + format!( + "--bitcoind.zmqpubrawblock=tcp://127.0.0.1:{}", + zmq_block_port + ), + format!("--bitcoind.zmqpubrawtx=tcp://127.0.0.1:{}", zmq_tx_port), + format!( + "--bitcoind.rpchost={:?}", + bitcoind_connect_params.rpc_socket + ), + ]; + + // TODO: For Windows we might need to add ".exe" at the end. + let cmd = Command::new("./lnd-itest") + .args(args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("Failed to execute lnd command"); + + LndNode { + address: format!("https://{}", rpc_addr), + _lnd_dir_tmp: lnd_dir_binding, + cert_path: cert_path, + macaroon_path: macaroon_path, + _handle: cmd, + client: None, + } + } + + // Setup the client we need to interact with the LND node. + async fn setup_client(&mut self) { + // We need to give lnd some time to start up before we'll be able to interact with it via the client. + let mut retry = false; + let mut retry_num = 0; + while retry_num == 0 || retry { + thread::sleep(Duration::from_secs(3)); + + let client_result = tonic_lnd::connect( + self.address.clone(), + self.cert_path.clone(), + self.macaroon_path.clone(), + ) + .await; + + match client_result { + Ok(client) => { + self.client = Some(client); + + retry = false; + retry_num += 1; + } + Err(err) => { + println!( + "getting client error {err}, retrying call {} time", + retry_num + ); + if retry_num == 6 { + panic!("could not set up client: {err}") + } + retry = true; + retry_num += 1; + } + } + } + } + + #[allow(dead_code)] + pub async fn get_info(&mut self) -> tonic_lnd::lnrpc::GetInfoResponse { + let resp = if let Some(client) = self.client.clone() { + let get_info_req = GetInfoRequest {}; + let make_request = || async { + client + .clone() + .lightning() + .get_info(get_info_req.clone()) + .await + }; + let resp = test_utils::retry_async(make_request, String::from("get_info")); + resp.await.unwrap() + } else { + panic!("No client") + }; + + resp + } + + // connect_to_peer connects to the specified peer. + pub async fn connect_to_peer( + &mut self, + node_id: PublicKey, + addr: SocketAddr, + ) -> tonic_lnd::lnrpc::ConnectPeerResponse { + let ln_addr = tonic_lnd::lnrpc::LightningAddress { + pubkey: node_id.to_string(), + host: addr.to_string(), + }; + + let connect_req = tonic_lnd::lnrpc::ConnectPeerRequest { + addr: Some(ln_addr), + timeout: 20, + ..Default::default() + }; + + let resp = if let Some(client) = self.client.clone() { + let make_request = || async { + client + .clone() + .lightning() + .connect_peer(connect_req.clone()) + .await + }; + let resp = test_utils::retry_async(make_request, String::from("connect_peer")); + resp.await.unwrap() + } else { + panic!("No client") + }; + + resp + } } diff --git a/tests/common/test_utils.rs b/tests/common/test_utils.rs new file mode 100644 index 00000000..0412b853 --- /dev/null +++ b/tests/common/test_utils.rs @@ -0,0 +1,34 @@ +use std::fmt::Debug; +use std::future::Future; +use std::ops::FnMut; +use tokio::time::{sleep, Duration}; +use tonic_lnd::tonic::{Response, Status}; + +// If a grpc call returns an error, retry_async will retry the grpc function call in case lnd is still in +// the process of starting up. A note on implementation: We can't pass in a future directly to a function +// because futures cannot be cloned/copied in order to retry the future. Instead retry_async takes in an +// async closure that is able to "copy" the function for us so we can call it multiple times for retries. +pub(crate) async fn retry_async(mut f: F, func_name: String) -> Result +where + F: FnMut() -> Fut + std::marker::Copy, + Fut: Future, Status>>, + D: Debug, +{ + let mut retry_num = 0; + let resp = Err(()); + while retry_num < 3 { + sleep(Duration::from_secs(3)).await; + match f().await { + Err(_) => { + println!("retrying {} call", func_name.clone()); + retry_num += 1; + if retry_num == 5 { + panic!("{} call failed after 3 retries", func_name); + } + continue; + } + Ok(resp) => return Ok(resp.into_inner()), + }; + } + resp +} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index f65ad7f4..5cf85425 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -3,7 +3,7 @@ mod common; #[tokio::test(flavor = "multi_thread")] async fn test_ldk_send_onion_message() { let test_name = "send_onion_message"; - let (_bitcoind, _bitcoin_dir, ldk1, ldk2) = common::setup_test_infrastructure(test_name).await; + let (_bitcoind, _lnd, ldk1, ldk2) = common::setup_test_infrastructure(test_name).await; let (node_id_2, node_addr_2) = ldk2.get_node_info(); ldk1.connect_to_peer(node_id_2, node_addr_2).await.unwrap(); @@ -11,3 +11,11 @@ async fn test_ldk_send_onion_message() { let res = ldk1.send_onion_message(vec![node_id_2], 65, data).await; assert!(res.is_ok()); } + +#[tokio::test(flavor = "multi_thread")] +async fn test_ldk_lnd_connect() { + let test_name = "ldk_lnd_connect"; + let (_bitcoind, mut lnd, ldk1, _ldk2) = common::setup_test_infrastructure(test_name).await; + let (pubkey, addr) = ldk1.get_node_info(); + lnd.connect_to_peer(pubkey, addr).await; +}