Multiply the feerate by the spike multiple in `can_accept_incoming_htlc`
What changed, and why it matters
This commit tightens how Lightning Dev Kit (LDK) checks whether an incoming payment (HTLC) can be safely accepted. It changes the fee-spike buffer calculation so that LDK now rejects more borderline HTLCs than before, reducing the risk that a sudden on-chain fee spike leaves the channel unable to close safely. The change is described by the authors as a stricter policy alignment, not a fix for a known active exploit.
Treat as a hardening/refactoring change rather than an emergency vulnerability fix. Node operators and integrators using LDK should review whether the stricter HTLC acceptance affects their channel liquidity or routing behavior, especially during high-fee regimes. No immediate patch deployment is required solely for security reasons, but staying current with the release that includes this commit is prudent.
Security signals we found
Stricter incoming-HTLC acceptance policy to account for on-chain fee spikes
Fee-spike buffer now applied to feerate rather than absolute commitment fee
Non-dust HTLCs that would become dust under multiplied feerate are now excluded from transaction weight
Reserve and fee checks simplified to use post-fee balances
Test expectations updated to match new error messages and stricter behavior
Evidence from the diff
The patch refactors can_accept_incoming_htlc and related helpers. Previously, NextCommitmentStats exposed pre-fee balances and a separate commit_tx_fee_sat, and callers subtracted fees manually. Now TxBuilder::get_channel_stats returns balances that already include the commitment transaction fee, and callers compare those post-fee balances directly against reserve requirements. The key behavioral change is in the fee-spike buffer check: instead of multiplying the commitment fee by FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, LDK now multiplies the feerate by that multiple and recomputes the commitment stats at the higher feerate. This also causes non-dust HTLCs that would become dust at the spiked feerate to be excluded from the weight calculation. As a result, LDC rejects some HTLCs that older versions would have accepted.
Changed components
lightning/src/ln/channel.rslightning/src/ln/update_fee_tests.rslightning/src/sign/tx_builder.rsInspect captured patch +128 / −157
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 1a3c32a..b038d70 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -3941,23 +3941,18 @@ impl<SP: SignerProvider> ChannelContext<SP> {
let include_counterparty_unknown_htlcs = false;
let addl_nondust_htlc_count = MIN_AFFORDABLE_HTLC_COUNT;
let dust_exposure_limiting_feerate = channel_context.get_dust_exposure_limiting_feerate(&fee_estimator, funding.get_channel_type());
- let remote_stats = channel_context.get_next_remote_commitment_stats(
+ let (remote_stats, _remote_htlcs) = channel_context.get_next_remote_commitment_stats(
&funding,
htlc_candidate,
include_counterparty_unknown_htlcs,
addl_nondust_htlc_count,
channel_context.feerate_per_kw,
dust_exposure_limiting_feerate
- ).map_err(|()| ChannelError::close(format!("Funding amount ({} sats) can't even pay fee for two anchors on the initial commitment transaction", funders_amount_msat / 1000)))?;
+ ).map_err(|()| ChannelError::close(format!("Funding amount ({} sats) can't even pay fee for initial commitment transaction.", funders_amount_msat / 1000)))?;
- if remote_stats.commitment_stats.counterparty_balance_before_fee_msat / 1000 < remote_stats.commitment_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.commitment_stats.commit_tx_fee_sat)));
- }
-
- let to_remote_satoshis = remote_stats.commitment_stats.counterparty_balance_before_fee_msat / 1000 - remote_stats.commitment_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 < funding.holder_selected_channel_reserve_satoshis {
+ if remote_stats.commitment_stats.counterparty_balance_msat / 1000 < funding.holder_selected_channel_reserve_satoshis {
return Err(ChannelError::close("Insufficient funding amount for initial reserve".to_owned()));
}
@@ -4188,18 +4183,14 @@ impl<SP: SignerProvider> ChannelContext<SP> {
let include_counterparty_unknown_htlcs = false;
let addl_nondust_htlc_count = MIN_AFFORDABLE_HTLC_COUNT;
let dust_exposure_limiting_feerate = channel_context.get_dust_exposure_limiting_feerate(&fee_estimator, funding.get_channel_type());
- let local_stats = channel_context.get_next_local_commitment_stats(
+ let _local_stats = channel_context.get_next_local_commitment_stats(
&funding,
htlc_candidate,
include_counterparty_unknown_htlcs,
addl_nondust_htlc_count,
channel_context.feerate_per_kw,
dust_exposure_limiting_feerate,
- ).map_err(|()| APIError::APIMisuseError { err: format!("Funding amount ({} sats) can't even pay fee for two anchors on the initial commitment transaction", funding.get_value_to_self_msat() / 1000)})?;
-
- if local_stats.commitment_stats.holder_balance_before_fee_msat / 1000 < local_stats.commitment_stats.commit_tx_fee_sat {
- return Err(APIError::APIMisuseError{ err: format!("Funding amount ({}) can't even pay fee for initial commitment transaction fee of {}.", funding.get_value_to_self_msat() / 1000, local_stats.commitment_stats.commit_tx_fee_sat) });
- }
+ ).map_err(|()| APIError::APIMisuseError { err: format!("Funding amount ({}) can't even pay fee for initial commitment transaction.", funding.get_value_to_self_msat() / 1000)})?;
Ok((funding, channel_context))
}
@@ -4881,7 +4872,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
&self, funding: &FundingScope, htlc_candidate: Option<HTLCAmountDirection>,
include_counterparty_unknown_htlcs: bool, addl_nondust_htlc_count: usize,
feerate_per_kw: u32, dust_exposure_limiting_feerate: Option<u32>,
- ) -> Result<ChannelStats, ()> {
+ ) -> Result<(ChannelStats, Vec<HTLCAmountDirection>), ()> {
let next_commitment_htlcs = self.get_next_commitment_htlcs(
true,
htlc_candidate,
@@ -4924,7 +4915,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
self.holder_dust_limit_satoshis,
funding.get_channel_type(),
)
- .expect("Balance after HTLCs and anchors exhausted on local commitment")
+ .expect("Balance exhausted on local commitment")
.commitment_stats;
*funding.next_local_fee.lock().unwrap() = PredictedNextFee {
predicted_feerate: feerate_per_kw,
@@ -4934,14 +4925,14 @@ impl<SP: SignerProvider> ChannelContext<SP> {
}
}
- Ok(local_stats)
+ Ok((local_stats, next_commitment_htlcs))
}
fn get_next_remote_commitment_stats(
&self, funding: &FundingScope, htlc_candidate: Option<HTLCAmountDirection>,
include_counterparty_unknown_htlcs: bool, addl_nondust_htlc_count: usize,
feerate_per_kw: u32, dust_exposure_limiting_feerate: Option<u32>,
- ) -> Result<ChannelStats, ()> {
+ ) -> Result<(ChannelStats, Vec<HTLCAmountDirection>), ()> {
let next_commitment_htlcs = self.get_next_commitment_htlcs(
false,
htlc_candidate,
@@ -4984,7 +4975,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
self.counterparty_dust_limit_satoshis,
funding.get_channel_type(),
)
- .expect("Balance after HTLCs and anchors exhausted on remote commitment")
+ .expect("Balance exhausted on remote commitment")
.commitment_stats;
*funding.next_remote_fee.lock().unwrap() = PredictedNextFee {
predicted_feerate: feerate_per_kw,
@@ -4994,7 +4985,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
}
}
- Ok(remote_stats)
+ Ok((remote_stats, next_commitment_htlcs))
}
fn validate_update_add_htlc<F: FeeEstimator>(
@@ -5014,7 +5005,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
let include_counterparty_unknown_htlcs = false;
// Don't include the extra fee spike buffer HTLC in calculations
let fee_spike_buffer_htlc = 0;
- let remote_stats = self
+ let (remote_stats, remote_htlcs) = self
.get_next_remote_commitment_stats(
funding,
Some(HTLCAmountDirection { outbound: false, amount_msat: msg.amount_msat }),
@@ -5027,17 +5018,19 @@ impl<SP: SignerProvider> ChannelContext<SP> {
ChannelError::close(String::from("Remote HTLC add would overdraw remaining funds"))
})?;
- if remote_stats.commitment_stats.inbound_htlcs_count
- > self.holder_max_accepted_htlcs as usize
- {
+ let inbound_htlcs_count = remote_htlcs.iter().filter(|htlc| !htlc.outbound).count();
+ let inbound_htlcs_value_msat: u64 = remote_htlcs
+ .iter()
+ .filter_map(|htlc| (!htlc.outbound).then_some(htlc.amount_msat))
+ .sum();
+
+ if inbound_htlcs_count > self.holder_max_accepted_htlcs as usize {
return Err(ChannelError::close(format!(
"Remote tried to push more than our max accepted HTLCs ({})",
self.holder_max_accepted_htlcs,
)));
}
- if remote_stats.commitment_stats.inbound_htlcs_value_msat
- > self.holder_max_htlc_value_in_flight_msat
- {
+ if inbound_htlcs_value_msat > self.holder_max_htlc_value_in_flight_msat {
return Err(ChannelError::close(format!(
"Remote HTLC add would put them over our max HTLC value ({})",
self.holder_max_htlc_value_in_flight_msat,
@@ -5059,33 +5052,16 @@ impl<SP: SignerProvider> ChannelContext<SP> {
// violate the reserve value if we do not do this (as we forget inbound HTLCs from the
// Channel state once they will not be present in the next received commitment
// transaction).
+ if remote_stats.commitment_stats.counterparty_balance_msat
+ < funding.holder_selected_channel_reserve_satoshis * 1000
{
- let remote_commit_tx_fee_msat = if funding.is_outbound() {
- 0
- } else {
- remote_stats.commitment_stats.commit_tx_fee_sat * 1000
- };
- if remote_stats.commitment_stats.counterparty_balance_before_fee_msat
- < remote_commit_tx_fee_msat
- {
- return Err(ChannelError::close(
- "Remote HTLC add would not leave enough to pay for fees".to_owned(),
- ));
- };
- if remote_stats
- .commitment_stats
- .counterparty_balance_before_fee_msat
- .saturating_sub(remote_commit_tx_fee_msat)
- < funding.holder_selected_channel_reserve_satoshis * 1000
- {
- return Err(ChannelError::close(
- "Remote HTLC add would put them under remote reserve value".to_owned(),
- ));
- }
+ return Err(ChannelError::close(
+ "Remote HTLC add would put them under remote reserve value".to_owned(),
+ ));
}
if funding.is_outbound() {
- let local_stats = self
+ let (local_stats, _local_htlcs) = self
.get_next_local_commitment_stats(
funding,
Some(HTLCAmountDirection { outbound: false, amount_msat: msg.amount_msat }),
@@ -5095,14 +5071,11 @@ impl<SP: SignerProvider> ChannelContext<SP> {
dust_exposure_limiting_feerate,
)
.map_err(|()| {
- ChannelError::close(String::from(
- "Balance after HTLCs and anchors exhausted on local commitment",
- ))
+ ChannelError::close(String::from("Balance exhausted on local commitment"))
})?;
// Check that they won't violate our local required channel reserve by adding this HTLC.
- if local_stats.commitment_stats.holder_balance_before_fee_msat
+ if local_stats.commitment_stats.holder_balance_msat
< funding.counterparty_selected_channel_reserve_satoshis.unwrap() * 1000
- + local_stats.commitment_stats.commit_tx_fee_sat * 1000
{
return Err(ChannelError::close(
"Cannot accept HTLC that would put our balance under counterparty-announced channel reserve value".to_owned()
@@ -5123,7 +5096,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
// Do not include outbound update_add_htlc's in the holding cell, or those which haven't yet been ACK'ed
// by the counterparty (ie. LocalAnnounced HTLCs)
let include_counterparty_unknown_htlcs = false;
- let local_stats = self
+ let (local_stats, _local_htlcs) = self
.get_next_local_commitment_stats(
funding,
None,
@@ -5133,24 +5106,18 @@ impl<SP: SignerProvider> ChannelContext<SP> {
dust_exposure_limiting_feerate,
)
.map_err(|()| {
- ChannelError::close(String::from(
- "Balance after HTLCs and anchors exhausted on local commitment",
- ))
+ ChannelError::close(String::from("Funding remote cannot afford proposed new fee"))
})?;
local_stats
.commitment_stats
- .get_holder_counterparty_balances_incl_fee_msat()
- .and_then(|(_, counterparty_balance_incl_fee_msat)| {
- counterparty_balance_incl_fee_msat
- .checked_sub(funding.holder_selected_channel_reserve_satoshis * 1000)
- .ok_or(())
- })
- .map_err(|()| {
- ChannelError::close("Funding remote cannot afford proposed new fee".to_owned())
- })?;
+ .counterparty_balance_msat
+ .checked_sub(funding.holder_selected_channel_reserve_satoshis * 1000)
+ .ok_or(ChannelError::close(
+ "Funding remote cannot afford proposed new fee".to_owned(),
+ ))?;
- let remote_stats = self
+ let (remote_stats, _remote_htlcs) = self
.get_next_remote_commitment_stats(
funding,
None,
@@ -5160,9 +5127,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
dust_exposure_limiting_feerate,
)
.map_err(|()| {
- ChannelError::close(String::from(
- "Balance after HTLCs and anchors exhausted on remote commitment",
- ))
+ ChannelError::close(String::from("Balance exhausted on remote commitment"))
})?;
let max_dust_htlc_exposure_msat =
@@ -5327,27 +5292,27 @@ impl<SP: SignerProvider> ChannelContext<SP> {
// Include outbound update_add_htlc's in the holding cell, and those which haven't yet been ACK'ed by
// the counterparty (ie. LocalAnnounced HTLCs)
let include_counterparty_unknown_htlcs = true;
- let remote_stats = if let Ok(stats) = self.get_next_remote_commitment_stats(
- funding,
- None,
- include_counterparty_unknown_htlcs,
- CONCURRENT_INBOUND_HTLC_FEE_BUFFER as usize,
- feerate_per_kw,
- dust_exposure_limiting_feerate,
- ) {
+ let (remote_stats, _remote_htlcs) = if let Ok(stats) = self
+ .get_next_remote_commitment_stats(
+ funding,
+ None,
+ include_counterparty_unknown_htlcs,
+ CONCURRENT_INBOUND_HTLC_FEE_BUFFER as usize,
+ feerate_per_kw,
+ dust_exposure_limiting_feerate,
+ ) {
stats
} else {
log_debug!(
logger,
- "Cannot afford to send new feerate due to balance after HTLCs and anchors exhausted on remote commitment",
+ "Cannot afford to send new feerate due to balance exhausted on remote commitment",
);
return false;
};
// Note that `stats.commit_tx_fee_sat` accounts for any HTLCs that transition from non-dust to dust
// under a higher feerate (in the case where HTLC-transactions pay endogenous fees).
- if remote_stats.commitment_stats.holder_balance_before_fee_msat
- < remote_stats.commitment_stats.commit_tx_fee_sat * 1000
- + funding.counterparty_selected_channel_reserve_satoshis.unwrap() * 1000
+ if remote_stats.commitment_stats.holder_balance_msat
+ < funding.counterparty_selected_channel_reserve_satoshis.unwrap() * 1000
{
//TODO: auto-close after a number of failures?
log_debug!(logger, "Cannot afford to send new feerate at {}", feerate_per_kw);
@@ -5367,7 +5332,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
return false;
}
- let local_stats = if let Ok(stats) = self.get_next_local_commitment_stats(
+ let (local_stats, _local_htlcs) = if let Ok(stats) = self.get_next_local_commitment_stats(
funding,
None,
include_counterparty_unknown_htlcs,
@@ -5379,7 +5344,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
} else {
log_debug!(
logger,
- "Cannot afford to send new feerate due to balance after HTLCs and anchors exhausted on local commitment",
+ "Cannot afford to send new feerate due to balance exhausted on local commitment",
);
return false;
};
@@ -5413,7 +5378,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
cmp::max(self.feerate_per_kw, self.pending_update_fee.map(|(fee, _)| fee).unwrap_or(0));
// A `None` `HTLCCandidate` is used as in this case because we're already accounting for
// the incoming HTLC as it has been fully committed by both sides.
- let local_stats = self
+ let (local_stats, _local_htlcs) = self
.get_next_local_commitment_stats(
funding,
None,
@@ -5423,10 +5388,13 @@ impl<SP: SignerProvider> ChannelContext<SP> {
dust_exposure_limiting_feerate,
)
.map_err(|()| {
- log_trace!(logger, "Attempting to fail HTLC due to balance after HTLCs and anchors exhausted on local commitment");
+ log_trace!(
+ logger,
+ "Attempting to fail HTLC due to balance exhausted on local commitment"
+ );
LocalHTLCFailureReason::ChannelBalanceOverdrawn
})?;
- let remote_stats = self
+ let (remote_stats, _remote_htlcs) = self
.get_next_remote_commitment_stats(
funding,
None,
@@ -5436,7 +5404,10 @@ impl<SP: SignerProvider> ChannelContext<SP> {
dust_exposure_limiting_feerate,
)
.map_err(|()| {
- log_trace!(logger, "Attempting to fail HTLC due to balance after HTLCs and anchors exhausted on remote commitment");
+ log_trace!(
+ logger,
+ "Attempting to fail HTLC due to balance exhausted on remote commitment"
+ );
LocalHTLCFailureReason::ChannelBalanceOverdrawn
})?;
@@ -5464,19 +5435,33 @@ impl<SP: SignerProvider> ChannelContext<SP> {
}
if !funding.is_outbound() {
- let mut remote_fee_incl_fee_spike_buffer_htlc_msat =
- remote_stats.commitment_stats.commit_tx_fee_sat * 1000;
// Note that with anchor outputs we are no longer as sensitive to fee spikes, so we don't need
// to account for them.
- if !funding.get_channel_type().supports_anchors_zero_fee_htlc_tx() {
- remote_fee_incl_fee_spike_buffer_htlc_msat *=
- FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE;
- }
- if remote_stats
- .commitment_stats
- .counterparty_balance_before_fee_msat
- .saturating_sub(funding.holder_selected_channel_reserve_satoshis * 1000)
- < remote_fee_incl_fee_spike_buffer_htlc_msat
+ let fee_spike_multiple =
+ if !funding.get_channel_type().supports_anchors_zero_fee_htlc_tx() {
+ FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32
+ } else {
+ 1
+ };
+ let spiked_feerate = feerate * fee_spike_multiple;
+ let (remote_stats, _remote_htlcs) = self
+ .get_next_remote_commitment_stats(
+ funding,
+ None,
+ include_counterparty_unknown_htlcs,
+ fee_spike_buffer_htlc,
+ spiked_feerate,
+ dust_exposure_limiting_feerate,
+ )
+ .map_err(|()| {
+ log_trace!(
+ logger,
+ "Attempting to fail HTLC due to balance exhausted on remote commitment"
+ );
+ LocalHTLCFailureReason::FeeSpikeBuffer
+ })?;
+ if remote_stats.commitment_stats.counterparty_balance_msat
+ < funding.holder_selected_channel_reserve_satoshis * 1000
{
log_info!(
logger,
@@ -12627,7 +12612,7 @@ where
// We are not interested in dust exposure
let dust_exposure_limiting_feerate = None;
- let local_stats = self
+ let (local_stats, _local_htlcs) = self
.context
.get_next_local_commitment_stats(
funding,
@@ -12637,14 +12622,9 @@ where
self.context.feerate_per_kw,
dust_exposure_limiting_feerate,
)
- .map_err(|()| "Balance after HTLCs and anchors exhausted on local commitment")?;
- let (holder_balance_on_local_msat, counterparty_balance_on_local_msat) =
- local_stats
- .commitment_stats
- .get_holder_counterparty_balances_incl_fee_msat()
- .map_err(|()| "Channel funder cannot afford the fee on local commitment")?;
-
- let remote_stats = self
+ .map_err(|()| "Balance exhausted on local commitment")?;
+
+ let (remote_stats, _remote_htlcs) = self
.context
.get_next_remote_commitment_stats(
funding,
@@ -12654,19 +12634,19 @@ where
self.context.feerate_per_kw,
dust_exposure_limiting_feerate,
)
- .map_err(|()| "Balance after HTLCs and anchors exhausted on remote commitment")?;
- let (holder_balance_on_remote_msat, counterparty_balance_on_remote_msat) =
- remote_stats
- .commitment_stats
- .get_holder_counterparty_balances_incl_fee_msat()
- .map_err(|()| "Channel funder cannot afford the fee on remote commitment")?;
+ .map_err(|()| "Balance exhausted on remote commitment")?;
let holder_balance_floor = Amount::from_sat(
- cmp::min(holder_balance_on_local_msat, holder_balance_on_remote_msat) / 1000,
+ cmp::min(
+ local_stats.commitment_stats.holder_balance_msat,
+ remote_stats.commitment_stats.holder_balance_msat,
+ ) / 1000,
);
let counterparty_balance_floor = Amount::from_sat(
- cmp::min(counterparty_balance_on_local_msat, counterparty_balance_on_remote_msat)
- / 1000,
+ cmp::min(
+ local_stats.commitment_stats.counterparty_balance_msat,
+ remote_stats.commitment_stats.counterparty_balance_msat,
+ ) / 1000,
);
Ok((holder_balance_floor, counterparty_balance_floor))
diff --git a/lightning/src/ln/update_fee_tests.rs b/lightning/src/ln/update_fee_tests.rs
index ff3e2a0..99dcfd9 100644
--- a/lightning/src/ln/update_fee_tests.rs
+++ b/lightning/src/ln/update_fee_tests.rs
@@ -882,8 +882,13 @@ pub fn test_chan_init_feerate_unaffordability() {
MIN_AFFORDABLE_HTLC_COUNT as u64,
&channel_type_features,
);
- assert_eq!(nodes[0].node.create_channel(node_b_id, 100_000, push_amt + 1, 42, None, None).unwrap_err(),
- APIError::APIMisuseError { err: "Funding amount (356) can't even pay fee for initial commitment transaction fee of 357.".to_string() });
+ assert_eq!(
+ nodes[0].node.create_channel(node_b_id, 100_000, push_amt + 1, 42, None, None).unwrap_err(),
+ APIError::APIMisuseError {
+ err: "Funding amount (356) can't even pay fee for initial commitment transaction."
+ .to_string()
+ }
+ );
// During open, we don't have a "counterparty channel reserve" to check against, so that
// requirement only comes into play on the open_channel handling side.
diff --git a/lightning/src/sign/tx_builder.rs b/lightning/src/sign/tx_builder.rs
index 8bcfe12..3840cc7 100644
--- a/lightning/src/sign/tx_builder.rs
+++ b/lightning/src/sign/tx_builder.rs
@@ -1,5 +1,4 @@
//! Defines the `TxBuilder` trait, and the `SpecTxBuilder` type
-#![allow(dead_code)]
use core::cmp;
@@ -34,40 +33,19 @@ impl HTLCAmountDirection {
}
pub(crate) struct NextCommitmentStats {
- pub is_outbound_from_holder: bool,
- pub inbound_htlcs_count: usize,
- pub inbound_htlcs_value_msat: u64,
- pub holder_balance_before_fee_msat: u64,
- pub counterparty_balance_before_fee_msat: u64,
+ pub holder_balance_msat: u64,
+ pub counterparty_balance_msat: u64,
+ pub dust_exposure_msat: u64,
+ #[cfg(any(test, fuzzing))]
pub nondust_htlc_count: usize,
+ #[cfg(any(test, fuzzing))]
pub commit_tx_fee_sat: u64,
- pub dust_exposure_msat: u64,
}
pub(crate) struct ChannelStats {
pub commitment_stats: NextCommitmentStats,
}
-impl NextCommitmentStats {
- pub(crate) fn get_holder_counterparty_balances_incl_fee_msat(&self) -> Result<(u64, u64), ()> {
- if self.is_outbound_from_holder {
- Ok((
- self.holder_balance_before_fee_msat
- .checked_sub(self.commit_tx_fee_sat * 1000)
- .ok_or(())?,
- self.counterparty_balance_before_fee_msat,
- ))
- } else {
- Ok((
- self.holder_balance_before_fee_msat,
- self.counterparty_balance_before_fee_msat
- .checked_sub(self.commit_tx_fee_sat * 1000)
- .ok_or(())?,
- ))
- }
- }
-}
-
fn commit_plus_htlc_tx_fees_msat(
local: bool, next_commitment_htlcs: &[HTLCAmountDirection], dust_buffer_feerate: u32,
feerate: u32, broadcaster_dust_limit_satoshis: u64, channel_type: &ChannelTypeFeatures,
@@ -238,9 +216,6 @@ fn get_next_commitment_stats(
debug_assert_eq!(feerate_per_kw, 0);
}
- // Calculate inbound htlc count
- let inbound_htlcs_count = next_commitment_htlcs.iter().filter(|htlc| !htlc.outbound).count();
-
// Calculate balances after htlcs
let value_to_counterparty_msat =
(channel_value_satoshis * 1000).checked_sub(value_to_holder_msat).ok_or(())?;
@@ -296,15 +271,26 @@ fn get_next_commitment_stats(
channel_type,
);
+ let (holder_balance_msat, counterparty_balance_msat) = if is_outbound_from_holder {
+ (
+ holder_balance_before_fee_msat.checked_sub(commit_tx_fee_sat * 1000).ok_or(())?,
+ counterparty_balance_before_fee_msat,
+ )
+ } else {
+ (
+ holder_balance_before_fee_msat,
+ counterparty_balance_before_fee_msat.checked_sub(commit_tx_fee_sat * 1000).ok_or(())?,
+ )
+ };
+
Ok(NextCommitmentStats {
- is_outbound_from_holder,
- inbound_htlcs_count,
- inbound_htlcs_value_msat,
- holder_balance_before_fee_msat,
- counterparty_balance_before_fee_msat,
+ holder_balance_msat,
+ counterparty_balance_msat,
+ dust_exposure_msat,
+ #[cfg(any(test, fuzzing))]
nondust_htlc_count: nondust_htlc_count + addl_nondust_htlc_count,
+ #[cfg(any(test, fuzzing))]
commit_tx_fee_sat,
- dust_exposure_msat,
})
}
Why this scored 45/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.