Cleanup dust exposure due to excess fees in `get_next_commitment_stats`
What changed, and why it matters
This commit changes how Lightning Dev Kit calculates 'dust exposure'—a risk measure for tiny outputs that can be abused to make a channel uneconomical to close. The old code only counted extra fees on the counterparty's transaction when the counterparty set a feerate above a configured limit, and it stored that as an optional value. The new code always adds excess fees to dust exposure on counterparty transactions and returns a plain number for the extra exposure if one more accepted HTLC were added. The change appears to fix an accounting gap where excess fees on counterparty commitments were not always included in the local node's dust-exposure check, which could let a remote peer push fees high enough to make the channel unrecoverable.
Review callers of `get_next_commitment_stats` to ensure they handle the new non-optional `extra_accepted_htlc_dust_exposure_msat` correctly. Verify that the change does not introduce regressions for anchor-zero-fee channels, where `feerate_per_kw` and excess feerate are expected to be zero. Consider adding or updating tests that exercise high counterparty feerates and confirm dust exposure is now always bounded.
Security signals we found
Change in fee/dust accounting for counterparty commitment transactions
Removal of optional dust-exposure field in favor of always-computed value
Use of `saturating_sub` instead of `checked_sub` for excess feerate
Renaming from 'counterparty' to 'local/broadcaster' semantics in helper
Commit title explicitly mentions 'Cleanup dust exposure due to excess fees'
Evidence from the diff
In lightning/src/sign/tx_builder.rs, get_next_commitment_stats is reworked. Previously excess_feerate_opt was feerate_per_kw - dust_exposure_limiting_feerate, and only when local == false were excess fees on the counterparty commitment counted as dust exposure. The helper excess_fees_on_counterparty_tx_dust_exposure_msat computed current and ‘extra HTLC’ exposure by adding dust_htlc_exposure_msat to fee-derived amounts. The patch replaces that helper with commit_plus_htlc_tx_fees_msat, which computes (total_fees_msat, extra_accepted_htlc_total_fees_msat) for whichever side is the broadcaster (local). For counterparty transactions (!local) it now always adds excess_fees_msat to dust_exposure_msat and extra_accepted_htlc_excess_fees_msat to the extra-HTLC exposure. The struct field changes from Option<u64> extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat to u64 extra_accepted_htlc_dust_exposure_msat. This suggests the prior logic undercounted dust exposure when the counterparty’s feerate exceeded the local limit, potentially allowing a peer to inflate commitment fees without the local node treating that as dust exposure.
Changed components
lightning/src/sign/tx_builder.rsNextCommitmentStats structget_next_commitment_stats methodDust exposure / fee accounting logicInspect captured patch +47 / −61
diff --git a/lightning/src/sign/tx_builder.rs b/lightning/src/sign/tx_builder.rs
index 8583bde..74941ec 100644
--- a/lightning/src/sign/tx_builder.rs
+++ b/lightning/src/sign/tx_builder.rs
@@ -43,10 +43,7 @@ pub(crate) struct NextCommitmentStats {
pub nondust_htlc_count: usize,
pub commit_tx_fee_sat: u64,
pub dust_exposure_msat: u64,
- // If the counterparty sets a feerate on the channel in excess of our dust_exposure_limiting_feerate,
- // this should be set to the dust exposure that would result from us adding an additional nondust outbound
- // htlc on the counterparty's commitment transaction.
- pub extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat: Option<u64>,
+ pub extra_accepted_htlc_dust_exposure_msat: u64,
}
impl NextCommitmentStats {
@@ -69,65 +66,52 @@ impl NextCommitmentStats {
}
}
-fn excess_fees_on_counterparty_tx_dust_exposure_msat(
- next_commitment_htlcs: &[HTLCAmountDirection], dust_buffer_feerate: u32, excess_feerate: u32,
- counterparty_dust_limit_satoshis: u64, dust_htlc_exposure_msat: u64,
- channel_type: &ChannelTypeFeatures,
+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,
) -> (u64, u64) {
- let on_counterparty_tx_accepted_nondust_htlcs = next_commitment_htlcs
+ let accepted_nondust_htlcs = next_commitment_htlcs
.iter()
.filter(|htlc| {
- htlc.outbound
+ htlc.outbound != local
&& !htlc.is_dust(
- false,
+ local,
dust_buffer_feerate,
- counterparty_dust_limit_satoshis,
+ broadcaster_dust_limit_satoshis,
channel_type,
)
})
.count();
- let on_counterparty_tx_offered_nondust_htlcs = next_commitment_htlcs
+ let offered_nondust_htlcs = next_commitment_htlcs
.iter()
.filter(|htlc| {
- !htlc.outbound
+ htlc.outbound == local
&& !htlc.is_dust(
- false,
+ local,
dust_buffer_feerate,
- counterparty_dust_limit_satoshis,
+ broadcaster_dust_limit_satoshis,
channel_type,
)
})
.count();
- let commitment_fee_sat = commit_tx_fee_sat(
- excess_feerate,
- on_counterparty_tx_accepted_nondust_htlcs + on_counterparty_tx_offered_nondust_htlcs,
- channel_type,
- );
- let second_stage_fees_sat = htlc_tx_fees_sat(
- excess_feerate,
- on_counterparty_tx_accepted_nondust_htlcs,
- on_counterparty_tx_offered_nondust_htlcs,
- channel_type,
- );
- let on_counterparty_tx_dust_exposure_msat =
- dust_htlc_exposure_msat + (commitment_fee_sat + second_stage_fees_sat) * 1000;
+ let commitment_fee_sat =
+ commit_tx_fee_sat(feerate, accepted_nondust_htlcs + offered_nondust_htlcs, channel_type);
+ let second_stage_fees_sat =
+ htlc_tx_fees_sat(feerate, accepted_nondust_htlcs, offered_nondust_htlcs, channel_type);
+ let total_fees_msat = (commitment_fee_sat + second_stage_fees_sat) * 1000;
- let extra_htlc_commitment_fee_sat = commit_tx_fee_sat(
- excess_feerate,
- on_counterparty_tx_accepted_nondust_htlcs + 1 + on_counterparty_tx_offered_nondust_htlcs,
+ let extra_accepted_htlc_commitment_fee_sat = commit_tx_fee_sat(
+ feerate,
+ accepted_nondust_htlcs + 1 + offered_nondust_htlcs,
channel_type,
);
- let extra_htlc_second_stage_fees_sat = htlc_tx_fees_sat(
- excess_feerate,
- on_counterparty_tx_accepted_nondust_htlcs + 1,
- on_counterparty_tx_offered_nondust_htlcs,
- channel_type,
- );
- let extra_htlc_dust_exposure_msat = dust_htlc_exposure_msat
- + (extra_htlc_commitment_fee_sat + extra_htlc_second_stage_fees_sat) * 1000;
+ let extra_accepted_htlc_second_stage_fees_sat =
+ htlc_tx_fees_sat(feerate, accepted_nondust_htlcs + 1, offered_nondust_htlcs, channel_type);
+ let extra_accepted_htlc_total_fees_msat =
+ (extra_accepted_htlc_commitment_fee_sat + extra_accepted_htlc_second_stage_fees_sat) * 1000;
- (on_counterparty_tx_dust_exposure_msat, extra_htlc_dust_exposure_msat)
+ (total_fees_msat, extra_accepted_htlc_total_fees_msat)
}
fn subtract_addl_outputs(
@@ -205,11 +189,11 @@ impl TxBuilder for SpecTxBuilder {
dust_exposure_limiting_feerate: Option<u32>, broadcaster_dust_limit_satoshis: u64,
channel_type: &ChannelTypeFeatures,
) -> Result<NextCommitmentStats, ()> {
- let excess_feerate_opt =
- feerate_per_kw.checked_sub(dust_exposure_limiting_feerate.unwrap_or(feerate_per_kw));
+ let excess_feerate =
+ feerate_per_kw.saturating_sub(dust_exposure_limiting_feerate.unwrap_or(feerate_per_kw));
if channel_type.supports_anchor_zero_fee_commitments() {
debug_assert_eq!(feerate_per_kw, 0);
- debug_assert_eq!(excess_feerate_opt, Some(0));
+ debug_assert_eq!(excess_feerate, 0);
debug_assert_eq!(addl_nondust_htlc_count, 0);
}
@@ -272,22 +256,24 @@ impl TxBuilder for SpecTxBuilder {
})
.sum();
- // Count the excess fees on the counterparty's transaction as dust
- let (dust_exposure_msat, extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat) =
- if let (Some(excess_feerate), false) = (excess_feerate_opt, local) {
- let (dust_exposure_msat, extra_nondust_htlc_exposure_msat) =
- excess_fees_on_counterparty_tx_dust_exposure_msat(
- &next_commitment_htlcs,
- dust_buffer_feerate,
- excess_feerate,
- broadcaster_dust_limit_satoshis,
- dust_exposure_msat,
- channel_type,
- );
- (dust_exposure_msat, Some(extra_nondust_htlc_exposure_msat))
- } else {
- (dust_exposure_msat, None)
- };
+ // Add any excess fees to dust exposure on counterparty transactions
+ let (dust_exposure_msat, extra_accepted_htlc_dust_exposure_msat) = if local {
+ (dust_exposure_msat, dust_exposure_msat)
+ } else {
+ let (excess_fees_msat, extra_accepted_htlc_excess_fees_msat) =
+ commit_plus_htlc_tx_fees_msat(
+ local,
+ &next_commitment_htlcs,
+ dust_buffer_feerate,
+ excess_feerate,
+ broadcaster_dust_limit_satoshis,
+ channel_type,
+ );
+ (
+ dust_exposure_msat + excess_fees_msat,
+ dust_exposure_msat + extra_accepted_htlc_excess_fees_msat,
+ )
+ };
Ok(NextCommitmentStats {
is_outbound_from_holder,
@@ -298,7 +284,7 @@ impl TxBuilder for SpecTxBuilder {
nondust_htlc_count: nondust_htlc_count + addl_nondust_htlc_count,
commit_tx_fee_sat,
dust_exposure_msat,
- extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat,
+ extra_accepted_htlc_dust_exposure_msat,
})
}
fn commit_tx_fee_sat(
Why this scored 59/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.