From c7f108056620ffb63e225e1454fff5849e79d409 Mon Sep 17 00:00:00 2001 From: shaavan Date: Fri, 16 May 2025 16:25:11 +0530 Subject: [PATCH 1/7] 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 8b41c53a635..1439e11da6c 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -326,7 +326,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 5281a933526..435bb24dae1 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -405,7 +405,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 a5782dacd42..fd3294fab28 100644 --- a/fuzz/src/onion_message.rs +++ b/fuzz/src/onion_message.rs @@ -228,7 +228,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 a956f2ebae2..9277775b38d 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -668,7 +668,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 18a1089d647..43de09a936c 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -83,7 +83,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(); @@ -168,7 +168,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(); @@ -222,7 +222,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, @@ -1342,7 +1342,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( @@ -1396,7 +1396,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, @@ -1931,7 +1931,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, @@ -2016,7 +2016,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 170d8261d5a..68a91b0e36f 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -256,7 +256,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, @@ -3576,7 +3576,7 @@ where ) -> Self { 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(); ChannelManager { default_configuration: config.clone(), chain_hash: ChainHash::using_genesis_block(params.network), @@ -14569,7 +14569,7 @@ where }, None)); } - 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 53b212428ca..6a0b52d40b5 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 95a1fbaaa10..8fce1f20e3b 100644 --- a/lightning/src/ln/max_payment_path_len_tests.rs +++ b/lightning/src/ln/max_payment_path_len_tests.rs @@ -173,7 +173,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 2e9c3b90957..8b5b77f9560 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -3443,7 +3443,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); } @@ -3595,7 +3595,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 f8649111a0c..bc9abd59914 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -2223,7 +2223,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 f35a407634a..57af75d1ef1 100644 --- a/lightning/src/sign/mod.rs +++ b/lightning/src/sign/mod.rs @@ -820,19 +820,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; /// Get node id based on the provided [`Recipient`]. /// @@ -2113,7 +2118,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() } @@ -2274,7 +2279,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 939e40cf7c4..2efedbb2355 100644 --- a/lightning/src/util/dyn_signer.rs +++ b/lightning/src/util/dyn_signer.rs @@ -214,7 +214,7 @@ inner, fn sign_bolt12_invoice(, invoice: &crate::offers::invoice::UnsignedBolt12Invoice ) -> Result, - fn get_inbound_payment_key(,) -> ExpandedKey + fn get_expanded_key(,) -> ExpandedKey ); delegate!(DynKeysInterface, SignerProvider, @@ -278,7 +278,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 ); impl SignerProvider for DynPhantomKeysInterface { diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index f90bfb97ef7..7945dc587c8 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -1486,7 +1486,7 @@ impl TestNodeSigner { } impl NodeSigner for TestNodeSigner { - fn get_inbound_payment_key(&self) -> ExpandedKey { + fn get_expanded_key(&self) -> ExpandedKey { unreachable!() } @@ -1559,8 +1559,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 a3e89a782f412f67e7d8a335b71aaae10d76659a Mon Sep 17 00:00:00 2001 From: shaavan Date: Wed, 23 Apr 2025 20:24:45 +0530 Subject: [PATCH 2/7] f: Rename Verification trait function to a more generic name This prepares the trait for use in dummy hop verification and Offer messages. Renaming helps generalize its purpose ahead of upcoming changes. --- lightning/src/blinded_path/payment.rs | 5 +---- lightning/src/ln/channelmanager.rs | 30 +++++++++++++-------------- lightning/src/ln/msgs.rs | 4 ++-- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index f015886933a..ae26d51b1f2 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -351,10 +351,7 @@ impl UnauthenticatedReceiveTlvs { /// Creates an authenticated [`ReceiveTlvs`], which includes an HMAC and the provide [`Nonce`] /// that can be use later to verify it authenticity. pub fn authenticate(self, nonce: Nonce, expanded_key: &ExpandedKey) -> ReceiveTlvs { - ReceiveTlvs { - authentication: (self.hmac_for_offer_payment(nonce, expanded_key), nonce), - tlvs: self, - } + ReceiveTlvs { authentication: (self.hmac_data(nonce, expanded_key), nonce), tlvs: self } } } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 68a91b0e36f..71285938c6b 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -475,12 +475,12 @@ impl Ord for ClaimableHTLC { pub trait Verification { /// Constructs an HMAC to include in [`OffersContext`] for the data along with the given /// [`Nonce`]. - fn hmac_for_offer_payment( + fn hmac_data( &self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Hmac; /// Authenticates the data using an HMAC and a [`Nonce`] taken from an [`OffersContext`]. - fn verify_for_offer_payment( + fn verify_data( &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Result<(), ()>; } @@ -488,7 +488,7 @@ pub trait Verification { impl Verification for PaymentHash { /// Constructs an HMAC to include in [`OffersContext::InboundPayment`] for the payment hash /// along with the given [`Nonce`]. - fn hmac_for_offer_payment( + fn hmac_data( &self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Hmac { signer::hmac_for_payment_hash(*self, nonce, expanded_key) @@ -496,7 +496,7 @@ impl Verification for PaymentHash { /// Authenticates the payment id using an HMAC and a [`Nonce`] taken from an /// [`OffersContext::InboundPayment`]. - fn verify_for_offer_payment( + fn verify_data( &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Result<(), ()> { signer::verify_payment_hash(*self, hmac, nonce, expanded_key) @@ -504,13 +504,13 @@ impl Verification for PaymentHash { } impl Verification for UnauthenticatedReceiveTlvs { - fn hmac_for_offer_payment( + fn hmac_data( &self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Hmac { signer::hmac_for_payment_tlvs(self, nonce, expanded_key) } - fn verify_for_offer_payment( + fn verify_data( &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Result<(), ()> { signer::verify_payment_tlvs(self, hmac, nonce, expanded_key) @@ -550,7 +550,7 @@ impl PaymentId { impl Verification for PaymentId { /// Constructs an HMAC to include in [`OffersContext::OutboundPayment`] for the payment id /// along with the given [`Nonce`]. - fn hmac_for_offer_payment( + fn hmac_data( &self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Hmac { signer::hmac_for_offer_payment_id(*self, nonce, expanded_key) @@ -558,7 +558,7 @@ impl Verification for PaymentId { /// Authenticates the payment id using an HMAC and a [`Nonce`] taken from an /// [`OffersContext::OutboundPayment`]. - fn verify_for_offer_payment( + fn verify_data( &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, ) -> Result<(), ()> { signer::verify_offer_payment_id(*self, hmac, nonce, expanded_key) @@ -10560,7 +10560,7 @@ where }; let invoice_request = builder.build_and_sign()?; - let hmac = payment_id.hmac_for_offer_payment(nonce, expanded_key); + let hmac = payment_id.hmac_data(nonce, expanded_key); let context = MessageContext::Offers( OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) } ); @@ -10664,7 +10664,7 @@ where let invoice = builder.allow_mpp().build_and_sign(secp_ctx)?; let nonce = Nonce::from_entropy_source(entropy); - let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key); + let hmac = payment_hash.hmac_data(nonce, expanded_key); let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash: invoice.payment_hash(), nonce, hmac }); @@ -12444,7 +12444,7 @@ where .release_invoice_requests_awaiting_invoice() { let RetryableInvoiceRequest { invoice_request, nonce, .. } = retryable_invoice_request; - let hmac = payment_id.hmac_for_offer_payment(nonce, &self.inbound_payment_key); + let hmac = payment_id.hmac_data(nonce, &self.inbound_payment_key); let context = MessageContext::Offers(OffersContext::OutboundPayment { payment_id, nonce, @@ -12627,7 +12627,7 @@ where match response { Ok(invoice) => { let nonce = Nonce::from_entropy_source(&*self.entropy_source); - let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key); + let hmac = payment_hash.hmac_data(nonce, expanded_key); let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash, nonce, hmac }); Some((OffersMessage::Invoice(invoice), responder.respond_with_reply_path(context))) }, @@ -12664,7 +12664,7 @@ where OffersMessage::StaticInvoice(invoice) => { let payment_id = match context { Some(OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) }) => { - if payment_id.verify_for_offer_payment(hmac, nonce, expanded_key).is_err() { + if payment_id.verify_data(hmac, nonce, expanded_key).is_err() { return None } payment_id @@ -12677,7 +12677,7 @@ where OffersMessage::InvoiceError(invoice_error) => { let payment_hash = match context { Some(OffersContext::InboundPayment { payment_hash, nonce, hmac }) => { - match payment_hash.verify_for_offer_payment(hmac, nonce, expanded_key) { + match payment_hash.verify_data(hmac, nonce, expanded_key) { Ok(_) => Some(payment_hash), Err(_) => None, } @@ -12690,7 +12690,7 @@ where match context { Some(OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) }) => { - if let Ok(()) = payment_id.verify_for_offer_payment(hmac, nonce, expanded_key) { + if let Ok(()) = payment_id.verify_data(hmac, nonce, expanded_key) { self.abandon_payment_with_reason( payment_id, PaymentFailureReason::InvoiceRequestRejected, ); diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 8b5b77f9560..1c19c3a15d5 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -3444,7 +3444,7 @@ where ChaChaPolyReadAdapter { readable: BlindedPaymentTlvs::Receive(receive_tlvs) } => { let ReceiveTlvs { tlvs, authentication: (hmac, nonce) } = receive_tlvs; let expanded_key = node_signer.get_expanded_key(); - if tlvs.verify_for_offer_payment(hmac, nonce, &expanded_key).is_err() { + if tlvs.verify_data(hmac, nonce, &expanded_key).is_err() { return Err(DecodeError::InvalidValue); } @@ -3596,7 +3596,7 @@ where } => { let ReceiveTlvs { tlvs, authentication: (hmac, nonce) } = receive_tlvs; let expanded_key = node_signer.get_expanded_key(); - if tlvs.verify_for_offer_payment(hmac, nonce, &expanded_key).is_err() { + if tlvs.verify_data(hmac, nonce, &expanded_key).is_err() { return Err(DecodeError::InvalidValue); } From 3548d564b825d8056900428d4bc964459487a354 Mon Sep 17 00:00:00 2001 From: shaavan Date: Tue, 22 Apr 2025 19:39:26 +0530 Subject: [PATCH 3/7] Introduce DummyTlvs Adds new `Dummy` variant to `ControlTlvs`, allowing insertion of arbitrary dummy hops before the final `ReceiveTlvs`. This increases the length of the blinded path, making it harder for a malicious actor to infer the position of the true final hop. --- lightning/src/blinded_path/message.rs | 62 ++++++++++++++++++++++++++- lightning/src/offers/signer.rs | 24 +++++++++++ lightning/src/onion_message/packet.rs | 27 +++++++++++- 3 files changed, 109 insertions(+), 4 deletions(-) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 0e3977ab68e..650aab2d1ef 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -11,6 +11,7 @@ use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey}; +use crate::offers::signer; #[allow(unused_imports)] use crate::prelude::*; @@ -19,9 +20,9 @@ use crate::blinded_path::{BlindedHop, BlindedPath, Direction, IntroductionNode, use crate::crypto::streams::ChaChaPolyReadAdapter; use crate::io; use crate::io::Cursor; -use crate::ln::channelmanager::PaymentId; +use crate::ln::channelmanager::{PaymentId, Verification}; use crate::ln::msgs::DecodeError; -use crate::ln::onion_utils; +use crate::ln::{inbound_payment, onion_utils}; use crate::offers::nonce::Nonce; use crate::onion_message::packet::ControlTlvs; use crate::routing::gossip::{NodeId, ReadOnlyNetworkGraph}; @@ -258,6 +259,63 @@ pub(crate) struct ForwardTlvs { pub(crate) next_blinding_override: Option, } +/// A blank struct, representing dummy tlv prior to authentication. +/// +/// For more details, see [`DummyTlv`]. +pub(crate) struct UnauthenticatedDummyTlv {} + +impl Writeable for UnauthenticatedDummyTlv { + fn write(&self, _writer: &mut W) -> Result<(), io::Error> { + Ok(()) + } +} + +impl Verification for UnauthenticatedDummyTlv { + /// Constructs an HMAC to include in [`OffersContext`] for the data along with the given + /// [`Nonce`]. + fn hmac_data(&self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey) -> Hmac { + signer::hmac_for_dummy_tlv(self, nonce, expanded_key) + } + + /// Authenticates the data using an HMAC and a [`Nonce`] taken from an [`OffersContext`]. + fn verify_data( + &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, + ) -> Result<(), ()> { + signer::verify_dummy_tlv(self, hmac, nonce, expanded_key) + } +} + +/// 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. +/// +/// ## Authentication +/// Authentication provides an additional layer of security, ensuring that the path is legitimate +/// and terminates in valid [`ReceiveTlvs`] data. Verification begins with the first dummy hop and +/// continues recursively until the final [`ReceiveTlvs`] is reached. +/// +/// This prevents an attacker from crafting a bogus blinded path consisting solely of dummy tlv +/// without any valid payload, which could otherwise waste resources through recursive +/// processing — a potential vector for DoS-like attacks. +pub(crate) struct DummyTlv { + pub(crate) dummy_tlv: UnauthenticatedDummyTlv, + /// An HMAC of `tlvs` along with a nonce used to construct it. + pub(crate) authentication: (Hmac, Nonce), +} + +impl Writeable for DummyTlv { + fn write(&self, writer: &mut W) -> Result<(), io::Error> { + encode_tlv_stream!(writer, { + (65539, self.authentication, 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/offers/signer.rs b/lightning/src/offers/signer.rs index 329b90d2076..82b5b6a3f78 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -9,6 +9,7 @@ //! Utilities for signing offer messages and verifying metadata. +use crate::blinded_path::message::UnauthenticatedDummyTlv; use crate::blinded_path::payment::UnauthenticatedReceiveTlvs; use crate::ln::channelmanager::PaymentId; use crate::ln::inbound_payment::{ExpandedKey, IV_LEN}; @@ -570,3 +571,26 @@ pub(crate) fn verify_held_htlc_available_context( Err(()) } } + +pub(crate) fn hmac_for_dummy_tlv( + tlvs: &UnauthenticatedDummyTlv, nonce: Nonce, expanded_key: &ExpandedKey, +) -> Hmac { + const IV_BYTES: &[u8; IV_LEN] = b"LDK Msgs Dummies"; + let mut hmac = expanded_key.hmac_for_offer(); + hmac.input(IV_BYTES); + hmac.input(&nonce.0); + hmac.input(PAYMENT_TLVS_HMAC_INPUT); + tlvs.write(&mut hmac).unwrap(); + + Hmac::from_engine(hmac) +} + +pub(crate) fn verify_dummy_tlv( + tlvs: &UnauthenticatedDummyTlv, hmac: Hmac, nonce: Nonce, expanded_key: &ExpandedKey, +) -> Result<(), ()> { + if hmac_for_dummy_tlv(tlvs, nonce, expanded_key) == hmac { + Ok(()) + } else { + Err(()) + } +} diff --git a/lightning/src/onion_message/packet.rs b/lightning/src/onion_message/packet.rs index 632cbc9c8a3..302e3b4ec85 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, UnauthenticatedDummyTlv, +}; use crate::crypto::streams::{ChaChaPolyReadAdapter, ChaChaPolyWriteAdapter}; use crate::ln::msgs::DecodeError; use crate::ln::onion_utils; @@ -111,6 +113,8 @@ impl LengthReadable for Packet { pub(super) enum Payload { /// This payload is for an intermediate hop. Forward(ForwardControlTlvs), + /// This payload is dummy, and is inteded to be peeled. + Dummy(DummyControlTlvs), /// This payload is for the final hop. Receive { control_tlvs: ReceiveControlTlvs, reply_path: Option, message: T }, } @@ -204,6 +208,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`]. @@ -234,6 +243,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(DummyControlTlvs::Unblinded(control_tlvs)) => { + 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, @@ -310,6 +323,9 @@ impl ReadableArgs<(Sh } Ok(Payload::Forward(ForwardControlTlvs::Unblinded(tlvs))) }, + Some(ChaChaPolyReadAdapter { readable: ControlTlvs::Dummy(tlvs) }) => { + Ok(Payload::Dummy(DummyControlTlvs::Unblinded(tlvs))) + }, Some(ChaChaPolyReadAdapter { readable: ControlTlvs::Receive(tlvs) }) => { Ok(Payload::Receive { control_tlvs: ReceiveControlTlvs::Unblinded(tlvs), @@ -328,6 +344,8 @@ impl ReadableArgs<(Sh 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), } @@ -343,6 +361,7 @@ impl Readable for ControlTlvs { (4, next_node_id, option), (8, next_blinding_override, option), (65537, context, option), + (65539, authentication, option), }); let next_hop = match (short_channel_id, next_node_id) { @@ -363,7 +382,10 @@ impl Readable for ControlTlvs { } else if valid_recv_fmt { ControlTlvs::Receive(ReceiveTlvs { context }) } else { - return Err(DecodeError::InvalidValue); + ControlTlvs::Dummy(DummyTlv { + dummy_tlv: UnauthenticatedDummyTlv {}, + authentication: authentication.ok_or(DecodeError::InvalidValue)?, + }) }; Ok(payload_fmt) @@ -374,6 +396,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 124b762f0802a2df3cb249260b2de8be82e4b6a9 Mon Sep 17 00:00:00 2001 From: shaavan Date: Mon, 12 May 2025 21:51:17 +0530 Subject: [PATCH 4/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. Co-authored-by: valentinewallace --- lightning-background-processor/src/lib.rs | 3 +- lightning-dns-resolver/src/lib.rs | 19 +- lightning-liquidity/tests/common/mod.rs | 9 +- lightning/src/blinded_path/message.rs | 60 +++++- lightning/src/ln/functional_test_utils.rs | 6 +- lightning/src/ln/functional_tests.rs | 8 +- .../src/onion_message/functional_tests.rs | 186 ++++++++++++++---- lightning/src/onion_message/messenger.rs | 47 +++-- lightning/src/onion_message/packet.rs | 3 +- lightning/src/util/test_utils.rs | 7 +- 10 files changed, 269 insertions(+), 79 deletions(-) diff --git a/lightning-background-processor/src/lib.rs b/lightning-background-processor/src/lib.rs index 239ff3f0c98..3a03f96679d 100644 --- a/lightning-background-processor/src/lib.rs +++ b/lightning-background-processor/src/lib.rs @@ -1174,7 +1174,7 @@ mod tests { use lightning::routing::gossip::{NetworkGraph, P2PGossipSync}; use lightning::routing::router::{CandidateRouteHop, DefaultRouter, Path, RouteHop}; use lightning::routing::scoring::{ChannelUsage, LockableScore, ScoreLookUp, ScoreUpdate}; - use lightning::sign::{ChangeDestinationSourceSync, InMemorySigner, KeysManager}; + use lightning::sign::{ChangeDestinationSourceSync, InMemorySigner, KeysManager, NodeSigner}; use lightning::types::features::{ChannelFeatures, NodeFeatures}; use lightning::types::payment::PaymentHash; use lightning::util::config::UserConfig; @@ -1650,6 +1650,7 @@ mod tests { let msg_router = Arc::new(DefaultMessageRouter::new( network_graph.clone(), Arc::clone(&keys_manager), + keys_manager.get_expanded_key(), )); let chain_source = Arc::new(test_utils::TestChainSource::new(Network::Bitcoin)); let kv_store = diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs index 6de6b77be90..6288053006e 100644 --- a/lightning-dns-resolver/src/lib.rs +++ b/lightning-dns-resolver/src/lib.rs @@ -232,7 +232,14 @@ mod test { secp_ctx: &Secp256k1, ) -> Result, ()> { let keys = KeysManager::new(&[0; 32], 42, 43); - Ok(vec![BlindedMessagePath::one_hop(recipient, context, &keys, secp_ctx).unwrap()]) + Ok(vec![BlindedMessagePath::one_hop( + recipient, + context, + keys.get_expanded_key(), + &keys, + secp_ctx, + ) + .unwrap()]) } } impl Deref for DirectlyConnectedRouter { @@ -334,8 +341,14 @@ mod test { let (msg, context) = payer.resolver.resolve_name(payment_id, name.clone(), &*payer_keys).unwrap(); let query_context = MessageContext::DNSResolver(context); - let reply_path = - BlindedMessagePath::one_hop(payer_id, query_context, &*payer_keys, &secp_ctx).unwrap(); + let reply_path = BlindedMessagePath::one_hop( + payer_id, + query_context, + payer_keys.get_expanded_key(), + &*payer_keys, + &secp_ctx, + ) + .unwrap(); payer.pending_messages.lock().unwrap().push(( DNSResolverMessage::DNSSECQuery(msg), MessageSendInstructions::WithSpecifiedReplyPath { diff --git a/lightning-liquidity/tests/common/mod.rs b/lightning-liquidity/tests/common/mod.rs index 2259d1eae06..95b3ca4580b 100644 --- a/lightning-liquidity/tests/common/mod.rs +++ b/lightning-liquidity/tests/common/mod.rs @@ -5,7 +5,7 @@ #![allow(unused_macros)] use lightning::chain::Filter; -use lightning::sign::EntropySource; +use lightning::sign::{EntropySource, NodeSigner}; use bitcoin::blockdata::constants::{genesis_block, ChainHash}; use bitcoin::blockdata::transaction::Transaction; @@ -418,8 +418,11 @@ pub(crate) fn create_liquidity_node( scorer.clone(), Default::default(), )); - let msg_router = - Arc::new(DefaultMessageRouter::new(Arc::clone(&network_graph), Arc::clone(&keys_manager))); + let msg_router = Arc::new(DefaultMessageRouter::new( + Arc::clone(&network_graph), + Arc::clone(&keys_manager), + keys_manager.get_expanded_key(), + )); let chain_source = Arc::new(test_utils::TestChainSource::new(Network::Bitcoin)); let kv_store = Arc::new(FilesystemStore::new(format!("{}_persister_{}", &persist_dir, i).into())); diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 650aab2d1ef..07df61ee448 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -56,23 +56,51 @@ impl Readable for BlindedMessagePath { impl BlindedMessagePath { /// Create a one-hop blinded path for a message. pub fn one_hop( - recipient_node_id: PublicKey, context: MessageContext, entropy_source: ES, - secp_ctx: &Secp256k1, + recipient_node_id: PublicKey, context: MessageContext, + expanded_key: inbound_payment::ExpandedKey, entropy_source: ES, secp_ctx: &Secp256k1, ) -> Result where ES::Target: EntropySource, { - Self::new(&[], recipient_node_id, context, entropy_source, secp_ctx) + Self::new(&[], recipient_node_id, context, entropy_source, expanded_key, secp_ctx) } /// Create a path for an onion message, to be forwarded along `node_pks`. The last node /// pubkey in `node_pks` will be the destination node. /// /// Errors if no hops are provided or if `node_pk`(s) are invalid. - // TODO: make all payloads the same size with padding + add dummy hops pub fn new( intermediate_nodes: &[MessageForwardNode], recipient_node_id: PublicKey, - context: MessageContext, entropy_source: ES, secp_ctx: &Secp256k1, + context: MessageContext, entropy_source: ES, expanded_key: inbound_payment::ExpandedKey, + secp_ctx: &Secp256k1, + ) -> Result + where + ES::Target: EntropySource, + { + BlindedMessagePath::new_with_dummy_hops( + intermediate_nodes, + 0, + recipient_node_id, + context, + entropy_source, + expanded_key, + secp_ctx, + ) + } + + /// Create a path for an onion message, to be forwarded along `node_pks`. + /// + /// Additionally allows appending a number of dummy hops before the final hop, + /// increasing the total path length and enhancing privacy by obscuring the true + /// distance between sender and recipient. + /// + /// The last node pubkey in `node_pks` will be the destination node. + /// + /// Errors if no hops are provided or if `node_pk`(s) are invalid. + pub fn new_with_dummy_hops( + intermediate_nodes: &[MessageForwardNode], dummy_hops_count: u8, + recipient_node_id: PublicKey, context: MessageContext, entropy_source: ES, + expanded_key: inbound_payment::ExpandedKey, secp_ctx: &Secp256k1, ) -> Result where ES::Target: EntropySource, @@ -89,9 +117,12 @@ impl BlindedMessagePath { blinding_point: PublicKey::from_secret_key(secp_ctx, &blinding_secret), blinded_hops: blinded_hops( secp_ctx, + entropy_source, + expanded_key, intermediate_nodes, recipient_node_id, context, + dummy_hops_count, &blinding_secret, ) .map_err(|_| ())?, @@ -563,13 +594,18 @@ impl_writeable_tlv_based!(DNSResolverContext, { 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, -) -> Result, secp256k1::Error> { +pub(super) fn blinded_hops( + secp_ctx: &Secp256k1, entropy_source: ES, expanded_key: inbound_payment::ExpandedKey, + intermediate_nodes: &[MessageForwardNode], recipient_node_id: PublicKey, + context: MessageContext, dummy_hops_count: u8, session_priv: &SecretKey, +) -> Result, secp256k1::Error> +where + ES::Target: EntropySource, +{ let pks = intermediate_nodes .iter() .map(|node| node.node_id) + .chain((0..dummy_hops_count).map(|_| recipient_node_id)) .chain(core::iter::once(recipient_node_id)); let is_compact = intermediate_nodes.iter().any(|node| node.short_channel_id.is_some()); @@ -584,6 +620,12 @@ pub(super) fn blinded_hops( .map(|next_hop| { ControlTlvs::Forward(ForwardTlvs { next_hop, next_blinding_override: None }) }) + .chain((0..dummy_hops_count).map(|_| { + let dummy_tlv = UnauthenticatedDummyTlv {}; + let nonce = Nonce::from_entropy_source(&*entropy_source); + let hmac = dummy_tlv.hmac_data(nonce, &expanded_key); + ControlTlvs::Dummy(DummyTlv { dummy_tlv, authentication: (hmac, nonce) }) + })) .chain(core::iter::once(ControlTlvs::Receive(ReceiveTlvs { context: Some(context) }))); if is_compact { diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 8a9a5cb1762..875143f8bb4 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -29,7 +29,7 @@ use crate::onion_message::messenger::OnionMessenger; use crate::ln::onion_utils::LocalHTLCFailureReason; use crate::routing::gossip::{P2PGossipSync, NetworkGraph, NetworkUpdate}; use crate::routing::router::{self, PaymentParameters, Route, RouteParameters}; -use crate::sign::{EntropySource, RandomBytes}; +use crate::sign::{EntropySource, NodeSigner, RandomBytes}; use crate::util::config::{MaxDustHTLCExposure, UserConfig}; use crate::util::logger::Logger; use crate::util::scid_utils; @@ -725,7 +725,7 @@ impl<'a, 'b, 'c> Drop for Node<'a, 'b, 'c> { signer_provider: self.keys_manager, fee_estimator: &test_utils::TestFeeEstimator::new(253), router: &test_utils::TestRouter::new(Arc::clone(&network_graph), &self.logger, &scorer), - message_router: &test_utils::TestMessageRouter::new(network_graph, self.keys_manager), + message_router: &test_utils::TestMessageRouter::new(network_graph, self.keys_manager, self.keys_manager.get_expanded_key()), chain_monitor: self.chain_monitor, tx_broadcaster: &broadcaster, logger: &self.logger, @@ -3353,7 +3353,7 @@ pub fn create_node_cfgs_with_persisters<'a>(node_count: usize, chanmon_cfgs: &'a tx_broadcaster: &chanmon_cfgs[i].tx_broadcaster, fee_estimator: &chanmon_cfgs[i].fee_estimator, router: test_utils::TestRouter::new(network_graph.clone(), &chanmon_cfgs[i].logger, &chanmon_cfgs[i].scorer), - message_router: test_utils::TestMessageRouter::new(network_graph.clone(), &chanmon_cfgs[i].keys_manager), + message_router: test_utils::TestMessageRouter::new(network_graph.clone(), &chanmon_cfgs[i].keys_manager, chanmon_cfgs[i].keys_manager.get_expanded_key()), chain_monitor, keys_manager: &chanmon_cfgs[i].keys_manager, node_seed: seed, diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 0c0bb0713cb..2130819a5a9 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -50,7 +50,7 @@ use crate::routing::gossip::{NetworkGraph, NetworkUpdate}; use crate::routing::router::{ get_route, Path, PaymentParameters, Route, RouteHop, RouteParameters, }; -use crate::sign::{EntropySource, OutputSpender, SignerProvider}; +use crate::sign::{EntropySource, NodeSigner, OutputSpender, SignerProvider}; use crate::types::features::{ChannelFeatures, ChannelTypeFeatures, NodeFeatures}; use crate::types::payment::{PaymentHash, PaymentSecret}; use crate::util::config::{ @@ -5112,7 +5112,11 @@ pub fn test_key_derivation_params() { let scorer = RwLock::new(test_utils::TestScorer::new()); let router = test_utils::TestRouter::new(network_graph.clone(), &chanmon_cfgs[0].logger, &scorer); - let message_router = test_utils::TestMessageRouter::new(network_graph.clone(), &keys_manager); + let message_router = test_utils::TestMessageRouter::new( + network_graph.clone(), + &keys_manager, + keys_manager.get_expanded_key(), + ); let node = NodeCfg { chain_source: &chanmon_cfgs[0].chain_source, logger: &chanmon_cfgs[0].logger, diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index b28819ee692..e07c280430a 100644 --- a/lightning/src/onion_message/functional_tests.rs +++ b/lightning/src/onion_message/functional_tests.rs @@ -26,6 +26,7 @@ use crate::blinded_path::message::{ use crate::blinded_path::utils::is_padded; use crate::blinded_path::EmptyNodeIdLookUp; use crate::events::{Event, EventsProvider}; +use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{self, BaseMessageHandler, DecodeError, OnionMessageHandler}; use crate::routing::gossip::{NetworkGraph, P2PGossipSync}; use crate::routing::test_utils::{add_channel, add_or_update_node}; @@ -279,8 +280,11 @@ fn create_nodes_using_cfgs(cfgs: Vec) -> Vec { let node_signer = Arc::new(TestNodeSigner::new(secret_key)); let node_id_lookup = Arc::new(EmptyNodeIdLookUp {}); - let message_router = - Arc::new(DefaultMessageRouter::new(network_graph.clone(), entropy_source.clone())); + let message_router = Arc::new(DefaultMessageRouter::new( + network_graph.clone(), + entropy_source.clone(), + node_signer.get_expanded_key(), + )); let offers_message_handler = Arc::new(TestOffersMessageHandler {}); let async_payments_message_handler = Arc::new(TestAsyncPaymentsMessageHandler {}); let dns_resolver_message_handler = Arc::new(TestDNSResolverMessageHandler {}); @@ -409,8 +413,10 @@ fn one_blinded_hop() { let secp_ctx = Secp256k1::new(); let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[1].entropy_source; + let expanded_key = ExpandedKey::new([42; 32]); let blinded_path = - BlindedMessagePath::new(&[], nodes[1].node_id, context, entropy, &secp_ctx).unwrap(); + BlindedMessagePath::new(&[], nodes[1].node_id, context, entropy, expanded_key, &secp_ctx) + .unwrap(); let destination = Destination::BlindedPath(blinded_path); let instructions = MessageSendInstructions::WithoutReplyPath { destination }; nodes[0].messenger.send_onion_message(test_msg, instructions).unwrap(); @@ -428,9 +434,16 @@ fn two_unblinded_two_blinded() { [MessageForwardNode { node_id: nodes[3].node_id, short_channel_id: None }]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[4].entropy_source; - let blinded_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[4].node_id, context, entropy, &secp_ctx) - .unwrap(); + let expanded_key = ExpandedKey::new([42; 32]); + let blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[4].node_id, + context, + entropy, + expanded_key, + &secp_ctx, + ) + .unwrap(); let path = OnionMessagePath { intermediate_nodes: vec![nodes[1].node_id, nodes[2].node_id], destination: Destination::BlindedPath(blinded_path), @@ -454,9 +467,16 @@ fn three_blinded_hops() { ]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[3].entropy_source; - let blinded_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[3].node_id, context, entropy, &secp_ctx) - .unwrap(); + let expanded_key = ExpandedKey::new([42; 32]); + let blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[3].node_id, + context, + entropy, + expanded_key, + &secp_ctx, + ) + .unwrap(); let destination = Destination::BlindedPath(blinded_path); let instructions = MessageSendInstructions::WithoutReplyPath { destination }; @@ -481,8 +501,10 @@ fn async_response_over_one_blinded_hop() { let secp_ctx = Secp256k1::new(); let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[1].entropy_source; + let expanded_key = ExpandedKey::new([42; 32]); let reply_path = - BlindedMessagePath::new(&[], nodes[1].node_id, context, entropy, &secp_ctx).unwrap(); + BlindedMessagePath::new(&[], nodes[1].node_id, context, entropy, expanded_key, &secp_ctx) + .unwrap(); // 4. Create a responder using the reply path for Alice. let responder = Some(Responder::new(reply_path)); @@ -520,9 +542,16 @@ fn async_response_with_reply_path_succeeds() { // Alice receives a message from Bob with an added reply_path for responding back. let message = TestCustomMessage::Ping; let context = MessageContext::Custom(Vec::new()); - let reply_path = - BlindedMessagePath::new(&[], bob.node_id, context, &*bob.entropy_source, &secp_ctx) - .unwrap(); + let expanded_key = ExpandedKey::new([42; 32]); + let reply_path = BlindedMessagePath::new( + &[], + bob.node_id, + context, + &*bob.entropy_source, + expanded_key, + &secp_ctx, + ) + .unwrap(); // Alice asynchronously responds to Bob, expecting a response back from him. let responder = Responder::new(reply_path); @@ -561,9 +590,16 @@ fn async_response_with_reply_path_fails() { // Alice receives a message from Bob with an added reply_path for responding back. let message = TestCustomMessage::Ping; let context = MessageContext::Custom(Vec::new()); - let reply_path = - BlindedMessagePath::new(&[], bob.node_id, context, &*bob.entropy_source, &secp_ctx) - .unwrap(); + let expanded_key = ExpandedKey::new([42; 32]); + let reply_path = BlindedMessagePath::new( + &[], + bob.node_id, + context, + &*bob.entropy_source, + expanded_key, + &secp_ctx, + ) + .unwrap(); // Alice tries to asynchronously respond to Bob, but fails because the nodes are unannounced and // disconnected. Thus, a reply path could no be created for the response. @@ -604,6 +640,7 @@ fn test_blinded_path_padding_for_full_length_path() { let test_msg = TestCustomMessage::Pong; let secp_ctx = Secp256k1::new(); + let expanded_key = ExpandedKey::new([42; 32]); let intermediate_nodes = [ MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }, MessageForwardNode { node_id: nodes[2].node_id, short_channel_id: None }, @@ -616,6 +653,7 @@ fn test_blinded_path_padding_for_full_length_path() { nodes[3].node_id, context, &*nodes[3].entropy_source, + expanded_key, &secp_ctx, ) .unwrap(); @@ -635,6 +673,7 @@ fn test_blinded_path_no_padding_for_compact_path() { // Check that for a compact blinded path, no padding is applied. let nodes = create_nodes(4); let secp_ctx = Secp256k1::new(); + let expanded_key = ExpandedKey::new([42; 32]); // Include some short_channel_id, so that MessageRouter uses this to create compact blinded paths. let intermediate_nodes = [ @@ -649,6 +688,7 @@ fn test_blinded_path_no_padding_for_compact_path() { nodes[3].node_id, context, &*nodes[3].entropy_source, + expanded_key, &secp_ctx, ) .unwrap(); @@ -664,15 +704,22 @@ fn we_are_intro_node() { let test_msg = TestCustomMessage::Pong; let secp_ctx = Secp256k1::new(); + let expanded_key = ExpandedKey::new([42; 32]); let intermediate_nodes = [ MessageForwardNode { node_id: nodes[0].node_id, short_channel_id: None }, MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }, ]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[2].entropy_source; - let blinded_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[2].node_id, context, entropy, &secp_ctx) - .unwrap(); + let blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[2].node_id, + context, + entropy, + expanded_key, + &secp_ctx, + ) + .unwrap(); let destination = Destination::BlindedPath(blinded_path); let instructions = MessageSendInstructions::WithoutReplyPath { destination }; @@ -685,9 +732,15 @@ fn we_are_intro_node() { [MessageForwardNode { node_id: nodes[0].node_id, short_channel_id: None }]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[1].entropy_source; - let blinded_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[1].node_id, context, entropy, &secp_ctx) - .unwrap(); + let blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[1].node_id, + context, + entropy, + expanded_key, + &secp_ctx, + ) + .unwrap(); let destination = Destination::BlindedPath(blinded_path); let instructions = MessageSendInstructions::WithoutReplyPath { destination }; @@ -704,13 +757,20 @@ fn invalid_blinded_path_error() { let test_msg = TestCustomMessage::Pong; let secp_ctx = Secp256k1::new(); + let expanded_key = ExpandedKey::new([42; 32]); let intermediate_nodes = [MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[2].entropy_source; - let mut blinded_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[2].node_id, context, entropy, &secp_ctx) - .unwrap(); + let mut blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[2].node_id, + context, + entropy, + expanded_key, + &secp_ctx, + ) + .unwrap(); blinded_path.clear_blinded_hops(); let destination = Destination::BlindedPath(blinded_path); let instructions = MessageSendInstructions::WithoutReplyPath { destination }; @@ -724,6 +784,7 @@ fn reply_path() { let mut nodes = create_nodes(4); let test_msg = TestCustomMessage::Ping; let secp_ctx = Secp256k1::new(); + let expanded_key = ExpandedKey::new([42; 32]); // Destination::Node let path = OnionMessagePath { @@ -737,9 +798,15 @@ fn reply_path() { ]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[0].entropy_source; - let reply_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[0].node_id, context, entropy, &secp_ctx) - .unwrap(); + let reply_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[0].node_id, + context, + entropy, + expanded_key, + &secp_ctx, + ) + .unwrap(); nodes[0] .messenger .send_onion_message_using_path(path, test_msg.clone(), Some(reply_path)) @@ -758,9 +825,15 @@ fn reply_path() { ]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[3].entropy_source; - let blinded_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[3].node_id, context, entropy, &secp_ctx) - .unwrap(); + let blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[3].node_id, + context, + entropy, + expanded_key, + &secp_ctx, + ) + .unwrap(); let destination = Destination::BlindedPath(blinded_path); let intermediate_nodes = [ MessageForwardNode { node_id: nodes[2].node_id, short_channel_id: None }, @@ -768,9 +841,15 @@ fn reply_path() { ]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[0].entropy_source; - let reply_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[0].node_id, context, entropy, &secp_ctx) - .unwrap(); + let reply_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[0].node_id, + context, + entropy, + expanded_key, + &secp_ctx, + ) + .unwrap(); let instructions = MessageSendInstructions::WithSpecifiedReplyPath { destination, reply_path }; nodes[0].messenger.send_onion_message(test_msg, instructions).unwrap(); @@ -866,9 +945,16 @@ fn requests_peer_connection_for_buffered_messages() { [MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[0].entropy_source; - let blinded_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[2].node_id, context, entropy, &secp_ctx) - .unwrap(); + let expanded_key = ExpandedKey::new([42; 32]); + let blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[2].node_id, + context, + entropy, + expanded_key, + &secp_ctx, + ) + .unwrap(); let destination = Destination::BlindedPath(blinded_path); let instructions = MessageSendInstructions::WithoutReplyPath { destination }; @@ -908,9 +994,16 @@ fn drops_buffered_messages_waiting_for_peer_connection() { [MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[0].entropy_source; - let blinded_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[2].node_id, context, entropy, &secp_ctx) - .unwrap(); + let expanded_key = ExpandedKey::new([42; 32]); + let blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[2].node_id, + context, + entropy, + expanded_key, + &secp_ctx, + ) + .unwrap(); let destination = Destination::BlindedPath(blinded_path); let instructions = MessageSendInstructions::WithoutReplyPath { destination }; @@ -966,9 +1059,16 @@ fn intercept_offline_peer_oms() { [MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[2].entropy_source; - let blinded_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[2].node_id, context, entropy, &secp_ctx) - .unwrap(); + let expanded_key = ExpandedKey::new([42; 32]); + let blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[2].node_id, + context, + entropy, + expanded_key, + &secp_ctx, + ) + .unwrap(); let destination = Destination::BlindedPath(blinded_path); let instructions = MessageSendInstructions::WithoutReplyPath { destination }; diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 6009a276976..f17cdd1fc1d 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -38,7 +38,7 @@ use crate::events::{Event, EventHandler, EventsProvider, ReplayEvent}; use crate::ln::msgs::{ self, BaseMessageHandler, MessageSendEvent, OnionMessage, OnionMessageHandler, SocketAddress, }; -use crate::ln::onion_utils; +use crate::ln::{inbound_payment, onion_utils}; use crate::routing::gossip::{NetworkGraph, NodeId, ReadOnlyNetworkGraph}; use crate::sign::{EntropySource, NodeSigner, Recipient}; use crate::types::features::{InitFeatures, NodeFeatures}; @@ -191,7 +191,7 @@ where /// # use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey, self}; /// # use lightning::blinded_path::EmptyNodeIdLookUp; /// # use lightning::blinded_path::message::{BlindedMessagePath, MessageForwardNode, MessageContext}; -/// # use lightning::sign::{EntropySource, KeysManager}; +/// # use lightning::sign::{EntropySource, KeysManager, NodeSigner}; /// # use lightning::ln::peer_handler::IgnoringMessageHandler; /// # use lightning::onion_message::messenger::{Destination, MessageRouter, MessageSendInstructions, OnionMessagePath, OnionMessenger}; /// # use lightning::onion_message::packet::OnionMessageContents; @@ -272,8 +272,10 @@ where /// MessageForwardNode { node_id: hop_node_id3, short_channel_id: None }, /// MessageForwardNode { node_id: hop_node_id4, short_channel_id: None }, /// ]; +/// /// let context = MessageContext::Custom(Vec::new()); -/// let blinded_path = BlindedMessagePath::new(&hops, your_node_id, context, &keys_manager, &secp_ctx).unwrap(); +/// let expanded_key = keys_manager.get_expanded_key(); +/// let blinded_path = BlindedMessagePath::new(&hops, your_node_id, context, &keys_manager, expanded_key, &secp_ctx).unwrap(); /// /// // Send a custom onion message to a blinded path. /// let destination = Destination::BlindedPath(blinded_path); @@ -539,6 +541,7 @@ where { network_graph: G, entropy_source: ES, + inbound_payment_key: inbound_payment::ExpandedKey, } impl>, L: Deref, ES: Deref> DefaultMessageRouter @@ -547,8 +550,10 @@ where ES::Target: EntropySource, { /// Creates a [`DefaultMessageRouter`] using the given [`NetworkGraph`]. - pub fn new(network_graph: G, entropy_source: ES) -> Self { - Self { network_graph, entropy_source } + pub fn new( + network_graph: G, entropy_source: ES, expanded_key: inbound_payment::ExpandedKey, + ) -> Self { + Self { network_graph, entropy_source, inbound_payment_key: expanded_key } } fn create_blinded_paths_from_iter< @@ -556,7 +561,8 @@ where T: secp256k1::Signing + secp256k1::Verification, >( network_graph: &G, recipient: PublicKey, context: MessageContext, peers: I, - entropy_source: &ES, secp_ctx: &Secp256k1, compact_paths: bool, + entropy_source: &ES, expanded_key: &inbound_payment::ExpandedKey, secp_ctx: &Secp256k1, + compact_paths: bool, ) -> Result, ()> { // Limit the number of blinded paths that are computed. const MAX_PATHS: usize = 3; @@ -595,7 +601,14 @@ where let paths = peer_info .into_iter() .map(|(peer, _, _)| { - BlindedMessagePath::new(&[peer], recipient, context.clone(), entropy, secp_ctx) + BlindedMessagePath::new( + &[peer], + recipient, + context.clone(), + entropy, + *expanded_key, + secp_ctx, + ) }) .take(MAX_PATHS) .collect::, _>>(); @@ -604,8 +617,15 @@ where Ok(paths) if !paths.is_empty() => Ok(paths), _ => { if is_recipient_announced { - BlindedMessagePath::new(&[], recipient, context, &**entropy_source, secp_ctx) - .map(|path| vec![path]) + BlindedMessagePath::new( + &[], + recipient, + context, + &**entropy_source, + *expanded_key, + secp_ctx, + ) + .map(|path| vec![path]) } else { Err(()) } @@ -664,7 +684,7 @@ where pub(crate) fn create_blinded_paths( network_graph: &G, recipient: PublicKey, context: MessageContext, peers: Vec, - entropy_source: &ES, secp_ctx: &Secp256k1, + entropy_source: &ES, expanded_key: &inbound_payment::ExpandedKey, secp_ctx: &Secp256k1, ) -> Result, ()> { let peers = peers.into_iter().map(|node_id| MessageForwardNode { node_id, short_channel_id: None }); @@ -674,6 +694,7 @@ where context, peers.into_iter(), entropy_source, + expanded_key, secp_ctx, false, ) @@ -681,7 +702,8 @@ where pub(crate) fn create_compact_blinded_paths( network_graph: &G, recipient: PublicKey, context: MessageContext, - peers: Vec, entropy_source: &ES, secp_ctx: &Secp256k1, + peers: Vec, entropy_source: &ES, + expanded_key: &inbound_payment::ExpandedKey, secp_ctx: &Secp256k1, ) -> Result, ()> { Self::create_blinded_paths_from_iter( network_graph, @@ -689,6 +711,7 @@ where context, peers.into_iter(), entropy_source, + expanded_key, secp_ctx, true, ) @@ -717,6 +740,7 @@ where context, peers, &self.entropy_source, + &self.inbound_payment_key, secp_ctx, ) } @@ -731,6 +755,7 @@ where context, peers, &self.entropy_source, + &self.inbound_payment_key, secp_ctx, ) } diff --git a/lightning/src/onion_message/packet.rs b/lightning/src/onion_message/packet.rs index 302e3b4ec85..db480203b2d 100644 --- a/lightning/src/onion_message/packet.rs +++ b/lightning/src/onion_message/packet.rs @@ -372,7 +372,8 @@ impl Readable for ControlTlvs { }; let valid_fwd_fmt = next_hop.is_some(); - let valid_recv_fmt = next_hop.is_none() && next_blinding_override.is_none(); + let valid_recv_fmt = + next_hop.is_none() && next_blinding_override.is_none() && authentication.is_none(); let payload_fmt = if valid_fwd_fmt { ControlTlvs::Forward(ForwardTlvs { diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index 7945dc587c8..c658a4ab87a 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -25,11 +25,11 @@ use crate::events::bump_transaction::{Utxo, WalletSource}; #[cfg(any(test, feature = "_externalize_tests"))] use crate::ln::chan_utils::CommitmentTransaction; use crate::ln::channel_state::ChannelDetails; -use crate::ln::channelmanager; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{BaseMessageHandler, MessageSendEvent}; use crate::ln::script::ShutdownScript; use crate::ln::types::ChannelId; +use crate::ln::{channelmanager, inbound_payment}; use crate::ln::{msgs, wire}; use crate::offers::invoice::UnsignedBolt12Invoice; use crate::onion_message::messenger::{ @@ -324,8 +324,9 @@ pub struct TestMessageRouter<'a> { impl<'a> TestMessageRouter<'a> { pub fn new( network_graph: Arc>, entropy_source: &'a TestKeysInterface, + expanded_key: inbound_payment::ExpandedKey, ) -> Self { - Self { inner: DefaultMessageRouter::new(network_graph, entropy_source) } + Self { inner: DefaultMessageRouter::new(network_graph, entropy_source, expanded_key) } } } @@ -1487,7 +1488,7 @@ impl TestNodeSigner { impl NodeSigner for TestNodeSigner { fn get_expanded_key(&self) -> ExpandedKey { - unreachable!() + ExpandedKey::new([42; 32]) } fn get_node_id(&self, recipient: Recipient) -> Result { From 9e654bf93cd21f086ce4aee2e4d5c100851de655 Mon Sep 17 00:00:00 2001 From: shaavan Date: Mon, 12 May 2025 21:56:31 +0530 Subject: [PATCH 5/7] Introduce parsing logic for DummyTlvs --- lightning/src/onion_message/messenger.rs | 107 +++++++++++++---------- 1 file changed, 63 insertions(+), 44 deletions(-) diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index f17cdd1fc1d..07dcff5d592 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -20,8 +20,8 @@ use super::async_payments::AsyncPaymentsMessage; use super::async_payments::AsyncPaymentsMessageHandler; use super::dns_resolution::{DNSResolverMessage, DNSResolverMessageHandler}; use super::offers::{OffersMessage, OffersMessageHandler}; -use super::packet::OnionMessageContents; use super::packet::ParsedOnionMessageContents; +use super::packet::{DummyControlTlvs, OnionMessageContents}; use super::packet::{ ForwardControlTlvs, Packet, Payload, ReceiveControlTlvs, BIG_PACKET_HOP_DATA_LEN, SMALL_PACKET_HOP_DATA_LEN, @@ -29,12 +29,13 @@ 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}; use crate::events::{Event, EventHandler, EventsProvider, ReplayEvent}; +use crate::ln::channelmanager::Verification; use crate::ln::msgs::{ self, BaseMessageHandler, MessageSendEvent, OnionMessage, OnionMessageHandler, SocketAddress, }; @@ -1099,6 +1100,44 @@ where msg.onion_routing_packet.hmac, (control_tlvs_ss, custom_handler.deref(), 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 { @@ -1140,6 +1179,21 @@ where Err(()) }, }, + Ok(( + Payload::Dummy(DummyControlTlvs::Unblinded(DummyTlv { dummy_tlv, authentication })), + Some((next_hop_hmac, new_packet_bytes)), + )) => { + let expanded_key = node_signer.get_expanded_key(); + dummy_tlv.verify_data(authentication.0, authentication.1, &expanded_key)?; + + 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, @@ -1147,47 +1201,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)) }, Err(e) => { From 9ae8e25fa61bd146ffae3da8279e9176f977639c Mon Sep 17 00:00:00 2001 From: shaavan Date: Wed, 9 Apr 2025 18:00:27 +0530 Subject: [PATCH 6/7] 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 | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 07dcff5d592..f39e776d051 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -572,6 +572,19 @@ 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 = compact_paths.then_some(0).unwrap_or_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)); @@ -602,8 +615,9 @@ where let paths = peer_info .into_iter() .map(|(peer, _, _)| { - BlindedMessagePath::new( + BlindedMessagePath::new_with_dummy_hops( &[peer], + dummy_hops_count, recipient, context.clone(), entropy, From d2aeac9f759777893b5b4bbea0372a509e637a99 Mon Sep 17 00:00:00 2001 From: shaavan Date: Wed, 9 Apr 2025 17:36:51 +0530 Subject: [PATCH 7/7] 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. Co-authored-by: valentinewallace --- .../src/onion_message/functional_tests.rs | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index e07c280430a..749a9bdb063 100644 --- a/lightning/src/onion_message/functional_tests.rs +++ b/lightning/src/onion_message/functional_tests.rs @@ -424,6 +424,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 expanded_key = ExpandedKey::new([42; 32]); + let blinded_path = BlindedMessagePath::new_with_dummy_hops( + &[], + 5, + nodes[1].node_id, + context, + entropy, + expanded_key, + &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); @@ -648,8 +676,9 @@ fn test_blinded_path_padding_for_full_length_path() { // Update the context to create a larger final receive TLVs, ensuring that // the hop sizes vary before padding. let context = MessageContext::Custom(vec![0u8; 42]); - let blinded_path = BlindedMessagePath::new( + let blinded_path = BlindedMessagePath::new_with_dummy_hops( &intermediate_nodes, + 5, nodes[3].node_id, context, &*nodes[3].entropy_source, @@ -683,8 +712,9 @@ fn test_blinded_path_no_padding_for_compact_path() { // Update the context to create a larger final receive TLVs, ensuring that // the hop sizes vary before padding. let context = MessageContext::Custom(vec![0u8; 42]); - let blinded_path = BlindedMessagePath::new( + let blinded_path = BlindedMessagePath::new_with_dummy_hops( &intermediate_nodes, + 5, nodes[3].node_id, context, &*nodes[3].entropy_source,