Delete `TxBuilder::commit_tx_fee_sat`
What changed, and why it matters
This commit is a code cleanup in the Lightning Dev Kit's rust-lightning project. It removes a helper method called `commit_tx_fee_sat` from a transaction-building trait and moves the fee-checking logic into a more central place when opening channels. The goal is to keep the same behavior while simplifying the code. There is no direct evidence in the commit that this fixes a security vulnerability.
Treat as a normal refactor commit. Reviewers should verify that the moved fee checks produce identical thresholds and error messages, and that removing the `addl_nondust_htlc_count` debug assertion does not mask incorrect HTLC counts in anchor-zero-fee channels. No immediate security response is indicated by the commit itself.
Security signals we found
Refactor moves fee-sufficiency checks from channel-open setup into post-construction validation using commitment stats
Removal of `debug_assert_eq!(addl_nondust_htlc_count, 0)` in anchor-zero-fee commitment path
Test-only addition of `PredictedNextFee` defaults to satisfy new code paths
Evidence from the diff
The patch deletes TxBuilder::commit_tx_fee_sat and replaces its callers with either ChannelContext::get_next_{local,remote}_commitment_stats (for initial channel funding checks in new_for_inbound_channel and new_for_outbound_channel) or the standalone chan_utils::commit_tx_fee_sat (in functions slated for deletion). It also removes a debug_assert_eq!(addl_nondust_htlc_count, 0) for anchor-zero-fee channels and adds test setup for next_local_fee/next_remote_fee. The commit message frames this as a refactor preserving exact behavior.
Changed components
lightning/src/ln/channel.rslightning/src/sign/tx_builder.rsInspect captured patch +54 / −43
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 509202b..d09e117 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -3731,23 +3731,6 @@ impl<SP: SignerProvider> ChannelContext<SP> {
debug_assert!(our_funding_satoshis == 0 || msg_push_msat == 0);
let value_to_self_msat = our_funding_satoshis * 1000 + msg_push_msat;
- // 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 to_remote_satoshis = remote_balance_before_fee_msat / 1000 - 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 {
- return Err(ChannelError::close("Insufficient funding amount for initial reserve".to_owned()));
- }
-
let counterparty_shutdown_scriptpubkey = if their_features.supports_upfront_shutdown_script() {
match &open_channel_fields.shutdown_scriptpubkey {
&Some(ref script) => {
@@ -3948,6 +3931,33 @@ impl<SP: SignerProvider> ChannelContext<SP> {
interactive_tx_signing_session: None,
};
+ // 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 = funding.get_value_satoshis() * 1000 - funding.get_value_to_self_msat();
+ let htlc_candidate = None;
+ 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(
+ &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)))?;
+
+ 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 {
+ return Err(ChannelError::close("Insufficient funding amount for initial reserve".to_owned()));
+ }
+
Ok((funding, channel_context))
}
@@ -3998,17 +4008,6 @@ impl<SP: SignerProvider> ChannelContext<SP> {
);
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 mut secp_ctx = Secp256k1::new();
secp_ctx.seeded_randomize(&entropy_source.get_secure_random_bytes());
@@ -4182,6 +4181,23 @@ impl<SP: SignerProvider> ChannelContext<SP> {
interactive_tx_signing_session: None,
};
+ let htlc_candidate = None;
+ 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(
+ &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) });
+ }
+
Ok((funding, channel_context))
}
@@ -5771,10 +5787,10 @@ impl<SP: SignerProvider> ChannelContext<SP> {
}
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_commit_tx_fee_sat = chan_utils::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 commit_tx_fee_sat = chan_utils::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;
@@ -6112,7 +6128,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
}
let num_htlcs = included_htlcs + addl_htlcs;
- SpecTxBuilder {}.commit_tx_fee_sat(context.feerate_per_kw, num_htlcs, funding.get_channel_type()) * 1000
+ chan_utils::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
@@ -6189,7 +6205,7 @@ impl<SP: SignerProvider> ChannelContext<SP> {
}
let num_htlcs = included_htlcs + addl_htlcs;
- SpecTxBuilder {}.commit_tx_fee_sat(context.feerate_per_kw, num_htlcs, funding.get_channel_type()) * 1000
+ chan_utils::commit_tx_fee_sat(context.feerate_per_kw, num_htlcs, funding.get_channel_type()) * 1000
}
#[rustfmt::skip]
@@ -17088,7 +17104,7 @@ mod tests {
ChannelPublicKeys, CounterpartyChannelTransactionParameters,
HolderCommitmentTransaction,
};
- use crate::ln::channel::HTLCOutputInCommitment;
+ use crate::ln::channel::{HTLCOutputInCommitment, PredictedNextFee};
use crate::ln::channel_keys::{DelayedPaymentBasepoint, HtlcBasepoint};
use crate::sign::{ecdsa::EcdsaChannelSigner, ChannelDerivationParameters, HTLCDescriptor};
use crate::sync::Arc;
@@ -17217,6 +17233,8 @@ mod tests {
macro_rules! test_commitment {
( $counterparty_sig_hex: expr, $sig_hex: expr, $tx_hex: expr, $($remain:tt)* ) => {
chan.funding.channel_transaction_parameters.channel_type_features = ChannelTypeFeatures::only_static_remote_key();
+ chan.funding.next_local_fee = Mutex::new(PredictedNextFee::default());
+ chan.funding.next_remote_fee = Mutex::new(PredictedNextFee::default());
test_commitment_common!(chan, logger, secp_ctx, signer, holder_pubkeys, per_commitment_point, $counterparty_sig_hex, $sig_hex, $tx_hex, &ChannelTypeFeatures::only_static_remote_key(), $($remain)*);
};
}
@@ -17224,6 +17242,8 @@ mod tests {
macro_rules! test_commitment_with_anchors {
( $counterparty_sig_hex: expr, $sig_hex: expr, $tx_hex: expr, $($remain:tt)* ) => {
chan.funding.channel_transaction_parameters.channel_type_features = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies();
+ chan.funding.next_local_fee = Mutex::new(PredictedNextFee::default());
+ chan.funding.next_remote_fee = Mutex::new(PredictedNextFee::default());
test_commitment_common!(chan, logger, secp_ctx, signer, holder_pubkeys, per_commitment_point, $counterparty_sig_hex, $sig_hex, $tx_hex, &ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(), $($remain)*);
};
}
diff --git a/lightning/src/sign/tx_builder.rs b/lightning/src/sign/tx_builder.rs
index d004cc9..3b34fb8 100644
--- a/lightning/src/sign/tx_builder.rs
+++ b/lightning/src/sign/tx_builder.rs
@@ -169,7 +169,6 @@ fn get_next_commitment_stats(
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
@@ -271,9 +270,6 @@ pub(crate) trait TxBuilder {
dust_exposure_limiting_feerate: Option<u32>, broadcaster_dust_limit_satoshis: u64,
channel_type: &ChannelTypeFeatures,
) -> Result<ChannelStats, ()>;
- 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,
@@ -311,11 +307,6 @@ impl TxBuilder for SpecTxBuilder {
Ok(ChannelStats { commitment_stats })
}
- 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 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,
@@ -399,7 +390,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,
Why this scored 27/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.