From d8a5e6d202e58657791d2fb039d212089a5e1751 Mon Sep 17 00:00:00 2001 From: shaavan Date: Wed, 23 Apr 2025 20:24:45 +0530 Subject: [PATCH 1/6] 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 ea186c4e329..939e3928dba 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) @@ -10535,7 +10535,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) } ); @@ -10639,7 +10639,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 }); @@ -12419,7 +12419,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, @@ -12602,7 +12602,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))) }, @@ -12639,7 +12639,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 @@ -12652,7 +12652,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, } @@ -12665,7 +12665,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 c27db4a55b4..07023eb7b7a 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -3363,7 +3363,7 @@ impl ReadableArgs<(Option, NS)> for InboundOnionPayload wh ChaChaPolyReadAdapter { readable: BlindedPaymentTlvs::Receive(receive_tlvs) } => { let ReceiveTlvs { tlvs, authentication: (hmac, nonce) } = receive_tlvs; let expanded_key = node_signer.get_inbound_payment_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); } @@ -3489,7 +3489,7 @@ impl ReadableArgs<(Option, NS)> for InboundTrampolinePaylo ChaChaPolyReadAdapter { readable: BlindedTrampolineTlvs::Receive(receive_tlvs) } => { let ReceiveTlvs { tlvs, authentication: (hmac, nonce) } = receive_tlvs; let expanded_key = node_signer.get_inbound_payment_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 92d2c757a0ae339c92d78d2c18f500c4a8dc5939 Mon Sep 17 00:00:00 2001 From: shaavan Date: Tue, 22 Apr 2025 19:39:26 +0530 Subject: [PATCH 2/6] 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 | 44 +++++++++++++++++++++++++-- lightning/src/offers/signer.rs | 24 +++++++++++++++ lightning/src/onion_message/packet.rs | 28 +++++++++++++++-- 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 0e3977ab68e..6980de29288 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,45 @@ pub(crate) struct ForwardTlvs { pub(crate) next_blinding_override: Option, } +pub(crate) struct UnauthenticatedDummyTlvs {} + +impl Writeable for UnauthenticatedDummyTlvs { + fn write(&self, _writer: &mut W) -> Result<(), io::Error> { + Ok(()) + } +} + +impl Verification for UnauthenticatedDummyTlvs { + /// 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_tlvs(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_tlvs(self, hmac, nonce, expanded_key) + } +} + +pub(crate) struct DummyTlvs { + pub(crate) dummy_tlvs: UnauthenticatedDummyTlvs, + /// An HMAC of `tlvs` along with a nonce used to construct it. + pub(crate) authentication: (Hmac, Nonce), +} + +impl Writeable for DummyTlvs { + 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..5124c7e9ce6 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::UnauthenticatedDummyTlvs; 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_tlvs( + tlvs: &UnauthenticatedDummyTlvs, 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_tlvs( + tlvs: &UnauthenticatedDummyTlvs, hmac: Hmac, nonce: Nonce, expanded_key: &ExpandedKey, +) -> Result<(), ()> { + if hmac_for_dummy_tlvs(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..0ec0d39787b 100644 --- a/lightning/src/onion_message/packet.rs +++ b/lightning/src/onion_message/packet.rs @@ -17,7 +17,10 @@ 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, DummyTlvs, ForwardTlvs, NextMessageHop, ReceiveTlvs, + UnauthenticatedDummyTlvs, +}; use crate::crypto::streams::{ChaChaPolyReadAdapter, ChaChaPolyWriteAdapter}; use crate::ln::msgs::DecodeError; use crate::ln::onion_utils; @@ -111,6 +114,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 +209,11 @@ pub(super) enum ForwardControlTlvs { Unblinded(ForwardTlvs), } +pub(super) enum DummyControlTlvs { + /// See [`ForwardControlTlvs::Unblinded`] + Unblinded(DummyTlvs), +} + /// Receive control TLVs in their blinded and unblinded form. pub(super) enum ReceiveControlTlvs { /// See [`ForwardControlTlvs::Blinded`]. @@ -234,6 +244,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 +324,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 +345,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(DummyTlvs), /// This onion message is intended to be received. Receive(ReceiveTlvs), } @@ -343,6 +362,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 +383,10 @@ impl Readable for ControlTlvs { } else if valid_recv_fmt { ControlTlvs::Receive(ReceiveTlvs { context }) } else { - return Err(DecodeError::InvalidValue); + ControlTlvs::Dummy(DummyTlvs { + dummy_tlvs: UnauthenticatedDummyTlvs {}, + authentication: authentication.ok_or(DecodeError::InvalidValue)?, + }) }; Ok(payload_fmt) @@ -374,6 +397,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 83b55d7887b95dd27b593983dd1c850284c1b47a Mon Sep 17 00:00:00 2001 From: shaavan Date: Wed, 9 Apr 2025 17:28:11 +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. 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 | 4 +- .../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, 265 insertions(+), 79 deletions(-) diff --git a/lightning-background-processor/src/lib.rs b/lightning-background-processor/src/lib.rs index 1f7147f3203..7c1e26522cd 100644 --- a/lightning-background-processor/src/lib.rs +++ b/lightning-background-processor/src/lib.rs @@ -1127,7 +1127,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::{ChangeDestinationSource, InMemorySigner, KeysManager}; + use lightning::sign::{ChangeDestinationSource, InMemorySigner, KeysManager, NodeSigner}; use lightning::types::features::{ChannelFeatures, NodeFeatures}; use lightning::types::payment::PaymentHash; use lightning::util::config::UserConfig; @@ -1603,6 +1603,7 @@ mod tests { let msg_router = Arc::new(DefaultMessageRouter::new( network_graph.clone(), Arc::clone(&keys_manager), + keys_manager.get_inbound_payment_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..c5ee0e75bfc 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_inbound_payment_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_inbound_payment_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..cccdd6954d3 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_inbound_payment_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 6980de29288..69ff4112e18 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(|_| ())?, @@ -545,13 +576,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()); @@ -566,6 +602,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_tlvs = UnauthenticatedDummyTlvs {}; + let nonce = Nonce::from_entropy_source(&*entropy_source); + let hmac = dummy_tlvs.hmac_data(nonce, &expanded_key); + ControlTlvs::Dummy(DummyTlvs { dummy_tlvs, 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 73af3a83729..7397c2eddc8 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -27,7 +27,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; @@ -723,7 +723,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_inbound_payment_key()), chain_monitor: self.chain_monitor, tx_broadcaster: &broadcaster, logger: &self.logger, @@ -3347,7 +3347,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_inbound_payment_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 3fdd579535b..10afca6b99b 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -18,7 +18,7 @@ use crate::chain::channelmonitor; use crate::chain::channelmonitor::{Balance, ChannelMonitorUpdateStep, CLTV_CLAIM_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS, ANTI_REORG_DELAY, COUNTERPARTY_CLAIMABLE_WITHIN_BLOCKS_PINNABLE}; use crate::chain::transaction::OutPoint; use crate::ln::onion_utils::LocalHTLCFailureReason; -use crate::sign::{ecdsa::EcdsaChannelSigner, EntropySource, OutputSpender, SignerProvider}; +use crate::sign::{ecdsa::EcdsaChannelSigner, EntropySource, NodeSigner, OutputSpender, SignerProvider}; use crate::events::bump_transaction::WalletSource; use crate::events::{Event, FundingInfo, PathFailure, PaymentPurpose, ClosureReason, HTLCHandlingFailureType, PaymentFailureReason}; use crate::ln::types::ChannelId; @@ -5992,7 +5992,7 @@ pub fn test_key_derivation_params() { let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &chanmon_cfgs[0].logger)); 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_inbound_payment_key()); let node = NodeCfg { chain_source: &chanmon_cfgs[0].chain_source, logger: &chanmon_cfgs[0].logger, tx_broadcaster: &chanmon_cfgs[0].tx_broadcaster, fee_estimator: &chanmon_cfgs[0].fee_estimator, router, message_router, chain_monitor, keys_manager: &keys_manager, network_graph, node_seed: seed, override_init_features: alloc::rc::Rc::new(core::cell::RefCell::new(None)) }; let mut node_cfgs = create_node_cfgs(3, &chanmon_cfgs); node_cfgs.remove(0); diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index b28819ee692..061528493a9 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_inbound_payment_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..0d6547928f0 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_inbound_payment_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 0ec0d39787b..246281efa7a 100644 --- a/lightning/src/onion_message/packet.rs +++ b/lightning/src/onion_message/packet.rs @@ -373,7 +373,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 f90bfb97ef7..31b9a6b3cd4 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_inbound_payment_key(&self) -> ExpandedKey { - unreachable!() + ExpandedKey::new([42; 32]) } fn get_node_id(&self, recipient: Recipient) -> Result { From 72e2fa44a746e381805eeac62096cec7e26ba8c1 Mon Sep 17 00:00:00 2001 From: shaavan Date: Wed, 23 Apr 2025 19:12:07 +0530 Subject: [PATCH 4/6] Introduce parsing logic for DummyTlvs --- lightning/src/onion_message/messenger.rs | 51 ++++++++++++++++++++---- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 0d6547928f0..03e8b0851d6 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, DummyTlvs, 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, }; @@ -1140,6 +1141,45 @@ where Err(()) }, }, + Ok(( + Payload::Dummy(DummyControlTlvs::Unblinded(DummyTlvs { dummy_tlvs, authentication })), + Some((next_hop_hmac, new_packet_bytes)), + )) => { + let expanded_key = node_signer.get_inbound_payment_key(); + dummy_tlvs.verify_data(authentication.0, authentication.1, &expanded_key)?; + + 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 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, + }; + peel_onion_message(&onion_message, secp_ctx, node_signer, logger, custom_handler) + }, Ok(( Payload::Forward(ForwardControlTlvs::Unblinded(ForwardTlvs { next_hop, @@ -1147,11 +1187,6 @@ 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); From 7a46f3d56c02fafa8f531e047f05f41192e38f28 Mon Sep 17 00:00:00 2001 From: shaavan Date: Wed, 9 Apr 2025 18:00:27 +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 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 03e8b0851d6..790047488a3 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -572,6 +572,13 @@ where // recipient's node_id. const MIN_PEER_CHANNELS: usize = 3; + // Add a random number (1 to 5) of dummy hops to each non-compact blinded path + // to make it harder to infer the recipient's position. + let dummy_hops_count = compact_paths.then_some(0).unwrap_or_else(|| { + let random_byte = entropy_source.get_secure_random_bytes()[0]; + (random_byte % 5) + 1 + }); + let network_graph = network_graph.deref().read_only(); let is_recipient_announced = network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)); @@ -602,8 +609,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 c7f158c192dbfaeb54351fce27b446fad0a1eac5 Mon Sep 17 00:00:00 2001 From: shaavan Date: Wed, 9 Apr 2025 17:36:51 +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. 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 061528493a9..99e724380b6 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(); + // Make sure that dummy hops are do 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,