From 7ff3c7e03383b598787228ddce85633414c5531c Mon Sep 17 00:00:00 2001 From: Alexander Shevtsov Date: Mon, 9 Mar 2026 15:41:21 +0100 Subject: [PATCH 1/2] Add builders' stub for bip157 sync Introduce new backend for syncing via compact filters (bip157). Create new chain source which start kyoto under the hood. Fees retrieval has two options: either esplora/electrum or a rough estimate via the latest block from kyoto. Test bitcoind node is run with filter flags. --- Cargo.toml | 2 +- src/builder.rs | 70 +++++- src/chain/electrum.rs | 2 +- src/chain/kyoto.rs | 400 ++++++++++++++++++++++++++++++ src/chain/mod.rs | 65 +++++ src/config.rs | 2 +- tests/common/mod.rs | 22 +- tests/integration_tests_bip157.rs | 144 +++++++++++ 8 files changed, 701 insertions(+), 6 deletions(-) create mode 100644 src/chain/kyoto.rs create mode 100644 tests/integration_tests_bip157.rs diff --git a/Cargo.toml b/Cargo.toml index 18947b72f..da9d117d2 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,7 @@ serde_json = { version = "1.0.128", default-features = false, features = ["std"] log = { version = "0.4.22", default-features = false, features = ["std"]} async-trait = { version = "0.1", default-features = false } +bip157 = "0.3.4" vss-client = { package = "vss-client-ng", version = "0.5" } prost = { version = "0.11.6", default-features = false} #bitcoin-payment-instructions = { version = "0.6" } @@ -91,7 +92,6 @@ proptest = "1.0.0" regex = "1.5.6" criterion = { version = "0.7.0", features = ["async_tokio"] } ldk-node-062 = { package = "ldk-node", version = "=0.6.2" } - [target.'cfg(not(no_download))'.dev-dependencies] electrsd = { version = "0.36.1", default-features = false, features = ["legacy", "esplora_a33e97e1", "corepc-node_27_2"] } diff --git a/src/builder.rs b/src/builder.rs index 7641a767d..4e59f8363 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -42,7 +42,7 @@ use lightning::util::sweep::OutputSweeper; use lightning_persister::fs_store::v1::FilesystemStore; use vss_client::headers::VssHeaderProvider; -use crate::chain::ChainSource; +use crate::chain::{ChainSource, FeeSourceConfig}; use crate::config::{ default_user_config, may_announce_channel, AnnounceError, AsyncPaymentsRole, BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, @@ -105,6 +105,9 @@ enum ChainDataSourceConfig { rpc_password: String, rest_client_config: Option, }, + CompactBlockFilter { + peers: Vec, + }, } #[derive(Debug, Clone)] @@ -240,6 +243,7 @@ impl std::error::Error for BuildError {} pub struct NodeBuilder { config: Config, chain_data_source_config: Option, + fee_source_config: Option, gossip_source_config: Option, liquidity_source_config: Option, log_writer_config: Option, @@ -259,6 +263,7 @@ impl NodeBuilder { /// Creates a new builder instance from an [`Config`]. pub fn from_config(config: Config) -> Self { let chain_data_source_config = None; + let fee_source_config = None; let gossip_source_config = None; let liquidity_source_config = None; let log_writer_config = None; @@ -268,6 +273,7 @@ impl NodeBuilder { Self { config, chain_data_source_config, + fee_source_config, gossip_source_config, liquidity_source_config, log_writer_config, @@ -352,6 +358,40 @@ impl NodeBuilder { self } + /// Configures the [`Node`] instance to source its chain data via BIP157 compact block filters. + /// + /// The given `peers` will be used as seed peers for the compact block filter node. An empty + /// list is valid for standard networks (mainnet, testnet, signet) where DNS seeds are used + /// for peer discovery. For custom networks (e.g. custom signets), at least one peer must be + /// provided. + pub fn set_chain_source_bip157(&mut self, peers: Vec) -> &mut Self { + self.chain_data_source_config = + Some(ChainDataSourceConfig::CompactBlockFilter { peers }); + self + } + + /// Configures the BIP-157 chain source to use an Esplora server for fee rate estimation. + /// + /// By default the BIP-157 backend derives fee rates from the latest block's coinbase output. + /// Setting this provides more accurate, per-target fee estimates from a mempool-aware server. + /// + /// Only takes effect when the chain source is set to BIP-157 via [`Self::set_chain_source_bip157`]. + pub fn set_fee_source_esplora(&mut self, server_url: String) -> &mut Self { + self.fee_source_config = Some(FeeSourceConfig::Esplora(server_url)); + self + } + + /// Configures the BIP-157 chain source to use an Electrum server for fee rate estimation. + /// + /// By default the BIP-157 backend derives fee rates from the latest block's coinbase output. + /// Setting this provides more accurate, per-target fee estimates from a mempool-aware server. + /// + /// Only takes effect when the chain source is set to BIP-157 via [`Self::set_chain_source_bip157`]. + pub fn set_fee_source_electrum(&mut self, server_url: String) -> &mut Self { + self.fee_source_config = Some(FeeSourceConfig::Electrum(server_url)); + self + } + /// Configures the [`Node`] instance to synchronize chain data from a Bitcoin Core REST endpoint. /// /// This method enables chain data synchronization via Bitcoin Core's REST interface. We pass @@ -727,6 +767,7 @@ impl NodeBuilder { build_with_store_internal( config, self.chain_data_source_config.as_ref(), + self.fee_source_config.clone(), self.gossip_source_config.as_ref(), self.liquidity_source_config.as_ref(), self.pathfinding_scores_sync_config.as_ref(), @@ -847,6 +888,21 @@ impl ArcedNodeBuilder { ); } + /// Configures the [`Node`] instance to synchronize chain data via BIP-157 compact block filters. + pub fn set_chain_source_bip157(&self, peers: Vec) { + self.inner.write().unwrap().set_chain_source_bip157(peers); + } + + /// Configures the BIP-157 chain source to use an Esplora server for fee rate estimation. + pub fn set_fee_source_esplora(&self, server_url: String) { + self.inner.write().unwrap().set_fee_source_esplora(server_url); + } + + /// Configures the BIP-157 chain source to use an Electrum server for fee rate estimation. + pub fn set_fee_source_electrum(&self, server_url: String) { + self.inner.write().unwrap().set_fee_source_electrum(server_url); + } + /// Configures the [`Node`] instance to source its gossip data from the Lightning peer-to-peer /// network. pub fn set_gossip_source_p2p(&self) { @@ -1121,7 +1177,7 @@ impl ArcedNodeBuilder { /// Builds a [`Node`] instance according to the options previously configured. fn build_with_store_internal( config: Arc, chain_data_source_config: Option<&ChainDataSourceConfig>, - gossip_source_config: Option<&GossipSourceConfig>, + fee_source_config: Option, gossip_source_config: Option<&GossipSourceConfig>, liquidity_source_config: Option<&LiquiditySourceConfig>, pathfinding_scores_sync_config: Option<&PathfindingScoresSyncConfig>, async_payments_role: Option, recovery_mode: bool, seed_bytes: [u8; 64], @@ -1254,6 +1310,16 @@ fn build_with_store_internal( .await }), }, + Some(ChainDataSourceConfig::CompactBlockFilter { peers }) => ChainSource::new_kyoto( + peers.clone(), + fee_source_config, + Arc::clone(&fee_estimator), + Arc::clone(&tx_broadcaster), + Arc::clone(&kv_store), + Arc::clone(&config), + Arc::clone(&logger), + Arc::clone(&node_metrics), + ), None => { // Default to Esplora client. diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 7b08c3845..b1abd4659 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -37,7 +37,7 @@ use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::NodeMetrics; const BDK_ELECTRUM_CLIENT_BATCH_SIZE: usize = 5; -const ELECTRUM_CLIENT_NUM_RETRIES: u8 = 3; +pub(crate) const ELECTRUM_CLIENT_NUM_RETRIES: u8 = 3; pub(super) struct ElectrumChainSource { server_url: String, diff --git a/src/chain/kyoto.rs b/src/chain/kyoto.rs new file mode 100644 index 000000000..fd3a4632a --- /dev/null +++ b/src/chain/kyoto.rs @@ -0,0 +1,400 @@ +// 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 std::collections::HashMap; +use std::path::PathBuf; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use bitcoin::{BlockHash, FeeRate, Network, Transaction}; +use bip157::Info; +use electrum_client::{Batch, Client as ElectrumClient, ConfigBuilder as ElectrumConfigBuilder, ElectrumApi}; +use esplora_client::AsyncClient as EsploraAsyncClient; +use tokio::sync::mpsc; + +use super::FeeSourceConfig; +use crate::chain::electrum::ELECTRUM_CLIENT_NUM_RETRIES; +use crate::config::{ + Config, DEFAULT_FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, DEFAULT_PER_REQUEST_TIMEOUT_SECS, +}; +use crate::fee_estimator::{ + apply_post_estimation_adjustments, get_all_conf_targets, get_num_block_defaults_for_target, + OnchainFeeEstimator, +}; +use crate::io::utils::write_node_metrics; +use crate::logger::{log_debug, log_error, log_info, LdkLogger, Logger}; +use crate::runtime::Runtime; +use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; +use crate::{Error, NodeMetrics}; + +/// How long to wait for kyoto to establish the required peer connections before +/// considering the startup failed. +const KYOTO_CONNECTION_TIMEOUT: Duration = Duration::from_secs(10); + +/// The fee estimation back-end used by a BIP-157 chain source. +enum FeeSource { + /// Derive fee rates from the coinbase reward of the most-recently seen block. + /// + /// Provides a single average rate applied uniformly across all confirmation targets. + /// Less accurate than a mempool-aware source but requires no extra connectivity. + Kyoto, + /// Delegate fee estimation to an Esplora HTTP server. + Esplora { client: EsploraAsyncClient, timeout_secs: u64 }, + /// Delegate fee estimation to an Electrum server. + /// + /// A fresh connection is opened for each estimation cycle; the server URL is stored + /// rather than the client because `ElectrumClient` is not `Sync`. + Electrum { server_url: String }, +} + +pub(super) struct KyotoChainSource { + /// The kyoto node, held until `start()` spawns it. + node: Mutex>, + /// Info channel, held until `start()` uses it to wait for initial connections. + info_rx: Mutex>>, + /// The remaining client channels, held until `continuously_sync_wallets()` takes them. + warn_rx: Mutex>>, + event_rx: Mutex>>, + /// Cloneable handle used to send commands (e.g. shutdown) to the running node. + requester: bip157::Requester, + /// The most recently observed chain tip block hash, updated by `FiltersSynced` events. + /// Only used when `fee_source` is [`FeeSource::Kyoto`]. + tip_hash: Mutex>, + fee_source: FeeSource, + fee_estimator: Arc, + kv_store: Arc, + config: Arc, + logger: Arc, + node_metrics: Arc>, +} + +impl KyotoChainSource { + pub(super) fn new( + peers: Vec, fee_source_config: Option, + fee_estimator: Arc, kv_store: Arc, config: Arc, + logger: Arc, node_metrics: Arc>, + ) -> Self { + let data_dir = PathBuf::from(&config.storage_dir_path).join("kyoto"); + let (node, client) = bip157::Builder::new(config.network) + .add_peers(peers.into_iter().map(Into::into)) + .data_dir(data_dir) + .build(); + let bip157::Client { requester, info_rx, warn_rx, event_rx } = client; + + let fee_source = match fee_source_config { + Some(FeeSourceConfig::Esplora(url)) => { + let esplora_client = esplora_client::Builder::new(&url) + .timeout(DEFAULT_PER_REQUEST_TIMEOUT_SECS as u64) + .build_async() + .unwrap(); + FeeSource::Esplora { + client: esplora_client, + timeout_secs: DEFAULT_FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, + } + }, + Some(FeeSourceConfig::Electrum(server_url)) => FeeSource::Electrum { server_url }, + None => FeeSource::Kyoto, + }; + + Self { + node: Mutex::new(Some(node)), + info_rx: Mutex::new(Some(info_rx)), + warn_rx: Mutex::new(Some(warn_rx)), + event_rx: Mutex::new(Some(event_rx)), + requester, + tip_hash: Mutex::new(None), + fee_source, + fee_estimator, + kv_store, + config, + logger, + node_metrics, + } + } + + /// Spawn the kyoto node task, then block until it connects to the required number of peers. + /// + /// Returns `Err(Error::ConnectionFailed)` if no connection is established within + /// [`KYOTO_CONNECTION_TIMEOUT`] or if the node exits before connecting. + pub(super) fn start(&self, runtime: Arc) -> Result<(), Error> { + let node = self.node.lock().unwrap().take().expect("kyoto node already started"); + let mut info_rx = + self.info_rx.lock().unwrap().take().expect("kyoto info_rx already taken"); + let logger = Arc::clone(&self.logger); + + runtime.spawn_background_task(async move { + if let Err(e) = node.run().await { + log_error!(logger, "Kyoto node exited with error: {}", e); + } + }); + + // Block until kyoto connects to the required number of peers or we time out. + runtime.block_on(async { + let result = tokio::time::timeout(KYOTO_CONNECTION_TIMEOUT, async { + while let Some(info) = info_rx.recv().await { + match info { + Info::ConnectionsMet => return Ok(()), + _ => { + log_debug!(self.logger, "Kyoto: {}", info); + }, + } + } + // Channel closed — node exited before connecting. + Err(Error::ConnectionFailed) + }) + .await; + + match result { + Ok(res) => res, + Err(_timeout) => { + log_error!( + self.logger, + "Kyoto failed to connect to peers within {}s.", + KYOTO_CONNECTION_TIMEOUT.as_secs() + ); + Err(Error::ConnectionFailed) + }, + } + }) + } + + /// Signal the kyoto node to shut down. Called during `Node::stop()`. + pub(super) fn stop(&self) { + let _ = self.requester.shutdown(); + } + + /// Main sync loop driven by kyoto events. Runs for the lifetime of the node. + pub(super) async fn continuously_sync_wallets( + &self, mut stop_sync_receiver: tokio::sync::watch::Receiver<()>, + _onchain_wallet: Arc, _channel_manager: Arc, + _chain_monitor: Arc, _output_sweeper: Arc, + ) { + let mut warn_rx = + self.warn_rx.lock().unwrap().take().expect("kyoto warn_rx already consumed"); + let mut event_rx = + self.event_rx.lock().unwrap().take().expect("kyoto event_rx already consumed"); + + loop { + tokio::select! { + _ = stop_sync_receiver.changed() => { + let _ = self.requester.shutdown(); + return; + } + event = event_rx.recv() => { + let Some(event) = event else { + log_error!(self.logger, "Kyoto node stopped unexpectedly."); + return; + }; + match event { + bip157::Event::ChainUpdate(_changes) => { + // TODO: notify LDK of connected/disconnected blocks via the + // `Listen` trait (`block_connected` / `block_disconnected`). + todo!("Handle chain update") + }, + bip157::Event::IndexedFilter(_filter) => { + // TODO: check the filter against wallet script pubkeys; + // if a match, request the full block via `requester.get_block()`. + todo!("Handle indexed filter") + }, + bip157::Event::Block(_indexed_block) => { + // TODO: apply the full block to the onchain wallet and LDK. + todo!("Handle block") + }, + bip157::Event::FiltersSynced(update) => { + let tip = update.tip(); + log_info!( + self.logger, + "BIP157 compact filters synced to height {}.", + tip.height, + ); + *self.tip_hash.lock().unwrap() = Some(tip.hash); + }, + } + } + Some(warn) = warn_rx.recv() => { + log_debug!(self.logger, "Kyoto: {}", warn); + } + } + } + } + + pub(super) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { + // Returns None when the update should be skipped without error (e.g. no tip yet). + let new_fee_rate_cache = match &self.fee_source { + FeeSource::Kyoto => self.fee_rate_cache_from_kyoto().await?, + FeeSource::Esplora { client, timeout_secs } => { + Some(self.fee_rate_cache_from_esplora(client, *timeout_secs).await?) + }, + FeeSource::Electrum { server_url } => { + Some(self.fee_rate_cache_from_electrum(server_url).await?) + }, + }; + + let Some(new_fee_rate_cache) = new_fee_rate_cache else { + return Ok(()); + }; + + self.fee_estimator.set_fee_rate_cache(new_fee_rate_cache); + + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt; + write_node_metrics(&*locked_node_metrics, &*self.kv_store, &*self.logger)?; + } + + Ok(()) + } + + /// Derive a uniform fee rate from the latest block's coinbase reward minus the block subsidy. + /// + /// Returns `Ok(None)` when no chain tip is available yet (first startup before sync). + async fn fee_rate_cache_from_kyoto( + &self, + ) -> Result>, Error> { + let tip_hash = *self.tip_hash.lock().unwrap(); + let Some(hash) = tip_hash else { + log_debug!( + self.logger, + "BIP157 fee estimation skipped: no chain tip available yet." + ); + return Ok(None); + }; + + let fee_rate = self + .requester + .average_fee_rate(hash) + .await + .map_err(|_| Error::FeerateEstimationUpdateFailed)?; + + let mut cache = HashMap::with_capacity(10); + for target in get_all_conf_targets() { + let adjusted = apply_post_estimation_adjustments(target, fee_rate); + log_debug!( + self.logger, + "Fee rate estimation updated for {:?}: {} sats/kwu", + target, + adjusted.to_sat_per_kwu(), + ); + cache.insert(target, adjusted); + } + Ok(Some(cache)) + } + + /// Fetch per-target fee rates from an Esplora server. + async fn fee_rate_cache_from_esplora( + &self, client: &EsploraAsyncClient, timeout_secs: u64, + ) -> Result, Error> { + let estimates = tokio::time::timeout( + Duration::from_secs(timeout_secs), + client.get_fee_estimates(), + ) + .await + .map_err(|e| { + log_error!(self.logger, "Updating fee rate estimates timed out: {}", e); + Error::FeerateEstimationUpdateTimeout + })? + .map_err(|e| { + log_error!(self.logger, "Failed to retrieve fee rate estimates: {}", e); + Error::FeerateEstimationUpdateFailed + })?; + + if estimates.is_empty() && self.config.network == Network::Bitcoin { + log_error!( + self.logger, + "Failed to retrieve fee rate estimates: empty estimates are disallowed on Mainnet." + ); + return Err(Error::FeerateEstimationUpdateFailed); + } + + let mut cache = HashMap::with_capacity(10); + for target in get_all_conf_targets() { + let num_blocks = get_num_block_defaults_for_target(target); + let converted = esplora_client::convert_fee_rate(num_blocks, estimates.clone()) + .map_or(1.0_f32, |r| r.max(1.0)); + let fee_rate = FeeRate::from_sat_per_kwu((converted * 250.0_f32) as u64); + let adjusted = apply_post_estimation_adjustments(target, fee_rate); + log_debug!( + self.logger, + "Fee rate estimation updated for {:?}: {} sats/kwu", + target, + adjusted.to_sat_per_kwu(), + ); + cache.insert(target, adjusted); + } + Ok(cache) + } + + /// Fetch per-target fee rates from an Electrum server. + /// + /// Opens a fresh connection for each call; `ElectrumClient` is not `Sync` so it cannot be + /// stored as a struct field and shared across async tasks. + async fn fee_rate_cache_from_electrum( + &self, server_url: &str, + ) -> Result, Error> { + let server_url = server_url.to_owned(); + let confirmation_targets = get_all_conf_targets(); + + let raw_estimates = tokio::time::timeout( + Duration::from_secs(DEFAULT_FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS), + tokio::task::spawn_blocking(move || { + let electrum_config = ElectrumConfigBuilder::new() + .retry(ELECTRUM_CLIENT_NUM_RETRIES) + .timeout(Some(DEFAULT_PER_REQUEST_TIMEOUT_SECS)) + .build(); + let client = ElectrumClient::from_config(&server_url, electrum_config) + .map_err(|_| Error::FeerateEstimationUpdateFailed)?; + let mut batch = Batch::default(); + for target in confirmation_targets { + batch.estimate_fee(get_num_block_defaults_for_target(target)); + } + client.batch_call(&batch).map_err(|_| Error::FeerateEstimationUpdateFailed) + }), + ) + .await + .map_err(|e| { + log_error!(self.logger, "Updating fee rate estimates timed out: {}", e); + Error::FeerateEstimationUpdateTimeout + })? + .map_err(|_| Error::FeerateEstimationUpdateFailed)? // JoinError + ?; // inner Result + + if raw_estimates.len() != confirmation_targets.len() + && self.config.network == Network::Bitcoin + { + log_error!( + self.logger, + "Failed to retrieve fee rate estimates: Electrum server didn't return all expected results." + ); + return Err(Error::FeerateEstimationUpdateFailed); + } + + let mut cache = HashMap::with_capacity(10); + for (target, raw_rate) in confirmation_targets.into_iter().zip(raw_estimates.into_iter()) { + // Electrum returns BTC/KvB; fall back to 1 sat/vb (= 0.00001 BTC/KvB) on failure. + let fee_rate_btc_per_kvb = raw_rate.as_f64().map_or(0.00001_f64, |v| v.max(0.00001)); + // Convert BTC/KvB → sat/kwu: multiply by 25_000_000 (= 10^8 / 4). + let fee_rate = FeeRate::from_sat_per_kwu((fee_rate_btc_per_kvb * 25_000_000.0).round() as u64); + let adjusted = apply_post_estimation_adjustments(target, fee_rate); + log_debug!( + self.logger, + "Fee rate estimation updated for {:?}: {} sats/kwu", + target, + adjusted.to_sat_per_kwu(), + ); + cache.insert(target, adjusted); + } + Ok(cache) + } + + pub(super) async fn process_broadcast_package(&self, _package: Vec) { + // TODO: broadcast transactions via the kyoto P2P network using + // `requester.broadcast_tx()`. + todo!("Transaction broadcasting is not yet supported for the BIP157 chain source") + } +} diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 49c011a78..961ed0504 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -8,6 +8,7 @@ pub(crate) mod bitcoind; mod electrum; mod esplora; +mod kyoto; use std::collections::HashMap; use std::sync::{Arc, Mutex, RwLock}; @@ -19,6 +20,7 @@ use lightning::chain::{BestBlock, Filter}; use crate::chain::bitcoind::{BitcoindChainSource, UtxoSourceClient}; use crate::chain::electrum::ElectrumChainSource; use crate::chain::esplora::EsploraChainSource; +use crate::chain::kyoto::KyotoChainSource; use crate::config::{ BackgroundSyncConfig, BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, WALLET_SYNC_INTERVAL_MINIMUM_SECS, @@ -29,6 +31,15 @@ use crate::runtime::Runtime; use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::{Error, NodeMetrics}; +/// Optional fee estimation source for chain backends that lack mempool visibility (e.g. BIP157). +#[derive(Debug, Clone)] +pub(crate) enum FeeSourceConfig { + /// Use an Esplora HTTP server for fee rate estimation. + Esplora(String), + /// Use an Electrum server for fee rate estimation. + Electrum(String), +} + pub(crate) enum WalletSyncStatus { Completed, InProgress { subscribers: tokio::sync::broadcast::Sender> }, @@ -93,6 +104,7 @@ enum ChainSourceKind { Esplora(EsploraChainSource), Electrum(ElectrumChainSource), Bitcoind(BitcoindChainSource), + Kyoto(KyotoChainSource), } impl ChainSource { @@ -160,6 +172,26 @@ impl ChainSource { (Self { kind, registered_txids, tx_broadcaster, logger }, best_block) } + pub(crate) fn new_kyoto( + peers: Vec, fee_source_config: Option, + fee_estimator: Arc, tx_broadcaster: Arc, + kv_store: Arc, config: Arc, logger: Arc, + node_metrics: Arc>, + ) -> (Self, Option) { + let kyoto_chain_source = KyotoChainSource::new( + peers, + fee_source_config, + fee_estimator, + kv_store, + Arc::clone(&config), + Arc::clone(&logger), + node_metrics, + ); + let kind = ChainSourceKind::Kyoto(kyoto_chain_source); + let registered_txids = Mutex::new(Vec::new()); + (Self { kind, registered_txids, tx_broadcaster, logger }, None) + } + pub(crate) async fn new_bitcoind_rest( rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, fee_estimator: Arc, tx_broadcaster: Arc, @@ -189,6 +221,9 @@ impl ChainSource { ChainSourceKind::Electrum(electrum_chain_source) => { electrum_chain_source.start(runtime)? }, + ChainSourceKind::Kyoto(kyoto_chain_source) => { + kyoto_chain_source.start(runtime)? + }, _ => { // Nothing to do for other chain sources. }, @@ -199,6 +234,7 @@ impl ChainSource { pub(crate) fn stop(&self) { match &self.kind { ChainSourceKind::Electrum(electrum_chain_source) => electrum_chain_source.stop(), + ChainSourceKind::Kyoto(kyoto_chain_source) => kyoto_chain_source.stop(), _ => { // Nothing to do for other chain sources. }, @@ -223,6 +259,7 @@ impl ChainSource { ChainSourceKind::Esplora(_) => true, ChainSourceKind::Electrum { .. } => true, ChainSourceKind::Bitcoind { .. } => false, + ChainSourceKind::Kyoto { .. } => false, } } @@ -289,6 +326,17 @@ impl ChainSource { ) .await }, + ChainSourceKind::Kyoto(kyoto_chain_source) => { + kyoto_chain_source + .continuously_sync_wallets( + stop_sync_receiver, + onchain_wallet, + channel_manager, + chain_monitor, + output_sweeper, + ) + .await + }, } } @@ -368,6 +416,9 @@ impl ChainSource { // `ChainPoller`. So nothing to do here. unreachable!("Onchain wallet will be synced via chain polling") }, + ChainSourceKind::Kyoto { .. } => { + unreachable!("Onchain wallet will be synced via the kyoto event loop") + }, } } @@ -393,6 +444,9 @@ impl ChainSource { // `ChainPoller`. So nothing to do here. unreachable!("Lightning wallet will be synced via chain polling") }, + ChainSourceKind::Kyoto { .. } => { + unreachable!("Lightning wallet will be synced via the kyoto event loop") + }, } } @@ -421,6 +475,9 @@ impl ChainSource { ) .await }, + ChainSourceKind::Kyoto { .. } => { + unreachable!("Listeners will be synced via the kyoto event loop") + }, } } @@ -435,6 +492,9 @@ impl ChainSource { ChainSourceKind::Bitcoind(bitcoind_chain_source) => { bitcoind_chain_source.update_fee_rate_estimates().await }, + ChainSourceKind::Kyoto(kyoto_chain_source) => { + kyoto_chain_source.update_fee_rate_estimates().await + }, } } @@ -463,6 +523,9 @@ impl ChainSource { ChainSourceKind::Bitcoind(bitcoind_chain_source) => { bitcoind_chain_source.process_broadcast_package(next_package).await }, + ChainSourceKind::Kyoto(kyoto_chain_source) => { + kyoto_chain_source.process_broadcast_package(next_package).await + }, } } } @@ -481,6 +544,7 @@ impl Filter for ChainSource { electrum_chain_source.register_tx(txid, script_pubkey) }, ChainSourceKind::Bitcoind { .. } => (), + ChainSourceKind::Kyoto { .. } => (), } } fn register_output(&self, output: lightning::chain::WatchedOutput) { @@ -492,6 +556,7 @@ impl Filter for ChainSource { electrum_chain_source.register_output(output) }, ChainSourceKind::Bitcoind { .. } => (), + ChainSourceKind::Kyoto { .. } => (), } } } diff --git a/src/config.rs b/src/config.rs index 96a6f49d9..7be0ebafd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -42,7 +42,7 @@ pub(crate) const DEFAULT_FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS: u64 = 10; pub(crate) const DEFAULT_TX_BROADCAST_TIMEOUT_SECS: u64 = 10; // The default {Esplora,Electrum} client timeout we're using. -const DEFAULT_PER_REQUEST_TIMEOUT_SECS: u8 = 10; +pub(crate) const DEFAULT_PER_REQUEST_TIMEOUT_SECS: u8 = 10; /// The default log level. pub const DEFAULT_LOG_LEVEL: LogLevel = LogLevel::Debug; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 7854a77f2..1975d4253 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -222,6 +222,11 @@ pub(crate) fn setup_bitcoind_and_electrsd() -> (BitcoinD, ElectrsD) { let mut bitcoind_conf = corepc_node::Conf::default(); bitcoind_conf.network = "regtest"; bitcoind_conf.args.push("-rest"); + // Enable compact block filters so BIP-157 (kyoto) nodes can connect to this instance. + // The overhead for regtest-scale tests is negligible. + bitcoind_conf.args.push("-blockfilterindex=1"); + bitcoind_conf.args.push("-peerblockfilters=1"); + bitcoind_conf.p2p = corepc_node::P2P::Yes; let bitcoind = BitcoinD::with_conf(bitcoind_exe, &bitcoind_conf).unwrap(); let electrs_exe = env::var("ELECTRS_EXE") @@ -326,6 +331,9 @@ pub(crate) enum TestChainSource<'a> { Electrum(&'a ElectrsD), BitcoindRpcSync(&'a BitcoinD), BitcoindRestSync(&'a BitcoinD), + /// BIP-157 compact block filters via kyoto. The inner `BitcoinD` must have been started with + /// `-blockfilterindex=1 -peerblockfilters=1`; use [`setup_bitcoind_with_filters`]. + Bip157(&'a BitcoinD), } #[derive(Clone, Copy)] @@ -463,6 +471,14 @@ pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) -> rpc_password, ); }, + TestChainSource::Bip157(bitcoind) => { + let p2p_addr: std::net::SocketAddr = bitcoind + .params + .p2p_socket + .expect("BIP-157 tests require P2P to be enabled on bitcoind") + .into(); + builder.set_chain_source_bip157(vec![p2p_addr]); + }, } match &config.log_writer { @@ -497,7 +513,11 @@ pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) -> node.start().unwrap(); assert!(node.status().is_running); - assert!(node.status().latest_fee_rate_cache_update_timestamp.is_some()); + // BIP-157 nodes have no chain tip on first startup so the fee rate cache may not be + // populated yet (the first update_fee_rate_estimates call returns Ok(None) for kyoto). + if !matches!(chain_source, TestChainSource::Bip157(_)) { + assert!(node.status().latest_fee_rate_cache_update_timestamp.is_some()); + } node } diff --git a/tests/integration_tests_bip157.rs b/tests/integration_tests_bip157.rs new file mode 100644 index 000000000..c3f9ffcbc --- /dev/null +++ b/tests/integration_tests_bip157.rs @@ -0,0 +1,144 @@ +// 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. + +mod common; + +use bitcoin::{Amount, Network}; +use common::{ + expect_channel_pending_event, expect_channel_ready_event, expect_event, + expect_payment_received_event, expect_payment_successful_event, generate_blocks_and_wait, + premine_and_distribute_funds, random_storage_path, setup_bitcoind_and_electrsd, + setup_builder, setup_two_nodes, wait_for_tx, TestChainSource, +}; +use ldk_node::config::Config; +use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy}; +use ldk_node::payment::{PaymentKind, PaymentStatus}; +use ldk_node::{Builder, Event}; +use lightning_invoice::{Bolt11InvoiceDescription, Description}; + +/// Smoke test: build and start an ldk-node instance backed by BIP157 compact block filters. +/// +/// Uses signet with an empty peer list so kyoto will bootstrap via DNS seeds. +/// This test is expected to fail until `ChainSource::new_kyoto` is fully implemented. +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_start_node_with_bip157_backend() { + let mut config = Config::default(); + config.network = Network::Signet; + config.storage_dir_path = random_storage_path().to_str().unwrap().to_owned(); + + setup_builder!(builder, config); + + // Empty peer list — kyoto will use DNS seeds for signet peer discovery. + builder.set_chain_source_bip157(vec![]); + + let mnemonic = generate_entropy_mnemonic(None); + let entropy = NodeEntropy::from_bip39_mnemonic(mnemonic, None); + + let node = builder.build_with_fs_store(entropy.into()).expect("Node build should succeed"); + node.start().expect("Node start should succeed"); + + assert!(node.status().is_running); + + node.stop().unwrap(); +} + +/// Full channel lifecycle test for the BIP-157 chain backend. +/// +/// Spins up a local regtest bitcoind with compact block filters enabled, connects two +/// ldk-node instances to it via kyoto, funds them, and drives a complete channel open → +/// payment → close cycle. +/// +/// The test is event-driven (no `sync_wallets()` calls): wallet and LDK state are updated +/// by the kyoto background event loop as blocks arrive. +/// +/// Expected failures until the kyoto event handlers are implemented: +/// - `ChannelReady` will not fire because `ChainUpdate` in `continuously_sync_wallets` +/// still has a `todo!()` — LDK never receives block confirmations. +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn channel_full_cycle_bip157() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Bip157(&bitcoind); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + do_channel_full_cycle_bip157(node_a, node_b, &bitcoind.client, &electrsd.client).await; +} + +/// Like `do_channel_full_cycle` but without `sync_wallets()` calls. +/// +/// For kyoto the wallet is updated by the background event loop so there is no need (and +/// no meaningful way) to manually trigger a sync. The test paces itself via LDK events +/// (`ChannelPending`, `ChannelReady`, `PaymentSuccessful`, …) instead. +async fn do_channel_full_cycle_bip157( + node_a: ldk_node::Node, node_b: ldk_node::Node, bitcoind: &electrsd::corepc_node::Client, + electrsd: &E, +) { + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + + // Fund both nodes. Kyoto will pick up the confirmed outputs once the block that + // contains the funding txs is processed by the background event loop. + premine_and_distribute_funds( + bitcoind, + electrsd, + vec![addr_a, addr_b], + Amount::from_sat(2_100_000), + ) + .await; + + // Open an announced channel from A → B, balanced 50/50. + println!("\nA -- open_channel -> B"); + let funding_amount_sat = 2_080_000; + let push_msat = (funding_amount_sat / 2) * 1000; + node_a + .open_announced_channel( + node_b.node_id(), + node_b.listening_addresses().unwrap().first().unwrap().clone(), + funding_amount_sat, + Some(push_msat), + None, + ) + .unwrap(); + + // Both nodes emit ChannelPending as soon as the funding tx is broadcast (no chain + // confirmation required). + let funding_txo_a = expect_channel_pending_event!(node_a, node_b.node_id()); + let funding_txo_b = expect_channel_pending_event!(node_b, node_a.node_id()); + assert_eq!(funding_txo_a, funding_txo_b); + wait_for_tx(electrsd, funding_txo_a.txid).await; + + // Mine 6 blocks to confirm the channel. Kyoto will deliver ChainUpdate events to the + // background loop which notifies LDK via the Listen trait, triggering ChannelReady. + generate_blocks_and_wait(bitcoind, electrsd, 6).await; + + let user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + // Send a payment B → A. + println!("\nA receive"); + let invoice_amount_msat = 2_500_000; + let invoice_description = + Bolt11InvoiceDescription::Direct(Description::new(String::from("test")).unwrap()); + let invoice = node_a + .bolt11_payment() + .receive(invoice_amount_msat, &invoice_description.into(), 9217) + .unwrap(); + + println!("\nB send"); + let payment_id = node_b.bolt11_payment().send(&invoice, None).unwrap(); + + expect_payment_successful_event!(node_b, Some(payment_id), None); + expect_payment_received_event!(node_a, invoice_amount_msat); + assert_eq!(node_b.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); + + // Cooperative close. + println!("\nA -- close_channel -> B"); + node_a.close_channel(&user_channel_id_a, node_b.node_id()).unwrap(); + expect_event!(node_a, ChannelClosed); + expect_event!(node_b, ChannelClosed); + + node_a.stop().unwrap(); + node_b.stop().unwrap(); +} From 90eb045064bf090e8610ca2cef225944bd8a76d3 Mon Sep 17 00:00:00 2001 From: Alexander Shevtsov Date: Tue, 17 Mar 2026 16:17:02 +0100 Subject: [PATCH 2/2] Implement syncing via bip157 (kyoto) Add a storage of watched scripts Add a mechanism to process the filters and watched scripts Add last synced checkpoint, channel to watch its updates Make possible the kyoto node to start/stop, maintain the original peer list provided. Add header cache for not to redownload them when applying. Add wait_until_synced Add tx broadcasting. Add tests. --- Cargo.toml | 2 +- src/chain/kyoto.rs | 362 +- src/chain/mod.rs | 35 +- src/lib.rs | 5 +- src/wallet/mod.rs | 152 +- tests/common/mod.rs | 137 +- tests/integration_tests_bip157.rs | 4 +- tests/integration_tests_rust.rs | 5339 +++++++++++++++-------------- 8 files changed, 3283 insertions(+), 2753 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index da9d117d2..7fd930484 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,7 +76,7 @@ serde_json = { version = "1.0.128", default-features = false, features = ["std"] log = { version = "0.4.22", default-features = false, features = ["std"]} async-trait = { version = "0.1", default-features = false } -bip157 = "0.3.4" +bip157 = "0.4.2" vss-client = { package = "vss-client-ng", version = "0.5" } prost = { version = "0.11.6", default-features = false} #bitcoin-payment-instructions = { version = "0.6" } diff --git a/src/chain/kyoto.rs b/src/chain/kyoto.rs index fd3a4632a..45049ab04 100644 --- a/src/chain/kyoto.rs +++ b/src/chain/kyoto.rs @@ -5,16 +5,20 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::sync::{Arc, Mutex, RwLock}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use bitcoin::{BlockHash, FeeRate, Network, Transaction}; -use bip157::Info; -use electrum_client::{Batch, Client as ElectrumClient, ConfigBuilder as ElectrumConfigBuilder, ElectrumApi}; +use bip157::chain::{BlockHeaderChanges, IndexedHeader}; +use bip157::{HeaderCheckpoint, Info}; +use bitcoin::{BlockHash, FeeRate, Network, ScriptBuf, Transaction}; +use electrum_client::{ + Batch, Client as ElectrumClient, ConfigBuilder as ElectrumConfigBuilder, ElectrumApi, +}; use esplora_client::AsyncClient as EsploraAsyncClient; -use tokio::sync::mpsc; +use lightning::chain::{BestBlock, Listen}; +use tokio::sync::{mpsc, watch}; use super::FeeSourceConfig; use crate::chain::electrum::ELECTRUM_CLIENT_NUM_RETRIES; @@ -31,8 +35,7 @@ use crate::runtime::Runtime; use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::{Error, NodeMetrics}; -/// How long to wait for kyoto to establish the required peer connections before -/// considering the startup failed. +/// How long to wait for kyoto to establish the required peer connections before considering the startup failed. const KYOTO_CONNECTION_TIMEOUT: Duration = Duration::from_secs(10); /// The fee estimation back-end used by a BIP-157 chain source. @@ -52,18 +55,41 @@ enum FeeSource { } pub(super) struct KyotoChainSource { - /// The kyoto node, held until `start()` spawns it. + /// Peers at construction time. + peers: Vec, + /// The kyoto node, built in `new()` and rebuilt on each subsequent `start()` call. + /// Consumed (taken) by `start()` when the node task is spawned. node: Mutex>, - /// Info channel, held until `start()` uses it to wait for initial connections. + /// Info channel, consumed by `start()` during the initial connection wait. info_rx: Mutex>>, - /// The remaining client channels, held until `continuously_sync_wallets()` takes them. + /// The remaining client channels, consumed by `continuously_sync_wallets()`. warn_rx: Mutex>>, event_rx: Mutex>>, /// Cloneable handle used to send commands (e.g. shutdown) to the running node. - requester: bip157::Requester, - /// The most recently observed chain tip block hash, updated by `FiltersSynced` events. - /// Only used when `fee_source` is [`FeeSource::Kyoto`]. - tip_hash: Mutex>, + /// Replaced on each `start()` with the handle for the newly-built node instance. + requester: Mutex>, + /// The most recently completed sync checkpoint, updated by `FiltersSynced` events. + /// On restart this is passed to kyoto's `chain_state` so it resumes from where it left off. + synced_checkpoint: Mutex>, + /// Sender half of the sync-tip watch channel, held until `continuously_sync_wallets()` takes + /// it. When the background loop exits (node died or stop signal), dropping this sender causes + /// all `wait_until_synced` callers to receive `RecvError` immediately. + synced_checkpoint_tx: Mutex>>>, + /// Receiver half of the sync-tip watch channel. Cloned by `wait_until_synced` to observe + /// `FiltersSynced` checkpoints emitted by the background loop. + /// Replaced on each `start()` alongside the sender. + synced_checkpoint_rx: Mutex>>, + /// Scripts registered by LDK (via `chain::Filter`) and by the BDK onchain wallet. + /// On each `IndexedFilter` event the compact filter is checked against this set; + /// a match triggers a full-block request via `requester.get_block()`. + watched_scripts: Mutex>, + /// Block headers accumulated from `ChainUpdate::Connected` events, keyed by block hash. + /// + /// Kyoto downloads all headers first, then scans compact filters. When we process an + /// `IndexedFilter` event we need the actual `Header` struct (for `filtered_block_connected`) + /// but the filter event only carries the block hash. We look it up here and remove the + /// entry once the filter has been handled to bound memory usage. + header_cache: Mutex>, fee_source: FeeSource, fee_estimator: Arc, kv_store: Arc, @@ -80,8 +106,9 @@ impl KyotoChainSource { ) -> Self { let data_dir = PathBuf::from(&config.storage_dir_path).join("kyoto"); let (node, client) = bip157::Builder::new(config.network) - .add_peers(peers.into_iter().map(Into::into)) + .add_peers(peers.iter().copied().map(Into::into)) .data_dir(data_dir) + .fetch_witness_data() .build(); let bip157::Client { requester, info_rx, warn_rx, event_rx } = client; @@ -100,13 +127,20 @@ impl KyotoChainSource { None => FeeSource::Kyoto, }; + let (synced_checkpoint_tx, synced_checkpoint_rx) = watch::channel(None); + Self { + peers, node: Mutex::new(Some(node)), info_rx: Mutex::new(Some(info_rx)), warn_rx: Mutex::new(Some(warn_rx)), event_rx: Mutex::new(Some(event_rx)), - requester, - tip_hash: Mutex::new(None), + requester: Mutex::new(Some(requester)), + synced_checkpoint: Mutex::new(None), + synced_checkpoint_tx: Mutex::new(Some(synced_checkpoint_tx)), + synced_checkpoint_rx: Mutex::new(synced_checkpoint_rx), + watched_scripts: Mutex::new(HashSet::new()), + header_cache: Mutex::new(HashMap::new()), fee_source, fee_estimator, kv_store, @@ -118,12 +152,62 @@ impl KyotoChainSource { /// Spawn the kyoto node task, then block until it connects to the required number of peers. /// + /// Rebuilds the kyoto node on each call so that the node can be restarted after a stop. + /// On the first start the node was already built in `new()` (with explicit peers); on + /// subsequent starts a fresh node is rebuilt with the same peers so that nodes without a + /// populated kyoto peer database (e.g. regtest) can reconnect after a stop/start cycle. + /// /// Returns `Err(Error::ConnectionFailed)` if no connection is established within /// [`KYOTO_CONNECTION_TIMEOUT`] or if the node exits before connecting. pub(super) fn start(&self, runtime: Arc) -> Result<(), Error> { - let node = self.node.lock().unwrap().take().expect("kyoto node already started"); - let mut info_rx = - self.info_rx.lock().unwrap().take().expect("kyoto info_rx already taken"); + // If this is a restart (node was consumed by the previous start), rebuild from disk. + if self.node.lock().unwrap().is_none() { + let data_dir = PathBuf::from(&self.config.storage_dir_path).join("kyoto"); + let checkpoint = *self.synced_checkpoint.lock().unwrap(); + let mut builder = bip157::Builder::new(self.config.network) + .add_peers(self.peers.iter().copied().map(Into::into)) + .data_dir(data_dir) + .fetch_witness_data(); + if let Some(cp) = checkpoint { + builder = builder.chain_state(bip157::chain::ChainState::Checkpoint(cp)); + } + let (node, client) = builder.build(); + let bip157::Client { requester, info_rx, warn_rx, event_rx } = client; + let (synced_checkpoint_tx, synced_checkpoint_rx) = watch::channel(None); + *self.node.lock().unwrap() = Some(node); + *self.info_rx.lock().unwrap() = Some(info_rx); + *self.warn_rx.lock().unwrap() = Some(warn_rx); + *self.event_rx.lock().unwrap() = Some(event_rx); + *self.requester.lock().unwrap() = Some(requester); + *self.synced_checkpoint_tx.lock().unwrap() = Some(synced_checkpoint_tx); + *self.synced_checkpoint_rx.lock().unwrap() = synced_checkpoint_rx; + // header_cache entries are invalid for the new node instance. + self.header_cache.lock().unwrap().clear(); + // synced_checkpoint is passed to the builder above; reset it so fee + // estimation skips gracefully until the new instance hits FiltersSynced. + *self.synced_checkpoint.lock().unwrap() = None; + } + + // peers: Vec, + // node: Mutex>, + // info_rx: Mutex>>, + // warn_rx: Mutex>>, + // event_rx: Mutex>>, + // requester: Mutex>, + // synced_checkpoint: Mutex>, + // synced_checkpoint_tx: Mutex>>>, + // synced_checkpoint_rx: Mutex>>, + // watched_scripts: Mutex>, + // header_cache: Mutex>, + // fee_source: FeeSource, + // fee_estimator: Arc, + // kv_store: Arc, + // config: Arc, + // logger: Arc, + // node_metrics: Arc>, + + let node = self.node.lock().unwrap().take().unwrap(); + let mut info_rx = self.info_rx.lock().unwrap().take().unwrap(); let logger = Arc::clone(&self.logger); runtime.spawn_background_task(async move { @@ -164,24 +248,45 @@ impl KyotoChainSource { /// Signal the kyoto node to shut down. Called during `Node::stop()`. pub(super) fn stop(&self) { - let _ = self.requester.shutdown(); + if let Some(r) = self.requester.lock().unwrap().as_ref() { + let _ = r.shutdown(); + } + } + + /// Register a script pubkey to watch in compact block filters. + /// + /// Called from [`super::ChainSource`]'s [`lightning::chain::Filter`] impl when LDK + /// registers a channel output or transaction script, and when seeding the BDK wallet's + /// known scripts at startup. + pub(super) fn register_output(&self, script: ScriptBuf) { + log_debug!(self.logger, "BIP157: register_output script={}", script); + self.watched_scripts.lock().unwrap().insert(script); } /// Main sync loop driven by kyoto events. Runs for the lifetime of the node. pub(super) async fn continuously_sync_wallets( &self, mut stop_sync_receiver: tokio::sync::watch::Receiver<()>, - _onchain_wallet: Arc, _channel_manager: Arc, - _chain_monitor: Arc, _output_sweeper: Arc, + onchain_wallet: Arc, channel_manager: Arc, + chain_monitor: Arc, output_sweeper: Arc, ) { let mut warn_rx = self.warn_rx.lock().unwrap().take().expect("kyoto warn_rx already consumed"); let mut event_rx = self.event_rx.lock().unwrap().take().expect("kyoto event_rx already consumed"); + let synced_checkpoint_tx = self + .synced_checkpoint_tx + .lock() + .unwrap() + .take() + .expect("kyoto synced_checkpoint_tx already consumed"); + // Clone the requester handle once so async calls inside the loop don't need to lock. + let requester = + self.requester.lock().unwrap().as_ref().expect("kyoto requester not set").clone(); loop { tokio::select! { _ = stop_sync_receiver.changed() => { - let _ = self.requester.shutdown(); + let _ = requester.shutdown(); return; } event = event_rx.recv() => { @@ -190,28 +295,113 @@ impl KyotoChainSource { return; }; match event { - bip157::Event::ChainUpdate(_changes) => { - // TODO: notify LDK of connected/disconnected blocks via the - // `Listen` trait (`block_connected` / `block_disconnected`). - todo!("Handle chain update") + bip157::Event::ChainUpdate(changes) => { + match changes { + BlockHeaderChanges::Connected(indexed_header) => { + // Cache the header so we can look it up when the corresponding `IndexedFilter` event arrives. + self.header_cache.lock().unwrap().insert(indexed_header.block_hash(), indexed_header); + // Advance the BDK wallet's LocalChain so that block_connected + // calls on filter-matching blocks can connect. BDK requires the + // parent block to already be in the chain. + onchain_wallet.apply_header(&indexed_header.header, indexed_header.height); + }, + BlockHeaderChanges::Reorganized { accepted, reorganized } => { + // Notify LDK of the reorg using the fork-point block, + // i.e. the last block common to both forks. + if let Some(first_disconnected) = reorganized.first() { + let fork_hash = first_disconnected.header.prev_blockhash; + let fork_height = first_disconnected.height.saturating_sub(1); + let fork_point = BestBlock::new(fork_hash, fork_height); + channel_manager.blocks_disconnected(fork_point); + chain_monitor.blocks_disconnected(fork_point); + output_sweeper.blocks_disconnected(fork_point); + // BDK wallet treats disconnections as a no-op. + onchain_wallet.blocks_disconnected(fork_point); + } + // Update the header cache: drop stale entries and cache + // the newly-accepted headers whose filters will follow. + let mut cache = self.header_cache.lock().unwrap(); + for h in &reorganized { + cache.remove(&h.block_hash()); + } + for h in &accepted { + cache.insert(h.block_hash(), *h); + onchain_wallet.apply_header(&h.header, h.height); + } + }, + BlockHeaderChanges::ForkAdded(_) => { + // A competing fork that hasn't overtaken the main chain; + // nothing to do. + }, + } }, - bip157::Event::IndexedFilter(_filter) => { - // TODO: check the filter against wallet script pubkeys; - // if a match, request the full block via `requester.get_block()`. - todo!("Handle indexed filter") + bip157::Event::IndexedFilter(filter) => { + let block_hash = filter.block_hash(); + let height = filter.height(); + + // Retrieve the cached header — it must have arrived in an + // earlier `ChainUpdate::Connected` event. + let indexed_header = self.header_cache.lock().unwrap().get(&block_hash).copied(); + let Some(indexed_header) = indexed_header else { + log_error!(self.logger, "BIP157: missing cached header for block {} at height {}; skipping block.", block_hash, height); + continue; + }; + + // Skip blocks already in LDK's chain. On restart kyoto re-emits + // blocks from the last checkpoint, but LDK's in-memory state is + // preserved across stop/start so feeding it old blocks would panic. + let ldk_tip_height = channel_manager.current_best_block().height; + if height <= ldk_tip_height { + self.header_cache.lock().unwrap().remove(&block_hash); + continue; + } + + let has_match = { + let scripts = self.watched_scripts.lock().unwrap(); + filter.contains_any(scripts.iter()) + }; + + if has_match { + log_debug!(self.logger, "BIP157: compact filter matched at height {}; fetching full block.", height); + match requester.get_block(block_hash).await { + Ok(indexed_block) => { + log_info!(self.logger, "BIP157: block_connected height={} txs={}", height, indexed_block.block.txdata.len()); + onchain_wallet.block_connected(&indexed_block.block, height); + channel_manager.block_connected(&indexed_block.block, height); + log_info!(self.logger, "BIP157: channel_manager block_connected done height={}", height); + chain_monitor.block_connected(&indexed_block.block, height); + output_sweeper.block_connected(&indexed_block.block, height); + }, + Err(e) => { + log_error!(self.logger, "BIP157: failed to fetch block {} at height {}: {}", block_hash, height, e); + }, + } + } else { + log_info!(self.logger, "BIP157: filtered_block_connected height={} (no match)", height); + // No script match: tell LDK the block passed but carry no + // transactions. BDK does not need non-matching blocks. + let txdata = []; + channel_manager.filtered_block_connected(&indexed_header.header, &txdata, height); + log_info!(self.logger, "BIP157: channel_manager filtered_block_connected done height={}", height); + chain_monitor.filtered_block_connected(&indexed_header.header, &txdata, height); + output_sweeper.filtered_block_connected(&indexed_header.header, &txdata, height); + } + + // Header no longer needed after the filter is processed. + self.header_cache.lock().unwrap().remove(&block_hash); }, bip157::Event::Block(_indexed_block) => { - // TODO: apply the full block to the onchain wallet and LDK. - todo!("Handle block") + // Blocks requested via `requester.get_block()` are delivered + // directly via the oneshot channel in the `IndexedFilter` + // handler above. This event path is unused in the current + // version of kyoto. + log_debug!(self.logger, "BIP157: received unexpected Event::Block; ignoring."); }, bip157::Event::FiltersSynced(update) => { let tip = update.tip(); - log_info!( - self.logger, - "BIP157 compact filters synced to height {}.", - tip.height, - ); - *self.tip_hash.lock().unwrap() = Some(tip.hash); + log_info!(self.logger, "BIP157 compact filters synced to height {}.", tip.height); + *self.synced_checkpoint.lock().unwrap() = Some(tip); + let _ = synced_checkpoint_tx.send(Some(tip)); }, } } @@ -222,6 +412,21 @@ impl KyotoChainSource { } } + /// Block until the background sync loop has processed all blocks up to the current chain tip. + /// + /// Gets the current tip from the kyoto node, then waits for `FiltersSynced` to fire at or + /// beyond that height. If the loop has already synced past the tip this returns immediately. + pub(super) async fn wait_until_synced(&self) -> Result<(), Error> { + let requester = + self.requester.lock().unwrap().as_ref().ok_or(Error::WalletOperationFailed)?.clone(); + let tip = requester.chain_tip().await.map_err(|_| Error::WalletOperationFailed)?; + let mut rx = self.synced_checkpoint_rx.lock().unwrap().clone(); + rx.wait_for(|v: &Option| v.map_or(false, |cp| cp.height >= tip.height)) + .await + .map(|_| ()) + .map_err(|_| Error::WalletOperationFailed) + } + pub(super) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { // Returns None when the update should be skipped without error (e.g. no tip yet). let new_fee_rate_cache = match &self.fee_source { @@ -257,18 +462,27 @@ impl KyotoChainSource { async fn fee_rate_cache_from_kyoto( &self, ) -> Result>, Error> { - let tip_hash = *self.tip_hash.lock().unwrap(); - let Some(hash) = tip_hash else { - log_debug!( - self.logger, - "BIP157 fee estimation skipped: no chain tip available yet." - ); + let checkpoint = *self.synced_checkpoint.lock().unwrap(); + let Some(checkpoint) = checkpoint else { + log_debug!(self.logger, "BIP157 fee estimation skipped: no chain tip available yet."); return Ok(None); }; - let fee_rate = self + if checkpoint.height == 0 { + // Genesis is never stored in kyoto's peer-downloaded headers map, nothing to estimate. + log_debug!(self.logger, "BIP157 fee estimation skipped: chain tip is at genesis."); + return Ok(None); + } + + let requester = self .requester - .average_fee_rate(hash) + .lock() + .unwrap() + .as_ref() + .ok_or(Error::FeerateEstimationUpdateFailed)? + .clone(); + let fee_rate = requester + .average_fee_rate(checkpoint.hash) .await .map_err(|_| Error::FeerateEstimationUpdateFailed)?; @@ -279,7 +493,7 @@ impl KyotoChainSource { self.logger, "Fee rate estimation updated for {:?}: {} sats/kwu", target, - adjusted.to_sat_per_kwu(), + adjusted.to_sat_per_kwu() ); cache.insert(target, adjusted); } @@ -290,19 +504,17 @@ impl KyotoChainSource { async fn fee_rate_cache_from_esplora( &self, client: &EsploraAsyncClient, timeout_secs: u64, ) -> Result, Error> { - let estimates = tokio::time::timeout( - Duration::from_secs(timeout_secs), - client.get_fee_estimates(), - ) - .await - .map_err(|e| { - log_error!(self.logger, "Updating fee rate estimates timed out: {}", e); - Error::FeerateEstimationUpdateTimeout - })? - .map_err(|e| { - log_error!(self.logger, "Failed to retrieve fee rate estimates: {}", e); - Error::FeerateEstimationUpdateFailed - })?; + let estimates = + tokio::time::timeout(Duration::from_secs(timeout_secs), client.get_fee_estimates()) + .await + .map_err(|e| { + log_error!(self.logger, "Updating fee rate estimates timed out: {}", e); + Error::FeerateEstimationUpdateTimeout + })? + .map_err(|e| { + log_error!(self.logger, "Failed to retrieve fee rate estimates: {}", e); + Error::FeerateEstimationUpdateFailed + })?; if estimates.is_empty() && self.config.network == Network::Bitcoin { log_error!( @@ -323,7 +535,7 @@ impl KyotoChainSource { self.logger, "Fee rate estimation updated for {:?}: {} sats/kwu", target, - adjusted.to_sat_per_kwu(), + adjusted.to_sat_per_kwu() ); cache.insert(target, adjusted); } @@ -379,22 +591,34 @@ impl KyotoChainSource { // Electrum returns BTC/KvB; fall back to 1 sat/vb (= 0.00001 BTC/KvB) on failure. let fee_rate_btc_per_kvb = raw_rate.as_f64().map_or(0.00001_f64, |v| v.max(0.00001)); // Convert BTC/KvB → sat/kwu: multiply by 25_000_000 (= 10^8 / 4). - let fee_rate = FeeRate::from_sat_per_kwu((fee_rate_btc_per_kvb * 25_000_000.0).round() as u64); + let fee_rate = + FeeRate::from_sat_per_kwu((fee_rate_btc_per_kvb * 25_000_000.0).round() as u64); let adjusted = apply_post_estimation_adjustments(target, fee_rate); log_debug!( self.logger, "Fee rate estimation updated for {:?}: {} sats/kwu", target, - adjusted.to_sat_per_kwu(), + adjusted.to_sat_per_kwu() ); cache.insert(target, adjusted); } Ok(cache) } - pub(super) async fn process_broadcast_package(&self, _package: Vec) { - // TODO: broadcast transactions via the kyoto P2P network using - // `requester.broadcast_tx()`. - todo!("Transaction broadcasting is not yet supported for the BIP157 chain source") + pub(super) async fn process_broadcast_package(&self, package: Vec) { + let requester = match self.requester.lock().unwrap().as_ref() { + Some(r) => r.clone(), + None => return, + }; + for tx in package { + match requester.broadcast_tx(tx).await { + Ok(wtxid) => { + log_debug!(self.logger, "BIP157: broadcast transaction {}", wtxid); + }, + Err(e) => { + log_error!(self.logger, "BIP157: failed to broadcast transaction: {}", e); + }, + } + } } } diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 961ed0504..ffa28fb1b 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -14,7 +14,7 @@ use std::collections::HashMap; use std::sync::{Arc, Mutex, RwLock}; use std::time::Duration; -use bitcoin::{Script, Txid}; +use bitcoin::{Script, ScriptBuf, Txid}; use lightning::chain::{BestBlock, Filter}; use crate::chain::bitcoind::{BitcoindChainSource, UtxoSourceClient}; @@ -104,7 +104,7 @@ enum ChainSourceKind { Esplora(EsploraChainSource), Electrum(ElectrumChainSource), Bitcoind(BitcoindChainSource), - Kyoto(KyotoChainSource), + Kyoto(KyotoChainSource), } impl ChainSource { @@ -221,9 +221,7 @@ impl ChainSource { ChainSourceKind::Electrum(electrum_chain_source) => { electrum_chain_source.start(runtime)? }, - ChainSourceKind::Kyoto(kyoto_chain_source) => { - kyoto_chain_source.start(runtime)? - }, + ChainSourceKind::Kyoto(kyoto_chain_source) => kyoto_chain_source.start(runtime)?, _ => { // Nothing to do for other chain sources. }, @@ -254,6 +252,16 @@ impl ChainSource { self.registered_txids.lock().unwrap().clone() } + /// Register a script pubkey to watch in compact block filters. + /// + /// This is a no-op for chain backends other than BIP157/kyoto, which rely on transaction-based + /// sync and don't need an explicit script watchlist. + pub(crate) fn register_script(&self, script: ScriptBuf) { + if let ChainSourceKind::Kyoto(kyoto_chain_source) = &self.kind { + kyoto_chain_source.register_output(script); + } + } + pub(crate) fn is_transaction_based(&self) -> bool { match &self.kind { ChainSourceKind::Esplora(_) => true, @@ -399,8 +407,7 @@ impl ChainSource { } } - // Synchronize the onchain wallet via transaction-based protocols (i.e., Esplora, Electrum, - // etc.) + // Synchronize the onchain wallet via transaction-based protocols (i.e., Esplora, Electrum, etc.) pub(crate) async fn sync_onchain_wallet( &self, onchain_wallet: Arc, ) -> Result<(), Error> { @@ -450,7 +457,7 @@ impl ChainSource { } } - pub(crate) async fn poll_and_update_listeners( + pub(crate) async fn sync_listeners_to_tip( &self, onchain_wallet: Arc, channel_manager: Arc, chain_monitor: Arc, output_sweeper: Arc, ) -> Result<(), Error> { @@ -475,8 +482,8 @@ impl ChainSource { ) .await }, - ChainSourceKind::Kyoto { .. } => { - unreachable!("Listeners will be synced via the kyoto event loop") + ChainSourceKind::Kyoto(kyoto_chain_source) => { + kyoto_chain_source.wait_until_synced().await }, } } @@ -544,7 +551,9 @@ impl Filter for ChainSource { electrum_chain_source.register_tx(txid, script_pubkey) }, ChainSourceKind::Bitcoind { .. } => (), - ChainSourceKind::Kyoto { .. } => (), + ChainSourceKind::Kyoto(kyoto_chain_source) => { + kyoto_chain_source.register_output(script_pubkey.to_owned()) + }, } } fn register_output(&self, output: lightning::chain::WatchedOutput) { @@ -556,7 +565,9 @@ impl Filter for ChainSource { electrum_chain_source.register_output(output) }, ChainSourceKind::Bitcoind { .. } => (), - ChainSourceKind::Kyoto { .. } => (), + ChainSourceKind::Kyoto(kyoto_chain_source) => { + kyoto_chain_source.register_output(output.script_pubkey) + }, } } } diff --git a/src/lib.rs b/src/lib.rs index 3e5180dcb..78cb5200e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -272,6 +272,9 @@ impl Node { let chain_source = Arc::clone(&self.chain_source); self.runtime.block_on(async move { chain_source.update_fee_rate_estimates().await })?; + // For BIP157 backends: seed kyoto's script watchlist with all previously-derived wallet addresses + self.wallet.seed_watched_scripts(); + // Spawn background task continuously syncing onchain, lightning, and fee rate cache. let stop_sync_receiver = self.stop_sender.subscribe(); let chain_source = Arc::clone(&self.chain_source); @@ -1642,7 +1645,7 @@ impl Node { } else { chain_source.update_fee_rate_estimates().await?; chain_source - .poll_and_update_listeners( + .sync_listeners_to_tip( sync_wallet, sync_cman, sync_cmon, diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 0e80a46db..2a16a35ea 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -9,6 +9,7 @@ use std::future::Future; use std::ops::Deref; use std::str::FromStr; use std::sync::{Arc, Mutex}; +use std::time::{SystemTime, UNIX_EPOCH}; use bdk_chain::spk_client::{FullScanRequest, SyncRequest}; use bdk_wallet::descriptor::ExtendedDescriptor; @@ -122,6 +123,35 @@ impl Wallet { self.inner.lock().unwrap().start_sync_with_revealed_spks().build() } + /// Register all previously-revealed wallet scripts into the chain source's script watchlist. + /// + /// For BIP157 backends the kyoto compact-filter scanner only checks scripts that have been + /// explicitly registered. When the node restarts, previously-derived addresses need to be + /// re-seeded so that blocks containing payments to those addresses are not silently skipped. + /// This is a no-op for non-BIP157 backends. + pub(crate) fn seed_watched_scripts(&self) { + let locked_wallet = self.inner.lock().unwrap(); + for ((_, _), spk) in locked_wallet.spk_index().revealed_spks(..) { + self.chain_source.register_script(spk); + } + } + + /// Advance the wallet's internal [`LocalChain`] to include the given block header. + /// + /// Called by the BIP157 backend for every `ChainUpdate::Connected` event so the chain stays + /// in sync with kyoto's header stream. Without this, subsequent `block_connected` calls would + /// fail because BDK requires the parent block to already be in the chain before connecting the + /// next block. + pub(crate) fn apply_header(&self, header: &bitcoin::block::Header, height: u32) { + let mut locked_wallet = self.inner.lock().unwrap(); + let block_id = bdk_chain::BlockId { height, hash: header.block_hash() }; + let latest_cp = locked_wallet.latest_checkpoint().insert(block_id); + let update = bdk_wallet::Update { chain: Some(latest_cp), ..Default::default() }; + if let Err(e) = locked_wallet.apply_update(update) { + log_error!(self.logger, "BIP157: Failed to apply header at height {}: {}", height, e); + } + } + pub(crate) fn get_cached_txs(&self) -> Vec> { self.inner.lock().unwrap().tx_graph().full_txs().map(|tx_node| tx_node.tx).collect() } @@ -329,13 +359,17 @@ impl Wallet { let tx_refs: Vec<( &Transaction, lightning::chain::chaininterface::TransactionType, - )> = - txs_to_broadcast - .iter() - .map(|tx| { - (tx, lightning::chain::chaininterface::TransactionType::Sweep { channels: vec![] }) - }) - .collect(); + )> = txs_to_broadcast + .iter() + .map(|tx| { + ( + tx, + lightning::chain::chaininterface::TransactionType::Sweep { + channels: vec![], + }, + ) + }) + .collect(); self.broadcaster.broadcast_transactions(&tx_refs); log_info!( self.logger, @@ -426,44 +460,50 @@ impl Wallet { ) -> Result { let fee_rate = self.fee_estimator.estimate_fee_rate(confirmation_target); - let mut locked_wallet = self.inner.lock().unwrap(); - let mut tx_builder = locked_wallet.build_tx(); + let tx = { + let mut locked_wallet = self.inner.lock().unwrap(); + let mut tx_builder = locked_wallet.build_tx(); - tx_builder.add_recipient(output_script, amount).fee_rate(fee_rate).nlocktime(locktime); + tx_builder.add_recipient(output_script, amount).fee_rate(fee_rate).nlocktime(locktime); - let mut psbt = match tx_builder.finish() { - Ok(psbt) => { - log_trace!(self.logger, "Created funding PSBT: {:?}", psbt); - psbt - }, - Err(err) => { - log_error!(self.logger, "Failed to create funding transaction: {}", err); - return Err(err.into()); - }, - }; + let mut psbt = match tx_builder.finish() { + Ok(psbt) => { + log_trace!(self.logger, "Created funding PSBT: {:?}", psbt); + psbt + }, + Err(err) => { + log_error!(self.logger, "Failed to create funding transaction: {}", err); + return Err(err.into()); + }, + }; - match locked_wallet.sign(&mut psbt, SignOptions::default()) { - Ok(finalized) => { - if !finalized { - return Err(Error::OnchainTxCreationFailed); - } - }, - Err(err) => { - log_error!(self.logger, "Failed to create funding transaction: {}", err); - return Err(err.into()); - }, - } + match locked_wallet.sign(&mut psbt, SignOptions::default()) { + Ok(finalized) => { + if !finalized { + return Err(Error::OnchainTxCreationFailed); + } + }, + Err(err) => { + log_error!(self.logger, "Failed to create funding transaction: {}", err); + return Err(err.into()); + }, + } - let mut locked_persister = self.persister.lock().unwrap(); - locked_wallet.persist(&mut locked_persister).map_err(|e| { - log_error!(self.logger, "Failed to persist wallet: {}", e); - Error::PersistenceFailed - })?; + let mut locked_persister = self.persister.lock().unwrap(); + locked_wallet.persist(&mut locked_persister).map_err(|e| { + log_error!(self.logger, "Failed to persist wallet: {}", e); + Error::PersistenceFailed + })?; - let tx = psbt.extract_tx().map_err(|e| { - log_error!(self.logger, "Failed to extract transaction: {}", e); - e - })?; + psbt.extract_tx().map_err(|e| { + log_error!(self.logger, "Failed to extract transaction: {}", e); + e + })? + }; // locked_wallet drops here + + // Re-seed watched scripts so the BIP-157 chain source learns about any + // change addresses that BDK derived internally when building the tx. + self.seed_watched_scripts(); Ok(tx) } @@ -473,6 +513,8 @@ impl Wallet { let mut locked_persister = self.persister.lock().unwrap(); let address_info = locked_wallet.reveal_next_address(KeychainKind::External); + // For BIP157 backends, register the script so kyoto watches it in compact filters. + self.chain_source.register_script(address_info.address.script_pubkey()); locked_wallet.persist(&mut locked_persister).map_err(|e| { log_error!(self.logger, "Failed to persist wallet: {}", e); Error::PersistenceFailed @@ -485,6 +527,8 @@ impl Wallet { let mut locked_persister = self.persister.lock().unwrap(); let address_info = locked_wallet.next_unused_address(KeychainKind::Internal); + // For BIP157 backends, register the script so kyoto watches it in compact filters. + self.chain_source.register_script(address_info.address.script_pubkey()); locked_wallet.persist(&mut locked_persister).map_err(|e| { log_error!(self.logger, "Failed to persist wallet: {}", e); Error::PersistenceFailed @@ -811,7 +855,8 @@ impl Wallet { let (sent, received) = locked_wallet.sent_and_received(&psbt.unsigned_tx); let drain_amount = sent - received; if spendable_amount_sats < drain_amount.to_sat() { - log_error!(self.logger, + log_error!( + self.logger, "Unable to send payment due to insufficient funds. Available: {}sats, Required: {}", spendable_amount_sats, drain_amount, @@ -846,6 +891,10 @@ impl Wallet { })? }; + // Re-seed watched scripts so the BIP-157 chain source learns about any + // change addresses that BDK derived internally when building the tx. + self.seed_watched_scripts(); + self.broadcaster.broadcast_transactions(&[( &tx, lightning::chain::chaininterface::TransactionType::Sweep { channels: vec![] }, @@ -853,6 +902,19 @@ impl Wallet { let txid = tx.compute_txid(); + // Eagerly register the just-sent tx as unconfirmed in BDK so the payment + // store entry is immediately visible via Node::payment(). Without this, + // backends without mempool support (e.g. BIP157) would only see the payment + // after the tx is confirmed in a block. + let now_secs = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(); + if let Err(e) = self.apply_mempool_txs(vec![(tx, now_secs)], vec![]) { + log_error!( + self.logger, + "Failed to eagerly apply outgoing transaction to payment store: {:?}", + e + ); + } + match send_amount { OnchainSendAmount::ExactRetainingReserve { amount_sats, .. } => { log_info!( @@ -1336,7 +1398,13 @@ impl Wallet { final_fee_rate_sat_per_kwu.saturating_mul(3).saturating_div(2), ); if required_fee_rate > max_allowed_fee_rate { - log_error!( self.logger, "BDK required fee rate {} exceeds sanity cap {} (1.5x our estimate) for tx {}", required_fee_rate, max_allowed_fee_rate, txid ); + log_error!( + self.logger, + "BDK required fee rate {} exceeds sanity cap {} (1.5x our estimate) for tx {}", + required_fee_rate, + max_allowed_fee_rate, + txid + ); return Err(Error::InvalidFeeRate); } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 1975d4253..859e7cdf3 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -240,10 +240,33 @@ pub(crate) fn setup_bitcoind_and_electrsd() -> (BitcoinD, ElectrsD) { (bitcoind, electrsd) } +/// Skip the current test when the BIP-157 chain source is active. +/// +/// Some tests rely on mempool visibility or gossip behaviour that is fundamentally +/// incompatible with compact block filters (no mempool, no `GossipVerifier`). +macro_rules! skip_if_bip157 { + ($chain_source:expr) => { + if matches!($chain_source, TestChainSource::Bip157(_)) { + println!("Skipping test: not compatible with BIP-157 chain source"); + return; + } + }; +} +pub(crate) use skip_if_bip157; + pub(crate) fn random_chain_source<'a>( bitcoind: &'a BitcoinD, electrsd: &'a ElectrsD, ) -> TestChainSource<'a> { - let r = rand::random_range(0..3); + // Allow forcing a specific backend via LDK_TEST_CHAIN_SOURCE env var. + // Valid values: "esplora"=0, "electrum"=1, "bitcoind-rpc"=2, "bitcoind-rest"=3, "bip157"=4 + let r = match std::env::var("LDK_TEST_CHAIN_SOURCE").ok().as_deref() { + Some("esplora") => 0, + Some("electrum") => 1, + Some("bitcoind-rpc") => 2, + Some("bitcoind-rest") => 3, + Some("bip157") => 4, + _ => rand::random_range(0..5), + }; match r { 0 => { println!("Randomly setting up Esplora chain syncing..."); @@ -261,6 +284,10 @@ pub(crate) fn random_chain_source<'a>( println!("Randomly setting up Bitcoind REST chain syncing..."); TestChainSource::BitcoindRestSync(bitcoind) }, + 4 => { + println!("Randomly setting up BIP-157 compact block filter syncing..."); + TestChainSource::Bip157(bitcoind) + }, _ => unreachable!(), } } @@ -523,7 +550,7 @@ pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) -> pub(crate) async fn generate_blocks_and_wait( bitcoind: &BitcoindClient, electrs: &E, num: usize, -) { +) -> u32 { let _ = bitcoind.create_wallet("ldk_node_test"); let _ = bitcoind.load_wallet("ldk_node_test"); print!("Generating {} blocks...", num); @@ -532,9 +559,11 @@ pub(crate) async fn generate_blocks_and_wait( let address = bitcoind.new_address().expect("failed to get new address"); // TODO: expect this Result once the WouldBlock issue is resolved upstream. let _block_hashes_res = bitcoind.generate_to_address(num, &address); - wait_for_block(electrs, cur_height as usize + num).await; + let target_height = cur_height as usize + num; + wait_for_block(electrs, target_height).await; print!(" Done!"); println!("\n"); + target_height as u32 } pub(crate) fn invalidate_blocks(bitcoind: &BitcoindClient, num_blocks: usize) { @@ -575,32 +604,34 @@ pub(crate) async fn wait_for_block(electrs: &E, min_height: usiz } } -pub(crate) async fn wait_for_tx(electrs: &E, txid: Txid) { - if electrs.transaction_get(&txid).is_ok() { +/// Wait until the node's LDK channel manager has processed blocks up to `min_height`. +/// +/// For BIP-157 backends the wallet is synced by a background event loop, so +/// `current_best_block` advances asynchronously as kyoto delivers blocks. This +/// function polls until the node has caught up, which is necessary before +/// checking balances after mining new blocks. +pub(crate) async fn wait_for_node_block(node: &TestNode, min_height: u32) { + if node.status().current_best_block.height >= min_height { return; } - exponential_backoff_poll(|| { - electrs.ping().unwrap(); - electrs.transaction_get(&txid).ok() + if node.status().current_best_block.height >= min_height { + Some(()) + } else { + None + } }) .await; } -pub(crate) async fn wait_for_outpoint_spend(electrs: &E, outpoint: OutPoint) { - let tx = electrs.transaction_get(&outpoint.txid).unwrap(); - let txout_script = tx.output.get(outpoint.vout as usize).unwrap().clone().script_pubkey; - - let is_spent = !electrs.script_get_history(&txout_script).unwrap().is_empty(); - if is_spent { +pub(crate) async fn wait_for_tx(electrs: &E, txid: Txid) { + if electrs.transaction_get(&txid).is_ok() { return; } exponential_backoff_poll(|| { electrs.ping().unwrap(); - - let is_spent = !electrs.script_get_history(&txout_script).unwrap().is_empty(); - is_spent.then_some(()) + electrs.transaction_get(&txid).ok() }) .await; } @@ -887,7 +918,9 @@ pub(crate) async fn do_channel_full_cycle( wait_for_tx(electrsd, funding_txo_a.txid).await; if !allow_0conf { - generate_blocks_and_wait(&bitcoind, electrsd, 6).await; + let tip = generate_blocks_and_wait(&bitcoind, electrsd, 6).await; + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; } node_a.sync_wallets().unwrap(); @@ -1236,17 +1269,23 @@ pub(crate) async fn do_channel_full_cycle( ); // Mine a block to give time for the HTLC to resolve - generate_blocks_and_wait(&bitcoind, electrsd, 1).await; + let tip = generate_blocks_and_wait(&bitcoind, electrsd, 1).await; + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; println!("\nB splices out to pay A"); let addr_a = node_a.onchain_payment().new_address().unwrap(); let splice_out_sat = funding_amount_sat / 2; node_b.splice_out(&user_channel_id_b, node_a.node_id(), &addr_a, splice_out_sat).unwrap(); - expect_splice_pending_event!(node_a, node_b.node_id()); + let splice_out_txo = expect_splice_pending_event!(node_a, node_b.node_id()); + wait_for_tx(electrsd, splice_out_txo.txid).await; expect_splice_pending_event!(node_b, node_a.node_id()); - generate_blocks_and_wait(&bitcoind, electrsd, 6).await; + let tip = generate_blocks_and_wait(&bitcoind, electrsd, 6).await; + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; + node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); @@ -1265,12 +1304,15 @@ pub(crate) async fn do_channel_full_cycle( let splice_in_sat = splice_out_sat; node_a.splice_in(&user_channel_id_a, node_b.node_id(), splice_in_sat).unwrap(); - expect_splice_pending_event!(node_a, node_b.node_id()); + let splice_in_txo = expect_splice_pending_event!(node_a, node_b.node_id()); expect_splice_pending_event!(node_b, node_a.node_id()); + wait_for_tx(electrsd, splice_in_txo.txid).await; - generate_blocks_and_wait(&bitcoind, electrsd, 6).await; + let tip = generate_blocks_and_wait(&bitcoind, electrsd, 6).await; node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; expect_channel_ready_event!(node_a, node_b.node_id()); expect_channel_ready_event!(node_b, node_a.node_id()); @@ -1285,24 +1327,30 @@ pub(crate) async fn do_channel_full_cycle( println!("\nB close_channel (force: {})", force_close); if force_close { - tokio::time::sleep(Duration::from_secs(1)).await; node_a.force_close_channel(&user_channel_id_a, node_b.node_id(), None).unwrap(); + tokio::time::sleep(Duration::from_secs(3)).await; } else { node_a.close_channel(&user_channel_id_a, node_b.node_id()).unwrap(); + tokio::time::sleep(Duration::from_secs(3)).await; } expect_event!(node_a, ChannelClosed); expect_event!(node_b, ChannelClosed); - wait_for_outpoint_spend(electrsd, funding_txo_b).await; + wait_for_tx(electrsd, splice_in_txo.txid).await; - generate_blocks_and_wait(&bitcoind, electrsd, 1).await; + let tip = generate_blocks_and_wait(&bitcoind, electrsd, 1).await; node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; if force_close { // Check node_b properly sees all balances and sweeps them. assert_eq!(node_b.list_balances().lightning_balances.len(), 1); + println!("before debug"); + println!("{:?}", node_b.list_balances().lightning_balances[0]); + println!("after debug"); match node_b.list_balances().lightning_balances[0] { LightningBalance::ClaimableAwaitingConfirmations { counterparty_node_id, @@ -1312,9 +1360,12 @@ pub(crate) async fn do_channel_full_cycle( assert_eq!(counterparty_node_id, node_a.node_id()); let cur_height = node_b.status().current_best_block.height; let blocks_to_go = confirmation_height - cur_height; - generate_blocks_and_wait(&bitcoind, electrsd, blocks_to_go as usize).await; - node_b.sync_wallets().unwrap(); + let tip = + generate_blocks_and_wait(&bitcoind, electrsd, blocks_to_go as usize).await; node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; }, _ => panic!("Unexpected balance state!"), } @@ -1322,12 +1373,16 @@ pub(crate) async fn do_channel_full_cycle( assert!(node_b.list_balances().lightning_balances.is_empty()); assert_eq!(node_b.list_balances().pending_balances_from_channel_closures.len(), 1); match node_b.list_balances().pending_balances_from_channel_closures[0] { - PendingSweepBalance::BroadcastAwaitingConfirmation { .. } => {}, + PendingSweepBalance::BroadcastAwaitingConfirmation { latest_spending_txid, .. } => { + wait_for_tx(electrsd, latest_spending_txid).await; + }, _ => panic!("Unexpected balance state!"), } - generate_blocks_and_wait(&bitcoind, electrsd, 1).await; + let tip = generate_blocks_and_wait(&bitcoind, electrsd, 1).await; node_b.sync_wallets().unwrap(); node_a.sync_wallets().unwrap(); + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; assert!(node_b.list_balances().lightning_balances.is_empty()); assert_eq!(node_b.list_balances().pending_balances_from_channel_closures.len(), 1); @@ -1335,9 +1390,11 @@ pub(crate) async fn do_channel_full_cycle( PendingSweepBalance::AwaitingThresholdConfirmations { .. } => {}, _ => panic!("Unexpected balance state!"), } - generate_blocks_and_wait(&bitcoind, electrsd, 5).await; + let tip = generate_blocks_and_wait(&bitcoind, electrsd, 5).await; node_b.sync_wallets().unwrap(); node_a.sync_wallets().unwrap(); + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; assert!(node_b.list_balances().lightning_balances.is_empty()); assert_eq!(node_b.list_balances().pending_balances_from_channel_closures.len(), 1); @@ -1353,9 +1410,12 @@ pub(crate) async fn do_channel_full_cycle( assert_eq!(counterparty_node_id, node_b.node_id()); let cur_height = node_a.status().current_best_block.height; let blocks_to_go = confirmation_height - cur_height; - generate_blocks_and_wait(&bitcoind, electrsd, blocks_to_go as usize).await; + let tip = + generate_blocks_and_wait(&bitcoind, electrsd, blocks_to_go as usize).await; node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; }, _ => panic!("Unexpected balance state!"), } @@ -1363,22 +1423,29 @@ pub(crate) async fn do_channel_full_cycle( assert!(node_a.list_balances().lightning_balances.is_empty()); assert_eq!(node_a.list_balances().pending_balances_from_channel_closures.len(), 1); match node_a.list_balances().pending_balances_from_channel_closures[0] { - PendingSweepBalance::BroadcastAwaitingConfirmation { .. } => {}, + PendingSweepBalance::BroadcastAwaitingConfirmation { latest_spending_txid, .. } => { + wait_for_tx(electrsd, latest_spending_txid).await; + }, _ => panic!("Unexpected balance state!"), } - generate_blocks_and_wait(&bitcoind, electrsd, 1).await; + let tip = generate_blocks_and_wait(&bitcoind, electrsd, 1).await; node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; + assert!(node_a.list_balances().lightning_balances.is_empty()); assert_eq!(node_a.list_balances().pending_balances_from_channel_closures.len(), 1); match node_a.list_balances().pending_balances_from_channel_closures[0] { PendingSweepBalance::AwaitingThresholdConfirmations { .. } => {}, _ => panic!("Unexpected balance state!"), } - generate_blocks_and_wait(&bitcoind, electrsd, 5).await; + let tip = generate_blocks_and_wait(&bitcoind, electrsd, 5).await; node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; } let sum_of_all_payments_sat = (push_msat diff --git a/tests/integration_tests_bip157.rs b/tests/integration_tests_bip157.rs index c3f9ffcbc..603e854b3 100644 --- a/tests/integration_tests_bip157.rs +++ b/tests/integration_tests_bip157.rs @@ -11,8 +11,8 @@ use bitcoin::{Amount, Network}; use common::{ expect_channel_pending_event, expect_channel_ready_event, expect_event, expect_payment_received_event, expect_payment_successful_event, generate_blocks_and_wait, - premine_and_distribute_funds, random_storage_path, setup_bitcoind_and_electrsd, - setup_builder, setup_two_nodes, wait_for_tx, TestChainSource, + premine_and_distribute_funds, random_storage_path, setup_bitcoind_and_electrsd, setup_builder, + setup_two_nodes, wait_for_tx, TestChainSource, }; use ldk_node::config::Config; use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy}; diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 3fde52dc4..defb91bb6 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -17,21 +17,23 @@ use bitcoin::hashes::Hash; use bitcoin::{Address, Amount, ScriptBuf, Txid}; use common::logging::{init_log_logger, validate_log_entry, MultiNodeLogger, TestLogWriter}; use common::{ - bump_fee_and_broadcast, distribute_funds_unconfirmed, do_channel_full_cycle, - expect_channel_pending_event, expect_channel_ready_event, expect_channel_ready_events, - expect_event, expect_payment_claimable_event, expect_payment_received_event, - expect_payment_successful_event, expect_splice_pending_event, generate_blocks_and_wait, - open_channel, open_channel_push_amt, open_channel_with_all, premine_and_distribute_funds, - premine_blocks, prepare_rbf, random_chain_source, random_config, random_listening_addresses, - setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, splice_in_with_all, - wait_for_tx, TestChainSource, TestStoreType, TestSyncStore, + bump_fee_and_broadcast, distribute_funds_unconfirmed, do_channel_full_cycle, + expect_channel_pending_event, expect_channel_ready_event, expect_channel_ready_events, + expect_event, expect_payment_claimable_event, expect_payment_received_event, + expect_payment_successful_event, expect_splice_pending_event, generate_blocks_and_wait, + open_channel, open_channel_push_amt, open_channel_with_all, premine_and_distribute_funds, + premine_blocks, prepare_rbf, random_chain_source, random_config, random_listening_addresses, + setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, skip_if_bip157, + splice_in_with_all, wait_for_node_block, wait_for_tx, TestChainSource, TestStoreType, + TestSyncStore, }; +use electrum_client::ElectrumApi; use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig}; use ldk_node::entropy::NodeEntropy; use ldk_node::liquidity::LSPS2ServiceConfig; use ldk_node::payment::{ - ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, - UnifiedPaymentResult, + ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, + UnifiedPaymentResult, }; use ldk_node::{Builder, Event, NodeError}; use lightning::ln::channelmanager::PaymentId; @@ -43,2765 +45,2920 @@ use log::LevelFilter; #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn channel_full_cycle() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); - do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false) - .await; + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false) + .await; } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn channel_full_cycle_force_close() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); - do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, true) - .await; + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, true) + .await; } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn channel_full_cycle_force_close_trusted_no_reserve() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, true); - do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, true) - .await; + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, true); + do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, true) + .await; } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn channel_full_cycle_0conf() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, true, true, false); - do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, true, true, false) - .await; + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + // BIP-157 has no mempool visibility: unconfirmed funding txs are invisible, so the + // outbound-payment assertion that follows sync_wallets() would always fail. + skip_if_bip157!(chain_source); + let (node_a, node_b) = setup_two_nodes(&chain_source, true, true, false); + do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, true, true, false) + .await; } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn channel_full_cycle_legacy_staticremotekey() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); - do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, false, false) - .await; + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); + do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, false, false) + .await; } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn channel_open_fails_when_funds_insufficient() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); - - let addr_a = node_a.onchain_payment().new_address().unwrap(); - let addr_b = node_b.onchain_payment().new_address().unwrap(); - - let premine_amount_sat = 100_000; - - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![addr_a, addr_b], - Amount::from_sat(premine_amount_sat), - ) - .await; - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); - assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); - - println!("\nA -- open_channel -> B"); - assert_eq!( - Err(NodeError::InsufficientFunds), - node_a.open_channel( - node_b.node_id(), - node_b.listening_addresses().unwrap().first().unwrap().clone(), - 120000, - None, - None, - ) - ); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + + let premine_amount_sat = 100_000; + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_a, addr_b], + Amount::from_sat(premine_amount_sat), + ) + .await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + + println!("\nA -- open_channel -> B"); + assert_eq!( + Err(NodeError::InsufficientFunds), + node_a.open_channel( + node_b.node_id(), + node_b.listening_addresses().unwrap().first().unwrap().clone(), + 120000, + None, + None, + ) + ); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn multi_hop_sending() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); - - // Setup and fund 5 nodes - let mut nodes = Vec::new(); - for _ in 0..5 { - let config = random_config(true); - let mut sync_config = EsploraSyncConfig::default(); - sync_config.background_sync_config = None; - setup_builder!(builder, config.node_config); - builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - let node = builder.build(config.node_entropy.into()).unwrap(); - node.start().unwrap(); - nodes.push(node); - } - - let addresses = nodes.iter().map(|n| n.onchain_payment().new_address().unwrap()).collect(); - let premine_amount_sat = 5_000_000; - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - addresses, - Amount::from_sat(premine_amount_sat), - ) - .await; - - for n in &nodes { - n.sync_wallets().unwrap(); - assert_eq!(n.list_balances().spendable_onchain_balance_sats, premine_amount_sat); - assert_eq!(n.next_event(), None); - } - - // Setup channel topology: - // (1M:0)- N2 -(1M:0) - // / \ - // N0 -(100k:0)-> N1 N4 - // \ / - // (1M:0)- N3 -(1M:0) - - open_channel(&nodes[0], &nodes[1], 100_000, true, &electrsd).await; - open_channel(&nodes[1], &nodes[2], 1_000_000, true, &electrsd).await; - // We need to sync wallets in-between back-to-back channel opens from the same node so BDK - // wallet picks up on the broadcast funding tx and doesn't double-spend itself. - // - // TODO: Remove once fixed in BDK. - nodes[1].sync_wallets().unwrap(); - open_channel(&nodes[1], &nodes[3], 1_000_000, true, &electrsd).await; - open_channel(&nodes[2], &nodes[4], 1_000_000, true, &electrsd).await; - open_channel(&nodes[3], &nodes[4], 1_000_000, true, &electrsd).await; - - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - - for n in &nodes { - n.sync_wallets().unwrap(); - } - - expect_event!(nodes[0], ChannelReady); - expect_event!(nodes[1], ChannelReady); - expect_event!(nodes[1], ChannelReady); - expect_event!(nodes[1], ChannelReady); - expect_event!(nodes[2], ChannelReady); - expect_event!(nodes[2], ChannelReady); - expect_event!(nodes[3], ChannelReady); - expect_event!(nodes[3], ChannelReady); - expect_event!(nodes[4], ChannelReady); - expect_event!(nodes[4], ChannelReady); - - // Sleep a bit for gossip to propagate. - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - - let route_params = RouteParametersConfig { - max_total_routing_fee_msat: Some(75_000), - max_total_cltv_expiry_delta: 1000, - max_path_count: 10, - max_channel_saturation_power_of_half: 2, - }; - - let invoice_description = - Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()); - let invoice = nodes[4] - .bolt11_payment() - .receive(2_500_000, &invoice_description.clone().into(), 9217) - .unwrap(); - nodes[0].bolt11_payment().send(&invoice, Some(route_params)).unwrap(); - - expect_event!(nodes[1], PaymentForwarded); - - // We expect that the payment goes through N2 or N3, so we check both for the PaymentForwarded event. - let node_2_fwd_event = matches!(nodes[2].next_event(), Some(Event::PaymentForwarded { .. })); - let node_3_fwd_event = matches!(nodes[3].next_event(), Some(Event::PaymentForwarded { .. })); - assert!(node_2_fwd_event || node_3_fwd_event); - - let payment_id = expect_payment_received_event!(&nodes[4], 2_500_000); - let fee_paid_msat = Some(2000); - expect_payment_successful_event!(nodes[0], payment_id, Some(fee_paid_msat)); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + + // Setup and fund 5 nodes + let mut nodes = Vec::new(); + for _ in 0..5 { + let config = random_config(true); + let mut sync_config = EsploraSyncConfig::default(); + sync_config.background_sync_config = None; + setup_builder!(builder, config.node_config); + builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + let node = builder.build(config.node_entropy.into()).unwrap(); + node.start().unwrap(); + nodes.push(node); + } + + let addresses = nodes.iter().map(|n| n.onchain_payment().new_address().unwrap()).collect(); + let premine_amount_sat = 5_000_000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + addresses, + Amount::from_sat(premine_amount_sat), + ) + .await; + + for n in &nodes { + n.sync_wallets().unwrap(); + assert_eq!(n.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(n.next_event(), None); + } + + // Setup channel topology: + // (1M:0)- N2 -(1M:0) + // / \ + // N0 -(100k:0)-> N1 N4 + // \ / + // (1M:0)- N3 -(1M:0) + + open_channel(&nodes[0], &nodes[1], 100_000, true, &electrsd).await; + open_channel(&nodes[1], &nodes[2], 1_000_000, true, &electrsd).await; + // We need to sync wallets in-between back-to-back channel opens from the same node so BDK + // wallet picks up on the broadcast funding tx and doesn't double-spend itself. + // + // TODO: Remove once fixed in BDK. + nodes[1].sync_wallets().unwrap(); + open_channel(&nodes[1], &nodes[3], 1_000_000, true, &electrsd).await; + open_channel(&nodes[2], &nodes[4], 1_000_000, true, &electrsd).await; + open_channel(&nodes[3], &nodes[4], 1_000_000, true, &electrsd).await; + + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + for n in &nodes { + n.sync_wallets().unwrap(); + wait_for_node_block(&n, tip).await; + } + + expect_event!(nodes[0], ChannelReady); + expect_event!(nodes[1], ChannelReady); + expect_event!(nodes[1], ChannelReady); + expect_event!(nodes[1], ChannelReady); + expect_event!(nodes[2], ChannelReady); + expect_event!(nodes[2], ChannelReady); + expect_event!(nodes[3], ChannelReady); + expect_event!(nodes[3], ChannelReady); + expect_event!(nodes[4], ChannelReady); + expect_event!(nodes[4], ChannelReady); + + // Sleep a bit for gossip to propagate. + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let route_params = RouteParametersConfig { + max_total_routing_fee_msat: Some(75_000), + max_total_cltv_expiry_delta: 1000, + max_path_count: 10, + max_channel_saturation_power_of_half: 2, + }; + + let invoice_description = + Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()); + let invoice = nodes[4] + .bolt11_payment() + .receive(2_500_000, &invoice_description.clone().into(), 9217) + .unwrap(); + nodes[0].bolt11_payment().send(&invoice, Some(route_params)).unwrap(); + + expect_event!(nodes[1], PaymentForwarded); + + // We expect that the payment goes through N2 or N3, so we check both for the PaymentForwarded event. + let node_2_fwd_event = matches!(nodes[2].next_event(), Some(Event::PaymentForwarded { .. })); + let node_3_fwd_event = matches!(nodes[3].next_event(), Some(Event::PaymentForwarded { .. })); + assert!(node_2_fwd_event || node_3_fwd_event); + + let payment_id = expect_payment_received_event!(&nodes[4], 2_500_000); + let fee_paid_msat = Some(2000); + expect_payment_successful_event!(nodes[0], payment_id, Some(fee_paid_msat)); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn start_stop_reinit() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let config = random_config(true); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let config = random_config(true); - let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); - let test_sync_store = TestSyncStore::new(config.node_config.storage_dir_path.clone().into()); + let test_sync_store = TestSyncStore::new(config.node_config.storage_dir_path.clone().into()); - let mut sync_config = EsploraSyncConfig::default(); - sync_config.background_sync_config = None; - setup_builder!(builder, config.node_config); - builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + let mut sync_config = EsploraSyncConfig::default(); + sync_config.background_sync_config = None; + setup_builder!(builder, config.node_config); + builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - let node = - builder.build_with_store(config.node_entropy.into(), test_sync_store.clone()).unwrap(); - node.start().unwrap(); + let node = + builder.build_with_store(config.node_entropy.into(), test_sync_store.clone()).unwrap(); + node.start().unwrap(); - let expected_node_id = node.node_id(); - assert_eq!(node.start(), Err(NodeError::AlreadyRunning)); + let expected_node_id = node.node_id(); + assert_eq!(node.start(), Err(NodeError::AlreadyRunning)); - let funding_address = node.onchain_payment().new_address().unwrap(); + let funding_address = node.onchain_payment().new_address().unwrap(); - assert_eq!(node.list_balances().total_onchain_balance_sats, 0); + assert_eq!(node.list_balances().total_onchain_balance_sats, 0); - let expected_amount = Amount::from_sat(100000); - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![funding_address], - expected_amount, - ) - .await; + let expected_amount = Amount::from_sat(100000); + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![funding_address], + expected_amount, + ) + .await; - node.sync_wallets().unwrap(); - assert_eq!(node.list_balances().spendable_onchain_balance_sats, expected_amount.to_sat()); + node.sync_wallets().unwrap(); + assert_eq!(node.list_balances().spendable_onchain_balance_sats, expected_amount.to_sat()); - let log_file = format!("{}/ldk_node.log", config.node_config.clone().storage_dir_path); - assert!(std::path::Path::new(&log_file).exists()); + let log_file = format!("{}/ldk_node.log", config.node_config.clone().storage_dir_path); + assert!(std::path::Path::new(&log_file).exists()); - node.stop().unwrap(); - assert_eq!(node.stop(), Err(NodeError::NotRunning)); + node.stop().unwrap(); + assert_eq!(node.stop(), Err(NodeError::NotRunning)); - node.start().unwrap(); - assert_eq!(node.start(), Err(NodeError::AlreadyRunning)); + node.start().unwrap(); + assert_eq!(node.start(), Err(NodeError::AlreadyRunning)); - node.stop().unwrap(); - assert_eq!(node.stop(), Err(NodeError::NotRunning)); - drop(node); + node.stop().unwrap(); + assert_eq!(node.stop(), Err(NodeError::NotRunning)); + drop(node); - setup_builder!(builder, config.node_config); - builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + setup_builder!(builder, config.node_config); + builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - let reinitialized_node = - builder.build_with_store(config.node_entropy.into(), test_sync_store).unwrap(); - reinitialized_node.start().unwrap(); - assert_eq!(reinitialized_node.node_id(), expected_node_id); + let reinitialized_node = + builder.build_with_store(config.node_entropy.into(), test_sync_store).unwrap(); + reinitialized_node.start().unwrap(); + assert_eq!(reinitialized_node.node_id(), expected_node_id); - assert_eq!( - reinitialized_node.list_balances().spendable_onchain_balance_sats, - expected_amount.to_sat() - ); + assert_eq!( + reinitialized_node.list_balances().spendable_onchain_balance_sats, + expected_amount.to_sat() + ); - reinitialized_node.sync_wallets().unwrap(); - assert_eq!( - reinitialized_node.list_balances().spendable_onchain_balance_sats, - expected_amount.to_sat() - ); + reinitialized_node.sync_wallets().unwrap(); + assert_eq!( + reinitialized_node.list_balances().spendable_onchain_balance_sats, + expected_amount.to_sat() + ); - reinitialized_node.stop().unwrap(); + reinitialized_node.stop().unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn onchain_send_receive() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); - - let addr_a = node_a.onchain_payment().new_address().unwrap(); - let addr_b = node_b.onchain_payment().new_address().unwrap(); - // This is a Bitcoin Testnet address. Sending funds to this address from the Regtest network will fail - let static_address = "tb1q0d40e5rta4fty63z64gztf8c3v20cvet6v2jdh"; - let unchecked_address = Address::::from_str(static_address).unwrap(); - let addr_c = unchecked_address.assume_checked(); - - let premine_amount_sat = 1_100_000; - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![addr_a.clone(), addr_b.clone()], - Amount::from_sat(premine_amount_sat), - ) - .await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); - assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); - - let node_a_payments = node_a.list_payments(); - let node_b_payments = node_b.list_payments(); - for payments in [&node_a_payments, &node_b_payments] { - assert_eq!(payments.len(), 1) - } - for p in [node_a_payments.first().unwrap(), node_b_payments.first().unwrap()] { - assert_eq!(p.amount_msat, Some(premine_amount_sat * 1000)); - assert_eq!(p.direction, PaymentDirection::Inbound); - // We got only 1-conf here, so we're only pending for now. - assert_eq!(p.status, PaymentStatus::Pending); - match p.kind { - PaymentKind::Onchain { status, .. } => { - assert!(matches!(status, ConfirmationStatus::Confirmed { .. })); - }, - _ => panic!("Unexpected payment kind"), - } - } - - let channel_amount_sat = 1_000_000; - let reserve_amount_sat = 25_000; - open_channel(&node_b, &node_a, channel_amount_sat, true, &electrsd).await; - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - expect_channel_ready_event!(node_a, node_b.node_id()); - expect_channel_ready_event!(node_b, node_a.node_id()); - - let node_a_payments = - node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); - assert_eq!(node_a_payments.len(), 1); - let node_b_payments = - node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); - assert_eq!(node_b_payments.len(), 2); - - let onchain_fee_buffer_sat = 1000; - let expected_node_a_balance = premine_amount_sat - reserve_amount_sat; - let expected_node_b_balance_lower = - premine_amount_sat - channel_amount_sat - reserve_amount_sat - onchain_fee_buffer_sat; - let expected_node_b_balance_upper = - premine_amount_sat - channel_amount_sat - reserve_amount_sat; - assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, expected_node_a_balance); - assert!(node_b.list_balances().spendable_onchain_balance_sats > expected_node_b_balance_lower); - assert!(node_b.list_balances().spendable_onchain_balance_sats < expected_node_b_balance_upper); - - assert_eq!( - Err(NodeError::InsufficientFunds), - node_a.onchain_payment().send_to_address(&addr_b, expected_node_a_balance + 1, None) - ); - - assert_eq!( - Err(NodeError::InvalidAddress), - node_a.onchain_payment().send_to_address(&addr_c, expected_node_a_balance + 1, None) - ); - - assert_eq!( - Err(NodeError::InvalidAddress), - node_a.onchain_payment().send_all_to_address(&addr_c, true, None) - ); - - let amount_to_send_sats = 54321; - let txid = - node_b.onchain_payment().send_to_address(&addr_a, amount_to_send_sats, None).unwrap(); - wait_for_tx(&electrsd.client, txid).await; - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - let payment_id = PaymentId(txid.to_byte_array()); - let payment_a = node_a.payment(&payment_id).unwrap(); - assert_eq!(payment_a.status, PaymentStatus::Pending); - match payment_a.kind { - PaymentKind::Onchain { status, .. } => { - assert!(matches!(status, ConfirmationStatus::Unconfirmed)); - }, - _ => panic!("Unexpected payment kind"), - } - assert!(payment_a.fee_paid_msat > Some(0)); - let payment_b = node_b.payment(&payment_id).unwrap(); - assert_eq!(payment_b.status, PaymentStatus::Pending); - match payment_a.kind { - PaymentKind::Onchain { status, .. } => { - assert!(matches!(status, ConfirmationStatus::Unconfirmed)); - }, - _ => panic!("Unexpected payment kind"), - } - assert!(payment_b.fee_paid_msat > Some(0)); - assert_eq!(payment_a.amount_msat, Some(amount_to_send_sats * 1000)); - assert_eq!(payment_a.amount_msat, payment_b.amount_msat); - assert_eq!(payment_a.fee_paid_msat, payment_b.fee_paid_msat); - - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - let expected_node_a_balance = expected_node_a_balance + amount_to_send_sats; - let expected_node_b_balance_lower = expected_node_b_balance_lower - amount_to_send_sats; - let expected_node_b_balance_upper = expected_node_b_balance_upper - amount_to_send_sats; - assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, expected_node_a_balance); - assert!(node_b.list_balances().spendable_onchain_balance_sats > expected_node_b_balance_lower); - assert!(node_b.list_balances().spendable_onchain_balance_sats < expected_node_b_balance_upper); - - let node_a_payments = - node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); - assert_eq!(node_a_payments.len(), 2); - let node_b_payments = - node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); - assert_eq!(node_b_payments.len(), 3); - - let payment_a = node_a.payment(&payment_id).unwrap(); - match payment_a.kind { - PaymentKind::Onchain { txid: _txid, status } => { - assert_eq!(_txid, txid); - assert!(matches!(status, ConfirmationStatus::Confirmed { .. })); - }, - _ => panic!("Unexpected payment kind"), - } - - let payment_b = node_a.payment(&payment_id).unwrap(); - match payment_b.kind { - PaymentKind::Onchain { txid: _txid, status } => { - assert_eq!(_txid, txid); - assert!(matches!(status, ConfirmationStatus::Confirmed { .. })); - }, - _ => panic!("Unexpected payment kind"), - } - - let addr_b = node_b.onchain_payment().new_address().unwrap(); - let txid = node_a.onchain_payment().send_all_to_address(&addr_b, true, None).unwrap(); - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - wait_for_tx(&electrsd.client, txid).await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - let expected_node_b_balance_lower = expected_node_b_balance_lower + expected_node_a_balance; - let expected_node_b_balance_upper = expected_node_b_balance_upper + expected_node_a_balance; - let expected_node_a_balance = 0; - assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, expected_node_a_balance); - assert_eq!(node_a.list_balances().total_onchain_balance_sats, reserve_amount_sat); - assert!(node_b.list_balances().spendable_onchain_balance_sats > expected_node_b_balance_lower); - assert!(node_b.list_balances().spendable_onchain_balance_sats < expected_node_b_balance_upper); - - let node_a_payments = - node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); - assert_eq!(node_a_payments.len(), 3); - let node_b_payments = - node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); - assert_eq!(node_b_payments.len(), 4); - - let addr_b = node_b.onchain_payment().new_address().unwrap(); - let txid = node_a.onchain_payment().send_all_to_address(&addr_b, false, None).unwrap(); - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - wait_for_tx(&electrsd.client, txid).await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - let expected_node_b_balance_lower = expected_node_b_balance_lower + reserve_amount_sat; - let expected_node_b_balance_upper = expected_node_b_balance_upper + reserve_amount_sat; - let expected_node_a_balance = 0; - - assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, expected_node_a_balance); - assert_eq!(node_a.list_balances().total_onchain_balance_sats, expected_node_a_balance); - assert!(node_b.list_balances().spendable_onchain_balance_sats > expected_node_b_balance_lower); - assert!(node_b.list_balances().spendable_onchain_balance_sats < expected_node_b_balance_upper); - - let node_a_payments = - node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); - assert_eq!(node_a_payments.len(), 4); - let node_b_payments = - node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); - assert_eq!(node_b_payments.len(), 5); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + // This is a Bitcoin Testnet address. Sending funds to this address from the Regtest network will fail + let static_address = "tb1q0d40e5rta4fty63z64gztf8c3v20cvet6v2jdh"; + let unchecked_address = Address::::from_str(static_address).unwrap(); + let addr_c = unchecked_address.assume_checked(); + + let premine_amount_sat = 1_100_000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_a.clone(), addr_b.clone()], + Amount::from_sat(premine_amount_sat), + ) + .await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + + let node_a_payments = node_a.list_payments(); + let node_b_payments = node_b.list_payments(); + for payments in [&node_a_payments, &node_b_payments] { + assert_eq!(payments.len(), 1) + } + for p in [node_a_payments.first().unwrap(), node_b_payments.first().unwrap()] { + assert_eq!(p.amount_msat, Some(premine_amount_sat * 1000)); + assert_eq!(p.direction, PaymentDirection::Inbound); + // We got only 1-conf here, so we're only pending for now. + assert_eq!(p.status, PaymentStatus::Pending); + match p.kind { + PaymentKind::Onchain { status, .. } => { + assert!(matches!(status, ConfirmationStatus::Confirmed { .. })); + }, + _ => panic!("Unexpected payment kind"), + } + } + + let channel_amount_sat = 1_000_000; + let reserve_amount_sat = 25_000; + let z = open_channel(&node_b, &node_a, channel_amount_sat, true, &electrsd).await; + + wait_for_tx(&electrsd.client, z.txid).await; + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + println!( + "[funding] target tip={} node_a best_block={} node_b best_block={}", + tip, + node_a.status().current_best_block.height, + node_b.status().current_best_block.height, + ); + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; + println!( + "[funding] after wait node_a best_block={} node_b best_block={}", + node_a.status().current_best_block.height, + node_b.status().current_best_block.height, + ); + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + let node_a_payments = + node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); + assert_eq!(node_a_payments.len(), 1); + let node_b_payments = + node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); + assert_eq!(node_b_payments.len(), 2); + + // Compute the exact fee paid for the channel funding transaction so the balance + // assertion is backend-agnostic (different backends use different fee rates). + let funding_tx = electrsd.client.transaction_get(&z.txid).unwrap(); + let funding_tx_input_sats: u64 = funding_tx + .input + .iter() + .map(|inp| { + electrsd.client.transaction_get(&inp.previous_output.txid).unwrap().output + [inp.previous_output.vout as usize] + .value + .to_sat() + }) + .sum(); + let funding_tx_output_sats: u64 = funding_tx.output.iter().map(|o| o.value.to_sat()).sum(); + let funding_tx_fee_sats = funding_tx_input_sats - funding_tx_output_sats; + + let expected_node_a_balance = premine_amount_sat - reserve_amount_sat; + let expected_node_b_balance = + premine_amount_sat - channel_amount_sat - reserve_amount_sat - funding_tx_fee_sats; + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, expected_node_a_balance); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, expected_node_b_balance); + + assert_eq!( + Err(NodeError::InsufficientFunds), + node_a.onchain_payment().send_to_address(&addr_b, expected_node_a_balance + 1, None) + ); + + assert_eq!( + Err(NodeError::InvalidAddress), + node_a.onchain_payment().send_to_address(&addr_c, expected_node_a_balance + 1, None) + ); + + assert_eq!( + Err(NodeError::InvalidAddress), + node_a.onchain_payment().send_all_to_address(&addr_c, true, None) + ); + + let amount_to_send_sats = 54321; + let txid = + node_b.onchain_payment().send_to_address(&addr_a, amount_to_send_sats, None).unwrap(); + wait_for_tx(&electrsd.client, txid).await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + let payment_id = PaymentId(txid.to_byte_array()); + // BIP-157 has no mempool visibility, so the receiving node only sees the tx once + // it is confirmed in a block. Skip pre-confirmation checks for BIP-157. + if !matches!(chain_source, TestChainSource::Bip157(_)) { + let payment_a = node_a.payment(&payment_id).unwrap(); + assert_eq!(payment_a.status, PaymentStatus::Pending); + match payment_a.kind { + PaymentKind::Onchain { status, .. } => { + assert!(matches!(status, ConfirmationStatus::Unconfirmed)); + }, + _ => panic!("Unexpected payment kind"), + } + assert!(payment_a.fee_paid_msat > Some(0)); + let payment_b = node_b.payment(&payment_id).unwrap(); + assert_eq!(payment_b.status, PaymentStatus::Pending); + match payment_b.kind { + PaymentKind::Onchain { status, .. } => { + assert!(matches!(status, ConfirmationStatus::Unconfirmed)); + }, + _ => panic!("Unexpected payment kind"), + } + assert!(payment_b.fee_paid_msat > Some(0)); + assert_eq!(payment_a.amount_msat, Some(amount_to_send_sats * 1000)); + assert_eq!(payment_a.amount_msat, payment_b.amount_msat); + assert_eq!(payment_a.fee_paid_msat, payment_b.fee_paid_msat); + } + + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + println!( + "[send] target tip={} node_a best_block={} node_b best_block={}", + tip, + node_a.status().current_best_block.height, + node_b.status().current_best_block.height, + ); + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; + println!( + "[send] after wait node_a best_block={} node_b best_block={}", + node_a.status().current_best_block.height, + node_b.status().current_best_block.height, + ); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + let send_tx = electrsd.client.transaction_get(&txid).unwrap(); + let send_tx_input_sats: u64 = send_tx + .input + .iter() + .map(|inp| { + electrsd.client.transaction_get(&inp.previous_output.txid).unwrap().output + [inp.previous_output.vout as usize] + .value + .to_sat() + }) + .sum(); + let send_tx_output_sats: u64 = send_tx.output.iter().map(|o| o.value.to_sat()).sum(); + let send_tx_fee_sats = send_tx_input_sats - send_tx_output_sats; + + let expected_node_a_balance = expected_node_a_balance + amount_to_send_sats; + let expected_node_b_balance = expected_node_b_balance - amount_to_send_sats - send_tx_fee_sats; + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, expected_node_a_balance); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, expected_node_b_balance); + + let node_a_payments = + node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); + assert_eq!(node_a_payments.len(), 2); + let node_b_payments = + node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); + assert_eq!(node_b_payments.len(), 3); + + let payment_a = node_a.payment(&payment_id).unwrap(); + match payment_a.kind { + PaymentKind::Onchain { txid: _txid, status } => { + assert_eq!(_txid, txid); + assert!(matches!(status, ConfirmationStatus::Confirmed { .. })); + }, + _ => panic!("Unexpected payment kind"), + } + + let payment_b = node_a.payment(&payment_id).unwrap(); + match payment_b.kind { + PaymentKind::Onchain { txid: _txid, status } => { + assert_eq!(_txid, txid); + assert!(matches!(status, ConfirmationStatus::Confirmed { .. })); + }, + _ => panic!("Unexpected payment kind"), + } + + let addr_b = node_b.onchain_payment().new_address().unwrap(); + let txid = node_a.onchain_payment().send_all_to_address(&addr_b, true, None).unwrap(); + wait_for_tx(&electrsd.client, txid).await; + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; + + let send_all_tx = electrsd.client.transaction_get(&txid).unwrap(); + let send_all_received_by_b = send_all_tx + .output + .iter() + .find(|o| o.script_pubkey == addr_b.script_pubkey()) + .map(|o| o.value.to_sat()) + .expect("send_all output to addr_b not found"); + let expected_node_b_balance = expected_node_b_balance + send_all_received_by_b; + let expected_node_a_balance = 0; + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, expected_node_a_balance); + assert_eq!(node_a.list_balances().total_onchain_balance_sats, reserve_amount_sat); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, expected_node_b_balance); + + let node_a_payments = + node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); + assert_eq!(node_a_payments.len(), 3); + let node_b_payments = + node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); + assert_eq!(node_b_payments.len(), 4); + + let addr_b = node_b.onchain_payment().new_address().unwrap(); + let txid = node_a.onchain_payment().send_all_to_address(&addr_b, false, None).unwrap(); + wait_for_tx(&electrsd.client, txid).await; + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; + + let send_all_tx2 = electrsd.client.transaction_get(&txid).unwrap(); + let send_all2_received_by_b = send_all_tx2 + .output + .iter() + .find(|o| o.script_pubkey == addr_b.script_pubkey()) + .map(|o| o.value.to_sat()) + .expect("second send_all output to addr_b not found"); + let expected_node_b_balance = expected_node_b_balance + send_all2_received_by_b; + let expected_node_a_balance = 0; + + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, expected_node_a_balance); + assert_eq!(node_a.list_balances().total_onchain_balance_sats, expected_node_a_balance); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, expected_node_b_balance); + + let node_a_payments = + node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); + assert_eq!(node_a_payments.len(), 4); + let node_b_payments = + node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); + assert_eq!(node_b_payments.len(), 5); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn onchain_send_all_retains_reserve() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); - - // Setup nodes - let addr_a = node_a.onchain_payment().new_address().unwrap(); - let addr_b = node_b.onchain_payment().new_address().unwrap(); - - let premine_amount_sat = 1_000_000; - let reserve_amount_sat = 25_000; - let onchain_fee_buffer_sat = 1000; - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![addr_a.clone(), addr_b.clone()], - Amount::from_sat(premine_amount_sat), - ) - .await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); - assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); - - // Send all over, with 0 reserve as we don't have any channels open. - let txid = node_a.onchain_payment().send_all_to_address(&addr_b, true, None).unwrap(); - - wait_for_tx(&electrsd.client, txid).await; - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - // Check node a sent all and node b received it - assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, 0); - assert!(((premine_amount_sat * 2 - onchain_fee_buffer_sat)..=(premine_amount_sat * 2)) - .contains(&node_b.list_balances().spendable_onchain_balance_sats)); - - // Refill to make sure we have enough reserve for the channel open. - let txid = bitcoind - .client - .send_to_address(&addr_a, Amount::from_sat(reserve_amount_sat)) - .unwrap() - .0 - .parse() - .unwrap(); - wait_for_tx(&electrsd.client, txid).await; - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, reserve_amount_sat); - - // Open a channel. - open_channel(&node_b, &node_a, premine_amount_sat, false, &electrsd).await; - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - expect_channel_ready_event!(node_a, node_b.node_id()); - expect_channel_ready_event!(node_b, node_a.node_id()); - - // Check node a sent all and node b received it - assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, 0); - assert!(((premine_amount_sat - reserve_amount_sat - onchain_fee_buffer_sat) - ..=premine_amount_sat) - .contains(&node_b.list_balances().spendable_onchain_balance_sats)); - - // Send all over again, this time ensuring the reserve is accounted for - let txid = node_b.onchain_payment().send_all_to_address(&addr_a, true, None).unwrap(); - - wait_for_tx(&electrsd.client, txid).await; - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - // Check node b sent all and node a received it - assert_eq!(node_b.list_balances().total_onchain_balance_sats, reserve_amount_sat); - assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, 0); - assert!(((premine_amount_sat - reserve_amount_sat - onchain_fee_buffer_sat) - ..=premine_amount_sat) - .contains(&node_a.list_balances().spendable_onchain_balance_sats)); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + + // Setup nodes + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + + let premine_amount_sat = 1_000_000; + let reserve_amount_sat = 25_000; + let onchain_fee_buffer_sat = 1000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_a.clone(), addr_b.clone()], + Amount::from_sat(premine_amount_sat), + ) + .await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + + // Send all over, with 0 reserve as we don't have any channels open. + let txid = node_a.onchain_payment().send_all_to_address(&addr_b, true, None).unwrap(); + + wait_for_tx(&electrsd.client, txid).await; + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; + + // Check node a sent all and node b received it + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, 0); + assert!(((premine_amount_sat * 2 - onchain_fee_buffer_sat)..=(premine_amount_sat * 2)) + .contains(&node_b.list_balances().spendable_onchain_balance_sats)); + + // Refill to make sure we have enough reserve for the channel open. + let txid = bitcoind + .client + .send_to_address(&addr_a, Amount::from_sat(reserve_amount_sat)) + .unwrap() + .0 + .parse() + .unwrap(); + wait_for_tx(&electrsd.client, txid).await; + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; + + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, reserve_amount_sat); + + // Open a channel. + let outpoint = open_channel(&node_b, &node_a, premine_amount_sat, false, &electrsd).await; + wait_for_tx(&electrsd.client, outpoint.txid).await; + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; + + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + // Check node a sent all and node b received it + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, 0); + assert!(((premine_amount_sat - reserve_amount_sat - onchain_fee_buffer_sat) + ..=premine_amount_sat) + .contains(&node_b.list_balances().spendable_onchain_balance_sats)); + + // Send all over again, this time ensuring the reserve is accounted for + let txid = node_b.onchain_payment().send_all_to_address(&addr_a, true, None).unwrap(); + + wait_for_tx(&electrsd.client, txid).await; + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; + + // Check node b sent all and node a received it + assert_eq!(node_b.list_balances().total_onchain_balance_sats, reserve_amount_sat); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, 0); + assert!(((premine_amount_sat - reserve_amount_sat - onchain_fee_buffer_sat) + ..=premine_amount_sat) + .contains(&node_a.list_balances().spendable_onchain_balance_sats)); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn onchain_wallet_recovery() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - - let chain_source = random_chain_source(&bitcoind, &electrsd); - - let original_config = random_config(true); - let original_node_entropy = original_config.node_entropy; - let original_node = setup_node(&chain_source, original_config); - - let premine_amount_sat = 100_000; - - let addr_1 = original_node.onchain_payment().new_address().unwrap(); - - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![addr_1], - Amount::from_sat(premine_amount_sat), - ) - .await; - original_node.sync_wallets().unwrap(); - assert_eq!(original_node.list_balances().spendable_onchain_balance_sats, premine_amount_sat); - - let addr_2 = original_node.onchain_payment().new_address().unwrap(); - - let txid = bitcoind - .client - .send_to_address(&addr_2, Amount::from_sat(premine_amount_sat)) - .unwrap() - .0 - .parse() - .unwrap(); - wait_for_tx(&electrsd.client, txid).await; - - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1).await; - - original_node.sync_wallets().unwrap(); - assert_eq!( - original_node.list_balances().spendable_onchain_balance_sats, - premine_amount_sat * 2 - ); - - original_node.stop().unwrap(); - drop(original_node); - - // Now we start from scratch, only the seed remains the same. - let mut recovered_config = random_config(true); - recovered_config.node_entropy = original_node_entropy; - recovered_config.recovery_mode = true; - let recovered_node = setup_node(&chain_source, recovered_config); - - recovered_node.sync_wallets().unwrap(); - assert_eq!( - recovered_node.list_balances().spendable_onchain_balance_sats, - premine_amount_sat * 2 - ); - - // Check we sync even when skipping some addresses. - let _addr_3 = recovered_node.onchain_payment().new_address().unwrap(); - let _addr_4 = recovered_node.onchain_payment().new_address().unwrap(); - let _addr_5 = recovered_node.onchain_payment().new_address().unwrap(); - let addr_6 = recovered_node.onchain_payment().new_address().unwrap(); - - let txid = bitcoind - .client - .send_to_address(&addr_6, Amount::from_sat(premine_amount_sat)) - .unwrap() - .0 - .parse() - .unwrap(); - wait_for_tx(&electrsd.client, txid).await; - - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1).await; - - recovered_node.sync_wallets().unwrap(); - assert_eq!( - recovered_node.list_balances().spendable_onchain_balance_sats, - premine_amount_sat * 3 - ); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + + let chain_source = random_chain_source(&bitcoind, &electrsd); + + let original_config = random_config(true); + let original_node_entropy = original_config.node_entropy; + let original_node = setup_node(&chain_source, original_config); + + let premine_amount_sat = 100_000; + + let addr_1 = original_node.onchain_payment().new_address().unwrap(); + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_1], + Amount::from_sat(premine_amount_sat), + ) + .await; + original_node.sync_wallets().unwrap(); + assert_eq!(original_node.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + + let addr_2 = original_node.onchain_payment().new_address().unwrap(); + + let txid = bitcoind + .client + .send_to_address(&addr_2, Amount::from_sat(premine_amount_sat)) + .unwrap() + .0 + .parse() + .unwrap(); + wait_for_tx(&electrsd.client, txid).await; + + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1).await; + + original_node.sync_wallets().unwrap(); + wait_for_node_block(&original_node, tip).await; + + assert_eq!( + original_node.list_balances().spendable_onchain_balance_sats, + premine_amount_sat * 2 + ); + + original_node.stop().unwrap(); + drop(original_node); + + // Now we start from scratch, only the seed remains the same. + let mut recovered_config = random_config(true); + recovered_config.node_entropy = original_node_entropy; + recovered_config.recovery_mode = true; + let recovered_node = setup_node(&chain_source, recovered_config); + + recovered_node.sync_wallets().unwrap(); + assert_eq!( + recovered_node.list_balances().spendable_onchain_balance_sats, + premine_amount_sat * 2 + ); + + // Check we sync even when skipping some addresses. + let _addr_3 = recovered_node.onchain_payment().new_address().unwrap(); + let _addr_4 = recovered_node.onchain_payment().new_address().unwrap(); + let _addr_5 = recovered_node.onchain_payment().new_address().unwrap(); + let addr_6 = recovered_node.onchain_payment().new_address().unwrap(); + + let txid = bitcoind + .client + .send_to_address(&addr_6, Amount::from_sat(premine_amount_sat)) + .unwrap() + .0 + .parse() + .unwrap(); + wait_for_tx(&electrsd.client, txid).await; + + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1).await; + + recovered_node.sync_wallets().unwrap(); + wait_for_node_block(&recovered_node, tip).await; + + assert_eq!( + recovered_node.list_balances().spendable_onchain_balance_sats, + premine_amount_sat * 3 + ); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_rbf_via_mempool() { - run_rbf_test(false).await; + run_rbf_test(false).await; } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_rbf_via_direct_block_insertion() { - run_rbf_test(true).await; + run_rbf_test(true).await; } // `is_insert_block`: // - `true`: transaction is mined immediately (no mempool), testing confirmed-Tx handling. // - `false`: transaction stays in mempool until confirmation, testing unconfirmed-Tx handling. async fn run_rbf_test(is_insert_block: bool) { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source_bitcoind = TestChainSource::BitcoindRpcSync(&bitcoind); - let chain_source_electrsd = TestChainSource::Electrum(&electrsd); - let chain_source_esplora = TestChainSource::Esplora(&electrsd); - - macro_rules! config_node { - ($chain_source:expr, $anchor_channels:expr) => {{ - let config_a = random_config($anchor_channels); - let node = setup_node(&$chain_source, config_a); - node - }}; - } - let anchor_channels = false; - let nodes = vec![ - config_node!(chain_source_electrsd, anchor_channels), - config_node!(chain_source_bitcoind, anchor_channels), - config_node!(chain_source_esplora, anchor_channels), - ]; - - let (bitcoind, electrs) = (&bitcoind.client, &electrsd.client); - premine_blocks(bitcoind, electrs).await; - - // Helpers declaration before starting the test - let all_addrs = - nodes.iter().map(|node| node.onchain_payment().new_address().unwrap()).collect::>(); - let amount_sat = 2_100_000; - let mut txid; - macro_rules! distribute_funds_all_nodes { - () => { - txid = distribute_funds_unconfirmed( - bitcoind, - electrs, - all_addrs.clone(), - Amount::from_sat(amount_sat), - ) - .await; - }; - } - macro_rules! validate_balances { - ($expected_balance_sat:expr, $is_spendable:expr) => { - let spend_balance = if $is_spendable { $expected_balance_sat } else { 0 }; - for node in &nodes { - node.sync_wallets().unwrap(); - let balances = node.list_balances(); - assert_eq!(balances.spendable_onchain_balance_sats, spend_balance); - assert_eq!(balances.total_onchain_balance_sats, $expected_balance_sat); - } - }; - } - - let scripts_buf: HashSet = - all_addrs.iter().map(|addr| addr.script_pubkey()).collect(); - let mut tx; - let mut fee_output_index; - - // Modify the output to the nodes - distribute_funds_all_nodes!(); - validate_balances!(amount_sat, false); - (tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf); - tx.output.iter_mut().for_each(|output| { - if scripts_buf.contains(&output.script_pubkey) { - let new_addr = bitcoind.new_address().unwrap(); - output.script_pubkey = new_addr.script_pubkey(); - } - }); - bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block).await; - validate_balances!(0, is_insert_block); - - // Not modifying the output scripts, but still bumping the fee. - distribute_funds_all_nodes!(); - validate_balances!(amount_sat, false); - (tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf); - bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block).await; - validate_balances!(amount_sat, is_insert_block); - - let mut final_amount_sat = amount_sat * 2; - let value_sat = 21_000; - - // Increase the value of the nodes' outputs - distribute_funds_all_nodes!(); - (tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf); - tx.output.iter_mut().for_each(|output| { - if scripts_buf.contains(&output.script_pubkey) { - output.value = Amount::from_sat(output.value.to_sat() + value_sat); - } - }); - bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block).await; - final_amount_sat += value_sat; - validate_balances!(final_amount_sat, is_insert_block); - - // Decreases the value of the nodes' outputs - distribute_funds_all_nodes!(); - final_amount_sat += amount_sat; - (tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf); - tx.output.iter_mut().for_each(|output| { - if scripts_buf.contains(&output.script_pubkey) { - output.value = Amount::from_sat(output.value.to_sat() - value_sat); - } - }); - bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block).await; - final_amount_sat -= value_sat; - validate_balances!(final_amount_sat, is_insert_block); - - if !is_insert_block { - generate_blocks_and_wait(bitcoind, electrs, 1).await; - validate_balances!(final_amount_sat, true); - } - - // Check if it is possible to send all funds from the node - let mut txids = Vec::new(); - let addr = bitcoind.new_address().unwrap(); - nodes.iter().for_each(|node| { - let txid = node.onchain_payment().send_all_to_address(&addr, true, None).unwrap(); - txids.push(txid); - }); - for txid in txids { - wait_for_tx(electrs, txid).await; - } - generate_blocks_and_wait(bitcoind, electrs, 6).await; - validate_balances!(0, true); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source_bitcoind = TestChainSource::BitcoindRpcSync(&bitcoind); + let chain_source_electrsd = TestChainSource::Electrum(&electrsd); + let chain_source_esplora = TestChainSource::Esplora(&electrsd); + + macro_rules! config_node { + ($chain_source:expr, $anchor_channels:expr) => {{ + let config_a = random_config($anchor_channels); + let node = setup_node(&$chain_source, config_a); + node + }}; + } + let anchor_channels = false; + let nodes = vec![ + config_node!(chain_source_electrsd, anchor_channels), + config_node!(chain_source_bitcoind, anchor_channels), + config_node!(chain_source_esplora, anchor_channels), + ]; + + let (bitcoind, electrs) = (&bitcoind.client, &electrsd.client); + premine_blocks(bitcoind, electrs).await; + + // Helpers declaration before starting the test + let all_addrs = + nodes.iter().map(|node| node.onchain_payment().new_address().unwrap()).collect::>(); + let amount_sat = 2_100_000; + let mut txid; + macro_rules! distribute_funds_all_nodes { + () => { + txid = distribute_funds_unconfirmed( + bitcoind, + electrs, + all_addrs.clone(), + Amount::from_sat(amount_sat), + ) + .await; + }; + } + macro_rules! validate_balances { + ($expected_balance_sat:expr, $is_spendable:expr) => { + let spend_balance = if $is_spendable { $expected_balance_sat } else { 0 }; + for node in &nodes { + node.sync_wallets().unwrap(); + let balances = node.list_balances(); + assert_eq!(balances.spendable_onchain_balance_sats, spend_balance); + assert_eq!(balances.total_onchain_balance_sats, $expected_balance_sat); + } + }; + } + + let scripts_buf: HashSet = + all_addrs.iter().map(|addr| addr.script_pubkey()).collect(); + let mut tx; + let mut fee_output_index; + + // Modify the output to the nodes + distribute_funds_all_nodes!(); + validate_balances!(amount_sat, false); + (tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf); + tx.output.iter_mut().for_each(|output| { + if scripts_buf.contains(&output.script_pubkey) { + let new_addr = bitcoind.new_address().unwrap(); + output.script_pubkey = new_addr.script_pubkey(); + } + }); + bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block).await; + validate_balances!(0, is_insert_block); + + // Not modifying the output scripts, but still bumping the fee. + distribute_funds_all_nodes!(); + validate_balances!(amount_sat, false); + (tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf); + bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block).await; + validate_balances!(amount_sat, is_insert_block); + + let mut final_amount_sat = amount_sat * 2; + let value_sat = 21_000; + + // Increase the value of the nodes' outputs + distribute_funds_all_nodes!(); + (tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf); + tx.output.iter_mut().for_each(|output| { + if scripts_buf.contains(&output.script_pubkey) { + output.value = Amount::from_sat(output.value.to_sat() + value_sat); + } + }); + bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block).await; + final_amount_sat += value_sat; + validate_balances!(final_amount_sat, is_insert_block); + + // Decreases the value of the nodes' outputs + distribute_funds_all_nodes!(); + final_amount_sat += amount_sat; + (tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf); + tx.output.iter_mut().for_each(|output| { + if scripts_buf.contains(&output.script_pubkey) { + output.value = Amount::from_sat(output.value.to_sat() - value_sat); + } + }); + bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block).await; + final_amount_sat -= value_sat; + validate_balances!(final_amount_sat, is_insert_block); + + if !is_insert_block { + generate_blocks_and_wait(bitcoind, electrs, 1).await; + validate_balances!(final_amount_sat, true); + } + + // Check if it is possible to send all funds from the node + let mut txids = Vec::new(); + let addr = bitcoind.new_address().unwrap(); + nodes.iter().for_each(|node| { + let txid = node.onchain_payment().send_all_to_address(&addr, true, None).unwrap(); + txids.push(txid); + }); + for txid in txids { + wait_for_tx(electrs, txid).await; + } + generate_blocks_and_wait(bitcoind, electrs, 6).await; + validate_balances!(0, true); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn sign_verify_msg() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let config = random_config(true); - let chain_source = random_chain_source(&bitcoind, &electrsd); - let node = setup_node(&chain_source, config); - - // Tests arbitrary message signing and later verification - let msg = "OK computer".as_bytes(); - let sig = node.sign_message(msg); - let pkey = node.node_id(); - assert!(node.verify_signature(msg, sig.as_str(), &pkey)); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let config = random_config(true); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let node = setup_node(&chain_source, config); + + // Tests arbitrary message signing and later verification + let msg = "OK computer".as_bytes(); + let sig = node.sign_message(msg); + let pkey = node.node_id(); + assert!(node.verify_signature(msg, sig.as_str(), &pkey)); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn connection_multi_listen() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); - let node_id_b = node_b.node_id(); + let node_id_b = node_b.node_id(); - let node_addrs_b = node_b.listening_addresses().unwrap(); - for node_addr_b in &node_addrs_b { - node_a.connect(node_id_b, node_addr_b.clone(), false).unwrap(); - node_a.disconnect(node_id_b).unwrap(); - } + let node_addrs_b = node_b.listening_addresses().unwrap(); + for node_addr_b in &node_addrs_b { + node_a.connect(node_id_b, node_addr_b.clone(), false).unwrap(); + node_a.disconnect(node_id_b).unwrap(); + } } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn connection_restart_behavior() { - do_connection_restart_behavior(true).await; - do_connection_restart_behavior(false).await; + do_connection_restart_behavior(true).await; + do_connection_restart_behavior(false).await; } async fn do_connection_restart_behavior(persist: bool) { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); - - let node_id_a = node_a.node_id(); - let node_id_b = node_b.node_id(); - - let node_addr_b = node_b.listening_addresses().unwrap().first().unwrap().clone(); - node_a.connect(node_id_b, node_addr_b, persist).unwrap(); - - let peer_details_a = node_a.list_peers().first().unwrap().clone(); - assert_eq!(peer_details_a.node_id, node_id_b); - assert_eq!(peer_details_a.is_persisted, persist); - assert!(peer_details_a.is_connected); - - let peer_details_b = node_b.list_peers().first().unwrap().clone(); - assert_eq!(peer_details_b.node_id, node_id_a); - assert_eq!(peer_details_b.is_persisted, false); - assert!(peer_details_a.is_connected); - - // Restart nodes. - node_a.stop().unwrap(); - node_b.stop().unwrap(); - node_b.start().unwrap(); - node_a.start().unwrap(); - - // Sleep a bit to allow for the reconnect to happen. - tokio::time::sleep(std::time::Duration::from_secs(5)).await; - - if persist { - let peer_details_a = node_a.list_peers().first().unwrap().clone(); - assert_eq!(peer_details_a.node_id, node_id_b); - assert_eq!(peer_details_a.is_persisted, persist); - assert!(peer_details_a.is_connected); - - let peer_details_b = node_b.list_peers().first().unwrap().clone(); - assert_eq!(peer_details_b.node_id, node_id_a); - assert_eq!(peer_details_b.is_persisted, false); - assert!(peer_details_a.is_connected); - } else { - assert!(node_a.list_peers().is_empty()); - assert!(node_b.list_peers().is_empty()); - } + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); + + let node_id_a = node_a.node_id(); + let node_id_b = node_b.node_id(); + + let node_addr_b = node_b.listening_addresses().unwrap().first().unwrap().clone(); + node_a.connect(node_id_b, node_addr_b, persist).unwrap(); + + let peer_details_a = node_a.list_peers().first().unwrap().clone(); + assert_eq!(peer_details_a.node_id, node_id_b); + assert_eq!(peer_details_a.is_persisted, persist); + assert!(peer_details_a.is_connected); + + let peer_details_b = node_b.list_peers().first().unwrap().clone(); + assert_eq!(peer_details_b.node_id, node_id_a); + assert_eq!(peer_details_b.is_persisted, false); + assert!(peer_details_a.is_connected); + + // Restart nodes. + node_a.stop().unwrap(); + node_b.stop().unwrap(); + node_b.start().unwrap(); + node_a.start().unwrap(); + + // Sleep a bit to allow for the reconnect to happen. + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + + if persist { + let peer_details_a = node_a.list_peers().first().unwrap().clone(); + assert_eq!(peer_details_a.node_id, node_id_b); + assert_eq!(peer_details_a.is_persisted, persist); + assert!(peer_details_a.is_connected); + + let peer_details_b = node_b.list_peers().first().unwrap().clone(); + assert_eq!(peer_details_b.node_id, node_id_a); + assert_eq!(peer_details_b.is_persisted, false); + assert!(peer_details_a.is_connected); + } else { + assert!(node_a.list_peers().is_empty()); + assert!(node_b.list_peers().is_empty()); + } } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn concurrent_connections_succeed() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); - - let node_a = Arc::new(node_a); - let node_b = Arc::new(node_b); - - let node_id_b = node_b.node_id(); - let node_addr_b = node_b.listening_addresses().unwrap().first().unwrap().clone(); - - let mut handles = Vec::new(); - for _ in 0..10 { - let thread_node = Arc::clone(&node_a); - let thread_addr = node_addr_b.clone(); - let handle = std::thread::spawn(move || { - thread_node.connect(node_id_b, thread_addr, false).unwrap(); - }); - handles.push(handle); - } - - for h in handles { - h.join().unwrap(); - } + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + + let node_a = Arc::new(node_a); + let node_b = Arc::new(node_b); + + let node_id_b = node_b.node_id(); + let node_addr_b = node_b.listening_addresses().unwrap().first().unwrap().clone(); + + let mut handles = Vec::new(); + for _ in 0..10 { + let thread_node = Arc::clone(&node_a); + let thread_addr = node_addr_b.clone(); + let handle = std::thread::spawn(move || { + thread_node.connect(node_id_b, thread_addr, false).unwrap(); + }); + handles.push(handle); + } + + for h in handles { + h.join().unwrap(); + } } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn splice_channel() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); - - let address_a = node_a.onchain_payment().new_address().unwrap(); - let address_b = node_b.onchain_payment().new_address().unwrap(); - let premine_amount_sat = 5_000_000; - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![address_a, address_b], - Amount::from_sat(premine_amount_sat), - ) - .await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - assert_eq!(node_a.list_balances().total_onchain_balance_sats, premine_amount_sat); - assert_eq!(node_b.list_balances().total_onchain_balance_sats, premine_amount_sat); - - open_channel(&node_a, &node_b, 4_000_000, false, &electrsd).await; - - // Open a channel with Node A contributing the funding - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - let user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id()); - let user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id()); - - let opening_transaction_fee_sat = 156; - let closing_transaction_fee_sat = 614; - let anchor_output_sat = 330; - - assert_eq!( - node_a.list_balances().total_onchain_balance_sats, - premine_amount_sat - 4_000_000 - opening_transaction_fee_sat - ); - assert_eq!( - node_a.list_balances().total_lightning_balance_sats, - 4_000_000 - closing_transaction_fee_sat - anchor_output_sat - ); - assert_eq!(node_b.list_balances().total_lightning_balance_sats, 0); - - // Test that splicing and payments fail when there are insufficient funds - let address = node_b.onchain_payment().new_address().unwrap(); - let amount_msat = 400_000_000; - - assert_eq!( - node_b.splice_in(&user_channel_id_b, node_b.node_id(), 5_000_000), - Err(NodeError::ChannelSplicingFailed), - ); - assert_eq!( - node_b.splice_out(&user_channel_id_b, node_b.node_id(), &address, amount_msat / 1000), - Err(NodeError::ChannelSplicingFailed), - ); - assert_eq!( - node_b.spontaneous_payment().send(amount_msat, node_a.node_id(), None), - Err(NodeError::PaymentSendingFailed) - ); - - // Splice-in funds for Node B so that it has outbound liquidity to make a payment - node_b.splice_in(&user_channel_id_b, node_a.node_id(), 4_000_000).unwrap(); - - let txo = expect_splice_pending_event!(node_a, node_b.node_id()); - expect_splice_pending_event!(node_b, node_a.node_id()); - - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - expect_channel_ready_event!(node_a, node_b.node_id()); - expect_channel_ready_event!(node_b, node_a.node_id()); - - let expected_splice_in_fee_sat = 255; - - let payments = node_b.list_payments(); - let payment = - payments.into_iter().find(|p| p.id == PaymentId(txo.txid.to_byte_array())).unwrap(); - assert_eq!(payment.fee_paid_msat, Some(expected_splice_in_fee_sat * 1_000)); - - assert_eq!( - node_b.list_balances().total_onchain_balance_sats, - premine_amount_sat - 4_000_000 - expected_splice_in_fee_sat - ); - assert_eq!(node_b.list_balances().total_lightning_balance_sats, 4_000_000); - - let payment_id = - node_b.spontaneous_payment().send(amount_msat, node_a.node_id(), None).unwrap(); - - expect_payment_successful_event!(node_b, Some(payment_id), None); - expect_payment_received_event!(node_a, amount_msat); - - // Mine a block to give time for the HTLC to resolve - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1).await; - - assert_eq!( - node_a.list_balances().total_lightning_balance_sats, - 4_000_000 - closing_transaction_fee_sat - anchor_output_sat + amount_msat / 1000 - ); - assert_eq!(node_b.list_balances().total_lightning_balance_sats, 4_000_000 - amount_msat / 1000); - - // Splice-out funds for Node A from the payment sent by Node B - let address = node_a.onchain_payment().new_address().unwrap(); - node_a.splice_out(&user_channel_id_a, node_b.node_id(), &address, amount_msat / 1000).unwrap(); - - let txo = expect_splice_pending_event!(node_a, node_b.node_id()); - expect_splice_pending_event!(node_b, node_a.node_id()); - - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - expect_channel_ready_event!(node_a, node_b.node_id()); - expect_channel_ready_event!(node_b, node_a.node_id()); - - let expected_splice_out_fee_sat = 183; - - let payments = node_a.list_payments(); - let payment = - payments.into_iter().find(|p| p.id == PaymentId(txo.txid.to_byte_array())).unwrap(); - assert_eq!(payment.fee_paid_msat, Some(expected_splice_out_fee_sat * 1_000)); - - assert_eq!( - node_a.list_balances().total_onchain_balance_sats, - premine_amount_sat - 4_000_000 - opening_transaction_fee_sat + amount_msat / 1000 - ); - assert_eq!( - node_a.list_balances().total_lightning_balance_sats, - 4_000_000 - closing_transaction_fee_sat - anchor_output_sat - expected_splice_out_fee_sat - ); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + + let address_a = node_a.onchain_payment().new_address().unwrap(); + let address_b = node_b.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 5_000_000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![address_a, address_b], + Amount::from_sat(premine_amount_sat), + ) + .await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + assert_eq!(node_a.list_balances().total_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_b.list_balances().total_onchain_balance_sats, premine_amount_sat); + + let outpoint = open_channel(&node_a, &node_b, 4_000_000, false, &electrsd).await; + wait_for_tx(&electrsd.client, outpoint.txid).await; + + // Open a channel with Node A contributing the funding + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; + + let user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id()); + let user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id()); + + let opening_transaction_fee_sat = 156; + let closing_transaction_fee_sat = 614; + let anchor_output_sat = 330; + + assert_eq!( + node_a.list_balances().total_onchain_balance_sats, + premine_amount_sat - 4_000_000 - opening_transaction_fee_sat + ); + assert_eq!( + node_a.list_balances().total_lightning_balance_sats, + 4_000_000 - closing_transaction_fee_sat - anchor_output_sat + ); + assert_eq!(node_b.list_balances().total_lightning_balance_sats, 0); + + // Test that splicing and payments fail when there are insufficient funds + let address = node_b.onchain_payment().new_address().unwrap(); + let amount_msat = 400_000_000; + + assert_eq!( + node_b.splice_in(&user_channel_id_b, node_b.node_id(), 5_000_000), + Err(NodeError::ChannelSplicingFailed), + ); + assert_eq!( + node_b.splice_out(&user_channel_id_b, node_b.node_id(), &address, amount_msat / 1000), + Err(NodeError::ChannelSplicingFailed), + ); + assert_eq!( + node_b.spontaneous_payment().send(amount_msat, node_a.node_id(), None), + Err(NodeError::PaymentSendingFailed) + ); + + // Splice-in funds for Node B so that it has outbound liquidity to make a payment + node_b.splice_in(&user_channel_id_b, node_a.node_id(), 4_000_000).unwrap(); + + let txo = expect_splice_pending_event!(node_a, node_b.node_id()); + expect_splice_pending_event!(node_b, node_a.node_id()); + + wait_for_tx(&electrsd.client, txo.txid).await; + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; + + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + let expected_splice_in_fee_sat = 255; + + let payments = node_b.list_payments(); + let payment = + payments.into_iter().find(|p| p.id == PaymentId(txo.txid.to_byte_array())).unwrap(); + assert_eq!(payment.fee_paid_msat, Some(expected_splice_in_fee_sat * 1_000)); + + assert_eq!( + node_b.list_balances().total_onchain_balance_sats, + premine_amount_sat - 4_000_000 - expected_splice_in_fee_sat + ); + assert_eq!(node_b.list_balances().total_lightning_balance_sats, 4_000_000); + + let payment_id = + node_b.spontaneous_payment().send(amount_msat, node_a.node_id(), None).unwrap(); + + expect_payment_successful_event!(node_b, Some(payment_id), None); + expect_payment_received_event!(node_a, amount_msat); + + // Mine a block to give time for the HTLC to resolve + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1).await; + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; + + assert_eq!( + node_a.list_balances().total_lightning_balance_sats, + 4_000_000 - closing_transaction_fee_sat - anchor_output_sat + amount_msat / 1000 + ); + assert_eq!(node_b.list_balances().total_lightning_balance_sats, 4_000_000 - amount_msat / 1000); + + // Splice-out funds for Node A from the payment sent by Node B + let address = node_a.onchain_payment().new_address().unwrap(); + node_a.splice_out(&user_channel_id_a, node_b.node_id(), &address, amount_msat / 1000).unwrap(); + + let txo = expect_splice_pending_event!(node_a, node_b.node_id()); + expect_splice_pending_event!(node_b, node_a.node_id()); + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + let expected_splice_out_fee_sat = 183; + + let payments = node_a.list_payments(); + let payment = + payments.into_iter().find(|p| p.id == PaymentId(txo.txid.to_byte_array())).unwrap(); + assert_eq!(payment.fee_paid_msat, Some(expected_splice_out_fee_sat * 1_000)); + + assert_eq!( + node_a.list_balances().total_onchain_balance_sats, + premine_amount_sat - 4_000_000 - opening_transaction_fee_sat + amount_msat / 1000 + ); + assert_eq!( + node_a.list_balances().total_lightning_balance_sats, + 4_000_000 - closing_transaction_fee_sat - anchor_output_sat - expected_splice_out_fee_sat + ); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn simple_bolt12_send_receive() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); - - let address_a = node_a.onchain_payment().new_address().unwrap(); - let premine_amount_sat = 5_000_000; - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![address_a], - Amount::from_sat(premine_amount_sat), - ) - .await; - - node_a.sync_wallets().unwrap(); - open_channel(&node_a, &node_b, 4_000_000, true, &electrsd).await; - - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - expect_channel_ready_event!(node_a, node_b.node_id()); - expect_channel_ready_event!(node_b, node_a.node_id()); - - // Sleep until we broadcasted a node announcement. - while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() { - tokio::time::sleep(std::time::Duration::from_millis(10)).await; - } - - // Sleep one more sec to make sure the node announcement propagates. - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - - let expected_amount_msat = 100_000_000; - let offer = - node_b.bolt12_payment().receive(expected_amount_msat, "asdf", None, Some(1)).unwrap(); - let expected_quantity = Some(1); - let expected_payer_note = Some("Test".to_string()); - let payment_id = node_a - .bolt12_payment() - .send(&offer, expected_quantity, expected_payer_note.clone(), None) - .unwrap(); - - let event = node_a.next_event_async().await; - match event { - ref e @ Event::PaymentSuccessful { payment_id: ref evt_id, ref bolt12_invoice, .. } => { - println!("{} got event {:?}", node_a.node_id(), e); - assert_eq!(*evt_id, Some(payment_id)); - assert!( - bolt12_invoice.is_some(), - "bolt12_invoice should be present for BOLT12 payments" - ); - node_a.event_handled().unwrap(); - }, - ref e => panic!("{} got unexpected event!: {:?}", "node_a", e), - } - let node_a_payments = - node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Bolt12Offer { .. })); - assert_eq!(node_a_payments.len(), 1); - match node_a_payments.first().unwrap().kind { - PaymentKind::Bolt12Offer { - hash, - preimage, - secret: _, - offer_id, - quantity: ref qty, - payer_note: ref note, - } => { - assert!(hash.is_some()); - assert!(preimage.is_some()); - assert_eq!(offer_id, offer.id()); - assert_eq!(&expected_quantity, qty); - assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0); - // TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 - // API currently doesn't allow to do that. - }, - _ => { - panic!("Unexpected payment kind"); - }, - } - assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); - - expect_payment_received_event!(node_b, expected_amount_msat); - let node_b_payments = - node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Bolt12Offer { .. })); - assert_eq!(node_b_payments.len(), 1); - match node_b_payments.first().unwrap().kind { - PaymentKind::Bolt12Offer { hash, preimage, secret, offer_id, .. } => { - assert!(hash.is_some()); - assert!(preimage.is_some()); - assert!(secret.is_some()); - assert_eq!(offer_id, offer.id()); - }, - _ => { - panic!("Unexpected payment kind"); - }, - } - assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); - - // Test send_using_amount - let offer_amount_msat = 100_000_000; - let less_than_offer_amount = offer_amount_msat - 10_000; - let expected_amount_msat = offer_amount_msat + 10_000; - let offer = node_b.bolt12_payment().receive(offer_amount_msat, "asdf", None, Some(1)).unwrap(); - let expected_quantity = Some(1); - let expected_payer_note = Some("Test".to_string()); - assert!(node_a - .bolt12_payment() - .send_using_amount(&offer, less_than_offer_amount, None, None, None) - .is_err()); - let payment_id = node_a - .bolt12_payment() - .send_using_amount( - &offer, - expected_amount_msat, - expected_quantity, - expected_payer_note.clone(), - None, - ) - .unwrap(); - - expect_payment_successful_event!(node_a, Some(payment_id), None); - let node_a_payments = node_a.list_payments_with_filter(|p| { - matches!(p.kind, PaymentKind::Bolt12Offer { .. }) && p.id == payment_id - }); - assert_eq!(node_a_payments.len(), 1); - let payment_hash = match node_a_payments.first().unwrap().kind { - PaymentKind::Bolt12Offer { - hash, - preimage, - secret: _, - offer_id, - quantity: ref qty, - payer_note: ref note, - } => { - assert!(hash.is_some()); - assert!(preimage.is_some()); - assert_eq!(offer_id, offer.id()); - assert_eq!(&expected_quantity, qty); - assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0); - // TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 - // API currently doesn't allow to do that. - hash.unwrap() - }, - _ => { - panic!("Unexpected payment kind"); - }, - }; - assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); - - expect_payment_received_event!(node_b, expected_amount_msat); - let node_b_payment_id = PaymentId(payment_hash.0); - let node_b_payments = node_b.list_payments_with_filter(|p| { - matches!(p.kind, PaymentKind::Bolt12Offer { .. }) && p.id == node_b_payment_id - }); - assert_eq!(node_b_payments.len(), 1); - match node_b_payments.first().unwrap().kind { - PaymentKind::Bolt12Offer { hash, preimage, secret, offer_id, .. } => { - assert!(hash.is_some()); - assert!(preimage.is_some()); - assert!(secret.is_some()); - assert_eq!(offer_id, offer.id()); - }, - _ => { - panic!("Unexpected payment kind"); - }, - } - assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); - - // Now node_b refunds the amount node_a just overpaid. - let overpaid_amount = expected_amount_msat - offer_amount_msat; - let expected_quantity = Some(1); - let expected_payer_note = Some("Test".to_string()); - let refund = node_b - .bolt12_payment() - .initiate_refund( - overpaid_amount, - 3600, - expected_quantity, - expected_payer_note.clone(), - None, - ) - .unwrap(); - let invoice = node_a.bolt12_payment().request_refund_payment(&refund).unwrap(); - expect_payment_received_event!(node_a, overpaid_amount); - - let node_b_payment_id = node_b - .list_payments_with_filter(|p| { - matches!(p.kind, PaymentKind::Bolt12Refund { .. }) - && p.amount_msat == Some(overpaid_amount) - }) - .first() - .unwrap() - .id; - expect_payment_successful_event!(node_b, Some(node_b_payment_id), None); - - let node_b_payments = node_b.list_payments_with_filter(|p| { - matches!(p.kind, PaymentKind::Bolt12Refund { .. }) && p.id == node_b_payment_id - }); - assert_eq!(node_b_payments.len(), 1); - match node_b_payments.first().unwrap().kind { - PaymentKind::Bolt12Refund { - hash, - preimage, - secret: _, - quantity: ref qty, - payer_note: ref note, - } => { - assert!(hash.is_some()); - assert!(preimage.is_some()); - assert_eq!(&expected_quantity, qty); - assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0) - // TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 - // API currently doesn't allow to do that. - }, - _ => { - panic!("Unexpected payment kind"); - }, - } - assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(overpaid_amount)); - - let node_a_payment_id = PaymentId(invoice.payment_hash().0); - let node_a_payments = node_a.list_payments_with_filter(|p| { - matches!(p.kind, PaymentKind::Bolt12Refund { .. }) && p.id == node_a_payment_id - }); - assert_eq!(node_a_payments.len(), 1); - match node_a_payments.first().unwrap().kind { - PaymentKind::Bolt12Refund { hash, preimage, secret, .. } => { - assert!(hash.is_some()); - assert!(preimage.is_some()); - assert!(secret.is_some()); - }, - _ => { - panic!("Unexpected payment kind"); - }, - } - assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(overpaid_amount)); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + + let address_a = node_a.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 5_000_000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![address_a], + Amount::from_sat(premine_amount_sat), + ) + .await; + + node_a.sync_wallets().unwrap(); + open_channel(&node_a, &node_b, 4_000_000, true, &electrsd).await; + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + // Sleep until we broadcasted a node announcement. + while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() { + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + } + + // Sleep one more sec to make sure the node announcement propagates. + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let expected_amount_msat = 100_000_000; + let offer = + node_b.bolt12_payment().receive(expected_amount_msat, "asdf", None, Some(1)).unwrap(); + let expected_quantity = Some(1); + let expected_payer_note = Some("Test".to_string()); + let payment_id = node_a + .bolt12_payment() + .send(&offer, expected_quantity, expected_payer_note.clone(), None) + .unwrap(); + + let event = node_a.next_event_async().await; + match event { + ref e @ Event::PaymentSuccessful { payment_id: ref evt_id, ref bolt12_invoice, .. } => { + println!("{} got event {:?}", node_a.node_id(), e); + assert_eq!(*evt_id, Some(payment_id)); + assert!( + bolt12_invoice.is_some(), + "bolt12_invoice should be present for BOLT12 payments" + ); + node_a.event_handled().unwrap(); + }, + ref e => panic!("{} got unexpected event!: {:?}", "node_a", e), + } + let node_a_payments = + node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Bolt12Offer { .. })); + assert_eq!(node_a_payments.len(), 1); + match node_a_payments.first().unwrap().kind { + PaymentKind::Bolt12Offer { + hash, + preimage, + secret: _, + offer_id, + quantity: ref qty, + payer_note: ref note, + } => { + assert!(hash.is_some()); + assert!(preimage.is_some()); + assert_eq!(offer_id, offer.id()); + assert_eq!(&expected_quantity, qty); + assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0); + // TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 + // API currently doesn't allow to do that. + }, + _ => { + panic!("Unexpected payment kind"); + }, + } + assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); + + expect_payment_received_event!(node_b, expected_amount_msat); + let node_b_payments = + node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Bolt12Offer { .. })); + assert_eq!(node_b_payments.len(), 1); + match node_b_payments.first().unwrap().kind { + PaymentKind::Bolt12Offer { hash, preimage, secret, offer_id, .. } => { + assert!(hash.is_some()); + assert!(preimage.is_some()); + assert!(secret.is_some()); + assert_eq!(offer_id, offer.id()); + }, + _ => { + panic!("Unexpected payment kind"); + }, + } + assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); + + // Test send_using_amount + let offer_amount_msat = 100_000_000; + let less_than_offer_amount = offer_amount_msat - 10_000; + let expected_amount_msat = offer_amount_msat + 10_000; + let offer = node_b.bolt12_payment().receive(offer_amount_msat, "asdf", None, Some(1)).unwrap(); + let expected_quantity = Some(1); + let expected_payer_note = Some("Test".to_string()); + assert!(node_a + .bolt12_payment() + .send_using_amount(&offer, less_than_offer_amount, None, None, None) + .is_err()); + let payment_id = node_a + .bolt12_payment() + .send_using_amount( + &offer, + expected_amount_msat, + expected_quantity, + expected_payer_note.clone(), + None, + ) + .unwrap(); + + expect_payment_successful_event!(node_a, Some(payment_id), None); + let node_a_payments = node_a.list_payments_with_filter(|p| { + matches!(p.kind, PaymentKind::Bolt12Offer { .. }) && p.id == payment_id + }); + assert_eq!(node_a_payments.len(), 1); + let payment_hash = match node_a_payments.first().unwrap().kind { + PaymentKind::Bolt12Offer { + hash, + preimage, + secret: _, + offer_id, + quantity: ref qty, + payer_note: ref note, + } => { + assert!(hash.is_some()); + assert!(preimage.is_some()); + assert_eq!(offer_id, offer.id()); + assert_eq!(&expected_quantity, qty); + assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0); + // TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 + // API currently doesn't allow to do that. + hash.unwrap() + }, + _ => { + panic!("Unexpected payment kind"); + }, + }; + assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); + + expect_payment_received_event!(node_b, expected_amount_msat); + let node_b_payment_id = PaymentId(payment_hash.0); + let node_b_payments = node_b.list_payments_with_filter(|p| { + matches!(p.kind, PaymentKind::Bolt12Offer { .. }) && p.id == node_b_payment_id + }); + assert_eq!(node_b_payments.len(), 1); + match node_b_payments.first().unwrap().kind { + PaymentKind::Bolt12Offer { hash, preimage, secret, offer_id, .. } => { + assert!(hash.is_some()); + assert!(preimage.is_some()); + assert!(secret.is_some()); + assert_eq!(offer_id, offer.id()); + }, + _ => { + panic!("Unexpected payment kind"); + }, + } + assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(expected_amount_msat)); + + // Now node_b refunds the amount node_a just overpaid. + let overpaid_amount = expected_amount_msat - offer_amount_msat; + let expected_quantity = Some(1); + let expected_payer_note = Some("Test".to_string()); + let refund = node_b + .bolt12_payment() + .initiate_refund( + overpaid_amount, + 3600, + expected_quantity, + expected_payer_note.clone(), + None, + ) + .unwrap(); + let invoice = node_a.bolt12_payment().request_refund_payment(&refund).unwrap(); + expect_payment_received_event!(node_a, overpaid_amount); + + let node_b_payment_id = node_b + .list_payments_with_filter(|p| { + matches!(p.kind, PaymentKind::Bolt12Refund { .. }) + && p.amount_msat == Some(overpaid_amount) + }) + .first() + .unwrap() + .id; + expect_payment_successful_event!(node_b, Some(node_b_payment_id), None); + + let node_b_payments = node_b.list_payments_with_filter(|p| { + matches!(p.kind, PaymentKind::Bolt12Refund { .. }) && p.id == node_b_payment_id + }); + assert_eq!(node_b_payments.len(), 1); + match node_b_payments.first().unwrap().kind { + PaymentKind::Bolt12Refund { + hash, + preimage, + secret: _, + quantity: ref qty, + payer_note: ref note, + } => { + assert!(hash.is_some()); + assert!(preimage.is_some()); + assert_eq!(&expected_quantity, qty); + assert_eq!(expected_payer_note.unwrap(), note.clone().unwrap().0) + // TODO: We should eventually set and assert the secret sender-side, too, but the BOLT12 + // API currently doesn't allow to do that. + }, + _ => { + panic!("Unexpected payment kind"); + }, + } + assert_eq!(node_b_payments.first().unwrap().amount_msat, Some(overpaid_amount)); + + let node_a_payment_id = PaymentId(invoice.payment_hash().0); + let node_a_payments = node_a.list_payments_with_filter(|p| { + matches!(p.kind, PaymentKind::Bolt12Refund { .. }) && p.id == node_a_payment_id + }); + assert_eq!(node_a_payments.len(), 1); + match node_a_payments.first().unwrap().kind { + PaymentKind::Bolt12Refund { hash, preimage, secret, .. } => { + assert!(hash.is_some()); + assert!(preimage.is_some()); + assert!(secret.is_some()); + }, + _ => { + panic!("Unexpected payment kind"); + }, + } + assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(overpaid_amount)); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn async_payment() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - - let mut config_sender = random_config(true); - config_sender.node_config.listening_addresses = None; - config_sender.node_config.node_alias = None; - config_sender.log_writer = - TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("sender ".to_string()))); - config_sender.async_payments_role = Some(AsyncPaymentsRole::Client); - let node_sender = setup_node(&chain_source, config_sender); - - let mut config_sender_lsp = random_config(true); - config_sender_lsp.log_writer = - TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("sender_lsp ".to_string()))); - config_sender_lsp.async_payments_role = Some(AsyncPaymentsRole::Server); - let node_sender_lsp = setup_node(&chain_source, config_sender_lsp); - - let mut config_receiver_lsp = random_config(true); - config_receiver_lsp.log_writer = - TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("receiver_lsp".to_string()))); - config_receiver_lsp.async_payments_role = Some(AsyncPaymentsRole::Server); - - let node_receiver_lsp = setup_node(&chain_source, config_receiver_lsp); - - let mut config_receiver = random_config(true); - config_receiver.node_config.listening_addresses = None; - config_receiver.node_config.node_alias = None; - config_receiver.log_writer = - TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("receiver ".to_string()))); - let node_receiver = setup_node(&chain_source, config_receiver); - - let address_sender = node_sender.onchain_payment().new_address().unwrap(); - let address_sender_lsp = node_sender_lsp.onchain_payment().new_address().unwrap(); - let address_receiver_lsp = node_receiver_lsp.onchain_payment().new_address().unwrap(); - let address_receiver = node_receiver.onchain_payment().new_address().unwrap(); - let premine_amount_sat = 4_000_000; - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![address_sender, address_sender_lsp, address_receiver_lsp, address_receiver], - Amount::from_sat(premine_amount_sat), - ) - .await; - - node_sender.sync_wallets().unwrap(); - node_sender_lsp.sync_wallets().unwrap(); - node_receiver_lsp.sync_wallets().unwrap(); - node_receiver.sync_wallets().unwrap(); - - open_channel(&node_sender, &node_sender_lsp, 400_000, false, &electrsd).await; - open_channel(&node_sender_lsp, &node_receiver_lsp, 400_000, true, &electrsd).await; - open_channel_push_amt( - &node_receiver, - &node_receiver_lsp, - 400_000, - Some(200_000_000), - false, - &electrsd, - ) - .await; - - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - - node_sender.sync_wallets().unwrap(); - node_sender_lsp.sync_wallets().unwrap(); - node_receiver_lsp.sync_wallets().unwrap(); - node_receiver.sync_wallets().unwrap(); - - expect_channel_ready_event!(node_sender, node_sender_lsp.node_id()); - expect_channel_ready_events!( - node_sender_lsp, - node_sender.node_id(), - node_receiver_lsp.node_id() - ); - expect_channel_ready_events!( - node_receiver_lsp, - node_sender_lsp.node_id(), - node_receiver.node_id() - ); - expect_channel_ready_event!(node_receiver, node_receiver_lsp.node_id()); - - let has_node_announcements = |node: &ldk_node::Node| { - node.network_graph() - .list_nodes() - .iter() - .filter(|n| { - node.network_graph().node(n).map_or(false, |info| info.announcement_info.is_some()) - }) - .count() >= 2 - }; - - // Wait for everyone to see all channels and node announcements. - while node_sender.network_graph().list_channels().len() < 1 - || node_sender_lsp.network_graph().list_channels().len() < 1 - || node_receiver_lsp.network_graph().list_channels().len() < 1 - || node_receiver.network_graph().list_channels().len() < 1 - || !has_node_announcements(&node_sender) - || !has_node_announcements(&node_sender_lsp) - || !has_node_announcements(&node_receiver_lsp) - || !has_node_announcements(&node_receiver) - { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - } - - let recipient_id = vec![1, 2, 3]; - let blinded_paths = - node_receiver_lsp.bolt12_payment().blinded_paths_for_async_recipient(recipient_id).unwrap(); - node_receiver.bolt12_payment().set_paths_to_static_invoice_server(blinded_paths).unwrap(); - - let offer = loop { - if let Ok(offer) = node_receiver.bolt12_payment().receive_async() { - break offer; - } - - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - }; - - node_receiver.stop().unwrap(); - - let payment_id = - node_sender.bolt12_payment().send_using_amount(&offer, 5_000, None, None, None).unwrap(); - - // Sleep to allow the payment reach a state where the htlc is held and waiting for the receiver to come online. - tokio::time::sleep(std::time::Duration::from_millis(3000)).await; - - node_receiver.start().unwrap(); - - expect_payment_successful_event!(node_sender, Some(payment_id), None); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + + let mut config_sender = random_config(true); + config_sender.node_config.listening_addresses = None; + config_sender.node_config.node_alias = None; + config_sender.log_writer = + TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("sender ".to_string()))); + config_sender.async_payments_role = Some(AsyncPaymentsRole::Client); + let node_sender = setup_node(&chain_source, config_sender); + + let mut config_sender_lsp = random_config(true); + config_sender_lsp.log_writer = + TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("sender_lsp ".to_string()))); + config_sender_lsp.async_payments_role = Some(AsyncPaymentsRole::Server); + let node_sender_lsp = setup_node(&chain_source, config_sender_lsp); + + let mut config_receiver_lsp = random_config(true); + config_receiver_lsp.log_writer = + TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("receiver_lsp".to_string()))); + config_receiver_lsp.async_payments_role = Some(AsyncPaymentsRole::Server); + + let node_receiver_lsp = setup_node(&chain_source, config_receiver_lsp); + + let mut config_receiver = random_config(true); + config_receiver.node_config.listening_addresses = None; + config_receiver.node_config.node_alias = None; + config_receiver.log_writer = + TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("receiver ".to_string()))); + let node_receiver = setup_node(&chain_source, config_receiver); + + let address_sender = node_sender.onchain_payment().new_address().unwrap(); + let address_sender_lsp = node_sender_lsp.onchain_payment().new_address().unwrap(); + let address_receiver_lsp = node_receiver_lsp.onchain_payment().new_address().unwrap(); + let address_receiver = node_receiver.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 4_000_000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![address_sender, address_sender_lsp, address_receiver_lsp, address_receiver], + Amount::from_sat(premine_amount_sat), + ) + .await; + + node_sender.sync_wallets().unwrap(); + node_sender_lsp.sync_wallets().unwrap(); + node_receiver_lsp.sync_wallets().unwrap(); + node_receiver.sync_wallets().unwrap(); + + let outpoint = open_channel(&node_sender, &node_sender_lsp, 400_000, false, &electrsd).await; + wait_for_tx(&electrsd.client, outpoint.txid).await; + let outpoint = + open_channel(&node_sender_lsp, &node_receiver_lsp, 400_000, true, &electrsd).await; + wait_for_tx(&electrsd.client, outpoint.txid).await; + let outpoint = open_channel_push_amt( + &node_receiver, + &node_receiver_lsp, + 400_000, + Some(200_000_000), + false, + &electrsd, + ) + .await; + wait_for_tx(&electrsd.client, outpoint.txid).await; + + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + wait_for_node_block(&node_sender, tip).await; + wait_for_node_block(&node_sender_lsp, tip).await; + wait_for_node_block(&node_receiver, tip).await; + wait_for_node_block(&node_receiver_lsp, tip).await; + + node_sender.sync_wallets().unwrap(); + node_sender_lsp.sync_wallets().unwrap(); + node_receiver_lsp.sync_wallets().unwrap(); + node_receiver.sync_wallets().unwrap(); + + expect_channel_ready_event!(node_sender, node_sender_lsp.node_id()); + expect_channel_ready_events!( + node_sender_lsp, + node_sender.node_id(), + node_receiver_lsp.node_id() + ); + expect_channel_ready_events!( + node_receiver_lsp, + node_sender_lsp.node_id(), + node_receiver.node_id() + ); + expect_channel_ready_event!(node_receiver, node_receiver_lsp.node_id()); + + let has_node_announcements = |node: &ldk_node::Node| { + node.network_graph() + .list_nodes() + .iter() + .filter(|n| { + node.network_graph().node(n).map_or(false, |info| info.announcement_info.is_some()) + }) + .count() + >= 2 + }; + + // Wait for everyone to see all channels and node announcements. + // This ensures the receiver knows sender_lsp's address before the async payment starts, + // so that after receiver restarts, LDK's ConnectionNeeded event has a valid address. + while node_sender.network_graph().list_channels().len() < 1 + || node_sender_lsp.network_graph().list_channels().len() < 1 + || node_receiver_lsp.network_graph().list_channels().len() < 1 + || node_receiver.network_graph().list_channels().len() < 1 + || !has_node_announcements(&node_sender) + || !has_node_announcements(&node_sender_lsp) + || !has_node_announcements(&node_receiver_lsp) + || !has_node_announcements(&node_receiver) + { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + + let recipient_id = vec![1, 2, 3]; + let blinded_paths = + node_receiver_lsp.bolt12_payment().blinded_paths_for_async_recipient(recipient_id).unwrap(); + node_receiver.bolt12_payment().set_paths_to_static_invoice_server(blinded_paths).unwrap(); + + let offer = loop { + if let Ok(offer) = node_receiver.bolt12_payment().receive_async() { + break offer; + } + + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + }; + + node_receiver.stop().unwrap(); + + let payment_id = + node_sender.bolt12_payment().send_using_amount(&offer, 5_000, None, None, None).unwrap(); + + // Sleep to allow the payment reach a state where the htlc is held and waiting for the receiver to come online. + tokio::time::sleep(std::time::Duration::from_millis(3000)).await; + + node_receiver.start().unwrap(); + + expect_payment_successful_event!(node_sender, Some(payment_id), None); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_node_announcement_propagation() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - - // Node A will use both listening and announcement addresses - let mut config_a = random_config(true); - let node_a_alias_string = "ldk-node-a".to_string(); - let mut node_a_alias_bytes = [0u8; 32]; - node_a_alias_bytes[..node_a_alias_string.as_bytes().len()] - .copy_from_slice(node_a_alias_string.as_bytes()); - let node_a_node_alias = Some(NodeAlias(node_a_alias_bytes)); - let node_a_announcement_addresses = random_listening_addresses(); - config_a.node_config.node_alias = node_a_node_alias.clone(); - config_a.node_config.listening_addresses = Some(random_listening_addresses()); - config_a.node_config.announcement_addresses = Some(node_a_announcement_addresses.clone()); - - // Node B will only use listening addresses - let mut config_b = random_config(true); - let node_b_alias_string = "ldk-node-b".to_string(); - let mut node_b_alias_bytes = [0u8; 32]; - node_b_alias_bytes[..node_b_alias_string.as_bytes().len()] - .copy_from_slice(node_b_alias_string.as_bytes()); - let node_b_node_alias = Some(NodeAlias(node_b_alias_bytes)); - let node_b_listening_addresses = random_listening_addresses(); - config_b.node_config.node_alias = node_b_node_alias.clone(); - config_b.node_config.listening_addresses = Some(node_b_listening_addresses.clone()); - config_b.node_config.announcement_addresses = None; - - let node_a = setup_node(&chain_source, config_a); - let node_b = setup_node(&chain_source, config_b); - - let address_a = node_a.onchain_payment().new_address().unwrap(); - let premine_amount_sat = 5_000_000; - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![address_a], - Amount::from_sat(premine_amount_sat), - ) - .await; - - node_a.sync_wallets().unwrap(); - - // Open an announced channel from node_a to node_b - open_channel(&node_a, &node_b, 4_000_000, true, &electrsd).await; - - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - expect_channel_ready_event!(node_a, node_b.node_id()); - expect_channel_ready_event!(node_b, node_a.node_id()); - - // Wait until node_b broadcasts a node announcement - while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() { - tokio::time::sleep(std::time::Duration::from_millis(10)).await; - } - - // Sleep to make sure the node announcement propagates - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - - // Get node info from the other node's perspective - let node_a_info = node_b.network_graph().node(&NodeId::from_pubkey(&node_a.node_id())).unwrap(); - let node_a_announcement_info = node_a_info.announcement_info.as_ref().unwrap(); - - let node_b_info = node_a.network_graph().node(&NodeId::from_pubkey(&node_b.node_id())).unwrap(); - let node_b_announcement_info = node_b_info.announcement_info.as_ref().unwrap(); - - // Assert that the aliases and addresses match the expected values - #[cfg(not(feature = "uniffi"))] - assert_eq!(node_a_announcement_info.alias(), &node_a_node_alias.unwrap()); - #[cfg(feature = "uniffi")] - assert_eq!(node_a_announcement_info.alias, node_a_alias_string); - - #[cfg(not(feature = "uniffi"))] - assert_eq!(node_a_announcement_info.addresses(), &node_a_announcement_addresses); - #[cfg(feature = "uniffi")] - assert_eq!(node_a_announcement_info.addresses, node_a_announcement_addresses); - - #[cfg(not(feature = "uniffi"))] - assert_eq!(node_b_announcement_info.alias(), &node_b_node_alias.unwrap()); - #[cfg(feature = "uniffi")] - assert_eq!(node_b_announcement_info.alias, node_b_alias_string); - - #[cfg(not(feature = "uniffi"))] - assert_eq!(node_b_announcement_info.addresses(), &node_b_listening_addresses); - #[cfg(feature = "uniffi")] - assert_eq!(node_b_announcement_info.addresses, node_b_listening_addresses); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + + // Node A will use both listening and announcement addresses + let mut config_a = random_config(true); + let node_a_alias_string = "ldk-node-a".to_string(); + let mut node_a_alias_bytes = [0u8; 32]; + node_a_alias_bytes[..node_a_alias_string.as_bytes().len()] + .copy_from_slice(node_a_alias_string.as_bytes()); + let node_a_node_alias = Some(NodeAlias(node_a_alias_bytes)); + let node_a_announcement_addresses = random_listening_addresses(); + config_a.node_config.node_alias = node_a_node_alias.clone(); + config_a.node_config.listening_addresses = Some(random_listening_addresses()); + config_a.node_config.announcement_addresses = Some(node_a_announcement_addresses.clone()); + + // Node B will only use listening addresses + let mut config_b = random_config(true); + let node_b_alias_string = "ldk-node-b".to_string(); + let mut node_b_alias_bytes = [0u8; 32]; + node_b_alias_bytes[..node_b_alias_string.as_bytes().len()] + .copy_from_slice(node_b_alias_string.as_bytes()); + let node_b_node_alias = Some(NodeAlias(node_b_alias_bytes)); + let node_b_listening_addresses = random_listening_addresses(); + config_b.node_config.node_alias = node_b_node_alias.clone(); + config_b.node_config.listening_addresses = Some(node_b_listening_addresses.clone()); + config_b.node_config.announcement_addresses = None; + + let node_a = setup_node(&chain_source, config_a); + let node_b = setup_node(&chain_source, config_b); + + let address_a = node_a.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 5_000_000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![address_a], + Amount::from_sat(premine_amount_sat), + ) + .await; + + node_a.sync_wallets().unwrap(); + + // Open an announced channel from node_a to node_b + open_channel(&node_a, &node_b, 4_000_000, true, &electrsd).await; + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + // Wait until node_b broadcasts a node announcement + while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() { + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + } + + // Sleep to make sure the node announcement propagates + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + // Get node info from the other node's perspective + let node_a_info = node_b.network_graph().node(&NodeId::from_pubkey(&node_a.node_id())).unwrap(); + let node_a_announcement_info = node_a_info.announcement_info.as_ref().unwrap(); + + let node_b_info = node_a.network_graph().node(&NodeId::from_pubkey(&node_b.node_id())).unwrap(); + let node_b_announcement_info = node_b_info.announcement_info.as_ref().unwrap(); + + // Assert that the aliases and addresses match the expected values + #[cfg(not(feature = "uniffi"))] + assert_eq!(node_a_announcement_info.alias(), &node_a_node_alias.unwrap()); + #[cfg(feature = "uniffi")] + assert_eq!(node_a_announcement_info.alias, node_a_alias_string); + + #[cfg(not(feature = "uniffi"))] + assert_eq!(node_a_announcement_info.addresses(), &node_a_announcement_addresses); + #[cfg(feature = "uniffi")] + assert_eq!(node_a_announcement_info.addresses, node_a_announcement_addresses); + + #[cfg(not(feature = "uniffi"))] + assert_eq!(node_b_announcement_info.alias(), &node_b_node_alias.unwrap()); + #[cfg(feature = "uniffi")] + assert_eq!(node_b_announcement_info.alias, node_b_alias_string); + + #[cfg(not(feature = "uniffi"))] + assert_eq!(node_b_announcement_info.addresses(), &node_b_listening_addresses); + #[cfg(feature = "uniffi")] + assert_eq!(node_b_announcement_info.addresses, node_b_listening_addresses); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn generate_bip21_uri() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); - - let address_a = node_a.onchain_payment().new_address().unwrap(); - let premined_sats = 5_000_000; - - let expected_amount_sats = 100_000; - let expiry_sec = 4_000; - - // Test 1: Verify URI generation (on-chain + BOLT11) works - // even before any channels are opened. This checks the graceful fallback behavior. - let initial_uni_payment = node_b - .unified_payment() - .receive(expected_amount_sats, "asdf", expiry_sec) - .expect("Failed to generate URI"); - println!("Initial URI (no channels): {}", initial_uni_payment); - - assert!(initial_uni_payment.contains("bitcoin:")); - assert!(initial_uni_payment.contains("lightning=")); - assert!(!initial_uni_payment.contains("lno=")); // BOLT12 requires channels - - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![address_a], - Amount::from_sat(premined_sats), - ) - .await; - - node_a.sync_wallets().unwrap(); - open_channel(&node_a, &node_b, 4_000_000, true, &electrsd).await; - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - expect_channel_ready_event!(node_a, node_b.node_id()); - expect_channel_ready_event!(node_b, node_a.node_id()); - - // Test 2: Verify URI generation (on-chain + BOLT11 + BOLT12) works after channels are established. - let uni_payment = node_b - .unified_payment() - .receive(expected_amount_sats, "asdf", expiry_sec) - .expect("Failed to generate URI"); - - println!("Generated URI: {}", uni_payment); - assert!(uni_payment.contains("bitcoin:")); - assert!(uni_payment.contains("lightning=")); - assert!(uni_payment.contains("lno=")); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + + let address_a = node_a.onchain_payment().new_address().unwrap(); + let premined_sats = 5_000_000; + + let expected_amount_sats = 100_000; + let expiry_sec = 4_000; + + // Test 1: Verify URI generation (on-chain + BOLT11) works + // even before any channels are opened. This checks the graceful fallback behavior. + let initial_uni_payment = node_b + .unified_payment() + .receive(expected_amount_sats, "asdf", expiry_sec) + .expect("Failed to generate URI"); + println!("Initial URI (no channels): {}", initial_uni_payment); + + assert!(initial_uni_payment.contains("bitcoin:")); + assert!(initial_uni_payment.contains("lightning=")); + assert!(!initial_uni_payment.contains("lno=")); // BOLT12 requires channels + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![address_a], + Amount::from_sat(premined_sats), + ) + .await; + + node_a.sync_wallets().unwrap(); + open_channel(&node_a, &node_b, 4_000_000, true, &electrsd).await; + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + // Test 2: Verify URI generation (on-chain + BOLT11 + BOLT12) works after channels are established. + let uni_payment = node_b + .unified_payment() + .receive(expected_amount_sats, "asdf", expiry_sec) + .expect("Failed to generate URI"); + + println!("Generated URI: {}", uni_payment); + assert!(uni_payment.contains("bitcoin:")); + assert!(uni_payment.contains("lightning=")); + assert!(uni_payment.contains("lno=")); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn unified_send_receive_bip21_uri() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); - - let address_a = node_a.onchain_payment().new_address().unwrap(); - let premined_sats = 5_000_000; - - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![address_a], - Amount::from_sat(premined_sats), - ) - .await; - - node_a.sync_wallets().unwrap(); - open_channel(&node_a, &node_b, 4_000_000, true, &electrsd).await; - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - expect_channel_ready_event!(node_a, node_b.node_id()); - expect_channel_ready_event!(node_b, node_a.node_id()); - - // Sleep until we broadcast a node announcement. - while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() { - tokio::time::sleep(std::time::Duration::from_millis(10)).await; - } - - // Sleep one more sec to make sure the node announcement propagates. - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - - let expected_amount_sats = 100_000; - let expiry_sec = 4_000; - - let uni_payment = node_b.unified_payment().receive(expected_amount_sats, "asdf", expiry_sec); - let uri_str = uni_payment.clone().unwrap(); - let offer_payment_id: PaymentId = - match node_a.unified_payment().send(&uri_str, None, None).await { - Ok(UnifiedPaymentResult::Bolt12 { payment_id }) => { - println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id); - payment_id - }, - Ok(UnifiedPaymentResult::Bolt11 { payment_id: _ }) => { - panic!("Expected Bolt12 payment but got Bolt11"); - }, - Ok(UnifiedPaymentResult::Onchain { txid: _ }) => { - panic!("Expected Bolt12 payment but got On-chain transaction"); - }, - Err(e) => { - panic!("Expected Bolt12 payment but got error: {:?}", e); - }, - }; - - expect_payment_successful_event!(node_a, Some(offer_payment_id), None); - - // Cut off the BOLT12 part to fallback to BOLT11. - let uri_str_without_offer = uri_str.split("&lno=").next().unwrap(); - let invoice_payment_id: PaymentId = - match node_a.unified_payment().send(uri_str_without_offer, None, None).await { - Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => { - panic!("Expected Bolt11 payment but got Bolt12"); - }, - Ok(UnifiedPaymentResult::Bolt11 { payment_id }) => { - println!("\nBolt11 payment sent successfully with PaymentID: {:?}", payment_id); - payment_id - }, - Ok(UnifiedPaymentResult::Onchain { txid: _ }) => { - panic!("Expected Bolt11 payment but got on-chain transaction"); - }, - Err(e) => { - panic!("Expected Bolt11 payment but got error: {:?}", e); - }, - }; - expect_payment_successful_event!(node_a, Some(invoice_payment_id), None); - - let expect_onchain_amount_sats = 800_000; - let onchain_uni_payment = - node_b.unified_payment().receive(expect_onchain_amount_sats, "asdf", 4_000).unwrap(); - - // Cut off any lightning part to fallback to on-chain only. - let uri_str_without_lightning = onchain_uni_payment.split("&lightning=").next().unwrap(); - let txid = match node_a.unified_payment().send(&uri_str_without_lightning, None, None).await { - Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => { - panic!("Expected on-chain payment but got Bolt12") - }, - Ok(UnifiedPaymentResult::Bolt11 { payment_id: _ }) => { - panic!("Expected on-chain payment but got Bolt11"); - }, - Ok(UnifiedPaymentResult::Onchain { txid }) => { - println!("\nOn-chain transaction successful with Txid: {}", txid); - txid - }, - Err(e) => { - panic!("Expected on-chain payment but got error: {:?}", e); - }, - }; - - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - wait_for_tx(&electrsd.client, txid).await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - assert_eq!(node_b.list_balances().total_onchain_balance_sats, 800_000); - assert_eq!(node_b.list_balances().total_lightning_balance_sats, 200_000); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + + let address_a = node_a.onchain_payment().new_address().unwrap(); + let premined_sats = 5_000_000; + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![address_a], + Amount::from_sat(premined_sats), + ) + .await; + + node_a.sync_wallets().unwrap(); + open_channel(&node_a, &node_b, 4_000_000, true, &electrsd).await; + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + // Sleep until we broadcast a node announcement. + while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() { + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + } + + // Sleep one more sec to make sure the node announcement propagates. + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + + let expected_amount_sats = 100_000; + let expiry_sec = 4_000; + + let uni_payment = node_b.unified_payment().receive(expected_amount_sats, "asdf", expiry_sec); + let uri_str = uni_payment.clone().unwrap(); + let offer_payment_id: PaymentId = + match node_a.unified_payment().send(&uri_str, None, None).await { + Ok(UnifiedPaymentResult::Bolt12 { payment_id }) => { + println!("\nBolt12 payment sent successfully with PaymentID: {:?}", payment_id); + payment_id + }, + Ok(UnifiedPaymentResult::Bolt11 { payment_id: _ }) => { + panic!("Expected Bolt12 payment but got Bolt11"); + }, + Ok(UnifiedPaymentResult::Onchain { txid: _ }) => { + panic!("Expected Bolt12 payment but got On-chain transaction"); + }, + Err(e) => { + panic!("Expected Bolt12 payment but got error: {:?}", e); + }, + }; + + expect_payment_successful_event!(node_a, Some(offer_payment_id), None); + + // Cut off the BOLT12 part to fallback to BOLT11. + let uri_str_without_offer = uri_str.split("&lno=").next().unwrap(); + let invoice_payment_id: PaymentId = + match node_a.unified_payment().send(uri_str_without_offer, None, None).await { + Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => { + panic!("Expected Bolt11 payment but got Bolt12"); + }, + Ok(UnifiedPaymentResult::Bolt11 { payment_id }) => { + println!("\nBolt11 payment sent successfully with PaymentID: {:?}", payment_id); + payment_id + }, + Ok(UnifiedPaymentResult::Onchain { txid: _ }) => { + panic!("Expected Bolt11 payment but got on-chain transaction"); + }, + Err(e) => { + panic!("Expected Bolt11 payment but got error: {:?}", e); + }, + }; + expect_payment_successful_event!(node_a, Some(invoice_payment_id), None); + + let expect_onchain_amount_sats = 800_000; + let onchain_uni_payment = + node_b.unified_payment().receive(expect_onchain_amount_sats, "asdf", 4_000).unwrap(); + + // Cut off any lightning part to fallback to on-chain only. + let uri_str_without_lightning = onchain_uni_payment.split("&lightning=").next().unwrap(); + let txid = match node_a.unified_payment().send(&uri_str_without_lightning, None, None).await { + Ok(UnifiedPaymentResult::Bolt12 { payment_id: _ }) => { + panic!("Expected on-chain payment but got Bolt12") + }, + Ok(UnifiedPaymentResult::Bolt11 { payment_id: _ }) => { + panic!("Expected on-chain payment but got Bolt11"); + }, + Ok(UnifiedPaymentResult::Onchain { txid }) => { + println!("\nOn-chain transaction successful with Txid: {}", txid); + txid + }, + Err(e) => { + panic!("Expected on-chain payment but got error: {:?}", e); + }, + }; + + wait_for_tx(&electrsd.client, txid).await; + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; + + assert_eq!(node_b.list_balances().total_onchain_balance_sats, 800_000); + assert_eq!(node_b.list_balances().total_lightning_balance_sats, 200_000); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn lsps2_client_service_integration() { - do_lsps2_client_service_integration(true).await; - do_lsps2_client_service_integration(false).await; + do_lsps2_client_service_integration(true).await; + do_lsps2_client_service_integration(false).await; } async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); - - let mut sync_config = EsploraSyncConfig::default(); - sync_config.background_sync_config = None; - - // Setup three nodes: service, client, and payer - let channel_opening_fee_ppm = 10_000; - let channel_over_provisioning_ppm = 100_000; - let lsps2_service_config = LSPS2ServiceConfig { - require_token: None, - advertise_service: false, - channel_opening_fee_ppm, - channel_over_provisioning_ppm, - max_payment_size_msat: 1_000_000_000, - min_payment_size_msat: 0, - min_channel_lifetime: 100, - min_channel_opening_fee_msat: 0, - max_client_to_self_delay: 1024, - client_trusts_lsp, - }; - - let service_config = random_config(true); - setup_builder!(service_builder, service_config.node_config); - service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - service_builder.set_liquidity_provider_lsps2(lsps2_service_config); - let service_node = service_builder.build(service_config.node_entropy.into()).unwrap(); - service_node.start().unwrap(); - - let service_node_id = service_node.node_id(); - let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); - - let client_config = random_config(true); - setup_builder!(client_builder, client_config.node_config); - client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - client_builder.set_liquidity_source_lsps2(service_node_id, service_addr, None); - let client_node = client_builder.build(client_config.node_entropy.into()).unwrap(); - client_node.start().unwrap(); - - let payer_config = random_config(true); - setup_builder!(payer_builder, payer_config.node_config); - payer_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - let payer_node = payer_builder.build(payer_config.node_entropy.into()).unwrap(); - payer_node.start().unwrap(); - - let service_addr = service_node.onchain_payment().new_address().unwrap(); - let client_addr = client_node.onchain_payment().new_address().unwrap(); - let payer_addr = payer_node.onchain_payment().new_address().unwrap(); - - let premine_amount_sat = 10_000_000; - - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![service_addr, client_addr, payer_addr], - Amount::from_sat(premine_amount_sat), - ) - .await; - service_node.sync_wallets().unwrap(); - client_node.sync_wallets().unwrap(); - payer_node.sync_wallets().unwrap(); - - // Open a channel payer -> service that will allow paying the JIT invoice - println!("Opening channel payer_node -> service_node!"); - open_channel(&payer_node, &service_node, 5_000_000, false, &electrsd).await; - - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - service_node.sync_wallets().unwrap(); - payer_node.sync_wallets().unwrap(); - expect_channel_ready_event!(payer_node, service_node.node_id()); - expect_channel_ready_event!(service_node, payer_node.node_id()); - - let invoice_description = - Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()); - let jit_amount_msat = 100_000_000; - - println!("Generating JIT invoice!"); - let jit_invoice = client_node - .bolt11_payment() - .receive_via_jit_channel(jit_amount_msat, &invoice_description.into(), 1024, None) - .unwrap(); - - // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. - println!("Paying JIT invoice!"); - let payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); - expect_channel_pending_event!(service_node, client_node.node_id()); - expect_channel_ready_event!(service_node, client_node.node_id()); - expect_event!(service_node, PaymentForwarded); - expect_channel_pending_event!(client_node, service_node.node_id()); - expect_channel_ready_event!(client_node, service_node.node_id()); - - let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; - let expected_received_amount_msat = jit_amount_msat - service_fee_msat; - expect_payment_successful_event!(payer_node, Some(payment_id), None); - let client_payment_id = - expect_payment_received_event!(client_node, expected_received_amount_msat).unwrap(); - let client_payment = client_node.payment(&client_payment_id).unwrap(); - match client_payment.kind { - PaymentKind::Bolt11Jit { counterparty_skimmed_fee_msat, .. } => { - assert_eq!(counterparty_skimmed_fee_msat, Some(service_fee_msat)); - }, - _ => panic!("Unexpected payment kind"), - } - - let expected_channel_overprovisioning_msat = - (expected_received_amount_msat * channel_over_provisioning_ppm as u64) / 1_000_000; - let expected_channel_size_sat = - (expected_received_amount_msat + expected_channel_overprovisioning_msat) / 1000; - let channel_value_sats = client_node.list_channels().first().unwrap().channel_value_sats; - assert_eq!(channel_value_sats, expected_channel_size_sat); - - println!("Generating regular invoice!"); - let invoice_description = - Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()).into(); - let amount_msat = 5_000_000; - let invoice = - client_node.bolt11_payment().receive(amount_msat, &invoice_description, 1024).unwrap(); - - // Have the payer_node pay the invoice, to check regular forwards service_node -> client_node - // are working as expected. - println!("Paying regular invoice!"); - let payment_id = payer_node.bolt11_payment().send(&invoice, None).unwrap(); - expect_payment_successful_event!(payer_node, Some(payment_id), None); - expect_event!(service_node, PaymentForwarded); - expect_payment_received_event!(client_node, amount_msat); - - //////////////////////////////////////////////////////////////////////////// - // receive_via_jit_channel_for_hash and claim_for_hash - //////////////////////////////////////////////////////////////////////////// - println!("Generating JIT invoice!"); - // Increase the amount to make sure it does not fit into the existing channels. - let jit_amount_msat = 200_000_000; - let manual_preimage = PaymentPreimage([42u8; 32]); - let manual_payment_hash: PaymentHash = manual_preimage.into(); - let jit_invoice = client_node - .bolt11_payment() - .receive_via_jit_channel_for_hash( - jit_amount_msat, - &invoice_description, - 1024, - None, - manual_payment_hash, - ) - .unwrap(); - - // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. - println!("Paying JIT invoice!"); - let payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); - expect_channel_pending_event!(service_node, client_node.node_id()); - expect_channel_ready_event!(service_node, client_node.node_id()); - expect_channel_pending_event!(client_node, service_node.node_id()); - expect_channel_ready_event!(client_node, service_node.node_id()); - - let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; - let expected_received_amount_msat = jit_amount_msat - service_fee_msat; - let claimable_amount_msat = expect_payment_claimable_event!( - client_node, - payment_id, - manual_payment_hash, - expected_received_amount_msat - ); - println!("Claiming payment!"); - client_node - .bolt11_payment() - .claim_for_hash(manual_payment_hash, claimable_amount_msat, manual_preimage) - .unwrap(); - - expect_event!(service_node, PaymentForwarded); - expect_payment_successful_event!(payer_node, Some(payment_id), None); - let client_payment_id = - expect_payment_received_event!(client_node, expected_received_amount_msat).unwrap(); - let client_payment = client_node.payment(&client_payment_id).unwrap(); - match client_payment.kind { - PaymentKind::Bolt11Jit { counterparty_skimmed_fee_msat, .. } => { - assert_eq!(counterparty_skimmed_fee_msat, Some(service_fee_msat)); - }, - _ => panic!("Unexpected payment kind"), - } - - //////////////////////////////////////////////////////////////////////////// - // receive_via_jit_channel_for_hash and fail_for_hash - //////////////////////////////////////////////////////////////////////////// - println!("Generating JIT invoice!"); - // Increase the amount to make sure it does not fit into the existing channels. - let jit_amount_msat = 400_000_000; - let manual_preimage = PaymentPreimage([43u8; 32]); - let manual_payment_hash: PaymentHash = manual_preimage.into(); - let jit_invoice = client_node - .bolt11_payment() - .receive_via_jit_channel_for_hash( - jit_amount_msat, - &invoice_description, - 1024, - None, - manual_payment_hash, - ) - .unwrap(); - - // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. - println!("Paying JIT invoice!"); - let payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); - expect_channel_pending_event!(service_node, client_node.node_id()); - expect_channel_ready_event!(service_node, client_node.node_id()); - expect_channel_pending_event!(client_node, service_node.node_id()); - expect_channel_ready_event!(client_node, service_node.node_id()); - - let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; - let expected_received_amount_msat = jit_amount_msat - service_fee_msat; - expect_payment_claimable_event!( - client_node, - payment_id, - manual_payment_hash, - expected_received_amount_msat - ); - println!("Failing payment!"); - client_node.bolt11_payment().fail_for_hash(manual_payment_hash).unwrap(); - - expect_event!(payer_node, PaymentFailed); - assert_eq!(client_node.payment(&payment_id).unwrap().status, PaymentStatus::Failed); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + + let mut sync_config = EsploraSyncConfig::default(); + sync_config.background_sync_config = None; + + // Setup three nodes: service, client, and payer + let channel_opening_fee_ppm = 10_000; + let channel_over_provisioning_ppm = 100_000; + let lsps2_service_config = LSPS2ServiceConfig { + require_token: None, + advertise_service: false, + channel_opening_fee_ppm, + channel_over_provisioning_ppm, + max_payment_size_msat: 1_000_000_000, + min_payment_size_msat: 0, + min_channel_lifetime: 100, + min_channel_opening_fee_msat: 0, + max_client_to_self_delay: 1024, + client_trusts_lsp, + }; + + let service_config = random_config(true); + setup_builder!(service_builder, service_config.node_config); + service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + service_builder.set_liquidity_provider_lsps2(lsps2_service_config); + let service_node = service_builder.build(service_config.node_entropy.into()).unwrap(); + service_node.start().unwrap(); + + let service_node_id = service_node.node_id(); + let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); + + let client_config = random_config(true); + setup_builder!(client_builder, client_config.node_config); + client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + client_builder.set_liquidity_source_lsps2(service_node_id, service_addr, None); + let client_node = client_builder.build(client_config.node_entropy.into()).unwrap(); + client_node.start().unwrap(); + + let payer_config = random_config(true); + setup_builder!(payer_builder, payer_config.node_config); + payer_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + let payer_node = payer_builder.build(payer_config.node_entropy.into()).unwrap(); + payer_node.start().unwrap(); + + let service_addr = service_node.onchain_payment().new_address().unwrap(); + let client_addr = client_node.onchain_payment().new_address().unwrap(); + let payer_addr = payer_node.onchain_payment().new_address().unwrap(); + + let premine_amount_sat = 10_000_000; + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![service_addr, client_addr, payer_addr], + Amount::from_sat(premine_amount_sat), + ) + .await; + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + + // Open a channel payer -> service that will allow paying the JIT invoice + println!("Opening channel payer_node -> service_node!"); + let outpoint = open_channel(&payer_node, &service_node, 5_000_000, false, &electrsd).await; + wait_for_tx(&electrsd.client, outpoint.txid).await; + + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + service_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + + wait_for_node_block(&service_node, tip).await; + wait_for_node_block(&payer_node, tip).await; + expect_channel_ready_event!(payer_node, service_node.node_id()); + expect_channel_ready_event!(service_node, payer_node.node_id()); + + let invoice_description = + Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()); + let jit_amount_msat = 100_000_000; + + println!("Generating JIT invoice!"); + let jit_invoice = client_node + .bolt11_payment() + .receive_via_jit_channel(jit_amount_msat, &invoice_description.into(), 1024, None) + .unwrap(); + + // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. + println!("Paying JIT invoice!"); + let payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); + expect_channel_pending_event!(service_node, client_node.node_id()); + expect_channel_ready_event!(service_node, client_node.node_id()); + expect_event!(service_node, PaymentForwarded); + expect_channel_pending_event!(client_node, service_node.node_id()); + expect_channel_ready_event!(client_node, service_node.node_id()); + + let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; + let expected_received_amount_msat = jit_amount_msat - service_fee_msat; + expect_payment_successful_event!(payer_node, Some(payment_id), None); + let client_payment_id = + expect_payment_received_event!(client_node, expected_received_amount_msat).unwrap(); + let client_payment = client_node.payment(&client_payment_id).unwrap(); + match client_payment.kind { + PaymentKind::Bolt11Jit { counterparty_skimmed_fee_msat, .. } => { + assert_eq!(counterparty_skimmed_fee_msat, Some(service_fee_msat)); + }, + _ => panic!("Unexpected payment kind"), + } + + let expected_channel_overprovisioning_msat = + (expected_received_amount_msat * channel_over_provisioning_ppm as u64) / 1_000_000; + let expected_channel_size_sat = + (expected_received_amount_msat + expected_channel_overprovisioning_msat) / 1000; + let channel_value_sats = client_node.list_channels().first().unwrap().channel_value_sats; + assert_eq!(channel_value_sats, expected_channel_size_sat); + + println!("Generating regular invoice!"); + let invoice_description = + Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()).into(); + let amount_msat = 5_000_000; + let invoice = + client_node.bolt11_payment().receive(amount_msat, &invoice_description, 1024).unwrap(); + + // Have the payer_node pay the invoice, to check regular forwards service_node -> client_node + // are working as expected. + println!("Paying regular invoice!"); + let payment_id = payer_node.bolt11_payment().send(&invoice, None).unwrap(); + expect_payment_successful_event!(payer_node, Some(payment_id), None); + expect_event!(service_node, PaymentForwarded); + expect_payment_received_event!(client_node, amount_msat); + + //////////////////////////////////////////////////////////////////////////// + // receive_via_jit_channel_for_hash and claim_for_hash + //////////////////////////////////////////////////////////////////////////// + println!("Generating JIT invoice!"); + // Increase the amount to make sure it does not fit into the existing channels. + let jit_amount_msat = 200_000_000; + let manual_preimage = PaymentPreimage([42u8; 32]); + let manual_payment_hash: PaymentHash = manual_preimage.into(); + let jit_invoice = client_node + .bolt11_payment() + .receive_via_jit_channel_for_hash( + jit_amount_msat, + &invoice_description, + 1024, + None, + manual_payment_hash, + ) + .unwrap(); + + // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. + println!("Paying JIT invoice!"); + let payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); + expect_channel_pending_event!(service_node, client_node.node_id()); + expect_channel_ready_event!(service_node, client_node.node_id()); + expect_channel_pending_event!(client_node, service_node.node_id()); + expect_channel_ready_event!(client_node, service_node.node_id()); + + let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; + let expected_received_amount_msat = jit_amount_msat - service_fee_msat; + let claimable_amount_msat = expect_payment_claimable_event!( + client_node, + payment_id, + manual_payment_hash, + expected_received_amount_msat + ); + println!("Claiming payment!"); + client_node + .bolt11_payment() + .claim_for_hash(manual_payment_hash, claimable_amount_msat, manual_preimage) + .unwrap(); + + expect_event!(service_node, PaymentForwarded); + expect_payment_successful_event!(payer_node, Some(payment_id), None); + let client_payment_id = + expect_payment_received_event!(client_node, expected_received_amount_msat).unwrap(); + let client_payment = client_node.payment(&client_payment_id).unwrap(); + match client_payment.kind { + PaymentKind::Bolt11Jit { counterparty_skimmed_fee_msat, .. } => { + assert_eq!(counterparty_skimmed_fee_msat, Some(service_fee_msat)); + }, + _ => panic!("Unexpected payment kind"), + } + + //////////////////////////////////////////////////////////////////////////// + // receive_via_jit_channel_for_hash and fail_for_hash + //////////////////////////////////////////////////////////////////////////// + println!("Generating JIT invoice!"); + // Increase the amount to make sure it does not fit into the existing channels. + let jit_amount_msat = 400_000_000; + let manual_preimage = PaymentPreimage([43u8; 32]); + let manual_payment_hash: PaymentHash = manual_preimage.into(); + let jit_invoice = client_node + .bolt11_payment() + .receive_via_jit_channel_for_hash( + jit_amount_msat, + &invoice_description, + 1024, + None, + manual_payment_hash, + ) + .unwrap(); + + // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. + println!("Paying JIT invoice!"); + let payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); + expect_channel_pending_event!(service_node, client_node.node_id()); + expect_channel_ready_event!(service_node, client_node.node_id()); + expect_channel_pending_event!(client_node, service_node.node_id()); + expect_channel_ready_event!(client_node, service_node.node_id()); + + let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; + let expected_received_amount_msat = jit_amount_msat - service_fee_msat; + expect_payment_claimable_event!( + client_node, + payment_id, + manual_payment_hash, + expected_received_amount_msat + ); + println!("Failing payment!"); + client_node.bolt11_payment().fail_for_hash(manual_payment_hash).unwrap(); + + expect_event!(payer_node, PaymentFailed); + assert_eq!(client_node.payment(&payment_id).unwrap().status, PaymentStatus::Failed); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn facade_logging() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); - let logger = init_log_logger(LevelFilter::Trace); - let mut config = random_config(false); - config.log_writer = TestLogWriter::LogFacade; + let logger = init_log_logger(LevelFilter::Trace); + let mut config = random_config(false); + config.log_writer = TestLogWriter::LogFacade; - println!("== Facade logging starts =="); - let _node = setup_node(&chain_source, config); + println!("== Facade logging starts =="); + let _node = setup_node(&chain_source, config); - assert!(!logger.retrieve_logs().is_empty()); - for (_, entry) in logger.retrieve_logs().iter().enumerate() { - validate_log_entry(entry); - } + assert!(!logger.retrieve_logs().is_empty()); + for (_, entry) in logger.retrieve_logs().iter().enumerate() { + validate_log_entry(entry); + } } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn spontaneous_send_with_custom_preimage() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); - - let address_a = node_a.onchain_payment().new_address().unwrap(); - let premine_sat = 1_000_000; - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![address_a], - Amount::from_sat(premine_sat), - ) - .await; - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - open_channel(&node_a, &node_b, 500_000, true, &electrsd).await; - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - expect_channel_ready_event!(node_a, node_b.node_id()); - expect_channel_ready_event!(node_b, node_a.node_id()); - - let seed = b"test_payment_preimage"; - let bytes: Sha256Hash = Sha256Hash::hash(seed); - let custom_bytes = bytes.to_byte_array(); - let custom_preimage = PaymentPreimage(custom_bytes); - - let amount_msat = 100_000; - let payment_id = node_a - .spontaneous_payment() - .send_with_preimage(amount_msat, node_b.node_id(), custom_preimage, None) - .unwrap(); - - // check payment status and verify stored preimage - expect_payment_successful_event!(node_a, Some(payment_id), None); - let details: PaymentDetails = - node_a.list_payments_with_filter(|p| p.id == payment_id).first().unwrap().clone(); - assert_eq!(details.status, PaymentStatus::Succeeded); - if let PaymentKind::Spontaneous { preimage: Some(pi), .. } = details.kind { - assert_eq!(pi.0, custom_bytes); - } else { - panic!("Expected a spontaneous PaymentKind with a preimage"); - } - - // Verify receiver side (node_b) - expect_payment_received_event!(node_b, amount_msat); - let receiver_payments: Vec = node_b.list_payments_with_filter(|p| { - p.direction == PaymentDirection::Inbound - && matches!(p.kind, PaymentKind::Spontaneous { .. }) - }); - - assert_eq!(receiver_payments.len(), 1); - let receiver_details = &receiver_payments[0]; - assert_eq!(receiver_details.status, PaymentStatus::Succeeded); - assert_eq!(receiver_details.amount_msat, Some(amount_msat)); - assert_eq!(receiver_details.direction, PaymentDirection::Inbound); - - // Verify receiver also has the same preimage - if let PaymentKind::Spontaneous { preimage: Some(pi), .. } = &receiver_details.kind { - assert_eq!(pi.0, custom_bytes); - } else { - panic!("Expected receiver to have spontaneous PaymentKind with preimage"); - } + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + + let address_a = node_a.onchain_payment().new_address().unwrap(); + let premine_sat = 1_000_000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![address_a], + Amount::from_sat(premine_sat), + ) + .await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + let outpoint = open_channel(&node_a, &node_b, 500_000, true, &electrsd).await; + wait_for_tx(&electrsd.client, outpoint.txid).await; + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; + + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + let seed = b"test_payment_preimage"; + let bytes: Sha256Hash = Sha256Hash::hash(seed); + let custom_bytes = bytes.to_byte_array(); + let custom_preimage = PaymentPreimage(custom_bytes); + + let amount_msat = 100_000; + let payment_id = node_a + .spontaneous_payment() + .send_with_preimage(amount_msat, node_b.node_id(), custom_preimage, None) + .unwrap(); + + // check payment status and verify stored preimage + expect_payment_successful_event!(node_a, Some(payment_id), None); + let details: PaymentDetails = + node_a.list_payments_with_filter(|p| p.id == payment_id).first().unwrap().clone(); + assert_eq!(details.status, PaymentStatus::Succeeded); + if let PaymentKind::Spontaneous { preimage: Some(pi), .. } = details.kind { + assert_eq!(pi.0, custom_bytes); + } else { + panic!("Expected a spontaneous PaymentKind with a preimage"); + } + + // Verify receiver side (node_b) + expect_payment_received_event!(node_b, amount_msat); + let receiver_payments: Vec = node_b.list_payments_with_filter(|p| { + p.direction == PaymentDirection::Inbound + && matches!(p.kind, PaymentKind::Spontaneous { .. }) + }); + + assert_eq!(receiver_payments.len(), 1); + let receiver_details = &receiver_payments[0]; + assert_eq!(receiver_details.status, PaymentStatus::Succeeded); + assert_eq!(receiver_details.amount_msat, Some(amount_msat)); + assert_eq!(receiver_details.direction, PaymentDirection::Inbound); + + // Verify receiver also has the same preimage + if let PaymentKind::Spontaneous { preimage: Some(pi), .. } = &receiver_details.kind { + assert_eq!(pi.0, custom_bytes); + } else { + panic!("Expected receiver to have spontaneous PaymentKind with preimage"); + } } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn drop_in_async_context() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - let config = random_config(true); - let node = setup_node(&chain_source, config); - node.stop().unwrap(); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let config = random_config(true); + let node = setup_node(&chain_source, config); + node.stop().unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn lsps2_client_trusts_lsp() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - - let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); - - let mut sync_config = EsploraSyncConfig::default(); - sync_config.background_sync_config = None; - - // Setup three nodes: service, client, and payer - let channel_opening_fee_ppm = 10_000; - let channel_over_provisioning_ppm = 100_000; - let lsps2_service_config = LSPS2ServiceConfig { - require_token: None, - advertise_service: false, - channel_opening_fee_ppm, - channel_over_provisioning_ppm, - max_payment_size_msat: 1_000_000_000, - min_payment_size_msat: 0, - min_channel_lifetime: 100, - min_channel_opening_fee_msat: 0, - max_client_to_self_delay: 1024, - client_trusts_lsp: true, - }; - - let service_config = random_config(true); - setup_builder!(service_builder, service_config.node_config); - service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - service_builder.set_liquidity_provider_lsps2(lsps2_service_config); - let service_node = service_builder.build(service_config.node_entropy.into()).unwrap(); - service_node.start().unwrap(); - let service_node_id = service_node.node_id(); - let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); - - let client_config = random_config(true); - setup_builder!(client_builder, client_config.node_config); - client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - client_builder.set_liquidity_source_lsps2(service_node_id, service_addr.clone(), None); - let client_node = client_builder.build(client_config.node_entropy.into()).unwrap(); - client_node.start().unwrap(); - let client_node_id = client_node.node_id(); - - let payer_config = random_config(true); - setup_builder!(payer_builder, payer_config.node_config); - payer_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - let payer_node = payer_builder.build(payer_config.node_entropy.into()).unwrap(); - payer_node.start().unwrap(); - - let service_addr_onchain = service_node.onchain_payment().new_address().unwrap(); - let client_addr_onchain = client_node.onchain_payment().new_address().unwrap(); - let payer_addr_onchain = payer_node.onchain_payment().new_address().unwrap(); - - let premine_amount_sat = 10_000_000; - - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![service_addr_onchain, client_addr_onchain, payer_addr_onchain], - Amount::from_sat(premine_amount_sat), - ) - .await; - service_node.sync_wallets().unwrap(); - client_node.sync_wallets().unwrap(); - payer_node.sync_wallets().unwrap(); - println!("Premine complete!"); - // Open a channel payer -> service that will allow paying the JIT invoice - open_channel(&payer_node, &service_node, 5_000_000, false, &electrsd).await; - - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - service_node.sync_wallets().unwrap(); - payer_node.sync_wallets().unwrap(); - expect_channel_ready_event!(payer_node, service_node.node_id()); - expect_channel_ready_event!(service_node, payer_node.node_id()); - - let invoice_description = - Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()); - let jit_amount_msat = 100_000_000; - - println!("Generating JIT invoice!"); - let manual_preimage = PaymentPreimage([42u8; 32]); - let manual_payment_hash: PaymentHash = manual_preimage.into(); - let res = client_node - .bolt11_payment() - .receive_via_jit_channel_for_hash( - jit_amount_msat, - &invoice_description.into(), - 1024, - None, - manual_payment_hash, - ) - .unwrap(); - - // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. - println!("Paying JIT invoice!"); - let payment_id = payer_node.bolt11_payment().send(&res, None).unwrap(); - println!("Payment ID: {:?}", payment_id); - let funding_txo = expect_channel_pending_event!(service_node, client_node.node_id()); - expect_channel_ready_event!(service_node, client_node.node_id()); - expect_channel_pending_event!(client_node, service_node.node_id()); - expect_channel_ready_event!(client_node, service_node.node_id()); - - // Check the funding transaction hasn't been broadcasted yet and nodes aren't seeing it. - println!("Try to find funding tx... It won't be found yet, as the client has not claimed it."); - tokio::time::sleep(std::time::Duration::from_secs(3)).await; - let mempool = bitcoind.client.get_raw_mempool().unwrap().into_model().unwrap(); - let funding_tx_found = mempool.0.iter().any(|txid| *txid == funding_txo.txid); - assert!(!funding_tx_found, "Funding transaction should NOT be broadcast yet"); - - service_node.sync_wallets().unwrap(); - client_node.sync_wallets().unwrap(); - assert_eq!( - client_node - .list_channels() - .iter() - .find(|c| c.counterparty_node_id == service_node_id) - .unwrap() - .confirmations, - Some(0) - ); - assert_eq!( - service_node - .list_channels() - .iter() - .find(|c| c.counterparty_node_id == client_node_id) - .unwrap() - .confirmations, - Some(0) - ); - - // Now claim the JIT payment, which should release the funding transaction - let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; - let expected_received_amount_msat = jit_amount_msat - service_fee_msat; - - let _ = expect_payment_claimable_event!( - client_node, - payment_id, - manual_payment_hash, - expected_received_amount_msat - ); - - client_node - .bolt11_payment() - .claim_for_hash(manual_payment_hash, jit_amount_msat, manual_preimage) - .unwrap(); - - expect_payment_successful_event!(payer_node, Some(payment_id), None); - - let _ = expect_payment_received_event!(client_node, expected_received_amount_msat).unwrap(); - - // Check the nodes pick up on the confirmed funding tx now. - wait_for_tx(&electrsd.client, funding_txo.txid).await; - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - service_node.sync_wallets().unwrap(); - client_node.sync_wallets().unwrap(); - assert_eq!( - client_node - .list_channels() - .iter() - .find(|c| c.counterparty_node_id == service_node_id) - .unwrap() - .confirmations, - Some(6) - ); - assert_eq!( - service_node - .list_channels() - .iter() - .find(|c| c.counterparty_node_id == client_node_id) - .unwrap() - .confirmations, - Some(6) - ); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + + let mut sync_config = EsploraSyncConfig::default(); + sync_config.background_sync_config = None; + + // Setup three nodes: service, client, and payer + let channel_opening_fee_ppm = 10_000; + let channel_over_provisioning_ppm = 100_000; + let lsps2_service_config = LSPS2ServiceConfig { + require_token: None, + advertise_service: false, + channel_opening_fee_ppm, + channel_over_provisioning_ppm, + max_payment_size_msat: 1_000_000_000, + min_payment_size_msat: 0, + min_channel_lifetime: 100, + min_channel_opening_fee_msat: 0, + max_client_to_self_delay: 1024, + client_trusts_lsp: true, + }; + + let service_config = random_config(true); + setup_builder!(service_builder, service_config.node_config); + service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + service_builder.set_liquidity_provider_lsps2(lsps2_service_config); + let service_node = service_builder.build(service_config.node_entropy.into()).unwrap(); + service_node.start().unwrap(); + let service_node_id = service_node.node_id(); + let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); + + let client_config = random_config(true); + setup_builder!(client_builder, client_config.node_config); + client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + client_builder.set_liquidity_source_lsps2(service_node_id, service_addr.clone(), None); + let client_node = client_builder.build(client_config.node_entropy.into()).unwrap(); + client_node.start().unwrap(); + let client_node_id = client_node.node_id(); + + let payer_config = random_config(true); + setup_builder!(payer_builder, payer_config.node_config); + payer_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + let payer_node = payer_builder.build(payer_config.node_entropy.into()).unwrap(); + payer_node.start().unwrap(); + + let service_addr_onchain = service_node.onchain_payment().new_address().unwrap(); + let client_addr_onchain = client_node.onchain_payment().new_address().unwrap(); + let payer_addr_onchain = payer_node.onchain_payment().new_address().unwrap(); + + let premine_amount_sat = 10_000_000; + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![service_addr_onchain, client_addr_onchain, payer_addr_onchain], + Amount::from_sat(premine_amount_sat), + ) + .await; + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + println!("Premine complete!"); + // Open a channel payer -> service that will allow paying the JIT invoice + let outpoint = open_channel(&payer_node, &service_node, 5_000_000, false, &electrsd).await; + wait_for_tx(&electrsd.client, outpoint.txid).await; + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + service_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + + wait_for_node_block(&service_node, tip).await; + wait_for_node_block(&payer_node, tip).await; + expect_channel_ready_event!(payer_node, service_node.node_id()); + expect_channel_ready_event!(service_node, payer_node.node_id()); + + let invoice_description = + Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()); + let jit_amount_msat = 100_000_000; + + println!("Generating JIT invoice!"); + let manual_preimage = PaymentPreimage([42u8; 32]); + let manual_payment_hash: PaymentHash = manual_preimage.into(); + let res = client_node + .bolt11_payment() + .receive_via_jit_channel_for_hash( + jit_amount_msat, + &invoice_description.into(), + 1024, + None, + manual_payment_hash, + ) + .unwrap(); + + // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. + println!("Paying JIT invoice!"); + let payment_id = payer_node.bolt11_payment().send(&res, None).unwrap(); + println!("Payment ID: {:?}", payment_id); + let funding_txo = expect_channel_pending_event!(service_node, client_node.node_id()); + expect_channel_ready_event!(service_node, client_node.node_id()); + expect_channel_pending_event!(client_node, service_node.node_id()); + expect_channel_ready_event!(client_node, service_node.node_id()); + + // Check the funding transaction hasn't been broadcasted yet and nodes aren't seeing it. + println!("Try to find funding tx... It won't be found yet, as the client has not claimed it."); + tokio::time::sleep(std::time::Duration::from_secs(3)).await; + let mempool = bitcoind.client.get_raw_mempool().unwrap().into_model().unwrap(); + let funding_tx_found = mempool.0.iter().any(|txid| *txid == funding_txo.txid); + assert!(!funding_tx_found, "Funding transaction should NOT be broadcast yet"); + + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + assert_eq!( + client_node + .list_channels() + .iter() + .find(|c| c.counterparty_node_id == service_node_id) + .unwrap() + .confirmations, + Some(0) + ); + assert_eq!( + service_node + .list_channels() + .iter() + .find(|c| c.counterparty_node_id == client_node_id) + .unwrap() + .confirmations, + Some(0) + ); + + // Now claim the JIT payment, which should release the funding transaction + let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; + let expected_received_amount_msat = jit_amount_msat - service_fee_msat; + + let _ = expect_payment_claimable_event!( + client_node, + payment_id, + manual_payment_hash, + expected_received_amount_msat + ); + + client_node + .bolt11_payment() + .claim_for_hash(manual_payment_hash, jit_amount_msat, manual_preimage) + .unwrap(); + + expect_payment_successful_event!(payer_node, Some(payment_id), None); + + let _ = expect_payment_received_event!(client_node, expected_received_amount_msat).unwrap(); + + // Check the nodes pick up on the confirmed funding tx now. + wait_for_tx(&electrsd.client, funding_txo.txid).await; + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + wait_for_node_block(&service_node, tip).await; + wait_for_node_block(&client_node, tip).await; + assert_eq!( + client_node + .list_channels() + .iter() + .find(|c| c.counterparty_node_id == service_node_id) + .unwrap() + .confirmations, + Some(6) + ); + assert_eq!( + service_node + .list_channels() + .iter() + .find(|c| c.counterparty_node_id == client_node_id) + .unwrap() + .confirmations, + Some(6) + ); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn lsps2_lsp_trusts_client_but_client_does_not_claim() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - - let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); - - let mut sync_config = EsploraSyncConfig::default(); - sync_config.background_sync_config = None; - - // Setup three nodes: service, client, and payer - let channel_opening_fee_ppm = 10_000; - let channel_over_provisioning_ppm = 100_000; - let lsps2_service_config = LSPS2ServiceConfig { - require_token: None, - advertise_service: false, - channel_opening_fee_ppm, - channel_over_provisioning_ppm, - max_payment_size_msat: 1_000_000_000, - min_payment_size_msat: 0, - min_channel_lifetime: 100, - min_channel_opening_fee_msat: 0, - max_client_to_self_delay: 1024, - client_trusts_lsp: false, - }; - - let service_config = random_config(true); - setup_builder!(service_builder, service_config.node_config); - service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - service_builder.set_liquidity_provider_lsps2(lsps2_service_config); - let service_node = service_builder.build(service_config.node_entropy.into()).unwrap(); - service_node.start().unwrap(); - - let service_node_id = service_node.node_id(); - let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); - - let client_config = random_config(true); - setup_builder!(client_builder, client_config.node_config); - client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - client_builder.set_liquidity_source_lsps2(service_node_id, service_addr.clone(), None); - let client_node = client_builder.build(client_config.node_entropy.into()).unwrap(); - client_node.start().unwrap(); - - let client_node_id = client_node.node_id(); - - let payer_config = random_config(true); - setup_builder!(payer_builder, payer_config.node_config); - payer_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - let payer_node = payer_builder.build(payer_config.node_entropy.into()).unwrap(); - payer_node.start().unwrap(); - - let service_addr_onchain = service_node.onchain_payment().new_address().unwrap(); - let client_addr_onchain = client_node.onchain_payment().new_address().unwrap(); - let payer_addr_onchain = payer_node.onchain_payment().new_address().unwrap(); - - let premine_amount_sat = 10_000_000; - - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![service_addr_onchain, client_addr_onchain, payer_addr_onchain], - Amount::from_sat(premine_amount_sat), - ) - .await; - service_node.sync_wallets().unwrap(); - client_node.sync_wallets().unwrap(); - payer_node.sync_wallets().unwrap(); - println!("Premine complete!"); - // Open a channel payer -> service that will allow paying the JIT invoice - open_channel(&payer_node, &service_node, 5_000_000, false, &electrsd).await; - - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - service_node.sync_wallets().unwrap(); - payer_node.sync_wallets().unwrap(); - expect_channel_ready_event!(payer_node, service_node.node_id()); - expect_channel_ready_event!(service_node, payer_node.node_id()); - - let invoice_description = - Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()); - let jit_amount_msat = 100_000_000; - - println!("Generating JIT invoice!"); - let manual_preimage = PaymentPreimage([42u8; 32]); - let manual_payment_hash: PaymentHash = manual_preimage.into(); - let res = client_node - .bolt11_payment() - .receive_via_jit_channel_for_hash( - jit_amount_msat, - &invoice_description.into(), - 1024, - None, - manual_payment_hash, - ) - .unwrap(); - - // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. - println!("Paying JIT invoice!"); - let _payment_id = payer_node.bolt11_payment().send(&res, None).unwrap(); - let funding_txo = expect_channel_pending_event!(service_node, client_node.node_id()); - expect_channel_ready_event!(service_node, client_node.node_id()); - expect_channel_pending_event!(client_node, service_node.node_id()); - expect_channel_ready_event!(client_node, service_node.node_id()); - println!("Waiting for funding transaction to be broadcast..."); - - // Check the nodes pick up on the confirmed funding tx now. - wait_for_tx(&electrsd.client, funding_txo.txid).await; - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - service_node.sync_wallets().unwrap(); - client_node.sync_wallets().unwrap(); - assert_eq!( - client_node - .list_channels() - .iter() - .find(|c| c.counterparty_node_id == service_node_id) - .unwrap() - .confirmations, - Some(6) - ); - assert_eq!( - service_node - .list_channels() - .iter() - .find(|c| c.counterparty_node_id == client_node_id) - .unwrap() - .confirmations, - Some(6) - ); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + + let mut sync_config = EsploraSyncConfig::default(); + sync_config.background_sync_config = None; + + // Setup three nodes: service, client, and payer + let channel_opening_fee_ppm = 10_000; + let channel_over_provisioning_ppm = 100_000; + let lsps2_service_config = LSPS2ServiceConfig { + require_token: None, + advertise_service: false, + channel_opening_fee_ppm, + channel_over_provisioning_ppm, + max_payment_size_msat: 1_000_000_000, + min_payment_size_msat: 0, + min_channel_lifetime: 100, + min_channel_opening_fee_msat: 0, + max_client_to_self_delay: 1024, + client_trusts_lsp: false, + }; + + let service_config = random_config(true); + setup_builder!(service_builder, service_config.node_config); + service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + service_builder.set_liquidity_provider_lsps2(lsps2_service_config); + let service_node = service_builder.build(service_config.node_entropy.into()).unwrap(); + service_node.start().unwrap(); + + let service_node_id = service_node.node_id(); + let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); + + let client_config = random_config(true); + setup_builder!(client_builder, client_config.node_config); + client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + client_builder.set_liquidity_source_lsps2(service_node_id, service_addr.clone(), None); + let client_node = client_builder.build(client_config.node_entropy.into()).unwrap(); + client_node.start().unwrap(); + + let client_node_id = client_node.node_id(); + + let payer_config = random_config(true); + setup_builder!(payer_builder, payer_config.node_config); + payer_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + let payer_node = payer_builder.build(payer_config.node_entropy.into()).unwrap(); + payer_node.start().unwrap(); + + let service_addr_onchain = service_node.onchain_payment().new_address().unwrap(); + let client_addr_onchain = client_node.onchain_payment().new_address().unwrap(); + let payer_addr_onchain = payer_node.onchain_payment().new_address().unwrap(); + + let premine_amount_sat = 10_000_000; + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![service_addr_onchain, client_addr_onchain, payer_addr_onchain], + Amount::from_sat(premine_amount_sat), + ) + .await; + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + println!("Premine complete!"); + // Open a channel payer -> service that will allow paying the JIT invoice + let outpoint = open_channel(&payer_node, &service_node, 5_000_000, false, &electrsd).await; + wait_for_tx(&electrsd.client, outpoint.txid).await; + + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + service_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + wait_for_node_block(&service_node, tip).await; + wait_for_node_block(&payer_node, tip).await; + expect_channel_ready_event!(payer_node, service_node.node_id()); + expect_channel_ready_event!(service_node, payer_node.node_id()); + + let invoice_description = + Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()); + let jit_amount_msat = 100_000_000; + + println!("Generating JIT invoice!"); + let manual_preimage = PaymentPreimage([42u8; 32]); + let manual_payment_hash: PaymentHash = manual_preimage.into(); + let res = client_node + .bolt11_payment() + .receive_via_jit_channel_for_hash( + jit_amount_msat, + &invoice_description.into(), + 1024, + None, + manual_payment_hash, + ) + .unwrap(); + + // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. + println!("Paying JIT invoice!"); + let _payment_id = payer_node.bolt11_payment().send(&res, None).unwrap(); + let funding_txo = expect_channel_pending_event!(service_node, client_node.node_id()); + expect_channel_ready_event!(service_node, client_node.node_id()); + expect_channel_pending_event!(client_node, service_node.node_id()); + expect_channel_ready_event!(client_node, service_node.node_id()); + println!("Waiting for funding transaction to be broadcast..."); + + // Check the nodes pick up on the confirmed funding tx now. + wait_for_tx(&electrsd.client, funding_txo.txid).await; + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + wait_for_node_block(&service_node, tip).await; + wait_for_node_block(&client_node, tip).await; + wait_for_node_block(&payer_node, tip).await; + + assert_eq!( + client_node + .list_channels() + .iter() + .find(|c| c.counterparty_node_id == service_node_id) + .unwrap() + .confirmations, + Some(6) + ); + assert_eq!( + service_node + .list_channels() + .iter() + .find(|c| c.counterparty_node_id == client_node_id) + .unwrap() + .confirmations, + Some(6) + ); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn payment_persistence_after_restart() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - - // Setup nodes manually so we can restart node_a with the same config - println!("== Node A =="); - let mut config_a = random_config(true); - config_a.store_type = TestStoreType::Sqlite; - - let num_payments = 200; - let payment_amount_msat = 1_000_000; // 1000 sats per payment - - { - let node_a = setup_node(&chain_source, config_a.clone()); - - println!("\n== Node B =="); - let config_b = random_config(true); - let node_b = setup_node(&chain_source, config_b); - - let addr_a = node_a.onchain_payment().new_address().unwrap(); - let addr_b = node_b.onchain_payment().new_address().unwrap(); - - // Premine sufficient funds for a large channel and many payments - let premine_amount_sat = 10_000_000; - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![addr_a, addr_b], - Amount::from_sat(premine_amount_sat), - ) - .await; - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); - assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); - - // Open a large channel from node_a to node_b - let channel_amount_sat = 5_000_000; - open_channel(&node_a, &node_b, channel_amount_sat, true, &electrsd).await; - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - expect_channel_ready_event!(node_a, node_b.node_id()); - expect_channel_ready_event!(node_b, node_a.node_id()); - - // Send 200 payments from node_a to node_b - println!("\nSending {} payments from A to B...", num_payments); - let invoice_description = - Bolt11InvoiceDescription::Direct(Description::new(String::from("test")).unwrap()); - - for i in 0..num_payments { - let invoice = node_b - .bolt11_payment() - .receive(payment_amount_msat, &invoice_description.clone().into(), 3600) - .unwrap(); - let payment_id = node_a.bolt11_payment().send(&invoice, None).unwrap(); - expect_event!(node_a, PaymentSuccessful); - expect_event!(node_b, PaymentReceived); - - if (i + 1) % 50 == 0 { - println!("Completed {} payments", i + 1); - } - - // Verify payment succeeded - assert_eq!(node_a.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); - } - println!("All {} payments completed successfully", num_payments); - - // Verify node_a has 200 outbound Bolt11 payments before shutdown - let outbound_payments_before = node_a.list_payments_with_filter(|p| { - p.direction == PaymentDirection::Outbound - && matches!(p.kind, PaymentKind::Bolt11 { .. }) - }); - assert_eq!(outbound_payments_before.len(), num_payments); - - // Shut down both nodes - println!("\nShutting down nodes..."); - node_a.stop().unwrap(); - node_b.stop().unwrap(); - } - - // Restart node_a with the same config - println!("\nRestarting node A..."); - let restarted_node_a = setup_node(&chain_source, config_a); - - // Assert all 200 payments are still in the store - let outbound_payments_after = restarted_node_a.list_payments_with_filter(|p| { - p.direction == PaymentDirection::Outbound && matches!(p.kind, PaymentKind::Bolt11 { .. }) - }); - assert_eq!( - outbound_payments_after.len(), - num_payments, - "Expected {} payments after restart, found {}", - num_payments, - outbound_payments_after.len() - ); - - // Verify all payments have the correct status - for payment in &outbound_payments_after { - assert_eq!( - payment.status, - PaymentStatus::Succeeded, - "Payment {:?} has unexpected status {:?}", - payment.id, - payment.status - ); - assert_eq!(payment.amount_msat, Some(payment_amount_msat)); - } - - println!( - "Successfully verified {} payments persisted after restart", - outbound_payments_after.len() - ); - - restarted_node_a.stop().unwrap(); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + + // Setup nodes manually so we can restart node_a with the same config + println!("== Node A =="); + let mut config_a = random_config(true); + config_a.store_type = TestStoreType::Sqlite; + + let num_payments = 200; + let payment_amount_msat = 1_000_000; // 1000 sats per payment + + { + let node_a = setup_node(&chain_source, config_a.clone()); + + println!("\n== Node B =="); + let config_b = random_config(true); + let node_b = setup_node(&chain_source, config_b); + + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + + // Premine sufficient funds for a large channel and many payments + let premine_amount_sat = 10_000_000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_a, addr_b], + Amount::from_sat(premine_amount_sat), + ) + .await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + + // Open a large channel from node_a to node_b + let channel_amount_sat = 5_000_000; + let outpoint = open_channel(&node_a, &node_b, channel_amount_sat, true, &electrsd).await; + wait_for_tx(&electrsd.client, outpoint.txid).await; + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; + + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + // Send 200 payments from node_a to node_b + println!("\nSending {} payments from A to B...", num_payments); + let invoice_description = + Bolt11InvoiceDescription::Direct(Description::new(String::from("test")).unwrap()); + + for i in 0..num_payments { + let invoice = node_b + .bolt11_payment() + .receive(payment_amount_msat, &invoice_description.clone().into(), 3600) + .unwrap(); + let payment_id = node_a.bolt11_payment().send(&invoice, None).unwrap(); + expect_event!(node_a, PaymentSuccessful); + expect_event!(node_b, PaymentReceived); + + if (i + 1) % 50 == 0 { + println!("Completed {} payments", i + 1); + } + + // Verify payment succeeded + assert_eq!(node_a.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); + } + println!("All {} payments completed successfully", num_payments); + + // Verify node_a has 200 outbound Bolt11 payments before shutdown + let outbound_payments_before = node_a.list_payments_with_filter(|p| { + p.direction == PaymentDirection::Outbound + && matches!(p.kind, PaymentKind::Bolt11 { .. }) + }); + assert_eq!(outbound_payments_before.len(), num_payments); + + // Shut down both nodes + println!("\nShutting down nodes..."); + node_a.stop().unwrap(); + node_b.stop().unwrap(); + } + + // Restart node_a with the same config + println!("\nRestarting node A..."); + let restarted_node_a = setup_node(&chain_source, config_a); + + // Assert all 200 payments are still in the store + let outbound_payments_after = restarted_node_a.list_payments_with_filter(|p| { + p.direction == PaymentDirection::Outbound && matches!(p.kind, PaymentKind::Bolt11 { .. }) + }); + assert_eq!( + outbound_payments_after.len(), + num_payments, + "Expected {} payments after restart, found {}", + num_payments, + outbound_payments_after.len() + ); + + // Verify all payments have the correct status + for payment in &outbound_payments_after { + assert_eq!( + payment.status, + PaymentStatus::Succeeded, + "Payment {:?} has unexpected status {:?}", + payment.id, + payment.status + ); + assert_eq!(payment.amount_msat, Some(payment_amount_msat)); + } + + println!( + "Successfully verified {} payments persisted after restart", + outbound_payments_after.len() + ); + + restarted_node_a.stop().unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn persistence_backwards_compatibility() { - let (bitcoind, electrsd) = common::setup_bitcoind_and_electrsd(); - let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); - - let storage_path = common::random_storage_path().to_str().unwrap().to_owned(); - let seed_bytes = [42u8; 64]; - - // Setup a v0.6.2 `Node` - let (old_balance, old_node_id) = { - let mut builder_old = ldk_node_062::Builder::new(); - builder_old.set_network(bitcoin::Network::Regtest); - builder_old.set_storage_dir_path(storage_path.clone()); - builder_old.set_entropy_seed_bytes(seed_bytes); - builder_old.set_chain_source_esplora(esplora_url.clone(), None); - let node_old = builder_old.build().unwrap(); - - node_old.start().unwrap(); - let addr_old = node_old.onchain_payment().new_address().unwrap(); - common::premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![addr_old], - bitcoin::Amount::from_sat(100_000), - ) - .await; - node_old.sync_wallets().unwrap(); - - let balance = node_old.list_balances().spendable_onchain_balance_sats; - assert!(balance > 0); - let node_id = node_old.node_id(); - - node_old.stop().unwrap(); - - (balance, node_id) - }; - - // Now ensure we can still reinit from the same backend. - #[cfg(feature = "uniffi")] - let builder_new = Builder::new(); - #[cfg(not(feature = "uniffi"))] - let mut builder_new = Builder::new(); - builder_new.set_network(bitcoin::Network::Regtest); - builder_new.set_storage_dir_path(storage_path); - builder_new.set_chain_source_esplora(esplora_url, None); - - #[cfg(feature = "uniffi")] - let node_entropy = NodeEntropy::from_seed_bytes(seed_bytes.to_vec()).unwrap(); - #[cfg(not(feature = "uniffi"))] - let node_entropy = NodeEntropy::from_seed_bytes(seed_bytes); - let node_new = builder_new.build(node_entropy.into()).unwrap(); - - node_new.start().unwrap(); - node_new.sync_wallets().unwrap(); - - let new_balance = node_new.list_balances().spendable_onchain_balance_sats; - let new_node_id = node_new.node_id(); - - assert_eq!(old_node_id, new_node_id); - assert_eq!(old_balance, new_balance); - - node_new.stop().unwrap(); + let (bitcoind, electrsd) = common::setup_bitcoind_and_electrsd(); + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + + let storage_path = common::random_storage_path().to_str().unwrap().to_owned(); + let seed_bytes = [42u8; 64]; + + // Setup a v0.6.2 `Node` + let (old_balance, old_node_id) = { + let mut builder_old = ldk_node_062::Builder::new(); + builder_old.set_network(bitcoin::Network::Regtest); + builder_old.set_storage_dir_path(storage_path.clone()); + builder_old.set_entropy_seed_bytes(seed_bytes); + builder_old.set_chain_source_esplora(esplora_url.clone(), None); + let node_old = builder_old.build().unwrap(); + + node_old.start().unwrap(); + let addr_old = node_old.onchain_payment().new_address().unwrap(); + common::premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_old], + bitcoin::Amount::from_sat(100_000), + ) + .await; + node_old.sync_wallets().unwrap(); + + let balance = node_old.list_balances().spendable_onchain_balance_sats; + assert!(balance > 0); + let node_id = node_old.node_id(); + + node_old.stop().unwrap(); + + (balance, node_id) + }; + + // Now ensure we can still reinit from the same backend. + #[cfg(feature = "uniffi")] + let builder_new = Builder::new(); + #[cfg(not(feature = "uniffi"))] + let mut builder_new = Builder::new(); + builder_new.set_network(bitcoin::Network::Regtest); + builder_new.set_storage_dir_path(storage_path); + builder_new.set_chain_source_esplora(esplora_url, None); + + #[cfg(feature = "uniffi")] + let node_entropy = NodeEntropy::from_seed_bytes(seed_bytes.to_vec()).unwrap(); + #[cfg(not(feature = "uniffi"))] + let node_entropy = NodeEntropy::from_seed_bytes(seed_bytes); + let node_new = builder_new.build(node_entropy.into()).unwrap(); + + node_new.start().unwrap(); + node_new.sync_wallets().unwrap(); + + let new_balance = node_new.list_balances().spendable_onchain_balance_sats; + let new_node_id = node_new.node_id(); + + assert_eq!(old_node_id, new_node_id); + assert_eq!(old_balance, new_balance); + + node_new.stop().unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn onchain_fee_bump_rbf() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); - - // Fund both nodes - let addr_a = node_a.onchain_payment().new_address().unwrap(); - let addr_b = node_b.onchain_payment().new_address().unwrap(); - - let premine_amount_sat = 500_000; - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![addr_a.clone(), addr_b.clone()], - Amount::from_sat(premine_amount_sat), - ) - .await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - // Send a transaction from node_b to node_a that we'll later bump - let amount_to_send_sats = 100_000; - let txid = - node_b.onchain_payment().send_to_address(&addr_a, amount_to_send_sats, None).unwrap(); - wait_for_tx(&electrsd.client, txid).await; - // Give the chain source time to index the unconfirmed transaction before syncing. - // Without this, Esplora may not yet have the tx, causing sync to miss it and - // leaving the BDK wallet graph empty. - tokio::time::sleep(std::time::Duration::from_secs(5)).await; - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - let payment_id = PaymentId(txid.to_byte_array()); - let original_payment = node_b.payment(&payment_id).unwrap(); - let original_fee = original_payment.fee_paid_msat.unwrap(); - - // Non-existent payment id - let fake_txid = - Txid::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(); - let invalid_payment_id = PaymentId(fake_txid.to_byte_array()); - assert_eq!( - Err(NodeError::InvalidPaymentId), - node_b.onchain_payment().bump_fee_rbf(invalid_payment_id, None) - ); - - // Bump an inbound payment - assert_eq!( - Err(NodeError::InvalidPaymentId), - node_a.onchain_payment().bump_fee_rbf(payment_id, None) - ); - - // Successful fee bump - let new_txid = node_b.onchain_payment().bump_fee_rbf(payment_id, None).unwrap(); - wait_for_tx(&electrsd.client, new_txid).await; - // Give the chain source time to index the unconfirmed transaction before syncing. - // Without this, Esplora may not yet have the tx, causing sync to miss it and - // leaving the BDK wallet graph empty. - tokio::time::sleep(std::time::Duration::from_secs(5)).await; - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - // Verify fee increased and txid updated for node_b - let new_payment = node_b.payment(&payment_id).unwrap(); - assert!( - new_payment.fee_paid_msat > Some(original_fee), - "Fee should increase after RBF bump. Original: {}, New: {}", - original_fee, - new_payment.fee_paid_msat.unwrap() - ); - match &new_payment.kind { - PaymentKind::Onchain { txid, .. } => { - assert_eq!( - *txid, new_txid, - "node_b payment txid should be updated to the replacement txid" - ); - }, - _ => panic!("Unexpected payment kind"), - } - - // Multiple consecutive bumps - let second_bump_txid = node_b.onchain_payment().bump_fee_rbf(payment_id, None).unwrap(); - wait_for_tx(&electrsd.client, second_bump_txid).await; - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - // Verify second bump payment exists and txid updated for node_b - let second_payment = node_b.payment(&payment_id).unwrap(); - assert!( - second_payment.fee_paid_msat > new_payment.fee_paid_msat, - "Second bump should have higher fee than first bump" - ); - match &second_payment.kind { - PaymentKind::Onchain { txid, .. } => { - assert_eq!( - *txid, second_bump_txid, - "node_b payment txid should be updated to the second replacement txid" - ); - }, - _ => panic!("Unexpected payment kind"), - } - - // Confirm the transaction and try to bump again (should fail) - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - assert_eq!( - Err(NodeError::InvalidPaymentId), - node_b.onchain_payment().bump_fee_rbf(payment_id, None) - ); - - // Verify final payment is confirmed - let final_payment = node_b.payment(&payment_id).unwrap(); - assert_eq!(final_payment.status, PaymentStatus::Succeeded); - match final_payment.kind { - PaymentKind::Onchain { status, .. } => { - assert!(matches!(status, ConfirmationStatus::Confirmed { .. })); - }, - _ => panic!("Unexpected payment kind"), - } - - // Verify node A received the funds correctly - let node_a_received_payment = node_a.list_payments_with_filter(|p| { - p.id == payment_id && matches!(p.kind, PaymentKind::Onchain { .. }) - }); - - assert_eq!(node_a_received_payment.len(), 1); - match &node_a_received_payment[0].kind { - PaymentKind::Onchain { txid: inbound_txid, .. } => { - assert_eq!( - *inbound_txid, second_bump_txid, - "node_a inbound payment txid should be updated to the second replacement txid" - ); - }, - _ => panic!("Unexpected payment kind"), - } - assert_eq!(node_a_received_payment[0].amount_msat, Some(amount_to_send_sats * 1000)); - assert_eq!(node_a_received_payment[0].status, PaymentStatus::Succeeded); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + + // Fund both nodes + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + + let premine_amount_sat = 500_000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_a.clone(), addr_b.clone()], + Amount::from_sat(premine_amount_sat), + ) + .await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + // Send a transaction from node_b to node_a that we'll later bump + let amount_to_send_sats = 100_000; + let txid = + node_b.onchain_payment().send_to_address(&addr_a, amount_to_send_sats, None).unwrap(); + wait_for_tx(&electrsd.client, txid).await; + // Give the chain source time to index the unconfirmed transaction before syncing. + // Without this, Esplora may not yet have the tx, causing sync to miss it and + // leaving the BDK wallet graph empty. + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + let payment_id = PaymentId(txid.to_byte_array()); + let original_payment = node_b.payment(&payment_id).unwrap(); + let original_fee = original_payment.fee_paid_msat.unwrap(); + + // Non-existent payment id + let fake_txid = + Txid::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(); + let invalid_payment_id = PaymentId(fake_txid.to_byte_array()); + assert_eq!( + Err(NodeError::InvalidPaymentId), + node_b.onchain_payment().bump_fee_rbf(invalid_payment_id, None) + ); + + // Bump an inbound payment + assert_eq!( + Err(NodeError::InvalidPaymentId), + node_a.onchain_payment().bump_fee_rbf(payment_id, None) + ); + + // Successful fee bump + let new_txid = node_b.onchain_payment().bump_fee_rbf(payment_id, None).unwrap(); + wait_for_tx(&electrsd.client, new_txid).await; + // Give the chain source time to index the unconfirmed transaction before syncing. + // Without this, Esplora may not yet have the tx, causing sync to miss it and + // leaving the BDK wallet graph empty. + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + // Verify fee increased and txid updated for node_b + let new_payment = node_b.payment(&payment_id).unwrap(); + assert!( + new_payment.fee_paid_msat > Some(original_fee), + "Fee should increase after RBF bump. Original: {}, New: {}", + original_fee, + new_payment.fee_paid_msat.unwrap() + ); + match &new_payment.kind { + PaymentKind::Onchain { txid, .. } => { + assert_eq!( + *txid, new_txid, + "node_b payment txid should be updated to the replacement txid" + ); + }, + _ => panic!("Unexpected payment kind"), + } + + // Multiple consecutive bumps + let second_bump_txid = node_b.onchain_payment().bump_fee_rbf(payment_id, None).unwrap(); + wait_for_tx(&electrsd.client, second_bump_txid).await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + // Verify second bump payment exists and txid updated for node_b + let second_payment = node_b.payment(&payment_id).unwrap(); + assert!( + second_payment.fee_paid_msat > new_payment.fee_paid_msat, + "Second bump should have higher fee than first bump" + ); + match &second_payment.kind { + PaymentKind::Onchain { txid, .. } => { + assert_eq!( + *txid, second_bump_txid, + "node_b payment txid should be updated to the second replacement txid" + ); + }, + _ => panic!("Unexpected payment kind"), + } + + // Confirm the transaction and try to bump again (should fail) + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; + + assert_eq!( + Err(NodeError::InvalidPaymentId), + node_b.onchain_payment().bump_fee_rbf(payment_id, None) + ); + + // Verify final payment is confirmed + let final_payment = node_b.payment(&payment_id).unwrap(); + assert_eq!(final_payment.status, PaymentStatus::Succeeded); + match final_payment.kind { + PaymentKind::Onchain { status, .. } => { + assert!(matches!(status, ConfirmationStatus::Confirmed { .. })); + }, + _ => panic!("Unexpected payment kind"), + } + + // Verify node A received the funds correctly + let node_a_received_payment = node_a.list_payments_with_filter(|p| { + p.id == payment_id && matches!(p.kind, PaymentKind::Onchain { .. }) + }); + + assert_eq!(node_a_received_payment.len(), 1); + match &node_a_received_payment[0].kind { + PaymentKind::Onchain { txid: inbound_txid, .. } => { + assert_eq!( + *inbound_txid, second_bump_txid, + "node_a inbound payment txid should be updated to the second replacement txid" + ); + }, + _ => panic!("Unexpected payment kind"), + } + assert_eq!(node_a_received_payment[0].amount_msat, Some(amount_to_send_sats * 1000)); + assert_eq!(node_a_received_payment[0].status, PaymentStatus::Succeeded); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn open_channel_with_all_with_anchors() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); - - let addr_a = node_a.onchain_payment().new_address().unwrap(); - let addr_b = node_b.onchain_payment().new_address().unwrap(); - - let premine_amount_sat = 1_000_000; - - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![addr_a, addr_b], - Amount::from_sat(premine_amount_sat), - ) - .await; - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); - - let funding_txo = open_channel_with_all(&node_a, &node_b, false, &electrsd).await; - - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - let _user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id()); - let _user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id()); - - // After opening a channel with all balance, the remaining on-chain balance should only - // be the anchor reserve (25k sats by default) plus a small margin for change - let anchor_reserve_sat = 25_000; - let remaining_balance = node_a.list_balances().spendable_onchain_balance_sats; - assert!( + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + + let premine_amount_sat = 1_000_000; + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_a, addr_b], + Amount::from_sat(premine_amount_sat), + ) + .await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + + let funding_txo = open_channel_with_all(&node_a, &node_b, false, &electrsd).await; + wait_for_tx(&electrsd.client, funding_txo.txid).await; + + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; + + let _user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id()); + let _user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id()); + + // After opening a channel with all balance, the remaining on-chain balance should only + // be the anchor reserve (25k sats by default) plus a small margin for change + let anchor_reserve_sat = 25_000; + let remaining_balance = node_a.list_balances().spendable_onchain_balance_sats; + assert!( remaining_balance < anchor_reserve_sat + 500, "Remaining balance {remaining_balance} should be close to the anchor reserve {anchor_reserve_sat}" ); - // Verify a channel was opened with most of the funds - let channels = node_a.list_channels(); - assert_eq!(channels.len(), 1); - let channel = &channels[0]; - assert!(channel.channel_value_sats > premine_amount_sat - anchor_reserve_sat - 500); - assert_eq!(channel.counterparty_node_id, node_b.node_id()); - assert_eq!(channel.funding_txo.unwrap(), funding_txo); + // Verify a channel was opened with most of the funds + let channels = node_a.list_channels(); + assert_eq!(channels.len(), 1); + let channel = &channels[0]; + assert!(channel.channel_value_sats > premine_amount_sat - anchor_reserve_sat - 500); + assert_eq!(channel.counterparty_node_id, node_b.node_id()); + assert_eq!(channel.funding_txo.unwrap(), funding_txo); - node_a.stop().unwrap(); - node_b.stop().unwrap(); + node_a.stop().unwrap(); + node_b.stop().unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn open_channel_with_all_without_anchors() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); - - let addr_a = node_a.onchain_payment().new_address().unwrap(); - let addr_b = node_b.onchain_payment().new_address().unwrap(); - - let premine_amount_sat = 1_000_000; - - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![addr_a, addr_b], - Amount::from_sat(premine_amount_sat), - ) - .await; - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); - - let funding_txo = open_channel_with_all(&node_a, &node_b, false, &electrsd).await; - - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - let _user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id()); - let _user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id()); - - // Without anchors, there should be no remaining balance - let remaining_balance = node_a.list_balances().spendable_onchain_balance_sats; - assert_eq!( - remaining_balance, 0, - "Remaining balance {remaining_balance} should be zero without anchor reserve" - ); - - // Verify a channel was opened with all the funds accounting for fees - let channels = node_a.list_channels(); - assert_eq!(channels.len(), 1); - let channel = &channels[0]; - assert!(channel.channel_value_sats > premine_amount_sat - 500); - assert_eq!(channel.counterparty_node_id, node_b.node_id()); - assert_eq!(channel.funding_txo.unwrap(), funding_txo); - - node_a.stop().unwrap(); - node_b.stop().unwrap(); + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); + + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + + let premine_amount_sat = 1_000_000; + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_a, addr_b], + Amount::from_sat(premine_amount_sat), + ) + .await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + + let funding_txo = open_channel_with_all(&node_a, &node_b, false, &electrsd).await; + wait_for_tx(&electrsd.client, funding_txo.txid).await; + + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; + + let _user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id()); + let _user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id()); + + // Without anchors, there should be no remaining balance + let remaining_balance = node_a.list_balances().spendable_onchain_balance_sats; + assert_eq!( + remaining_balance, 0, + "Remaining balance {remaining_balance} should be zero without anchor reserve" + ); + + // Verify a channel was opened with all the funds accounting for fees + let channels = node_a.list_channels(); + assert_eq!(channels.len(), 1); + let channel = &channels[0]; + assert!(channel.channel_value_sats > premine_amount_sat - 500); + assert_eq!(channel.counterparty_node_id, node_b.node_id()); + assert_eq!(channel.funding_txo.unwrap(), funding_txo); + + node_a.stop().unwrap(); + node_b.stop().unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn splice_in_with_all_balance() { - let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = random_chain_source(&bitcoind, &electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); - - let addr_a = node_a.onchain_payment().new_address().unwrap(); - let addr_b = node_b.onchain_payment().new_address().unwrap(); - - let premine_amount_sat = 5_000_000; - let channel_amount_sat = 1_000_000; - - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![addr_a, addr_b], - Amount::from_sat(premine_amount_sat), - ) - .await; - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); - - // Open a channel with a fixed amount first - let funding_txo = open_channel(&node_a, &node_b, channel_amount_sat, false, &electrsd).await; - - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - let user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id()); - let _user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id()); - - let channels = node_a.list_channels(); - assert_eq!(channels.len(), 1); - assert_eq!(channels[0].channel_value_sats, channel_amount_sat); - assert_eq!(channels[0].funding_txo.unwrap(), funding_txo); - - let balance_before_splice = node_a.list_balances().spendable_onchain_balance_sats; - assert!(balance_before_splice > 0); - - // Splice in with all remaining on-chain funds - splice_in_with_all(&node_a, &node_b, &user_channel_id_a, &electrsd).await; - - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - let _user_channel_id_a2 = expect_channel_ready_event!(node_a, node_b.node_id()); - let _user_channel_id_b2 = expect_channel_ready_event!(node_b, node_a.node_id()); - - // After splicing with all balance, channel value should be close to the premined amount - // minus fees and anchor reserve - let anchor_reserve_sat = 25_000; - let channels = node_a.list_channels(); - assert_eq!(channels.len(), 1); - let channel = &channels[0]; - assert!( - channel.channel_value_sats > premine_amount_sat - anchor_reserve_sat - 1000, - "Channel value {} should be close to premined amount {} minus anchor reserve {} and fees", - channel.channel_value_sats, - premine_amount_sat, - anchor_reserve_sat, - ); - - // Remaining on-chain balance should be close to just the anchor reserve - let remaining_balance = node_a.list_balances().spendable_onchain_balance_sats; - assert!( + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + + let premine_amount_sat = 5_000_000; + let channel_amount_sat = 1_000_000; + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_a, addr_b], + Amount::from_sat(premine_amount_sat), + ) + .await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + + // Open a channel with a fixed amount first + let funding_txo = open_channel(&node_a, &node_b, channel_amount_sat, false, &electrsd).await; + wait_for_tx(&electrsd.client, funding_txo.txid).await; + + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; + + let user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id()); + let _user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id()); + + let channels = node_a.list_channels(); + assert_eq!(channels.len(), 1); + assert_eq!(channels[0].channel_value_sats, channel_amount_sat); + assert_eq!(channels[0].funding_txo.unwrap(), funding_txo); + + let balance_before_splice = node_a.list_balances().spendable_onchain_balance_sats; + assert!(balance_before_splice > 0); + + // Splice in with all remaining on-chain funds + splice_in_with_all(&node_a, &node_b, &user_channel_id_a, &electrsd).await; + // wait_for_tx(&electrsd.client, outpoint.txid).await; + + let tip = generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + wait_for_node_block(&node_a, tip).await; + wait_for_node_block(&node_b, tip).await; + + let _user_channel_id_a2 = expect_channel_ready_event!(node_a, node_b.node_id()); + let _user_channel_id_b2 = expect_channel_ready_event!(node_b, node_a.node_id()); + + // After splicing with all balance, channel value should be close to the premined amount + // minus fees and anchor reserve + let anchor_reserve_sat = 25_000; + let channels = node_a.list_channels(); + assert_eq!(channels.len(), 1); + let channel = &channels[0]; + assert!( + channel.channel_value_sats > premine_amount_sat - anchor_reserve_sat - 1000, + "Channel value {} should be close to premined amount {} minus anchor reserve {} and fees", + channel.channel_value_sats, + premine_amount_sat, + anchor_reserve_sat, + ); + + // Remaining on-chain balance should be close to just the anchor reserve + let remaining_balance = node_a.list_balances().spendable_onchain_balance_sats; + assert!( remaining_balance < anchor_reserve_sat + 500, "Remaining balance {remaining_balance} should be close to the anchor reserve {anchor_reserve_sat}" ); - node_a.stop().unwrap(); - node_b.stop().unwrap(); + node_a.stop().unwrap(); + node_b.stop().unwrap(); }