ln: add trampoline routing payment claiming
What changed, and why it matters
This commit finishes a previously unimplemented code path for 'trampoline routing' in the Lightning Dev Kit. Before this change, if a payment arrived via a trampoline forward, the software had a placeholder 'todo!()' where it should claim the funds. The commit adds the actual claiming logic, including safety checks that prevent the next channel step from proceeding until the incoming payment proof is durably saved. It is a feature-completion patch with defensive safety logic, not a fix for a known active vulnerability.
Treat as normal feature-completion code review. Verify that claim_funds_from_htlc_forward_hop is idempotent or safely handles duplicate calls for the same HTLC, that the RAA blocker is released correctly on persistence failure, and that the single PaymentForwarded event accurately represents the aggregate trampoline forward. No urgent security action is indicated by the supplied materials.
Security signals we found
Replaces a todo!() panic path with real fund-claiming logic
Adds RAA blocking for multiple inbound trampoline HTLCs to ensure preimage persistence before outbound channel advances
Touches trampoline forwarding, a newer and less battle-tested LDK feature
No explicit bug, CVE, or security disclosure referenced in commit or materials
Evidence from the diff
The patch implements payment claiming for HTLCSource::TrampolineForward in ChannelManager. It iterates over previous_hop_data, calls claim_funds_from_htlc_forward_hop for each inbound HTLC, emits a single PaymentForwarded event, and blocks the outbound channel’s revoke_and_ack monitor update until each inbound preimage is durably persisted. It also generalizes the RAA-blocking logic to handle both PreviousHopData and TrampolineForward sources.
Changed components
lightning/src/ln/channelmanager.rsHTLCSource::TrampolineForward handlingclaim_funds_internal / claim_funds_from_htlc_forward_hopRAA monitor update blocking logicInspect captured patch +56 / −8
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index d8661f4..1bc4dd0 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -9890,7 +9890,50 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
send_timestamp,
);
},
- HTLCSource::TrampolineForward { .. } => todo!(),
+ HTLCSource::TrampolineForward { previous_hop_data, .. } => {
+ // Only emit a single event for trampoline claims.
+ let prev_htlcs: Vec<events::HTLCLocator> =
+ previous_hop_data.iter().map(Into::into).collect();
+ for (i, current_previous_hop_data) in previous_hop_data.into_iter().enumerate() {
+ self.claim_funds_from_htlc_forward_hop(
+ payment_preimage,
+ |_: Option<u64>| -> Option<events::Event> {
+ if i == 0 {
+ Some(events::Event::PaymentForwarded {
+ prev_htlcs: prev_htlcs.clone(),
+ // TODO: When trampoline payments are tracked in our
+ // pending_outbound_payments, we'll be able to provide all the
+ // outgoing htlcs for this forward.
+ 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),
+ }],
+ // TODO: When trampoline payments are tracked in our
+ // pending_outbound_payments, we'll be able to lookup our total
+ // fee earnings.
+ total_fee_earned_msat: None,
+ skimmed_fee_msat,
+ claim_from_onchain_tx: from_onchain,
+ // TODO: When trampoline payments are tracked in our
+ // pending_outbound_payments, set to the total amount sent (not
+ // just the amount of the outgoing htlc that was first settled).
+ outbound_amount_forwarded_msat: forwarded_htlc_value_msat,
+ })
+ } else {
+ None
+ }
+ },
+ startup_replay,
+ next_channel_counterparty_node_id,
+ next_channel_outpoint,
+ next_channel_id,
+ current_previous_hop_data,
+ attribution_data.clone(),
+ send_timestamp,
+ );
+ }
+ },
}
}
@@ -12282,20 +12325,25 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
chan.update_fulfill_htlc(&msg),
chan_entry
);
- if let HTLCSource::PreviousHopData(prev_hop) = &res.0 {
- let logger =
- WithChannelContext::from(&self.logger, &chan.context, None);
+ let prev_hops = match &res.0 {
+ HTLCSource::PreviousHopData(prev_hop) => vec![prev_hop],
+ HTLCSource::TrampolineForward { previous_hop_data, .. } => {
+ previous_hop_data.iter().collect()
+ },
+ _ => vec![],
+ };
+ let logger = WithChannelContext::from(&self.logger, &chan.context, None);
+ for prev_hop in prev_hops {
log_trace!(logger,
"Holding the next revoke_and_ack until the preimage is durably persisted in the inbound edge's ChannelMonitor",
- );
+ );
peer_state
.actions_blocking_raa_monitor_updates
.entry(msg.channel_id)
.or_insert_with(Vec::new)
- .push(RAAMonitorUpdateBlockingAction::from_prev_hop_data(
- &prev_hop,
- ));
+ .push(RAAMonitorUpdateBlockingAction::from_prev_hop_data(prev_hop));
}
+
// Note that we do not need to push an `actions_blocking_raa_monitor_updates`
// entry here, even though we *do* need to block the next RAA monitor update.
// We do this instead in the `claim_funds_internal` by attaching a
Why this scored 29/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.