ln: add trampoline routing failure handling
What changed, and why it matters
This commit fills in a previously unimplemented 'todo!' placeholder for handling failed trampoline-routed payments. When a trampoline forward fails, the node now properly reports the failure back through each incoming payment hop using a standard temporary trampoline failure message, instead of crashing. This is a robustness improvement that prevents a panic path and ensures correct Lightning protocol behavior for an in-development feature.
Treat as a normal code-quality/robustness commit. Reviewers should verify that TemporaryTrampolineFailure is the correct failure code per BOLT specifications and that the over-failing behavior noted in the TODO does not create denial-of-service or unfair penalty risks. No immediate security response appears necessary.
Security signals we found
Replaces unimplemented todo!() panic with structured failure handling
Adds failure propagation for TrampolineForward HTLC source
Uses TemporaryTrampolineFailure as the standardized failure reason
Emits a single HTLCHandlingFailed event after failing all incoming HTLCs
Contains a TODO indicating the logic is incomplete and may over-fail incoming HTLCs
Evidence from the diff
The patch implements HTLCSource::TrampolineForward failure propagation in ChannelManager::fail_htlc_offchain. It decodes the onion failure, logs the failing short channel id, then iterates over previous_hop_data and calls push_forward_htlcs_failure for each incoming HTLC with LocalHTLCFailureReason::TemporaryTrampolineFailure. It finally emits a single HTLCHandlingFailed event covering all affected previous channels. A TODO notes that future work should avoid failing all incoming HTLCs immediately when only one outgoing trampoline HTLC fails. The prior code was a todo!() panic, so this change removes a crash path for trampoline forwarding failures.
Changed components
lightning/src/ln/channelmanager.rsHTLCSource::TrampolineForward handlingTrampoline forwarding failure pathInspect captured patch +68 / −1
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index 81b5bd1..5ac5c0d 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -9094,7 +9094,74 @@ impl<
None,
));
},
- HTLCSource::TrampolineForward { .. } => todo!(),
+ HTLCSource::TrampolineForward {
+ previous_hop_data,
+ incoming_trampoline_shared_secret,
+ ..
+ } => {
+ let decoded_onion_failure =
+ onion_error.decode_onion_failure(&self.secp_ctx, &self.logger, &source);
+ log_trace!(
+ WithContext::from(&self.logger, None, None, Some(*payment_hash)),
+ "Trampoline forward failed downstream on {}",
+ if let Some(scid) = decoded_onion_failure.short_channel_id {
+ scid.to_string()
+ } else {
+ "unknown channel".to_string()
+ },
+ );
+ let incoming_trampoline_shared_secret = Some(*incoming_trampoline_shared_secret);
+
+ // TODO: when we receive a failure from a single outgoing trampoline HTLC, we don't
+ // necessarily want to fail all of our incoming HTLCs back yet. We may have other
+ // outgoing HTLCs that need to resolve first. This will be tracked in our
+ // pending_outbound_payments in a followup.
+ for current_hop_data in previous_hop_data {
+ let HTLCPreviousHopData {
+ prev_outbound_scid_alias,
+ htlc_id,
+ incoming_packet_shared_secret,
+ blinded_failure,
+ channel_id,
+ ..
+ } = current_hop_data;
+ log_trace!(
+ WithContext::from(&self.logger, None, Some(*channel_id), Some(*payment_hash)),
+ "Failing {}HTLC with payment_hash {} backwards from us following Trampoline forwarding failure: {:?}",
+ if blinded_failure.is_some() { "blinded " } else { "" }, &payment_hash, onion_error
+ );
+ let onion_error = HTLCFailReason::reason(
+ LocalHTLCFailureReason::TemporaryTrampolineFailure,
+ Vec::new(),
+ );
+ push_forward_htlcs_failure(
+ *prev_outbound_scid_alias,
+ get_htlc_forward_failure(
+ blinded_failure,
+ &onion_error,
+ incoming_packet_shared_secret,
+ &incoming_trampoline_shared_secret,
+ &None,
+ *htlc_id,
+ ),
+ );
+ }
+
+ // We only want to emit a single event for trampoline failures, so we do it once
+ // we've failed back all of our incoming HTLCs.
+ let mut pending_events = self.pending_events.lock().unwrap();
+ pending_events.push_back((
+ events::Event::HTLCHandlingFailed {
+ prev_channel_ids: previous_hop_data
+ .iter()
+ .map(|prev| prev.channel_id)
+ .collect(),
+ failure_type,
+ failure_reason: Some(onion_error.into()),
+ },
+ None,
+ ));
+ },
}
}
Why this scored 32/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.