Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5875ab3
ln/refactor: add previous_hop_data helper for HTLCSource
carlaKC Mar 2, 2026
cd2ff9b
ln/refactor: rename shared secret and populate in HTLCPreviousHopData
carlaKC Mar 12, 2026
030e485
ln/refactor: move MPP information into separate struct to ClaimableHTLC
carlaKC Mar 30, 2026
8c2c0ef
f remove constructor for ClaimableHTLC and MppPart
carlaKC Apr 10, 2026
0cbf163
ln/refactor: move mpp timeout into helper function
carlaKC Apr 10, 2026
98d5e85
f Remove single use mpp_timer_tick
carlaKC Apr 10, 2026
8f81a01
ln/refactor: move on chain timeout check into claimable htlc
carlaKC Apr 10, 2026
41d8c03
f hardcode HTLC timeout buffer
carlaKC Apr 10, 2026
aaa4b7f
ln/refactor: remove claimable htlc from fail_htlc macro
carlaKC Apr 10, 2026
c8cbe4f
ln/refactor: move checks on incoming mpp accumulation into method
carlaKC Mar 30, 2026
6089170
f remove comment
carlaKC Apr 10, 2026
3f81b28
f Use Err(()) rather than Err(_)
carlaKC Apr 7, 2026
381b653
ln/refactor: introduce HasMppPart generic to share incoming mpp
carlaKC Apr 10, 2026
8a21852
ln/refactor: pass minimum delta into check_incoming_htlc_cltv
carlaKC Feb 12, 2026
5b18620
blinded_path/refactor: make construction generic over forwarding type
carlaKC Mar 17, 2026
0b82628
f seal ForwardTlvsInfo trait
carlaKC Apr 10, 2026
1b15a0e
blinded_path: add constructor for trampoline blinded path
carlaKC Mar 17, 2026
380f744
ln/test: add multi-purpose trampoline test helper
carlaKC Mar 17, 2026
19b6c47
ln: remove incoming trampoline secret from HTLCSource
carlaKC Mar 12, 2026
49f0e48
ln: store incoming mpp data in PendingHTLCRouting
carlaKC Jan 27, 2026
4dcee8f
ln: use total_msat to calculate the amount for our next trampoline
carlaKC Feb 25, 2026
6d5f458
ln: use outer onion cltv values in PendingHTLCInfo for trampoline
carlaKC Feb 25, 2026
2c88311
ln: store next trampoline amount and cltv in PendingHTLCRouting
carlaKC Feb 25, 2026
a0f54e2
ln: use outer onion values for trampoline NextPacketDetails
carlaKC Feb 12, 2026
a3d5510
ln: add awaiting_trampoline_forwards to accumulate inbound MPP
carlaKC Mar 30, 2026
ee06fcc
f: don't pass hardcoded buffer to check_onchain_timeout
carlaKC Apr 10, 2026
bd05221
ln: add trampoline mpp accumulation with rejection on completion
carlaKC Apr 10, 2026
3849ac3
ln: double encrypt errors received from downstream failures
carlaKC Mar 12, 2026
53febfb
ln: handle DecodedOnionFailure for local trampoline failures
carlaKC Mar 12, 2026
448a641
ln: process added trampoline htlcs with CLTV validation
carlaKC Feb 25, 2026
05e344a
ln/test: add test coverage for MPP trampoline
carlaKC Mar 17, 2026
5be3abb
ln/test: add tests for mpp accumulation of trampoline forwards
carlaKC Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 106 additions & 21 deletions lightning/src/blinded_path/payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,35 @@ impl BlindedPaymentPath {
)
}

