Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8925925
ln/refactor: use amount_msat and counterparty_skimmed_fee_msat vars
carlaKC Apr 14, 2026
6168bed
ln: remove incoming trampoline secret from HTLCSource
carlaKC Mar 12, 2026
a54a4d9
ln: store incoming mpp data in PendingHTLCRouting
carlaKC Jan 27, 2026
98f00cb
f: don't rebind incoming_multipath_data in destructure
carlaKC May 12, 2026
7d012ae
ln: use total_msat to calculate the amount for our next trampoline
carlaKC Feb 25, 2026
c266716
f: clarify total_msat in check_blinded_forward for MPP
carlaKC May 12, 2026
bbeec58
ln: use outer onion values in PendingHTLCInfo for trampoline
carlaKC May 12, 2026
6b50ad6
ln: store next trampoline amount and cltv in PendingHTLCRouting
carlaKC May 12, 2026
75b316b
ln: use outer onion values for trampoline NextPacketDetails
carlaKC Feb 12, 2026
7b17d4f
ln: add awaiting_trampoline_forwards to accumulate inbound MPP
carlaKC Mar 30, 2026
05d9791
ln: add trampoline mpp accumulation with rejection on completion
carlaKC Apr 10, 2026
4b26edc
f: use doc comment for handle_trampoline_htlc
carlaKC May 12, 2026
dd81ad9
f: use compute_fees and document per-trampoline-hop fee
carlaKC May 12, 2026
4bac8b0
f: add trampoline mpp accumulation with rejection on completion
carlaKC May 12, 2026
164f203
ln: double encrypt errors received from downstream failures
carlaKC Mar 12, 2026
fd5dc1e
ln: handle DecodedOnionFailure for local trampoline failures
carlaKC Mar 12, 2026
90a0a4b
f: replace decoded_onion_failure macro with closure
carlaKC May 12, 2026
4e53faf
ln: process added trampoline htlcs with CLTV validation in tests
carlaKC Feb 25, 2026
ff4f993
ln/test: add test coverage for MPP trampoline
carlaKC Mar 17, 2026
5ba7094
ln/test: add tests for mpp accumulation of trampoline forwards
carlaKC Apr 27, 2026
e48b8f8
f: dispatch trampoline MPP via public send_payment_with_route
carlaKC May 12, 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
398 changes: 276 additions & 122 deletions lightning/src/ln/blinded_payment_tests.rs

Large diffs are not rendered by default.

