ln/refactor: add claim funds for htlc forward helper
What changed, and why it matters
This commit is a pure code reorganization (refactor) in the Lightning Dev Kit's channel manager. It extracts existing HTLC forwarding claim logic into a new helper function so the same code can be reused for an upcoming 'trampoline forwarding' feature. No behavior changes, bug fixes, or security-sensitive modifications are present in the diff.
No security action required. Treat as routine refactoring. Review the follow-up trampoline forwarding commits when they land, as those will contain the actual behavioral and security-relevant changes.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change moves the inline handling of HTLCSource::PreviousHopData in channelmanager.rs into a new private method claim_funds_from_htlc_forward_hop. The logic, variable names, control flow, assertions, and event emission are preserved exactly. The only functional addition is a todo!() placeholder for HTLCSource::TrampolineForward, which does not execute any code path today. This is preparatory refactoring for trampoline payment forwarding support.
Changed components
lightning/src/ln/channelmanager.rsInspect captured patch +159 / −133
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index 69cf85a..9039373 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -9321,6 +9321,153 @@ impl<
}
}
+ /// Claims funds for a forwarded HTLC where we are an intermediate hop.
+ ///
+ /// Processes attribution data, calculates fees earned, and emits a [`Event::PaymentForwarded`]
+ /// event upon successful claim.
+ fn claim_funds_from_htlc_forward_hop(
+ &self, payment_preimage: PaymentPreimage, forwarded_htlc_value_msat: Option<u64>,
+ skimmed_fee_msat: Option<u64>, from_onchain: bool, startup_replay: bool,
+ next_channel_counterparty_node_id: PublicKey, next_channel_outpoint: OutPoint,
+ next_channel_id: ChannelId, next_user_channel_id: Option<u128>,
+ hop_data: HTLCPreviousHopData, attribution_data: Option<AttributionData>,
+ send_timestamp: Option<Duration>,
+ ) {
+ let prev_channel_id = hop_data.channel_id;
+ let prev_user_channel_id = hop_data.user_channel_id;
+ let prev_node_id = hop_data.counterparty_node_id;
+ let completed_blocker = RAAMonitorUpdateBlockingAction::from_prev_hop_data(&hop_data);
+
+ // Obtain hold time, if available.
+ let hold_time = hold_time_since(send_timestamp).unwrap_or(0);
+
+ // If attribution data was received from downstream, we shift it and get it ready for adding our hold
+ // time. Note that fulfilled HTLCs take a fast path to the incoming side. We don't need to wait for RAA
+ // to record the hold time like we do for failed HTLCs.
+ let attribution_data = process_fulfill_attribution_data(
+ attribution_data,
+ &hop_data.incoming_packet_shared_secret,
+ hold_time,
+ );
+
+ #[cfg(test)]
+ let claiming_chan_funding_outpoint = hop_data.outpoint;
+ self.claim_funds_from_hop(
+ hop_data,
+ payment_preimage,
+ None,
+ Some(attribution_data),
+ |htlc_claim_value_msat, definitely_duplicate| {
+ let chan_to_release = EventUnblockedChannel {
+ counterparty_node_id: next_channel_counterparty_node_id,
+ funding_txo: next_channel_outpoint,
+ channel_id: next_channel_id,
+ blocking_action: completed_blocker,
+ };
+
+ if definitely_duplicate && startup_replay {
+ // On startup we may get redundant claims which are related to
+ // monitor updates still in flight. In that case, we shouldn't
+ // immediately free, but instead let that monitor update complete
+ // in the background.
+ #[cfg(test)]
+ {
+ let per_peer_state = self.per_peer_state.deadlocking_read();
+ // The channel we'd unblock should already be closed, or...
+ let channel_closed = per_peer_state
+ .get(&next_channel_counterparty_node_id)
+ .map(|lck| lck.deadlocking_lock())
+ .map(|peer| !peer.channel_by_id.contains_key(&next_channel_id))
+ .unwrap_or(true);
+ let background_events = self.pending_background_events.lock().unwrap();
+ // there should be a `BackgroundEvent` pending...
+ let matching_bg_event =
+ background_events.iter().any(|ev| {
+ match ev {
+ // to apply a monitor update that blocked the claiming channel,
+ BackgroundEvent::MonitorUpdateRegeneratedOnStartup {
+ funding_txo,
+ update,
+ ..
+ } => {
+ if *funding_txo == claiming_chan_funding_outpoint {
+ assert!(
+ update.updates.iter().any(|upd| {
+ if let ChannelMonitorUpdateStep::PaymentPreimage {
+ payment_preimage: update_preimage, ..
+ } = upd {
+ payment_preimage == *update_preimage
+ } else { false }
+ }),
+ "{:?}",
+ update
+ );
+ true
+ } else {
+ false
+ }
+ },
+ // or the monitor update has completed and will unblock
+ // immediately once we get going.
+ BackgroundEvent::MonitorUpdatesComplete {
+ channel_id, ..
+ } => *channel_id == prev_channel_id,
+ }
+ });
+ assert!(channel_closed || matching_bg_event, "{:?}", *background_events);
+ }
+ (None, None)
+ } else if definitely_duplicate {
+ (
+ Some(MonitorUpdateCompletionAction::FreeDuplicateClaimImmediately {
+ downstream_counterparty_node_id: chan_to_release.counterparty_node_id,
+ downstream_channel_id: chan_to_release.channel_id,
+ blocking_action: chan_to_release.blocking_action,
+ }),
+ None,
+ )
+ } else {
+ let total_fee_earned_msat =
+ if let Some(forwarded_htlc_value) = forwarded_htlc_value_msat {
+ if let Some(claimed_htlc_value) = htlc_claim_value_msat {
+ Some(claimed_htlc_value - forwarded_htlc_value)
+ } else {
+ None
+ }
+ } else {
+ None
+ };
+ debug_assert!(
+ skimmed_fee_msat <= total_fee_earned_msat,
+ "skimmed_fee_msat must always be included in total_fee_earned_msat"
+ );
+ (
+ Some(MonitorUpdateCompletionAction::EmitEventOptionAndFreeOtherChannel {
+ event: Some(events::Event::PaymentForwarded {
+ prev_htlcs: vec![events::HTLCLocator {
+ channel_id: prev_channel_id,
+ user_channel_id: prev_user_channel_id,
+ node_id: prev_node_id,
+ }],
+ next_htlcs: vec![events::HTLCLocator {
+ channel_id: next_channel_id,
+ user_channel_id: next_user_channel_id,
+ node_id: Some(next_channel_counterparty_node_id),
+ }],
+ total_fee_earned_msat,
+ skimmed_fee_msat,
+ claim_from_onchain_tx: from_onchain,
+ outbound_amount_forwarded_msat: forwarded_htlc_value_msat,
+ }),
+ downstream_counterparty_and_funding_outpoint: chan_to_release,
+ }),
+ None,
+ )
+ }
+ },
+ );
+ }
+
fn claim_funds_from_hop<
ComplFunc: FnOnce(
Option<u64>,
@@ -9716,140 +9863,19 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
}
},
HTLCSource::PreviousHopData(hop_data) => {
- let prev_channel_id = hop_data.channel_id;
- let prev_user_channel_id = hop_data.user_channel_id;
- let prev_node_id = hop_data.counterparty_node_id;
- let completed_blocker =
- RAAMonitorUpdateBlockingAction::from_prev_hop_data(&hop_data);
-
- // Obtain hold time, if available.
- let hold_time = hold_time_since(send_timestamp).unwrap_or(0);
-
- // If attribution data was received from downstream, we shift it and get it ready for adding our hold
- // time. Note that fulfilled HTLCs take a fast path to the incoming side. We don't need to wait for RAA
- // to record the hold time like we do for failed HTLCs.
- let attribution_data = process_fulfill_attribution_data(
- attribution_data,
- &hop_data.incoming_packet_shared_secret,
- hold_time,
- );
-
- #[cfg(test)]
- let claiming_chan_funding_outpoint = hop_data.outpoint;
- self.claim_funds_from_hop(
- hop_data,
+ self.claim_funds_from_htlc_forward_hop(
payment_preimage,
- None,
- Some(attribution_data),
- |htlc_claim_value_msat, definitely_duplicate| {
- let chan_to_release = EventUnblockedChannel {
- counterparty_node_id: next_channel_counterparty_node_id,
- funding_txo: next_channel_outpoint,
- channel_id: next_channel_id,
- blocking_action: completed_blocker,
- };
-
- if definitely_duplicate && startup_replay {
- // On startup we may get redundant claims which are related to
- // monitor updates still in flight. In that case, we shouldn't
- // immediately free, but instead let that monitor update complete
- // in the background.
- #[cfg(test)]
- {
- let per_peer_state = self.per_peer_state.deadlocking_read();
- // The channel we'd unblock should already be closed, or...
- let channel_closed = per_peer_state
- .get(&next_channel_counterparty_node_id)
- .map(|lck| lck.deadlocking_lock())
- .map(|peer| !peer.channel_by_id.contains_key(&next_channel_id))
- .unwrap_or(true);
- let background_events =
- self.pending_background_events.lock().unwrap();
- // there should be a `BackgroundEvent` pending...
- let matching_bg_event =
- background_events.iter().any(|ev| {
- match ev {
- // to apply a monitor update that blocked the claiming channel,
- BackgroundEvent::MonitorUpdateRegeneratedOnStartup {
- funding_txo, update, ..
- } => {
- if *funding_txo == claiming_chan_funding_outpoint {
- assert!(update.updates.iter().any(|upd|
- if let ChannelMonitorUpdateStep::PaymentPreimage {
- payment_preimage: update_preimage, ..
- } = upd {
- payment_preimage == *update_preimage
- } else { false }
- ), "{:?}", update);
- true
- } else { false }
- },
- // or the monitor update has completed and will unblock
- // immediately once we get going.
- BackgroundEvent::MonitorUpdatesComplete {
- channel_id, ..
- } =>
- *channel_id == prev_channel_id,
- }
- });
- assert!(
- channel_closed || matching_bg_event,
- "{:?}",
- *background_events
- );
- }
- (None, None)
- } else if definitely_duplicate {
- (
- Some(
- MonitorUpdateCompletionAction::FreeDuplicateClaimImmediately {
- downstream_counterparty_node_id: chan_to_release
- .counterparty_node_id,
- downstream_channel_id: chan_to_release.channel_id,
- blocking_action: chan_to_release.blocking_action,
- },
- ),
- None,
- )
- } else {
- let total_fee_earned_msat =
- if let Some(forwarded_htlc_value) = forwarded_htlc_value_msat {
- if let Some(claimed_htlc_value) = htlc_claim_value_msat {
- Some(claimed_htlc_value - forwarded_htlc_value)
- } else {
- None
- }
- } else {
- None
- };
- debug_assert!(
- skimmed_fee_msat <= total_fee_earned_msat,
- "skimmed_fee_msat must always be included in total_fee_earned_msat"
- );
- (
- Some(MonitorUpdateCompletionAction::EmitEventOptionAndFreeOtherChannel {
- event: Some(events::Event::PaymentForwarded {
- prev_htlcs: vec![events::HTLCLocator {
- channel_id: prev_channel_id,
- user_channel_id: prev_user_channel_id,
- node_id: prev_node_id,
- }],
- next_htlcs: vec![events::HTLCLocator {
- channel_id: next_channel_id,
- user_channel_id: next_user_channel_id,
- node_id: Some(next_channel_counterparty_node_id),
- }],
- total_fee_earned_msat,
- skimmed_fee_msat,
- claim_from_onchain_tx: from_onchain,
- outbound_amount_forwarded_msat: forwarded_htlc_value_msat,
- }),
- downstream_counterparty_and_funding_outpoint: chan_to_release,
- }),
- None,
- )
- }
- },
+ forwarded_htlc_value_msat,
+ skimmed_fee_msat,
+ from_onchain,
+ startup_replay,
+ next_channel_counterparty_node_id,
+ next_channel_outpoint,
+ next_channel_id,
+ next_user_channel_id,
+ hop_data,
+ attribution_data,
+ send_timestamp,
);
},
HTLCSource::TrampolineForward { .. } => todo!(),
Why this scored 15/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.