From 4ac1fdad3334c762f03190c0996b220cd5d8bf70 Mon Sep 17 00:00:00 2001 From: shaavan Date: Tue, 29 Jul 2025 17:51:10 +0530 Subject: [PATCH 1/6] Rename get_inbound_payment_key to get_expanded_key The use of ExpandedKey has grown beyond just encrypting inbound payment data-it now also supports BOLT 12 Offers, spontaneous payments, and authentication of various payment metadata. To reflect this broader purpose, this commit renames the function and updates its documentation accordingly. --- fuzz/src/chanmon_consistency.rs | 2 +- fuzz/src/full_stack.rs | 4 +-- fuzz/src/onion_message.rs | 2 +- lightning/src/ln/async_payments_tests.rs | 4 +-- lightning/src/ln/blinded_payment_tests.rs | 16 ++++++------ lightning/src/ln/channelmanager.rs | 6 ++--- lightning/src/ln/inbound_payment.rs | 12 ++++----- lightning/src/ln/invoice_utils.rs | 2 +- .../src/ln/max_payment_path_len_tests.rs | 2 +- lightning/src/ln/msgs.rs | 4 +-- lightning/src/ln/offers_tests.rs | 2 +- lightning/src/sign/mod.rs | 26 ++++++++++++------- lightning/src/util/dyn_signer.rs | 4 +-- lightning/src/util/test_utils.rs | 6 ++--- 14 files changed, 49 insertions(+), 43 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index ff5189f9346..3d7ca378a3f 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -332,7 +332,7 @@ impl NodeSigner for KeyProvider { Ok(SharedSecret::new(other_key, &node_secret)) } - fn get_inbound_payment_key(&self) -> ExpandedKey { + fn get_expanded_key(&self) -> ExpandedKey { #[rustfmt::skip] let random_bytes = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, self.node_secret[31]]; ExpandedKey::new(random_bytes) diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index 88e68e5e01d..2e4e6bd4af2 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -80,9 +80,9 @@ use bitcoin::secp256k1::{self, Message, PublicKey, Scalar, Secp256k1, SecretKey} use lightning::util::dyn_signer::DynSigner; -use std::collections::VecDeque; use std::cell::RefCell; use std::cmp; +use std::collections::VecDeque; use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; @@ -406,7 +406,7 @@ impl NodeSigner for KeyProvider { Ok(SharedSecret::new(other_key, &node_secret)) } - fn get_inbound_payment_key(&self) -> ExpandedKey { + fn get_expanded_key(&self) -> ExpandedKey { self.inbound_payment_key } diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs index 85ba6263b2a..d58b44fa7b6 100644 --- a/fuzz/src/onion_message.rs +++ b/fuzz/src/onion_message.rs @@ -255,7 +255,7 @@ impl NodeSigner for KeyProvider { Ok(SharedSecret::new(other_key, &node_secret)) } - fn get_inbound_payment_key(&self) -> ExpandedKey { + fn get_expanded_key(&self) -> ExpandedKey { unreachable!() } diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index 2fa5dee8a84..e617f6fbf1f 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -255,7 +255,7 @@ fn create_static_invoice_builder<'a>( let created_at = recipient.node.duration_since_epoch(); let payment_secret = inbound_payment::create_for_spontaneous_payment( - &recipient.keys_manager.get_inbound_payment_key(), + &recipient.keys_manager.get_expanded_key(), amount_msat, relative_expiry_secs, created_at.as_secs(), @@ -982,7 +982,7 @@ fn amount_doesnt_match_invreq() { valid_invreq = Some(invoice_request.clone()); *invoice_request = offer .request_invoice( - &nodes[0].keys_manager.get_inbound_payment_key(), + &nodes[0].keys_manager.get_expanded_key(), Nonce::from_entropy_source(nodes[0].keys_manager), &secp_ctx, payment_id, diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 0631db3d408..8db793054dc 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -87,7 +87,7 @@ pub fn blinded_payment_path( }; let nonce = Nonce([42u8; 16]); - let expanded_key = keys_manager.get_inbound_payment_key(); + let expanded_key = keys_manager.get_expanded_key(); let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key); let mut secp_ctx = Secp256k1::new(); @@ -172,7 +172,7 @@ fn do_one_hop_blinded_path(success: bool) { payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), }; let nonce = Nonce([42u8; 16]); - let expanded_key = chanmon_cfgs[1].keys_manager.get_inbound_payment_key(); + let expanded_key = chanmon_cfgs[1].keys_manager.get_expanded_key(); let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key); let mut secp_ctx = Secp256k1::new(); @@ -226,7 +226,7 @@ fn mpp_to_one_hop_blinded_path() { payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), }; let nonce = Nonce([42u8; 16]); - let expanded_key = chanmon_cfgs[3].keys_manager.get_inbound_payment_key(); + let expanded_key = chanmon_cfgs[3].keys_manager.get_expanded_key(); let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key); let blinded_path = BlindedPaymentPath::new( &[], nodes[3].node.get_our_node_id(), payee_tlvs, u64::MAX, TEST_FINAL_CLTV as u16, @@ -1336,7 +1336,7 @@ fn custom_tlvs_to_blinded_path() { payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), }; let nonce = Nonce([42u8; 16]); - let expanded_key = chanmon_cfgs[1].keys_manager.get_inbound_payment_key(); + let expanded_key = chanmon_cfgs[1].keys_manager.get_expanded_key(); let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key); let mut secp_ctx = Secp256k1::new(); let blinded_path = BlindedPaymentPath::new( @@ -1390,7 +1390,7 @@ fn fails_receive_tlvs_authentication() { payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), }; let nonce = Nonce([42u8; 16]); - let expanded_key = chanmon_cfgs[1].keys_manager.get_inbound_payment_key(); + let expanded_key = chanmon_cfgs[1].keys_manager.get_expanded_key(); let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key); let mut secp_ctx = Secp256k1::new(); @@ -1622,7 +1622,7 @@ fn route_blinding_spec_test_vector() { } Ok(SharedSecret::new(other_key, &node_secret)) } - fn get_inbound_payment_key(&self) -> ExpandedKey { unreachable!() } + fn get_expanded_key(&self) -> ExpandedKey { unreachable!() } fn get_node_id(&self, _recipient: Recipient) -> Result { unreachable!() } fn sign_invoice( &self, _invoice: &RawBolt11Invoice, _recipient: Recipient, @@ -1935,7 +1935,7 @@ fn test_trampoline_inbound_payment_decoding() { } Ok(SharedSecret::new(other_key, &node_secret)) } - fn get_inbound_payment_key(&self) -> ExpandedKey { unreachable!() } + fn get_expanded_key(&self) -> ExpandedKey { unreachable!() } fn get_node_id(&self, _recipient: Recipient) -> Result { unreachable!() } fn sign_invoice( &self, _invoice: &RawBolt11Invoice, _recipient: Recipient, @@ -2023,7 +2023,7 @@ fn do_test_trampoline_single_hop_receive(success: bool) { }; let nonce = Nonce([42u8; 16]); - let expanded_key = nodes[2].keys_manager.get_inbound_payment_key(); + let expanded_key = nodes[2].keys_manager.get_expanded_key(); let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key); let carol_unblinded_tlvs = payee_tlvs.encode(); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index c953e39d6d6..fa12f1e4b39 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -315,7 +315,7 @@ pub enum PendingHTLCRouting { requires_blinded_error: bool, /// Set if we are receiving a keysend to a blinded path, meaning we created the /// [`PaymentSecret`] and should verify it using our - /// [`NodeSigner::get_inbound_payment_key`]. + /// [`NodeSigner::get_expanded_key`]. has_recipient_created_payment_secret: bool, /// The [`InvoiceRequest`] associated with the [`Offer`] corresponding to this payment. invoice_request: Option, @@ -3732,7 +3732,7 @@ where let mut secp_ctx = Secp256k1::new(); secp_ctx.seeded_randomize(&entropy_source.get_secure_random_bytes()); - let expanded_inbound_key = node_signer.get_inbound_payment_key(); + let expanded_inbound_key = node_signer.get_expanded_key(); let our_network_pubkey = node_signer.get_node_id(Recipient::Node).unwrap(); let flow = OffersMessageFlow::new( @@ -16697,7 +16697,7 @@ where } } - let expanded_inbound_key = args.node_signer.get_inbound_payment_key(); + let expanded_inbound_key = args.node_signer.get_expanded_key(); let mut claimable_payments = hash_map_with_capacity(claimable_htlcs_list.len()); if let Some(purposes) = claimable_htlc_purposes { diff --git a/lightning/src/ln/inbound_payment.rs b/lightning/src/ln/inbound_payment.rs index f7b2a4a2b57..17c2526e78d 100644 --- a/lightning/src/ln/inbound_payment.rs +++ b/lightning/src/ln/inbound_payment.rs @@ -37,9 +37,9 @@ const AMT_MSAT_LEN: usize = 8; // retrieve said payment type bits. const METHOD_TYPE_OFFSET: usize = 5; -/// A set of keys that were HKDF-expanded. Returned by [`NodeSigner::get_inbound_payment_key`]. +/// A set of keys that were HKDF-expanded. Returned by [`NodeSigner::get_expanded_key`]. /// -/// [`NodeSigner::get_inbound_payment_key`]: crate::sign::NodeSigner::get_inbound_payment_key +/// [`NodeSigner::get_expanded_key`]: crate::sign::NodeSigner::get_expanded_key #[derive(Hash, Copy, Clone, PartialEq, Eq, Debug)] pub struct ExpandedKey { /// The key used to encrypt the bytes containing the payment metadata (i.e. the amount and @@ -133,7 +133,7 @@ fn min_final_cltv_expiry_delta_from_metadata(bytes: [u8; METADATA_LEN]) -> u16 { /// `ChannelManager` is required. Useful for generating invoices for [phantom node payments] without /// a `ChannelManager`. /// -/// `keys` is generated by calling [`NodeSigner::get_inbound_payment_key`]. It is recommended to +/// `keys` is generated by calling [`NodeSigner::get_expanded_key`]. It is recommended to /// cache this value and not regenerate it for each new inbound payment. /// /// `current_time` is a Unix timestamp representing the current time. @@ -142,7 +142,7 @@ fn min_final_cltv_expiry_delta_from_metadata(bytes: [u8; METADATA_LEN]) -> u16 { /// on versions of LDK prior to 0.0.114. /// /// [phantom node payments]: crate::sign::PhantomKeysManager -/// [`NodeSigner::get_inbound_payment_key`]: crate::sign::NodeSigner::get_inbound_payment_key +/// [`NodeSigner::get_expanded_key`]: crate::sign::NodeSigner::get_expanded_key pub fn create( keys: &ExpandedKey, min_value_msat: Option, invoice_expiry_delta_secs: u32, entropy_source: &ES, current_time: u64, min_final_cltv_expiry_delta: Option, @@ -321,7 +321,7 @@ fn construct_payment_secret( /// For payments including a custom `min_final_cltv_expiry_delta`, the metadata is constructed as: /// payment method (3 bits) || payment amount (8 bytes - 3 bits) || min_final_cltv_expiry_delta (2 bytes) || expiry (6 bytes) /// -/// In both cases the result is then encrypted using a key derived from [`NodeSigner::get_inbound_payment_key`]. +/// In both cases the result is then encrypted using a key derived from [`NodeSigner::get_expanded_key`]. /// /// Then on payment receipt, we verify in this method that the payment preimage and payment secret /// match what was constructed. @@ -342,7 +342,7 @@ fn construct_payment_secret( /// /// See [`ExpandedKey`] docs for more info on the individual keys used. /// -/// [`NodeSigner::get_inbound_payment_key`]: crate::sign::NodeSigner::get_inbound_payment_key +/// [`NodeSigner::get_expanded_key`]: crate::sign::NodeSigner::get_expanded_key /// [`create_inbound_payment`]: crate::ln::channelmanager::ChannelManager::create_inbound_payment /// [`create_inbound_payment_for_hash`]: crate::ln::channelmanager::ChannelManager::create_inbound_payment_for_hash pub(super) fn verify( diff --git a/lightning/src/ln/invoice_utils.rs b/lightning/src/ln/invoice_utils.rs index 509cb2e3b7b..c08d4fa14c5 100644 --- a/lightning/src/ln/invoice_utils.rs +++ b/lightning/src/ln/invoice_utils.rs @@ -195,7 +195,7 @@ where }, }; - let keys = node_signer.get_inbound_payment_key(); + let keys = node_signer.get_expanded_key(); let (payment_hash, payment_secret) = if let Some(payment_hash) = payment_hash { let payment_secret = create_from_hash( &keys, diff --git a/lightning/src/ln/max_payment_path_len_tests.rs b/lightning/src/ln/max_payment_path_len_tests.rs index 4efa105e0ad..177050baf90 100644 --- a/lightning/src/ln/max_payment_path_len_tests.rs +++ b/lightning/src/ln/max_payment_path_len_tests.rs @@ -222,7 +222,7 @@ fn one_hop_blinded_path_with_custom_tlv() { payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), }; let nonce = Nonce([42u8; 16]); - let expanded_key = chanmon_cfgs[2].keys_manager.get_inbound_payment_key(); + let expanded_key = chanmon_cfgs[2].keys_manager.get_expanded_key(); let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key); let mut secp_ctx = Secp256k1::new(); let blinded_path = BlindedPaymentPath::new( diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 71f73e04c2f..6d025293b9e 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -3589,7 +3589,7 @@ where }, ChaChaPolyReadAdapter { readable: BlindedPaymentTlvs::Receive(receive_tlvs) } => { let ReceiveTlvs { tlvs, authentication: (hmac, nonce) } = receive_tlvs; - let expanded_key = node_signer.get_inbound_payment_key(); + let expanded_key = node_signer.get_expanded_key(); if tlvs.verify_for_offer_payment(hmac, nonce, &expanded_key).is_err() { return Err(DecodeError::InvalidValue); } @@ -3741,7 +3741,7 @@ where readable: BlindedTrampolineTlvs::Receive(receive_tlvs), } => { let ReceiveTlvs { tlvs, authentication: (hmac, nonce) } = receive_tlvs; - let expanded_key = node_signer.get_inbound_payment_key(); + let expanded_key = node_signer.get_expanded_key(); if tlvs.verify_for_offer_payment(hmac, nonce, &expanded_key).is_err() { return Err(DecodeError::InvalidValue); } diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 6c56ecc4270..c2971b31dab 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -2272,7 +2272,7 @@ fn fails_paying_invoice_with_unknown_required_features() { let payment_paths = invoice.payment_paths().to_vec(); let payment_hash = invoice.payment_hash(); - let expanded_key = alice.keys_manager.get_inbound_payment_key(); + let expanded_key = alice.keys_manager.get_expanded_key(); let secp_ctx = Secp256k1::new(); let created_at = alice.node.duration_since_epoch(); diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs index 057a7406a5a..4a3383c443f 100644 --- a/lightning/src/sign/mod.rs +++ b/lightning/src/sign/mod.rs @@ -837,19 +837,25 @@ pub trait EntropySource { /// A trait that can handle cryptographic operations at the scope level of a node. pub trait NodeSigner { - /// Get the [`ExpandedKey`] for use in encrypting and decrypting inbound payment data. + /// Get the [`ExpandedKey`] which provides cryptographic material for various Lightning Network operations. /// - /// If the implementor of this trait supports [phantom node payments], then every node that is - /// intended to be included in the phantom invoice route hints must return the same value from - /// this method. - // This is because LDK avoids storing inbound payment data by encrypting payment data in the - // payment hash and/or payment secret, therefore for a payment to be receivable by multiple - // nodes, they must share the key that encrypts this payment data. + /// This key set is used for: + /// - Encrypting and decrypting inbound payment metadata + /// - Authenticating payment hashes (both LDK-provided and user-provided) + /// - Supporting BOLT 12 Offers functionality (key derivation and authentication) + /// - Authenticating spontaneous payments' metadata /// /// This method must return the same value each time it is called. /// + /// If the implementor of this trait supports [phantom node payments], then every node that is + /// intended to be included in the phantom invoice route hints must return the same value from + /// this method. This is because LDK avoids storing inbound payment data. Instead, this key + /// is used to construct a payment secret which is received in the payment onion and used to + /// reconstruct the payment preimage. Therefore, for a payment to be receivable by multiple + /// nodes, they must share the same key. + /// /// [phantom node payments]: PhantomKeysManager - fn get_inbound_payment_key(&self) -> ExpandedKey; + fn get_expanded_key(&self) -> ExpandedKey; /// Defines a method to derive a 32-byte encryption key for peer storage. /// @@ -2184,7 +2190,7 @@ impl NodeSigner for KeysManager { Ok(SharedSecret::new(other_key, &node_secret)) } - fn get_inbound_payment_key(&self) -> ExpandedKey { + fn get_expanded_key(&self) -> ExpandedKey { self.inbound_payment_key.clone() } @@ -2357,7 +2363,7 @@ impl NodeSigner for PhantomKeysManager { Ok(SharedSecret::new(other_key, &node_secret)) } - fn get_inbound_payment_key(&self) -> ExpandedKey { + fn get_expanded_key(&self) -> ExpandedKey { self.inbound_payment_key.clone() } diff --git a/lightning/src/util/dyn_signer.rs b/lightning/src/util/dyn_signer.rs index fc2f6329aeb..b040ecab6f4 100644 --- a/lightning/src/util/dyn_signer.rs +++ b/lightning/src/util/dyn_signer.rs @@ -217,7 +217,7 @@ inner, fn sign_bolt12_invoice(, invoice: &crate::offers::invoice::UnsignedBolt12Invoice ) -> Result, - fn get_inbound_payment_key(,) -> ExpandedKey, + fn get_expanded_key(,) -> ExpandedKey, fn get_peer_storage_key(,) -> PeerStorageKey, fn get_receive_auth_key(,) -> ReceiveAuthKey ); @@ -284,7 +284,7 @@ delegate!(DynPhantomKeysInterface, NodeSigner, fn sign_invoice(, invoice: &RawBolt11Invoice, recipient: Recipient) -> Result, fn sign_bolt12_invoice(, invoice: &crate::offers::invoice::UnsignedBolt12Invoice ) -> Result, - fn get_inbound_payment_key(,) -> ExpandedKey, + fn get_expanded_key(,) -> ExpandedKey, fn get_peer_storage_key(,) -> PeerStorageKey, fn get_receive_auth_key(,) -> ReceiveAuthKey ); diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index e5388b76049..22334b182dc 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -1550,7 +1550,7 @@ impl TestNodeSigner { } impl NodeSigner for TestNodeSigner { - fn get_inbound_payment_key(&self) -> ExpandedKey { + fn get_expanded_key(&self) -> ExpandedKey { unreachable!() } @@ -1636,8 +1636,8 @@ impl NodeSigner for TestKeysInterface { self.backing.ecdh(recipient, other_key, tweak) } - fn get_inbound_payment_key(&self) -> ExpandedKey { - self.backing.get_inbound_payment_key() + fn get_expanded_key(&self) -> ExpandedKey { + self.backing.get_expanded_key() } fn sign_invoice( From 224ef96a6439f6c3f9a913621a6ef93207117768 Mon Sep 17 00:00:00 2001 From: shaavan Date: Sat, 19 Jul 2025 17:05:39 +0530 Subject: [PATCH 2/6] Introduce DummyTlv for blinded path privacy DummyTlv represents an empty TLV inserted immediately before the actual ReceiveTlvs in a blinded path. These dummy hops are recursively authenticated like real ones, but carry no content. By allowing arbitrary dummy hops before the final ReceiveTlvs, we can obscure the true position of the recipient in the route. This makes it harder for a malicious onlooker to infer the actual destination, thereby strengthening recipient privacy. --- lightning/src/blinded_path/message.rs | 17 ++++++++++++ lightning/src/onion_message/packet.rs | 40 ++++++++++++++++++--------- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 954247d936d..5f62cf832bb 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -266,6 +266,23 @@ pub(crate) struct ForwardTlvs { pub(crate) next_blinding_override: Option, } +/// Represents the dummy TLV encoded immediately before the actual [`ReceiveTlvs`] in a blinded path. +/// These TLVs are intended for the final node and are recursively authenticated until the real +/// [`ReceiveTlvs`] is reached. +/// +/// Their purpose is to arbitrarily extend the path length, obscuring the receiver's position in the +/// route and thereby enhancing privacy. +pub(crate) struct DummyTlv; + +impl Writeable for DummyTlv { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + encode_tlv_stream!(writer, { + (65539, (), required), + }); + Ok(()) + } +} + /// Similar to [`ForwardTlvs`], but these TLVs are for the final node. pub(crate) struct ReceiveTlvs { /// If `context` is `Some`, it is used to identify the blinded path that this onion message is diff --git a/lightning/src/onion_message/packet.rs b/lightning/src/onion_message/packet.rs index ee41ee98572..2e0ccaf3a3e 100644 --- a/lightning/src/onion_message/packet.rs +++ b/lightning/src/onion_message/packet.rs @@ -16,7 +16,9 @@ use super::async_payments::AsyncPaymentsMessage; use super::dns_resolution::DNSResolverMessage; use super::messenger::CustomOnionMessageHandler; use super::offers::OffersMessage; -use crate::blinded_path::message::{BlindedMessagePath, ForwardTlvs, NextMessageHop, ReceiveTlvs}; +use crate::blinded_path::message::{ + BlindedMessagePath, DummyTlv, ForwardTlvs, NextMessageHop, ReceiveTlvs, +}; use crate::crypto::streams::{ChaChaDualPolyReadAdapter, ChaChaPolyWriteAdapter}; use crate::ln::msgs::DecodeError; use crate::ln::onion_utils; @@ -111,6 +113,12 @@ impl LengthReadable for Packet { pub(super) enum Payload { /// This payload is for an intermediate hop. Forward(ForwardControlTlvs), + /// This payload is a dummy hop, and is intended to be peeled. + Dummy { + /// The payload was authenticated with the additional key that was + /// provided to [`ReadableArgs::read`]. + control_tlvs_authenticated: bool, + }, /// This payload is for the final hop. Receive { /// The [`ReceiveControlTlvs`] were authenticated with the additional key which was @@ -237,6 +245,10 @@ impl Writeable for (Payload, [u8; 32]) { let write_adapter = ChaChaPolyWriteAdapter::new(self.1, &control_tlvs); _encode_varint_length_prefixed_tlv!(w, { (4, write_adapter, required) }) }, + Payload::Dummy { control_tlvs_authenticated: _ } => { + let write_adapter = ChaChaPolyWriteAdapter::new(self.1, &DummyTlv); + _encode_varint_length_prefixed_tlv!(w, { (4, write_adapter, required) }) + }, Payload::Receive { control_tlvs: ReceiveControlTlvs::Unblinded(control_tlvs), reply_path, @@ -316,6 +328,9 @@ impl } Ok(Payload::Forward(ForwardControlTlvs::Unblinded(tlvs))) }, + Some(ChaChaDualPolyReadAdapter { readable: ControlTlvs::Dummy, used_aad }) => { + Ok(Payload::Dummy { control_tlvs_authenticated: used_aad }) + }, Some(ChaChaDualPolyReadAdapter { readable: ControlTlvs::Receive(tlvs), used_aad }) => { Ok(Payload::Receive { control_tlvs: ReceiveControlTlvs::Unblinded(tlvs), @@ -335,6 +350,8 @@ impl pub(crate) enum ControlTlvs { /// This onion message is intended to be forwarded. Forward(ForwardTlvs), + /// This onion message is a dummy, and is intended to be peeled by the final recipient. + Dummy, /// This onion message is intended to be received. Receive(ReceiveTlvs), } @@ -350,6 +367,7 @@ impl Readable for ControlTlvs { (4, next_node_id, option), (8, next_blinding_override, option), (65537, context, option), + (65539, is_dummy, option), }); let next_hop = match (short_channel_id, next_node_id) { @@ -359,18 +377,13 @@ impl Readable for ControlTlvs { (None, None) => None, }; - let valid_fwd_fmt = next_hop.is_some(); - let valid_recv_fmt = next_hop.is_none() && next_blinding_override.is_none(); - - let payload_fmt = if valid_fwd_fmt { - ControlTlvs::Forward(ForwardTlvs { - next_hop: next_hop.unwrap(), - next_blinding_override, - }) - } else if valid_recv_fmt { - ControlTlvs::Receive(ReceiveTlvs { context }) - } else { - return Err(DecodeError::InvalidValue); + let payload_fmt = match (next_hop, next_blinding_override, is_dummy) { + (Some(hop), _, None) => { + ControlTlvs::Forward(ForwardTlvs { next_hop: hop, next_blinding_override }) + }, + (None, None, Some(())) => ControlTlvs::Dummy, + (None, None, None) => ControlTlvs::Receive(ReceiveTlvs { context }), + _ => return Err(DecodeError::InvalidValue), }; Ok(payload_fmt) @@ -381,6 +394,7 @@ impl Writeable for ControlTlvs { fn write(&self, w: &mut W) -> Result<(), io::Error> { match self { Self::Forward(tlvs) => tlvs.write(w), + Self::Dummy => DummyTlv.write(w), Self::Receive(tlvs) => tlvs.write(w), } } From 4f5b367612dfcdd8474f6b7bf87448d1a05c4bd3 Mon Sep 17 00:00:00 2001 From: shaavan Date: Sat, 19 Jul 2025 17:20:57 +0530 Subject: [PATCH 3/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/message.rs | 40 +++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 5f62cf832bb..8105bb206ee 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -31,9 +31,9 @@ use crate::types::payment::PaymentHash; use crate::util::scid_utils; use crate::util::ser::{FixedLengthReader, LengthReadableArgs, Readable, Writeable, Writer}; -use core::mem; use core::ops::Deref; use core::time::Duration; +use core::{cmp, mem}; /// A blinded path to be used for sending or receiving a message, hiding the identity of the /// recipient. @@ -74,6 +74,29 @@ impl BlindedMessagePath { local_node_receive_key: ReceiveAuthKey, context: MessageContext, entropy_source: ES, secp_ctx: &Secp256k1, ) -> Result + where + ES::Target: EntropySource, + { + BlindedMessagePath::new_with_dummy_hops( + intermediate_nodes, + recipient_node_id, + 0, + local_node_receive_key, + context, + entropy_source, + secp_ctx, + ) + } + + /// Same as [`BlindedMessagePath::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: &[MessageForwardNode], recipient_node_id: PublicKey, + dummy_hop_count: usize, local_node_receive_key: ReceiveAuthKey, context: MessageContext, + entropy_source: ES, secp_ctx: &Secp256k1, + ) -> Result where ES::Target: EntropySource, { @@ -91,6 +114,7 @@ impl BlindedMessagePath { secp_ctx, intermediate_nodes, recipient_node_id, + dummy_hop_count, context, &blinding_secret, local_node_receive_key, @@ -635,15 +659,24 @@ impl_writeable_tlv_based!(DNSResolverContext, { /// to pad message blinded path's [`BlindedHop`] pub(crate) const MESSAGE_PADDING_ROUND_OFF: usize = 100; +/// The maximum number of dummy hops that can be added to a blinded path. +/// This is to prevent paths from becoming too long and potentially causing +/// issues with message processing or routing. +pub const MAX_DUMMY_HOPS_COUNT: usize = 10; + /// Construct blinded onion message hops for the given `intermediate_nodes` and `recipient_node_id`. pub(super) fn blinded_hops( secp_ctx: &Secp256k1, intermediate_nodes: &[MessageForwardNode], - recipient_node_id: PublicKey, context: MessageContext, session_priv: &SecretKey, - local_node_receive_key: ReceiveAuthKey, + recipient_node_id: PublicKey, dummy_hop_count: usize, context: MessageContext, + session_priv: &SecretKey, local_node_receive_key: ReceiveAuthKey, ) -> Result, secp256k1::Error> { + let dummy_count = cmp::min(dummy_hop_count, MAX_DUMMY_HOPS_COUNT); let pks = intermediate_nodes .iter() .map(|node| (node.node_id, None)) + .chain( + core::iter::repeat((recipient_node_id, Some(local_node_receive_key))).take(dummy_count), + ) .chain(core::iter::once((recipient_node_id, Some(local_node_receive_key)))); let is_compact = intermediate_nodes.iter().any(|node| node.short_channel_id.is_some()); @@ -658,6 +691,7 @@ pub(super) fn blinded_hops( .map(|next_hop| { ControlTlvs::Forward(ForwardTlvs { next_hop, next_blinding_override: None }) }) + .chain((0..dummy_count).map(|_| ControlTlvs::Dummy)) .chain(core::iter::once(ControlTlvs::Receive(ReceiveTlvs { context: Some(context) }))); if is_compact { From b8d62fc862b655caa78a91df585cadd53e0c81b2 Mon Sep 17 00:00:00 2001 From: shaavan Date: Tue, 29 Jul 2025 17:55:58 +0530 Subject: [PATCH 4/6] Introduce parsing logic for DummyTlvs --- lightning/src/onion_message/messenger.rs | 101 ++++++++++++++--------- 1 file changed, 61 insertions(+), 40 deletions(-) diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 553977dfe18..bb8cbbad3cd 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -1144,6 +1144,44 @@ where msg.onion_routing_packet.hmac, (control_tlvs_ss, custom_handler.deref(), receiving_context_auth_key, logger.deref()), ); + + // Constructs the next onion message using packet data and blinding logic. + let build_outbound_onion_message = |packet_pubkey: PublicKey, + next_hop_hmac: [u8; 32], + new_packet_bytes: Vec, + blinding_point_opt: Option| + -> Result { + let new_pubkey = + match onion_utils::next_hop_pubkey(&secp_ctx, packet_pubkey, &onion_decode_ss) { + Ok(pk) => pk, + Err(e) => { + log_trace!(logger, "Failed to compute next hop packet pubkey: {}", e); + return Err(()); + }, + }; + let outgoing_packet = Packet { + version: 0, + public_key: new_pubkey, + hop_data: new_packet_bytes, + hmac: next_hop_hmac, + }; + let blinding_point = match blinding_point_opt { + Some(bp) => bp, + None => match onion_utils::next_hop_pubkey( + &secp_ctx, + msg.blinding_point, + control_tlvs_ss.as_ref(), + ) { + Ok(bp) => bp, + Err(e) => { + log_trace!(logger, "Failed to compute next blinding point: {}", e); + return Err(()); + }, + }, + }; + Ok(OnionMessage { blinding_point, onion_routing_packet: outgoing_packet }) + }; + match next_hop { Ok(( Payload::Receive { @@ -1216,6 +1254,23 @@ where Err(()) }, }, + Ok(( + Payload::Dummy { control_tlvs_authenticated }, + Some((next_hop_hmac, new_packet_bytes)), + )) => { + if !control_tlvs_authenticated { + log_trace!(logger, "Received an unauthenticated dummy onion message"); + return Err(()); + } + + let onion_message = build_outbound_onion_message( + msg.onion_routing_packet.public_key, + next_hop_hmac, + new_packet_bytes, + None, + )?; + peel_onion_message(&onion_message, secp_ctx, node_signer, logger, custom_handler) + }, Ok(( Payload::Forward(ForwardControlTlvs::Unblinded(ForwardTlvs { next_hop, @@ -1223,46 +1278,12 @@ where })), Some((next_hop_hmac, new_packet_bytes)), )) => { - // TODO: we need to check whether `next_hop` is our node, in which case this is a dummy - // blinded hop and this onion message is destined for us. In this situation, we should keep - // unwrapping the onion layers to get to the final payload. Since we don't have the option - // of creating blinded paths with dummy hops currently, we should be ok to not handle this - // for now. - let packet_pubkey = msg.onion_routing_packet.public_key; - let new_pubkey_opt = - onion_utils::next_hop_pubkey(&secp_ctx, packet_pubkey, &onion_decode_ss); - let new_pubkey = match new_pubkey_opt { - Ok(pk) => pk, - Err(e) => { - log_trace!(logger, "Failed to compute next hop packet pubkey: {}", e); - return Err(()); - }, - }; - let outgoing_packet = Packet { - version: 0, - public_key: new_pubkey, - hop_data: new_packet_bytes, - hmac: next_hop_hmac, - }; - let onion_message = OnionMessage { - blinding_point: match next_blinding_override { - Some(blinding_point) => blinding_point, - None => { - match onion_utils::next_hop_pubkey( - &secp_ctx, - msg.blinding_point, - control_tlvs_ss.as_ref(), - ) { - Ok(bp) => bp, - Err(e) => { - log_trace!(logger, "Failed to compute next blinding point: {}", e); - return Err(()); - }, - } - }, - }, - onion_routing_packet: outgoing_packet, - }; + let onion_message = build_outbound_onion_message( + msg.onion_routing_packet.public_key, + next_hop_hmac, + new_packet_bytes, + next_blinding_override, + )?; Ok(PeeledOnion::Forward(next_hop, onion_message)) }, From 988defb8671c63bd0fadd145d0b2d3de17bd68ab Mon Sep 17 00:00:00 2001 From: shaavan Date: Sat, 19 Jul 2025 17:32:06 +0530 Subject: [PATCH 5/6] Update Default Blinded Path constructor to use Dummy Hops Applies dummy hops by default when constructing blinded paths via `DefaultMessageRouter`, enhancing privacy by obscuring the true path length. Uses a predefined `DUMMY_HOPS_COUNT` to apply dummy hops consistently without requiring explicit user input. --- lightning/src/onion_message/messenger.rs | 76 ++++++++++++++---------- 1 file changed, 45 insertions(+), 31 deletions(-) diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index bb8cbbad3cd..ede82b41aa8 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -540,6 +540,14 @@ where entropy_source: ES, } +// Target total length (in hops) for non-compact blinded paths. +// We pad with dummy hops until the path reaches this length, +// obscuring the recipient's true position. +// +// Compact paths are optimized for minimal size, so we avoid +// adding dummy hops to them. +pub(crate) const PADDED_PATH_LENGTH: usize = 4; + impl>, L: Deref, ES: Deref> DefaultMessageRouter where L::Target: Logger, @@ -595,40 +603,46 @@ where a_tor_only.cmp(b_tor_only).then(a_channels.cmp(b_channels).reverse()) }); - let entropy = &**entropy_source; - let paths = peer_info + let build_path = |intermediate_hops: &[MessageForwardNode]| { + let dummy_hops_count = if compact_paths { + 0 + } else { + // Add one for the final recipient TLV + PADDED_PATH_LENGTH.saturating_sub(intermediate_hops.len() + 1) + }; + + BlindedMessagePath::new_with_dummy_hops( + intermediate_hops, + recipient, + dummy_hops_count, + local_node_receive_key, + context.clone(), + &**entropy_source, + secp_ctx, + ) + }; + + // Try to create paths from peer info, fall back to direct path if needed + let mut paths = peer_info .into_iter() - .map(|(peer, _, _)| { - BlindedMessagePath::new( - &[peer], - recipient, - local_node_receive_key, - context.clone(), - entropy, - secp_ctx, - ) - }) + .map(|(peer, _, _)| build_path(&[peer])) .take(MAX_PATHS) - .collect::, _>>(); - - let mut paths = match paths { - Ok(paths) if !paths.is_empty() => Ok(paths), - _ => { - if is_recipient_announced { - BlindedMessagePath::new( - &[], - recipient, - local_node_receive_key, - context, - &**entropy_source, - secp_ctx, - ) + .collect::, _>>() + .ok() + .filter(|paths| !paths.is_empty()) + .or_else(|| { + is_recipient_announced + .then(|| build_path(&[])) + .and_then(|result| result.ok()) .map(|path| vec![path]) - } else { - Err(()) - } - }, - }?; + }) + .ok_or(())?; + + // Sanity check: Ones the paths are created for the non-compact case, ensure + // each of them are of the length `PADDED_PATH_LENGTH`. + if !compact_paths { + debug_assert!(paths.iter().all(|path| path.blinded_hops().len() == PADDED_PATH_LENGTH)); + } if compact_paths { for path in &mut paths { From fdf352b7f38b833c662b20115140c02f5169220f Mon Sep 17 00:00:00 2001 From: shaavan Date: Sat, 19 Jul 2025 17:37:31 +0530 Subject: [PATCH 6/6] Add test for dummy hop insertion Introduces a test to verify correct handling of dummy hops in constructed blinded paths. Ensures that the added dummy hops are properly included and do not interfere with the real path. --- lightning/src/blinded_path/utils.rs | 47 +++++++++--- lightning/src/ln/offers_tests.rs | 72 ++++++++++++++++++- .../src/onion_message/functional_tests.rs | 37 +++++++++- 3 files changed, 143 insertions(+), 13 deletions(-) diff --git a/lightning/src/blinded_path/utils.rs b/lightning/src/blinded_path/utils.rs index 976c821ad5b..3956fd9bcdf 100644 --- a/lightning/src/blinded_path/utils.rs +++ b/lightning/src/blinded_path/utils.rs @@ -276,16 +276,43 @@ impl Writeable for BlindedPathWithPadding { } #[cfg(test)] -/// Checks if all the packets in the blinded path are properly padded. +/// Verifies whether all hops in the blinded path follow the expected padding scheme. +/// +/// In the padded encoding scheme, each hop's encrypted payload is expected to be of the form: +/// `n * padding_round_off + extra`, where: +/// - `padding_round_off` is the fixed block size to which unencrypted payloads are padded. +/// - `n` is a positive integer (n ≥ 1). +/// - `extra` is the fixed overhead added during encryption (assumed uniform across hops). +/// +/// This function infers the `extra` from the first hop, and checks that all other hops conform +/// to the same pattern. +/// +/// # Returns +/// - `true` if all hop payloads are padded correctly. +/// - `false` if padding is incorrectly applied or intentionally absent (e.g., in compact paths). pub fn is_padded(hops: &[BlindedHop], padding_round_off: usize) -> bool { let first_hop = hops.first().expect("BlindedPath must have at least one hop"); - let first_payload_size = first_hop.encrypted_payload.len(); - - // The unencrypted payload data is padded before getting encrypted. - // Assuming the first payload is padded properly, get the extra data length. - let extra_length = first_payload_size % padding_round_off; - hops.iter().all(|hop| { - // Check that every packet is padded to the round off length subtracting the extra length. - (hop.encrypted_payload.len() - extra_length) % padding_round_off == 0 - }) + let first_len = first_hop.encrypted_payload.len(); + + // Early rejection: if the first hop is too small, it can't be correctly padded. + if first_len <= padding_round_off { + return false; + } + + let extra = first_len % padding_round_off; + + // Helper to check if a hop follows the padding pattern + let is_hop_padded = |hop: &BlindedHop| { + let len = hop.encrypted_payload.len(); + len > extra && (len - extra) % padding_round_off == 0 + }; + + // All hops must follow the same padding structure AND + // all hops except the final one must have the same length as the first + // to ensure proper masking. + hops.iter().all(is_hop_padded) + && hops + .iter() + .take(hops.len().saturating_sub(1)) + .all(|hop| hop.encrypted_payload.len() == first_len) } diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index c2971b31dab..e2bdfc44e29 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -60,7 +60,7 @@ use crate::offers::invoice_error::InvoiceError; use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields}; use crate::offers::nonce::Nonce; use crate::offers::parse::Bolt12SemanticError; -use crate::onion_message::messenger::{Destination, MessageSendInstructions, NodeIdMessageRouter, NullMessageRouter, PeeledOnion}; +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}; @@ -435,6 +435,76 @@ fn prefers_more_connected_nodes_in_blinded_paths() { } } +/// Tests the dummy hop behavior of Offers based on the message router used: +/// - Compact paths (`DefaultMessageRouter`) should not include dummy hops. +/// - Node ID paths (`NodeIdMessageRouter`) may include 0 to [`MAX_DUMMY_HOPS_COUNT`] dummy hops. +/// +/// Also verifies that the resulting paths are functional: the counterparty can respond with a valid `invoice_request`. +#[test] +fn check_dummy_hop_pattern_in_offer() { + 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); + + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + + let alice = &nodes[0]; + let alice_id = alice.node.get_our_node_id(); + let bob = &nodes[1]; + let bob_id = bob.node.get_our_node_id(); + + // Case 1: DefaultMessageRouter → uses compact blinded paths (via SCIDs) + // Expected: No dummy hops; each path contains only the recipient. + let default_router = DefaultMessageRouter::new(alice.network_graph, alice.keys_manager); + + let compact_offer = alice.node + .create_offer_builder_using_router(&default_router).unwrap() + .amount_msats(10_000_000) + .build().unwrap(); + + assert!(!compact_offer.paths().is_empty()); + + for path in compact_offer.paths() { + assert_eq!( + path.blinded_hops().len(), 1, + "Compact paths must include only the recipient" + ); + } + + let payment_id = PaymentId([1; 32]); + bob.node.pay_for_offer(&compact_offer, None, None, None, payment_id, Retry::Attempts(0), RouteParametersConfig::default()).unwrap(); + + let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); + let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); + + assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); + assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); + assert!(check_compact_path_introduction_node(&reply_path, alice, bob_id)); + + // Case 2: NodeIdMessageRouter → uses node ID-based blinded paths + // Expected: 0 to MAX_DUMMY_HOPS_COUNT dummy hops, followed by recipient. + let node_id_router = NodeIdMessageRouter::new(alice.network_graph, alice.keys_manager); + + let padded_offer = alice.node + .create_offer_builder_using_router(&node_id_router).unwrap() + .amount_msats(10_000_000) + .build().unwrap(); + + assert!(!padded_offer.paths().is_empty()); + assert!(padded_offer.paths().iter().all(|path| path.blinded_hops().len() == PADDED_PATH_LENGTH)); + + let payment_id = PaymentId([2; 32]); + bob.node.pay_for_offer(&padded_offer, None, None, None, payment_id, Retry::Attempts(0), RouteParametersConfig::default()).unwrap(); + + let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); + let (invoice_request, reply_path) = extract_invoice_request(alice, &onion_message); + + assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); + assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); + assert!(check_compact_path_introduction_node(&reply_path, alice, bob_id)); +} + /// Checks that blinded paths are compact for short-lived offers. #[test] fn creates_short_lived_offer() { diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index 3cbb618dc0b..4bec3dc31b3 100644 --- a/lightning/src/onion_message/functional_tests.rs +++ b/lightning/src/onion_message/functional_tests.rs @@ -145,6 +145,9 @@ const CUSTOM_PONG_MESSAGE_TYPE: u64 = 4343; const CUSTOM_PING_MESSAGE_CONTENTS: [u8; 32] = [42; 32]; const CUSTOM_PONG_MESSAGE_CONTENTS: [u8; 32] = [43; 32]; +/// A dummy hop count for testing purposes. +const TEST_DUMMY_HOP_COUNT: usize = 5; + impl OnionMessageContents for TestCustomMessage { fn tlv_type(&self) -> u64 { match self { @@ -443,6 +446,34 @@ fn one_blinded_hop() { pass_along_path(&nodes); } +#[test] +fn blinded_path_with_dummy_hops() { + let nodes = create_nodes(2); + let test_msg = TestCustomMessage::Pong; + + let secp_ctx = Secp256k1::new(); + let context = MessageContext::Custom(Vec::new()); + let entropy = &*nodes[1].entropy_source; + let receive_key = nodes[1].messenger.node_signer.get_receive_auth_key(); + let blinded_path = BlindedMessagePath::new_with_dummy_hops( + &[], + nodes[1].node_id, + TEST_DUMMY_HOP_COUNT, + receive_key, + context, + entropy, + &secp_ctx, + ) + .unwrap(); + // Ensure that dummy hops are added to the blinded path. + assert_eq!(blinded_path.blinded_hops().len(), 6); + let destination = Destination::BlindedPath(blinded_path); + let instructions = MessageSendInstructions::WithoutReplyPath { destination }; + nodes[0].messenger.send_onion_message(test_msg, instructions).unwrap(); + nodes[1].custom_message_handler.expect_message(TestCustomMessage::Pong); + pass_along_path(&nodes); +} + #[test] fn two_unblinded_two_blinded() { let nodes = create_nodes(5); @@ -658,9 +689,10 @@ fn test_blinded_path_padding_for_full_length_path() { let context = MessageContext::Custom(vec![0u8; 42]); let entropy = &*nodes[3].entropy_source; let receive_key = nodes[3].messenger.node_signer.get_receive_auth_key(); - let blinded_path = BlindedMessagePath::new( + let blinded_path = BlindedMessagePath::new_with_dummy_hops( &intermediate_nodes, nodes[3].node_id, + TEST_DUMMY_HOP_COUNT, receive_key, context, entropy, @@ -694,9 +726,10 @@ fn test_blinded_path_no_padding_for_compact_path() { let context = MessageContext::Custom(vec![0u8; 42]); let entropy = &*nodes[3].entropy_source; let receive_key = nodes[3].messenger.node_signer.get_receive_auth_key(); - let blinded_path = BlindedMessagePath::new( + let blinded_path = BlindedMessagePath::new_with_dummy_hops( &intermediate_nodes, nodes[3].node_id, + TEST_DUMMY_HOP_COUNT, receive_key, context, entropy,