408 changes: 371 additions & 37 deletions lightning/src/ln/channelmanager.rs

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions lightning/src/ln/functional_test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5776,12 +5776,16 @@ pub fn get_scid_from_channel_id<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, channel_id:
///
/// The resulting tail contains blinded hops built from `intermediate_nodes` plus a dummy receive
/// TLV, with the `TrampolineHop` fee and CLTV derived from the blinded path's aggregated payinfo.
/// The constructed [`BlindedPaymentPath`] is also returned so callers can register it in
/// [`PaymentParameters`].
///
/// [`PaymentParameters`]: crate::routing::router::PaymentParameters
pub fn create_trampoline_forward_blinded_tail<ES: EntropySource>(
secp_ctx: &bitcoin::secp256k1::Secp256k1<bitcoin::secp256k1::All>, entropy_source: ES,
intermediate_nodes: &[ForwardNode<TrampolineForwardTlvs>], payee_node_id: PublicKey,
payee_receive_key: ReceiveAuthKey, payee_tlvs: ReceiveTlvs, min_final_cltv_expiry_delta: u32,
excess_final_cltv_delta: u32, final_value_msat: u64,
) -> BlindedTail {
) -> (BlindedTail, BlindedPaymentPath) {
let blinded_path = BlindedPaymentPath::new_for_trampoline(
intermediate_nodes,
payee_node_id,
Expand All @@ -5794,7 +5798,7 @@ pub fn create_trampoline_forward_blinded_tail<ES: EntropySource>(
)
.unwrap();

BlindedTail {
let tail = BlindedTail {
trampoline_hops: vec![TrampolineHop {
pubkey: intermediate_nodes.first().map(|n| n.node_id).unwrap_or(payee_node_id),
node_features: types::features::Features::empty(),
Expand All @@ -5813,5 +5817,6 @@ pub fn create_trampoline_forward_blinded_tail<ES: EntropySource>(
blinding_point: blinded_path.blinding_point(),
excess_final_cltv_expiry_delta: excess_final_cltv_delta,
final_value_msat,
}
};
(tail, blinded_path)
}
2 changes: 2 additions & 0 deletions lightning/src/ln/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ mod reorg_tests;
mod shutdown_tests;
#[cfg(any(feature = "_test_utils", test))]
pub mod splicing_tests;
#[cfg(test)]
mod trampoline_forward_tests;
#[cfg(any(test, feature = "_externalize_tests"))]
#[allow(unused_mut)]
pub mod update_fee_tests;
Expand Down
60 changes: 34 additions & 26 deletions lightning/src/ln/onion_payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ enum RoutingInfo {
next_hop_hmac: [u8; 32],
shared_secret: SharedSecret,
current_path_key: Option<PublicKey>,
incoming_multipath_data: Option<msgs::FinalOnionHopData>,
next_trampoline_amt_msat: u64,
next_trampoline_cltv: u32,
},
}

Expand Down Expand Up @@ -167,24 +170,31 @@ pub(super) fn create_fwd_pending_htlc_info(
reason: LocalHTLCFailureReason::InvalidOnionPayload,
err_data: Vec::new(),
}),
onion_utils::Hop::TrampolineForward { next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => {
onion_utils::Hop::TrampolineForward { outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => {
(
RoutingInfo::Trampoline {
next_trampoline: next_trampoline_hop_data.next_trampoline,
new_packet_bytes: new_trampoline_packet_bytes,
next_hop_hmac: next_trampoline_hop_hmac,
shared_secret: trampoline_shared_secret,
current_path_key: None
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,
},
next_trampoline_hop_data.amt_to_forward,
next_trampoline_hop_data.outgoing_cltv_value,
outer_hop_data.amt_to_forward,
outer_hop_data.outgoing_cltv_value,
None,
None
)
},
onion_utils::Hop::TrampolineBlindedForward { outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => {
let (amt_to_forward, outgoing_cltv_value) = check_blinded_forward(
msg.amount_msat, msg.cltv_expiry, &next_trampoline_hop_data.payment_relay, &next_trampoline_hop_data.payment_constraints, &next_trampoline_hop_data.features
// The blinded path's payment_relay and payment_constraints apply to the aggregate
// 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(
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
Comment on lines +196 to +197
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes the input to check_blinded_forward from msg.amount_msat (single HTLC amount) to the MPP total_msat. This is significant: the fee computation in amt_to_forward_msat and the check_blinded_payment_constraints (including htlc_minimum_msat check) now operate on the total MPP amount rather than the per-HTLC amount.

For the fee computation, this is correct — the blinded relay parameters are designed to be applied to the total amount, not per-part. But check_blinded_payment_constraints at line 69 of this file calls check_blinded_payment_constraints(inbound_amt_msat, ...) which checks against htlc_minimum_msat. Using the total here means a per-HTLC amount below htlc_minimum_msat would still pass if the total is above it. Is that the intended behavior for trampoline MPP?

If multipath_trampoline_data is None, this falls back to msg.amount_msat which is the per-HTLC amount (non-MPP case) — that's correct.

).map_err(|()| {
// We should be returning malformed here if `msg.blinding_point` is set, but this is
// unreachable right now since we checked it in `decode_update_add_htlc_onion`.
Expand All @@ -200,10 +210,13 @@ pub(super) fn create_fwd_pending_htlc_info(
new_packet_bytes: new_trampoline_packet_bytes,
next_hop_hmac: next_trampoline_hop_hmac,
shared_secret: trampoline_shared_secret,
current_path_key: outer_hop_data.current_path_key
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,
},
amt_to_forward,
outgoing_cltv_value,
outer_hop_data.amt_to_forward,
outer_hop_data.outgoing_cltv_value,
next_trampoline_hop_data.intro_node_blinding_point,
next_trampoline_hop_data.next_blinding_override
)
Expand Down Expand Up @@ -233,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 } => {
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 {
Expand All @@ -260,7 +273,11 @@ pub(super) fn create_fwd_pending_htlc_info(
failure: intro_node_blinding_point
.map(|_| BlindedFailure::FromIntroductionNode)
.unwrap_or(BlindedFailure::FromBlindedNode),
})
}),
incoming_multipath_data,
next_trampoline_amt_msat,
next_trampoline_cltv_expiry: next_trampoline_cltv,

}
}
};
Expand Down Expand Up @@ -683,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
Expand Down
49 changes: 32 additions & 17 deletions lightning/src/ln/onion_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2128,6 +2128,10 @@ impl HTLCFailReason {
let mut err = err.clone();
let hold_time = hold_time.unwrap_or(0);

if let Some(secondary_shared_secret) = secondary_shared_secret {
process_failure_packet(&mut err, secondary_shared_secret, hold_time);
crypt_failure_packet(secondary_shared_secret, &mut err);
}
process_failure_packet(&mut err, incoming_packet_shared_secret, hold_time);
crypt_failure_packet(incoming_packet_shared_secret, &mut err);

Expand All @@ -2139,33 +2143,44 @@ impl HTLCFailReason {
pub(super) fn decode_onion_failure<T: secp256k1::Signing, L: Logger>(
&self, secp_ctx: &Secp256k1<T>, logger: &L, htlc_source: &HTLCSource,
) -> DecodedOnionFailure {
let decoded_onion_failure = |short_channel_id: Option<u64>,
_failure_reason: LocalHTLCFailureReason,
_data: &[u8]| {
DecodedOnionFailure {
network_update: None,
payment_failed_permanently: false,
short_channel_id,
failed_within_blinded_path: false,
hold_times: Vec::new(),
#[cfg(any(test, feature = "_test_utils"))]
onion_error_code: Some(_failure_reason),
#[cfg(any(test, feature = "_test_utils"))]
onion_error_data: Some(_data.to_vec()),
#[cfg(test)]
attribution_failed_channel: None,
}
};
match self.0 {
HTLCFailReasonRepr::LightningError { ref err, .. } => {
process_onion_failure(secp_ctx, logger, &htlc_source, err.clone())
},
#[allow(unused)]
HTLCFailReasonRepr::Reason { ref data, ref failure_reason } => {
// we get a fail_malformed_htlc from the first hop
// TODO: We'd like to generate a NetworkUpdate for temporary
// failures here, but that would be insufficient as find_route
// generally ignores its view of our own channels as we provide them via
// ChannelDetails.
if let &HTLCSource::OutboundRoute { ref path, .. } = htlc_source {
DecodedOnionFailure {
network_update: None,
payment_failed_permanently: false,
short_channel_id: Some(path.hops[0].short_channel_id),
failed_within_blinded_path: false,
hold_times: Vec::new(),
#[cfg(any(test, feature = "_test_utils"))]
onion_error_code: Some(*failure_reason),
#[cfg(any(test, feature = "_test_utils"))]
onion_error_data: Some(data.clone()),
#[cfg(test)]
attribution_failed_channel: None,
}
} else {
unreachable!();
match htlc_source {
&HTLCSource::OutboundRoute { ref path, .. } => decoded_onion_failure(
Some(path.hops[0].short_channel_id),
*failure_reason,
data,
),
&HTLCSource::TrampolineForward { ref outbound_payment, .. } => {
debug_assert!(outbound_payment.is_none());
decoded_onion_failure(None, *failure_reason, data)
},
_ => unreachable!(),
}
},
}
Expand Down
23 changes: 21 additions & 2 deletions lightning/src/ln/outbound_payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

use bitcoin::hashes::sha256::Hash as Sha256;
use bitcoin::hashes::Hash;
use bitcoin::secp256k1::{self, Secp256k1, SecretKey};
use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey};
use lightning_invoice::Bolt11Invoice;

use crate::blinded_path::{IntroductionNode, NodeIdLookUp};
Expand All @@ -21,7 +21,7 @@ use crate::ln::channelmanager::{
EventCompletionAction, HTLCSource, OptionalBolt11PaymentParams, PaymentCompleteUpdate,
PaymentId,
};
use crate::ln::msgs::DecodeError;
use crate::ln::msgs::{DecodeError, TrampolineOnionPacket};
use crate::ln::onion_utils;
use crate::ln::onion_utils::{DecodedOnionFailure, HTLCFailReason};
use crate::offers::invoice::{Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder};
Expand Down Expand Up @@ -167,6 +167,25 @@ pub(crate) enum PendingOutboundPayment {
},
}

#[derive(Clone, Eq, PartialEq)]
pub(crate) struct NextTrampolineHopInfo {
/// The Trampoline packet to include for the next Trampoline hop.
pub(crate) onion_packet: TrampolineOnionPacket,
/// If blinded, the current_path_key to set at the next Trampoline hop.
pub(crate) blinding_point: Option<PublicKey>,
/// The amount that the next trampoline is expecting to receive.
pub(crate) amount_msat: u64,
/// The cltv expiry height that the next trampoline is expecting.
pub(crate) cltv_expiry_height: u32,
}

impl_writeable_tlv_based!(NextTrampolineHopInfo, {
(1, onion_packet, required),
(3, blinding_point, option),
(5, amount_msat, required),
(7, cltv_expiry_height, required),
});

#[derive(Clone)]
pub(crate) struct RetryableInvoiceRequest {
pub(crate) invoice_request: InvoiceRequest,
Expand Down
Loading
Loading