ln: use outer onion values for trampoline NextPacketDetails
What changed, and why it matters
This change fixes how trampoline-style Lightning payments decide how much money and when (time lock) to forward to the next hop. Previously the code used values from the inner trampoline onion, which could differ from what the forwarding node actually received in the outer onion. Now it uses the outer onion values, so a forwarding node cannot silently pay the next hop less than it should while still satisfying the inner trampoline instructions. This prevents a potential fee/amount mismatch in multi-hop trampoline routing.
Review the full trampoline payment flow to confirm outer_hop_data is authenticated and cannot be manipulated independently of the inner trampoline payload. Ensure the removed blinded-forward constraints are enforced elsewhere before this point. Consider adding regression tests that verify a forwarding node cannot substitute lower outer-onion values for trampoline forwards.
Security signals we found
Amount/CLTV mismatch between outer and inner trampoline onions
Removal of blinded-forward amount/cltv recalculation in favor of outer onion values
Trampoline forwarding logic change in payment onion decoding
Potential underpayment or premature expiry to next trampoline hop
Evidence from the diff
In onion_payment.rs, the construction of NextPacketDetails for TrampolineForward and TrampolineBlindedForward no longer derives outgoing_amt_msat and outgoing_cltv_value from the inbound trampoline payload fields. Instead it reads them from outer_hop_data, which reflects the values the sender encoded for this node’s outbound HTLC. For TrampolineBlindedForward, the previous check_blinded_forward calculation and its associated underflow error path are removed. The commit message states the intent: validate outer-onion amounts/expiry to ensure forwarding nodes did not send less than intended.
Changed components
lightning/src/ln/onion_payment.rsTrampolineForward handlingTrampolineBlindedForward handlingNextPacketDetails constructionInspect captured patch +6 / −15
diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs
index 36270eb..3dbb274 100644
--- a/lightning/src/ln/onion_payment.rs
+++ b/lightning/src/ln/onion_payment.rs
@@ -700,33 +700,24 @@ pub(super) fn decode_incoming_update_add_htlc_onion<NS: NodeSigner, L: Logger, T
Some(NextPacketDetails { next_packet_pubkey, outgoing_connector: HopConnector::Dummy, outgoing_amt_msat: amt_to_forward, outgoing_cltv_value })
}
- onion_utils::Hop::TrampolineForward { next_trampoline_hop_data: msgs::InboundTrampolineForwardPayload { amt_to_forward, outgoing_cltv_value, next_trampoline }, trampoline_shared_secret, incoming_trampoline_public_key, .. } => {
+ onion_utils::Hop::TrampolineForward { next_trampoline_hop_data: msgs::InboundTrampolineForwardPayload { next_trampoline, .. }, ref outer_hop_data, trampoline_shared_secret, incoming_trampoline_public_key, .. } => {
let next_trampoline_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx,
incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes());
Some(NextPacketDetails {
next_packet_pubkey: next_trampoline_packet_pubkey,
outgoing_connector: HopConnector::Trampoline(next_trampoline),
- outgoing_amt_msat: amt_to_forward,
- outgoing_cltv_value,
+ outgoing_amt_msat: outer_hop_data.amt_to_forward,
+ outgoing_cltv_value: outer_hop_data.outgoing_cltv_value,
})
}
- onion_utils::Hop::TrampolineBlindedForward { next_trampoline_hop_data: msgs::InboundTrampolineBlindedForwardPayload { next_trampoline, ref payment_relay, ref payment_constraints, ref features, .. }, outer_shared_secret, trampoline_shared_secret, incoming_trampoline_public_key, .. } => {
- let (amt_to_forward, outgoing_cltv_value) = match check_blinded_forward(
- msg.amount_msat, msg.cltv_expiry, &payment_relay, &payment_constraints, &features
- ) {
- Ok((amt, cltv)) => (amt, cltv),
- Err(()) => {
- return encode_relay_error("Underflow calculating outbound amount or cltv value for blinded trampoline forward",
- LocalHTLCFailureReason::InvalidOnionBlinding, outer_shared_secret.secret_bytes(), Some(trampoline_shared_secret.secret_bytes()), &[0; 32]);
- }
- };
+ onion_utils::Hop::TrampolineBlindedForward { next_trampoline_hop_data: msgs::InboundTrampolineBlindedForwardPayload { next_trampoline, .. }, ref outer_hop_data, trampoline_shared_secret, incoming_trampoline_public_key, .. } => {
let next_trampoline_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx,
incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes());
Some(NextPacketDetails {
next_packet_pubkey: next_trampoline_packet_pubkey,
outgoing_connector: HopConnector::Trampoline(next_trampoline),
- outgoing_amt_msat: amt_to_forward,
- outgoing_cltv_value,
+ outgoing_amt_msat: outer_hop_data.amt_to_forward,
+ outgoing_cltv_value: outer_hop_data.outgoing_cltv_value,
})
}
_ => None
Why this scored 60/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.