From 0bc52cced379ca5d379ab2794f41dc52148f8321 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Sun, 24 Aug 2025 23:46:48 +0000 Subject: [PATCH 01/13] Remove `addl_nondust_htlc_count` debug statement This will make our lives easier when we use `get_next_commitment_stats` to determine the available balances in the channel. --- lightning/src/sign/tx_builder.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/lightning/src/sign/tx_builder.rs b/lightning/src/sign/tx_builder.rs index 74941ec8a87..e6d598f1aab 100644 --- a/lightning/src/sign/tx_builder.rs +++ b/lightning/src/sign/tx_builder.rs @@ -194,7 +194,6 @@ impl TxBuilder for SpecTxBuilder { if channel_type.supports_anchor_zero_fee_commitments() { debug_assert_eq!(feerate_per_kw, 0); debug_assert_eq!(excess_feerate, 0); - debug_assert_eq!(addl_nondust_htlc_count, 0); } // Calculate inbound htlc count From f2bfd6706ca9f648360ad5888c7fa509ecfe3956 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Sun, 24 Aug 2025 02:17:33 +0000 Subject: [PATCH 02/13] Cleanup calculation of the biggest HTLC value that can be sent next No functional change. --- lightning/src/ln/channel.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 5b668ec2076..e90eac040f7 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -5723,16 +5723,12 @@ where // We will first subtract the fee as if we were above-dust. Then, if the resulting // value ends up being below dust, we have this fee available again. In that case, // match the value to right-below-dust. - let mut capacity_minus_commitment_fee_msat: i64 = available_capacity_msat as i64 - - max_reserved_commit_tx_fee_msat as i64; - if capacity_minus_commitment_fee_msat < (real_dust_limit_timeout_sat as i64) * 1000 { - let one_htlc_difference_msat = max_reserved_commit_tx_fee_msat - min_reserved_commit_tx_fee_msat; - debug_assert!(one_htlc_difference_msat != 0); - capacity_minus_commitment_fee_msat += one_htlc_difference_msat as i64; - capacity_minus_commitment_fee_msat = cmp::min(real_dust_limit_timeout_sat as i64 * 1000 - 1, capacity_minus_commitment_fee_msat); - available_capacity_msat = cmp::max(0, cmp::min(capacity_minus_commitment_fee_msat, available_capacity_msat as i64)) as u64; + let capacity_minus_max_commitment_fee_msat = available_capacity_msat.saturating_sub(max_reserved_commit_tx_fee_msat); + if capacity_minus_max_commitment_fee_msat < real_dust_limit_timeout_sat * 1000 { + let capacity_minus_min_commitment_fee_msat = available_capacity_msat.saturating_sub(min_reserved_commit_tx_fee_msat); + available_capacity_msat = cmp::min(real_dust_limit_timeout_sat * 1000 - 1, capacity_minus_min_commitment_fee_msat); } else { - available_capacity_msat = capacity_minus_commitment_fee_msat as u64; + available_capacity_msat = capacity_minus_max_commitment_fee_msat; } } else { // If the channel is inbound (i.e. counterparty pays the fee), we need to make sure From addbe07edbd082520066a73e20a1757e5ffb9056 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Sun, 24 Aug 2025 23:22:51 +0000 Subject: [PATCH 03/13] Create `ChannelConstraints` These will be useful to pass these constraints to `TxBuilder` to calculate `AvailableBalances`. --- lightning/src/ln/channel.rs | 44 ++++++++++++++++++++++++-------- lightning/src/sign/tx_builder.rs | 8 ++++++ 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index e90eac040f7..4c63d102fab 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -73,7 +73,7 @@ use crate::ln::LN_MAX_MSG_LEN; use crate::offers::static_invoice::StaticInvoice; use crate::routing::gossip::NodeId; use crate::sign::ecdsa::EcdsaChannelSigner; -use crate::sign::tx_builder::{HTLCAmountDirection, NextCommitmentStats, SpecTxBuilder, TxBuilder}; +use crate::sign::tx_builder::{HTLCAmountDirection, ChannelConstraints, NextCommitmentStats, SpecTxBuilder, TxBuilder}; use crate::sign::{ChannelSigner, EntropySource, NodeSigner, Recipient, SignerProvider}; use crate::types::features::{ChannelTypeFeatures, InitFeatures}; use crate::types::payment::{PaymentHash, PaymentPreimage}; @@ -5662,6 +5662,26 @@ where outbound_details } + fn get_holder_channel_constraints(&self, funding: &FundingScope) -> ChannelConstraints { + ChannelConstraints { + dust_limit_satoshis: self.holder_dust_limit_satoshis, + channel_reserve_satoshis: funding.counterparty_selected_channel_reserve_satoshis.unwrap_or(0), + htlc_minimum_msat: self.holder_htlc_minimum_msat, + max_accepted_htlcs: self.holder_max_accepted_htlcs as u64, + max_htlc_value_in_flight_msat: self.holder_max_htlc_value_in_flight_msat, + } + } + + fn get_counterparty_channel_constraints(&self, funding: &FundingScope) -> ChannelConstraints { + ChannelConstraints { + dust_limit_satoshis: self.counterparty_dust_limit_satoshis, + channel_reserve_satoshis: funding.holder_selected_channel_reserve_satoshis, + htlc_minimum_msat: self.counterparty_htlc_minimum_msat, + max_accepted_htlcs: self.counterparty_max_accepted_htlcs as u64, + max_htlc_value_in_flight_msat: self.counterparty_max_htlc_value_in_flight_msat, + } + } + #[rustfmt::skip] fn get_available_balances_for_scope( &self, funding: &FundingScope, fee_estimator: &LowerBoundedFeeEstimator, @@ -5670,6 +5690,8 @@ where F::Target: FeeEstimator, { let context = &self; + let holder_channel_constraints = self.get_holder_channel_constraints(funding); + let counterparty_channel_constraints = self.get_counterparty_channel_constraints(funding); // Note that we have to handle overflow due to the case mentioned in the docs in general // here. @@ -5688,7 +5710,7 @@ where let outbound_capacity_msat = local_balance_before_fee_msat .saturating_sub( - funding.counterparty_selected_channel_reserve_satoshis.unwrap_or(0) * 1000); + holder_channel_constraints.channel_reserve_satoshis * 1000); let mut available_capacity_msat = outbound_capacity_msat; let (real_htlc_success_tx_fee_sat, real_htlc_timeout_tx_fee_sat) = second_stage_tx_fees_sat( @@ -5709,7 +5731,7 @@ where Some(()) }; - let real_dust_limit_timeout_sat = real_htlc_timeout_tx_fee_sat + context.holder_dust_limit_satoshis; + let real_dust_limit_timeout_sat = real_htlc_timeout_tx_fee_sat + holder_channel_constraints.dust_limit_satoshis; let htlc_above_dust = HTLCCandidate::new(real_dust_limit_timeout_sat * 1000, HTLCInitiator::LocalOffered); let mut max_reserved_commit_tx_fee_msat = context.next_local_commit_tx_fee_msat(&funding, htlc_above_dust, fee_spike_buffer_htlc); let htlc_dust = HTLCCandidate::new(real_dust_limit_timeout_sat * 1000 - 1, HTLCInitiator::LocalOffered); @@ -5733,11 +5755,11 @@ where } else { // If the channel is inbound (i.e. counterparty pays the fee), we need to make sure // sending a new HTLC won't reduce their balance below our reserve threshold. - let real_dust_limit_success_sat = real_htlc_success_tx_fee_sat + context.counterparty_dust_limit_satoshis; + let real_dust_limit_success_sat = real_htlc_success_tx_fee_sat + counterparty_channel_constraints.dust_limit_satoshis; let htlc_above_dust = HTLCCandidate::new(real_dust_limit_success_sat * 1000, HTLCInitiator::LocalOffered); let max_reserved_commit_tx_fee_msat = context.next_remote_commit_tx_fee_msat(funding, Some(htlc_above_dust), None); - let holder_selected_chan_reserve_msat = funding.holder_selected_channel_reserve_satoshis * 1000; + let holder_selected_chan_reserve_msat = counterparty_channel_constraints.channel_reserve_satoshis * 1000; if remote_balance_before_fee_msat < max_reserved_commit_tx_fee_msat + holder_selected_chan_reserve_msat { // If another HTLC's fee would reduce the remote's balance below the reserve limit // we've selected for them, we can only send dust HTLCs. @@ -5745,7 +5767,7 @@ where } } - let mut next_outbound_htlc_minimum_msat = context.counterparty_htlc_minimum_msat; + let mut next_outbound_htlc_minimum_msat = counterparty_channel_constraints.htlc_minimum_msat; // If we get close to our maximum dust exposure, we end up in a situation where we can send // between zero and the remaining dust exposure limit remaining OR above the dust limit. @@ -5759,8 +5781,8 @@ where let (buffer_htlc_success_tx_fee_sat, buffer_htlc_timeout_tx_fee_sat) = second_stage_tx_fees_sat( funding.get_channel_type(), dust_buffer_feerate, ); - let buffer_dust_limit_success_sat = buffer_htlc_success_tx_fee_sat + context.counterparty_dust_limit_satoshis; - let buffer_dust_limit_timeout_sat = buffer_htlc_timeout_tx_fee_sat + context.holder_dust_limit_satoshis; + let buffer_dust_limit_success_sat = buffer_htlc_success_tx_fee_sat + counterparty_channel_constraints.dust_limit_satoshis; + let buffer_dust_limit_timeout_sat = buffer_htlc_timeout_tx_fee_sat + holder_channel_constraints.dust_limit_satoshis; if let Some(extra_htlc_dust_exposure) = htlc_stats.extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat { if extra_htlc_dust_exposure > max_dust_htlc_exposure_msat { @@ -5794,15 +5816,15 @@ where } available_capacity_msat = cmp::min(available_capacity_msat, - context.counterparty_max_htlc_value_in_flight_msat - htlc_stats.pending_outbound_htlcs_value_msat); + counterparty_channel_constraints.max_htlc_value_in_flight_msat - htlc_stats.pending_outbound_htlcs_value_msat); - if htlc_stats.pending_outbound_htlcs + 1 > context.counterparty_max_accepted_htlcs as usize { + if htlc_stats.pending_outbound_htlcs + 1 > counterparty_channel_constraints.max_accepted_htlcs as usize { available_capacity_msat = 0; } #[allow(deprecated)] // TODO: Remove once balance_msat is removed. AvailableBalances { - inbound_capacity_msat: remote_balance_before_fee_msat.saturating_sub(funding.holder_selected_channel_reserve_satoshis * 1000), + inbound_capacity_msat: remote_balance_before_fee_msat.saturating_sub(counterparty_channel_constraints.channel_reserve_satoshis * 1000), outbound_capacity_msat, next_outbound_htlc_limit_msat: available_capacity_msat, next_outbound_htlc_minimum_msat, diff --git a/lightning/src/sign/tx_builder.rs b/lightning/src/sign/tx_builder.rs index e6d598f1aab..240bcfc688e 100644 --- a/lightning/src/sign/tx_builder.rs +++ b/lightning/src/sign/tx_builder.rs @@ -154,6 +154,14 @@ fn get_dust_buffer_feerate(feerate_per_kw: u32) -> u32 { cmp::max(feerate_per_kw.saturating_add(2530), feerate_plus_quarter.unwrap_or(u32::MAX)) } +pub(crate) struct ChannelConstraints { + pub dust_limit_satoshis: u64, + pub channel_reserve_satoshis: u64, + pub htlc_minimum_msat: u64, + pub max_htlc_value_in_flight_msat: u64, + pub max_accepted_htlcs: u64, +} + pub(crate) trait TxBuilder { fn get_next_commitment_stats( &self, local: bool, is_outbound_from_holder: bool, channel_value_satoshis: u64, From 9a7e1dc4e5550a3a7f37c66384a1ad15acd8887c Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Sun, 24 Aug 2025 23:47:14 +0000 Subject: [PATCH 04/13] Use `TxBuilder::get_next_commitment_stats` to get `AvailableBalances` Also move things around to make the move in the next commit as straightforward as possible. We take the conservative route here and include all pending HTLCs, including those in the holding cell, no matter their state. --- lightning/src/ln/channel.rs | 100 +++++++++++++++++-------------- lightning/src/sign/tx_builder.rs | 2 +- 2 files changed, 56 insertions(+), 46 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 4c63d102fab..264e4c16945 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -5689,35 +5689,56 @@ where where F::Target: FeeEstimator, { - let context = &self; + use crate::sign::tx_builder::get_dust_buffer_feerate; + let holder_channel_constraints = self.get_holder_channel_constraints(funding); let counterparty_channel_constraints = self.get_counterparty_channel_constraints(funding); // Note that we have to handle overflow due to the case mentioned in the docs in general // here. + let pending_outbound_htlcs = self.pending_outbound_htlcs.iter().map(|htlc| HTLCAmountDirection { amount_msat: htlc.amount_msat, outbound: true }); + let pending_inbound_htlcs = self.pending_inbound_htlcs.iter().map(|htlc| HTLCAmountDirection { amount_msat: htlc.amount_msat, outbound: false }); + let holding_cell_htlcs = self.holding_cell_htlc_updates.iter().filter_map(|htlc| { + if let &HTLCUpdateAwaitingACK::AddHTLC { amount_msat, .. } = htlc { + Some(HTLCAmountDirection { outbound: true, amount_msat }) + } else { + None + } + }); + + let mut pending_htlcs: Vec = Vec::with_capacity(self.pending_outbound_htlcs.len() + self.pending_inbound_htlcs.len() + self.holding_cell_htlc_updates.len()); + pending_htlcs.extend(pending_outbound_htlcs.chain(pending_inbound_htlcs).chain(holding_cell_htlcs)); + let pending_htlcs = &pending_htlcs; + let dust_exposure_limiting_feerate = self.get_dust_exposure_limiting_feerate( &fee_estimator, funding.get_channel_type(), ); - let htlc_stats = context.get_pending_htlc_stats(funding, None, dust_exposure_limiting_feerate); + let max_dust_htlc_exposure_msat = self.get_max_dust_htlc_exposure_msat(dust_exposure_limiting_feerate); - // Subtract any non-HTLC outputs from the local and remote balances - let (local_balance_before_fee_msat, remote_balance_before_fee_msat) = SpecTxBuilder {}.subtract_non_htlc_outputs( - funding.is_outbound(), - funding.value_to_self_msat.saturating_sub(htlc_stats.pending_outbound_htlcs_value_msat), - (funding.get_value_satoshis() * 1000).checked_sub(funding.value_to_self_msat).unwrap().saturating_sub(htlc_stats.pending_inbound_htlcs_value_msat), - funding.get_channel_type(), - ); + let is_outbound_from_holder = funding.is_outbound(); + let channel_value_satoshis = funding.get_value_satoshis(); + let value_to_holder_msat = funding.get_value_to_self_msat(); + let feerate_per_kw = self.feerate_per_kw; + let channel_type = funding.get_channel_type(); + + let fee_spike_buffer_htlc = if channel_type.supports_anchor_zero_fee_commitments() { + 0 + } else { + 1 + }; - let outbound_capacity_msat = local_balance_before_fee_msat - .saturating_sub( - holder_channel_constraints.channel_reserve_satoshis * 1000); + let local_stats_max_fee = SpecTxBuilder {}.get_next_commitment_stats(true, is_outbound_from_holder, channel_value_satoshis, value_to_holder_msat, pending_htlcs, fee_spike_buffer_htlc + 1, feerate_per_kw, dust_exposure_limiting_feerate, holder_channel_constraints.dust_limit_satoshis, channel_type).unwrap(); + let local_stats_min_fee = SpecTxBuilder {}.get_next_commitment_stats(true, is_outbound_from_holder, channel_value_satoshis, value_to_holder_msat, pending_htlcs, fee_spike_buffer_htlc, feerate_per_kw, dust_exposure_limiting_feerate, holder_channel_constraints.dust_limit_satoshis, channel_type).unwrap(); + let remote_stats = SpecTxBuilder {}.get_next_commitment_stats(false, is_outbound_from_holder, channel_value_satoshis, value_to_holder_msat, pending_htlcs, 1, feerate_per_kw, dust_exposure_limiting_feerate, counterparty_channel_constraints.dust_limit_satoshis, channel_type).unwrap(); + + let outbound_capacity_msat = local_stats_max_fee.holder_balance_before_fee_msat.saturating_sub(holder_channel_constraints.channel_reserve_satoshis * 1000); let mut available_capacity_msat = outbound_capacity_msat; let (real_htlc_success_tx_fee_sat, real_htlc_timeout_tx_fee_sat) = second_stage_tx_fees_sat( - funding.get_channel_type(), context.feerate_per_kw, + channel_type, feerate_per_kw ); - if funding.is_outbound() { + if is_outbound_from_holder { // We should mind channel commit tx fee when computing how much of the available capacity // can be used in the next htlc. Mirrors the logic in send_htlc. // @@ -5725,21 +5746,14 @@ where // and the answer will in turn change the amount itself — making it a circular // dependency. // This complicates the computation around dust-values, up to the one-htlc-value. - let fee_spike_buffer_htlc = if funding.get_channel_type().supports_anchor_zero_fee_commitments() { - None - } else { - Some(()) - }; let real_dust_limit_timeout_sat = real_htlc_timeout_tx_fee_sat + holder_channel_constraints.dust_limit_satoshis; - let htlc_above_dust = HTLCCandidate::new(real_dust_limit_timeout_sat * 1000, HTLCInitiator::LocalOffered); - let mut max_reserved_commit_tx_fee_msat = context.next_local_commit_tx_fee_msat(&funding, htlc_above_dust, fee_spike_buffer_htlc); - let htlc_dust = HTLCCandidate::new(real_dust_limit_timeout_sat * 1000 - 1, HTLCInitiator::LocalOffered); - let mut min_reserved_commit_tx_fee_msat = context.next_local_commit_tx_fee_msat(&funding, htlc_dust, fee_spike_buffer_htlc); + let mut max_reserved_commit_tx_fee_msat = local_stats_max_fee.commit_tx_fee_sat * 1000; + let mut min_reserved_commit_tx_fee_msat = local_stats_min_fee.commit_tx_fee_sat * 1000; - if !funding.get_channel_type().supports_anchors_zero_fee_htlc_tx() { - max_reserved_commit_tx_fee_msat *= FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE; - min_reserved_commit_tx_fee_msat *= FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE; + if !channel_type.supports_anchors_zero_fee_htlc_tx() { + max_reserved_commit_tx_fee_msat *= crate::ln::channel::FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE; + min_reserved_commit_tx_fee_msat *= crate::ln::channel::FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE; } // We will first subtract the fee as if we were above-dust. Then, if the resulting @@ -5756,11 +5770,10 @@ where // If the channel is inbound (i.e. counterparty pays the fee), we need to make sure // sending a new HTLC won't reduce their balance below our reserve threshold. let real_dust_limit_success_sat = real_htlc_success_tx_fee_sat + counterparty_channel_constraints.dust_limit_satoshis; - let htlc_above_dust = HTLCCandidate::new(real_dust_limit_success_sat * 1000, HTLCInitiator::LocalOffered); - let max_reserved_commit_tx_fee_msat = context.next_remote_commit_tx_fee_msat(funding, Some(htlc_above_dust), None); + let max_reserved_commit_tx_fee_msat = remote_stats.commit_tx_fee_sat * 1000; let holder_selected_chan_reserve_msat = counterparty_channel_constraints.channel_reserve_satoshis * 1000; - if remote_balance_before_fee_msat < max_reserved_commit_tx_fee_msat + holder_selected_chan_reserve_msat { + if remote_stats.counterparty_balance_before_fee_msat < max_reserved_commit_tx_fee_msat + holder_selected_chan_reserve_msat { // If another HTLC's fee would reduce the remote's balance below the reserve limit // we've selected for them, we can only send dust HTLCs. available_capacity_msat = cmp::min(available_capacity_msat, real_dust_limit_success_sat * 1000 - 1); @@ -5775,35 +5788,32 @@ where // send above the dust limit (as the router can always overpay to meet the dust limit). let mut remaining_msat_below_dust_exposure_limit = None; let mut dust_exposure_dust_limit_msat = 0; - let max_dust_htlc_exposure_msat = context.get_max_dust_htlc_exposure_msat(dust_exposure_limiting_feerate); - let dust_buffer_feerate = self.get_dust_buffer_feerate(None); + let dust_buffer_feerate = get_dust_buffer_feerate(feerate_per_kw); let (buffer_htlc_success_tx_fee_sat, buffer_htlc_timeout_tx_fee_sat) = second_stage_tx_fees_sat( - funding.get_channel_type(), dust_buffer_feerate, + channel_type, dust_buffer_feerate, ); let buffer_dust_limit_success_sat = buffer_htlc_success_tx_fee_sat + counterparty_channel_constraints.dust_limit_satoshis; let buffer_dust_limit_timeout_sat = buffer_htlc_timeout_tx_fee_sat + holder_channel_constraints.dust_limit_satoshis; - if let Some(extra_htlc_dust_exposure) = htlc_stats.extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat { - if extra_htlc_dust_exposure > max_dust_htlc_exposure_msat { - // If adding an extra HTLC would put us over the dust limit in total fees, we cannot - // send any non-dust HTLCs. - available_capacity_msat = cmp::min(available_capacity_msat, buffer_dust_limit_success_sat * 1000); - } + if remote_stats.extra_accepted_htlc_dust_exposure_msat > max_dust_htlc_exposure_msat { + // If adding an extra HTLC would put us over the dust limit in total fees, we cannot + // send any non-dust HTLCs. + available_capacity_msat = cmp::min(available_capacity_msat, buffer_dust_limit_success_sat * 1000); } - if htlc_stats.on_counterparty_tx_dust_exposure_msat.saturating_add(buffer_dust_limit_success_sat * 1000) > max_dust_htlc_exposure_msat.saturating_add(1) { + if remote_stats.dust_exposure_msat.saturating_add(buffer_dust_limit_success_sat * 1000) > max_dust_htlc_exposure_msat.saturating_add(1) { // Note that we don't use the `counterparty_tx_dust_exposure` (with // `htlc_dust_exposure_msat`) here as it only applies to non-dust HTLCs. remaining_msat_below_dust_exposure_limit = - Some(max_dust_htlc_exposure_msat.saturating_sub(htlc_stats.on_counterparty_tx_dust_exposure_msat)); + Some(max_dust_htlc_exposure_msat.saturating_sub(remote_stats.dust_exposure_msat)); dust_exposure_dust_limit_msat = cmp::max(dust_exposure_dust_limit_msat, buffer_dust_limit_success_sat * 1000); } - if htlc_stats.on_holder_tx_dust_exposure_msat as i64 + buffer_dust_limit_timeout_sat as i64 * 1000 - 1 > max_dust_htlc_exposure_msat.try_into().unwrap_or(i64::max_value()) { + if local_stats_max_fee.dust_exposure_msat as i64 + buffer_dust_limit_timeout_sat as i64 * 1000 - 1 > max_dust_htlc_exposure_msat.try_into().unwrap_or(i64::max_value()) { remaining_msat_below_dust_exposure_limit = Some(cmp::min( remaining_msat_below_dust_exposure_limit.unwrap_or(u64::max_value()), - max_dust_htlc_exposure_msat.saturating_sub(htlc_stats.on_holder_tx_dust_exposure_msat))); + max_dust_htlc_exposure_msat.saturating_sub(local_stats_max_fee.dust_exposure_msat))); dust_exposure_dust_limit_msat = cmp::max(dust_exposure_dust_limit_msat, buffer_dust_limit_timeout_sat * 1000); } @@ -5816,15 +5826,15 @@ where } available_capacity_msat = cmp::min(available_capacity_msat, - counterparty_channel_constraints.max_htlc_value_in_flight_msat - htlc_stats.pending_outbound_htlcs_value_msat); + counterparty_channel_constraints.max_htlc_value_in_flight_msat - pending_htlcs.iter().filter(|htlc| htlc.outbound).map(|htlc| htlc.amount_msat).sum::()); - if htlc_stats.pending_outbound_htlcs + 1 > counterparty_channel_constraints.max_accepted_htlcs as usize { + if pending_htlcs.iter().filter(|htlc| htlc.outbound).count() + 1 > counterparty_channel_constraints.max_accepted_htlcs as usize { available_capacity_msat = 0; } #[allow(deprecated)] // TODO: Remove once balance_msat is removed. AvailableBalances { - inbound_capacity_msat: remote_balance_before_fee_msat.saturating_sub(counterparty_channel_constraints.channel_reserve_satoshis * 1000), + inbound_capacity_msat: remote_stats.counterparty_balance_before_fee_msat.saturating_sub(counterparty_channel_constraints.channel_reserve_satoshis * 1000), outbound_capacity_msat, next_outbound_htlc_limit_msat: available_capacity_msat, next_outbound_htlc_minimum_msat, diff --git a/lightning/src/sign/tx_builder.rs b/lightning/src/sign/tx_builder.rs index 240bcfc688e..ac33e76cd45 100644 --- a/lightning/src/sign/tx_builder.rs +++ b/lightning/src/sign/tx_builder.rs @@ -144,7 +144,7 @@ fn subtract_addl_outputs( } } -fn get_dust_buffer_feerate(feerate_per_kw: u32) -> u32 { +pub(crate) fn get_dust_buffer_feerate(feerate_per_kw: u32) -> u32 { // When calculating our exposure to dust HTLCs, we assume that the channel feerate // may, at any point, increase by at least 10 sat/vB (i.e 2530 sat/kWU) or 25%, // whichever is higher. This ensures that we aren't suddenly exposed to significantly From 6b2401f83abfac63de467a42df086bef23ea296e Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Sun, 24 Aug 2025 02:18:19 +0000 Subject: [PATCH 05/13] Add `TxBuilder::get_available_balances` Besides the changes in the `TxBuilder` API, this is a code move. --- lightning/src/ln/channel.rs | 146 +++---------------------------- lightning/src/sign/tx_builder.rs | 146 ++++++++++++++++++++++++++++++- 2 files changed, 158 insertions(+), 134 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 264e4c16945..fc33914b526 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1360,7 +1360,7 @@ impl HolderCommitmentPoint { #[cfg(any(fuzzing, test, feature = "_test_utils"))] pub const FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE: u64 = 2; #[cfg(not(any(fuzzing, test, feature = "_test_utils")))] -const FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE: u64 = 2; +pub(crate) const FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE: u64 = 2; /// If we fail to see a funding transaction confirmed on-chain within this many blocks after the /// channel creation on an inbound channel, we simply force-close and move on. @@ -5689,13 +5689,6 @@ where where F::Target: FeeEstimator, { - use crate::sign::tx_builder::get_dust_buffer_feerate; - - let holder_channel_constraints = self.get_holder_channel_constraints(funding); - let counterparty_channel_constraints = self.get_counterparty_channel_constraints(funding); - // Note that we have to handle overflow due to the case mentioned in the docs in general - // here. - let pending_outbound_htlcs = self.pending_outbound_htlcs.iter().map(|htlc| HTLCAmountDirection { amount_msat: htlc.amount_msat, outbound: true }); let pending_inbound_htlcs = self.pending_inbound_htlcs.iter().map(|htlc| HTLCAmountDirection { amount_msat: htlc.amount_msat, outbound: false }); let holding_cell_htlcs = self.holding_cell_htlc_updates.iter().filter_map(|htlc| { @@ -5708,137 +5701,24 @@ where let mut pending_htlcs: Vec = Vec::with_capacity(self.pending_outbound_htlcs.len() + self.pending_inbound_htlcs.len() + self.holding_cell_htlc_updates.len()); pending_htlcs.extend(pending_outbound_htlcs.chain(pending_inbound_htlcs).chain(holding_cell_htlcs)); - let pending_htlcs = &pending_htlcs; let dust_exposure_limiting_feerate = self.get_dust_exposure_limiting_feerate( &fee_estimator, funding.get_channel_type(), ); let max_dust_htlc_exposure_msat = self.get_max_dust_htlc_exposure_msat(dust_exposure_limiting_feerate); - let is_outbound_from_holder = funding.is_outbound(); - let channel_value_satoshis = funding.get_value_satoshis(); - let value_to_holder_msat = funding.get_value_to_self_msat(); - let feerate_per_kw = self.feerate_per_kw; - let channel_type = funding.get_channel_type(); - - let fee_spike_buffer_htlc = if channel_type.supports_anchor_zero_fee_commitments() { - 0 - } else { - 1 - }; - - let local_stats_max_fee = SpecTxBuilder {}.get_next_commitment_stats(true, is_outbound_from_holder, channel_value_satoshis, value_to_holder_msat, pending_htlcs, fee_spike_buffer_htlc + 1, feerate_per_kw, dust_exposure_limiting_feerate, holder_channel_constraints.dust_limit_satoshis, channel_type).unwrap(); - let local_stats_min_fee = SpecTxBuilder {}.get_next_commitment_stats(true, is_outbound_from_holder, channel_value_satoshis, value_to_holder_msat, pending_htlcs, fee_spike_buffer_htlc, feerate_per_kw, dust_exposure_limiting_feerate, holder_channel_constraints.dust_limit_satoshis, channel_type).unwrap(); - let remote_stats = SpecTxBuilder {}.get_next_commitment_stats(false, is_outbound_from_holder, channel_value_satoshis, value_to_holder_msat, pending_htlcs, 1, feerate_per_kw, dust_exposure_limiting_feerate, counterparty_channel_constraints.dust_limit_satoshis, channel_type).unwrap(); - - let outbound_capacity_msat = local_stats_max_fee.holder_balance_before_fee_msat.saturating_sub(holder_channel_constraints.channel_reserve_satoshis * 1000); - - let mut available_capacity_msat = outbound_capacity_msat; - let (real_htlc_success_tx_fee_sat, real_htlc_timeout_tx_fee_sat) = second_stage_tx_fees_sat( - channel_type, feerate_per_kw - ); - - if is_outbound_from_holder { - // We should mind channel commit tx fee when computing how much of the available capacity - // can be used in the next htlc. Mirrors the logic in send_htlc. - // - // The fee depends on whether the amount we will be sending is above dust or not, - // and the answer will in turn change the amount itself — making it a circular - // dependency. - // This complicates the computation around dust-values, up to the one-htlc-value. - - let real_dust_limit_timeout_sat = real_htlc_timeout_tx_fee_sat + holder_channel_constraints.dust_limit_satoshis; - let mut max_reserved_commit_tx_fee_msat = local_stats_max_fee.commit_tx_fee_sat * 1000; - let mut min_reserved_commit_tx_fee_msat = local_stats_min_fee.commit_tx_fee_sat * 1000; - - if !channel_type.supports_anchors_zero_fee_htlc_tx() { - max_reserved_commit_tx_fee_msat *= crate::ln::channel::FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE; - min_reserved_commit_tx_fee_msat *= crate::ln::channel::FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE; - } - - // We will first subtract the fee as if we were above-dust. Then, if the resulting - // value ends up being below dust, we have this fee available again. In that case, - // match the value to right-below-dust. - let capacity_minus_max_commitment_fee_msat = available_capacity_msat.saturating_sub(max_reserved_commit_tx_fee_msat); - if capacity_minus_max_commitment_fee_msat < real_dust_limit_timeout_sat * 1000 { - let capacity_minus_min_commitment_fee_msat = available_capacity_msat.saturating_sub(min_reserved_commit_tx_fee_msat); - available_capacity_msat = cmp::min(real_dust_limit_timeout_sat * 1000 - 1, capacity_minus_min_commitment_fee_msat); - } else { - available_capacity_msat = capacity_minus_max_commitment_fee_msat; - } - } else { - // If the channel is inbound (i.e. counterparty pays the fee), we need to make sure - // sending a new HTLC won't reduce their balance below our reserve threshold. - let real_dust_limit_success_sat = real_htlc_success_tx_fee_sat + counterparty_channel_constraints.dust_limit_satoshis; - let max_reserved_commit_tx_fee_msat = remote_stats.commit_tx_fee_sat * 1000; - - let holder_selected_chan_reserve_msat = counterparty_channel_constraints.channel_reserve_satoshis * 1000; - if remote_stats.counterparty_balance_before_fee_msat < max_reserved_commit_tx_fee_msat + holder_selected_chan_reserve_msat { - // If another HTLC's fee would reduce the remote's balance below the reserve limit - // we've selected for them, we can only send dust HTLCs. - available_capacity_msat = cmp::min(available_capacity_msat, real_dust_limit_success_sat * 1000 - 1); - } - } - - let mut next_outbound_htlc_minimum_msat = counterparty_channel_constraints.htlc_minimum_msat; - - // If we get close to our maximum dust exposure, we end up in a situation where we can send - // between zero and the remaining dust exposure limit remaining OR above the dust limit. - // Because we cannot express this as a simple min/max, we prefer to tell the user they can - // send above the dust limit (as the router can always overpay to meet the dust limit). - let mut remaining_msat_below_dust_exposure_limit = None; - let mut dust_exposure_dust_limit_msat = 0; - - let dust_buffer_feerate = get_dust_buffer_feerate(feerate_per_kw); - let (buffer_htlc_success_tx_fee_sat, buffer_htlc_timeout_tx_fee_sat) = second_stage_tx_fees_sat( - channel_type, dust_buffer_feerate, - ); - let buffer_dust_limit_success_sat = buffer_htlc_success_tx_fee_sat + counterparty_channel_constraints.dust_limit_satoshis; - let buffer_dust_limit_timeout_sat = buffer_htlc_timeout_tx_fee_sat + holder_channel_constraints.dust_limit_satoshis; - - if remote_stats.extra_accepted_htlc_dust_exposure_msat > max_dust_htlc_exposure_msat { - // If adding an extra HTLC would put us over the dust limit in total fees, we cannot - // send any non-dust HTLCs. - available_capacity_msat = cmp::min(available_capacity_msat, buffer_dust_limit_success_sat * 1000); - } - - if remote_stats.dust_exposure_msat.saturating_add(buffer_dust_limit_success_sat * 1000) > max_dust_htlc_exposure_msat.saturating_add(1) { - // Note that we don't use the `counterparty_tx_dust_exposure` (with - // `htlc_dust_exposure_msat`) here as it only applies to non-dust HTLCs. - remaining_msat_below_dust_exposure_limit = - Some(max_dust_htlc_exposure_msat.saturating_sub(remote_stats.dust_exposure_msat)); - dust_exposure_dust_limit_msat = cmp::max(dust_exposure_dust_limit_msat, buffer_dust_limit_success_sat * 1000); - } - - if local_stats_max_fee.dust_exposure_msat as i64 + buffer_dust_limit_timeout_sat as i64 * 1000 - 1 > max_dust_htlc_exposure_msat.try_into().unwrap_or(i64::max_value()) { - remaining_msat_below_dust_exposure_limit = Some(cmp::min( - remaining_msat_below_dust_exposure_limit.unwrap_or(u64::max_value()), - max_dust_htlc_exposure_msat.saturating_sub(local_stats_max_fee.dust_exposure_msat))); - dust_exposure_dust_limit_msat = cmp::max(dust_exposure_dust_limit_msat, buffer_dust_limit_timeout_sat * 1000); - } - - if let Some(remaining_limit_msat) = remaining_msat_below_dust_exposure_limit { - if available_capacity_msat < dust_exposure_dust_limit_msat { - available_capacity_msat = cmp::min(available_capacity_msat, remaining_limit_msat); - } else { - next_outbound_htlc_minimum_msat = cmp::max(next_outbound_htlc_minimum_msat, dust_exposure_dust_limit_msat); - } - } - - available_capacity_msat = cmp::min(available_capacity_msat, - counterparty_channel_constraints.max_htlc_value_in_flight_msat - pending_htlcs.iter().filter(|htlc| htlc.outbound).map(|htlc| htlc.amount_msat).sum::()); - - if pending_htlcs.iter().filter(|htlc| htlc.outbound).count() + 1 > counterparty_channel_constraints.max_accepted_htlcs as usize { - available_capacity_msat = 0; - } - - #[allow(deprecated)] // TODO: Remove once balance_msat is removed. - AvailableBalances { - inbound_capacity_msat: remote_stats.counterparty_balance_before_fee_msat.saturating_sub(counterparty_channel_constraints.channel_reserve_satoshis * 1000), - outbound_capacity_msat, - next_outbound_htlc_limit_msat: available_capacity_msat, - next_outbound_htlc_minimum_msat, - } + SpecTxBuilder {}.get_available_balances( + funding.is_outbound(), + funding.get_value_satoshis(), + funding.get_value_to_self_msat(), + &pending_htlcs, + self.feerate_per_kw, + dust_exposure_limiting_feerate, + max_dust_htlc_exposure_msat, + self.get_holder_channel_constraints(funding), + self.get_counterparty_channel_constraints(funding), + funding.get_channel_type(), + ) } /// Get the commitment tx fee for the local's (i.e. our) next commitment transaction based on the diff --git a/lightning/src/sign/tx_builder.rs b/lightning/src/sign/tx_builder.rs index ac33e76cd45..703df3d6d7d 100644 --- a/lightning/src/sign/tx_builder.rs +++ b/lightning/src/sign/tx_builder.rs @@ -144,7 +144,7 @@ fn subtract_addl_outputs( } } -pub(crate) fn get_dust_buffer_feerate(feerate_per_kw: u32) -> u32 { +fn get_dust_buffer_feerate(feerate_per_kw: u32) -> u32 { // When calculating our exposure to dust HTLCs, we assume that the channel feerate // may, at any point, increase by at least 10 sat/vB (i.e 2530 sat/kWU) or 25%, // whichever is higher. This ensures that we aren't suddenly exposed to significantly @@ -163,6 +163,19 @@ pub(crate) struct ChannelConstraints { } pub(crate) trait TxBuilder { + fn get_available_balances( + &self, + is_outbound_from_holder: bool, + channel_value_satoshis: u64, + value_to_holder_msat: u64, + pending_htlcs: &[HTLCAmountDirection], + feerate_per_kw: u32, + dust_exposure_limiting_feerate: Option, + max_dust_htlc_exposure_msat: u64, + holder_channel_constraints: ChannelConstraints, + counterparty_channel_constraints: ChannelConstraints, + channel_type: &ChannelTypeFeatures, + ) -> crate::ln::channel::AvailableBalances; fn get_next_commitment_stats( &self, local: bool, is_outbound_from_holder: bool, channel_value_satoshis: u64, value_to_holder_msat: u64, next_commitment_htlcs: &[HTLCAmountDirection], @@ -190,6 +203,137 @@ pub(crate) trait TxBuilder { pub(crate) struct SpecTxBuilder {} impl TxBuilder for SpecTxBuilder { + fn get_available_balances( + &self, + is_outbound_from_holder: bool, + channel_value_satoshis: u64, + value_to_holder_msat: u64, + pending_htlcs: &[HTLCAmountDirection], + feerate_per_kw: u32, + dust_exposure_limiting_feerate: Option, + max_dust_htlc_exposure_msat: u64, + holder_channel_constraints: ChannelConstraints, + counterparty_channel_constraints: ChannelConstraints, + channel_type: &ChannelTypeFeatures, + ) -> crate::ln::channel::AvailableBalances { + let fee_spike_buffer_htlc = if channel_type.supports_anchor_zero_fee_commitments() { + 0 + } else { + 1 + }; + + let local_stats_max_fee = SpecTxBuilder {}.get_next_commitment_stats(true, is_outbound_from_holder, channel_value_satoshis, value_to_holder_msat, pending_htlcs, fee_spike_buffer_htlc + 1, feerate_per_kw, dust_exposure_limiting_feerate, holder_channel_constraints.dust_limit_satoshis, channel_type).unwrap(); + let local_stats_min_fee = SpecTxBuilder {}.get_next_commitment_stats(true, is_outbound_from_holder, channel_value_satoshis, value_to_holder_msat, pending_htlcs, fee_spike_buffer_htlc, feerate_per_kw, dust_exposure_limiting_feerate, holder_channel_constraints.dust_limit_satoshis, channel_type).unwrap(); + let remote_stats = SpecTxBuilder {}.get_next_commitment_stats(false, is_outbound_from_holder, channel_value_satoshis, value_to_holder_msat, pending_htlcs, 1, feerate_per_kw, dust_exposure_limiting_feerate, counterparty_channel_constraints.dust_limit_satoshis, channel_type).unwrap(); + + let outbound_capacity_msat = local_stats_max_fee.holder_balance_before_fee_msat.saturating_sub(holder_channel_constraints.channel_reserve_satoshis * 1000); + + let mut available_capacity_msat = outbound_capacity_msat; + let (real_htlc_success_tx_fee_sat, real_htlc_timeout_tx_fee_sat) = second_stage_tx_fees_sat( + channel_type, feerate_per_kw + ); + + if is_outbound_from_holder { + // We should mind channel commit tx fee when computing how much of the available capacity + // can be used in the next htlc. Mirrors the logic in send_htlc. + // + // The fee depends on whether the amount we will be sending is above dust or not, + // and the answer will in turn change the amount itself — making it a circular + // dependency. + // This complicates the computation around dust-values, up to the one-htlc-value. + + let real_dust_limit_timeout_sat = real_htlc_timeout_tx_fee_sat + holder_channel_constraints.dust_limit_satoshis; + let mut max_reserved_commit_tx_fee_msat = local_stats_max_fee.commit_tx_fee_sat * 1000; + let mut min_reserved_commit_tx_fee_msat = local_stats_min_fee.commit_tx_fee_sat * 1000; + + if !channel_type.supports_anchors_zero_fee_htlc_tx() { + max_reserved_commit_tx_fee_msat *= crate::ln::channel::FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE; + min_reserved_commit_tx_fee_msat *= crate::ln::channel::FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE; + } + + // We will first subtract the fee as if we were above-dust. Then, if the resulting + // value ends up being below dust, we have this fee available again. In that case, + // match the value to right-below-dust. + let capacity_minus_max_commitment_fee_msat = available_capacity_msat.saturating_sub(max_reserved_commit_tx_fee_msat); + if capacity_minus_max_commitment_fee_msat < real_dust_limit_timeout_sat * 1000 { + let capacity_minus_min_commitment_fee_msat = available_capacity_msat.saturating_sub(min_reserved_commit_tx_fee_msat); + available_capacity_msat = cmp::min(real_dust_limit_timeout_sat * 1000 - 1, capacity_minus_min_commitment_fee_msat); + } else { + available_capacity_msat = capacity_minus_max_commitment_fee_msat; + } + } else { + // If the channel is inbound (i.e. counterparty pays the fee), we need to make sure + // sending a new HTLC won't reduce their balance below our reserve threshold. + let real_dust_limit_success_sat = real_htlc_success_tx_fee_sat + counterparty_channel_constraints.dust_limit_satoshis; + let max_reserved_commit_tx_fee_msat = remote_stats.commit_tx_fee_sat * 1000; + + let holder_selected_chan_reserve_msat = counterparty_channel_constraints.channel_reserve_satoshis * 1000; + if remote_stats.counterparty_balance_before_fee_msat < max_reserved_commit_tx_fee_msat + holder_selected_chan_reserve_msat { + // If another HTLC's fee would reduce the remote's balance below the reserve limit + // we've selected for them, we can only send dust HTLCs. + available_capacity_msat = cmp::min(available_capacity_msat, real_dust_limit_success_sat * 1000 - 1); + } + } + + let mut next_outbound_htlc_minimum_msat = counterparty_channel_constraints.htlc_minimum_msat; + + // If we get close to our maximum dust exposure, we end up in a situation where we can send + // between zero and the remaining dust exposure limit remaining OR above the dust limit. + // Because we cannot express this as a simple min/max, we prefer to tell the user they can + // send above the dust limit (as the router can always overpay to meet the dust limit). + let mut remaining_msat_below_dust_exposure_limit = None; + let mut dust_exposure_dust_limit_msat = 0; + + let dust_buffer_feerate = get_dust_buffer_feerate(feerate_per_kw); + let (buffer_htlc_success_tx_fee_sat, buffer_htlc_timeout_tx_fee_sat) = second_stage_tx_fees_sat( + channel_type, dust_buffer_feerate, + ); + let buffer_dust_limit_success_sat = buffer_htlc_success_tx_fee_sat + counterparty_channel_constraints.dust_limit_satoshis; + let buffer_dust_limit_timeout_sat = buffer_htlc_timeout_tx_fee_sat + holder_channel_constraints.dust_limit_satoshis; + + if remote_stats.extra_accepted_htlc_dust_exposure_msat > max_dust_htlc_exposure_msat { + // If adding an extra HTLC would put us over the dust limit in total fees, we cannot + // send any non-dust HTLCs. + available_capacity_msat = cmp::min(available_capacity_msat, buffer_dust_limit_success_sat * 1000); + } + + if remote_stats.dust_exposure_msat.saturating_add(buffer_dust_limit_success_sat * 1000) > max_dust_htlc_exposure_msat.saturating_add(1) { + // Note that we don't use the `counterparty_tx_dust_exposure` (with + // `htlc_dust_exposure_msat`) here as it only applies to non-dust HTLCs. + remaining_msat_below_dust_exposure_limit = + Some(max_dust_htlc_exposure_msat.saturating_sub(remote_stats.dust_exposure_msat)); + dust_exposure_dust_limit_msat = cmp::max(dust_exposure_dust_limit_msat, buffer_dust_limit_success_sat * 1000); + } + + if local_stats_max_fee.dust_exposure_msat as i64 + buffer_dust_limit_timeout_sat as i64 * 1000 - 1 > max_dust_htlc_exposure_msat.try_into().unwrap_or(i64::max_value()) { + remaining_msat_below_dust_exposure_limit = Some(cmp::min( + remaining_msat_below_dust_exposure_limit.unwrap_or(u64::max_value()), + max_dust_htlc_exposure_msat.saturating_sub(local_stats_max_fee.dust_exposure_msat))); + dust_exposure_dust_limit_msat = cmp::max(dust_exposure_dust_limit_msat, buffer_dust_limit_timeout_sat * 1000); + } + + if let Some(remaining_limit_msat) = remaining_msat_below_dust_exposure_limit { + if available_capacity_msat < dust_exposure_dust_limit_msat { + available_capacity_msat = cmp::min(available_capacity_msat, remaining_limit_msat); + } else { + next_outbound_htlc_minimum_msat = cmp::max(next_outbound_htlc_minimum_msat, dust_exposure_dust_limit_msat); + } + } + + available_capacity_msat = cmp::min(available_capacity_msat, + counterparty_channel_constraints.max_htlc_value_in_flight_msat - pending_htlcs.iter().filter(|htlc| htlc.outbound).map(|htlc| htlc.amount_msat).sum::()); + + if pending_htlcs.iter().filter(|htlc| htlc.outbound).count() + 1 > counterparty_channel_constraints.max_accepted_htlcs as usize { + available_capacity_msat = 0; + } + + crate::ln::channel::AvailableBalances { + inbound_capacity_msat: remote_stats.counterparty_balance_before_fee_msat.saturating_sub(counterparty_channel_constraints.channel_reserve_satoshis * 1000), + outbound_capacity_msat, + next_outbound_htlc_limit_msat: available_capacity_msat, + next_outbound_htlc_minimum_msat, + } + } fn get_next_commitment_stats( &self, local: bool, is_outbound_from_holder: bool, channel_value_satoshis: u64, value_to_holder_msat: u64, next_commitment_htlcs: &[HTLCAmountDirection], From fccbd470180ddb59d115d6a5019e74ee820deeae Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Sun, 24 Aug 2025 18:49:45 +0000 Subject: [PATCH 06/13] Delete `ChannelContext::get_pending_htlc_stats`, `HTLCStats` --- lightning/src/ln/channel.rs | 118 ------------------------------------ 1 file changed, 118 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index fc33914b526..0c2302724ca 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1071,19 +1071,6 @@ enum HTLCInitiator { RemoteOffered, } -/// Current counts of various HTLCs, useful for calculating current balances available exactly. -struct HTLCStats { - pending_outbound_htlcs: usize, - pending_inbound_htlcs_value_msat: u64, - pending_outbound_htlcs_value_msat: u64, - on_counterparty_tx_dust_exposure_msat: u64, - // If the counterparty sets a feerate on the channel in excess of our dust_exposure_limiting_feerate, - // this will be set to the dust exposure that would result from us adding an additional nondust outbound - // htlc on the counterparty's commitment transaction. - extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat: Option, - on_holder_tx_dust_exposure_msat: u64, -} - /// A struct gathering data on a commitment, either local or remote. struct CommitmentData<'a> { tx: CommitmentTransaction, @@ -5464,111 +5451,6 @@ where self.counterparty_forwarding_info.clone() } - /// Returns a HTLCStats about pending htlcs - #[rustfmt::skip] - fn get_pending_htlc_stats( - &self, funding: &FundingScope, outbound_feerate_update: Option, - dust_exposure_limiting_feerate: Option, - ) -> HTLCStats { - let context = self; - - let dust_buffer_feerate = self.get_dust_buffer_feerate(outbound_feerate_update); - let (htlc_success_tx_fee_sat, htlc_timeout_tx_fee_sat) = second_stage_tx_fees_sat( - funding.get_channel_type(), dust_buffer_feerate, - ); - - let mut on_holder_tx_dust_exposure_msat = 0; - let mut on_counterparty_tx_dust_exposure_msat = 0; - - let mut on_counterparty_tx_offered_nondust_htlcs = 0; - let mut on_counterparty_tx_accepted_nondust_htlcs = 0; - - let mut pending_inbound_htlcs_value_msat = 0; - - { - let counterparty_dust_limit_timeout_sat = htlc_timeout_tx_fee_sat + context.counterparty_dust_limit_satoshis; - let holder_dust_limit_success_sat = htlc_success_tx_fee_sat + context.holder_dust_limit_satoshis; - for htlc in context.pending_inbound_htlcs.iter() { - pending_inbound_htlcs_value_msat += htlc.amount_msat; - if htlc.amount_msat / 1000 < counterparty_dust_limit_timeout_sat { - on_counterparty_tx_dust_exposure_msat += htlc.amount_msat; - } else { - on_counterparty_tx_offered_nondust_htlcs += 1; - } - if htlc.amount_msat / 1000 < holder_dust_limit_success_sat { - on_holder_tx_dust_exposure_msat += htlc.amount_msat; - } - } - } - - let mut pending_outbound_htlcs_value_msat = 0; - let mut pending_outbound_htlcs = self.pending_outbound_htlcs.len(); - { - let counterparty_dust_limit_success_sat = htlc_success_tx_fee_sat + context.counterparty_dust_limit_satoshis; - let holder_dust_limit_timeout_sat = htlc_timeout_tx_fee_sat + context.holder_dust_limit_satoshis; - for htlc in context.pending_outbound_htlcs.iter() { - pending_outbound_htlcs_value_msat += htlc.amount_msat; - if htlc.amount_msat / 1000 < counterparty_dust_limit_success_sat { - on_counterparty_tx_dust_exposure_msat += htlc.amount_msat; - } else { - on_counterparty_tx_accepted_nondust_htlcs += 1; - } - if htlc.amount_msat / 1000 < holder_dust_limit_timeout_sat { - on_holder_tx_dust_exposure_msat += htlc.amount_msat; - } - } - - for update in context.holding_cell_htlc_updates.iter() { - if let &HTLCUpdateAwaitingACK::AddHTLC { ref amount_msat, .. } = update { - pending_outbound_htlcs += 1; - pending_outbound_htlcs_value_msat += amount_msat; - if *amount_msat / 1000 < counterparty_dust_limit_success_sat { - on_counterparty_tx_dust_exposure_msat += amount_msat; - } else { - on_counterparty_tx_accepted_nondust_htlcs += 1; - } - if *amount_msat / 1000 < holder_dust_limit_timeout_sat { - on_holder_tx_dust_exposure_msat += amount_msat; - } - } - } - } - - // Include any mining "excess" fees in the dust calculation - let excess_feerate_opt = outbound_feerate_update - .or(self.pending_update_fee.map(|(fee, _)| fee)) - .unwrap_or(self.feerate_per_kw) - .checked_sub(dust_exposure_limiting_feerate.unwrap_or(0)); - - // Dust exposure is only decoupled from feerate for zero fee commitment channels. - let is_zero_fee_comm = funding.get_channel_type().supports_anchor_zero_fee_commitments(); - debug_assert_eq!(is_zero_fee_comm, dust_exposure_limiting_feerate.is_none()); - if is_zero_fee_comm { - debug_assert_eq!(excess_feerate_opt, Some(0)); - } - - let extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat = excess_feerate_opt.map(|excess_feerate| { - let extra_htlc_commit_tx_fee_sat = SpecTxBuilder {}.commit_tx_fee_sat(excess_feerate, on_counterparty_tx_accepted_nondust_htlcs + 1 + on_counterparty_tx_offered_nondust_htlcs, funding.get_channel_type()); - let extra_htlc_htlc_tx_fees_sat = chan_utils::htlc_tx_fees_sat(excess_feerate, on_counterparty_tx_accepted_nondust_htlcs + 1, on_counterparty_tx_offered_nondust_htlcs, funding.get_channel_type()); - - let commit_tx_fee_sat = SpecTxBuilder {}.commit_tx_fee_sat(excess_feerate, on_counterparty_tx_accepted_nondust_htlcs + on_counterparty_tx_offered_nondust_htlcs, funding.get_channel_type()); - let htlc_tx_fees_sat = chan_utils::htlc_tx_fees_sat(excess_feerate, on_counterparty_tx_accepted_nondust_htlcs, on_counterparty_tx_offered_nondust_htlcs, funding.get_channel_type()); - - let extra_htlc_dust_exposure = on_counterparty_tx_dust_exposure_msat + (extra_htlc_commit_tx_fee_sat + extra_htlc_htlc_tx_fees_sat) * 1000; - on_counterparty_tx_dust_exposure_msat += (commit_tx_fee_sat + htlc_tx_fees_sat) * 1000; - extra_htlc_dust_exposure - }); - - HTLCStats { - pending_outbound_htlcs, - pending_inbound_htlcs_value_msat, - pending_outbound_htlcs_value_msat, - on_counterparty_tx_dust_exposure_msat, - extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat, - on_holder_tx_dust_exposure_msat, - } - } - /// Returns information on all pending inbound HTLCs. #[rustfmt::skip] pub fn get_pending_inbound_htlc_details(&self, funding: &FundingScope) -> Vec { From b725ed4fe37124f12282d0435afc7ae558ff8d33 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Sun, 24 Aug 2025 19:19:01 +0000 Subject: [PATCH 07/13] Delete `ChannelContext::next_{local, remote}_commit_tx_fee_msat` --- lightning/src/ln/channel.rs | 219 ++++-------------------------------- 1 file changed, 19 insertions(+), 200 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 0c2302724ca..620da3e249b 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -1064,13 +1064,6 @@ pub enum AnnouncementSigsState { PeerReceived, } -/// An enum indicating whether the local or remote side offered a given HTLC. -enum HTLCInitiator { - LocalOffered, - #[allow(dead_code)] - RemoteOffered, -} - /// A struct gathering data on a commitment, either local or remote. struct CommitmentData<'a> { tx: CommitmentTransaction, @@ -1090,18 +1083,6 @@ pub(crate) struct CommitmentStats { pub remote_balance_before_fee_msat: u64, } -/// Used when calculating whether we or the remote can afford an additional HTLC. -struct HTLCCandidate { - amount_msat: u64, - origin: HTLCInitiator, -} - -impl HTLCCandidate { - fn new(amount_msat: u64, origin: HTLCInitiator) -> Self { - Self { amount_msat, origin } - } -} - /// A return value enum for get_update_fulfill_htlc. See UpdateFulfillCommitFetch variants for /// description enum UpdateFulfillFetch { @@ -5603,169 +5584,6 @@ where ) } - /// Get the commitment tx fee for the local's (i.e. our) next commitment transaction based on the - /// number of pending HTLCs that are on track to be in our next commitment tx. - /// - /// Includes the `HTLCCandidate` given by `htlc` and an additional non-dust HTLC if - /// `fee_spike_buffer_htlc` is `Some`. - /// - /// The first extra HTLC is useful for determining whether we can accept a further HTLC, the - /// second allows for creating a buffer to ensure a further HTLC can always be accepted/added. - /// - /// Dust HTLCs are excluded. - #[rustfmt::skip] - fn next_local_commit_tx_fee_msat( - &self, funding: &FundingScope, htlc: HTLCCandidate, fee_spike_buffer_htlc: Option<()>, - ) -> u64 { - let context = self; - assert!(funding.is_outbound()); - - if funding.get_channel_type().supports_anchor_zero_fee_commitments() { - debug_assert_eq!(context.feerate_per_kw, 0); - debug_assert!(fee_spike_buffer_htlc.is_none()); - return 0; - } - - let (htlc_success_tx_fee_sat, htlc_timeout_tx_fee_sat) = second_stage_tx_fees_sat( - funding.get_channel_type(), context.feerate_per_kw, - ); - let real_dust_limit_success_sat = htlc_success_tx_fee_sat + context.holder_dust_limit_satoshis; - let real_dust_limit_timeout_sat = htlc_timeout_tx_fee_sat + context.holder_dust_limit_satoshis; - - let mut addl_htlcs = 0; - if fee_spike_buffer_htlc.is_some() { addl_htlcs += 1; } - match htlc.origin { - HTLCInitiator::LocalOffered => { - if htlc.amount_msat / 1000 >= real_dust_limit_timeout_sat { - addl_htlcs += 1; - } - }, - HTLCInitiator::RemoteOffered => { - if htlc.amount_msat / 1000 >= real_dust_limit_success_sat { - addl_htlcs += 1; - } - } - } - - let mut included_htlcs = 0; - for ref htlc in context.pending_inbound_htlcs.iter() { - if htlc.amount_msat / 1000 < real_dust_limit_success_sat { - continue - } - // We include LocalRemoved HTLCs here because we may still need to broadcast a commitment - // transaction including this HTLC if it times out before they RAA. - included_htlcs += 1; - } - - for ref htlc in context.pending_outbound_htlcs.iter() { - if htlc.amount_msat / 1000 < real_dust_limit_timeout_sat { - continue - } - match htlc.state { - OutboundHTLCState::LocalAnnounced {..} => included_htlcs += 1, - OutboundHTLCState::Committed => included_htlcs += 1, - OutboundHTLCState::RemoteRemoved {..} => included_htlcs += 1, - // We don't include AwaitingRemoteRevokeToRemove HTLCs because our next commitment - // transaction won't be generated until they send us their next RAA, which will mean - // dropping any HTLCs in this state. - _ => {}, - } - } - - for htlc in context.holding_cell_htlc_updates.iter() { - match htlc { - &HTLCUpdateAwaitingACK::AddHTLC { amount_msat, .. } => { - if amount_msat / 1000 < real_dust_limit_timeout_sat { - continue - } - included_htlcs += 1 - }, - _ => {}, // Don't include claims/fails that are awaiting ack, because once we get the - // ack we're guaranteed to never include them in commitment txs anymore. - } - } - - let num_htlcs = included_htlcs + addl_htlcs; - SpecTxBuilder {}.commit_tx_fee_sat(context.feerate_per_kw, num_htlcs, funding.get_channel_type()) * 1000 - } - - /// Get the commitment tx fee for the remote's next commitment transaction based on the number of - /// pending HTLCs that are on track to be in their next commitment tx - /// - /// Optionally includes the `HTLCCandidate` given by `htlc` and an additional non-dust HTLC if - /// `fee_spike_buffer_htlc` is `Some`. - /// - /// The first extra HTLC is useful for determining whether we can accept a further HTLC, the - /// second allows for creating a buffer to ensure a further HTLC can always be accepted/added. - /// - /// Dust HTLCs are excluded. - #[rustfmt::skip] - fn next_remote_commit_tx_fee_msat( - &self, funding: &FundingScope, htlc: Option, fee_spike_buffer_htlc: Option<()>, - ) -> u64 { - let context = self; - assert!(!funding.is_outbound()); - - if funding.get_channel_type().supports_anchor_zero_fee_commitments() { - debug_assert_eq!(context.feerate_per_kw, 0); - debug_assert!(fee_spike_buffer_htlc.is_none()); - return 0 - } - - debug_assert!(htlc.is_some() || fee_spike_buffer_htlc.is_some(), "At least one of the options must be set"); - - let (htlc_success_tx_fee_sat, htlc_timeout_tx_fee_sat) = second_stage_tx_fees_sat( - funding.get_channel_type(), context.feerate_per_kw, - ); - let real_dust_limit_success_sat = htlc_success_tx_fee_sat + context.counterparty_dust_limit_satoshis; - let real_dust_limit_timeout_sat = htlc_timeout_tx_fee_sat + context.counterparty_dust_limit_satoshis; - - let mut addl_htlcs = 0; - if fee_spike_buffer_htlc.is_some() { addl_htlcs += 1; } - if let Some(htlc) = &htlc { - match htlc.origin { - HTLCInitiator::LocalOffered => { - if htlc.amount_msat / 1000 >= real_dust_limit_success_sat { - addl_htlcs += 1; - } - }, - HTLCInitiator::RemoteOffered => { - if htlc.amount_msat / 1000 >= real_dust_limit_timeout_sat { - addl_htlcs += 1; - } - } - } - } - - // When calculating the set of HTLCs which will be included in their next commitment_signed, all - // non-dust inbound HTLCs are included (as all states imply it will be included) and only - // committed outbound HTLCs, see below. - let mut included_htlcs = 0; - for ref htlc in context.pending_inbound_htlcs.iter() { - if htlc.amount_msat / 1000 < real_dust_limit_timeout_sat { - continue - } - included_htlcs += 1; - } - - for ref htlc in context.pending_outbound_htlcs.iter() { - if htlc.amount_msat / 1000 < real_dust_limit_success_sat { - continue - } - // We only include outbound HTLCs if it will not be included in their next commitment_signed, - // i.e. if they've responded to us with an RAA after announcement. - match htlc.state { - OutboundHTLCState::Committed => included_htlcs += 1, - OutboundHTLCState::RemoteRemoved {..} => included_htlcs += 1, - OutboundHTLCState::LocalAnnounced { .. } => included_htlcs += 1, - _ => {}, - } - } - - let num_htlcs = included_htlcs + addl_htlcs; - SpecTxBuilder {}.commit_tx_fee_sat(context.feerate_per_kw, num_htlcs, funding.get_channel_type()) * 1000 - } - #[rustfmt::skip] fn if_unbroadcasted_funding(&self, f: F) -> Option where F: Fn() -> Option { match self.channel_state { @@ -15619,9 +15437,9 @@ mod tests { use crate::chain::BestBlock; use crate::ln::chan_utils::{self, commit_tx_fee_sat, ChannelTransactionParameters}; use crate::ln::channel::{ - AwaitingChannelReadyFlags, ChannelState, FundedChannel, HTLCCandidate, HTLCInitiator, - HTLCUpdateAwaitingACK, InboundHTLCOutput, InboundHTLCState, InboundV1Channel, - OutboundHTLCOutput, OutboundHTLCState, OutboundV1Channel, + AwaitingChannelReadyFlags, ChannelState, FundedChannel, HTLCUpdateAwaitingACK, + InboundHTLCOutput, InboundHTLCState, InboundV1Channel, OutboundHTLCOutput, + OutboundHTLCState, OutboundV1Channel, }; use crate::ln::channel::{ MAX_FUNDING_SATOSHIS_NO_WUMBO, MIN_THEIR_CHAN_RESERVE_SATOSHIS, @@ -15636,6 +15454,7 @@ mod tests { use crate::ln::script::ShutdownScript; use crate::prelude::*; use crate::routing::router::{Path, RouteHop}; + use crate::sign::tx_builder::HTLCAmountDirection; #[cfg(ldk_test_vectors)] use crate::sign::{ChannelSigner, EntropySource, InMemorySigner, SignerProvider}; use crate::sync::Mutex; @@ -15832,7 +15651,7 @@ mod tests { // Create Node A's channel pointing to Node B's pubkey let node_b_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let config = UserConfig::default(); - let mut node_a_chan = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_init_features(&config), 10000000, 100000, 42, &config, 0, 42, None, &logger).unwrap(); + let mut node_a_chan = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_init_features(&config), 10_000_000, 100_000_000, 42, &config, 0, 42, None, &logger).unwrap(); // Create Node B's channel by receiving Node A's open_channel message // Make sure A's dust limit is as we expect. @@ -15890,8 +15709,8 @@ mod tests { // Make sure when Node A calculates their local commitment transaction, none of the HTLCs pass // the dust limit check. - let htlc_candidate = HTLCCandidate::new(htlc_amount_msat, HTLCInitiator::LocalOffered); - let local_commit_tx_fee = node_a_chan.context.next_local_commit_tx_fee_msat(&node_a_chan.funding, htlc_candidate, None); + let htlc_candidate = HTLCAmountDirection { amount_msat: htlc_amount_msat, outbound: true }; + let local_commit_tx_fee = node_a_chan.context.get_next_local_commitment_stats(&node_a_chan.funding, Some(htlc_candidate), true, 0, node_a_chan.context.feerate_per_kw, None).unwrap().commit_tx_fee_sat * 1000; let local_commit_fee_0_htlcs = commit_tx_fee_sat(node_a_chan.context.feerate_per_kw, 0, node_a_chan.funding.get_channel_type()) * 1000; assert_eq!(local_commit_tx_fee, local_commit_fee_0_htlcs); @@ -15899,15 +15718,15 @@ mod tests { // of the HTLCs are seen to be above the dust limit. node_a_chan.funding.channel_transaction_parameters.is_outbound_from_holder = false; let remote_commit_fee_3_htlcs = commit_tx_fee_sat(node_a_chan.context.feerate_per_kw, 3, node_a_chan.funding.get_channel_type()) * 1000; - let htlc_candidate = HTLCCandidate::new(htlc_amount_msat, HTLCInitiator::LocalOffered); - let remote_commit_tx_fee = node_a_chan.context.next_remote_commit_tx_fee_msat(&node_a_chan.funding, Some(htlc_candidate), None); + let htlc_candidate = HTLCAmountDirection { amount_msat: htlc_amount_msat, outbound: true }; + let remote_commit_tx_fee = node_a_chan.context.get_next_remote_commitment_stats(&node_a_chan.funding, Some(htlc_candidate), true, 0, node_a_chan.context.feerate_per_kw, None).unwrap().commit_tx_fee_sat * 1000; assert_eq!(remote_commit_tx_fee, remote_commit_fee_3_htlcs); } #[test] #[rustfmt::skip] fn test_timeout_vs_success_htlc_dust_limit() { - // Make sure that when `next_remote_commit_tx_fee_msat` and `next_local_commit_tx_fee_msat` + // Make sure that when `get_next_local/remote_commitment_stats` // calculate the real dust limits for HTLCs (i.e. the dust limit given by the counterparty // *plus* the fees paid for the HTLC) they don't swap `HTLC_SUCCESS_TX_WEIGHT` for // `HTLC_TIMEOUT_TX_WEIGHT`, and vice versa. @@ -15921,7 +15740,7 @@ mod tests { let node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let config = UserConfig::default(); - let mut chan = OutboundV1Channel::<&TestKeysInterface>::new(&fee_est, &&keys_provider, &&keys_provider, node_id, &channelmanager::provided_init_features(&config), 10000000, 100000, 42, &config, 0, 42, None, &logger).unwrap(); + let mut chan = OutboundV1Channel::<&TestKeysInterface>::new(&fee_est, &&keys_provider, &&keys_provider, node_id, &channelmanager::provided_init_features(&config), 10_000_000, 100_000_000, 42, &config, 0, 42, None, &logger).unwrap(); let commitment_tx_fee_0_htlcs = commit_tx_fee_sat(chan.context.feerate_per_kw, 0, chan.funding.get_channel_type()) * 1000; let commitment_tx_fee_1_htlc = commit_tx_fee_sat(chan.context.feerate_per_kw, 1, chan.funding.get_channel_type()) * 1000; @@ -15932,28 +15751,28 @@ mod tests { // If HTLC_SUCCESS_TX_WEIGHT and HTLC_TIMEOUT_TX_WEIGHT were swapped: then this HTLC would be // counted as dust when it shouldn't be. let htlc_amt_above_timeout = (htlc_timeout_tx_fee_sat + chan.context.holder_dust_limit_satoshis + 1) * 1000; - let htlc_candidate = HTLCCandidate::new(htlc_amt_above_timeout, HTLCInitiator::LocalOffered); - let commitment_tx_fee = chan.context.next_local_commit_tx_fee_msat(&chan.funding, htlc_candidate, None); + let htlc_candidate = HTLCAmountDirection { amount_msat: htlc_amt_above_timeout, outbound: true }; + let commitment_tx_fee = chan.context.get_next_local_commitment_stats(&chan.funding, Some(htlc_candidate), true, 0, chan.context.feerate_per_kw, None).unwrap().commit_tx_fee_sat * 1000; assert_eq!(commitment_tx_fee, commitment_tx_fee_1_htlc); // If swapped: this HTLC would be counted as non-dust when it shouldn't be. let dust_htlc_amt_below_success = (htlc_success_tx_fee_sat + chan.context.holder_dust_limit_satoshis - 1) * 1000; - let htlc_candidate = HTLCCandidate::new(dust_htlc_amt_below_success, HTLCInitiator::RemoteOffered); - let commitment_tx_fee = chan.context.next_local_commit_tx_fee_msat(&chan.funding, htlc_candidate, None); + let htlc_candidate = HTLCAmountDirection { amount_msat: dust_htlc_amt_below_success, outbound: false }; + let commitment_tx_fee = chan.context.get_next_local_commitment_stats(&chan.funding, Some(htlc_candidate), true, 0, chan.context.feerate_per_kw, None).unwrap().commit_tx_fee_sat * 1000; assert_eq!(commitment_tx_fee, commitment_tx_fee_0_htlcs); chan.funding.channel_transaction_parameters.is_outbound_from_holder = false; // If swapped: this HTLC would be counted as non-dust when it shouldn't be. let dust_htlc_amt_above_timeout = (htlc_timeout_tx_fee_sat + chan.context.counterparty_dust_limit_satoshis + 1) * 1000; - let htlc_candidate = HTLCCandidate::new(dust_htlc_amt_above_timeout, HTLCInitiator::LocalOffered); - let commitment_tx_fee = chan.context.next_remote_commit_tx_fee_msat(&chan.funding, Some(htlc_candidate), None); + let htlc_candidate = HTLCAmountDirection { amount_msat: dust_htlc_amt_above_timeout, outbound: true }; + let commitment_tx_fee = chan.context.get_next_remote_commitment_stats(&chan.funding, Some(htlc_candidate), false, 0, chan.context.feerate_per_kw, None).unwrap().commit_tx_fee_sat * 1000; assert_eq!(commitment_tx_fee, commitment_tx_fee_0_htlcs); // If swapped: this HTLC would be counted as dust when it shouldn't be. let htlc_amt_below_success = (htlc_success_tx_fee_sat + chan.context.counterparty_dust_limit_satoshis - 1) * 1000; - let htlc_candidate = HTLCCandidate::new(htlc_amt_below_success, HTLCInitiator::RemoteOffered); - let commitment_tx_fee = chan.context.next_remote_commit_tx_fee_msat(&chan.funding, Some(htlc_candidate), None); + let htlc_candidate = HTLCAmountDirection { amount_msat: htlc_amt_below_success, outbound: false }; + let commitment_tx_fee = chan.context.get_next_remote_commitment_stats(&chan.funding, Some(htlc_candidate), false, 0, chan.context.feerate_per_kw, None).unwrap().commit_tx_fee_sat * 1000; assert_eq!(commitment_tx_fee, commitment_tx_fee_1_htlc); } From 90f23b161e50242e2386e7447d4439f28774efd6 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Sun, 24 Aug 2025 19:52:22 +0000 Subject: [PATCH 08/13] Use `TxBuilder::get_next_commitment_stats` in `new_for_inbound_channel` --- lightning/src/ln/channel.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 620da3e249b..6d41b8a54a0 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -3467,14 +3467,17 @@ where // check if the funder's amount for the initial commitment tx is sufficient // for full fee payment plus a few HTLCs to ensure the channel will be useful. let funders_amount_msat = open_channel_fields.funding_satoshis * 1000 - msg_push_msat; - let commit_tx_fee_sat = SpecTxBuilder {}.commit_tx_fee_sat(open_channel_fields.commitment_feerate_sat_per_1000_weight, MIN_AFFORDABLE_HTLC_COUNT, &channel_type); - // Subtract any non-HTLC outputs from the remote balance - let (_, remote_balance_before_fee_msat) = SpecTxBuilder {}.subtract_non_htlc_outputs(false, value_to_self_msat, funders_amount_msat, &channel_type); - if remote_balance_before_fee_msat / 1000 < commit_tx_fee_sat { - return Err(ChannelError::close(format!("Funding amount ({} sats) can't even pay fee for initial commitment transaction fee of {} sats.", funders_amount_msat / 1000, commit_tx_fee_sat))); + let local = false; + let is_outbound_from_holder = false; + // We are not interested in dust exposure + let dust_exposure_limiting_feerate = None; + let remote_stats = SpecTxBuilder {}.get_next_commitment_stats(local, is_outbound_from_holder, open_channel_fields.funding_satoshis, msg_push_msat, &[], MIN_AFFORDABLE_HTLC_COUNT, + open_channel_fields.commitment_feerate_sat_per_1000_weight, dust_exposure_limiting_feerate, open_channel_fields.dust_limit_satoshis, &channel_type).map_err(|()| ChannelError::close(format!("Funding amount ({} sats) can't even pay for non-HTLC outputs ie anchors.", funders_amount_msat / 1000)))?; + if remote_stats.counterparty_balance_before_fee_msat / 1000 < remote_stats.commit_tx_fee_sat { + return Err(ChannelError::close(format!("Funding amount ({} sats) can't even pay fee for initial commitment transaction fee of {} sats.", funders_amount_msat / 1000, remote_stats.commit_tx_fee_sat))); } - let to_remote_satoshis = remote_balance_before_fee_msat / 1000 - commit_tx_fee_sat; + let to_remote_satoshis = remote_stats.counterparty_balance_before_fee_msat / 1000 - remote_stats.commit_tx_fee_sat; // While it's reasonable for us to not meet the channel reserve initially (if they don't // want to push much to us), our counterparty should always have more than our reserve. if to_remote_satoshis < holder_selected_channel_reserve_satoshis { @@ -3550,7 +3553,7 @@ where channel_transaction_parameters: ChannelTransactionParameters { holder_pubkeys: pubkeys, holder_selected_contest_delay: config.channel_handshake_config.our_to_self_delay, - is_outbound_from_holder: false, + is_outbound_from_holder, counterparty_parameters: Some(CounterpartyChannelTransactionParameters { selected_contest_delay: open_channel_fields.to_self_delay, pubkeys: counterparty_pubkeys, From 0e9fa1ace22d0f98fde9827353917a68d58aabbc Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Sun, 24 Aug 2025 20:01:06 +0000 Subject: [PATCH 09/13] Use `TxBuilder::get_next_commitment_stats` in `new_for_outbound_channel` --- lightning/src/ln/channel.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 6d41b8a54a0..ce6a6a76ccd 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -3739,16 +3739,15 @@ where ); let value_to_self_msat = channel_value_satoshis * 1000 - push_msat; - let commit_tx_fee_sat = SpecTxBuilder {}.commit_tx_fee_sat(commitment_feerate, MIN_AFFORDABLE_HTLC_COUNT, &channel_type); - // Subtract any non-HTLC outputs from the local balance - let (local_balance_before_fee_msat, _) = SpecTxBuilder {}.subtract_non_htlc_outputs( - true, - value_to_self_msat, - push_msat, - &channel_type, - ); - if local_balance_before_fee_msat / 1000 < commit_tx_fee_sat { - return Err(APIError::APIMisuseError{ err: format!("Funding amount ({}) can't even pay fee for initial commitment transaction fee of {}.", value_to_self_msat / 1000, commit_tx_fee_sat) }); + let local = true; + let is_outbound_from_holder = true; + let value_to_holder_msat = channel_value_msat - push_msat; + // We are not interested in dust exposure + let dust_exposure_limiting_feerate = None; + let local_stats = SpecTxBuilder {}.get_next_commitment_stats(local, is_outbound_from_holder, channel_value_satoshis, value_to_holder_msat, &[], MIN_AFFORDABLE_HTLC_COUNT, + commitment_feerate, dust_exposure_limiting_feerate, MIN_CHAN_DUST_LIMIT_SATOSHIS, &channel_type).map_err(|()| APIError::APIMisuseError { err: format!("Funding amount ({} sats) can't even pay for non-HTLC outputs ie anchors.", value_to_self_msat / 1000)})?; + if local_stats.holder_balance_before_fee_msat / 1000 < local_stats.commit_tx_fee_sat { + return Err(APIError::APIMisuseError{ err: format!("Funding amount ({}) can't even pay fee for initial commitment transaction fee of {}.", value_to_self_msat / 1000, local_stats.commit_tx_fee_sat) }); } let mut secp_ctx = Secp256k1::new(); @@ -3796,7 +3795,7 @@ where channel_transaction_parameters: ChannelTransactionParameters { holder_pubkeys: pubkeys, holder_selected_contest_delay: config.channel_handshake_config.our_to_self_delay, - is_outbound_from_holder: true, + is_outbound_from_holder, counterparty_parameters: None, funding_outpoint: None, splice_parent_funding_txid: None, From b62e96ebc7f5c89118a81660bbfe9dacaa785179 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Sun, 24 Aug 2025 20:08:28 +0000 Subject: [PATCH 10/13] Delete `TxBuilder::subtract_non_htlc_outputs` --- lightning/src/sign/tx_builder.rs | 47 ++++---------------------------- 1 file changed, 6 insertions(+), 41 deletions(-) diff --git a/lightning/src/sign/tx_builder.rs b/lightning/src/sign/tx_builder.rs index 703df3d6d7d..906745e787f 100644 --- a/lightning/src/sign/tx_builder.rs +++ b/lightning/src/sign/tx_builder.rs @@ -186,10 +186,6 @@ pub(crate) trait TxBuilder { fn commit_tx_fee_sat( &self, feerate_per_kw: u32, nondust_htlc_count: usize, channel_type: &ChannelTypeFeatures, ) -> u64; - fn subtract_non_htlc_outputs( - &self, is_outbound_from_holder: bool, value_to_self_after_htlcs: u64, - value_to_remote_after_htlcs: u64, channel_type: &ChannelTypeFeatures, - ) -> (u64, u64); fn build_commitment_transaction( &self, local: bool, commitment_number: u64, per_commitment_point: &PublicKey, channel_parameters: &ChannelTransactionParameters, secp_ctx: &Secp256k1, @@ -443,36 +439,6 @@ impl TxBuilder for SpecTxBuilder { ) -> u64 { commit_tx_fee_sat(feerate_per_kw, nondust_htlc_count, channel_type) } - fn subtract_non_htlc_outputs( - &self, is_outbound_from_holder: bool, value_to_self_after_htlcs: u64, - value_to_remote_after_htlcs: u64, channel_type: &ChannelTypeFeatures, - ) -> (u64, u64) { - let total_anchors_sat = if channel_type.supports_anchors_zero_fee_htlc_tx() { - ANCHOR_OUTPUT_VALUE_SATOSHI * 2 - } else { - 0 - }; - - let mut local_balance_before_fee_msat = value_to_self_after_htlcs; - let mut remote_balance_before_fee_msat = value_to_remote_after_htlcs; - - // We MUST use saturating subs here, as the funder's balance is not guaranteed to be greater - // than or equal to `total_anchors_sat`. - // - // This is because when the remote party sends an `update_fee` message, we build the new - // commitment transaction *before* checking whether the remote party's balance is enough to - // cover the total anchor sum. - - if is_outbound_from_holder { - local_balance_before_fee_msat = - local_balance_before_fee_msat.saturating_sub(total_anchors_sat * 1000); - } else { - remote_balance_before_fee_msat = - remote_balance_before_fee_msat.saturating_sub(total_anchors_sat * 1000); - } - - (local_balance_before_fee_msat, remote_balance_before_fee_msat) - } fn build_commitment_transaction( &self, local: bool, commitment_number: u64, per_commitment_point: &PublicKey, channel_parameters: &ChannelTransactionParameters, secp_ctx: &Secp256k1, @@ -541,13 +507,12 @@ impl TxBuilder for SpecTxBuilder { .unwrap() .checked_sub(remote_htlc_total_msat) .unwrap(); - let (local_balance_before_fee_msat, remote_balance_before_fee_msat) = self - .subtract_non_htlc_outputs( - channel_parameters.is_outbound_from_holder, - value_to_self_after_htlcs_msat, - value_to_remote_after_htlcs_msat, - &channel_parameters.channel_type_features, - ); + let (local_balance_before_fee_msat, remote_balance_before_fee_msat) = subtract_addl_outputs( + channel_parameters.is_outbound_from_holder, + value_to_self_after_htlcs_msat, + value_to_remote_after_htlcs_msat, + &channel_parameters.channel_type_features + ).unwrap(); // We MUST use saturating subs here, as the funder's balance is not guaranteed to be greater // than or equal to `commit_tx_fee_sat`. From c3e6c43bf5f8d1c829a75cc4b68b7c4098da9e0c Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Sun, 24 Aug 2025 20:10:32 +0000 Subject: [PATCH 11/13] Delete `TxBuilder::commit_tx_fee_sat` --- lightning/src/sign/tx_builder.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/lightning/src/sign/tx_builder.rs b/lightning/src/sign/tx_builder.rs index 906745e787f..760d83c6afb 100644 --- a/lightning/src/sign/tx_builder.rs +++ b/lightning/src/sign/tx_builder.rs @@ -183,9 +183,6 @@ pub(crate) trait TxBuilder { dust_exposure_limiting_feerate: Option, broadcaster_dust_limit_satoshis: u64, channel_type: &ChannelTypeFeatures, ) -> Result; - fn commit_tx_fee_sat( - &self, feerate_per_kw: u32, nondust_htlc_count: usize, channel_type: &ChannelTypeFeatures, - ) -> u64; fn build_commitment_transaction( &self, local: bool, commitment_number: u64, per_commitment_point: &PublicKey, channel_parameters: &ChannelTransactionParameters, secp_ctx: &Secp256k1, @@ -434,11 +431,6 @@ impl TxBuilder for SpecTxBuilder { extra_accepted_htlc_dust_exposure_msat, }) } - fn commit_tx_fee_sat( - &self, feerate_per_kw: u32, nondust_htlc_count: usize, channel_type: &ChannelTypeFeatures, - ) -> u64 { - commit_tx_fee_sat(feerate_per_kw, nondust_htlc_count, channel_type) - } fn build_commitment_transaction( &self, local: bool, commitment_number: u64, per_commitment_point: &PublicKey, channel_parameters: &ChannelTransactionParameters, secp_ctx: &Secp256k1, @@ -495,7 +487,7 @@ impl TxBuilder for SpecTxBuilder { // The value going to each party MUST be 0 or positive, even if all HTLCs pending in the // commitment clear by failure. - let commit_tx_fee_sat = self.commit_tx_fee_sat( + let commit_tx_fee_sat = commit_tx_fee_sat( feerate_per_kw, htlcs_in_tx.len(), &channel_parameters.channel_type_features, From d4cbf7d009c11f389150e80cd5ad032964f28154 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 10 Dec 2025 01:31:36 +0000 Subject: [PATCH 12/13] fmt --- lightning/src/ln/channel.rs | 8 +- lightning/src/sign/tx_builder.rs | 193 ++++++++++++++++++++----------- 2 files changed, 134 insertions(+), 67 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index ce6a6a76ccd..881d8932b85 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -73,7 +73,9 @@ use crate::ln::LN_MAX_MSG_LEN; use crate::offers::static_invoice::StaticInvoice; use crate::routing::gossip::NodeId; use crate::sign::ecdsa::EcdsaChannelSigner; -use crate::sign::tx_builder::{HTLCAmountDirection, ChannelConstraints, NextCommitmentStats, SpecTxBuilder, TxBuilder}; +use crate::sign::tx_builder::{ + ChannelConstraints, HTLCAmountDirection, NextCommitmentStats, SpecTxBuilder, TxBuilder, +}; use crate::sign::{ChannelSigner, EntropySource, NodeSigner, Recipient, SignerProvider}; use crate::types::features::{ChannelTypeFeatures, InitFeatures}; use crate::types::payment::{PaymentHash, PaymentPreimage}; @@ -5530,7 +5532,9 @@ where fn get_holder_channel_constraints(&self, funding: &FundingScope) -> ChannelConstraints { ChannelConstraints { dust_limit_satoshis: self.holder_dust_limit_satoshis, - channel_reserve_satoshis: funding.counterparty_selected_channel_reserve_satoshis.unwrap_or(0), + channel_reserve_satoshis: funding + .counterparty_selected_channel_reserve_satoshis + .unwrap_or(0), htlc_minimum_msat: self.holder_htlc_minimum_msat, max_accepted_htlcs: self.holder_max_accepted_htlcs as u64, max_htlc_value_in_flight_msat: self.holder_max_htlc_value_in_flight_msat, diff --git a/lightning/src/sign/tx_builder.rs b/lightning/src/sign/tx_builder.rs index 760d83c6afb..623a2b6d219 100644 --- a/lightning/src/sign/tx_builder.rs +++ b/lightning/src/sign/tx_builder.rs @@ -164,17 +164,11 @@ pub(crate) struct ChannelConstraints { pub(crate) trait TxBuilder { fn get_available_balances( - &self, - is_outbound_from_holder: bool, - channel_value_satoshis: u64, - value_to_holder_msat: u64, - pending_htlcs: &[HTLCAmountDirection], - feerate_per_kw: u32, - dust_exposure_limiting_feerate: Option, - max_dust_htlc_exposure_msat: u64, + &self, is_outbound_from_holder: bool, channel_value_satoshis: u64, + value_to_holder_msat: u64, pending_htlcs: &[HTLCAmountDirection], feerate_per_kw: u32, + dust_exposure_limiting_feerate: Option, max_dust_htlc_exposure_msat: u64, holder_channel_constraints: ChannelConstraints, - counterparty_channel_constraints: ChannelConstraints, - channel_type: &ChannelTypeFeatures, + counterparty_channel_constraints: ChannelConstraints, channel_type: &ChannelTypeFeatures, ) -> crate::ln::channel::AvailableBalances; fn get_next_commitment_stats( &self, local: bool, is_outbound_from_holder: bool, channel_value_satoshis: u64, @@ -197,34 +191,65 @@ pub(crate) struct SpecTxBuilder {} impl TxBuilder for SpecTxBuilder { fn get_available_balances( - &self, - is_outbound_from_holder: bool, - channel_value_satoshis: u64, - value_to_holder_msat: u64, - pending_htlcs: &[HTLCAmountDirection], - feerate_per_kw: u32, - dust_exposure_limiting_feerate: Option, - max_dust_htlc_exposure_msat: u64, + &self, is_outbound_from_holder: bool, channel_value_satoshis: u64, + value_to_holder_msat: u64, pending_htlcs: &[HTLCAmountDirection], feerate_per_kw: u32, + dust_exposure_limiting_feerate: Option, max_dust_htlc_exposure_msat: u64, holder_channel_constraints: ChannelConstraints, - counterparty_channel_constraints: ChannelConstraints, - channel_type: &ChannelTypeFeatures, + counterparty_channel_constraints: ChannelConstraints, channel_type: &ChannelTypeFeatures, ) -> crate::ln::channel::AvailableBalances { - let fee_spike_buffer_htlc = if channel_type.supports_anchor_zero_fee_commitments() { - 0 - } else { - 1 - }; + let fee_spike_buffer_htlc = + if channel_type.supports_anchor_zero_fee_commitments() { 0 } else { 1 }; - let local_stats_max_fee = SpecTxBuilder {}.get_next_commitment_stats(true, is_outbound_from_holder, channel_value_satoshis, value_to_holder_msat, pending_htlcs, fee_spike_buffer_htlc + 1, feerate_per_kw, dust_exposure_limiting_feerate, holder_channel_constraints.dust_limit_satoshis, channel_type).unwrap(); - let local_stats_min_fee = SpecTxBuilder {}.get_next_commitment_stats(true, is_outbound_from_holder, channel_value_satoshis, value_to_holder_msat, pending_htlcs, fee_spike_buffer_htlc, feerate_per_kw, dust_exposure_limiting_feerate, holder_channel_constraints.dust_limit_satoshis, channel_type).unwrap(); - let remote_stats = SpecTxBuilder {}.get_next_commitment_stats(false, is_outbound_from_holder, channel_value_satoshis, value_to_holder_msat, pending_htlcs, 1, feerate_per_kw, dust_exposure_limiting_feerate, counterparty_channel_constraints.dust_limit_satoshis, channel_type).unwrap(); + let local_stats_max_fee = SpecTxBuilder {} + .get_next_commitment_stats( + true, + is_outbound_from_holder, + channel_value_satoshis, + value_to_holder_msat, + pending_htlcs, + fee_spike_buffer_htlc + 1, + feerate_per_kw, + dust_exposure_limiting_feerate, + holder_channel_constraints.dust_limit_satoshis, + channel_type, + ) + .unwrap(); + let local_stats_min_fee = SpecTxBuilder {} + .get_next_commitment_stats( + true, + is_outbound_from_holder, + channel_value_satoshis, + value_to_holder_msat, + pending_htlcs, + fee_spike_buffer_htlc, + feerate_per_kw, + dust_exposure_limiting_feerate, + holder_channel_constraints.dust_limit_satoshis, + channel_type, + ) + .unwrap(); + let remote_stats = SpecTxBuilder {} + .get_next_commitment_stats( + false, + is_outbound_from_holder, + channel_value_satoshis, + value_to_holder_msat, + pending_htlcs, + 1, + feerate_per_kw, + dust_exposure_limiting_feerate, + counterparty_channel_constraints.dust_limit_satoshis, + channel_type, + ) + .unwrap(); - let outbound_capacity_msat = local_stats_max_fee.holder_balance_before_fee_msat.saturating_sub(holder_channel_constraints.channel_reserve_satoshis * 1000); + let outbound_capacity_msat = local_stats_max_fee + .holder_balance_before_fee_msat + .saturating_sub(holder_channel_constraints.channel_reserve_satoshis * 1000); let mut available_capacity_msat = outbound_capacity_msat; - let (real_htlc_success_tx_fee_sat, real_htlc_timeout_tx_fee_sat) = second_stage_tx_fees_sat( - channel_type, feerate_per_kw - ); + let (real_htlc_success_tx_fee_sat, real_htlc_timeout_tx_fee_sat) = + second_stage_tx_fees_sat(channel_type, feerate_per_kw); if is_outbound_from_holder { // We should mind channel commit tx fee when computing how much of the available capacity @@ -235,40 +260,54 @@ impl TxBuilder for SpecTxBuilder { // dependency. // This complicates the computation around dust-values, up to the one-htlc-value. - let real_dust_limit_timeout_sat = real_htlc_timeout_tx_fee_sat + holder_channel_constraints.dust_limit_satoshis; + let real_dust_limit_timeout_sat = + real_htlc_timeout_tx_fee_sat + holder_channel_constraints.dust_limit_satoshis; let mut max_reserved_commit_tx_fee_msat = local_stats_max_fee.commit_tx_fee_sat * 1000; let mut min_reserved_commit_tx_fee_msat = local_stats_min_fee.commit_tx_fee_sat * 1000; if !channel_type.supports_anchors_zero_fee_htlc_tx() { - max_reserved_commit_tx_fee_msat *= crate::ln::channel::FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE; - min_reserved_commit_tx_fee_msat *= crate::ln::channel::FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE; + max_reserved_commit_tx_fee_msat *= + crate::ln::channel::FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE; + min_reserved_commit_tx_fee_msat *= + crate::ln::channel::FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE; } // We will first subtract the fee as if we were above-dust. Then, if the resulting // value ends up being below dust, we have this fee available again. In that case, // match the value to right-below-dust. - let capacity_minus_max_commitment_fee_msat = available_capacity_msat.saturating_sub(max_reserved_commit_tx_fee_msat); + let capacity_minus_max_commitment_fee_msat = + available_capacity_msat.saturating_sub(max_reserved_commit_tx_fee_msat); if capacity_minus_max_commitment_fee_msat < real_dust_limit_timeout_sat * 1000 { - let capacity_minus_min_commitment_fee_msat = available_capacity_msat.saturating_sub(min_reserved_commit_tx_fee_msat); - available_capacity_msat = cmp::min(real_dust_limit_timeout_sat * 1000 - 1, capacity_minus_min_commitment_fee_msat); + let capacity_minus_min_commitment_fee_msat = + available_capacity_msat.saturating_sub(min_reserved_commit_tx_fee_msat); + available_capacity_msat = cmp::min( + real_dust_limit_timeout_sat * 1000 - 1, + capacity_minus_min_commitment_fee_msat, + ); } else { available_capacity_msat = capacity_minus_max_commitment_fee_msat; } } else { // If the channel is inbound (i.e. counterparty pays the fee), we need to make sure // sending a new HTLC won't reduce their balance below our reserve threshold. - let real_dust_limit_success_sat = real_htlc_success_tx_fee_sat + counterparty_channel_constraints.dust_limit_satoshis; + let real_dust_limit_success_sat = + real_htlc_success_tx_fee_sat + counterparty_channel_constraints.dust_limit_satoshis; let max_reserved_commit_tx_fee_msat = remote_stats.commit_tx_fee_sat * 1000; - let holder_selected_chan_reserve_msat = counterparty_channel_constraints.channel_reserve_satoshis * 1000; - if remote_stats.counterparty_balance_before_fee_msat < max_reserved_commit_tx_fee_msat + holder_selected_chan_reserve_msat { + let holder_selected_chan_reserve_msat = + counterparty_channel_constraints.channel_reserve_satoshis * 1000; + if remote_stats.counterparty_balance_before_fee_msat + < max_reserved_commit_tx_fee_msat + holder_selected_chan_reserve_msat + { // If another HTLC's fee would reduce the remote's balance below the reserve limit // we've selected for them, we can only send dust HTLCs. - available_capacity_msat = cmp::min(available_capacity_msat, real_dust_limit_success_sat * 1000 - 1); + available_capacity_msat = + cmp::min(available_capacity_msat, real_dust_limit_success_sat * 1000 - 1); } } - let mut next_outbound_htlc_minimum_msat = counterparty_channel_constraints.htlc_minimum_msat; + let mut next_outbound_htlc_minimum_msat = + counterparty_channel_constraints.htlc_minimum_msat; // If we get close to our maximum dust exposure, we end up in a situation where we can send // between zero and the remaining dust exposure limit remaining OR above the dust limit. @@ -278,50 +317,72 @@ impl TxBuilder for SpecTxBuilder { let mut dust_exposure_dust_limit_msat = 0; let dust_buffer_feerate = get_dust_buffer_feerate(feerate_per_kw); - let (buffer_htlc_success_tx_fee_sat, buffer_htlc_timeout_tx_fee_sat) = second_stage_tx_fees_sat( - channel_type, dust_buffer_feerate, - ); - let buffer_dust_limit_success_sat = buffer_htlc_success_tx_fee_sat + counterparty_channel_constraints.dust_limit_satoshis; - let buffer_dust_limit_timeout_sat = buffer_htlc_timeout_tx_fee_sat + holder_channel_constraints.dust_limit_satoshis; + let (buffer_htlc_success_tx_fee_sat, buffer_htlc_timeout_tx_fee_sat) = + second_stage_tx_fees_sat(channel_type, dust_buffer_feerate); + let buffer_dust_limit_success_sat = + buffer_htlc_success_tx_fee_sat + counterparty_channel_constraints.dust_limit_satoshis; + let buffer_dust_limit_timeout_sat = + buffer_htlc_timeout_tx_fee_sat + holder_channel_constraints.dust_limit_satoshis; if remote_stats.extra_accepted_htlc_dust_exposure_msat > max_dust_htlc_exposure_msat { // If adding an extra HTLC would put us over the dust limit in total fees, we cannot // send any non-dust HTLCs. - available_capacity_msat = cmp::min(available_capacity_msat, buffer_dust_limit_success_sat * 1000); + available_capacity_msat = + cmp::min(available_capacity_msat, buffer_dust_limit_success_sat * 1000); } - if remote_stats.dust_exposure_msat.saturating_add(buffer_dust_limit_success_sat * 1000) > max_dust_htlc_exposure_msat.saturating_add(1) { + if remote_stats.dust_exposure_msat.saturating_add(buffer_dust_limit_success_sat * 1000) + > max_dust_htlc_exposure_msat.saturating_add(1) + { // Note that we don't use the `counterparty_tx_dust_exposure` (with // `htlc_dust_exposure_msat`) here as it only applies to non-dust HTLCs. remaining_msat_below_dust_exposure_limit = Some(max_dust_htlc_exposure_msat.saturating_sub(remote_stats.dust_exposure_msat)); - dust_exposure_dust_limit_msat = cmp::max(dust_exposure_dust_limit_msat, buffer_dust_limit_success_sat * 1000); + dust_exposure_dust_limit_msat = + cmp::max(dust_exposure_dust_limit_msat, buffer_dust_limit_success_sat * 1000); } - if local_stats_max_fee.dust_exposure_msat as i64 + buffer_dust_limit_timeout_sat as i64 * 1000 - 1 > max_dust_htlc_exposure_msat.try_into().unwrap_or(i64::max_value()) { + if local_stats_max_fee.dust_exposure_msat as i64 + + buffer_dust_limit_timeout_sat as i64 * 1000 + - 1 > max_dust_htlc_exposure_msat.try_into().unwrap_or(i64::max_value()) + { remaining_msat_below_dust_exposure_limit = Some(cmp::min( remaining_msat_below_dust_exposure_limit.unwrap_or(u64::max_value()), - max_dust_htlc_exposure_msat.saturating_sub(local_stats_max_fee.dust_exposure_msat))); - dust_exposure_dust_limit_msat = cmp::max(dust_exposure_dust_limit_msat, buffer_dust_limit_timeout_sat * 1000); + max_dust_htlc_exposure_msat.saturating_sub(local_stats_max_fee.dust_exposure_msat), + )); + dust_exposure_dust_limit_msat = + cmp::max(dust_exposure_dust_limit_msat, buffer_dust_limit_timeout_sat * 1000); } if let Some(remaining_limit_msat) = remaining_msat_below_dust_exposure_limit { if available_capacity_msat < dust_exposure_dust_limit_msat { available_capacity_msat = cmp::min(available_capacity_msat, remaining_limit_msat); } else { - next_outbound_htlc_minimum_msat = cmp::max(next_outbound_htlc_minimum_msat, dust_exposure_dust_limit_msat); + next_outbound_htlc_minimum_msat = + cmp::max(next_outbound_htlc_minimum_msat, dust_exposure_dust_limit_msat); } } - available_capacity_msat = cmp::min(available_capacity_msat, - counterparty_channel_constraints.max_htlc_value_in_flight_msat - pending_htlcs.iter().filter(|htlc| htlc.outbound).map(|htlc| htlc.amount_msat).sum::()); + available_capacity_msat = cmp::min( + available_capacity_msat, + counterparty_channel_constraints.max_htlc_value_in_flight_msat + - pending_htlcs + .iter() + .filter(|htlc| htlc.outbound) + .map(|htlc| htlc.amount_msat) + .sum::(), + ); - if pending_htlcs.iter().filter(|htlc| htlc.outbound).count() + 1 > counterparty_channel_constraints.max_accepted_htlcs as usize { + if pending_htlcs.iter().filter(|htlc| htlc.outbound).count() + 1 + > counterparty_channel_constraints.max_accepted_htlcs as usize + { available_capacity_msat = 0; } crate::ln::channel::AvailableBalances { - inbound_capacity_msat: remote_stats.counterparty_balance_before_fee_msat.saturating_sub(counterparty_channel_constraints.channel_reserve_satoshis * 1000), + inbound_capacity_msat: remote_stats + .counterparty_balance_before_fee_msat + .saturating_sub(counterparty_channel_constraints.channel_reserve_satoshis * 1000), outbound_capacity_msat, next_outbound_htlc_limit_msat: available_capacity_msat, next_outbound_htlc_minimum_msat, @@ -499,12 +560,14 @@ impl TxBuilder for SpecTxBuilder { .unwrap() .checked_sub(remote_htlc_total_msat) .unwrap(); - let (local_balance_before_fee_msat, remote_balance_before_fee_msat) = subtract_addl_outputs( - channel_parameters.is_outbound_from_holder, - value_to_self_after_htlcs_msat, - value_to_remote_after_htlcs_msat, - &channel_parameters.channel_type_features - ).unwrap(); + let (local_balance_before_fee_msat, remote_balance_before_fee_msat) = + subtract_addl_outputs( + channel_parameters.is_outbound_from_holder, + value_to_self_after_htlcs_msat, + value_to_remote_after_htlcs_msat, + &channel_parameters.channel_type_features, + ) + .unwrap(); // We MUST use saturating subs here, as the funder's balance is not guaranteed to be greater // than or equal to `commit_tx_fee_sat`. From f27cc1e1e50b173fd5c6fa0dac19276eee1375c7 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 10 Dec 2025 18:32:43 +0000 Subject: [PATCH 13/13] Add a question --- lightning/src/sign/tx_builder.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/lightning/src/sign/tx_builder.rs b/lightning/src/sign/tx_builder.rs index 623a2b6d219..f29188512aa 100644 --- a/lightning/src/sign/tx_builder.rs +++ b/lightning/src/sign/tx_builder.rs @@ -213,6 +213,7 @@ impl TxBuilder for SpecTxBuilder { holder_channel_constraints.dust_limit_satoshis, channel_type, ) + // TODO: should `get_available_balances` be fallible ? .unwrap(); let local_stats_min_fee = SpecTxBuilder {} .get_next_commitment_stats(