ln: store next trampoline amount and cltv in PendingHTLCRouting
What changed, and why it matters
This commit changes how Lightning trampoline payments are tracked internally. It stores the amount and expiry time that the next trampoline hop expects, so the node remembers them when forwarding a payment. The change looks like a correctness fix for trampoline routing rather than a typical security bug, but without the surrounding code it is hard to tell whether the missing fields previously caused fee or expiry mismatches that could be exploited.
Review the full trampoline forwarding path to confirm the newly stored values are used consistently in outbound HTLC construction and fee/CLTV validation. Verify serialization compatibility with existing persisted state. Treat as a routine correctness fix unless further review shows an exploitable inconsistency.
Security signals we found
Previously computed next-hop amount/CLTV values were intentionally discarded, suggesting a latent routing-state gap.
New fields are marked `required` in serialization, which can affect backward compatibility and persistence recovery.
Trampoline forwarding involves fee and timelock checks; missing expected values could theoretically lead to incorrect forwarding decisions.
No explicit security claim, CVE, or advisory is present in the commit or supplied references.
Evidence from the diff
The patch adds next_trampoline_amt_msat and next_trampoline_cltv_expiry to PendingHTLCRouting::TrampolineForward and serializes them via the TLV enum. It also captures these values from next_trampoline_hop_data in create_fwd_pending_htlc_info and threads them through RoutingInfo::Trampoline. Previously these values were computed by check_blinded_forward but discarded (bound to _next_hop_amount, _next_hop_cltv). The commit makes the next-hop expected amount and CLTV explicit and persistent.
Changed components
lightning/src/ln/channelmanager.rslightning/src/ln/onion_payment.rsPendingHTLCRouting::TrampolineForwardRoutingInfo::TrampolineTrampoline payment forwardingInspect captured patch +17 / −2
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index 7582321..0303483 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -239,6 +239,10 @@ pub enum PendingHTLCRouting {
incoming_cltv_expiry: u32,
/// MPP data for accumulating incoming HTLCs before dispatching an outbound payment.
incoming_multipath_data: Option<msgs::FinalOnionHopData>,
+ /// The amount that the next trampoline is expecting to receive.
+ next_trampoline_amt_msat: u64,
+ /// The CLTV expiry height that the next trampoline is expecting to receive.
+ next_trampoline_cltv_expiry: u32,
},
/// The onion indicates that this is a payment for an invoice (supposedly) generated by us.
///
@@ -17893,6 +17897,8 @@ impl_ser_tlv_based_enum!(PendingHTLCRouting,
(6, node_id, required),
(8, incoming_cltv_expiry, required),
(10, incoming_multipath_data, option),
+ (12, next_trampoline_amt_msat, required),
+ (14, next_trampoline_cltv_expiry, required),
}
);
diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs
index df32d50..36270eb 100644
--- a/lightning/src/ln/onion_payment.rs
+++ b/lightning/src/ln/onion_payment.rs
@@ -112,6 +112,8 @@ enum RoutingInfo {
shared_secret: SharedSecret,
current_path_key: Option<PublicKey>,
incoming_multipath_data: Option<msgs::FinalOnionHopData>,
+ next_trampoline_amt_msat: u64,
+ next_trampoline_cltv: u32,
},
}
@@ -177,6 +179,8 @@ pub(super) fn create_fwd_pending_htlc_info(
shared_secret: trampoline_shared_secret,
current_path_key: None,
incoming_multipath_data: outer_hop_data.multipath_trampoline_data,
+ next_trampoline_amt_msat: next_trampoline_hop_data.amt_to_forward,
+ next_trampoline_cltv: next_trampoline_hop_data.outgoing_cltv_value,
},
outer_hop_data.amt_to_forward,
outer_hop_data.outgoing_cltv_value,
@@ -189,7 +193,7 @@ pub(super) fn create_fwd_pending_htlc_info(
// amount that the trampoline node will forward onward, not the individual amount that
// arrives in a single (incoming MPP) HTLC. We used the desired total amount to
// calculate our outbound values.
- let (_next_hop_amount, _next_hop_cltv) = check_blinded_forward(
+ let (next_hop_amount, next_hop_cltv) = check_blinded_forward(
outer_hop_data.multipath_trampoline_data.as_ref().map(|f| f.total_msat).unwrap_or(msg.amount_msat), msg.cltv_expiry, &next_trampoline_hop_data.payment_relay, &next_trampoline_hop_data.payment_constraints, &next_trampoline_hop_data.features
).map_err(|()| {
// We should be returning malformed here if `msg.blinding_point` is set, but this is
@@ -208,6 +212,8 @@ pub(super) fn create_fwd_pending_htlc_info(
shared_secret: trampoline_shared_secret,
current_path_key: outer_hop_data.current_path_key,
incoming_multipath_data: outer_hop_data.multipath_trampoline_data,
+ next_trampoline_amt_msat: next_hop_amount,
+ next_trampoline_cltv: next_hop_cltv,
},
outer_hop_data.amt_to_forward,
outer_hop_data.outgoing_cltv_value,
@@ -240,7 +246,7 @@ pub(super) fn create_fwd_pending_htlc_info(
}),
}
}
- RoutingInfo::Trampoline { next_trampoline, new_packet_bytes, next_hop_hmac, shared_secret, current_path_key, incoming_multipath_data } => {
+ RoutingInfo::Trampoline { next_trampoline, new_packet_bytes, next_hop_hmac, shared_secret, current_path_key, incoming_multipath_data, next_trampoline_amt_msat, next_trampoline_cltv } => {
let next_trampoline_packet_pubkey = match next_packet_pubkey_opt {
Some(Ok(pubkey)) => pubkey,
_ => return Err(InboundHTLCErr {
@@ -269,6 +275,9 @@ pub(super) fn create_fwd_pending_htlc_info(
.unwrap_or(BlindedFailure::FromBlindedNode),
}),
incoming_multipath_data,
+ next_trampoline_amt_msat,
+ next_trampoline_cltv_expiry: next_trampoline_cltv,
+
}
}
};
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.