Skip to content

Commit 59439dc

Browse files
feat: allow configurable force-close buffer for claimable HTLCs
Add a `force_close_claimable_htlc_cltv_buffer` field to `ChannelConfig` that allows users to configure how many blocks before an inbound HTLC's CLTV expiry the channel will be force-closed to claim it on-chain. This gives users more tolerance for their own downtime at the expense of being less tolerant of counterparty unresponsiveness (more force-closes). Default value remains `CLTV_CLAIM_BUFFER` (36 blocks). Values below the minimum are rejected via `update_partial_channel_config`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2e4a2ac commit 59439dc

File tree

5 files changed

+87
-7
lines changed

5 files changed

+87
-7
lines changed

lightning/src/chain/channelmonitor.rs

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,9 @@ pub(crate) const MAX_BLOCKS_FOR_CONF: u32 = 18;
278278
/// If an HTLC expires within this many blocks, force-close the channel to broadcast the
279279
/// HTLC-Success transaction.
280280
///
281-
/// This is two times [`MAX_BLOCKS_FOR_CONF`] as we need to first get the commitment transaction
282-
/// confirmed, then get an HTLC transaction confirmed.
283-
pub(crate) const CLTV_CLAIM_BUFFER: u32 = MAX_BLOCKS_FOR_CONF * 2;
281+
/// This accounts for the time needed to first get the commitment transaction confirmed, then get
282+
/// an HTLC transaction confirmed.
283+
pub const CLTV_CLAIM_BUFFER: u32 = MAX_BLOCKS_FOR_CONF * 2;
284284
/// Number of blocks by which point we expect our counterparty to have seen new blocks on the
285285
/// network and done a full update_fail_htlc/commitment_signed dance (+ we've updated all our
286286
/// copies of ChannelMonitors, including watchtowers). We could enforce the contract by failing
@@ -706,6 +706,10 @@ pub(crate) enum ChannelMonitorUpdateStep {
706706
ReleasePaymentComplete {
707707
htlc: SentHTLCId,
708708
},
709+
/// Used to update the configurable force-close CLTV buffer for claimable HTLCs.
710+
ChannelConfigUpdated {
711+
force_close_claimable_htlc_cltv_buffer: u32,
712+
},
709713
}
710714

711715
impl ChannelMonitorUpdateStep {
@@ -723,6 +727,7 @@ impl ChannelMonitorUpdateStep {
723727
ChannelMonitorUpdateStep::RenegotiatedFunding { .. } => "RenegotiatedFunding",
724728
ChannelMonitorUpdateStep::RenegotiatedFundingLocked { .. } => "RenegotiatedFundingLocked",
725729
ChannelMonitorUpdateStep::ReleasePaymentComplete { .. } => "ReleasePaymentComplete",
730+
ChannelMonitorUpdateStep::ChannelConfigUpdated { .. } => "ChannelConfigUpdated",
726731
}
727732
}
728733
}
@@ -777,6 +782,9 @@ impl_writeable_tlv_based_enum_upgradable!(ChannelMonitorUpdateStep,
777782
(12, RenegotiatedFundingLocked) => {
778783
(1, funding_txid, required),
779784
},
785+
(14, ChannelConfigUpdated) => {
786+
(1, force_close_claimable_htlc_cltv_buffer, required),
787+
},
780788
);
781789

782790
/// Indicates whether the balance is derived from a cooperative close, a force-close
@@ -1234,6 +1242,10 @@ pub(crate) struct ChannelMonitorImpl<Signer: EcdsaChannelSigner> {
12341242

12351243
on_holder_tx_csv: u16,
12361244

1245+
/// The configurable number of blocks before an inbound HTLC's CLTV expiry at which we will
1246+
/// force-close the channel to claim it on-chain. Defaults to [`CLTV_CLAIM_BUFFER`].
1247+
force_close_claimable_htlc_cltv_buffer: u32,
1248+
12371249
commitment_secrets: CounterpartyCommitmentSecrets,
12381250
/// We cannot identify HTLC-Success or HTLC-Timeout transactions by themselves on the chain.
12391251
/// Nor can we figure out their commitment numbers without the commitment transaction they are
@@ -1754,6 +1766,7 @@ pub(crate) fn write_chanmon_internal<Signer: EcdsaChannelSigner, W: Writer>(
17541766
(34, channel_monitor.alternative_funding_confirmed, option),
17551767
(35, channel_monitor.is_manual_broadcast, required),
17561768
(37, channel_monitor.funding_seen_onchain, required),
1769+
(39, channel_monitor.force_close_claimable_htlc_cltv_buffer, (default_value, CLTV_CLAIM_BUFFER)),
17571770
});
17581771

