From ab0aa16f678ced93b58facb0b3673a1d532b09ce Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 13 Apr 2026 19:02:10 -0500 Subject: [PATCH 1/4] Adapt to lightning-block-sync API changes Update to use the new HeaderCache type instead of implementing the Cache trait, pass BestBlock instead of BlockHash to synchronize_listeners, and pass HeaderCache by value to SpvClient::new. Also adapt to BestBlock gaining a previous_blocks field and ChannelManager deserialization returning BestBlock instead of BlockHash. Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.toml | 28 ++++++++--------- src/builder.rs | 6 ++-- src/chain/bitcoind.rs | 73 +++++++------------------------------------ src/wallet/mod.rs | 10 +++++- 4 files changed, 38 insertions(+), 79 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e8364c909..54bda0b53 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,18 +40,18 @@ default = [] #lightning-macros = { version = "0.2.0" } #lightning-dns-resolver = { version = "0.3.0" } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38a62c32454d3eac22578144c479dbf9a6d9bff6", features = ["std"] } -lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38a62c32454d3eac22578144c479dbf9a6d9bff6" } -lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38a62c32454d3eac22578144c479dbf9a6d9bff6", features = ["std"] } -lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38a62c32454d3eac22578144c479dbf9a6d9bff6" } -lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38a62c32454d3eac22578144c479dbf9a6d9bff6", features = ["tokio"] } -lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38a62c32454d3eac22578144c479dbf9a6d9bff6" } -lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38a62c32454d3eac22578144c479dbf9a6d9bff6" } -lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38a62c32454d3eac22578144c479dbf9a6d9bff6", features = ["rest-client", "rpc-client", "tokio"] } -lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38a62c32454d3eac22578144c479dbf9a6d9bff6", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } -lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38a62c32454d3eac22578144c479dbf9a6d9bff6", features = ["std"] } -lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38a62c32454d3eac22578144c479dbf9a6d9bff6" } -lightning-dns-resolver = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38a62c32454d3eac22578144c479dbf9a6d9bff6" } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133", features = ["std"] } +lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133" } +lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133", features = ["std"] } +lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133" } +lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133", features = ["tokio"] } +lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133" } +lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133" } +lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133", features = ["rest-client", "rpc-client", "tokio"] } +lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } +lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133", features = ["std"] } +lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133" } +lightning-dns-resolver = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133" } bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] } bdk_esplora = { version = "0.22.0", default-features = false, features = ["async-https-rustls", "tokio"]} @@ -81,13 +81,13 @@ async-trait = { version = "0.1", default-features = false } vss-client = { package = "vss-client-ng", version = "0.5" } prost = { version = "0.11.6", default-features = false} #bitcoin-payment-instructions = { version = "0.6" } -bitcoin-payment-instructions = { git = "https://github.com/jkczyz/bitcoin-payment-instructions", rev = "a7b32d5fded9bb45f73bf82e6d7187adf705171c" } +bitcoin-payment-instructions = { git = "https://github.com/jkczyz/bitcoin-payment-instructions", rev = "340c535a600f7c43bef4c9f910edac4085f2e70c" } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "38a62c32454d3eac22578144c479dbf9a6d9bff6", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133", features = ["std", "_test_utils"] } rand = { version = "0.9.2", default-features = false, features = ["std", "thread_rng", "os_rng"] } proptest = "1.0.0" regex = "1.5.6" diff --git a/src/builder.rs b/src/builder.rs index 0b44dc153..91d89ddc0 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -19,7 +19,7 @@ use bdk_wallet::{KeychainKind, Wallet as BdkWallet}; use bitcoin::bip32::{ChildNumber, Xpriv}; use bitcoin::key::Secp256k1; use bitcoin::secp256k1::PublicKey; -use bitcoin::{BlockHash, Network}; +use bitcoin::Network; use bitcoin_payment_instructions::dns_resolver::DNSHrnResolver; use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver; use lightning::chain::{chainmonitor, BestBlock}; @@ -1695,8 +1695,8 @@ fn build_with_store_internal( user_config, channel_monitor_references, ); - let (_hash, channel_manager) = - <(BlockHash, ChannelManager)>::read(&mut &*reader, read_args).map_err(|e| { + let (_best_block, channel_manager) = + <(BestBlock, ChannelManager)>::read(&mut &*reader, read_args).map_err(|e| { log_error!(logger, "Failed to read channel manager from store: {}", e); BuildError::ReadFailed })?; diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 2bf059f4e..41ae6d649 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -5,7 +5,7 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use std::collections::{HashMap, VecDeque}; +use std::collections::HashMap; use std::fmt; use std::future::Future; use std::sync::atomic::{AtomicU64, Ordering}; @@ -25,7 +25,7 @@ use lightning_block_sync::poll::{ChainPoller, ChainTip, ValidatedBlockHeader}; use lightning_block_sync::rest::RestClient; use lightning_block_sync::rpc::{RpcClient, RpcClientError}; use lightning_block_sync::{ - BlockData, BlockHeaderData, BlockSource, BlockSourceError, BlockSourceErrorKind, Cache, + BlockData, BlockHeaderData, BlockSource, BlockSourceError, BlockSourceErrorKind, HeaderCache, SpvClient, }; use serde::Serialize; @@ -49,7 +49,6 @@ const CHAIN_POLLING_TIMEOUT_SECS: u64 = 10; pub(super) struct BitcoindChainSource { api_client: Arc, - header_cache: tokio::sync::Mutex, latest_chain_tip: RwLock>, wallet_polling_status: Mutex, fee_estimator: Arc, @@ -72,12 +71,10 @@ impl BitcoindChainSource { rpc_password.clone(), )); - let header_cache = tokio::sync::Mutex::new(BoundedHeaderCache::new()); let latest_chain_tip = RwLock::new(None); let wallet_polling_status = Mutex::new(WalletSyncStatus::Completed); Self { api_client, - header_cache, latest_chain_tip, wallet_polling_status, fee_estimator, @@ -103,13 +100,11 @@ impl BitcoindChainSource { rpc_password, )); - let header_cache = tokio::sync::Mutex::new(BoundedHeaderCache::new()); let latest_chain_tip = RwLock::new(None); let wallet_polling_status = Mutex::new(WalletSyncStatus::Completed); Self { api_client, - header_cache, latest_chain_tip, wallet_polling_status, fee_estimator, @@ -153,14 +148,14 @@ impl BitcoindChainSource { return; } - let channel_manager_best_block_hash = channel_manager.current_best_block().block_hash; - let sweeper_best_block_hash = output_sweeper.current_best_block().block_hash; - let onchain_wallet_best_block_hash = onchain_wallet.current_best_block().block_hash; + let onchain_wallet_best_block = onchain_wallet.current_best_block(); + let channel_manager_best_block = channel_manager.current_best_block(); + let sweeper_best_block = output_sweeper.current_best_block(); let mut chain_listeners = vec![ - (onchain_wallet_best_block_hash, &*onchain_wallet as &(dyn Listen + Send + Sync)), - (channel_manager_best_block_hash, &*channel_manager as &(dyn Listen + Send + Sync)), - (sweeper_best_block_hash, &*output_sweeper as &(dyn Listen + Send + Sync)), + (onchain_wallet_best_block, &*onchain_wallet as &(dyn Listen + Send + Sync)), + (channel_manager_best_block, &*channel_manager as &(dyn Listen + Send + Sync)), + (sweeper_best_block, &*output_sweeper as &(dyn Listen + Send + Sync)), ]; // TODO: Eventually we might want to see if we can synchronize `ChannelMonitor`s @@ -168,31 +163,28 @@ impl BitcoindChainSource { // trivial as we load them on initialization (in the `Builder`) and only gain // network access during `start`. For now, we just make sure we get the worst known // block hash and sychronize them via `ChainMonitor`. - if let Some(worst_channel_monitor_block_hash) = chain_monitor + if let Some(worst_channel_monitor_best_block) = chain_monitor .list_monitors() .iter() .flat_map(|channel_id| chain_monitor.get_monitor(*channel_id)) .map(|m| m.current_best_block()) .min_by_key(|b| b.height) - .map(|b| b.block_hash) { chain_listeners.push(( - worst_channel_monitor_block_hash, + worst_channel_monitor_best_block, &*chain_monitor as &(dyn Listen + Send + Sync), )); } - let mut locked_header_cache = self.header_cache.lock().await; let now = SystemTime::now(); match synchronize_listeners( self.api_client.as_ref(), self.config.network, - &mut *locked_header_cache, chain_listeners.clone(), ) .await { - Ok(chain_tip) => { + Ok((_header_cache, chain_tip)) => { { let elapsed_ms = now.elapsed().map(|d| d.as_millis()).unwrap_or(0); log_info!( @@ -400,7 +392,6 @@ impl BitcoindChainSource { let chain_tip = if let Some(tip) = latest_chain_tip_opt { tip } else { self.poll_chain_tip().await? }; - let mut locked_header_cache = self.header_cache.lock().await; let chain_poller = ChainPoller::new(Arc::clone(&self.api_client), self.config.network); let chain_listener = ChainListener { onchain_wallet: Arc::clone(&onchain_wallet), @@ -409,7 +400,7 @@ impl BitcoindChainSource { output_sweeper, }; let mut spv_client = - SpvClient::new(chain_tip, chain_poller, &mut *locked_header_cache, &chain_listener); + SpvClient::new(chain_tip, chain_poller, HeaderCache::new(), &chain_listener); let now = SystemTime::now(); match spv_client.poll_best_tip().await { @@ -1350,46 +1341,6 @@ pub(crate) enum FeeRateEstimationMode { Conservative, } -const MAX_HEADER_CACHE_ENTRIES: usize = 100; - -pub(crate) struct BoundedHeaderCache { - header_map: HashMap, - recently_seen: VecDeque, -} - -impl BoundedHeaderCache { - pub(crate) fn new() -> Self { - let header_map = HashMap::new(); - let recently_seen = VecDeque::new(); - Self { header_map, recently_seen } - } -} - -impl Cache for BoundedHeaderCache { - fn look_up(&self, block_hash: &BlockHash) -> Option<&ValidatedBlockHeader> { - self.header_map.get(block_hash) - } - - fn block_connected(&mut self, block_hash: BlockHash, block_header: ValidatedBlockHeader) { - self.recently_seen.push_back(block_hash); - self.header_map.insert(block_hash, block_header); - - if self.header_map.len() >= MAX_HEADER_CACHE_ENTRIES { - // Keep dropping old entries until we've actually removed a header entry. - while let Some(oldest_entry) = self.recently_seen.pop_front() { - if self.header_map.remove(&oldest_entry).is_some() { - break; - } - } - } - } - - fn block_disconnected(&mut self, block_hash: &BlockHash) -> Option { - self.recently_seen.retain(|e| e != block_hash); - self.header_map.remove(block_hash) - } -} - pub(crate) struct ChainListener { pub(crate) onchain_wallet: Arc, pub(crate) channel_manager: Arc, diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index cb982e303..98ad510e0 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -138,7 +138,15 @@ impl Wallet { pub(crate) fn current_best_block(&self) -> BestBlock { let checkpoint = self.inner.lock().expect("lock").latest_checkpoint(); - BestBlock { block_hash: checkpoint.hash(), height: checkpoint.height() } + let mut current_block = Some(checkpoint.clone()); + let previous_blocks = std::array::from_fn(|_| { + let child = current_block.take()?; + // BDK's checkpoint chain may be sparse; only accept contiguous parents. + let parent = child.prev().filter(|cp| cp.height() + 1 == child.height())?; + current_block = Some(parent.clone()); + Some(parent.hash()) + }); + BestBlock { block_hash: checkpoint.hash(), height: checkpoint.height(), previous_blocks } } pub(crate) fn apply_update(&self, update: impl Into) -> Result<(), Error> { From 2ab93c1d058764430974211912f6f667faf2348c Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Thu, 23 Apr 2026 17:30:47 -0500 Subject: [PATCH 2/4] Expose a bindings-compatible BestBlock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UniFFI cannot represent the fixed-size array that upstream's BestBlock carries via `previous_blocks`, so NodeStatus.current_best_block was unusable from Swift, Kotlin, and Python once upstream added that field. Introduce a small ldk-node BestBlock with just hash and height — the pieces bindings can handle — and expose it in place of the upstream type on the public API. Generated with assistance from Claude Code. Co-Authored-By: Claude Opus 4.7 (1M context) --- bindings/ldk_node.udl | 6 ------ src/builder.rs | 6 +++--- src/chain/bitcoind.rs | 4 ++-- src/chain/mod.rs | 10 +++++----- src/lib.rs | 19 ++++++++++++++++++- src/wallet/mod.rs | 8 ++++---- 6 files changed, 32 insertions(+), 21 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 7368b0291..f87c7b294 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -233,12 +233,6 @@ enum NodeError { typedef dictionary NodeStatus; -[Remote] -dictionary BestBlock { - BlockHash block_hash; - u32 height; -}; - typedef enum BuildError; [Trait, WithForeign] diff --git a/src/builder.rs b/src/builder.rs index 91d89ddc0..e153dcb17 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -22,7 +22,7 @@ use bitcoin::secp256k1::PublicKey; use bitcoin::Network; use bitcoin_payment_instructions::dns_resolver::DNSHrnResolver; use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver; -use lightning::chain::{chainmonitor, BestBlock}; +use lightning::chain::{chainmonitor, BestBlock as BlockLocator}; use lightning::ln::channelmanager::{self, ChainParameters, ChannelManagerReadArgs}; use lightning::ln::msgs::{RoutingMessageHandler, SocketAddress}; use lightning::ln::peer_handler::{IgnoringMessageHandler, MessageHandler}; @@ -1696,7 +1696,7 @@ fn build_with_store_internal( channel_monitor_references, ); let (_best_block, channel_manager) = - <(BestBlock, ChannelManager)>::read(&mut &*reader, read_args).map_err(|e| { + <(BlockLocator, ChannelManager)>::read(&mut &*reader, read_args).map_err(|e| { log_error!(logger, "Failed to read channel manager from store: {}", e); BuildError::ReadFailed })?; @@ -1704,7 +1704,7 @@ fn build_with_store_internal( } else { // We're starting a fresh node. let best_block = - chain_tip_opt.unwrap_or_else(|| BestBlock::from_network(config.network)); + chain_tip_opt.unwrap_or_else(|| BlockLocator::from_network(config.network)); let chain_params = ChainParameters { network: config.network.into(), best_block }; channelmanager::ChannelManager::new( diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 41ae6d649..7ece757ae 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -16,7 +16,7 @@ use base64::prelude::BASE64_STANDARD; use base64::Engine; use bitcoin::{BlockHash, FeeRate, Network, OutPoint, Transaction, Txid}; use lightning::chain::chaininterface::ConfirmationTarget as LdkConfirmationTarget; -use lightning::chain::{BestBlock, Listen}; +use lightning::chain::{BestBlock as BlockLocator, Listen}; use lightning::util::ser::Writeable; use lightning_block_sync::gossip::UtxoSource; use lightning_block_sync::http::{HttpClientError, JsonResponse}; @@ -325,7 +325,7 @@ impl BitcoindChainSource { } } - pub(super) async fn poll_best_block(&self) -> Result { + pub(super) async fn poll_best_block(&self) -> Result { self.poll_chain_tip().await.map(|tip| tip.to_best_block()) } diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 537ee04d3..b70620b99 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -14,7 +14,7 @@ use std::sync::{Arc, Mutex, RwLock}; use std::time::Duration; use bitcoin::{Script, Txid}; -use lightning::chain::{BestBlock, Filter}; +use lightning::chain::{BestBlock as BlockLocator, Filter}; use crate::chain::bitcoind::{BitcoindChainSource, UtxoSourceClient}; use crate::chain::electrum::ElectrumChainSource; @@ -101,7 +101,7 @@ impl ChainSource { fee_estimator: Arc, tx_broadcaster: Arc, kv_store: Arc, config: Arc, logger: Arc, node_metrics: Arc>, - ) -> Result<(Self, Option), ()> { + ) -> Result<(Self, Option), ()> { let esplora_chain_source = EsploraChainSource::new( server_url, headers, @@ -122,7 +122,7 @@ impl ChainSource { fee_estimator: Arc, tx_broadcaster: Arc, kv_store: Arc, config: Arc, logger: Arc, node_metrics: Arc>, - ) -> (Self, Option) { + ) -> (Self, Option) { let electrum_chain_source = ElectrumChainSource::new( server_url, sync_config, @@ -142,7 +142,7 @@ impl ChainSource { fee_estimator: Arc, tx_broadcaster: Arc, kv_store: Arc, config: Arc, logger: Arc, node_metrics: Arc>, - ) -> (Self, Option) { + ) -> (Self, Option) { let bitcoind_chain_source = BitcoindChainSource::new_rpc( rpc_host, rpc_port, @@ -165,7 +165,7 @@ impl ChainSource { fee_estimator: Arc, tx_broadcaster: Arc, kv_store: Arc, config: Arc, rest_client_config: BitcoindRestClientConfig, logger: Arc, node_metrics: Arc>, - ) -> (Self, Option) { + ) -> (Self, Option) { let bitcoind_chain_source = BitcoindChainSource::new_rest( rpc_host, rpc_port, diff --git a/src/lib.rs b/src/lib.rs index 6902228a6..ceb9ec68c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -117,6 +117,7 @@ pub use balance::{BalanceDetails, LightningBalance, PendingSweepBalance}; pub use bip39; pub use bitcoin; use bitcoin::secp256k1::PublicKey; +use bitcoin::BlockHash; #[cfg(feature = "uniffi")] pub use bitcoin::FeeRate; #[cfg(not(feature = "uniffi"))] @@ -145,7 +146,7 @@ use gossip::GossipSource; use graph::NetworkGraph; use io::utils::update_and_persist_node_metrics; pub use lightning; -use lightning::chain::BestBlock; +use lightning::chain::BestBlock as BlockLocator; use lightning::impl_writeable_tlv_based; use lightning::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT; use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; @@ -2056,6 +2057,22 @@ impl Drop for Node { } } +/// The best known block as identified by its hash and height. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +pub struct BestBlock { + /// The block's hash. + pub block_hash: BlockHash, + /// The height at which the block was confirmed. + pub height: u32, +} + +impl From for BestBlock { + fn from(locator: BlockLocator) -> Self { + Self { block_hash: locator.block_hash, height: locator.height } + } +} + /// Represents the status of the [`Node`]. #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 98ad510e0..daeb7becb 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -35,7 +35,7 @@ use lightning::chain::chaininterface::{ BroadcasterInterface, INCREMENTAL_RELAY_FEE_SAT_PER_1000_WEIGHT, }; use lightning::chain::channelmonitor::ANTI_REORG_DELAY; -use lightning::chain::{BestBlock, ClaimId, Listen}; +use lightning::chain::{BestBlock as BlockLocator, ClaimId, Listen}; use lightning::ln::channelmanager::PaymentId; use lightning::ln::funding::FundingTxInput; use lightning::ln::inbound_payment::ExpandedKey; @@ -136,7 +136,7 @@ impl Wallet { .collect() } - pub(crate) fn current_best_block(&self) -> BestBlock { + pub(crate) fn current_best_block(&self) -> BlockLocator { let checkpoint = self.inner.lock().expect("lock").latest_checkpoint(); let mut current_block = Some(checkpoint.clone()); let previous_blocks = std::array::from_fn(|_| { @@ -146,7 +146,7 @@ impl Wallet { current_block = Some(parent.clone()); Some(parent.hash()) }); - BestBlock { block_hash: checkpoint.hash(), height: checkpoint.height(), previous_blocks } + BlockLocator { block_hash: checkpoint.hash(), height: checkpoint.height(), previous_blocks } } pub(crate) fn apply_update(&self, update: impl Into) -> Result<(), Error> { @@ -1499,7 +1499,7 @@ impl Listen for Wallet { }; } - fn blocks_disconnected(&self, _fork_point_block: BestBlock) { + fn blocks_disconnected(&self, _fork_point_block: BlockLocator) { // This is a no-op as we don't have to tell BDK about disconnections. According to the BDK // team, it's sufficient in case of a reorg to always connect blocks starting from the last // point of disagreement. From 4fda0936306387bad7359b7e17ee5e9bd0a3e37e Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Fri, 17 Apr 2026 12:48:25 -0500 Subject: [PATCH 3/4] Drop max_inbound_htlc_value_in_flight_percent_of_channel The max_inbound_htlc_value_in_flight_percent_of_channel config setting was used when acting as an LSPS2 service in order to forward the initial payment. However, upstream divided the config setting into two for announced and unannounced channels, the latter defaulting to 100%. --- Cargo.toml | 28 ++++++++++++++-------------- src/builder.rs | 5 ----- src/event.rs | 13 ++----------- src/lib.rs | 8 -------- src/liquidity.rs | 5 +++-- 5 files changed, 19 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 54bda0b53..3cf5afd73 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,18 +40,18 @@ default = [] #lightning-macros = { version = "0.2.0" } #lightning-dns-resolver = { version = "0.3.0" } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133", features = ["std"] } -lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133" } -lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133", features = ["std"] } -lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133" } -lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133", features = ["tokio"] } -lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133" } -lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133" } -lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133", features = ["rest-client", "rpc-client", "tokio"] } -lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } -lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133", features = ["std"] } -lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133" } -lightning-dns-resolver = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133" } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144", features = ["std"] } +lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144" } +lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144", features = ["std"] } +lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144" } +lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144", features = ["tokio"] } +lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144" } +lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144" } +lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144", features = ["rest-client", "rpc-client", "tokio"] } +lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } +lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144", features = ["std"] } +lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144" } +lightning-dns-resolver = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144" } bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] } bdk_esplora = { version = "0.22.0", default-features = false, features = ["async-https-rustls", "tokio"]} @@ -81,13 +81,13 @@ async-trait = { version = "0.1", default-features = false } vss-client = { package = "vss-client-ng", version = "0.5" } prost = { version = "0.11.6", default-features = false} #bitcoin-payment-instructions = { version = "0.6" } -bitcoin-payment-instructions = { git = "https://github.com/jkczyz/bitcoin-payment-instructions", rev = "340c535a600f7c43bef4c9f910edac4085f2e70c" } +bitcoin-payment-instructions = { git = "https://github.com/jkczyz/bitcoin-payment-instructions", rev = "679dac50cc0d81ec4d31da94b93d467e5308f16a" } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "044f3fab42e3085edecd40f0c9b369093edb7133", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144", features = ["std", "_test_utils"] } rand = { version = "0.9.2", default-features = false, features = ["std", "thread_rng", "os_rng"] } proptest = "1.0.0" regex = "1.5.6" diff --git a/src/builder.rs b/src/builder.rs index e153dcb17..05f3cae76 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1657,11 +1657,6 @@ fn build_with_store_internal( // If we act as an LSPS2 service, we allow forwarding to unannounced channels. user_config.accept_forwards_to_priv_channels = true; - - // If we act as an LSPS2 service, set the HTLC-value-in-flight to 100% of the channel value - // to ensure we can forward the initial payment. - user_config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = - 100; } if let Some(role) = async_payments_role { diff --git a/src/event.rs b/src/event.rs index 3161daa2a..1ac33b411 100644 --- a/src/event.rs +++ b/src/event.rs @@ -26,9 +26,7 @@ use lightning::ln::channelmanager::{PaymentId, TrustedChannelFeatures}; use lightning::ln::types::ChannelId; use lightning::routing::gossip::NodeId; use lightning::sign::EntropySource; -use lightning::util::config::{ - ChannelConfigOverrides, ChannelConfigUpdate, ChannelHandshakeConfigUpdate, -}; +use lightning::util::config::{ChannelConfigOverrides, ChannelConfigUpdate}; use lightning::util::errors::APIError; use lightning::util::persist::KVStore; use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer}; @@ -1273,19 +1271,12 @@ where if lsp_node_id == counterparty_node_id { // When we're an LSPS2 client, allow claiming underpaying HTLCs as the LSP will skim off some fee. We'll // check that they don't take too much before claiming. - // - // We also set maximum allowed inbound HTLC value in flight - // to 100%. We should eventually be able to set this on a per-channel basis, but for - // now we just bump the default for all channels. channel_override_config = Some(ChannelConfigOverrides { - handshake_overrides: Some(ChannelHandshakeConfigUpdate { - max_inbound_htlc_value_in_flight_percent_of_channel: Some(100), - ..Default::default() - }), update_overrides: Some(ChannelConfigUpdate { accept_underpaying_htlcs: Some(true), ..Default::default() }), + ..Default::default() }); } } diff --git a/src/lib.rs b/src/lib.rs index ceb9ec68c..25ef75d2c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1195,14 +1195,6 @@ impl Node { let mut user_config = default_user_config(&self.config); user_config.channel_handshake_config.announce_for_forwarding = announce_for_forwarding; user_config.channel_config = (channel_config.unwrap_or_default()).clone().into(); - // We set the max inflight to 100% for private channels. - // FIXME: LDK will default to this behavior soon, too, at which point we should drop this - // manual override. - if !announce_for_forwarding { - user_config - .channel_handshake_config - .max_inbound_htlc_value_in_flight_percent_of_channel = 100; - } let push_msat = push_to_counterparty_msat.unwrap_or(0); let user_channel_id: u128 = u128::from_ne_bytes( diff --git a/src/liquidity.rs b/src/liquidity.rs index 9f02af886..224bceab0 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -781,11 +781,12 @@ where let mut config = self.channel_manager.get_current_config().clone(); - // We set these LSP-specific values during Node building, here we're making sure it's actually set. + // If we act as an LSPS2 service, the HTLC-value-in-flight must be 100% of the + // channel value to ensure we can forward the initial payment. debug_assert_eq!( config .channel_handshake_config - .max_inbound_htlc_value_in_flight_percent_of_channel, + .unannounced_channel_max_inbound_htlc_value_in_flight_percentage, 100 ); debug_assert!(config.accept_forwards_to_priv_channels); From 20bae9a94be52e8f2ec8a75f44c6bb32689167aa Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Tue, 28 Apr 2026 12:08:25 -0500 Subject: [PATCH 4/4] f - Assert unannounced-channel default stays at 100% Per review feedback on PR #878. The forwarding behavior for LSPS2 channels relies on LDK's default of 100% for the unannounced-channel inbound HTLC value-in-flight cap. Catch upstream changes to that default with debug asserts at the three sites that depend on it. Generated with assistance from Claude Code. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/event.rs | 11 +++++++++++ src/lib.rs | 11 +++++++++++ src/liquidity.rs | 4 +++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/event.rs b/src/event.rs index 1ac33b411..9932e2c7f 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1278,6 +1278,17 @@ where }), ..Default::default() }); + + // LSPS2 channels are unannounced; rely on LDK's default of 100% + // inbound HTLC value-in-flight so the LSP can forward the initial + // payment in full. + debug_assert_eq!( + self.channel_manager + .get_current_config() + .channel_handshake_config + .unannounced_channel_max_inbound_htlc_value_in_flight_percentage, + 100 + ); } } let res = if allow_0conf { diff --git a/src/lib.rs b/src/lib.rs index 25ef75d2c..24e063842 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1196,6 +1196,17 @@ impl Node { user_config.channel_handshake_config.announce_for_forwarding = announce_for_forwarding; user_config.channel_config = (channel_config.unwrap_or_default()).clone().into(); + // Unannounced channels rely on LDK's default of 100% inbound HTLC value-in-flight + // to support large initial payments via LSPS2. + if !announce_for_forwarding { + debug_assert_eq!( + user_config + .channel_handshake_config + .unannounced_channel_max_inbound_htlc_value_in_flight_percentage, + 100 + ); + } + let push_msat = push_to_counterparty_msat.unwrap_or(0); let user_channel_id: u128 = u128::from_ne_bytes( self.keys_manager.get_secure_random_bytes()[..16] diff --git a/src/liquidity.rs b/src/liquidity.rs index 224bceab0..30ab2c0df 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -782,13 +782,15 @@ where let mut config = self.channel_manager.get_current_config().clone(); // If we act as an LSPS2 service, the HTLC-value-in-flight must be 100% of the - // channel value to ensure we can forward the initial payment. + // channel value to ensure we can forward the initial payment. That cap only + // applies to unannounced channels, so the channel must also be unannounced. debug_assert_eq!( config .channel_handshake_config .unannounced_channel_max_inbound_htlc_value_in_flight_percentage, 100 ); + debug_assert!(!config.channel_handshake_config.announce_for_forwarding); debug_assert!(config.accept_forwards_to_priv_channels); // We set the forwarding fee to 0 for now as we're getting paid by the channel fee.