From 8cecad36391487f190635f35a888b452ebb11764 Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 9 Oct 2025 23:00:40 +0530 Subject: [PATCH 1/7] 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 | 62 +++++++++++++++++++-------- lightning/src/ln/channelmanager.rs | 8 ++++ lightning/src/ln/msgs.rs | 12 ++++++ lightning/src/ln/onion_payment.rs | 16 +++++++ lightning/src/ln/onion_utils.rs | 12 ++++++ 5 files changed, 91 insertions(+), 19 deletions(-) diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index 13ade222f5b..0993597576b 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -346,6 +346,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, /// This blinded payment data is for the receiving node. Receive(ReceiveTlvs), } @@ -363,6 +365,7 @@ pub(crate) enum BlindedTrampolineTlvs { // Used to include forward and receive TLVs in the same iterator for encoding. enum BlindedPaymentTlvsRef<'a> { Forward(&'a ForwardTlvs), + Dummy, Receive(&'a ReceiveTlvs), } @@ -532,6 +535,11 @@ 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 => { + encode_tlv_stream!(w, { + (65539, (), required), + }) + }, Self::Receive(tlvs) => tlvs.write(w)?, } Ok(()) @@ -548,32 +556,48 @@ impl Readable for BlindedPaymentTlvs { (2, scid, option), (8, next_blinding_override, option), (10, payment_relay, option), - (12, payment_constraints, required), + (12, payment_constraints, option), (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 { + match ( + scid, + next_blinding_override, + payment_relay, + payment_constraints, + features, + payment_secret, + payment_context, + is_dummy, + ) { + ( + Some(short_channel_id), + next_override, + Some(relay), + Some(constraints), + features, + None, + None, + None, + ) => Ok(BlindedPaymentTlvs::Forward(ForwardTlvs { short_channel_id, - payment_relay: payment_relay.ok_or(DecodeError::InvalidValue)?, - payment_constraints: payment_constraints.0.unwrap(), - next_blinding_override, + payment_relay: relay, + payment_constraints: constraints, + next_blinding_override: next_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)?, - })) + })), + (None, None, None, Some(constraints), None, Some(secret), Some(context), None) => { + Ok(BlindedPaymentTlvs::Receive(ReceiveTlvs { + payment_secret: secret, + payment_constraints: constraints, + payment_context: context, + })) + }, + (None, None, None, None, None, None, None, Some(())) => Ok(BlindedPaymentTlvs::Dummy), + _ => return Err(DecodeError::InvalidValue), } } } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index ea9b14211c5..2299cb33b5f 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5089,6 +5089,14 @@ 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, "Shouldn't be triggered."); + 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..ec8301a894e 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -2355,6 +2355,7 @@ mod fuzzy_internal_msgs { Receive(InboundOnionReceivePayload), BlindedForward(InboundOnionBlindedForwardPayload), BlindedReceive(InboundOnionBlindedReceivePayload), + Dummy { intro_node_blinding_point: Option }, } pub struct InboundTrampolineForwardPayload { @@ -3694,6 +3695,17 @@ where next_blinding_override, })) }, + ChaChaDualPolyReadAdapter { readable: BlindedPaymentTlvs::Dummy, 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 { 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 f52a2d56e85..0037ea87a98 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -123,6 +123,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, "This case shall not be triggered"); + 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", @@ -327,6 +335,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, "This case shall not be triggered."); + 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 e32b39775fe..8c932f71ec8 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -2202,6 +2202,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 { + /// Onion payload data used in interpreting the dummy hop + intro_node_blinding_point: Option, + /// Shared secret that was used to decrypt next_hop_data. + shared_secret: SharedSecret, + /// HMAC of the next hop's onion packet. + next_hop_hmac: [u8; 32], + /// Bytes of the onion packet we're forwarding. + 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 { @@ -2256,6 +2267,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 63f9f57e9c079c8e5ff9db6553f054e14dfbc620 Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 9 Oct 2025 23:09:50 +0530 Subject: [PATCH 2/7] 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 | 59 ++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index 0993597576b..516b98f38d0 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -12,6 +12,7 @@ use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey}; +use crate::blinded_path::message::MAX_DUMMY_HOPS_COUNT; use crate::blinded_path::utils::{self, BlindedPathWithPadding}; use crate::blinded_path::{BlindedHop, BlindedPath, IntroductionNode, NodeIdLookUp}; use crate::crypto::streams::ChaChaDualPolyReadAdapter; @@ -121,6 +122,32 @@ 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_with_dummy_hops( + intermediate_nodes, + payee_node_id, + 0, + 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. + /// + /// Note: + /// At most [`MAX_DUMMY_HOPS_COUNT`] dummy hops can be added to the blinded path. + pub fn new_with_dummy_hops( + intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey, + dummy_hop_count: usize, 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, { @@ -145,6 +172,7 @@ impl BlindedPaymentPath { secp_ctx, intermediate_nodes, payee_node_id, + dummy_hop_count, payee_tlvs, &blinding_secret, local_node_receive_key, @@ -644,22 +672,43 @@ 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_hop_count: usize, payee_tlvs: ReceiveTlvs, session_priv: &SecretKey, + local_node_receive_key: ReceiveAuthKey, ) -> Vec { + let dummy_count = core::cmp::min(dummy_hop_count, MAX_DUMMY_HOPS_COUNT); let pks = intermediate_nodes .iter() .map(|node| (node.node_id, None)) + .chain(core::iter::repeat((payee_node_id, Some(local_node_receive_key))).take(dummy_count)) .chain(core::iter::once((payee_node_id, Some(local_node_receive_key)))); let tlvs = intermediate_nodes .iter() .map(|node| BlindedPaymentTlvsRef::Forward(&node.tlvs)) + .chain((0..dummy_count).map(|_| BlindedPaymentTlvsRef::Dummy)) .chain(core::iter::once(BlindedPaymentTlvsRef::Receive(&payee_tlvs))); - let path = pks.zip( - tlvs.map(|tlv| BlindedPathWithPadding { tlvs: tlv, round_off: PAYMENT_PADDING_ROUND_OFF }), - ); + let path: Vec<_> = pks + .zip( + tlvs.map(|tlv| BlindedPathWithPadding { + tlvs: tlv, + round_off: PAYMENT_PADDING_ROUND_OFF, + }), + ) + .collect(); + + // Debug invariant: all non-final hops must have identical serialized size. + #[cfg(debug_assertions)] + if let Some((_, first)) = path.first() { + let expected = first.serialized_length(); + for (_, hop) in &path[..path.len() - 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) + utils::construct_blinded_hops(secp_ctx, path.into_iter(), session_priv) } /// `None` if underflow occurs. From a24efed3c11f9cf8531e24b8914b718b8aaa1a97 Mon Sep 17 00:00:00 2001 From: shaavan Date: Mon, 20 Oct 2025 17:13:31 +0530 Subject: [PATCH 3/7] Refactor: Introduce ForwardInfo NextPacketDetails currently bundles four fields used to define the forwarding details for the packet. With the introduction of dummy hops, not all of these fields apply in those paths. To avoid overloading NextPacketDetails with conditional semantics, this refactor extracts the forwarding-specific pieces into a dedicated ForwardInfo struct. This keeps the data model clean, reusable, and makes the logic around dummy hops easier to follow. --- lightning/src/ln/blinded_payment_tests.rs | 12 +++-- lightning/src/ln/channelmanager.rs | 31 ++++++++----- lightning/src/ln/onion_payment.rs | 55 ++++++++++++++++------- 3 files changed, 66 insertions(+), 32 deletions(-) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 5bf015e7c81..b21f2c5b2c4 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -1663,8 +1663,9 @@ fn route_blinding_spec_test_vector() { hop_data: carol_packet_bytes, hmac: carol_hmac, }; + let carol_forward_info = carol_packet_details.forward_info.unwrap(); let carol_update_add = update_add_msg( - carol_packet_details.outgoing_amt_msat, carol_packet_details.outgoing_cltv_value, + carol_forward_info.outgoing_amt_msat, carol_forward_info.outgoing_cltv_value, Some(pubkey_from_hex("034e09f450a80c3d252b258aba0a61215bf60dda3b0dc78ffb0736ea1259dfd8a0")), carol_onion ); @@ -1697,8 +1698,9 @@ fn route_blinding_spec_test_vector() { hop_data: dave_packet_bytes, hmac: dave_hmac, }; + let dave_forward_info = dave_packet_details.forward_info.unwrap(); let dave_update_add = update_add_msg( - dave_packet_details.outgoing_amt_msat, dave_packet_details.outgoing_cltv_value, + dave_forward_info.outgoing_amt_msat, dave_forward_info.outgoing_cltv_value, Some(pubkey_from_hex("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")), dave_onion ); @@ -1731,8 +1733,9 @@ fn route_blinding_spec_test_vector() { hop_data: eve_packet_bytes, hmac: eve_hmac, }; + let eve_forward_info = eve_packet_details.forward_info.unwrap(); let eve_update_add = update_add_msg( - eve_packet_details.outgoing_amt_msat, eve_packet_details.outgoing_cltv_value, + eve_forward_info.outgoing_amt_msat, eve_forward_info.outgoing_cltv_value, Some(pubkey_from_hex("03e09038ee76e50f444b19abf0a555e8697e035f62937168b80adf0931b31ce52a")), eve_onion ); @@ -1963,7 +1966,8 @@ fn test_trampoline_inbound_payment_decoding() { hop_data: carol_packet_bytes, hmac: carol_hmac, }; - let carol_update_add = update_add_msg(carol_packet_details.outgoing_amt_msat, carol_packet_details.outgoing_cltv_value, None, carol_onion); + let carol_forward_info = carol_packet_details.forward_info.unwrap(); + let carol_update_add = update_add_msg(carol_forward_info.outgoing_amt_msat, carol_forward_info.outgoing_cltv_value, None, carol_onion); let carol_node_signer = TestEcdhSigner { node_secret: carol_secret }; let (carol_peeled_onion, _) = onion_payment::decode_incoming_update_add_htlc_onion( diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 2299cb33b5f..04d91bd4a4b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -73,8 +73,8 @@ use crate::ln::msgs::{ }; use crate::ln::onion_payment::{ check_incoming_htlc_cltv, create_fwd_pending_htlc_info, create_recv_pending_htlc_info, - decode_incoming_update_add_htlc_onion, invalid_payment_err_data, HopConnector, InboundHTLCErr, - NextPacketDetails, + decode_incoming_update_add_htlc_onion, invalid_payment_err_data, ForwardInfo, HopConnector, + InboundHTLCErr, NextPacketDetails, }; use crate::ln::onion_utils::{self}; use crate::ln::onion_utils::{ @@ -4888,7 +4888,7 @@ where #[rustfmt::skip] fn can_forward_htlc_to_outgoing_channel( - &self, chan: &mut FundedChannel, msg: &msgs::UpdateAddHTLC, next_packet: &NextPacketDetails + &self, chan: &mut FundedChannel, msg: &msgs::UpdateAddHTLC, forward_info: &ForwardInfo ) -> Result<(), LocalHTLCFailureReason> { if !chan.context.should_announce() && !self.config.read().unwrap().accept_forwards_to_priv_channels @@ -4898,7 +4898,7 @@ where // we don't allow forwards outbound over them. return Err(LocalHTLCFailureReason::PrivateChannelForward); } - if let HopConnector::ShortChannelId(outgoing_scid) = next_packet.outgoing_connector { + if let HopConnector::ShortChannelId(outgoing_scid) = forward_info.outgoing_connector { if chan.funding.get_channel_type().supports_scid_privacy() && outgoing_scid != chan.context.outbound_scid_alias() { // `option_scid_alias` (referred to in LDK as `scid_privacy`) means // "refuse to forward unless the SCID alias was used", so we pretend @@ -4921,10 +4921,10 @@ where return Err(LocalHTLCFailureReason::ChannelNotReady); } } - if next_packet.outgoing_amt_msat < chan.context.get_counterparty_htlc_minimum_msat() { + if forward_info.outgoing_amt_msat < chan.context.get_counterparty_htlc_minimum_msat() { return Err(LocalHTLCFailureReason::AmountBelowMinimum); } - chan.htlc_satisfies_config(msg, next_packet.outgoing_amt_msat, next_packet.outgoing_cltv_value)?; + chan.htlc_satisfies_config(msg, forward_info.outgoing_amt_msat, forward_info.outgoing_cltv_value)?; Ok(()) } @@ -4956,14 +4956,20 @@ where fn can_forward_htlc( &self, msg: &msgs::UpdateAddHTLC, next_packet_details: &NextPacketDetails ) -> Result<(), LocalHTLCFailureReason> { - let outgoing_scid = match next_packet_details.outgoing_connector { + let forward_info = next_packet_details + .forward_info + .as_ref() + .ok_or(LocalHTLCFailureReason::InvalidOnionPayload)?; + + let outgoing_scid = match forward_info.outgoing_connector { HopConnector::ShortChannelId(scid) => scid, HopConnector::Trampoline(_) => { return Err(LocalHTLCFailureReason::InvalidTrampolineForward); } }; + match self.do_funded_channel_callback(outgoing_scid, |chan: &mut FundedChannel| { - self.can_forward_htlc_to_outgoing_channel(chan, msg, next_packet_details) + self.can_forward_htlc_to_outgoing_channel(chan, msg, forward_info) }) { Some(Ok(())) => {}, Some(Err(e)) => return Err(e), @@ -4980,7 +4986,7 @@ where } let cur_height = self.best_block.read().unwrap().height + 1; - check_incoming_htlc_cltv(cur_height, next_packet_details.outgoing_cltv_value, msg.cltv_expiry)?; + check_incoming_htlc_cltv(cur_height, forward_info.outgoing_cltv_value, msg.cltv_expiry)?; Ok(()) } @@ -6918,11 +6924,12 @@ where }; let is_intro_node_blinded_forward = next_hop.is_intro_node_blinded_forward(); - let outgoing_scid_opt = - next_packet_details_opt.as_ref().and_then(|d| match d.outgoing_connector { + let outgoing_scid_opt = next_packet_details_opt.as_ref().and_then(|d| { + d.forward_info.as_ref().and_then(|f| match f.outgoing_connector { HopConnector::ShortChannelId(scid) => Some(scid), HopConnector::Trampoline(_) => None, - }); + }) + }); let shared_secret = next_hop.shared_secret().secret_bytes(); // Nodes shouldn't expect us to hold HTLCs for them if we don't advertise htlc_hold feature diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 0037ea87a98..60d267f3a3f 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -469,16 +469,19 @@ where Ok(match hop { onion_utils::Hop::Forward { shared_secret, .. } | onion_utils::Hop::BlindedForward { shared_secret, .. } => { - let NextPacketDetails { - next_packet_pubkey, outgoing_amt_msat: _, outgoing_connector: _, outgoing_cltv_value - } = match next_packet_details_opt { - Some(next_packet_details) => next_packet_details, + let (next_packet_pubkey, outgoing_cltv_value) = match next_packet_details_opt { + Some(NextPacketDetails { + next_packet_pubkey, + forward_info: Some(ForwardInfo { outgoing_cltv_value, .. }), + }) => (next_packet_pubkey, outgoing_cltv_value), // Forward 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(), - }), + _ => { + return Err(InboundHTLCErr { + msg: "Failed to decode update add htlc onion", + reason: LocalHTLCFailureReason::InvalidOnionPayload, + err_data: Vec::new(), + }); + } }; if let Err(reason) = check_incoming_htlc_cltv( @@ -515,6 +518,10 @@ pub(super) enum HopConnector { pub(super) struct NextPacketDetails { pub(super) next_packet_pubkey: Result, + pub(super) forward_info: Option, +} + +pub(super) struct ForwardInfo { pub(super) outgoing_connector: HopConnector, pub(super) outgoing_amt_msat: u64, pub(super) outgoing_cltv_value: u32, @@ -591,8 +598,12 @@ where 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::ShortChannelId(short_channel_id), - outgoing_amt_msat: amt_to_forward, outgoing_cltv_value + next_packet_pubkey, + forward_info: Some(ForwardInfo { + outgoing_connector: HopConnector::ShortChannelId(short_channel_id), + outgoing_amt_msat: amt_to_forward, + outgoing_cltv_value, + }), }) } onion_utils::Hop::BlindedForward { next_hop_data: msgs::InboundOnionBlindedForwardPayload { short_channel_id, ref payment_relay, ref payment_constraints, ref features, .. }, shared_secret, .. } => { @@ -608,8 +619,12 @@ where 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::ShortChannelId(short_channel_id), outgoing_amt_msat: amt_to_forward, - outgoing_cltv_value + next_packet_pubkey, + forward_info: Some(ForwardInfo { + outgoing_connector: HopConnector::ShortChannelId(short_channel_id), + 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, .. } => { @@ -617,10 +632,18 @@ where 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, + forward_info: Some(ForwardInfo { + outgoing_connector: HopConnector::Trampoline(next_trampoline), + outgoing_amt_msat: amt_to_forward, + outgoing_cltv_value, + }), }) + }, + onion_utils::Hop::Dummy { shared_secret, .. } => { + 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, forward_info: None }) } _ => None }; From 96368a0b3be5f1d029d1a9151bd543e3983ca1d3 Mon Sep 17 00:00:00 2001 From: shaavan Date: Mon, 20 Oct 2025 17:00:05 +0530 Subject: [PATCH 4/7] Introduce Payment Dummy Hop parsing mechanism --- lightning/src/blinded_path/payment.rs | 19 +++++++++-- lightning/src/ln/channelmanager.rs | 46 ++++++++++++++++++++++++--- lightning/src/ln/onion_payment.rs | 28 +++++++++++++++- lightning/src/ln/onion_utils.rs | 6 ++++ 4 files changed, 91 insertions(+), 8 deletions(-) diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index 516b98f38d0..c5673c64283 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -239,6 +239,19 @@ impl BlindedPaymentPath { self.inner_path.blinded_hops.remove(0); Ok(()) }, + Ok((BlindedPaymentTlvs::Dummy, control_tlvs_ss)) => { + let next_node_id = node_signer.get_node_id(Recipient::Node)?; + 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(()), } } @@ -262,9 +275,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 04d91bd4a4b..bb97bd95445 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -72,9 +72,9 @@ use crate::ln::msgs::{ MessageSendEvent, }; use crate::ln::onion_payment::{ - check_incoming_htlc_cltv, create_fwd_pending_htlc_info, create_recv_pending_htlc_info, - decode_incoming_update_add_htlc_onion, invalid_payment_err_data, ForwardInfo, HopConnector, - InboundHTLCErr, NextPacketDetails, + check_incoming_htlc_cltv, create_fwd_pending_htlc_info, create_new_update_add_htlc, + create_recv_pending_htlc_info, decode_incoming_update_add_htlc_onion, invalid_payment_err_data, + ForwardInfo, HopConnector, InboundHTLCErr, NextPacketDetails, }; use crate::ln::onion_utils::{self}; use crate::ln::onion_utils::{ @@ -6851,6 +6851,7 @@ where pub(crate) 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| { @@ -6914,7 +6915,39 @@ where &*self.logger, &self.secp_ctx, ) { - Ok(decoded_onion) => decoded_onion, + Ok(decoded_onion) => match decoded_onion { + ( + onion_utils::Hop::Dummy { + intro_node_blinding_point, + next_hop_hmac, + new_packet_bytes, + .. + }, + Some(NextPacketDetails { next_packet_pubkey, forward_info }), + ) => { + debug_assert!( + forward_info.is_none(), + "Dummy hops must not contain any forward info, since they are not actually forwarded." + ); + let new_update_add_htlc = create_new_update_add_htlc( + update_add_htlc.clone(), + &*self.node_signer, + &self.secp_ctx, + intro_node_blinding_point, + next_packet_pubkey, + next_hop_hmac, + new_packet_bytes, + ); + + 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; @@ -7071,6 +7104,11 @@ where )); } } + + // Replace the decode queue with the peeled dummy HTLCs so they can be processed in the next iteration. + let mut decode_update_add_htlc_source = self.decode_update_add_htlcs.lock().unwrap(); + mem::swap(&mut *decode_update_add_htlc_source, &mut dummy_update_add_htlcs); + should_persist } diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 60d267f3a3f..6ff43e3d86e 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -15,7 +15,7 @@ use crate::ln::channelmanager::{ BlindedFailure, BlindedForward, HTLCFailureMsg, PendingHTLCInfo, PendingHTLCRouting, CLTV_FAR_FAR_AWAY, MIN_CLTV_EXPIRY_DELTA, }; -use crate::ln::msgs; +use crate::ln::msgs::{self, OnionPacket, UpdateAddHTLC}; use crate::ln::onion_utils; use crate::ln::onion_utils::{HTLCFailReason, LocalHTLCFailureReason, ONION_DATA_LEN}; use crate::sign::{NodeSigner, Recipient}; @@ -651,6 +651,32 @@ where Ok((next_hop, next_packet_details)) } +pub(super) fn create_new_update_add_htlc( + msg: msgs::UpdateAddHTLC, node_signer: NS, secp_ctx: &Secp256k1, + intro_node_blinding_point: Option, + new_packet_pubkey: Result, next_hop_hmac: [u8; 32], + new_packet_bytes: [u8; 1300], +) -> UpdateAddHTLC +where + NS::Target: NodeSigner, +{ + let new_packet = OnionPacket { + version: 0, + public_key: new_packet_pubkey, + hop_data: new_packet_bytes, + hmac: next_hop_hmac, + }; + + let next_blinding_point = + intro_node_blinding_point.or(msg.blinding_point).and_then(|blinding_point| { + let encrypted_tlvs_ss = + node_signer.ecdh(Recipient::Node, &blinding_point, None).unwrap().secret_bytes(); + onion_utils::next_hop_pubkey(&secp_ctx, blinding_point, &encrypted_tlvs_ss).ok() + }); + + UpdateAddHTLC { onion_routing_packet: new_packet, blinding_point: next_blinding_point, ..msg } +} + pub(super) fn check_incoming_htlc_cltv( cur_height: u32, outgoing_cltv_value: u32, cltv_expiry: u32, ) -> Result<(), LocalHTLCFailureReason> { diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 8c932f71ec8..832ff8b4ece 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -2335,6 +2335,12 @@ where new_packet_bytes, }) }, + msgs::InboundOnionPayload::Dummy { intro_node_blinding_point } => Ok(Hop::Dummy { + intro_node_blinding_point, + shared_secret, + next_hop_hmac, + new_packet_bytes, + }), _ => { if blinding_point.is_some() { return Err(OnionDecodeErr::Malformed { From 3d50170c2c8d6e102e9e82ad648085cf4bcd7908 Mon Sep 17 00:00:00 2001 From: shaavan Date: Tue, 21 Oct 2025 18:58:53 +0530 Subject: [PATCH 5/7] 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 | 51 +++++++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index e31630a4926..8287b1e2a22 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -3435,6 +3435,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_hop_override: Option, pub recv_value: u64, pub payment_hash: PaymentHash, pub payment_secret: Option, @@ -3456,6 +3457,7 @@ impl<'a, 'b, 'c, 'd> PassAlongPathArgs<'a, 'b, 'c, 'd> { Self { origin_node, expected_path, + dummy_hop_override: None, recv_value, payment_hash, payment_secret: None, @@ -3503,12 +3505,17 @@ impl<'a, 'b, 'c, 'd> PassAlongPathArgs<'a, 'b, 'c, 'd> { self.expected_failure = Some(failure); self } + pub fn with_dummy_override(mut self, dummy_override: usize) -> Self { + self.dummy_hop_override = Some(dummy_override); + self + } } pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option { let PassAlongPathArgs { origin_node, expected_path, + dummy_hop_override, recv_value, payment_hash: our_payment_hash, payment_secret: our_payment_secret, @@ -3755,6 +3762,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 v3.0, 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 +3808,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 +3824,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 @@ -3817,6 +3852,7 @@ pub fn pass_claimed_payment_along_route(args: ClaimAlongRouteArgs) -> u64 { payment_preimage: our_payment_preimage, allow_1_msat_fee_overpay, custom_tlvs, + .. } = args; let claim_event = expected_paths[0].last().unwrap().node.get_and_clear_pending_events(); assert_eq!(claim_event.len(), 1); @@ -4052,10 +4088,17 @@ pub fn pass_claimed_payment_along_route(args: ClaimAlongRouteArgs) -> u64 { 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 { From d8d01ff6e313d8c77f2628bbd156459e8d329d40 Mon Sep 17 00:00:00 2001 From: shaavan Date: Tue, 18 Nov 2025 23:26:03 +0530 Subject: [PATCH 6/7] Introduce payment dummy hops in DefaultRouter --- lightning-dns-resolver/src/lib.rs | 6 ++++ lightning/src/ln/async_payments_tests.rs | 9 ++++- lightning/src/ln/functional_test_utils.rs | 13 +++++++- lightning/src/ln/offers_tests.rs | 40 +++++++++++++++++++++-- lightning/src/routing/router.rs | 15 +++++---- 5 files changed, 72 insertions(+), 11 deletions(-) diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs index 471c7562702..ac8028649ef 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,11 @@ 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 { + 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..01abf21f6eb 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -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; @@ -1858,6 +1858,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!(), diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 8287b1e2a22..383cafb211b 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -39,7 +39,9 @@ use crate::ln::peer_handler::IgnoringMessageHandler; use crate::ln::types::ChannelId; use crate::onion_message::messenger::OnionMessenger; use crate::routing::gossip::{NetworkGraph, NetworkUpdate, P2PGossipSync}; -use crate::routing::router::{self, PaymentParameters, Route, RouteParameters}; +use crate::routing::router::{ + self, PaymentParameters, Route, RouteParameters, DEFAULT_PAYMENT_DUMMY_HOPS, +}; use crate::sign::{EntropySource, RandomBytes}; use crate::types::features::ChannelTypeFeatures; use crate::types::features::InitFeatures; @@ -3550,6 +3552,15 @@ 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_hop_override.unwrap_or(DEFAULT_PAYMENT_DUMMY_HOPS) { + 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 { diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 4c53aefe58d..4b0e08acbdd 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -1410,7 +1410,41 @@ 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); + // Extract PaymentClaimable, verify context, and obtain the payment preimage. + let recipient = &alice; + let payment_purpose = match get_event!(recipient, Event::PaymentClaimable) { + Event::PaymentClaimable { purpose, .. } => purpose, + _ => panic!("No Event::PaymentClaimable"), + }; + + let payment_preimage = payment_purpose + .preimage() + .expect("No preimage in Event::PaymentClaimable"); + + match payment_purpose { + PaymentPurpose::Bolt12OfferPayment { payment_context: ctx, .. } => { + assert_eq!(PaymentContext::Bolt12Offer(ctx), payment_context); + }, + PaymentPurpose::Bolt12RefundPayment { payment_context: ctx, .. } => { + assert_eq!(PaymentContext::Bolt12Refund(ctx), payment_context); + }, + _ => panic!("Unexpected payment purpose: {:?}", payment_purpose), + }; + + // Build ClaimAlongRouteArgs and compensate for the expected overpayment + // caused by the payer being the introduction node of the blinded path with + // dummy hops. + let expected_paths: &[&[&Node]] = &[&[alice]]; + let args = ClaimAlongRouteArgs::new( + bob, + expected_paths, + payment_preimage, + ).with_expected_extra_total_fees_msat(1000); + + // Execute the claim and verify the invoice was fulfilled. + let (inv, _events) = claim_payment_along_route(args); + assert_eq!(inv, Some(PaidBolt12Invoice::Bolt12Invoice(invoice.clone()))); + expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); } @@ -2425,9 +2459,9 @@ fn rejects_keysend_to_non_static_invoice_path() { .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }); 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] diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 371c5232511..bbcf7a320b3 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -74,6 +74,9 @@ pub struct DefaultRouter< score_params: SP, } +/// Default number of Dummy Hops +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, 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, 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 923982cb2121e41ca65486696dd13870d8350231 Mon Sep 17 00:00:00 2001 From: shaavan Date: Tue, 21 Oct 2025 19:10:53 +0530 Subject: [PATCH 7/7] Introduce Blinded Payment Dummy Path test --- lightning/src/ln/blinded_payment_tests.rs | 50 +++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index b21f2c5b2c4..4514adbb53c 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -190,6 +190,56 @@ 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_hops = 1; + + let mut secp_ctx = Secp256k1::new(); + let blinded_path = BlindedPaymentPath::new_with_dummy_hops( + &[], nodes[1].node.get_our_node_id(), dummy_hops, 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_override(dummy_hops) + .with_payment_secret(payment_secret); + + do_pass_along_path(args); + claim_payment(&nodes[0], &[&nodes[1]], payment_preimage); +} + #[test] fn mpp_to_one_hop_blinded_path() { let chanmon_cfgs = create_chanmon_cfgs(4);