17591772
Ok(())
@@ -1859,6 +1872,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {
18591872
initial_holder_commitment_tx: HolderCommitmentTransaction, best_block: BestBlock,
18601873
counterparty_node_id: PublicKey, channel_id: ChannelId,
18611874
is_manual_broadcast: bool,
1875+
force_close_claimable_htlc_cltv_buffer: u32,
18621876
) -> ChannelMonitor<Signer> {
18631877

18641878
assert!(commitment_transaction_number_obscure_factor <= (1 << 48));
@@ -1927,6 +1941,11 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {
19271941

19281942
on_holder_tx_csv: counterparty_channel_parameters.selected_contest_delay,
19291943

1944+
force_close_claimable_htlc_cltv_buffer: core::cmp::max(
1945+
force_close_claimable_htlc_cltv_buffer,
1946+
CLTV_CLAIM_BUFFER,
1947+
),
1948+
19301949
commitment_secrets: CounterpartyCommitmentSecrets::new(),
19311950
counterparty_commitment_txn_on_chain: new_hash_map(),
19321951
counterparty_hash_commitment_number: new_hash_map(),
@@ -4280,6 +4299,13 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
42804299
log_trace!(logger, "HTLC {htlc:?} permanently and fully resolved");
42814300
self.htlcs_resolved_to_user.insert(*htlc);
42824301
},
4302+
ChannelMonitorUpdateStep::ChannelConfigUpdated { force_close_claimable_htlc_cltv_buffer } => {
4303+
log_trace!(logger, "Updating ChannelMonitor force_close_claimable_htlc_cltv_buffer to {}", force_close_claimable_htlc_cltv_buffer);
4304+
self.force_close_claimable_htlc_cltv_buffer = core::cmp::max(
4305+
*force_close_claimable_htlc_cltv_buffer,
4306+
CLTV_CLAIM_BUFFER,
4307+
);
4308+
},
42834309
}
42844310
}
42854311

@@ -4312,6 +4338,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
43124338
ChannelMonitorUpdateStep::PaymentPreimage { .. } => {},
43134339
ChannelMonitorUpdateStep::ChannelForceClosed { .. } => {},
43144340
ChannelMonitorUpdateStep::ReleasePaymentComplete { .. } => {},
4341+
ChannelMonitorUpdateStep::ChannelConfigUpdated { .. } =>
4342+
is_pre_close_update = true,
43154343
}
43164344
}
43174345

@@ -5952,7 +5980,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
59525980
// on-chain for an expired HTLC.
59535981
let htlc_outbound = $holder_tx == htlc.offered;
59545982
if ( htlc_outbound && htlc.cltv_expiry + LATENCY_GRACE_PERIOD_BLOCKS <= height) ||
5955-
(!htlc_outbound && htlc.cltv_expiry <= height + CLTV_CLAIM_BUFFER && self.payment_preimages.contains_key(&htlc.payment_hash)) {
5983+
(!htlc_outbound && htlc.cltv_expiry <= height + self.force_close_claimable_htlc_cltv_buffer && self.payment_preimages.contains_key(&htlc.payment_hash)) {
59565984
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);
59575985
return Some(htlc.payment_hash);
59585986
}
@@ -6520,6 +6548,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
65206548
let mut alternative_funding_confirmed = None;
65216549
let mut is_manual_broadcast = RequiredWrapper(None);
65226550
let mut funding_seen_onchain = RequiredWrapper(None);
6551+
let mut force_close_claimable_htlc_cltv_buffer = CLTV_CLAIM_BUFFER;
65236552
read_tlv_fields!(reader, {
65246553
(1, funding_spend_confirmed, option),
65256554
(3, htlcs_resolved_on_chain, optional_vec),
@@ -6542,6 +6571,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
65426571
(34, alternative_funding_confirmed, option),
65436572
(35, is_manual_broadcast, (default_value, false)),
65446573
(37, funding_seen_onchain, (default_value, true)),
6574+
(39, force_close_claimable_htlc_cltv_buffer, (default_value, CLTV_CLAIM_BUFFER)),
65456575
});
65466576
// Note that `payment_preimages_with_info` was added (and is always written) in LDK 0.1, so
65476577
// 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
66816711

