Add `ChannelContext::get_next_{local, remote}_commitment_stats`
What changed, and why it matters
This commit adds four new private helper methods to the Lightning channel code. The methods are marked `#[allow(dead_code)]`, meaning they are not yet called anywhere. They are intended to replace older balance/fee estimation helpers in future commits. There is no functional change to how channels are validated today, and no security fix or vulnerability is introduced in this patch.
No immediate action required. Treat as routine refactoring. When the follow-up commits wire these helpers into validation paths, review the call sites for correct conservative assumptions and fee/balance consistency.
Security signals we found
New code is dead/unused (`#[allow(dead_code)]`)
No existing validation logic is modified
No caller sites are added in this commit
Commit message describes the change as preparatory refactoring
Evidence from the diff
The patch introduces ChannelContext::get_next_commitment_htlcs, get_next_commitment_value_to_self_msat, get_next_local_commitment_stats, and get_next_remote_commitment_stats in lightning/src/ln/channel.rs. These methods compute a conservative projection of which HTLCs will appear on the next local or remote commitment and derive the resulting value_to_self_msat and NextCommitmentStats via SpecTxBuilder::get_next_commitment_stats. All four methods are currently dead code (#[allow(dead_code)]) and are not wired into any validation path. The commit message explicitly frames this as preparatory refactoring for upcoming commits.
Changed components
lightning/src/ln/channel.rsInspect captured patch +162 / −1
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index f19037f..4839928 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -74,7 +74,7 @@ use crate::ln::script::{self, ShutdownScript};
use crate::ln::types::ChannelId;
use crate::routing::gossip::NodeId;
use crate::sign::ecdsa::EcdsaChannelSigner;
-use crate::sign::tx_builder::{SpecTxBuilder, TxBuilder};
+use crate::sign::tx_builder::{HTLCAmountDirection, NextCommitmentStats, SpecTxBuilder, TxBuilder};
use crate::sign::{ChannelSigner, EntropySource, NodeSigner, Recipient, SignerProvider};
use crate::types::features::{ChannelTypeFeatures, InitFeatures};
use crate::types::payment::{PaymentHash, PaymentPreimage};
@@ -4100,6 +4100,167 @@ where
);
}
+ /// Returns a best-effort guess of the set of HTLCs that will be present
+ /// on the next local or remote commitment. We cannot be certain as the
+ /// actual set of HTLCs present on the next commitment depends on the
+ /// ordering of commitment_signed and revoke_and_ack messages.
+ ///
+ /// We take the conservative approach and only assume that a HTLC will
+ /// not be in the next commitment when it is guaranteed that it won't be.
+ #[allow(dead_code)]
+ #[rustfmt::skip]
+ fn get_next_commitment_htlcs(
+ &self, local: bool, htlc_candidate: Option<HTLCAmountDirection>, include_counterparty_unknown_htlcs: bool,
+ ) -> Vec<HTLCAmountDirection> {
+ let mut commitment_htlcs = Vec::with_capacity(
+ 1 + self.pending_inbound_htlcs.len()
+ + self.pending_outbound_htlcs.len()
+ + self.holding_cell_htlc_updates.len(),
+ );
+ // `LocalRemoved` HTLCs will certainly not be present on any future remote
+ // commitments, but they could be in a future local commitment as the remote has
+ // not yet acknowledged the removal.
+ let pending_inbound_htlcs = self
+ .pending_inbound_htlcs
+ .iter()
+ .filter(|InboundHTLCOutput { state, .. }| match (state, local) {
+ (InboundHTLCState::RemoteAnnounced(..), _) => true,
+ (InboundHTLCState::AwaitingRemoteRevokeToAnnounce(..), _) => true,
+ (InboundHTLCState::AwaitingAnnouncedRemoteRevoke(..), _) => true,
+ (InboundHTLCState::Committed, _) => true,
+ (InboundHTLCState::LocalRemoved(..), true) => true,
+ (InboundHTLCState::LocalRemoved(..), false) => false,
+ })
+ .map(|&InboundHTLCOutput { amount_msat, .. }| HTLCAmountDirection { outbound: false, amount_msat });
+ // `RemoteRemoved` HTLCs can still be present on the next remote commitment if
+ // local produces a commitment before acknowledging the update. These HTLCs
+ // will for sure not be present on the next local commitment.
+ let pending_outbound_htlcs = self
+ .pending_outbound_htlcs
+ .iter()
+ .filter(|OutboundHTLCOutput { state, .. }| match (state, local) {
+ (OutboundHTLCState::LocalAnnounced(..), _) => include_counterparty_unknown_htlcs,
+ (OutboundHTLCState::Committed, _) => true,
+ (OutboundHTLCState::RemoteRemoved(..), true) => false,
+ (OutboundHTLCState::RemoteRemoved(..), false) => true,
+ (OutboundHTLCState::AwaitingRemoteRevokeToRemove(..), _) => false,
+ (OutboundHTLCState::AwaitingRemovedRemoteRevoke(..), _) => false,
+ })
+ .map(|&OutboundHTLCOutput { amount_msat, .. }| HTLCAmountDirection { outbound: true, amount_msat });
+
+ 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
+ }
+ });
+
+ if include_counterparty_unknown_htlcs {
+ commitment_htlcs.extend(
+ htlc_candidate.into_iter().chain(pending_inbound_htlcs).chain(pending_outbound_htlcs).chain(holding_cell_htlcs)
+ );
+ } else {
+ commitment_htlcs.extend(
+ htlc_candidate.into_iter().chain(pending_inbound_htlcs).chain(pending_outbound_htlcs)
+ );
+ }
+
+ commitment_htlcs
+ }
+
+ /// This returns the value of `value_to_self_msat` after accounting for all the
+ /// successful inbound and outbound HTLCs that won't be present on the next
+ /// commitment.
+ ///
+ /// To determine which HTLC claims to account for, we take the cases where a HTLC
+ /// will *not* be present on the next commitment from `next_commitment_htlcs`, and
+ /// check if their outcome is successful. If it is, we add the value of this claimed
+ /// HTLC to the balance of the claimer.
+ #[allow(dead_code)]
+ #[rustfmt::skip]
+ fn get_next_commitment_value_to_self_msat(&self, local: bool, funding: &FundingScope) -> u64 {
+ let inbound_claimed_htlc_msat: u64 =
+ self.pending_inbound_htlcs
+ .iter()
+ .filter(|InboundHTLCOutput { state, .. }| match (state, local) {
+ (InboundHTLCState::LocalRemoved(InboundHTLCRemovalReason::Fulfill(_, _)), true) => false,
+ (InboundHTLCState::LocalRemoved(InboundHTLCRemovalReason::Fulfill(_, _)), false) => true,
+ _ => false,
+ })
+ .map(|InboundHTLCOutput { amount_msat, .. }| amount_msat)
+ .sum();
+ let outbound_claimed_htlc_msat: u64 =
+ self.pending_outbound_htlcs
+ .iter()
+ .filter(|OutboundHTLCOutput { state, .. }| match (state, local) {
+ (OutboundHTLCState::RemoteRemoved(OutboundHTLCOutcome::Success(_, _)), true) => true,
+ (OutboundHTLCState::RemoteRemoved(OutboundHTLCOutcome::Success(_, _)), false) => false,
+ (OutboundHTLCState::AwaitingRemoteRevokeToRemove(OutboundHTLCOutcome::Success(_, _)), _) => true,
+ (OutboundHTLCState::AwaitingRemovedRemoteRevoke(OutboundHTLCOutcome::Success(_, _)), _) => true,
+ _ => false,
+ })
+ .map(|OutboundHTLCOutput { amount_msat, .. }| amount_msat)
+ .sum();
+
+ funding
+ .value_to_self_msat
+ .saturating_sub(outbound_claimed_htlc_msat)
+ .saturating_add(inbound_claimed_htlc_msat)
+ }
+
+ #[allow(dead_code)]
+ fn get_next_local_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>,
+ ) -> NextCommitmentStats {
+ let next_commitment_htlcs = self.get_next_commitment_htlcs(
+ true,
+ htlc_candidate,
+ include_counterparty_unknown_htlcs,
+ );
+ let next_value_to_self_msat = self.get_next_commitment_value_to_self_msat(true, funding);
+ SpecTxBuilder {}.get_next_commitment_stats(
+ true,
+ funding.is_outbound(),
+ funding.get_value_satoshis(),
+ next_value_to_self_msat,
+ &next_commitment_htlcs,
+ addl_nondust_htlc_count,
+ feerate_per_kw,
+ dust_exposure_limiting_feerate,
+ self.holder_dust_limit_satoshis,
+ funding.get_channel_type(),
+ )
+ }
+
+ #[allow(dead_code)]
+ 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>,
+ ) -> NextCommitmentStats {
+ let next_commitment_htlcs = self.get_next_commitment_htlcs(
+ false,
+ htlc_candidate,
+ include_counterparty_unknown_htlcs,
+ );
+ let next_value_to_self_msat = self.get_next_commitment_value_to_self_msat(false, funding);
+ SpecTxBuilder {}.get_next_commitment_stats(
+ false,
+ funding.is_outbound(),
+ funding.get_value_satoshis(),
+ next_value_to_self_msat,
+ &next_commitment_htlcs,
+ addl_nondust_htlc_count,
+ feerate_per_kw,
+ dust_exposure_limiting_feerate,
+ self.counterparty_dust_limit_satoshis,
+ funding.get_channel_type(),
+ )
+ }
+
#[rustfmt::skip]
fn validate_update_add_htlc<F: Deref>(
&self, funding: &FundingScope, msg: &msgs::UpdateAddHTLC,
Why this scored 11/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.