ln: add TrampolineForward variant to HTLCSource enum
What changed, and why it matters
This commit adds a new internal bookkeeping variant called TrampolineForward to LDK's HTLCSource enum, which tracks Lightning payments being forwarded through a trampoline routing node. It also adds placeholder 'todo!()' stubs in several code paths that are not yet implemented for this new variant. The commit is part of ongoing trampoline-payment feature work and does not by itself fix a known security bug. The unimplemented stubs could, in theory, cause a panic if a trampoline forward reaches those code paths, but the commit message and diff treat this as expected incomplete functionality rather than a disclosed vulnerability.
Treat as normal feature development; no immediate security patch required. Reviewers should ensure the remaining 'todo!()' arms are implemented before trampoline forwarding is enabled in production, and that the .expect() on missing outbound_payment cannot be triggered by network input.
Security signals we found
New enum variant for trampoline forwarding with unimplemented 'todo!()' match arms
Use of .expect() when deriving SentHTLCId if outbound_payment is None
Intentional deserialization rejection to prevent downgrades with in-flight trampoline forwards
No security-relevant description in commit message or diff comments
Evidence from the diff
The patch introduces HTLCSource::TrampolineForward { previous_hop_data, incoming_trampoline_shared_secret, outbound_payment } and a supporting TrampolineDispatch struct. It updates match arms in channelmonitor.rs to classify trampoline forwards as non-outbound payments, adds serialization/deserialization (with an intentional downgrade-blocking comment), and implements Hash/Writeable traits. Several match arms in channelmanager.rs are left as ‘todo!()’ for handling trampoline forwards during failure/claim/refund paths. The commit is explicitly incomplete (‘todo!()’ markers) and the message frames it as adding infrastructure for which full dispatch details are only available after payment dispatch.
Changed components
lightning/src/ln/channelmanager.rslightning/src/chain/channelmonitor.rslightning/src/routing/router.rsInspect captured patch +76 / −1
diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs
index a8d055a..f4d5714 100644
--- a/lightning/src/chain/channelmonitor.rs
+++ b/lightning/src/chain/channelmonitor.rs
@@ -2795,6 +2795,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
let outbound_payment = match source {
None => panic!("Outbound HTLCs should have a source"),
Some(&HTLCSource::PreviousHopData(_)) => false,
+ Some(&HTLCSource::TrampolineForward { .. }) => false,
Some(&HTLCSource::OutboundRoute { .. }) => true,
};
return Some(Balance::MaybeTimeoutClaimableHTLC {
@@ -3007,6 +3008,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {
let outbound_payment = match source {
None => panic!("Outbound HTLCs should have a source"),
Some(HTLCSource::PreviousHopData(_)) => false,
+ Some(HTLCSource::TrampolineForward { .. }) => false,
Some(HTLCSource::OutboundRoute { .. }) => true,
};
if outbound_payment {
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index 19322ba..053f8fe 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -759,12 +759,23 @@ pub(crate) enum SentHTLCId {
TrampolineForward { session_priv: [u8; SECRET_KEY_SIZE] },
}
impl SentHTLCId {
+ /// Creates an identifier for the [`HTLCSource`] provided. Note that for MPP trampoline payments
+ /// each outgoing HTLC will have a distinct identifier.
pub(crate) fn from_source(source: &HTLCSource) -> Self {
match source {
HTLCSource::PreviousHopData(hop_data) => Self::PreviousHopData {
prev_outbound_scid_alias: hop_data.prev_outbound_scid_alias,
htlc_id: hop_data.htlc_id,
},
+ HTLCSource::TrampolineForward {
+ ref outbound_payment,
+ ..
+ } => Self::TrampolineForward {
+ session_priv: outbound_payment
+ .as_ref()
+ .map(|o| o.session_priv.secret_bytes())
+ .expect("trying to identify a trampoline payment that we have no outbound_payment tracked for"),
+ },
HTLCSource::OutboundRoute { session_priv, .. } => {
Self::OutboundRoute { session_priv: session_priv.secret_bytes() }
},
@@ -789,11 +800,31 @@ type FailedHTLCForward = (HTLCSource, PaymentHash, HTLCFailReason, HTLCHandlingF
mod fuzzy_channelmanager {
use super::*;
+ /// Information about a HTLC sent as part of a (possibly MPP) payment to the next trampoline.
+ #[derive(Clone, Debug, PartialEq, Eq)]
+ pub struct TrampolineDispatch {
+ /// The payment ID used for the outbound payment.
+ pub payment_id: PaymentId,
+ /// The path used for the outbound payment.
+ pub path: Path,
+ /// The session private key used for inter-trampoline outer onions.
+ pub session_priv: SecretKey,
+ }
+
/// Tracks the inbound corresponding to an outbound HTLC
- #[allow(clippy::derive_hash_xor_eq)] // Our Hash is faithful to the data, we just don't have SecretKey::hash
+ #[allow(clippy::derive_hash_xor_eq, dead_code)] // Our Hash is faithful to the data, we just don't have SecretKey::hash
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum HTLCSource {
PreviousHopData(HTLCPreviousHopData),
+ TrampolineForward {
+ /// We might be forwarding an incoming payment that was received over MPP, and therefore
+ /// need to store the vector of corresponding `HTLCPreviousHopData` values.
+ previous_hop_data: Vec<HTLCPreviousHopData>,
+ incoming_trampoline_shared_secret: [u8; 32],
+ /// Track outbound payment details once the payment has been dispatched, will be `None`
+ /// when waiting for incoming MPP to accumulate.
+ outbound_payment: Option<TrampolineDispatch>,
+ },
OutboundRoute {
path: Path,
session_priv: SecretKey,
@@ -856,6 +887,20 @@ impl core::hash::Hash for HTLCSource {
first_hop_htlc_msat.hash(hasher);
bolt12_invoice.hash(hasher);
},
+ HTLCSource::TrampolineForward {
+ previous_hop_data,
+ incoming_trampoline_shared_secret,
+ outbound_payment,
+ } => {
+ 2u8.hash(hasher);
+ previous_hop_data.hash(hasher);
+ incoming_trampoline_shared_secret.hash(hasher);
+ if let Some(payment) = outbound_payment {
+ payment.payment_id.hash(hasher);
+ payment.path.hash(hasher);
+ payment.session_priv[..].hash(hasher);
+ }
+ },
}
}
}
@@ -9029,6 +9074,7 @@ impl<
None,
));
},
+ HTLCSource::TrampolineForward { .. } => todo!(),
}
}
@@ -9783,6 +9829,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
},
);
},
+ HTLCSource::TrampolineForward { .. } => todo!(),
}
}
@@ -17271,6 +17318,8 @@ impl Readable for HTLCSource {
})
}
1 => Ok(HTLCSource::PreviousHopData(Readable::read(reader)?)),
+ // Note: we intentionally do not read HTLCSource::TrampolineForward because we do not
+ // want to allow downgrades with in-flight trampoline forwards.
_ => Err(DecodeError::UnknownRequiredFeature),
}
}
@@ -17303,6 +17352,18 @@ impl Writeable for HTLCSource {
1u8.write(writer)?;
field.write(writer)?;
},
+ HTLCSource::TrampolineForward {
+ ref previous_hop_data,
+ incoming_trampoline_shared_secret,
+ ref outbound_payment,
+ } => {
+ 2u8.write(writer)?;
+ write_tlv_fields!(writer, {
+ (1, *previous_hop_data, required_vec),
+ (3, incoming_trampoline_shared_secret, required),
+ (5, outbound_payment, option),
+ });
+ },
}
Ok(())
}
@@ -17320,6 +17381,12 @@ impl_writeable_tlv_based!(PendingAddHTLCInfo, {
(9, prev_counterparty_node_id, required),
});
+impl_writeable_tlv_based!(TrampolineDispatch, {
+ (1, payment_id, required),
+ (3, path, required),
+ (5, session_priv, required),
+});
+
impl Writeable for HTLCForwardInfo {
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
const FAIL_HTLC_VARIANT_ID: u8 = 1;
@@ -19176,6 +19243,7 @@ impl<
} else { true }
});
},
+ HTLCSource::TrampolineForward { .. } => todo!(),
HTLCSource::OutboundRoute {
payment_id,
session_priv,
diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs
index 90697ad..874ea12 100644
--- a/lightning/src/routing/router.rs
+++ b/lightning/src/routing/router.rs
@@ -656,6 +656,11 @@ impl Path {
}
}
+impl_writeable_tlv_based!(Path,{
+ (1, hops, required_vec),
+ (3, blinded_tail, option),
+});
+
/// A route directs a payment from the sender (us) to the recipient. If the recipient supports MPP,
/// it can take multiple paths. Each path is composed of one or more hops through the network.
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
Why this scored 26/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.