fn new_inner<ES: EntropySource, T: secp256k1::Signing + secp256k1::Verification>(
intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey,
/// Create a blinded path for a trampoline payment, to be forwarded along `intermediate_nodes`.
#[cfg(any(test, feature = "_test_utils"))]
pub(crate) fn new_for_trampoline<
ES: EntropySource,
T: secp256k1::Signing + secp256k1::Verification,
>(
intermediate_nodes: &[ForwardNode<TrampolineForwardTlvs>], payee_node_id: PublicKey,
local_node_receive_key: ReceiveAuthKey, payee_tlvs: ReceiveTlvs, htlc_maximum_msat: u64,
min_final_cltv_expiry_delta: u16, entropy_source: ES, secp_ctx: &Secp256k1<T>,
) -> Result<Self, ()> {
Self::new_inner(
intermediate_nodes,
payee_node_id,
local_node_receive_key,
&[],
payee_tlvs,
htlc_maximum_msat,
min_final_cltv_expiry_delta,
entropy_source,
secp_ctx,
)
}

fn new_inner<
F: ForwardTlvsInfo,
ES: EntropySource,
T: secp256k1::Signing + secp256k1::Verification,
>(
intermediate_nodes: &[ForwardNode<F>], payee_node_id: PublicKey,
local_node_receive_key: ReceiveAuthKey, dummy_tlvs: &[DummyTlvs], payee_tlvs: ReceiveTlvs,
htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16, entropy_source: ES,
secp_ctx: &Secp256k1<T>,
Expand Down Expand Up @@ -323,18 +350,42 @@ impl BlindedPaymentPath {
}
}

/// An intermediate node, its outbound channel, and relay parameters.
mod sealed {
pub trait ForwardTlvsInfo {}
}

/// Common interface for forward TLV types used in blinded payment paths.
///
/// Both [`ForwardTlvs`] (channel-based forwarding) and [`TrampolineForwardTlvs`] (trampoline
/// node-based forwarding) implement this trait, allowing blinded path construction to be generic
/// over the forwarding mechanism.
///
/// This trait is sealed and is not intended for implementation outside of this crate.
pub trait ForwardTlvsInfo: Writeable + Clone + sealed::ForwardTlvsInfo {
/// The payment relay parameters for this hop.
fn payment_relay(&self) -> &PaymentRelay;
/// The payment constraints for this hop.
fn payment_constraints(&self) -> &PaymentConstraints;
/// The features for this hop.
fn features(&self) -> &BlindedHopFeatures;
}

/// An intermediate node, its forwarding parameters, and its [`ForwardTlvsInfo`] for use in a
/// [`BlindedPaymentPath`].
#[derive(Clone, Debug)]
pub struct PaymentForwardNode {
pub struct ForwardNode<F: ForwardTlvsInfo> {
/// The TLVs for this node's [`BlindedHop`], where the fee parameters contained within are also
/// used for [`BlindedPayInfo`] construction.
pub tlvs: ForwardTlvs,
pub tlvs: F,
/// This node's pubkey.
pub node_id: PublicKey,
/// The maximum value, in msat, that may be accepted by this node.
pub htlc_maximum_msat: u64,
}

/// An intermediate node for a regular (non-trampoline) [`BlindedPaymentPath`].
pub type PaymentForwardNode = ForwardNode<ForwardTlvs>;

/// Data to construct a [`BlindedHop`] for forwarding a payment.
#[derive(Clone, Debug)]
pub struct ForwardTlvs {
Expand All @@ -354,6 +405,20 @@ pub struct ForwardTlvs {
pub next_blinding_override: Option<PublicKey>,
}

impl sealed::ForwardTlvsInfo for ForwardTlvs {}

impl ForwardTlvsInfo for ForwardTlvs {
fn payment_relay(&self) -> &PaymentRelay {
&self.payment_relay
}
fn payment_constraints(&self) -> &PaymentConstraints {
&self.payment_constraints
}
fn features(&self) -> &BlindedHopFeatures {
&self.features
}
}

/// Data to construct a [`BlindedHop`] for forwarding a Trampoline payment.
#[derive(Clone, Debug)]
pub struct TrampolineForwardTlvs {
Expand All @@ -373,6 +438,20 @@ pub struct TrampolineForwardTlvs {
pub next_blinding_override: Option<PublicKey>,
}

impl sealed::ForwardTlvsInfo for TrampolineForwardTlvs {}

impl ForwardTlvsInfo for TrampolineForwardTlvs {
fn payment_relay(&self) -> &PaymentRelay {
&self.payment_relay
}
fn payment_constraints(&self) -> &PaymentConstraints {
&self.payment_constraints
}
fn features(&self) -> &BlindedHopFeatures {
&self.features
}
}

/// TLVs carried by a dummy hop within a blinded payment path.
///
/// Dummy hops do not correspond to real forwarding decisions, but are processed
Expand Down Expand Up @@ -440,8 +519,8 @@ pub(crate) enum BlindedTrampolineTlvs {

// Used to include forward and receive TLVs in the same iterator for encoding.
#[derive(Clone)]
enum BlindedPaymentTlvsRef<'a> {
Forward(&'a ForwardTlvs),
enum BlindedPaymentTlvsRef<'a, F: ForwardTlvsInfo = ForwardTlvs> {
Forward(&'a F),
Dummy(&'a DummyTlvs),
Receive(&'a ReceiveTlvs),
}
Expand Down Expand Up @@ -619,7 +698,7 @@ impl Writeable for ReceiveTlvs {
}
}

impl<'a> Writeable for BlindedPaymentTlvsRef<'a> {
impl<'a, F: ForwardTlvsInfo> Writeable for BlindedPaymentTlvsRef<'a, F> {
fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
match self {
Self::Forward(tlvs) => tlvs.write(w)?,
Expand Down Expand Up @@ -723,8 +802,8 @@ impl Readable for BlindedTrampolineTlvs {
pub(crate) const PAYMENT_PADDING_ROUND_OFF: usize = 30;

/// Construct blinded payment hops for the given `intermediate_nodes` and payee info.
pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>(
secp_ctx: &Secp256k1<T>, intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey,
pub(super) fn blinded_hops<F: ForwardTlvsInfo, T: secp256k1::Signing + secp256k1::Verification>(
secp_ctx: &Secp256k1<T>, intermediate_nodes: &[ForwardNode<F>], payee_node_id: PublicKey,
dummy_tlvs: &[DummyTlvs], payee_tlvs: ReceiveTlvs, session_priv: &SecretKey,
local_node_receive_key: ReceiveAuthKey,
) -> Vec<BlindedHop> {
Expand Down Expand Up @@ -823,15 +902,15 @@ where
Ok((curr_base_fee, curr_prop_mil))
}

pub(super) fn compute_payinfo(
intermediate_nodes: &[PaymentForwardNode], dummy_tlvs: &[DummyTlvs], payee_tlvs: &ReceiveTlvs,
pub(super) fn compute_payinfo<F: ForwardTlvsInfo>(
intermediate_nodes: &[ForwardNode<F>], dummy_tlvs: &[DummyTlvs], payee_tlvs: &ReceiveTlvs,
payee_htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16,
) -> Result<BlindedPayInfo, ()> {
let routing_fees = intermediate_nodes
.iter()
.map(|node| RoutingFees {
base_msat: node.tlvs.payment_relay.fee_base_msat,
proportional_millionths: node.tlvs.payment_relay.fee_proportional_millionths,
base_msat: node.tlvs.payment_relay().fee_base_msat,
proportional_millionths: node.tlvs.payment_relay().fee_proportional_millionths,
})
.chain(dummy_tlvs.iter().map(|tlvs| RoutingFees {
base_msat: tlvs.payment_relay.fee_base_msat,
Expand All @@ -847,24 +926,24 @@ pub(super) fn compute_payinfo(
for node in intermediate_nodes.iter() {
// In the future, we'll want to take the intersection of all supported features for the
// `BlindedPayInfo`, but there are no features in that context right now.
if node.tlvs.features.requires_unknown_bits_from(&BlindedHopFeatures::empty()) {
if node.tlvs.features().requires_unknown_bits_from(&BlindedHopFeatures::empty()) {
return Err(());
}

cltv_expiry_delta =
cltv_expiry_delta.checked_add(node.tlvs.payment_relay.cltv_expiry_delta).ok_or(())?;
cltv_expiry_delta.checked_add(node.tlvs.payment_relay().cltv_expiry_delta).ok_or(())?;

// The min htlc for an intermediate node is that node's min minus the fees charged by all of the
// following hops for forwarding that min, since that fee amount will automatically be included
// in the amount that this node receives and contribute towards reaching its min.
htlc_minimum_msat = amt_to_forward_msat(
core::cmp::max(node.tlvs.payment_constraints.htlc_minimum_msat, htlc_minimum_msat),
&node.tlvs.payment_relay,
core::cmp::max(node.tlvs.payment_constraints().htlc_minimum_msat, htlc_minimum_msat),
node.tlvs.payment_relay(),
)
.unwrap_or(1); // If underflow occurs, we definitely reached this node's min
htlc_maximum_msat = amt_to_forward_msat(
core::cmp::min(node.htlc_maximum_msat, htlc_maximum_msat),
&node.tlvs.payment_relay,
node.tlvs.payment_relay(),
)
.ok_or(())?; // If underflow occurs, we cannot send to this hop without exceeding their max
}
Expand Down Expand Up @@ -1038,8 +1117,14 @@ mod tests {
payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 1 },
payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}),
};
let blinded_payinfo =
super::compute_payinfo(&[], &[], &recv_tlvs, 4242, TEST_FINAL_CLTV as u16).unwrap();
let blinded_payinfo = super::compute_payinfo::<ForwardTlvs>(
&[],
&[],
&recv_tlvs,
4242,
TEST_FINAL_CLTV as u16,
)
.unwrap();
assert_eq!(blinded_payinfo.fee_base_msat, 0);
assert_eq!(blinded_payinfo.fee_proportional_millionths, 0);
assert_eq!(blinded_payinfo.cltv_expiry_delta, TEST_FINAL_CLTV as u16);
Expand Down
Loading
Loading