66826712
on_holder_tx_csv,
66836713

6714+
force_close_claimable_htlc_cltv_buffer,
6715+
66846716
commitment_secrets,
66856717
counterparty_commitment_txn_on_chain,
66866718
counterparty_hash_commitment_number,
@@ -6761,7 +6793,7 @@ mod tests {
67616793
use crate::chain::chaininterface::LowerBoundedFeeEstimator;
67626794
use crate::events::ClosureReason;
67636795

6764-
use super::ChannelMonitorUpdateStep;
6796+
use super::{ChannelMonitorUpdateStep, CLTV_CLAIM_BUFFER};
67656797
use crate::chain::channelmonitor::{ChannelMonitor, WithChannelMonitor};
67666798
use crate::chain::package::{
67676799
weight_offered_htlc, weight_received_htlc, weight_revoked_offered_htlc,
@@ -6996,7 +7028,7 @@ mod tests {
69967028
let monitor = ChannelMonitor::new(
69977029
Secp256k1::new(), keys, Some(shutdown_script.into_inner()), 0, &ScriptBuf::new(),
69987030
&channel_parameters, true, 0, HolderCommitmentTransaction::dummy(0, funding_outpoint, Vec::new()),
6999-
best_block, dummy_key, channel_id, false,
7031+
best_block, dummy_key, channel_id, false, CLTV_CLAIM_BUFFER,
70007032
);
70017033

70027034
let nondust_htlcs = preimages_slice_to_htlcs!(preimages[0..10]);
@@ -7257,7 +7289,7 @@ mod tests {
72577289
let monitor = ChannelMonitor::new(
72587290
Secp256k1::new(), keys, Some(shutdown_script.into_inner()), 0, &ScriptBuf::new(),
72597291
&channel_parameters, true, 0, HolderCommitmentTransaction::dummy(0, funding_outpoint, Vec::new()),
7260-
best_block, dummy_key, channel_id, false,
7292+
best_block, dummy_key, channel_id, false, CLTV_CLAIM_BUFFER,
72617293
);
72627294

72637295
let chan_id = monitor.inner.lock().unwrap().channel_id();

lightning/src/ln/channel.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3381,6 +3381,7 @@ trait InitialRemoteCommitmentReceiver<SP: SignerProvider> {
33813381
&funding.channel_transaction_parameters, funding.is_outbound(), obscure_factor,
33823382
holder_commitment_tx, best_block, context.counterparty_node_id, context.channel_id(),
33833383
context.is_manual_broadcast,
3384+
context.config.options.force_close_claimable_htlc_cltv_buffer,
33843385
);
33853386
channel_monitor.provide_initial_counterparty_commitment_tx(
33863387
counterparty_initial_commitment_tx.clone(),

lightning/src/ln/channel_open_tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,7 @@ pub fn test_manually_accept_inbound_channel_request() {
11191119
cltv_expiry_delta: None,
11201120
max_dust_htlc_exposure_msat: None,
11211121
force_close_avoidance_max_fee_satoshis: None,
1122+
force_close_claimable_htlc_cltv_buffer: None,
11221123
accept_underpaying_htlcs: None,
11231124
}),
11241125
};

lightning/src/ln/channelmanager.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6619,6 +6619,13 @@ impl<
66196619
err: format!("The chosen CLTV expiry delta is below the minimum of {}", MIN_CLTV_EXPIRY_DELTA),
66206620
});
66216621
}
6622+
if config_update.force_close_claimable_htlc_cltv_buffer
6623+
.map(|buf| buf < CLTV_CLAIM_BUFFER).unwrap_or(false)
6624+
{
6625+
return Err(APIError::APIMisuseError {
6626+
err: format!("The chosen force-close CLTV buffer is below the minimum of {}", CLTV_CLAIM_BUFFER),
6627+
});
6628+
}
66226629

