From c89422212d0b60b213126e935014359ccf637da6 Mon Sep 17 00:00:00 2001 From: okekefrancis112 Date: Sun, 8 Feb 2026 20:13:51 +0100 Subject: [PATCH 1/3] feat: allow configurable force-close buffer for claimable HTLCs --- lightning/src/chain/channelmonitor.rs | 46 ++++++++++++++++++++++---- lightning/src/ln/channel.rs | 1 + lightning/src/ln/channel_open_tests.rs | 1 + lightning/src/ln/channelmanager.rs | 7 ++++ lightning/src/util/config.rs | 39 ++++++++++++++++++++++ 5 files changed, 87 insertions(+), 7 deletions(-) diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 6205fa895a7..dbeabc2b5fc 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -278,9 +278,9 @@ pub(crate) const MAX_BLOCKS_FOR_CONF: u32 = 18; /// If an HTLC expires within this many blocks, force-close the channel to broadcast the /// HTLC-Success transaction. /// -/// This is two times [`MAX_BLOCKS_FOR_CONF`] as we need to first get the commitment transaction -/// confirmed, then get an HTLC transaction confirmed. -pub(crate) const CLTV_CLAIM_BUFFER: u32 = MAX_BLOCKS_FOR_CONF * 2; +/// This accounts for the time needed to first get the commitment transaction confirmed, then get +/// an HTLC transaction confirmed. +pub const CLTV_CLAIM_BUFFER: u32 = MAX_BLOCKS_FOR_CONF * 2; /// Number of blocks by which point we expect our counterparty to have seen new blocks on the /// network and done a full update_fail_htlc/commitment_signed dance (+ we've updated all our /// copies of ChannelMonitors, including watchtowers). We could enforce the contract by failing @@ -706,6 +706,10 @@ pub(crate) enum ChannelMonitorUpdateStep { ReleasePaymentComplete { htlc: SentHTLCId, }, + /// Used to update the configurable force-close CLTV buffer for claimable HTLCs. + ChannelConfigUpdated { + force_close_claimable_htlc_cltv_buffer: u32, + }, } impl ChannelMonitorUpdateStep { @@ -723,6 +727,7 @@ impl ChannelMonitorUpdateStep { ChannelMonitorUpdateStep::RenegotiatedFunding { .. } => "RenegotiatedFunding", ChannelMonitorUpdateStep::RenegotiatedFundingLocked { .. } => "RenegotiatedFundingLocked", ChannelMonitorUpdateStep::ReleasePaymentComplete { .. } => "ReleasePaymentComplete", + ChannelMonitorUpdateStep::ChannelConfigUpdated { .. } => "ChannelConfigUpdated", } } } @@ -777,6 +782,9 @@ impl_writeable_tlv_based_enum_upgradable!(ChannelMonitorUpdateStep, (12, RenegotiatedFundingLocked) => { (1, funding_txid, required), }, + (14, ChannelConfigUpdated) => { + (1, force_close_claimable_htlc_cltv_buffer, required), + }, ); /// Indicates whether the balance is derived from a cooperative close, a force-close @@ -1234,6 +1242,10 @@ pub(crate) struct ChannelMonitorImpl { on_holder_tx_csv: u16, + /// The configurable number of blocks before an inbound HTLC's CLTV expiry at which we will + /// force-close the channel to claim it on-chain. Defaults to [`CLTV_CLAIM_BUFFER`]. + force_close_claimable_htlc_cltv_buffer: u32, + commitment_secrets: CounterpartyCommitmentSecrets, /// We cannot identify HTLC-Success or HTLC-Timeout transactions by themselves on the chain. /// Nor can we figure out their commitment numbers without the commitment transaction they are @@ -1754,6 +1766,7 @@ pub(crate) fn write_chanmon_internal( (34, channel_monitor.alternative_funding_confirmed, option), (35, channel_monitor.is_manual_broadcast, required), (37, channel_monitor.funding_seen_onchain, required), + (39, channel_monitor.force_close_claimable_htlc_cltv_buffer, (default_value, CLTV_CLAIM_BUFFER)), }); Ok(()) @@ -1859,6 +1872,7 @@ impl ChannelMonitor { initial_holder_commitment_tx: HolderCommitmentTransaction, best_block: BestBlock, counterparty_node_id: PublicKey, channel_id: ChannelId, is_manual_broadcast: bool, + force_close_claimable_htlc_cltv_buffer: u32, ) -> ChannelMonitor { assert!(commitment_transaction_number_obscure_factor <= (1 << 48)); @@ -1927,6 +1941,11 @@ impl ChannelMonitor { on_holder_tx_csv: counterparty_channel_parameters.selected_contest_delay, + force_close_claimable_htlc_cltv_buffer: core::cmp::max( + force_close_claimable_htlc_cltv_buffer, + CLTV_CLAIM_BUFFER, + ), + commitment_secrets: CounterpartyCommitmentSecrets::new(), counterparty_commitment_txn_on_chain: new_hash_map(), counterparty_hash_commitment_number: new_hash_map(), @@ -4280,6 +4299,13 @@ impl ChannelMonitorImpl { log_trace!(logger, "HTLC {htlc:?} permanently and fully resolved"); self.htlcs_resolved_to_user.insert(*htlc); }, + ChannelMonitorUpdateStep::ChannelConfigUpdated { force_close_claimable_htlc_cltv_buffer } => { + log_trace!(logger, "Updating ChannelMonitor force_close_claimable_htlc_cltv_buffer to {}", force_close_claimable_htlc_cltv_buffer); + self.force_close_claimable_htlc_cltv_buffer = core::cmp::max( + *force_close_claimable_htlc_cltv_buffer, + CLTV_CLAIM_BUFFER, + ); + }, } } @@ -4312,6 +4338,8 @@ impl ChannelMonitorImpl { ChannelMonitorUpdateStep::PaymentPreimage { .. } => {}, ChannelMonitorUpdateStep::ChannelForceClosed { .. } => {}, ChannelMonitorUpdateStep::ReleasePaymentComplete { .. } => {}, + ChannelMonitorUpdateStep::ChannelConfigUpdated { .. } => + is_pre_close_update = true, } } @@ -5952,7 +5980,7 @@ impl ChannelMonitorImpl { // on-chain for an expired HTLC. let htlc_outbound = $holder_tx == htlc.offered; if ( htlc_outbound && htlc.cltv_expiry + LATENCY_GRACE_PERIOD_BLOCKS <= height) || - (!htlc_outbound && htlc.cltv_expiry <= height + CLTV_CLAIM_BUFFER && self.payment_preimages.contains_key(&htlc.payment_hash)) { + (!htlc_outbound && htlc.cltv_expiry <= height + self.force_close_claimable_htlc_cltv_buffer && self.payment_preimages.contains_key(&htlc.payment_hash)) { log_info!(logger, "Force-closing channel due to {} HTLC timeout - HTLC with payment hash {} expires at {}", if htlc_outbound { "outbound" } else { "inbound"}, htlc.payment_hash, htlc.cltv_expiry); return Some(htlc.payment_hash); } @@ -6520,6 +6548,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP let mut alternative_funding_confirmed = None; let mut is_manual_broadcast = RequiredWrapper(None); let mut funding_seen_onchain = RequiredWrapper(None); + let mut force_close_claimable_htlc_cltv_buffer = CLTV_CLAIM_BUFFER; read_tlv_fields!(reader, { (1, funding_spend_confirmed, option), (3, htlcs_resolved_on_chain, optional_vec), @@ -6542,6 +6571,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP (34, alternative_funding_confirmed, option), (35, is_manual_broadcast, (default_value, false)), (37, funding_seen_onchain, (default_value, true)), + (39, force_close_claimable_htlc_cltv_buffer, (default_value, CLTV_CLAIM_BUFFER)), }); // Note that `payment_preimages_with_info` was added (and is always written) in LDK 0.1, so // we can use it to determine if this monitor was last written by LDK 0.1 or later. @@ -6681,6 +6711,8 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP on_holder_tx_csv, + force_close_claimable_htlc_cltv_buffer, + commitment_secrets, counterparty_commitment_txn_on_chain, counterparty_hash_commitment_number, @@ -6761,7 +6793,7 @@ mod tests { use crate::chain::chaininterface::LowerBoundedFeeEstimator; use crate::events::ClosureReason; - use super::ChannelMonitorUpdateStep; + use super::{ChannelMonitorUpdateStep, CLTV_CLAIM_BUFFER}; use crate::chain::channelmonitor::{ChannelMonitor, WithChannelMonitor}; use crate::chain::package::{ weight_offered_htlc, weight_received_htlc, weight_revoked_offered_htlc, @@ -6996,7 +7028,7 @@ mod tests { let monitor = ChannelMonitor::new( Secp256k1::new(), keys, Some(shutdown_script.into_inner()), 0, &ScriptBuf::new(), &channel_parameters, true, 0, HolderCommitmentTransaction::dummy(0, funding_outpoint, Vec::new()), - best_block, dummy_key, channel_id, false, + best_block, dummy_key, channel_id, false, CLTV_CLAIM_BUFFER, ); let nondust_htlcs = preimages_slice_to_htlcs!(preimages[0..10]); @@ -7257,7 +7289,7 @@ mod tests { let monitor = ChannelMonitor::new( Secp256k1::new(), keys, Some(shutdown_script.into_inner()), 0, &ScriptBuf::new(), &channel_parameters, true, 0, HolderCommitmentTransaction::dummy(0, funding_outpoint, Vec::new()), - best_block, dummy_key, channel_id, false, + best_block, dummy_key, channel_id, false, CLTV_CLAIM_BUFFER, ); let chan_id = monitor.inner.lock().unwrap().channel_id(); diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index eb227a50855..b13676acd4b 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -3381,6 +3381,7 @@ trait InitialRemoteCommitmentReceiver { &funding.channel_transaction_parameters, funding.is_outbound(), obscure_factor, holder_commitment_tx, best_block, context.counterparty_node_id, context.channel_id(), context.is_manual_broadcast, + context.config.options.force_close_claimable_htlc_cltv_buffer, ); channel_monitor.provide_initial_counterparty_commitment_tx( counterparty_initial_commitment_tx.clone(), diff --git a/lightning/src/ln/channel_open_tests.rs b/lightning/src/ln/channel_open_tests.rs index b7965c4fb66..0469900ab70 100644 --- a/lightning/src/ln/channel_open_tests.rs +++ b/lightning/src/ln/channel_open_tests.rs @@ -1119,6 +1119,7 @@ pub fn test_manually_accept_inbound_channel_request() { cltv_expiry_delta: None, max_dust_htlc_exposure_msat: None, force_close_avoidance_max_fee_satoshis: None, + force_close_claimable_htlc_cltv_buffer: None, accept_underpaying_htlcs: None, }), }; diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 8a84de69cfc..07e205fea37 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -6619,6 +6619,13 @@ impl< err: format!("The chosen CLTV expiry delta is below the minimum of {}", MIN_CLTV_EXPIRY_DELTA), }); } + if config_update.force_close_claimable_htlc_cltv_buffer + .map(|buf| buf < CLTV_CLAIM_BUFFER).unwrap_or(false) + { + return Err(APIError::APIMisuseError { + err: format!("The chosen force-close CLTV buffer is below the minimum of {}", CLTV_CLAIM_BUFFER), + }); + } let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); let per_peer_state = self.per_peer_state.read().unwrap(); diff --git a/lightning/src/util/config.rs b/lightning/src/util/config.rs index feb326cfad6..c1ef51725bc 100644 --- a/lightning/src/util/config.rs +++ b/lightning/src/util/config.rs @@ -10,6 +10,7 @@ //! Various user-configurable channel limits and settings which ChannelManager //! applies for you. +use crate::chain::channelmonitor::CLTV_CLAIM_BUFFER; use crate::ln::channel::MAX_FUNDING_SATOSHIS_NO_WUMBO; use crate::ln::channelmanager::{BREAKDOWN_TIMEOUT, MAX_LOCAL_BREAKDOWN_TIMEOUT}; @@ -593,6 +594,26 @@ pub struct ChannelConfig { /// [`NonAnchorChannelFee`]: crate::chain::chaininterface::ConfirmationTarget::NonAnchorChannelFee /// [`ChannelCloseMinimum`]: crate::chain::chaininterface::ConfirmationTarget::ChannelCloseMinimum pub force_close_avoidance_max_fee_satoshis: u64, + /// The number of blocks before an inbound HTLC's CLTV expiry at which we will force-close the + /// channel in order to claim it on-chain with an HTLC-Success transaction. This applies when + /// we have the preimage for the HTLC (i.e., it is a claimable HTLC that we forwarded). + /// + /// Increasing this value gives you more tolerance for your own downtime at the expense of + /// being less tolerant of counterparty unresponsiveness (more force-closes). When set higher, + /// you force-close earlier, leaving more blocks to get the commitment transaction and + /// HTLC-Success transaction confirmed on-chain. + /// + /// Note that when changing this value, you should also ensure that + /// [`cltv_expiry_delta`] is large enough to accommodate the new buffer. + /// + /// Default value: [`CLTV_CLAIM_BUFFER`] (36 blocks) + /// + /// Minimum value: [`CLTV_CLAIM_BUFFER`] (Any values less than this will be treated as + /// [`CLTV_CLAIM_BUFFER`] instead.) + /// + /// [`cltv_expiry_delta`]: ChannelConfig::cltv_expiry_delta + /// [`CLTV_CLAIM_BUFFER`]: crate::chain::channelmonitor::CLTV_CLAIM_BUFFER + pub force_close_claimable_htlc_cltv_buffer: u32, /// If set, allows this channel's counterparty to skim an additional fee off this node's inbound /// HTLCs. Useful for liquidity providers to offload on-chain channel costs to end users. /// @@ -650,6 +671,11 @@ impl ChannelConfig { { self.force_close_avoidance_max_fee_satoshis = force_close_avoidance_max_fee_satoshis; } + if let Some(force_close_claimable_htlc_cltv_buffer) = + update.force_close_claimable_htlc_cltv_buffer + { + self.force_close_claimable_htlc_cltv_buffer = force_close_claimable_htlc_cltv_buffer; + } if let Some(accept_underpaying_htlcs) = update.accept_underpaying_htlcs { self.accept_underpaying_htlcs = accept_underpaying_htlcs; } @@ -665,6 +691,7 @@ impl Default for ChannelConfig { cltv_expiry_delta: 6 * 12, // 6 blocks/hour * 12 hours max_dust_htlc_exposure: MaxDustHTLCExposure::FeeRateMultiplier(10000), force_close_avoidance_max_fee_satoshis: 1000, + force_close_claimable_htlc_cltv_buffer: CLTV_CLAIM_BUFFER, accept_underpaying_htlcs: false, } } @@ -687,6 +714,7 @@ impl crate::util::ser::Writeable for ChannelConfig { // LegacyChannelConfig. To make sure that serialization is not compatible with this one, we use // the next required type of 10, which if seen by the old serialization will always fail. (10, self.force_close_avoidance_max_fee_satoshis, required), + (11, self.force_close_claimable_htlc_cltv_buffer, (default_value, CLTV_CLAIM_BUFFER)), }); Ok(()) } @@ -701,6 +729,7 @@ impl crate::util::ser::Readable for ChannelConfig { let mut max_dust_htlc_exposure_msat = None; let mut max_dust_htlc_exposure_enum = None; let mut force_close_avoidance_max_fee_satoshis = 1000; + let mut force_close_claimable_htlc_cltv_buffer = CLTV_CLAIM_BUFFER; read_tlv_fields!(reader, { (0, forwarding_fee_proportional_millionths, required), (1, accept_underpaying_htlcs, (default_value, false)), @@ -710,6 +739,7 @@ impl crate::util::ser::Readable for ChannelConfig { // Has always been written, but became optionally read in 0.0.116 (6, max_dust_htlc_exposure_msat, option), (10, force_close_avoidance_max_fee_satoshis, required), + (11, force_close_claimable_htlc_cltv_buffer, (default_value, CLTV_CLAIM_BUFFER)), }); let max_dust_htlc_fixed_limit = max_dust_htlc_exposure_msat.unwrap_or(5_000_000); let max_dust_htlc_exposure_msat = max_dust_htlc_exposure_enum @@ -721,6 +751,7 @@ impl crate::util::ser::Readable for ChannelConfig { cltv_expiry_delta, max_dust_htlc_exposure: max_dust_htlc_exposure_msat, force_close_avoidance_max_fee_satoshis, + force_close_claimable_htlc_cltv_buffer, }) } } @@ -747,6 +778,10 @@ pub struct ChannelConfigUpdate { /// funds. See [`ChannelConfig::force_close_avoidance_max_fee_satoshis`]. pub force_close_avoidance_max_fee_satoshis: Option, + /// The number of blocks before an inbound HTLC's CLTV expiry at which we will force-close to claim it + /// on-chain. See [`ChannelConfig::force_close_claimable_htlc_cltv_buffer`]. + pub force_close_claimable_htlc_cltv_buffer: Option, + /// If set, allows this channel's counterparty to skim an additional fee off this node's inbound HTLCs. See /// [`ChannelConfig::accept_underpaying_htlcs`]. pub accept_underpaying_htlcs: Option, @@ -764,6 +799,9 @@ impl From for ChannelConfigUpdate { force_close_avoidance_max_fee_satoshis: Some( config.force_close_avoidance_max_fee_satoshis, ), + force_close_claimable_htlc_cltv_buffer: Some( + config.force_close_claimable_htlc_cltv_buffer, + ), accept_underpaying_htlcs: Some(config.accept_underpaying_htlcs), } } @@ -846,6 +884,7 @@ impl crate::util::ser::Readable for LegacyChannelConfig { max_dust_htlc_exposure: max_dust_htlc_exposure_msat, cltv_expiry_delta, force_close_avoidance_max_fee_satoshis, + force_close_claimable_htlc_cltv_buffer: CLTV_CLAIM_BUFFER, forwarding_fee_base_msat, accept_underpaying_htlcs: false, }, From 1b32728bad46b64f5bd86eeaad893fab23393d9a Mon Sep 17 00:00:00 2001 From: okekefrancis112 Date: Wed, 11 Feb 2026 22:15:50 +0100 Subject: [PATCH 2/3] fix: refactor to remove persistency and use memory instead --- lightning/src/chain/chainmonitor.rs | 54 ++++++++++- lightning/src/chain/channelmonitor.rs | 129 ++++++++++++++------------ lightning/src/chain/mod.rs | 20 ++++ lightning/src/ln/channel.rs | 3 +- lightning/src/ln/channelmanager.rs | 7 ++ 5 files changed, 150 insertions(+), 63 deletions(-) diff --git a/lightning/src/chain/chainmonitor.rs b/lightning/src/chain/chainmonitor.rs index 17693f8ca7a..b01526e3a0c 100644 --- a/lightning/src/chain/chainmonitor.rs +++ b/lightning/src/chain/chainmonitor.rs @@ -33,7 +33,7 @@ use crate::chain::chaininterface::{BroadcasterInterface, FeeEstimator}; #[cfg(peer_storage)] use crate::chain::channelmonitor::write_chanmon_internal; use crate::chain::channelmonitor::{ - Balance, ChannelMonitor, ChannelMonitorUpdate, MonitorEvent, TransactionOutputs, + self, Balance, ChannelMonitor, ChannelMonitorUpdate, MonitorEvent, TransactionOutputs, WithChannelMonitor, }; use crate::chain::transaction::{OutPoint, TransactionData}; @@ -350,6 +350,12 @@ pub struct ChainMonitor< P::Target: Persist, { monitors: RwLock>>, + + /// Memory-only per-channel configuration for the CLTV buffer used when deciding + /// to force-close channels with claimable inbound HTLCs. This is not persisted + /// and is rebuilt from channel state on restart. + channel_force_close_buffers: RwLock>, + chain_source: Option, broadcaster: T, logger: L, @@ -402,6 +408,7 @@ where let event_notifier = Arc::new(Notifier::new()); Self { monitors: RwLock::new(new_hash_map()), + channel_force_close_buffers: RwLock::new(new_hash_map()), chain_source, broadcaster, logger, @@ -607,6 +614,7 @@ where ) -> Self { Self { monitors: RwLock::new(new_hash_map()), + channel_force_close_buffers: RwLock::new(new_hash_map()), chain_source, broadcaster, logger, @@ -622,6 +630,27 @@ where } } + /// Updates the force-close buffer configuration for a channel. + /// + /// This is a memory-only update and does not trigger persistence. The buffer value + /// determines how many blocks before an inbound HTLC's CLTV expiry the channel will + /// be force-closed to claim it on-chain. + /// + /// Returns an error if the buffer value is below [`CLTV_CLAIM_BUFFER`]. + /// + /// [`CLTV_CLAIM_BUFFER`]: channelmonitor::CLTV_CLAIM_BUFFER + pub fn update_channel_force_close_buffer( + &self, channel_id: ChannelId, force_close_buffer: u32, + ) -> Result<(), ()> { + if force_close_buffer < channelmonitor::CLTV_CLAIM_BUFFER { + return Err(()); + } + + let mut buffers = self.channel_force_close_buffers.write().unwrap(); + buffers.insert(channel_id, force_close_buffer); + Ok(()) + } + /// Gets the balances in the contained [`ChannelMonitor`]s which are claimable on-chain or /// claims which are awaiting confirmation. /// @@ -1128,10 +1157,16 @@ where height ); self.process_chain_data(header, Some(height), &txdata, |monitor, txdata| { + let channel_id = monitor.channel_id(); + let buffers = self.channel_force_close_buffers.read().unwrap(); + let force_close_buffer = + buffers.get(&channel_id).copied().unwrap_or(channelmonitor::CLTV_CLAIM_BUFFER); + monitor.block_connected( header, txdata, height, + force_close_buffer, &self.broadcaster, &self.fee_estimator, &self.logger, @@ -1188,10 +1223,16 @@ where header.block_hash() ); self.process_chain_data(header, None, txdata, |monitor, txdata| { + let channel_id = monitor.channel_id(); + let buffers = self.channel_force_close_buffers.read().unwrap(); + let force_close_buffer = + buffers.get(&channel_id).copied().unwrap_or(channelmonitor::CLTV_CLAIM_BUFFER); + monitor.transactions_confirmed( header, txdata, height, + force_close_buffer, &self.broadcaster, &self.fee_estimator, &self.logger, @@ -1225,9 +1266,15 @@ where // While in practice there shouldn't be any recursive calls when given empty txdata, // it's still possible if a chain::Filter implementation returns a transaction. debug_assert!(txdata.is_empty()); + let channel_id = monitor.channel_id(); + let buffers = self.channel_force_close_buffers.read().unwrap(); + let force_close_buffer = + buffers.get(&channel_id).copied().unwrap_or(channelmonitor::CLTV_CLAIM_BUFFER); + monitor.best_block_updated( header, height, + force_close_buffer, &self.broadcaster, &self.fee_estimator, &self.logger, @@ -1282,6 +1329,7 @@ where hash_map::Entry::Vacant(e) => e, }; log_trace!(logger, "Got new ChannelMonitor"); + let initial_buffer = monitor.get_initial_force_close_buffer(); let update_id = monitor.get_latest_update_id(); let mut pending_monitor_updates = Vec::new(); let persist_res = self.persister.persist_new_channel(monitor.persistence_key(), &monitor); @@ -1306,6 +1354,10 @@ where monitor, pending_monitor_updates: Mutex::new(pending_monitor_updates), }); + + let mut buffers = self.channel_force_close_buffers.write().unwrap(); + buffers.insert(channel_id, initial_buffer); + Ok(persist_res) } diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index dbeabc2b5fc..2c9aaec28f8 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -706,10 +706,6 @@ pub(crate) enum ChannelMonitorUpdateStep { ReleasePaymentComplete { htlc: SentHTLCId, }, - /// Used to update the configurable force-close CLTV buffer for claimable HTLCs. - ChannelConfigUpdated { - force_close_claimable_htlc_cltv_buffer: u32, - }, } impl ChannelMonitorUpdateStep { @@ -727,7 +723,6 @@ impl ChannelMonitorUpdateStep { ChannelMonitorUpdateStep::RenegotiatedFunding { .. } => "RenegotiatedFunding", ChannelMonitorUpdateStep::RenegotiatedFundingLocked { .. } => "RenegotiatedFundingLocked", ChannelMonitorUpdateStep::ReleasePaymentComplete { .. } => "ReleasePaymentComplete", - ChannelMonitorUpdateStep::ChannelConfigUpdated { .. } => "ChannelConfigUpdated", } } } @@ -782,9 +777,6 @@ impl_writeable_tlv_based_enum_upgradable!(ChannelMonitorUpdateStep, (12, RenegotiatedFundingLocked) => { (1, funding_txid, required), }, - (14, ChannelConfigUpdated) => { - (1, force_close_claimable_htlc_cltv_buffer, required), - }, ); /// Indicates whether the balance is derived from a cooperative close, a force-close @@ -1745,28 +1737,27 @@ pub(crate) fn write_chanmon_internal( }; write_tlv_fields!(writer, { - (1, channel_monitor.funding_spend_confirmed, option), - (3, channel_monitor.htlcs_resolved_on_chain, required_vec), - (5, pending_monitor_events, required_vec), - (7, channel_monitor.funding_spend_seen, required), - (9, channel_monitor.counterparty_node_id, required), - (11, channel_monitor.confirmed_commitment_tx_counterparty_output, option), - (13, channel_monitor.spendable_txids_confirmed, required_vec), - (15, channel_monitor.counterparty_fulfilled_htlcs, required), - (17, channel_monitor.initial_counterparty_commitment_info, option), - (19, channel_monitor.channel_id, required), - (21, channel_monitor.balances_empty_height, option), - (23, channel_monitor.holder_pays_commitment_tx_fee, option), - (25, channel_monitor.payment_preimages, required), - (27, channel_monitor.first_negotiated_funding_txo, required), - (29, channel_monitor.initial_counterparty_commitment_tx, option), - (31, channel_monitor.funding.channel_parameters, required), - (32, channel_monitor.pending_funding, optional_vec), - (33, channel_monitor.htlcs_resolved_to_user, required), - (34, channel_monitor.alternative_funding_confirmed, option), - (35, channel_monitor.is_manual_broadcast, required), - (37, channel_monitor.funding_seen_onchain, required), - (39, channel_monitor.force_close_claimable_htlc_cltv_buffer, (default_value, CLTV_CLAIM_BUFFER)), + (1, channel_monitor.funding_spend_confirmed, option), + (3, channel_monitor.htlcs_resolved_on_chain, required_vec), + (5, pending_monitor_events, required_vec), + (7, channel_monitor.funding_spend_seen, required), + (9, channel_monitor.counterparty_node_id, required), + (11, channel_monitor.confirmed_commitment_tx_counterparty_output, option), + (13, channel_monitor.spendable_txids_confirmed, required_vec), + (15, channel_monitor.counterparty_fulfilled_htlcs, required), + (17, channel_monitor.initial_counterparty_commitment_info, option), + (19, channel_monitor.channel_id, required), + (21, channel_monitor.balances_empty_height, option), + (23, channel_monitor.holder_pays_commitment_tx_fee, option), + (25, channel_monitor.payment_preimages, required), + (27, channel_monitor.first_negotiated_funding_txo, required), + (29, channel_monitor.initial_counterparty_commitment_tx, option), + (31, channel_monitor.funding.channel_parameters, required), + (32, channel_monitor.pending_funding, optional_vec), + (33, channel_monitor.htlcs_resolved_to_user, required), + (34, channel_monitor.alternative_funding_confirmed, option), + (35, channel_monitor.is_manual_broadcast, required), + (37, channel_monitor.funding_seen_onchain, required), }); Ok(()) @@ -1872,7 +1863,6 @@ impl ChannelMonitor { initial_holder_commitment_tx: HolderCommitmentTransaction, best_block: BestBlock, counterparty_node_id: PublicKey, channel_id: ChannelId, is_manual_broadcast: bool, - force_close_claimable_htlc_cltv_buffer: u32, ) -> ChannelMonitor { assert!(commitment_transaction_number_obscure_factor <= (1 << 48)); @@ -1941,10 +1931,7 @@ impl ChannelMonitor { on_holder_tx_csv: counterparty_channel_parameters.selected_contest_delay, - force_close_claimable_htlc_cltv_buffer: core::cmp::max( - force_close_claimable_htlc_cltv_buffer, - CLTV_CLAIM_BUFFER, - ), + force_close_claimable_htlc_cltv_buffer: CLTV_CLAIM_BUFFER, commitment_secrets: CounterpartyCommitmentSecrets::new(), counterparty_commitment_txn_on_chain: new_hash_map(), @@ -2132,6 +2119,15 @@ impl ChannelMonitor { self.inner.lock().unwrap().channel_type_features().clone() } + /// Gets the initial force-close buffer configuration. + /// + /// This is used during initialization to populate ChainMonitor's memory-only config. + /// For monitors deserialized from old versions, this returns the persisted value. + /// For new monitors, this returns the value passed during construction. + pub(crate) fn get_initial_force_close_buffer(&self) -> u32 { + self.inner.lock().unwrap().force_close_claimable_htlc_cltv_buffer + } + /// Gets a list of txids, with their output scripts (in the order they appear in the /// transaction), which we must learn about spends of via block_connected(). #[rustfmt::skip] @@ -2383,6 +2379,7 @@ impl ChannelMonitor { header: &Header, txdata: &TransactionData, height: u32, + force_close_buffer: u32, broadcaster: B, fee_estimator: F, logger: &L, @@ -2390,7 +2387,7 @@ impl ChannelMonitor { let mut inner = self.inner.lock().unwrap(); let logger = WithChannelMonitor::from_impl(logger, &*inner, None); inner.block_connected( - header, txdata, height, broadcaster, fee_estimator, &logger) + header, txdata, height, force_close_buffer, broadcaster, fee_estimator, &logger) } /// Determines if the disconnected block contained any transactions of interest and updates @@ -2416,6 +2413,7 @@ impl ChannelMonitor { header: &Header, txdata: &TransactionData, height: u32, + force_close_buffer: u32, broadcaster: B, fee_estimator: F, logger: &L, @@ -2424,7 +2422,7 @@ impl ChannelMonitor { let mut inner = self.inner.lock().unwrap(); let logger = WithChannelMonitor::from_impl(logger, &*inner, None); inner.transactions_confirmed( - header, txdata, height, broadcaster, &bounded_fee_estimator, &logger) + header, txdata, height, force_close_buffer, broadcaster, &bounded_fee_estimator, &logger) } /// Processes a transaction that was reorganized out of the chain. @@ -2461,6 +2459,7 @@ impl ChannelMonitor { &self, header: &Header, height: u32, + force_close_buffer: u32, broadcaster: B, fee_estimator: F, logger: &L, @@ -2469,7 +2468,7 @@ impl ChannelMonitor { let mut inner = self.inner.lock().unwrap(); let logger = WithChannelMonitor::from_impl(logger, &*inner, None); inner.best_block_updated( - header, height, broadcaster, &bounded_fee_estimator, &logger + header, height, force_close_buffer, broadcaster, &bounded_fee_estimator, &logger ) } @@ -4299,13 +4298,6 @@ impl ChannelMonitorImpl { log_trace!(logger, "HTLC {htlc:?} permanently and fully resolved"); self.htlcs_resolved_to_user.insert(*htlc); }, - ChannelMonitorUpdateStep::ChannelConfigUpdated { force_close_claimable_htlc_cltv_buffer } => { - log_trace!(logger, "Updating ChannelMonitor force_close_claimable_htlc_cltv_buffer to {}", force_close_claimable_htlc_cltv_buffer); - self.force_close_claimable_htlc_cltv_buffer = core::cmp::max( - *force_close_claimable_htlc_cltv_buffer, - CLTV_CLAIM_BUFFER, - ); - }, } } @@ -4338,8 +4330,6 @@ impl ChannelMonitorImpl { ChannelMonitorUpdateStep::PaymentPreimage { .. } => {}, ChannelMonitorUpdateStep::ChannelForceClosed { .. } => {}, ChannelMonitorUpdateStep::ReleasePaymentComplete { .. } => {}, - ChannelMonitorUpdateStep::ChannelConfigUpdated { .. } => - is_pre_close_update = true, } } @@ -5241,14 +5231,14 @@ impl ChannelMonitorImpl { #[rustfmt::skip] fn block_connected( - &mut self, header: &Header, txdata: &TransactionData, height: u32, broadcaster: B, + &mut self, header: &Header, txdata: &TransactionData, height: u32, force_close_buffer: u32, broadcaster: B, fee_estimator: F, logger: &WithContext, ) -> Vec { let block_hash = header.block_hash(); self.best_block = BestBlock::new(block_hash, height); let bounded_fee_estimator = LowerBoundedFeeEstimator::new(fee_estimator); - self.transactions_confirmed(header, txdata, height, broadcaster, &bounded_fee_estimator, logger) + self.transactions_confirmed(header, txdata, height, force_close_buffer, broadcaster, &bounded_fee_estimator, logger) } #[rustfmt::skip] @@ -5256,6 +5246,7 @@ impl ChannelMonitorImpl { &mut self, header: &Header, height: u32, + force_close_buffer: u32, broadcaster: B, fee_estimator: &LowerBoundedFeeEstimator, logger: &WithContext, @@ -5265,7 +5256,7 @@ impl ChannelMonitorImpl { if height > self.best_block.height { self.best_block = BestBlock::new(block_hash, height); log_trace!(logger, "Connecting new block {} at height {}", block_hash, height); - self.block_confirmed(height, block_hash, vec![], vec![], vec![], &broadcaster, &fee_estimator, logger) + self.block_confirmed(height, block_hash, vec![], vec![], vec![], force_close_buffer, &broadcaster, &fee_estimator, logger) } else if block_hash != self.best_block.block_hash { self.best_block = BestBlock::new(block_hash, height); log_trace!(logger, "Best block re-orged, replaced with new block {} at height {}", block_hash, height); @@ -5284,6 +5275,7 @@ impl ChannelMonitorImpl { header: &Header, txdata: &TransactionData, height: u32, + force_close_buffer: u32, broadcaster: B, fee_estimator: &LowerBoundedFeeEstimator, logger: &WithContext, @@ -5547,7 +5539,7 @@ impl ChannelMonitorImpl { watch_outputs.append(&mut outputs); } - self.block_confirmed(height, block_hash, txn_matched, watch_outputs, claimable_outpoints, &broadcaster, &fee_estimator, logger) + self.block_confirmed(height, block_hash, txn_matched, watch_outputs, claimable_outpoints, force_close_buffer, &broadcaster, &fee_estimator, logger) } /// Update state for new block(s)/transaction(s) confirmed. Note that the caller must update @@ -5566,6 +5558,7 @@ impl ChannelMonitorImpl { txn_matched: Vec<&Transaction>, mut watch_outputs: Vec, mut claimable_outpoints: Vec, + force_close_buffer: u32, broadcaster: &B, fee_estimator: &LowerBoundedFeeEstimator, logger: &WithContext, @@ -5575,7 +5568,7 @@ impl ChannelMonitorImpl { // Only generate claims if we haven't already done so (e.g., in transactions_confirmed). if claimable_outpoints.is_empty() { - let should_broadcast = self.should_broadcast_holder_commitment_txn(logger); + let should_broadcast = self.should_broadcast_holder_commitment_txn(force_close_buffer, logger); if let Some(payment_hash) = should_broadcast { let reason = ClosureReason::HTLCsTimedOut { payment_hash: Some(payment_hash) }; let (mut new_outpoints, mut new_outputs) = @@ -5941,7 +5934,7 @@ impl ChannelMonitorImpl { #[rustfmt::skip] fn should_broadcast_holder_commitment_txn( - &self, logger: &WithContext + &self, force_close_buffer: u32, logger: &WithContext ) -> Option { // There's no need to broadcast our commitment transaction if we've seen one confirmed (even // with 1 confirmation) as it'll be rejected as duplicate/conflicting. @@ -5980,7 +5973,7 @@ impl ChannelMonitorImpl { // on-chain for an expired HTLC. let htlc_outbound = $holder_tx == htlc.offered; if ( htlc_outbound && htlc.cltv_expiry + LATENCY_GRACE_PERIOD_BLOCKS <= height) || - (!htlc_outbound && htlc.cltv_expiry <= height + self.force_close_claimable_htlc_cltv_buffer && self.payment_preimages.contains_key(&htlc.payment_hash)) { + (!htlc_outbound && htlc.cltv_expiry <= height + force_close_buffer && self.payment_preimages.contains_key(&htlc.payment_hash)) { log_info!(logger, "Force-closing channel due to {} HTLC timeout - HTLC with payment hash {} expires at {}", if htlc_outbound { "outbound" } else { "inbound"}, htlc.payment_hash, htlc.cltv_expiry); return Some(htlc.payment_hash); } @@ -6290,7 +6283,15 @@ impl, T, F, L) { fn filtered_block_connected(&self, header: &Header, txdata: &TransactionData, height: u32) { - self.0.block_connected(header, txdata, height, &self.1, &self.2, &self.3); + self.0.block_connected( + header, + txdata, + height, + CLTV_CLAIM_BUFFER, + &self.1, + &self.2, + &self.3, + ); } fn blocks_disconnected(&self, fork_point: BestBlock) { @@ -6304,7 +6305,15 @@ where M: Deref>, { fn transactions_confirmed(&self, header: &Header, txdata: &TransactionData, height: u32) { - self.0.transactions_confirmed(header, txdata, height, &self.1, &self.2, &self.3); + self.0.transactions_confirmed( + header, + txdata, + height, + CLTV_CLAIM_BUFFER, + &self.1, + &self.2, + &self.3, + ); } fn transaction_unconfirmed(&self, txid: &Txid) { @@ -6312,7 +6321,7 @@ where } fn best_block_updated(&self, header: &Header, height: u32) { - self.0.best_block_updated(header, height, &self.1, &self.2, &self.3); + self.0.best_block_updated(header, height, CLTV_CLAIM_BUFFER, &self.1, &self.2, &self.3); } fn get_relevant_txids(&self) -> Vec<(Txid, u32, Option)> { @@ -6793,7 +6802,7 @@ mod tests { use crate::chain::chaininterface::LowerBoundedFeeEstimator; use crate::events::ClosureReason; - use super::{ChannelMonitorUpdateStep, CLTV_CLAIM_BUFFER}; + use super::ChannelMonitorUpdateStep; use crate::chain::channelmonitor::{ChannelMonitor, WithChannelMonitor}; use crate::chain::package::{ weight_offered_htlc, weight_received_htlc, weight_revoked_offered_htlc, @@ -7028,7 +7037,7 @@ mod tests { let monitor = ChannelMonitor::new( Secp256k1::new(), keys, Some(shutdown_script.into_inner()), 0, &ScriptBuf::new(), &channel_parameters, true, 0, HolderCommitmentTransaction::dummy(0, funding_outpoint, Vec::new()), - best_block, dummy_key, channel_id, false, CLTV_CLAIM_BUFFER, + best_block, dummy_key, channel_id, false, ); let nondust_htlcs = preimages_slice_to_htlcs!(preimages[0..10]); @@ -7289,7 +7298,7 @@ mod tests { let monitor = ChannelMonitor::new( Secp256k1::new(), keys, Some(shutdown_script.into_inner()), 0, &ScriptBuf::new(), &channel_parameters, true, 0, HolderCommitmentTransaction::dummy(0, funding_outpoint, Vec::new()), - best_block, dummy_key, channel_id, false, CLTV_CLAIM_BUFFER, + best_block, dummy_key, channel_id, false, ); let chan_id = monitor.inner.lock().unwrap().channel_id(); diff --git a/lightning/src/chain/mod.rs b/lightning/src/chain/mod.rs index bc47f1b1db6..79071f9dada 100644 --- a/lightning/src/chain/mod.rs +++ b/lightning/src/chain/mod.rs @@ -345,6 +345,20 @@ pub trait Watch { fn release_pending_monitor_events( &self, ) -> Vec<(OutPoint, ChannelId, Vec, PublicKey)>; + + /// Updates the force-close buffer configuration for a channel. + /// + /// This is a memory-only update that controls how many blocks before an inbound HTLC's + /// CLTV expiry the channel will be force-closed. This method is optional and has a default + /// no-op implementation for backward compatibility. + /// + /// Returns an error if the implementation cannot apply the update or if the buffer value + /// is invalid. + fn update_channel_force_close_buffer( + &self, _channel_id: ChannelId, _force_close_buffer: u32, + ) -> Result<(), ()> { + Ok(()) + } } impl + ?Sized, W: Deref> @@ -367,6 +381,12 @@ impl + ?Sized, W: Der ) -> Vec<(OutPoint, ChannelId, Vec, PublicKey)> { self.deref().release_pending_monitor_events() } + + fn update_channel_force_close_buffer( + &self, channel_id: ChannelId, force_close_buffer: u32, + ) -> Result<(), ()> { + self.deref().update_channel_force_close_buffer(channel_id, force_close_buffer) + } } /// The `Filter` trait defines behavior for indicating chain activity of interest pertaining to diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index b13676acd4b..2f604127e38 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -3381,8 +3381,7 @@ trait InitialRemoteCommitmentReceiver { &funding.channel_transaction_parameters, funding.is_outbound(), obscure_factor, holder_commitment_tx, best_block, context.counterparty_node_id, context.channel_id(), context.is_manual_broadcast, - context.config.options.force_close_claimable_htlc_cltv_buffer, - ); + ); channel_monitor.provide_initial_counterparty_commitment_tx( counterparty_initial_commitment_tx.clone(), ); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 07e205fea37..8f1e64edcda 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -6648,6 +6648,13 @@ impl< if !channel.context_mut().update_config(&config) { continue; } + if let Some(buffer) = config_update.force_close_claimable_htlc_cltv_buffer { + if let Err(_) = self.chain_monitor.update_channel_force_close_buffer(*channel_id, buffer) { + return Err(APIError::APIMisuseError { + err: format!("Failed to update chain monitor force-close buffer"), + }); + } + } if let Some(channel) = channel.as_funded() { if let Ok((msg, node_id_1, node_id_2)) = self.get_channel_update_for_broadcast(channel) { let mut pending_broadcast_messages = self.pending_broadcast_messages.lock().unwrap(); From 711eadc06f36463271b9b3153b0098cb2c702a93 Mon Sep 17 00:00:00 2001 From: okekefrancis112 Date: Wed, 11 Feb 2026 22:24:29 +0100 Subject: [PATCH 3/3] fix: ci issues --- lightning/src/ln/channelmanager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 8f1e64edcda..6b4b127beeb 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -6651,7 +6651,7 @@ impl< if let Some(buffer) = config_update.force_close_claimable_htlc_cltv_buffer { if let Err(_) = self.chain_monitor.update_channel_force_close_buffer(*channel_id, buffer) { return Err(APIError::APIMisuseError { - err: format!("Failed to update chain monitor force-close buffer"), + err: "Failed to update chain monitor force-close buffer".to_string(), }); } }