From 51e755d73cc7cd2e744f2565147cdbdc93660c6c Mon Sep 17 00:00:00 2001 From: shaavan Date: Fri, 16 May 2025 16:25:11 +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 | 2 +- fuzz/src/onion_message.rs | 2 +- lightning/src/ln/async_payments_tests.rs | 2 +- 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 | 21 ++++++++++++------- lightning/src/util/dyn_signer.rs | 4 ++-- lightning/src/util/test_utils.rs | 6 +++--- 14 files changed, 44 insertions(+), 39 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index d4ffb12d0b4..2dac3dd9503 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 d2bd5ae4d59..733f1337d6d 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -412,7 +412,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 85fb5a9f513..178cca03fd6 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 24699f33212..02a12c16811 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -904,7 +904,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 1be51ef4a17..7ef14877f5f 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, @@ -1346,7 +1346,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( @@ -1400,7 +1400,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(); @@ -1632,7 +1632,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, @@ -1943,7 +1943,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, @@ -2030,7 +2030,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 7f77f20e088..54e2ec3af95 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -322,7 +322,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, @@ -3705,7 +3705,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( @@ -16259,7 +16259,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 a7d45b896a9..2aeba0cdf89 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, @@ -322,7 +322,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. @@ -343,7 +343,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 f2e8284a617..876f5eccb27 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 ff5053644d8..f350f1da44c 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 03389c09609..d020cb43bbd 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -3548,7 +3548,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); } @@ -3700,7 +3700,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 ad0a8eea2aa..f826feb24e0 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -2225,7 +2225,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 a1ba428ded6..5bac05cdb54 100644 --- a/lightning/src/sign/mod.rs +++ b/lightning/src/sign/mod.rs @@ -837,19 +837,24 @@ 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. + /// + /// 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 (signing and encryption) + /// - Authenticating spontaneous payments' metadata /// /// 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 method must return the same value each time it is called. + /// This method must return the same value each time it is called, as LDK avoids storing inbound + /// payment data by encrypting it in the payment hash and/or payment secret. Consistency is also + /// required for signature and encryption verification in Offers and spontaneous payments. /// /// [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. /// @@ -2173,7 +2178,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() } @@ -2342,7 +2347,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 ea062de5c76..d3f08cc2955 100644 --- a/lightning/src/util/dyn_signer.rs +++ b/lightning/src/util/dyn_signer.rs @@ -216,7 +216,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 ); @@ -282,7 +282,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 4165afea767..b2869b9a74a 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -1522,7 +1522,7 @@ impl TestNodeSigner { } impl NodeSigner for TestNodeSigner { - fn get_inbound_payment_key(&self) -> ExpandedKey { + fn get_expanded_key(&self) -> ExpandedKey { unreachable!() } @@ -1604,8 +1604,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 fbb1ff7b7466bc47ae9707a5c851253d3f59d20e Mon Sep 17 00:00:00 2001 From: shaavan Date: Sat, 19 Jul 2025 17:05:39 +0530 Subject: [PATCH 2/6] Introduce DummyTlv --- lightning/src/blinded_path/message.rs | 17 +++++++++ lightning/src/onion_message/packet.rs | 52 ++++++++++++++++++++------- 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 218b2282141..0d59ffa8a31 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 and verified 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 301473fba6a..10b7a7c824b 100644 --- a/lightning/src/onion_message/packet.rs +++ b/lightning/src/onion_message/packet.rs @@ -17,7 +17,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; @@ -112,6 +114,13 @@ 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 [`DummyControlTlvs`] were authenticated with the additional key which was + /// provided to [`ReadableArgs::read`]. + control_tlvs_authenticated: bool, + control_tlvs: DummyControlTlvs, + }, /// This payload is for the final hop. Receive { /// The [`ReceiveControlTlvs`] were authenticated with the additional key which was @@ -212,6 +221,11 @@ pub(super) enum ForwardControlTlvs { Unblinded(ForwardTlvs), } +pub(super) enum DummyControlTlvs { + /// See [`ForwardControlTlvs::Unblinded`] + Unblinded(DummyTlv), +} + /// Receive control TLVs in their blinded and unblinded form. pub(super) enum ReceiveControlTlvs { /// See [`ForwardControlTlvs::Blinded`]. @@ -243,6 +257,13 @@ 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: DummyControlTlvs::Unblinded(control_tlvs), + control_tlvs_authenticated: _, + } => { + let write_adapter = ChaChaPolyWriteAdapter::new(self.1, &control_tlvs); + _encode_varint_length_prefixed_tlv!(w, { (4, write_adapter, required) }) + }, Payload::Receive { control_tlvs: ReceiveControlTlvs::Unblinded(control_tlvs), reply_path, @@ -323,6 +344,12 @@ impl } Ok(Payload::Forward(ForwardControlTlvs::Unblinded(tlvs))) }, + Some(ChaChaDualPolyReadAdapter { readable: ControlTlvs::Dummy(tlvs), used_aad }) => { + Ok(Payload::Dummy { + control_tlvs_authenticated: used_aad, + control_tlvs: DummyControlTlvs::Unblinded(tlvs), + }) + }, Some(ChaChaDualPolyReadAdapter { readable: ControlTlvs::Receive(tlvs), used_aad }) => { Ok(Payload::Receive { control_tlvs: ReceiveControlTlvs::Unblinded(tlvs), @@ -342,6 +369,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. + Dummy(DummyTlv), /// This onion message is intended to be received. Receive(ReceiveTlvs), } @@ -357,6 +386,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) { @@ -366,18 +396,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(DummyTlv {}), + (None, None, None) => ControlTlvs::Receive(ReceiveTlvs { context }), + _ => return Err(DecodeError::InvalidValue), }; Ok(payload_fmt) @@ -388,6 +413,7 @@ impl Writeable for ControlTlvs { 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), } } From f8ad4ba7bf64842fa485009a097b3de8a7a44ed6 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 | 30 +++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 0d59ffa8a31..098e229bb8f 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -74,6 +74,26 @@ 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 allow specifying a number of dummy hops + pub fn new_with_dummy_hops( + intermediate_nodes: &[MessageForwardNode], recipient_node_id: PublicKey, + dummy_hop_count: u8, local_node_receive_key: ReceiveAuthKey, context: MessageContext, + entropy_source: ES, secp_ctx: &Secp256k1, + ) -> Result where ES::Target: EntropySource, { @@ -91,6 +111,7 @@ impl BlindedMessagePath { secp_ctx, intermediate_nodes, recipient_node_id, + dummy_hop_count, context, &blinding_secret, local_node_receive_key, @@ -641,12 +662,16 @@ pub(crate) const MESSAGE_PADDING_ROUND_OFF: usize = 100; /// 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: u8, context: MessageContext, + session_priv: &SecretKey, local_node_receive_key: ReceiveAuthKey, ) -> Result, secp256k1::Error> { 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_hop_count as usize), + ) .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()); @@ -661,6 +686,7 @@ pub(super) fn blinded_hops( .map(|next_hop| { ControlTlvs::Forward(ForwardTlvs { next_hop, next_blinding_override: None }) }) + .chain((0..dummy_hop_count).map(|_| ControlTlvs::Dummy(DummyTlv {}))) .chain(core::iter::once(ControlTlvs::Receive(ReceiveTlvs { context: Some(context) }))); if is_compact { From 3b56c3677b998eb8d3c7f7d7281f5f44ef008f66 Mon Sep 17 00:00:00 2001 From: shaavan Date: Sat, 19 Jul 2025 17:28:26 +0530 Subject: [PATCH 4/6] Introduce parsing logic for DummyTlvs --- lightning/src/onion_message/messenger.rs | 109 ++++++++++++++--------- 1 file changed, 67 insertions(+), 42 deletions(-) diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index f73d189203b..2492ee66bfa 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -29,8 +29,8 @@ use super::packet::{ #[cfg(async_payments)] use crate::blinded_path::message::AsyncPaymentsContext; use crate::blinded_path::message::{ - BlindedMessagePath, DNSResolverContext, ForwardTlvs, MessageContext, MessageForwardNode, - NextMessageHop, OffersContext, ReceiveTlvs, + BlindedMessagePath, DNSResolverContext, DummyTlv, ForwardTlvs, MessageContext, + MessageForwardNode, NextMessageHop, OffersContext, ReceiveTlvs, }; use crate::blinded_path::utils; use crate::blinded_path::{IntroductionNode, NodeIdLookUp}; @@ -39,6 +39,7 @@ use crate::ln::msgs::{ self, BaseMessageHandler, MessageSendEvent, OnionMessage, OnionMessageHandler, SocketAddress, }; use crate::ln::onion_utils; +use crate::onion_message::packet::DummyControlTlvs; use crate::routing::gossip::{NetworkGraph, NodeId, ReadOnlyNetworkGraph}; use crate::sign::{EntropySource, NodeSigner, ReceiveAuthKey, Recipient}; use crate::types::features::{InitFeatures, NodeFeatures}; @@ -1111,6 +1112,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 compute_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 { @@ -1184,6 +1223,26 @@ where Err(()) }, }, + Ok(( + Payload::Dummy { + control_tlvs_authenticated, + control_tlvs: DummyControlTlvs::Unblinded(DummyTlv {}), + }, + 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 = compute_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, @@ -1191,46 +1250,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 = compute_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 2c5d4abb29119de02721ce7d28af6ffb35472e65 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 | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 2492ee66bfa..6211a092e9e 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -579,6 +579,23 @@ where // recipient's node_id. const MIN_PEER_CHANNELS: usize = 3; + // Add a random number (0 to 5) of dummy hops to each non-compact blinded path + // to make it harder to infer the recipient's position. + // + // # Note on compact paths: + // + // Compact paths are optimized for minimal size. Adding dummy hops to them + // would increase their size and negate their primary advantage. + // Therefore, we avoid adding dummy hops to compact paths. + let dummy_hops_count = if compact_paths { + 0 + } else { + { + let random_byte = entropy_source.get_secure_random_bytes()[0]; + random_byte % 6 + } + }; + let network_graph = network_graph.deref().read_only(); let is_recipient_announced = network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)); @@ -609,9 +626,10 @@ where let paths = peer_info .into_iter() .map(|(peer, _, _)| { - BlindedMessagePath::new( + BlindedMessagePath::new_with_dummy_hops( &[peer], recipient, + dummy_hops_count, local_node_receive_key, context.clone(), entropy, From 3fa00417663d5f2cfc3b1c2fa339ce59b227af03 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 | 6 ++++ .../src/onion_message/functional_tests.rs | 34 +++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/lightning/src/blinded_path/utils.rs b/lightning/src/blinded_path/utils.rs index 976c821ad5b..58a081847bf 100644 --- a/lightning/src/blinded_path/utils.rs +++ b/lightning/src/blinded_path/utils.rs @@ -281,6 +281,12 @@ 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(); + // If the hops are padded correctly, the first payload size must be at least + // the padding round off size. + if first_payload_size < padding_round_off { + return false; + } + // 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; diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index 3cbb618dc0b..bea7182c2aa 100644 --- a/lightning/src/onion_message/functional_tests.rs +++ b/lightning/src/onion_message/functional_tests.rs @@ -443,6 +443,34 @@ fn one_blinded_hop() { pass_along_path(&nodes); } +#[test] +fn blinded_path_with_dummy() { + 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, + 5, + 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 +686,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, + 5, receive_key, context, entropy, @@ -694,9 +723,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, + 5, receive_key, context, entropy,