diff --git a/e2e-tests/Cargo.lock b/e2e-tests/Cargo.lock index 13326fd..d9bc829 100644 --- a/e2e-tests/Cargo.lock +++ b/e2e-tests/Cargo.lock @@ -869,6 +869,7 @@ name = "e2e-tests" version = "0.1.0" dependencies = [ "corepc-node", + "electrsd", "futures-util", "hex-conservative", "lapin", @@ -876,6 +877,7 @@ dependencies = [ "ldk-server-client", "ldk-server-protos", "prost", + "rand 0.9.2", "serde_json", "tempfile", "tokio", @@ -887,6 +889,22 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "electrsd" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8926868af723c2819807809e54585992aaea0e26a6f5089ac8c2598eaec8d01" +dependencies = [ + "bitcoin_hashes", + "corepc-client", + "corepc-node", + "electrum-client", + "log", + "minreq", + "nix", + "zip", +] + [[package]] name = "electrum-client" version = "0.24.1" @@ -1526,7 +1544,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.2", "tokio", "tower-service", "tracing", @@ -2072,6 +2090,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -2143,6 +2170,20 @@ dependencies = [ "bitcoin", ] +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + [[package]] name = "nom" version = "7.1.3" @@ -2529,7 +2570,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls 0.23.36", - "socket2 0.5.10", + "socket2 0.6.2", "thiserror", "tokio", "tracing", @@ -2566,9 +2607,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] diff --git a/e2e-tests/Cargo.toml b/e2e-tests/Cargo.toml index 5576b7d..27aaef1 100644 --- a/e2e-tests/Cargo.toml +++ b/e2e-tests/Cargo.toml @@ -15,3 +15,5 @@ lapin = { version = "2.4.0", features = ["rustls"], default-features = false } prost = { version = "0.11.6", default-features = false, features = ["std"] } futures-util = "0.3" ldk-node = { git = "https://github.com/lightningdevkit/ldk-node", rev = "d1bbf978c8b7abe87ae2e40793556c1fe4e7ea49" } +electrsd = { version = "0.36", features = ["esplora_a33e97e1", "corepc-node_29_0"] } +rand = "0.9" diff --git a/e2e-tests/src/lib.rs b/e2e-tests/src/lib.rs index 8b34fd2..a7bc763 100644 --- a/e2e-tests/src/lib.rs +++ b/e2e-tests/src/lib.rs @@ -84,35 +84,54 @@ impl TestBitcoind { } } -/// Handle to a running ldk-server child process. -pub struct LdkServerHandle { - child: Option, +/// Wrapper around an electrsd process providing both Electrum and Esplora endpoints. +pub struct TestElectrs { + pub electrsd: electrsd::ElectrsD, +} + +impl TestElectrs { + /// Start an electrs instance connected to the given bitcoind with Esplora HTTP enabled. + pub fn new(bitcoind: &TestBitcoind) -> Self { + let mut conf = electrsd::Conf::default(); + conf.http_enabled = true; + let electrsd = + electrsd::ElectrsD::with_conf(electrsd::exe_path().unwrap(), &bitcoind.bitcoind, &conf) + .unwrap(); + Self { electrsd } + } + + pub fn electrum_url(&self) -> String { + // electrsd binds to 0.0.0.0 but that's not a connectable address for clients + self.electrsd.electrum_url.replace("0.0.0.0", "127.0.0.1") + } + + pub fn esplora_url(&self) -> String { + let url = self.electrsd.esplora_url.as_ref().expect("esplora not enabled"); + // electrsd binds to 0.0.0.0 but that's not a connectable address for clients + format!("http://{}", url.replace("0.0.0.0", "127.0.0.1")) + } + + /// Trigger electrs to sync with bitcoind. + pub fn trigger(&self) { + self.electrsd.trigger().unwrap(); + } +} + +/// Dynamic parameters available when building test configs. +pub struct TestServerParams { pub rest_port: u16, pub p2p_port: u16, pub storage_dir: PathBuf, - pub api_key: String, - pub tls_cert_path: PathBuf, - pub node_id: String, + pub rpc_address: String, + pub rpc_user: String, + pub rpc_password: String, pub exchange_name: String, - client: LdkServerClient, } -impl LdkServerHandle { - /// Starts a new ldk-server instance against the given bitcoind. - /// Waits until the server is ready to accept requests. - pub async fn start(bitcoind: &TestBitcoind) -> Self { - #[allow(deprecated)] - let storage_dir = tempfile::tempdir().unwrap().into_path(); - let rest_port = find_available_port(); - let p2p_port = find_available_port(); - - let (rpc_host, rpc_port_num, rpc_user, rpc_password) = bitcoind.rpc_details(); - let rpc_address = format!("{rpc_host}:{rpc_port_num}"); - - let exchange_name = format!("e2e_test_exchange_{rest_port}"); - - let config_content = format!( - r#"[node] +/// Generate a test config TOML with a custom chain source section. +pub fn test_config_with_chain_source(params: &TestServerParams, chain_source_toml: &str) -> String { + format!( + r#"[node] network = "regtest" listening_addresses = ["127.0.0.1:{p2p_port}"] rest_service_address = "127.0.0.1:{rest_port}" @@ -121,10 +140,7 @@ alias = "e2e-test-node" [storage.disk] dir_path = "{storage_dir}" -[bitcoind] -rpc_address = "{rpc_address}" -rpc_user = "{rpc_user}" -rpc_password = "{rpc_password}" +{chain_source} [rabbitmq] connection_string = "amqp://guest:guest@localhost:5672/%2f" @@ -141,21 +157,81 @@ min_payment_size_msat = 0 max_payment_size_msat = 1000000000 client_trusts_lsp = true "#, - storage_dir = storage_dir.display(), - ); + p2p_port = params.p2p_port, + rest_port = params.rest_port, + storage_dir = params.storage_dir.display(), + chain_source = chain_source_toml, + exchange_name = params.exchange_name, + ) +} + +/// Generate the default test config TOML with bitcoind RPC chain source. +pub fn default_test_config(params: &TestServerParams) -> String { + let chain_source = format!( + "[bitcoind]\nrpc_address = \"{}\"\nrpc_user = \"{}\"\nrpc_password = \"{}\"", + params.rpc_address, params.rpc_user, params.rpc_password + ); + test_config_with_chain_source(params, &chain_source) +} + +/// Handle to a running ldk-server child process. +pub struct LdkServerHandle { + child: Option, + pub rest_port: u16, + pub p2p_port: u16, + pub storage_dir: PathBuf, + pub api_key: String, + pub tls_cert_path: PathBuf, + pub node_id: String, + pub exchange_name: String, + client: LdkServerClient, + // Kept alive so the electrs process doesn't get dropped + _electrs: Option, +} - let config_path = storage_dir.join("config.toml"); - std::fs::write(&config_path, &config_content).unwrap(); +impl LdkServerHandle { + /// Starts a new ldk-server instance against the given bitcoind. + /// Randomly picks between bitcoind RPC, electrum, and esplora as the chain source. + pub async fn start(bitcoind: &TestBitcoind) -> Self { + match rand::random::() % 3 { + 0 => Self::start_with_config(bitcoind, default_test_config).await, + 1 => { + let electrs = TestElectrs::new(bitcoind); + let url = electrs.electrum_url(); + let mut handle = Self::start_with_config(bitcoind, move |params| { + test_config_with_chain_source( + params, + &format!("[electrum]\nserver_url = \"{}\"\nonchain_wallet_sync_interval_secs = 10\nlightning_wallet_sync_interval_secs = 10", url), + ) + }) + .await; + handle._electrs = Some(electrs); + handle + }, + 2 => { + let electrs = TestElectrs::new(bitcoind); + let url = electrs.esplora_url(); + let mut handle = Self::start_with_config(bitcoind, move |params| { + test_config_with_chain_source( + params, + &format!("[esplora]\nserver_url = \"{}\"\nonchain_wallet_sync_interval_secs = 10\nlightning_wallet_sync_interval_secs = 10", url), + ) + }) + .await; + handle._electrs = Some(electrs); + handle + }, + _ => unreachable!(), + } + } - let server_binary = server_binary_path(); - let mut child = Command::new(&server_binary) - .arg(config_path.to_str().unwrap()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .unwrap_or_else(|e| { - panic!("Failed to start ldk-server binary at {:?}: {}", server_binary, e) - }); + /// Starts a new ldk-server instance with a custom config. + /// The `config_fn` receives dynamic test parameters and returns the full TOML config string. + pub async fn start_with_config( + bitcoind: &TestBitcoind, config_fn: impl FnOnce(&TestServerParams) -> String, + ) -> Self { + let (mut child, params) = spawn_server(bitcoind, config_fn); + let TestServerParams { rest_port, p2p_port, storage_dir, exchange_name, .. } = params; // Spawn threads to forward stdout and stderr for debugging let stdout = child.stdout.take().unwrap(); @@ -204,6 +280,7 @@ client_trusts_lsp = true node_id: String::new(), exchange_name, client, + _electrs: None, }; // Wait for server to be ready and get node info @@ -235,6 +312,68 @@ impl Drop for LdkServerHandle { } } +/// Prepare test server params and spawn the ldk-server process. +fn spawn_server( + bitcoind: &TestBitcoind, config_fn: impl FnOnce(&TestServerParams) -> String, +) -> (Child, TestServerParams) { + #[allow(deprecated)] + let storage_dir = tempfile::tempdir().unwrap().into_path(); + let rest_port = find_available_port(); + let p2p_port = find_available_port(); + + let (rpc_host, rpc_port_num, rpc_user, rpc_password) = bitcoind.rpc_details(); + let rpc_address = format!("{rpc_host}:{rpc_port_num}"); + + let exchange_name = format!("e2e_test_exchange_{rest_port}"); + + let params = TestServerParams { + rest_port, + p2p_port, + storage_dir, + rpc_address, + rpc_user, + rpc_password, + exchange_name, + }; + + let config_content = config_fn(¶ms); + + let config_path = params.storage_dir.join("config.toml"); + std::fs::write(&config_path, &config_content).unwrap(); + + let server_binary = server_binary_path(); + let child = Command::new(&server_binary) + .arg(config_path.to_str().unwrap()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap_or_else(|e| { + panic!("Failed to start ldk-server binary at {:?}: {}", server_binary, e) + }); + + (child, params) +} + +/// Start ldk-server with the given config and expect it to fail (exit non-zero). +/// Returns the stderr output for assertion in tests. +pub fn start_expect_failure( + bitcoind: &TestBitcoind, config_fn: impl FnOnce(&TestServerParams) -> String, +) -> String { + let (child, ..) = spawn_server(bitcoind, config_fn); + + let output = child + .wait_with_output() + .unwrap_or_else(|e| panic!("Failed to wait for ldk-server process: {}", e)); + + assert!( + !output.status.success(), + "Expected server to fail but it exited with status: {}", + output.status + ); + + String::from_utf8_lossy(&output.stderr).to_string() +} + /// Find an available TCP port by binding to port 0. pub fn find_available_port() -> u16 { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); diff --git a/e2e-tests/tests/config.rs b/e2e-tests/tests/config.rs new file mode 100644 index 0000000..d5f4bcb --- /dev/null +++ b/e2e-tests/tests/config.rs @@ -0,0 +1,483 @@ +// 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. + +use e2e_tests::{ + default_test_config, start_expect_failure, test_config_with_chain_source, LdkServerHandle, + TestBitcoind, TestElectrs, +}; +use ldk_server_protos::api::GetNodeInfoRequest; + +fn remove_config_line(config: &str, key: &str) -> String { + config.lines().filter(|line| !line.trim_start().starts_with(key)).collect::>().join("\n") +} + +fn replace_config_line(config: &str, key: &str, new_line: &str) -> String { + config + .lines() + .map(|line| if line.trim_start().starts_with(key) { new_line } else { line }) + .collect::>() + .join("\n") +} + +fn remove_config_section(config: &str, section_header: &str) -> String { + let mut result = Vec::new(); + let mut skipping = false; + for line in config.lines() { + let trimmed = line.trim(); + if trimmed == section_header { + skipping = true; + continue; + } + if skipping && trimmed.starts_with('[') { + skipping = false; + } + if !skipping { + result.push(line); + } + } + result.join("\n") +} + +#[tokio::test] +async fn test_config_no_alias() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + remove_config_line(&default_test_config(params), "alias =") + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_no_listening_addresses() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let config = remove_config_line(&default_test_config(params), "listening_addresses ="); + // Alias requires listening addresses for announcement, so remove it too + remove_config_line(&config, "alias =") + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_multiple_listening_addresses() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let extra_port = e2e_tests::find_available_port(); + replace_config_line( + &default_test_config(params), + "listening_addresses =", + &format!( + "listening_addresses = [\"127.0.0.1:{}\", \"127.0.0.1:{}\"]", + params.p2p_port, extra_port + ), + ) + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_with_announcement_addresses() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let mut config = default_test_config(params); + // Insert announcement_addresses after alias line + config = config.replace( + "alias = \"e2e-test-node\"", + &format!( + "alias = \"e2e-test-node\"\nannouncement_addresses = [\"127.0.0.1:{}\"]", + params.p2p_port + ), + ); + config + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_log_level_trace() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let mut config = default_test_config(params); + config.push_str("\n[log]\nlevel = \"Trace\"\n"); + config + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_log_level_error() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let mut config = default_test_config(params); + config.push_str("\n[log]\nlevel = \"Error\"\n"); + config + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_log_level_warn() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let mut config = default_test_config(params); + config.push_str("\n[log]\nlevel = \"Warn\"\n"); + config + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_with_log_file() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let log_path = format!("{}/ldk-server.log", params.storage_dir.display()); + let mut config = default_test_config(params); + config.push_str(&format!("\n[log]\nlevel = \"Debug\"\nfile = \"{}\"\n", log_path)); + config + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_with_tls_hosts() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let mut config = default_test_config(params); + config.push_str("\n[tls]\nhosts = [\"example.com\", \"ldk-server.local\"]\n"); + config + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_lsps2_advertise_service() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + replace_config_line( + &default_test_config(params), + "advertise_service =", + "advertise_service = true", + ) + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_lsps2_with_require_token() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let mut config = default_test_config(params); + config = config.replace( + "client_trusts_lsp = true", + "client_trusts_lsp = true\nrequire_token = \"secret-token-123\"", + ); + config + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_lsps2_high_fees() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let config = default_test_config(params); + let config = replace_config_line( + &config, + "channel_opening_fee_ppm =", + "channel_opening_fee_ppm = 50000", + ); + let config = replace_config_line( + &config, + "min_channel_opening_fee_msat =", + "min_channel_opening_fee_msat = 10000000", + ); + let config = replace_config_line( + &config, + "channel_over_provisioning_ppm =", + "channel_over_provisioning_ppm = 500000", + ); + config + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_lsps2_restrictive_limits() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + let config = default_test_config(params); + let config = replace_config_line( + &config, + "min_payment_size_msat =", + "min_payment_size_msat = 10000000", + ); + let config = replace_config_line( + &config, + "max_payment_size_msat =", + "max_payment_size_msat = 100000000", + ); + let config = + replace_config_line(&config, "min_channel_lifetime =", "min_channel_lifetime = 4320"); + let config = replace_config_line( + &config, + "max_client_to_self_delay =", + "max_client_to_self_delay = 256", + ); + config + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_lsps2_client_trusts_lsp_false() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + replace_config_line( + &default_test_config(params), + "client_trusts_lsp =", + "client_trusts_lsp = false", + ) + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[test] +fn test_config_fail_missing_network() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + remove_config_line(&default_test_config(params), "network =") + }); + assert!(stderr.contains("Missing `network`"), "Unexpected stderr: {stderr}"); +} + +#[test] +fn test_config_fail_missing_rest_service_address() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + remove_config_line(&default_test_config(params), "rest_service_address =") + }); + assert!(stderr.contains("Missing `rest_service_address`"), "Unexpected stderr: {stderr}"); +} + +#[test] +fn test_config_fail_missing_rpc_address() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + remove_config_line(&default_test_config(params), "rpc_address =") + }); + assert!(stderr.contains("Missing `bitcoind_rpc_address`"), "Unexpected stderr: {stderr}"); +} + +#[test] +fn test_config_fail_missing_rpc_user() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + remove_config_line(&default_test_config(params), "rpc_user =") + }); + assert!(stderr.contains("Missing `bitcoind_rpc_user`"), "Unexpected stderr: {stderr}"); +} + +#[test] +fn test_config_fail_missing_rpc_password() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + remove_config_line(&default_test_config(params), "rpc_password =") + }); + assert!(stderr.contains("Missing `bitcoind_rpc_password`"), "Unexpected stderr: {stderr}"); +} + +#[test] +fn test_config_fail_multiple_chain_sources() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + let mut config = default_test_config(params); + config.push_str("\n[esplora]\nserver_url = \"https://mempool.space/api\"\n"); + config + }); + assert!(stderr.contains("Must set a single chain source"), "Unexpected stderr: {stderr}"); +} + +#[test] +fn test_config_fail_invalid_rest_service_address() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + replace_config_line( + &default_test_config(params), + "rest_service_address =", + "rest_service_address = \"not-a-valid-address\"", + ) + }); + assert!(stderr.contains("Invalid configuration"), "Unexpected stderr: {stderr}"); +} + +#[test] +fn test_config_fail_invalid_listening_address() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + replace_config_line( + &default_test_config(params), + "listening_addresses =", + "listening_addresses = [\"definitely not an address\"]", + ) + }); + assert!(stderr.contains("Invalid listening addresses"), "Unexpected stderr: {stderr}"); +} + +#[test] +fn test_config_fail_alias_too_long() { + let bitcoind = TestBitcoind::new(); + let long_alias = "a".repeat(33); + let stderr = start_expect_failure(&bitcoind, |params| { + replace_config_line( + &default_test_config(params), + "alias =", + &format!("alias = \"{}\"", long_alias), + ) + }); + assert!(stderr.contains("alias") && stderr.contains("32 bytes"), "Unexpected stderr: {stderr}"); +} + +#[test] +fn test_config_fail_invalid_log_level() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + let mut config = default_test_config(params); + config.push_str("\n[log]\nlevel = \"NotALevel\"\n"); + config + }); + assert!( + stderr.contains("Invalid log level") || stderr.contains("Invalid configuration"), + "Unexpected stderr: {stderr}" + ); +} + +#[test] +fn test_config_fail_missing_rabbitmq() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + remove_config_section(&default_test_config(params), "[rabbitmq]") + }); + assert!(stderr.contains("rabbitmq"), "Unexpected stderr: {stderr}"); +} + +#[test] +fn test_config_fail_missing_lsps2() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + remove_config_section(&default_test_config(params), "[liquidity.lsps2_service]") + }); + assert!( + stderr.contains("lsps2") || stderr.contains("liquidity"), + "Unexpected stderr: {stderr}" + ); +} + +#[test] +fn test_config_fail_invalid_toml() { + let bitcoind = TestBitcoind::new(); + let stderr = + start_expect_failure(&bitcoind, |_params| "this is not valid [[ toml {{{{".to_string()); + assert!( + stderr.contains("invalid TOML") || stderr.contains("Invalid configuration"), + "Unexpected stderr: {stderr}" + ); +} + +#[test] +fn test_config_fail_alias_without_listening_addresses() { + let bitcoind = TestBitcoind::new(); + let stderr = start_expect_failure(&bitcoind, |params| { + remove_config_line(&default_test_config(params), "listening_addresses =") + }); + assert!( + stderr.contains("Listening addresses") || stderr.contains("listening addresses"), + "Unexpected stderr: {stderr}" + ); +} + +#[tokio::test] +async fn test_config_chain_source_bitcoind_localhost() { + let bitcoind = TestBitcoind::new(); + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + // Use "localhost:port" instead of "127.0.0.1:port" to test hostname RPC support + let rpc_address = params.rpc_address.replace("127.0.0.1", "localhost"); + test_config_with_chain_source( + params, + &format!( + "[bitcoind]\nrpc_address = \"{}\"\nrpc_user = \"{}\"\nrpc_password = \"{}\"", + rpc_address, params.rpc_user, params.rpc_password + ), + ) + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_chain_source_esplora() { + let bitcoind = TestBitcoind::new(); + let electrs = TestElectrs::new(&bitcoind); + let esplora_url = electrs.esplora_url(); + + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + test_config_with_chain_source( + params, + &format!("[esplora]\nserver_url = \"{}\"", esplora_url), + ) + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} + +#[tokio::test] +async fn test_config_chain_source_electrum() { + let bitcoind = TestBitcoind::new(); + let electrs = TestElectrs::new(&bitcoind); + let electrum_url = electrs.electrum_url(); + + let server = LdkServerHandle::start_with_config(&bitcoind, |params| { + test_config_with_chain_source( + params, + &format!("[electrum]\nserver_url = \"{}\"", electrum_url), + ) + }) + .await; + let info = server.client().get_node_info(GetNodeInfoRequest {}).await.unwrap(); + assert!(info.current_best_block.is_some()); +} diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index 0b4460c..5d07968 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -23,7 +23,7 @@ use hex::DisplayHex; use hyper::server::conn::http1; use hyper_util::rt::TokioIo; use ldk_node::bitcoin::Network; -use ldk_node::config::Config; +use ldk_node::config::{BackgroundSyncConfig, Config, ElectrumSyncConfig, EsploraSyncConfig}; use ldk_node::entropy::NodeEntropy; use ldk_node::lightning::ln::channelmanager::PaymentId; use ldk_node::{Builder, Event, Node}; @@ -72,6 +72,24 @@ pub fn get_default_data_dir() -> Option { } } +fn build_background_sync_config( + onchain_wallet_sync_interval_secs: Option, + lightning_wallet_sync_interval_secs: Option, +) -> Option { + if onchain_wallet_sync_interval_secs.is_none() && lightning_wallet_sync_interval_secs.is_none() + { + return None; + } + let mut bg = BackgroundSyncConfig::default(); + if let Some(interval) = onchain_wallet_sync_interval_secs { + bg.onchain_wallet_sync_interval_secs = interval; + } + if let Some(interval) = lightning_wallet_sync_interval_secs { + bg.lightning_wallet_sync_interval_secs = interval; + } + Some(bg) +} + fn main() { let args_config = ArgsConfig::parse(); @@ -155,11 +173,32 @@ fn main() { ChainSource::Rpc { rpc_host, rpc_port, rpc_user, rpc_password } => { builder.set_chain_source_bitcoind_rpc(rpc_host, rpc_port, rpc_user, rpc_password); }, - ChainSource::Electrum { server_url } => { - builder.set_chain_source_electrum(server_url, None); + ChainSource::Electrum { + server_url, + onchain_wallet_sync_interval_secs, + lightning_wallet_sync_interval_secs, + } => { + let sync_config = build_background_sync_config( + onchain_wallet_sync_interval_secs, + lightning_wallet_sync_interval_secs, + ) + .map(|bg| ElectrumSyncConfig { + background_sync_config: Some(bg), + ..Default::default() + }); + builder.set_chain_source_electrum(server_url, sync_config); }, - ChainSource::Esplora { server_url } => { - builder.set_chain_source_esplora(server_url, None); + ChainSource::Esplora { + server_url, + onchain_wallet_sync_interval_secs, + lightning_wallet_sync_interval_secs, + } => { + let sync_config = build_background_sync_config( + onchain_wallet_sync_interval_secs, + lightning_wallet_sync_interval_secs, + ) + .map(|bg| EsploraSyncConfig { background_sync_config: Some(bg), ..Default::default() }); + builder.set_chain_source_esplora(server_url, sync_config); }, } diff --git a/ldk-server/src/util/config.rs b/ldk-server/src/util/config.rs index 43fa5f7..7e7bba2 100644 --- a/ldk-server/src/util/config.rs +++ b/ldk-server/src/util/config.rs @@ -62,9 +62,22 @@ pub struct TlsConfig { #[derive(Debug, PartialEq, Eq)] pub enum ChainSource { - Rpc { rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String }, - Electrum { server_url: String }, - Esplora { server_url: String }, + Rpc { + rpc_host: String, + rpc_port: u16, + rpc_user: String, + rpc_password: String, + }, + Electrum { + server_url: String, + onchain_wallet_sync_interval_secs: Option, + lightning_wallet_sync_interval_secs: Option, + }, + Esplora { + server_url: String, + onchain_wallet_sync_interval_secs: Option, + lightning_wallet_sync_interval_secs: Option, + }, } /// A builder for `Config`. @@ -79,6 +92,8 @@ struct ConfigBuilder { storage_dir_path: Option, electrum_url: Option, esplora_url: Option, + onchain_wallet_sync_interval_secs: Option, + lightning_wallet_sync_interval_secs: Option, bitcoind_rpc_address: Option, bitcoind_rpc_user: Option, bitcoind_rpc_password: Option, @@ -116,10 +131,22 @@ impl ConfigBuilder { if let Some(electrum) = toml.electrum { self.electrum_url = Some(electrum.server_url); + self.onchain_wallet_sync_interval_secs = electrum + .onchain_wallet_sync_interval_secs + .or(self.onchain_wallet_sync_interval_secs); + self.lightning_wallet_sync_interval_secs = electrum + .lightning_wallet_sync_interval_secs + .or(self.lightning_wallet_sync_interval_secs); } if let Some(esplora) = toml.esplora { self.esplora_url = Some(esplora.server_url); + self.onchain_wallet_sync_interval_secs = esplora + .onchain_wallet_sync_interval_secs + .or(self.onchain_wallet_sync_interval_secs); + self.lightning_wallet_sync_interval_secs = esplora + .lightning_wallet_sync_interval_secs + .or(self.lightning_wallet_sync_interval_secs); } if let Some(log) = toml.log { @@ -269,9 +296,17 @@ impl ConfigBuilder { ChainSource::Rpc { rpc_host, rpc_port, rpc_user, rpc_password } } else if let Some(url) = self.electrum_url { - ChainSource::Electrum { server_url: url } + ChainSource::Electrum { + server_url: url, + onchain_wallet_sync_interval_secs: self.onchain_wallet_sync_interval_secs, + lightning_wallet_sync_interval_secs: self.lightning_wallet_sync_interval_secs, + } } else if let Some(url) = self.esplora_url { - ChainSource::Esplora { server_url: url } + ChainSource::Esplora { + server_url: url, + onchain_wallet_sync_interval_secs: self.onchain_wallet_sync_interval_secs, + lightning_wallet_sync_interval_secs: self.lightning_wallet_sync_interval_secs, + } } else { return Err(io::Error::new(io::ErrorKind::InvalidInput, "No valid Chain Source configured. Provide Bitcoind RPC, Electrum, or Esplora details.")); }; @@ -391,11 +426,15 @@ struct BitcoindConfig { #[derive(Deserialize, Serialize)] struct ElectrumConfig { server_url: String, + onchain_wallet_sync_interval_secs: Option, + lightning_wallet_sync_interval_secs: Option, } #[derive(Deserialize, Serialize)] struct EsploraConfig { server_url: String, + onchain_wallet_sync_interval_secs: Option, + lightning_wallet_sync_interval_secs: Option, } #[derive(Deserialize, Serialize)] @@ -805,7 +844,7 @@ mod tests { fs::write(storage_path.join(config_file_name), toml_config).unwrap(); let config = load_config(&args_config).unwrap(); - let ChainSource::Electrum { server_url } = config.chain_source else { + let ChainSource::Electrum { server_url, .. } = config.chain_source else { panic!("unexpected chain source"); };