66236630
let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self);
66246631
let per_peer_state = self.per_peer_state.read().unwrap();

lightning/src/util/config.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//! Various user-configurable channel limits and settings which ChannelManager
1111
//! applies for you.
1212
13+
use crate::chain::channelmonitor::CLTV_CLAIM_BUFFER;
1314
use crate::ln::channel::MAX_FUNDING_SATOSHIS_NO_WUMBO;
1415
use crate::ln::channelmanager::{BREAKDOWN_TIMEOUT, MAX_LOCAL_BREAKDOWN_TIMEOUT};
1516

@@ -593,6 +594,26 @@ pub struct ChannelConfig {
593594
/// [`NonAnchorChannelFee`]: crate::chain::chaininterface::ConfirmationTarget::NonAnchorChannelFee
594595
/// [`ChannelCloseMinimum`]: crate::chain::chaininterface::ConfirmationTarget::ChannelCloseMinimum
595596
pub force_close_avoidance_max_fee_satoshis: u64,
597+
/// The number of blocks before an inbound HTLC's CLTV expiry at which we will force-close the
598+
/// channel in order to claim it on-chain with an HTLC-Success transaction. This applies when
599+
/// we have the preimage for the HTLC (i.e., it is a claimable HTLC that we forwarded).
600+
///
601+
/// Increasing this value gives you more tolerance for your own downtime at the expense of
602+
/// being less tolerant of counterparty unresponsiveness (more force-closes). When set higher,
603+
/// you force-close earlier, leaving more blocks to get the commitment transaction and
604+
/// HTLC-Success transaction confirmed on-chain.
605+
///
606+
/// Note that when changing this value, you should also ensure that
607+
/// [`cltv_expiry_delta`] is large enough to accommodate the new buffer.
608+
///
609+
/// Default value: [`CLTV_CLAIM_BUFFER`] (36 blocks)
610+
///
611+
/// Minimum value: [`CLTV_CLAIM_BUFFER`] (Any values less than this will be treated as
612+
/// [`CLTV_CLAIM_BUFFER`] instead.)
613+
///
614+
/// [`cltv_expiry_delta`]: ChannelConfig::cltv_expiry_delta
615+
/// [`CLTV_CLAIM_BUFFER`]: crate::chain::channelmonitor::CLTV_CLAIM_BUFFER
616+
pub force_close_claimable_htlc_cltv_buffer: u32,
596617
/// If set, allows this channel's counterparty to skim an additional fee off this node's inbound
597618
/// HTLCs. Useful for liquidity providers to offload on-chain channel costs to end users.
598619
///
@@ -650,6 +671,11 @@ impl ChannelConfig {
650671
{
651672
self.force_close_avoidance_max_fee_satoshis = force_close_avoidance_max_fee_satoshis;
652673
}
674+
if let Some(force_close_claimable_htlc_cltv_buffer) =
675+
update.force_close_claimable_htlc_cltv_buffer
676+
{
677+
self.force_close_claimable_htlc_cltv_buffer = force_close_claimable_htlc_cltv_buffer;
678+
}
653679
if let Some(accept_underpaying_htlcs) = update.accept_underpaying_htlcs {
654680
self.accept_underpaying_htlcs = accept_underpaying_htlcs;
655681
}
@@ -665,6 +691,7 @@ impl Default for ChannelConfig {
665691
cltv_expiry_delta: 6 * 12, // 6 blocks/hour * 12 hours
666692
max_dust_htlc_exposure: MaxDustHTLCExposure::FeeRateMultiplier(10000),
667693
force_close_avoidance_max_fee_satoshis: 1000,
694+
force_close_claimable_htlc_cltv_buffer: CLTV_CLAIM_BUFFER,
668695
accept_underpaying_htlcs: false,
669696
}
670697
}
@@ -687,6 +714,7 @@ impl crate::util::ser::Writeable for ChannelConfig {
687714
// LegacyChannelConfig. To make sure that serialization is not compatible with this one, we use
688715
// the next required type of 10, which if seen by the old serialization will always fail.
689716
(10, self.force_close_avoidance_max_fee_satoshis, required),
717+
(11, self.force_close_claimable_htlc_cltv_buffer, (default_value, CLTV_CLAIM_BUFFER)),
690718
});
691719
Ok(())
692720
}
@@ -701,6 +729,7 @@ impl crate::util::ser::Readable for ChannelConfig {
701729
let mut max_dust_htlc_exposure_msat = None;
702730
let mut max_dust_htlc_exposure_enum = None;
703731
let mut force_close_avoidance_max_fee_satoshis = 1000;
732+
let mut force_close_claimable_htlc_cltv_buffer = CLTV_CLAIM_BUFFER;
704733
read_tlv_fields!(reader, {
705734
(0, forwarding_fee_proportional_millionths, required),
706735
(1, accept_underpaying_htlcs, (default_value, false)),
@@ -710,6 +739,7 @@ impl crate::util::ser::Readable for ChannelConfig {
710739
// Has always been written, but became optionally read in 0.0.116
711740
(6, max_dust_htlc_exposure_msat, option),
712741
(10, force_close_avoidance_max_fee_satoshis, required),
742+
(11, force_close_claimable_htlc_cltv_buffer, (default_value, CLTV_CLAIM_BUFFER)),
713743
});
714744
let max_dust_htlc_fixed_limit = max_dust_htlc_exposure_msat.unwrap_or(5_000_000);
715745
let max_dust_htlc_exposure_msat = max_dust_htlc_exposure_enum
@@ -721,6 +751,7 @@ impl crate::util::ser::Readable for ChannelConfig {
721751
cltv_expiry_delta,
722752
max_dust_htlc_exposure: max_dust_htlc_exposure_msat,
723753
force_close_avoidance_max_fee_satoshis,
754+
force_close_claimable_htlc_cltv_buffer,
724755
})
725756
}
726757
}
@@ -747,6 +778,10 @@ pub struct ChannelConfigUpdate {
747778
/// funds. See [`ChannelConfig::force_close_avoidance_max_fee_satoshis`].
748779
pub force_close_avoidance_max_fee_satoshis: Option<u64>,
749780

781+
/// The number of blocks before an inbound HTLC's CLTV expiry at which we will force-close to claim it
782+
/// on-chain. See [`ChannelConfig::force_close_claimable_htlc_cltv_buffer`].
783+
pub force_close_claimable_htlc_cltv_buffer: Option<u32>,
784+
750785
/// If set, allows this channel's counterparty to skim an additional fee off this node's inbound HTLCs. See
751786
/// [`ChannelConfig::accept_underpaying_htlcs`].
752787
pub accept_underpaying_htlcs: Option<bool>,
@@ -764,6 +799,9 @@ impl From<ChannelConfig> for ChannelConfigUpdate {
764799
force_close_avoidance_max_fee_satoshis: Some(
765800
config.force_close_avoidance_max_fee_satoshis,
766801
),
802+
force_close_claimable_htlc_cltv_buffer: Some(
803+
config.force_close_claimable_htlc_cltv_buffer,
804+
),
767805
accept_underpaying_htlcs: Some(config.accept_underpaying_htlcs),
768806
}
769807
}
@@ -846,6 +884,7 @@ impl crate::util::ser::Readable for LegacyChannelConfig {
846884
max_dust_htlc_exposure: max_dust_htlc_exposure_msat,
847885
cltv_expiry_delta,
848886
force_close_avoidance_max_fee_satoshis,
887+
force_close_claimable_htlc_cltv_buffer: CLTV_CLAIM_BUFFER,
849888
forwarding_fee_base_msat,
850889
accept_underpaying_htlcs: false,
851890
},

0 commit comments

Comments
 (0)