From 900ffda4792121d8dcceaf64f3fc722d8506d5f2 Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 9 Oct 2025 23:00:40 +0530 Subject: [PATCH 1/6] Introduce Dummy BlindedPaymentTlv Dummy BlindedPaymentTlvs is an empty TLV inserted immediately before the actual ReceiveTlvs in a blinded path. Receivers treat these dummy hops as real hops, which prevents timing-based attacks. Allowing arbitrary dummy hops before the final ReceiveTlvs obscures the recipient's true position in the route and makes it harder for an onlooker to infer the destination, strengthening recipient privacy. --- lightning/src/blinded_path/payment.rs | 103 ++++++++++++++++++++------ lightning/src/ln/channelmanager.rs | 14 ++++ lightning/src/ln/msgs.rs | 27 ++++++- lightning/src/ln/onion_payment.rs | 16 ++++ lightning/src/ln/onion_utils.rs | 12 +++ 5 files changed, 149 insertions(+), 23 deletions(-) diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index 13ade222f5b..549eb38c38f 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -328,6 +328,37 @@ pub struct TrampolineForwardTlvs { pub next_blinding_override: Option, } +/// TLVs carried by a dummy hop within a blinded payment path. +/// +/// Dummy hops do not correspond to real forwarding decisions, but are processed +/// identically to real hops at the protocol level. The TLVs contained here define +/// the relay requirements and constraints that must be satisfied for the payment +/// to continue through this hop. +/// +/// By enforcing realistic relay semantics on dummy hops, the payment path remains +/// indistinguishable from a fully real route with respect to fees, CLTV deltas, and +/// validation behavior. +#[derive(Clone, Copy)] +pub struct DummyTlvs { + /// Relay requirements (fees and CLTV delta) that must be satisfied when + /// processing this dummy hop. + pub payment_relay: PaymentRelay, + /// Constraints that apply to the payment when relaying over this dummy hop. + pub payment_constraints: PaymentConstraints, +} + +impl Default for DummyTlvs { + fn default() -> Self { + let payment_relay = + PaymentRelay { cltv_expiry_delta: 0, fee_proportional_millionths: 0, fee_base_msat: 0 }; + + let payment_constraints = + PaymentConstraints { max_cltv_expiry: u32::MAX, htlc_minimum_msat: 0 }; + + Self { payment_relay, payment_constraints } + } +} + /// Data to construct a [`BlindedHop`] for receiving a payment. This payload is custom to LDK and /// may not be valid if received by another lightning implementation. #[derive(Clone, Debug)] @@ -346,6 +377,8 @@ pub struct ReceiveTlvs { pub(crate) enum BlindedPaymentTlvs { /// This blinded payment data is for a forwarding node. Forward(ForwardTlvs), + /// This blinded payment data is dummy and is to be peeled by receiving node. + Dummy(DummyTlvs), /// This blinded payment data is for the receiving node. Receive(ReceiveTlvs), } @@ -363,13 +396,14 @@ pub(crate) enum BlindedTrampolineTlvs { // Used to include forward and receive TLVs in the same iterator for encoding. enum BlindedPaymentTlvsRef<'a> { Forward(&'a ForwardTlvs), + Dummy(&'a DummyTlvs), Receive(&'a ReceiveTlvs), } /// Parameters for relaying over a given [`BlindedHop`]. /// /// [`BlindedHop`]: crate::blinded_path::BlindedHop -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct PaymentRelay { /// Number of blocks subtracted from an incoming HTLC's `cltv_expiry` for this [`BlindedHop`]. pub cltv_expiry_delta: u16, @@ -383,7 +417,7 @@ pub struct PaymentRelay { /// Constraints for relaying over a given [`BlindedHop`]. /// /// [`BlindedHop`]: crate::blinded_path::BlindedHop -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct PaymentConstraints { /// The maximum total CLTV that is acceptable when relaying a payment over this [`BlindedHop`]. pub max_cltv_expiry: u32, @@ -512,6 +546,17 @@ impl Writeable for TrampolineForwardTlvs { } } +impl Writeable for DummyTlvs { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + encode_tlv_stream!(w, { + (10, self.payment_relay, required), + (12, self.payment_constraints, required), + (65539, (), required), + }); + Ok(()) + } +} + // Note: The `authentication` TLV field was removed in LDK v0.3 following // the introduction of `ReceiveAuthKey`-based authentication for inbound // `BlindedPaymentPaths`s. Because we do not support receiving to those @@ -532,6 +577,7 @@ impl<'a> Writeable for BlindedPaymentTlvsRef<'a> { fn write(&self, w: &mut W) -> Result<(), io::Error> { match self { Self::Forward(tlvs) => tlvs.write(w)?, + Self::Dummy(tlvs) => tlvs.write(w)?, Self::Receive(tlvs) => tlvs.write(w)?, } Ok(()) @@ -552,28 +598,41 @@ impl Readable for BlindedPaymentTlvs { (14, features, (option, encoding: (BlindedHopFeatures, WithoutLength))), (65536, payment_secret, option), (65537, payment_context, option), + (65539, is_dummy, option) }); - if let Some(short_channel_id) = scid { - if payment_secret.is_some() { - return Err(DecodeError::InvalidValue); - } - Ok(BlindedPaymentTlvs::Forward(ForwardTlvs { - short_channel_id, - payment_relay: payment_relay.ok_or(DecodeError::InvalidValue)?, - payment_constraints: payment_constraints.0.unwrap(), - next_blinding_override, - features: features.unwrap_or_else(BlindedHopFeatures::empty), - })) - } else { - if payment_relay.is_some() || features.is_some() { - return Err(DecodeError::InvalidValue); - } - Ok(BlindedPaymentTlvs::Receive(ReceiveTlvs { - payment_secret: payment_secret.ok_or(DecodeError::InvalidValue)?, - payment_constraints: payment_constraints.0.unwrap(), - payment_context: payment_context.ok_or(DecodeError::InvalidValue)?, - })) + match ( + scid, + next_blinding_override, + payment_relay, + features, + payment_secret, + payment_context, + is_dummy, + ) { + (Some(short_channel_id), next_override, Some(relay), features, None, None, None) => { + Ok(BlindedPaymentTlvs::Forward(ForwardTlvs { + short_channel_id, + payment_relay: relay, + payment_constraints: payment_constraints.0.unwrap(), + next_blinding_override: next_override, + features: features.unwrap_or_else(BlindedHopFeatures::empty), + })) + }, + (None, None, None, None, Some(secret), Some(context), None) => { + Ok(BlindedPaymentTlvs::Receive(ReceiveTlvs { + payment_secret: secret, + payment_constraints: payment_constraints.0.unwrap(), + payment_context: context, + })) + }, + (None, None, Some(relay), None, None, None, Some(())) => { + Ok(BlindedPaymentTlvs::Dummy(DummyTlvs { + payment_relay: relay, + payment_constraints: payment_constraints.0.unwrap(), + })) + }, + _ => return Err(DecodeError::InvalidValue), } } } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 72585d69f80..aef57a66612 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5105,6 +5105,20 @@ where onion_utils::Hop::Forward { .. } | onion_utils::Hop::BlindedForward { .. } => { create_fwd_pending_htlc_info(msg, decoded_hop, shared_secret, next_packet_pubkey_opt) }, + onion_utils::Hop::Dummy { .. } => { + debug_assert!( + false, + "Reached unreachable dummy-hop HTLC. Dummy hops are peeled in \ + `process_pending_update_add_htlcs`, and the resulting HTLC is \ + re-enqueued for processing. Hitting this means the peel-and-requeue \ + step was missed." + ); + return Err(InboundHTLCErr { + msg: "Failed to decode update add htlc onion", + reason: LocalHTLCFailureReason::InvalidOnionPayload, + err_data: Vec::new(), + }) + }, onion_utils::Hop::TrampolineForward { .. } | onion_utils::Hop::TrampolineBlindedForward { .. } => { create_fwd_pending_htlc_info(msg, decoded_hop, shared_secret, next_packet_pubkey_opt) }, diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 8e230fab1d9..1a7d52ebca8 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -32,7 +32,7 @@ use bitcoin::secp256k1::PublicKey; use bitcoin::{secp256k1, Transaction, Witness}; use crate::blinded_path::message::BlindedMessagePath; -use crate::blinded_path::payment::{BlindedPaymentTlvs, ForwardTlvs, ReceiveTlvs}; +use crate::blinded_path::payment::{BlindedPaymentTlvs, DummyTlvs, ForwardTlvs, ReceiveTlvs}; use crate::blinded_path::payment::{BlindedTrampolineTlvs, TrampolineForwardTlvs}; use crate::ln::onion_utils; use crate::ln::types::ChannelId; @@ -2336,6 +2336,11 @@ mod fuzzy_internal_msgs { pub intro_node_blinding_point: Option, pub next_blinding_override: Option, } + pub struct InboundOnionDummyPayload { + pub payment_relay: PaymentRelay, + pub payment_constraints: PaymentConstraints, + pub intro_node_blinding_point: Option, + } pub struct InboundOnionBlindedReceivePayload { pub sender_intended_htlc_amt_msat: u64, pub total_msat: u64, @@ -2355,6 +2360,7 @@ mod fuzzy_internal_msgs { Receive(InboundOnionReceivePayload), BlindedForward(InboundOnionBlindedForwardPayload), BlindedReceive(InboundOnionBlindedReceivePayload), + Dummy(InboundOnionDummyPayload), } pub struct InboundTrampolineForwardPayload { @@ -3694,6 +3700,25 @@ where next_blinding_override, })) }, + ChaChaDualPolyReadAdapter { + readable: + BlindedPaymentTlvs::Dummy(DummyTlvs { payment_relay, payment_constraints }), + used_aad, + } => { + if amt.is_some() + || cltv_value.is_some() || total_msat.is_some() + || keysend_preimage.is_some() + || invoice_request.is_some() + || !used_aad + { + return Err(DecodeError::InvalidValue); + } + Ok(Self::Dummy(InboundOnionDummyPayload { + payment_relay, + payment_constraints, + intro_node_blinding_point, + })) + }, ChaChaDualPolyReadAdapter { readable: BlindedPaymentTlvs::Receive(receive_tlvs), used_aad, diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 1abe4330a25..c1d07f70486 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -149,6 +149,14 @@ pub(super) fn create_fwd_pending_htlc_info( (RoutingInfo::Direct { short_channel_id, new_packet_bytes, next_hop_hmac }, amt_to_forward, outgoing_cltv_value, intro_node_blinding_point, next_blinding_override) }, + onion_utils::Hop::Dummy { .. } => { + debug_assert!(false, "Dummy hop should have been peeled earlier"); + return Err(InboundHTLCErr { + msg: "Dummy Hop OnionHopData provided for us as an intermediary node", + reason: LocalHTLCFailureReason::InvalidOnionPayload, + err_data: Vec::new(), + }) + }, onion_utils::Hop::Receive { .. } | onion_utils::Hop::BlindedReceive { .. } => return Err(InboundHTLCErr { msg: "Final Node OnionHopData provided for us as an intermediary node", @@ -364,6 +372,14 @@ pub(super) fn create_recv_pending_htlc_info( msg: "Got blinded non final data with an HMAC of 0", }) }, + onion_utils::Hop::Dummy { .. } => { + debug_assert!(false, "Dummy hop should have been peeled earlier"); + return Err(InboundHTLCErr { + reason: LocalHTLCFailureReason::InvalidOnionBlinding, + err_data: vec![0; 32], + msg: "Got blinded non final data with an HMAC of 0", + }) + } onion_utils::Hop::TrampolineForward { .. } | onion_utils::Hop::TrampolineBlindedForward { .. } => { return Err(InboundHTLCErr { reason: LocalHTLCFailureReason::InvalidOnionPayload, diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 18aa43e27c6..7e879542eef 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -2223,6 +2223,17 @@ pub(crate) enum Hop { /// Bytes of the onion packet we're forwarding. new_packet_bytes: [u8; ONION_DATA_LEN], }, + /// This onion payload is dummy, and needs to be peeled by us. + Dummy { + /// Blinding point for introduction-node dummy hops. + dummy_hop_data: msgs::InboundOnionDummyPayload, + /// Shared secret for decrypting the next-hop public key. + shared_secret: SharedSecret, + /// HMAC of the next hop's onion packet. + next_hop_hmac: [u8; 32], + /// Onion packet bytes after this dummy layer is peeled. + new_packet_bytes: [u8; ONION_DATA_LEN], + }, /// This onion payload was for us, not for forwarding to a next-hop. Contains information for /// verifying the incoming payment. Receive { @@ -2277,6 +2288,7 @@ impl Hop { match self { Hop::Forward { shared_secret, .. } => shared_secret, Hop::BlindedForward { shared_secret, .. } => shared_secret, + Hop::Dummy { shared_secret, .. } => shared_secret, Hop::TrampolineForward { outer_shared_secret, .. } => outer_shared_secret, Hop::TrampolineBlindedForward { outer_shared_secret, .. } => outer_shared_secret, Hop::Receive { shared_secret, .. } => shared_secret, From 923949bcc0d8eb6b185043ee150641a6ed5b7ddb Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 9 Oct 2025 23:09:50 +0530 Subject: [PATCH 2/6] Introduce Dummy Hop support in Blinded Path Constructor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new constructor for blinded paths that allows specifying the number of dummy hops. This enables users to insert arbitrary hops before the real destination, enhancing privacy by making it harder to infer the sender–receiver distance or identify the final destination. Lays the groundwork for future use of dummy hops in blinded path construction. --- lightning/src/blinded_path/payment.rs | 119 ++++++++++++++++++++++++-- 1 file changed, 112 insertions(+), 7 deletions(-) diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index 549eb38c38f..f0bf3f92c3e 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -121,6 +121,61 @@ impl BlindedPaymentPath { local_node_receive_key: ReceiveAuthKey, payee_tlvs: ReceiveTlvs, htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16, entropy_source: ES, secp_ctx: &Secp256k1, ) -> Result + where + ES::Target: EntropySource, + { + BlindedPaymentPath::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, + ) + } + + /// Same as [`BlindedPaymentPath::new`], but allows specifying a number of dummy hops. + /// + /// Dummy TLVs allow callers to override the payment relay values used for dummy hops. + /// Any additional fees introduced by these dummy hops are ultimately paid to the final + /// recipient as part of the total amount. + /// + /// This improves privacy by making path-length analysis based on fee and CLTV delta + /// values less reliable. + /// + /// TODO: Add end-to-end tests validating fee aggregation, CLTV deltas, and + /// HTLC bounds when dummy hops are present, before exposing this API publicly. + pub(crate) fn new_with_dummy_hops( + intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey, + dummy_tlvs: &[DummyTlvs], local_node_receive_key: ReceiveAuthKey, payee_tlvs: ReceiveTlvs, + htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16, entropy_source: ES, + secp_ctx: &Secp256k1, + ) -> Result + where + ES::Target: EntropySource, + { + BlindedPaymentPath::new_inner( + intermediate_nodes, + payee_node_id, + local_node_receive_key, + dummy_tlvs, + payee_tlvs, + htlc_maximum_msat, + min_final_cltv_expiry_delta, + entropy_source, + secp_ctx, + ) + } + + fn new_inner( + intermediate_nodes: &[PaymentForwardNode], 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, + ) -> Result where ES::Target: EntropySource, { @@ -133,6 +188,7 @@ impl BlindedPaymentPath { let blinded_payinfo = compute_payinfo( intermediate_nodes, + dummy_tlvs, &payee_tlvs, htlc_maximum_msat, min_final_cltv_expiry_delta, @@ -145,6 +201,7 @@ impl BlindedPaymentPath { secp_ctx, intermediate_nodes, payee_node_id, + dummy_tlvs, payee_tlvs, &blinding_secret, local_node_receive_key, @@ -394,6 +451,7 @@ 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), Dummy(&'a DummyTlvs), @@ -679,21 +737,46 @@ 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( secp_ctx: &Secp256k1, intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey, - payee_tlvs: ReceiveTlvs, session_priv: &SecretKey, local_node_receive_key: ReceiveAuthKey, + dummy_tlvs: &[DummyTlvs], payee_tlvs: ReceiveTlvs, session_priv: &SecretKey, + local_node_receive_key: ReceiveAuthKey, ) -> Vec { let pks = intermediate_nodes .iter() .map(|node| (node.node_id, None)) + .chain(dummy_tlvs.iter().map(|_| (payee_node_id, Some(local_node_receive_key)))) .chain(core::iter::once((payee_node_id, Some(local_node_receive_key)))); let tlvs = intermediate_nodes .iter() .map(|node| BlindedPaymentTlvsRef::Forward(&node.tlvs)) + .chain(dummy_tlvs.iter().map(|tlvs| BlindedPaymentTlvsRef::Dummy(tlvs))) .chain(core::iter::once(BlindedPaymentTlvsRef::Receive(&payee_tlvs))); let path = pks.zip( tlvs.map(|tlv| BlindedPathWithPadding { tlvs: tlv, round_off: PAYMENT_PADDING_ROUND_OFF }), ); + // Debug invariant: all non-final hops must have identical serialized size. + #[cfg(debug_assertions)] + { + let mut iter = path.clone(); + if let Some((_, first)) = iter.next() { + let remaining = iter.clone().count(); // includes intermediate + final + + // At least one intermediate hop + if remaining > 1 { + let expected = first.serialized_length(); + + // skip final hop: take(remaining - 1) + for (_, hop) in iter.take(remaining - 1) { + debug_assert!( + hop.serialized_length() == expected, + "All intermediate blinded hops must have identical serialized size" + ); + } + } + } + } + utils::construct_blinded_hops(secp_ctx, path, session_priv) } @@ -753,14 +836,22 @@ where } pub(super) fn compute_payinfo( - intermediate_nodes: &[PaymentForwardNode], payee_tlvs: &ReceiveTlvs, + intermediate_nodes: &[PaymentForwardNode], dummy_tlvs: &[DummyTlvs], payee_tlvs: &ReceiveTlvs, payee_htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16, ) -> Result { - let (aggregated_base_fee, aggregated_prop_fee) = - compute_aggregated_base_prop_fee(intermediate_nodes.iter().map(|node| RoutingFees { + 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, - }))?; + }) + .chain(dummy_tlvs.iter().map(|tlvs| RoutingFees { + base_msat: tlvs.payment_relay.fee_base_msat, + proportional_millionths: tlvs.payment_relay.fee_proportional_millionths, + })); + + let (aggregated_base_fee, aggregated_prop_fee) = + compute_aggregated_base_prop_fee(routing_fees)?; let mut htlc_minimum_msat: u64 = 1; let mut htlc_maximum_msat: u64 = 21_000_000 * 100_000_000 * 1_000; // Total bitcoin supply @@ -789,6 +880,16 @@ pub(super) fn compute_payinfo( ) .ok_or(())?; // If underflow occurs, we cannot send to this hop without exceeding their max } + for dummy_tlvs in dummy_tlvs.iter() { + cltv_expiry_delta = + cltv_expiry_delta.checked_add(dummy_tlvs.payment_relay.cltv_expiry_delta).ok_or(())?; + + htlc_minimum_msat = amt_to_forward_msat( + core::cmp::max(dummy_tlvs.payment_constraints.htlc_minimum_msat, htlc_minimum_msat), + &dummy_tlvs.payment_relay, + ) + .unwrap_or(1); // If underflow occurs, we definitely reached this node's min + } htlc_minimum_msat = core::cmp::max(payee_tlvs.payment_constraints.htlc_minimum_msat, htlc_minimum_msat); htlc_maximum_msat = core::cmp::min(payee_htlc_maximum_msat, htlc_maximum_msat); @@ -933,7 +1034,7 @@ mod tests { }; let htlc_maximum_msat = 100_000; let blinded_payinfo = - super::compute_payinfo(&intermediate_nodes[..], &recv_tlvs, htlc_maximum_msat, 12) + super::compute_payinfo(&intermediate_nodes[..], &[], &recv_tlvs, htlc_maximum_msat, 12) .unwrap(); assert_eq!(blinded_payinfo.fee_base_msat, 201); assert_eq!(blinded_payinfo.fee_proportional_millionths, 1001); @@ -950,7 +1051,7 @@ mod tests { payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), }; let blinded_payinfo = - super::compute_payinfo(&[], &recv_tlvs, 4242, TEST_FINAL_CLTV as u16).unwrap(); + super::compute_payinfo(&[], &[], &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); @@ -1009,6 +1110,7 @@ mod tests { let htlc_maximum_msat = 100_000; let blinded_payinfo = super::compute_payinfo( &intermediate_nodes[..], + &[], &recv_tlvs, htlc_maximum_msat, TEST_FINAL_CLTV as u16, @@ -1068,6 +1170,7 @@ mod tests { let htlc_minimum_msat = 3798; assert!(super::compute_payinfo( &intermediate_nodes[..], + &[], &recv_tlvs, htlc_minimum_msat - 1, TEST_FINAL_CLTV as u16 @@ -1077,6 +1180,7 @@ mod tests { let htlc_maximum_msat = htlc_minimum_msat + 1; let blinded_payinfo = super::compute_payinfo( &intermediate_nodes[..], + &[], &recv_tlvs, htlc_maximum_msat, TEST_FINAL_CLTV as u16, @@ -1137,6 +1241,7 @@ mod tests { let blinded_payinfo = super::compute_payinfo( &intermediate_nodes[..], + &[], &recv_tlvs, 10_000, TEST_FINAL_CLTV as u16, From d2def54da241d4e740d19ee210f024adeb6cac53 Mon Sep 17 00:00:00 2001 From: shaavan Date: Mon, 20 Oct 2025 17:00:05 +0530 Subject: [PATCH 3/6] Introduce Payment Dummy Hop parsing mechanism --- lightning/src/blinded_path/payment.rs | 54 +++++++++++----------- lightning/src/ln/channelmanager.rs | 57 +++++++++++++++++++++++- lightning/src/ln/onion_payment.rs | 43 +++++++++++++++++- lightning/src/ln/onion_utils.rs | 64 ++++++++++++++++++++++++++- 4 files changed, 189 insertions(+), 29 deletions(-) diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index f0bf3f92c3e..b68be811cb4 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -33,7 +33,6 @@ use crate::util::ser::{ Writeable, Writer, }; -use core::mem; use core::ops::Deref; #[allow(unused_imports)] @@ -248,28 +247,31 @@ impl BlindedPaymentPath { NL::Target: NodeIdLookUp, T: secp256k1::Signing + secp256k1::Verification, { - match self.decrypt_intro_payload::(node_signer) { - Ok(( - BlindedPaymentTlvs::Forward(ForwardTlvs { short_channel_id, .. }), - control_tlvs_ss, - )) => { - let next_node_id = match node_id_lookup.next_node_id(short_channel_id) { - Some(node_id) => node_id, - None => return Err(()), - }; - let mut new_blinding_point = onion_utils::next_hop_pubkey( - secp_ctx, - self.inner_path.blinding_point, - control_tlvs_ss.as_ref(), - ) - .map_err(|_| ())?; - mem::swap(&mut self.inner_path.blinding_point, &mut new_blinding_point); - self.inner_path.introduction_node = IntroductionNode::NodeId(next_node_id); - self.inner_path.blinded_hops.remove(0); - Ok(()) - }, - _ => Err(()), - } + let (next_node_id, control_tlvs_ss) = + match self.decrypt_intro_payload::(node_signer).map_err(|_| ())? { + (BlindedPaymentTlvs::Forward(ForwardTlvs { short_channel_id, .. }), ss) => { + let node_id = node_id_lookup.next_node_id(short_channel_id).ok_or(())?; + (node_id, ss) + }, + (BlindedPaymentTlvs::Dummy(_), ss) => { + let node_id = node_signer.get_node_id(Recipient::Node)?; + (node_id, ss) + }, + _ => return Err(()), + }; + + let new_blinding_point = onion_utils::next_hop_pubkey( + secp_ctx, + self.inner_path.blinding_point, + control_tlvs_ss.as_ref(), + ) + .map_err(|_| ())?; + + self.inner_path.blinding_point = new_blinding_point; + self.inner_path.introduction_node = IntroductionNode::NodeId(next_node_id); + self.inner_path.blinded_hops.remove(0); + + Ok(()) } pub(crate) fn decrypt_intro_payload( @@ -291,9 +293,9 @@ impl BlindedPaymentPath { .map_err(|_| ())?; match (&readable, used_aad) { - (BlindedPaymentTlvs::Forward(_), false) | (BlindedPaymentTlvs::Receive(_), true) => { - Ok((readable, control_tlvs_ss)) - }, + (BlindedPaymentTlvs::Forward(_), false) + | (BlindedPaymentTlvs::Dummy(_), true) + | (BlindedPaymentTlvs::Receive(_), true) => Ok((readable, control_tlvs_ss)), _ => Err(()), } } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index aef57a66612..b55b1798d45 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -4974,6 +4974,11 @@ where ) -> Result<(), LocalHTLCFailureReason> { let outgoing_scid = match next_packet_details.outgoing_connector { HopConnector::ShortChannelId(scid) => scid, + HopConnector::Dummy => { + // Dummy hops are only used for path padding and must not reach HTLC processing. + debug_assert!(false, "Dummy hop reached HTLC handling."); + return Err(LocalHTLCFailureReason::InvalidOnionPayload); + } HopConnector::Trampoline(_) => { return Err(LocalHTLCFailureReason::InvalidTrampolineForward); } @@ -6878,6 +6883,7 @@ where fn process_pending_update_add_htlcs(&self) -> bool { let mut should_persist = false; let mut decode_update_add_htlcs = new_hash_map(); + let mut dummy_update_add_htlcs = new_hash_map(); mem::swap(&mut decode_update_add_htlcs, &mut self.decode_update_add_htlcs.lock().unwrap()); let get_htlc_failure_type = |outgoing_scid_opt: Option, payment_hash: PaymentHash| { @@ -6941,7 +6947,36 @@ where &*self.logger, &self.secp_ctx, ) { - Ok(decoded_onion) => decoded_onion, + Ok(decoded_onion) => match decoded_onion { + ( + onion_utils::Hop::Dummy { + dummy_hop_data, + next_hop_hmac, + new_packet_bytes, + .. + }, + Some(next_packet_details), + ) => { + let new_update_add_htlc = + onion_utils::peel_dummy_hop_update_add_htlc( + update_add_htlc, + dummy_hop_data, + next_hop_hmac, + new_packet_bytes, + next_packet_details, + &*self.node_signer, + &self.secp_ctx, + ); + + dummy_update_add_htlcs + .entry(incoming_scid_alias) + .or_insert_with(Vec::new) + .push(new_update_add_htlc); + + continue; + }, + _ => decoded_onion, + }, Err((htlc_fail, reason)) => { let failure_type = HTLCHandlingFailureType::InvalidOnion; @@ -6954,6 +6989,13 @@ where let outgoing_scid_opt = next_packet_details_opt.as_ref().and_then(|d| match d.outgoing_connector { HopConnector::ShortChannelId(scid) => Some(scid), + HopConnector::Dummy => { + debug_assert!( + false, + "Dummy hops must never be processed at this stage." + ); + None + }, HopConnector::Trampoline(_) => None, }); let shared_secret = next_hop.shared_secret().secret_bytes(); @@ -7097,6 +7139,19 @@ where )); } } + + // Merge peeled dummy HTLCs into the existing decode queue so they can be + // processed in the next iteration. We avoid replacing the whole queue + // (e.g. via mem::swap) because other threads may have enqueued new HTLCs + // meanwhile; merging preserves everything safely. + if !dummy_update_add_htlcs.is_empty() { + let mut decode_update_add_htlc_source = self.decode_update_add_htlcs.lock().unwrap(); + + for (incoming_scid_alias, htlcs) in dummy_update_add_htlcs.into_iter() { + decode_update_add_htlc_source.entry(incoming_scid_alias).or_default().extend(htlcs); + } + } + should_persist } diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index c1d07f70486..9e8672ab48c 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -494,7 +494,7 @@ where L::Target: Logger, { let (hop, next_packet_details_opt) = - decode_incoming_update_add_htlc_onion(msg, node_signer, logger, secp_ctx + decode_incoming_update_add_htlc_onion(msg, &*node_signer, &*logger, secp_ctx ).map_err(|(msg, failure_reason)| { let (reason, err_data) = match msg { HTLCFailureMsg::Malformed(_) => (failure_reason, Vec::new()), @@ -532,6 +532,29 @@ where // onion here and check it. create_fwd_pending_htlc_info(msg, hop, shared_secret.secret_bytes(), Some(next_packet_pubkey))? }, + onion_utils::Hop::Dummy { dummy_hop_data, next_hop_hmac, new_packet_bytes, .. } => { + let next_packet_details = match next_packet_details_opt { + Some(next_packet_details) => next_packet_details, + // Dummy Hops should always include the next hop details + None => return Err(InboundHTLCErr { + msg: "Failed to decode update add htlc onion", + reason: LocalHTLCFailureReason::InvalidOnionPayload, + err_data: Vec::new(), + }), + }; + + let new_update_add_htlc = onion_utils::peel_dummy_hop_update_add_htlc( + msg, + dummy_hop_data, + next_hop_hmac, + new_packet_bytes, + next_packet_details, + &*node_signer, + secp_ctx + ); + + peel_payment_onion(&new_update_add_htlc, node_signer, logger, secp_ctx, cur_height, allow_skimmed_fees)? + }, _ => { let shared_secret = hop.shared_secret().secret_bytes(); create_recv_pending_htlc_info( @@ -545,6 +568,8 @@ where pub(super) enum HopConnector { // scid-based routing ShortChannelId(u64), + // Dummy hop for path padding + Dummy, // Trampoline-based routing #[allow(unused)] Trampoline(PublicKey), @@ -649,6 +674,22 @@ where outgoing_cltv_value }) } + onion_utils::Hop::Dummy { dummy_hop_data: msgs::InboundOnionDummyPayload { ref payment_relay, ref payment_constraints, .. }, shared_secret, .. } => { + let (amt_to_forward, outgoing_cltv_value) = match check_blinded_forward( + msg.amount_msat, msg.cltv_expiry, &payment_relay, &payment_constraints, &BlindedHopFeatures::empty() + ) { + Ok((amt, cltv)) => (amt, cltv), + Err(()) => { + return encode_relay_error("Underflow calculating outbound amount or cltv value for blinded forward", + LocalHTLCFailureReason::InvalidOnionBlinding, shared_secret.secret_bytes(), None, &[0; 32]); + } + }; + + let next_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx, + msg.onion_routing_packet.public_key.unwrap(), &shared_secret.secret_bytes()); + + 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, .. } => { let next_trampoline_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx, incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes()); diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 7e879542eef..b82c60a896d 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -14,7 +14,8 @@ use crate::crypto::streams::ChaChaReader; use crate::events::HTLCHandlingFailureReason; use crate::ln::channel::TOTAL_BITCOIN_SUPPLY_SATOSHIS; use crate::ln::channelmanager::{HTLCSource, RecipientOnionFields}; -use crate::ln::msgs::{self, DecodeError}; +use crate::ln::msgs::{self, DecodeError, InboundOnionDummyPayload, OnionPacket, UpdateAddHTLC}; +use crate::ln::onion_payment::{HopConnector, NextPacketDetails}; use crate::offers::invoice_request::InvoiceRequest; use crate::routing::gossip::NetworkUpdate; use crate::routing::router::{BlindedTail, Path, RouteHop, RouteParameters, TrampolineHop}; @@ -2356,6 +2357,12 @@ where new_packet_bytes, }) }, + msgs::InboundOnionPayload::Dummy(dummy_hop_data) => Ok(Hop::Dummy { + dummy_hop_data, + shared_secret, + next_hop_hmac, + new_packet_bytes, + }), _ => { if blinding_point.is_some() { return Err(OnionDecodeErr::Malformed { @@ -2533,6 +2540,61 @@ where } } +/// Peels a single dummy hop from an inbound `UpdateAddHTLC` by reconstructing the next +/// onion packet and HTLC state. +/// +/// This helper is used when processing dummy hops in a blinded path. Dummy hops are not +/// forwarded on the network; instead, their onion layer is removed locally and a new +/// `UpdateAddHTLC` is constructed with the next onion packet and updated amount/CLTV +/// values. +/// +/// This function performs no validation and does not enqueue or forward the HTLC. +/// It only reconstructs the next `UpdateAddHTLC` for further local processing. +pub(super) fn peel_dummy_hop_update_add_htlc( + msg: &UpdateAddHTLC, dummy_hop_data: InboundOnionDummyPayload, next_hop_hmac: [u8; 32], + new_packet_bytes: [u8; ONION_DATA_LEN], next_packet_details: NextPacketDetails, + node_signer: NS, secp_ctx: &Secp256k1, +) -> UpdateAddHTLC +where + NS::Target: NodeSigner, +{ + let NextPacketDetails { + next_packet_pubkey, + outgoing_amt_msat, + outgoing_connector, + outgoing_cltv_value, + } = next_packet_details; + + debug_assert!( + matches!(outgoing_connector, HopConnector::Dummy), + "Dummy hop must always map to HopConnector::Dummy" + ); + + let next_blinding_point = dummy_hop_data + .intro_node_blinding_point + .or(msg.blinding_point) + .and_then(|blinding_point| { + let ss = node_signer.ecdh(Recipient::Node, &blinding_point, None).ok()?.secret_bytes(); + + next_hop_pubkey(secp_ctx, blinding_point, &ss).ok() + }); + + let new_onion_packet = OnionPacket { + version: 0, + public_key: next_packet_pubkey, + hop_data: new_packet_bytes, + hmac: next_hop_hmac, + }; + + UpdateAddHTLC { + onion_routing_packet: new_onion_packet, + blinding_point: next_blinding_point, + amount_msat: outgoing_amt_msat, + cltv_expiry: outgoing_cltv_value, + ..msg.clone() + } +} + /// Build a payment onion, returning the first hop msat and cltv values as well. /// `cur_block_height` should be set to the best known block height + 1. pub fn create_payment_onion( From 6abf36e1366115d499821f039bbfb86336ce0b34 Mon Sep 17 00:00:00 2001 From: shaavan Date: Tue, 21 Oct 2025 18:58:53 +0530 Subject: [PATCH 4/6] Update PaymentPath, and ClaimAlongRoute arguments Upcoming commits will need the ability to specify whether a blinded path contains dummy hops. This change adds that support to the testing framework ahead of time, so later tests can express dummy-hop scenarios explicitly. --- lightning/src/ln/functional_test_utils.rs | 62 +++++++++++++++++++++-- lightning/src/ln/offers_tests.rs | 48 +++++++++++++----- 2 files changed, 93 insertions(+), 17 deletions(-) diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index ff33d7508b5..6d607eb2771 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -10,6 +10,7 @@ //! A bunch of useful utilities for building networks of nodes and exchanging messages between //! nodes for functional tests. +use crate::blinded_path::payment::DummyTlvs; use crate::chain::channelmonitor::ChannelMonitor; use crate::chain::transaction::OutPoint; use crate::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen, Watch}; @@ -3435,6 +3436,7 @@ fn fail_payment_along_path<'a, 'b, 'c>(expected_path: &[&Node<'a, 'b, 'c>]) { pub struct PassAlongPathArgs<'a, 'b, 'c, 'd> { pub origin_node: &'a Node<'b, 'c, 'd>, pub expected_path: &'a [&'a Node<'b, 'c, 'd>], + pub dummy_tlvs: Vec, pub recv_value: u64, pub payment_hash: PaymentHash, pub payment_secret: Option, @@ -3456,6 +3458,7 @@ impl<'a, 'b, 'c, 'd> PassAlongPathArgs<'a, 'b, 'c, 'd> { Self { origin_node, expected_path, + dummy_tlvs: vec![], recv_value, payment_hash, payment_secret: None, @@ -3503,12 +3506,17 @@ impl<'a, 'b, 'c, 'd> PassAlongPathArgs<'a, 'b, 'c, 'd> { self.expected_failure = Some(failure); self } + pub fn with_dummy_tlvs(mut self, dummy_tlvs: &[DummyTlvs]) -> Self { + self.dummy_tlvs = dummy_tlvs.to_vec(); + self + } } pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option { let PassAlongPathArgs { origin_node, expected_path, + dummy_tlvs, recv_value, payment_hash: our_payment_hash, payment_secret: our_payment_secret, @@ -3543,6 +3551,16 @@ pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option node.node.process_pending_htlc_forwards(); } + if is_last_hop { + // At the final hop, the incoming packet contains N dummy-hop layers + // before the real HTLC. Each call to `process_pending_htlc_forwards` + // strips exactly one dummy layer, so we call it N times. + for _ in 0..dummy_tlvs.len() { + assert!(node.node.needs_pending_htlc_processing()); + node.node.process_pending_htlc_forwards(); + } + } + if is_last_hop && clear_recipient_events { let events_2 = node.node.get_and_clear_pending_events(); if payment_claimable_expected { @@ -3755,6 +3773,29 @@ pub struct ClaimAlongRouteArgs<'a, 'b, 'c, 'd> { pub origin_node: &'a Node<'b, 'c, 'd>, pub expected_paths: &'a [&'a [&'a Node<'b, 'c, 'd>]], pub expected_extra_fees: Vec, + /// A one-off adjustment used only in tests to account for an existing + /// fee-handling trade-off in LDK. + /// + /// When the payer is the introduction node of a blinded path, LDK does not + /// subtract the forward fee for the `payer -> next_hop` channel + /// (see [`BlindedPaymentPath::advance_path_by_one`]). This keeps the fee + /// logic simpler at the cost of a small, intentional overpayment. + /// + /// In the simple two-hop case (payer as introduction node → payee), + /// this overpayment has historically been avoided by simply not charging + /// the payer the forward fee, since the payer knows there is only + /// a single hop after them. + /// + /// However, with the introduction of dummy hops in LDK v0.3, even a + /// two-node real path (payer as introduction node → payee) may appear as a + /// multi-hop blinded path. This makes the existing overpayment surface in + /// tests. + /// + /// Until the fee-handling trade-off is revisited, this field allows tests + /// to compensate for that expected difference. + /// + /// [`BlindedPaymentPath::advance_path_by_one`]: crate::blinded_path::payment::BlindedPaymentPath::advance_path_by_one + pub expected_extra_total_fees_msat: u64, pub expected_min_htlc_overpay: Vec, pub skip_last: bool, pub payment_preimage: PaymentPreimage, @@ -3778,6 +3819,7 @@ impl<'a, 'b, 'c, 'd> ClaimAlongRouteArgs<'a, 'b, 'c, 'd> { origin_node, expected_paths, expected_extra_fees: vec![0; expected_paths.len()], + expected_extra_total_fees_msat: 0, expected_min_htlc_overpay: vec![0; expected_paths.len()], skip_last: false, payment_preimage, @@ -3793,6 +3835,10 @@ impl<'a, 'b, 'c, 'd> ClaimAlongRouteArgs<'a, 'b, 'c, 'd> { self.expected_extra_fees = extra_fees; self } + pub fn with_expected_extra_total_fees_msat(mut self, extra_total_fees: u64) -> Self { + self.expected_extra_total_fees_msat = extra_total_fees; + self + } pub fn with_expected_min_htlc_overpay(mut self, extra_fees: Vec) -> Self { self.expected_min_htlc_overpay = extra_fees; self @@ -4060,13 +4106,21 @@ pub fn pass_claimed_payment_along_route_from_ev( expected_total_fee_msat } + pub fn claim_payment_along_route( args: ClaimAlongRouteArgs, ) -> (Option, Vec) { - let origin_node = args.origin_node; - let payment_preimage = args.payment_preimage; - let skip_last = args.skip_last; - let expected_total_fee_msat = do_claim_payment_along_route(args); + let ClaimAlongRouteArgs { + origin_node, + payment_preimage, + skip_last, + expected_extra_total_fees_msat, + .. + } = args; + + let expected_total_fee_msat = + do_claim_payment_along_route(args) + expected_extra_total_fees_msat; + if !skip_last { expect_payment_sent!(origin_node, payment_preimage, Some(expected_total_fee_msat)) } else { diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 4c53aefe58d..0b2d5b86add 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -185,7 +185,20 @@ fn route_bolt12_payment<'a, 'b, 'c>( fn claim_bolt12_payment<'a, 'b, 'c>( node: &Node<'a, 'b, 'c>, path: &[&Node<'a, 'b, 'c>], expected_payment_context: PaymentContext, invoice: &Bolt12Invoice ) { - let recipient = &path[path.len() - 1]; + claim_bolt12_payment_with_extra_fees( + node, + path, + expected_payment_context, + invoice, + None, + ) +} + +fn claim_bolt12_payment_with_extra_fees<'a, 'b, 'c>( + node: &Node<'a, 'b, 'c>, path: &[&Node<'a, 'b, 'c>], expected_payment_context: PaymentContext, invoice: &Bolt12Invoice, + expected_extra_fees_msat: Option, +) { + let recipient = path.last().expect("Empty path?"); let payment_purpose = match get_event!(recipient, Event::PaymentClaimable) { Event::PaymentClaimable { purpose, .. } => purpose, _ => panic!("No Event::PaymentClaimable"), @@ -194,20 +207,29 @@ fn claim_bolt12_payment<'a, 'b, 'c>( Some(preimage) => preimage, None => panic!("No preimage in Event::PaymentClaimable"), }; - match payment_purpose { - PaymentPurpose::Bolt12OfferPayment { payment_context, .. } => { - assert_eq!(PaymentContext::Bolt12Offer(payment_context), expected_payment_context); - }, - PaymentPurpose::Bolt12RefundPayment { payment_context, .. } => { - assert_eq!(PaymentContext::Bolt12Refund(payment_context), expected_payment_context); - }, + let context = match payment_purpose { + PaymentPurpose::Bolt12OfferPayment { payment_context, .. } => + PaymentContext::Bolt12Offer(payment_context), + PaymentPurpose::Bolt12RefundPayment { payment_context, .. } => + PaymentContext::Bolt12Refund(payment_context), _ => panic!("Unexpected payment purpose: {:?}", payment_purpose), - } - if let Some(inv) = claim_payment(node, path, payment_preimage) { - assert_eq!(inv, PaidBolt12Invoice::Bolt12Invoice(invoice.to_owned())); - } else { - panic!("Expected PaidInvoice::Bolt12Invoice"); }; + + assert_eq!(context, expected_payment_context); + + let expected_paths = [path]; + let mut args = ClaimAlongRouteArgs::new( + node, + &expected_paths, + payment_preimage, + ); + + if let Some(extra) = expected_extra_fees_msat { + args = args.with_expected_extra_total_fees_msat(extra); + } + + let (inv, _) = claim_payment_along_route(args); + assert_eq!(inv, Some(PaidBolt12Invoice::Bolt12Invoice(invoice.clone()))); } fn extract_offer_nonce<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, message: &OnionMessage) -> Nonce { From 5981c172cbba5c3f3400923c1c143771fe0fc6b5 Mon Sep 17 00:00:00 2001 From: shaavan Date: Tue, 18 Nov 2025 23:26:03 +0530 Subject: [PATCH 5/6] Introduce payment dummy hops in DefaultRouter --- lightning-dns-resolver/src/lib.rs | 7 +++ lightning/src/ln/async_payments_tests.rs | 65 ++++++++++++++++-------- lightning/src/ln/offers_tests.rs | 35 +++++++++---- lightning/src/routing/router.rs | 19 ++++--- 4 files changed, 89 insertions(+), 37 deletions(-) diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs index 125d4316d12..b7f429de6c8 100644 --- a/lightning-dns-resolver/src/lib.rs +++ b/lightning-dns-resolver/src/lib.rs @@ -175,6 +175,7 @@ mod test { use lightning::onion_message::messenger::{ AOnionMessenger, Destination, MessageRouter, OnionMessagePath, OnionMessenger, }; + use lightning::routing::router::DEFAULT_PAYMENT_DUMMY_HOPS; use lightning::sign::{KeysManager, NodeSigner, ReceiveAuthKey, Recipient}; use lightning::types::features::InitFeatures; use lightning::types::payment::PaymentHash; @@ -419,6 +420,12 @@ mod test { let updates = get_htlc_update_msgs(&nodes[0], &payee_id); nodes[1].node.handle_update_add_htlc(payer_id, &updates.update_add_htlcs[0]); do_commitment_signed_dance(&nodes[1], &nodes[0], &updates.commitment_signed, false, false); + + for _ in 0..DEFAULT_PAYMENT_DUMMY_HOPS { + assert!(nodes[1].node.needs_pending_htlc_processing()); + nodes[1].node.process_pending_htlc_forwards(); + } + expect_and_process_pending_htlcs(&nodes[1], false); let claimable_events = nodes[1].node.get_and_clear_pending_events(); diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index 8e7fbdf94fd..a485e77f76b 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -10,8 +10,8 @@ use crate::blinded_path::message::{ BlindedMessagePath, MessageContext, NextMessageHop, OffersContext, }; -use crate::blinded_path::payment::PaymentContext; use crate::blinded_path::payment::{AsyncBolt12OfferContext, BlindedPaymentTlvs}; +use crate::blinded_path::payment::{DummyTlvs, PaymentContext}; use crate::chain::channelmonitor::{HTLC_FAIL_BACK_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS}; use crate::events::{ Event, EventsProvider, HTLCHandlingFailureReason, HTLCHandlingFailureType, PaidBolt12Invoice, @@ -55,7 +55,7 @@ use crate::onion_message::messenger::{ use crate::onion_message::offers::OffersMessage; use crate::onion_message::packet::ParsedOnionMessageContents; use crate::prelude::*; -use crate::routing::router::{Payee, PaymentParameters}; +use crate::routing::router::{Payee, PaymentParameters, DEFAULT_PAYMENT_DUMMY_HOPS}; use crate::sign::NodeSigner; use crate::sync::Mutex; use crate::types::features::Bolt12InvoiceFeatures; @@ -984,7 +984,8 @@ fn ignore_duplicate_invoice() { check_added_monitors!(sender, 1); let route: &[&[&Node]] = &[&[always_online_node, async_recipient]]; - let args = PassAlongPathArgs::new(sender, route[0], amt_msat, payment_hash, ev); + let args = PassAlongPathArgs::new(sender, route[0], amt_msat, payment_hash, ev) + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); let claimable_ev = do_pass_along_path(args).unwrap(); let keysend_preimage = extract_payment_preimage(&claimable_ev); let (res, _) = @@ -1063,7 +1064,8 @@ fn ignore_duplicate_invoice() { check_added_monitors!(sender, 1); let args = PassAlongPathArgs::new(sender, route[0], amt_msat, payment_hash, ev) - .without_clearing_recipient_events(); + .without_clearing_recipient_events() + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); do_pass_along_path(args); let payment_preimage = match get_event!(async_recipient, Event::PaymentClaimable) { @@ -1138,7 +1140,8 @@ fn async_receive_flow_success() { assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; - let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev); + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); let claimable_ev = do_pass_along_path(args).unwrap(); let keysend_preimage = extract_payment_preimage(&claimable_ev); let (res, _) = @@ -1375,11 +1378,13 @@ fn async_receive_mpp() { }; let args = PassAlongPathArgs::new(&nodes[0], expected_route[0], amt_msat, payment_hash, ev) - .without_claimable_event(); + .without_claimable_event() + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); do_pass_along_path(args); let ev = remove_first_msg_event_to_node(&nodes[2].node.get_our_node_id(), &mut events); - let args = PassAlongPathArgs::new(&nodes[0], expected_route[1], amt_msat, payment_hash, ev); + let args = PassAlongPathArgs::new(&nodes[0], expected_route[1], amt_msat, payment_hash, ev) + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); let claimable_ev = do_pass_along_path(args).unwrap(); let keysend_preimage = match claimable_ev { Event::PaymentClaimable { @@ -1497,7 +1502,8 @@ fn amount_doesnt_match_invreq() { let route: &[&[&Node]] = &[&[&nodes[1], &nodes[3]]]; let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) .without_claimable_event() - .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }); + .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }) + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); do_pass_along_path(args); // Modify the invoice request stored in our outbounds to be the correct one, to make sure the @@ -1521,7 +1527,8 @@ fn amount_doesnt_match_invreq() { ev, MessageSendEvent::UpdateHTLCs { ref updates, .. } if updates.update_add_htlcs.len() == 1)); check_added_monitors!(nodes[0], 1); let route: &[&[&Node]] = &[&[&nodes[2], &nodes[3]]]; - let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev); + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); let claimable_ev = do_pass_along_path(args).unwrap(); let keysend_preimage = extract_payment_preimage(&claimable_ev); claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage)); @@ -1712,7 +1719,8 @@ fn invalid_async_receive_with_retry( let payment_hash = extract_payment_hash(&ev); let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; - let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev); + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); do_pass_along_path(args); // Fail the HTLC backwards to enable us to more easily modify the now-Retryable outbound to test @@ -1739,7 +1747,8 @@ fn invalid_async_receive_with_retry( let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) .without_claimable_event() - .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }); + .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }) + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); do_pass_along_path(args); fail_blinded_htlc_backwards(payment_hash, 1, &[&nodes[0], &nodes[1], &nodes[2]], true); @@ -1751,7 +1760,8 @@ fn invalid_async_receive_with_retry( let mut ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); check_added_monitors!(nodes[0], 1); let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; - let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev); + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); let claimable_ev = do_pass_along_path(args).unwrap(); let keysend_preimage = extract_payment_preimage(&claimable_ev); claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage)); @@ -1858,6 +1868,13 @@ fn expired_static_invoice_payment_path() { blinded_path .advance_path_by_one(&nodes[1].keys_manager, &nodes[1].node, &secp_ctx) .unwrap(); + + for _ in 0..DEFAULT_PAYMENT_DUMMY_HOPS { + blinded_path + .advance_path_by_one(&nodes[2].keys_manager, &nodes[2].node, &secp_ctx) + .unwrap(); + } + match blinded_path.decrypt_intro_payload(&nodes[2].keys_manager).unwrap().0 { BlindedPaymentTlvs::Receive(tlvs) => tlvs.payment_constraints.max_cltv_expiry, _ => panic!(), @@ -1920,7 +1937,8 @@ fn expired_static_invoice_payment_path() { let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) .without_claimable_event() - .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }); + .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }) + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); do_pass_along_path(args); fail_blinded_htlc_backwards(payment_hash, 1, &[&nodes[0], &nodes[1], &nodes[2]], false); nodes[2].logger.assert_log_contains( @@ -2363,7 +2381,8 @@ fn refresh_static_invoices_for_used_offers() { check_added_monitors!(sender, 1); let route: &[&[&Node]] = &[&[server, recipient]]; - let args = PassAlongPathArgs::new(sender, route[0], amt_msat, payment_hash, ev); + let args = PassAlongPathArgs::new(sender, route[0], amt_msat, payment_hash, ev) + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); let claimable_ev = do_pass_along_path(args).unwrap(); let keysend_preimage = extract_payment_preimage(&claimable_ev); let res = claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage)); @@ -2697,7 +2716,8 @@ fn invoice_server_is_not_channel_peer() { check_added_monitors!(sender, 1); let route: &[&[&Node]] = &[&[forwarding_node, recipient]]; - let args = PassAlongPathArgs::new(sender, route[0], amt_msat, payment_hash, ev); + let args = PassAlongPathArgs::new(sender, route[0], amt_msat, payment_hash, ev) + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); let claimable_ev = do_pass_along_path(args).unwrap(); let keysend_preimage = extract_payment_preimage(&claimable_ev); let res = claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage)); @@ -2936,7 +2956,8 @@ fn async_payment_e2e() { check_added_monitors!(sender_lsp, 1); let path: &[&Node] = &[invoice_server, recipient]; - let args = PassAlongPathArgs::new(sender_lsp, path, amt_msat, payment_hash, ev); + let args = PassAlongPathArgs::new(sender_lsp, path, amt_msat, payment_hash, ev) + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); let claimable_ev = do_pass_along_path(args).unwrap(); let route: &[&[&Node]] = &[&[sender_lsp, invoice_server, recipient]]; @@ -3173,7 +3194,8 @@ fn intercepted_hold_htlc() { check_added_monitors!(lsp, 1); let path: &[&Node] = &[recipient]; - let args = PassAlongPathArgs::new(lsp, path, amt_msat, payment_hash, ev); + let args = PassAlongPathArgs::new(lsp, path, amt_msat, payment_hash, ev) + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); let claimable_ev = do_pass_along_path(args).unwrap(); let route: &[&[&Node]] = &[&[lsp, recipient]]; @@ -3276,7 +3298,8 @@ fn async_payment_mpp() { assert_eq!(events.len(), 1); let ev = remove_first_msg_event_to_node(&recipient.node.get_our_node_id(), &mut events); let args = PassAlongPathArgs::new(lsp_a, expected_path, amt_msat, payment_hash, ev) - .without_claimable_event(); + .without_claimable_event() + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); do_pass_along_path(args); lsp_b.node.process_pending_htlc_forwards(); @@ -3284,7 +3307,8 @@ fn async_payment_mpp() { let mut events = lsp_b.node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); let ev = remove_first_msg_event_to_node(&recipient.node.get_our_node_id(), &mut events); - let args = PassAlongPathArgs::new(lsp_b, expected_path, amt_msat, payment_hash, ev); + let args = PassAlongPathArgs::new(lsp_b, expected_path, amt_msat, payment_hash, ev) + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); let claimable_ev = do_pass_along_path(args).unwrap(); let keysend_preimage = match claimable_ev { @@ -3420,7 +3444,8 @@ fn release_htlc_races_htlc_onion_decode() { check_added_monitors!(sender_lsp, 1); let path: &[&Node] = &[invoice_server, recipient]; - let args = PassAlongPathArgs::new(sender_lsp, path, amt_msat, payment_hash, ev); + let args = PassAlongPathArgs::new(sender_lsp, path, amt_msat, payment_hash, ev) + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); let claimable_ev = do_pass_along_path(args).unwrap(); let route: &[&[&Node]] = &[&[sender_lsp, invoice_server, recipient]]; diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 0b2d5b86add..cd24c2de7ef 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -47,7 +47,7 @@ use bitcoin::secp256k1::{PublicKey, Secp256k1}; use core::time::Duration; use crate::blinded_path::IntroductionNode; use crate::blinded_path::message::BlindedMessagePath; -use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, PaymentContext}; +use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, DummyTlvs, PaymentContext}; use crate::blinded_path::message::OffersContext; use crate::events::{ClosureReason, Event, HTLCHandlingFailureType, PaidBolt12Invoice, PaymentFailureReason, PaymentPurpose}; use crate::ln::channelmanager::{Bolt12PaymentError, PaymentId, RecentPaymentDetails, RecipientOnionFields, Retry, self}; @@ -63,7 +63,7 @@ use crate::offers::parse::Bolt12SemanticError; use crate::onion_message::messenger::{DefaultMessageRouter, Destination, MessageSendInstructions, NodeIdMessageRouter, NullMessageRouter, PeeledOnion, PADDED_PATH_LENGTH}; use crate::onion_message::offers::OffersMessage; use crate::routing::gossip::{NodeAlias, NodeId}; -use crate::routing::router::{PaymentParameters, RouteParameters, RouteParametersConfig}; +use crate::routing::router::{DEFAULT_PAYMENT_DUMMY_HOPS, PaymentParameters, RouteParameters, RouteParametersConfig}; use crate::sign::{NodeSigner, Recipient}; use crate::util::ser::Writeable; @@ -178,7 +178,8 @@ fn route_bolt12_payment<'a, 'b, 'c>( let amount_msats = invoice.amount_msats(); let payment_hash = invoice.payment_hash(); let args = PassAlongPathArgs::new(node, path, amount_msats, payment_hash, ev) - .without_clearing_recipient_events(); + .without_clearing_recipient_events() + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); do_pass_along_path(args); } @@ -1432,7 +1433,20 @@ fn creates_offer_with_blinded_path_using_unannounced_introduction_node() { route_bolt12_payment(bob, &[alice], &invoice); expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); - claim_bolt12_payment(bob, &[alice], payment_context, &invoice); + // When the payer is the introduction node of a blinded path, LDK doesn't + // subtract the forward fee for the `payer -> next_hop` channel (see + // `BlindedPaymentPath::advance_path_by_one`). This keeps fee logic simple, + // at the cost of a small, intentional overpayment. + // + // In the old two-hop case (payer as introduction node → payee), this never + // surfaced because the payer simply wasn’t charged the forward fee. + // + // With dummy hops in LDK v0.3, even a real two-node path can appear as a + // longer blinded route, so the overpayment shows up in tests. + // + // Until the fee-handling trade-off is revisited, we pass an expected extra + // fee here so tests can compensate for it. + claim_bolt12_payment_with_extra_fees(bob, &[alice], payment_context, &invoice, Some(1000)); expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); } @@ -2444,12 +2458,13 @@ fn rejects_keysend_to_non_static_invoice_path() { let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) .with_payment_preimage(payment_preimage) - .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }); + .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }) + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); do_pass_along_path(args); let mut updates = get_htlc_update_msgs(&nodes[1], &nodes[0].node.get_our_node_id()); - nodes[0].node.handle_update_fail_htlc(nodes[1].node.get_our_node_id(), &updates.update_fail_htlcs[0]); + nodes[0].node.handle_update_fail_malformed_htlc(nodes[1].node.get_our_node_id(), &updates.update_fail_malformed_htlcs[0]); do_commitment_signed_dance(&nodes[0], &nodes[1], &updates.commitment_signed, false, false); - expect_payment_failed_conditions(&nodes[0], payment_hash, true, PaymentFailedConditions::new()); + expect_payment_failed_conditions(&nodes[0], payment_hash, false, PaymentFailedConditions::new()); } #[test] @@ -2508,12 +2523,14 @@ fn no_double_pay_with_stale_channelmanager() { let ev = remove_first_msg_event_to_node(&bob_id, &mut events); let args = PassAlongPathArgs::new(&nodes[0], expected_route[0], amt_msat, payment_hash, ev) - .without_clearing_recipient_events(); + .without_clearing_recipient_events() + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); do_pass_along_path(args); let ev = remove_first_msg_event_to_node(&bob_id, &mut events); let args = PassAlongPathArgs::new(&nodes[0], expected_route[0], amt_msat, payment_hash, ev) - .without_clearing_recipient_events(); + .without_clearing_recipient_events() + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); do_pass_along_path(args); expect_recent_payment!(nodes[0], RecentPaymentDetails::Pending, payment_id); diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index c06e5174263..cb1b3d0fc1d 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -13,8 +13,8 @@ use bitcoin::secp256k1::{self, PublicKey, Secp256k1}; use lightning_invoice::Bolt11Invoice; use crate::blinded_path::payment::{ - BlindedPaymentPath, ForwardTlvs, PaymentConstraints, PaymentForwardNode, PaymentRelay, - ReceiveTlvs, + BlindedPaymentPath, DummyTlvs, ForwardTlvs, PaymentConstraints, PaymentForwardNode, + PaymentRelay, ReceiveTlvs, }; use crate::blinded_path::{BlindedHop, Direction, IntroductionNode}; use crate::crypto::chacha20::ChaCha20; @@ -74,6 +74,9 @@ pub struct DefaultRouter< score_params: SP, } +/// The number of dummy hops included in [`BlindedPaymentPath`]s created by [`DefaultRouter`]. +pub const DEFAULT_PAYMENT_DUMMY_HOPS: usize = 3; + impl< G: Deref>, L: Deref, @@ -198,9 +201,9 @@ where }) }) .map(|forward_node| { - BlindedPaymentPath::new( - &[forward_node], recipient, local_node_receive_key, tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, - &*self.entropy_source, secp_ctx + BlindedPaymentPath::new_with_dummy_hops( + &[forward_node], recipient, &[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS], + local_node_receive_key, tlvs.clone(), u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, &*self.entropy_source, secp_ctx ) }) .take(MAX_PAYMENT_PATHS) @@ -210,9 +213,9 @@ where Ok(paths) if !paths.is_empty() => Ok(paths), _ => { if network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)) { - BlindedPaymentPath::new( - &[], recipient, local_node_receive_key, tlvs, u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, &*self.entropy_source, - secp_ctx + BlindedPaymentPath::new_with_dummy_hops( + &[], recipient, &[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS], + local_node_receive_key, tlvs, u64::MAX, MIN_FINAL_CLTV_EXPIRY_DELTA, &*self.entropy_source, secp_ctx ).map(|path| vec![path]) } else { Err(()) From 2a34be1929d9ca16ab6829f589763ce1107e0d48 Mon Sep 17 00:00:00 2001 From: shaavan Date: Tue, 21 Oct 2025 19:10:53 +0530 Subject: [PATCH 6/6] Introduce Blinded Payment Dummy Path test --- lightning/src/ln/blinded_payment_tests.rs | 70 ++++++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index a902cfebd12..3f7f36f454a 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -8,8 +8,8 @@ // licenses. use crate::blinded_path::payment::{ - BlindedPaymentPath, Bolt12RefundContext, ForwardTlvs, PaymentConstraints, PaymentContext, - PaymentForwardNode, PaymentRelay, ReceiveTlvs, PAYMENT_PADDING_ROUND_OFF, + BlindedPaymentPath, Bolt12RefundContext, DummyTlvs, ForwardTlvs, PaymentConstraints, + PaymentContext, PaymentForwardNode, PaymentRelay, ReceiveTlvs, PAYMENT_PADDING_ROUND_OFF, }; use crate::blinded_path::utils::is_padded; use crate::blinded_path::{self, BlindedHop}; @@ -196,6 +196,72 @@ fn do_one_hop_blinded_path(success: bool) { } } +#[test] +fn one_hop_blinded_path_with_dummy_hops() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let chan_upd = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0).0.contents; + + let amt_msat = 5000; + let (payment_preimage, payment_hash, payment_secret) = + get_payment_preimage_hash(&nodes[1], Some(amt_msat), None); + let payee_tlvs = ReceiveTlvs { + payment_secret, + payment_constraints: PaymentConstraints { + max_cltv_expiry: u32::max_value(), + htlc_minimum_msat: chan_upd.htlc_minimum_msat, + }, + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + }; + let receive_auth_key = chanmon_cfgs[1].keys_manager.get_receive_auth_key(); + let dummy_tlvs = [DummyTlvs::default(); 2]; + + let mut secp_ctx = Secp256k1::new(); + let blinded_path = BlindedPaymentPath::new_with_dummy_hops( + &[], + nodes[1].node.get_our_node_id(), + &dummy_tlvs, + receive_auth_key, + payee_tlvs, + u64::MAX, + TEST_FINAL_CLTV as u16, + &chanmon_cfgs[1].keys_manager, + &secp_ctx, + ) + .unwrap(); + + let route_params = RouteParameters::from_payment_params_and_value( + PaymentParameters::blinded(vec![blinded_path]), + amt_msat, + ); + nodes[0] + .node + .send_payment( + payment_hash, + RecipientOnionFields::spontaneous_empty(), + PaymentId(payment_hash.0), + route_params, + Retry::Attempts(0), + ) + .unwrap(); + check_added_monitors(&nodes[0], 1); + + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + + let path = &[&nodes[1]]; + let args = PassAlongPathArgs::new(&nodes[0], path, amt_msat, payment_hash, ev) + .with_dummy_tlvs(&dummy_tlvs) + .with_payment_secret(payment_secret); + + do_pass_along_path(args); + claim_payment(&nodes[0], &[&nodes[1]], payment_preimage); +} + #[test] #[rustfmt::skip] fn mpp_to_one_hop_blinded_path() {