From b0218a486cc14d29f0818081a0c4adba3abf7815 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 10 Sep 2025 11:55:33 -0400 Subject: [PATCH 1/3] Cache peers in Flow In upcoming commits, we'll move to creating multi-hop blinded paths during the process of creating a revoke_and_ack message within the Channel struct. These paths will be included in said RAA to be used as reply paths for often-offline senders held_htlc_available messages. Because we hold the per-peer lock corresponding to the Channel while creating this RAA, we can't use our typical approach of calling ChannelManager::get_peers_for_blinded_path to create these blinded paths. The ::get_peers method takes each peer's lock in turn in order to check for usable channels/onion message feature support, and it's not permitted to hold multiple peer state locks at the same time due to the potential for deadlocks (see the debug_sync module). To avoid taking other peer state locks while holding a particular Channel's peer state lock, here we cache the set of peers in the OffersMessageFlow, which is the struct that ultimately creates the blinded paths for the RAA. --- lightning/src/ln/channelmanager.rs | 14 ++++++++++++-- lightning/src/offers/flow.rs | 13 +++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 0bdca77b366..d8d99b66682 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -8077,6 +8077,8 @@ where should_persist }); + + self.flow.set_peers(self.get_peers_for_blinded_path()); } /// Indicates that the preimage for payment_hash is unknown or the received amount is incorrect @@ -10427,16 +10429,20 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ emit_initial_channel_ready_event!(pending_events, chan); } - Ok(()) } else { try_channel_entry!(self, peer_state, Err(ChannelError::close( "Got a channel_ready message for an unfunded channel!".into())), chan_entry) } }, hash_map::Entry::Vacant(_) => { - Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) + return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) } } + core::mem::drop(peer_state_lock); + core::mem::drop(per_peer_state); + + self.flow.set_peers(self.get_peers_for_blinded_path()); + Ok(()) } fn internal_shutdown( @@ -10530,6 +10536,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ self.fail_htlc_backwards_internal(&source, &hash, &reason, receiver, None); } + self.flow.set_peers(self.get_peers_for_blinded_path()); Ok(()) } @@ -13391,6 +13398,8 @@ where for (err, counterparty_node_id) in failed_channels.drain(..) { let _ = handle_error!(self, err, counterparty_node_id); } + + self.flow.set_peers(self.get_peers_for_blinded_path()); } #[rustfmt::skip] @@ -13503,6 +13512,7 @@ where // until we have some peer connection(s) to receive onion messages over, so as a minor optimization // refresh the cache when a peer connects. self.check_refresh_async_receive_offer_cache(false); + self.flow.set_peers(self.get_peers_for_blinded_path()); res } diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index a6484f0076e..3795a076566 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -100,6 +100,7 @@ where pending_async_payments_messages: Mutex>, async_receive_offer_cache: Mutex, + peers_cache: Mutex>, #[cfg(feature = "dnssec")] pub(crate) hrn_resolver: OMNameResolver, @@ -136,6 +137,7 @@ where pending_offers_messages: Mutex::new(Vec::new()), pending_async_payments_messages: Mutex::new(Vec::new()), + peers_cache: Mutex::new(Vec::new()), #[cfg(feature = "dnssec")] hrn_resolver: OMNameResolver::new(current_timestamp, best_block.height), @@ -1739,4 +1741,15 @@ where pub fn writeable_async_receive_offer_cache(&self) -> Vec { self.async_receive_offer_cache.encode() } + + /// Provides a set of connected peers of this node that can be used in [`BlindedMessagePath`]s + /// created by the [`OffersMessageFlow`]. + /// + /// All provided peers MUST advertise support for onion messages in their [`InitFeatures`]. + /// + /// [`InitFeatures`]: crate::types::features::InitFeatures + pub fn set_peers(&self, mut peers: Vec) { + let mut peers_cache = self.peers_cache.lock().unwrap(); + core::mem::swap(&mut *peers_cache, &mut peers); + } } From abe7ee360b097d96c202bdceea82d0dd09f13902 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 10 Sep 2025 12:12:53 -0400 Subject: [PATCH 2/3] Use cached peers in OffersMessageFlow In the previous commit, we started caching the set of peers in the OffersMessageFlow that are used when creating blinded paths. Here we start using that cache and stop passing in the peers for every method. --- lightning/src/ln/async_payments_tests.rs | 1 - lightning/src/ln/channelmanager.rs | 55 +++-------- lightning/src/offers/flow.rs | 115 ++++++++--------------- 3 files changed, 51 insertions(+), 120 deletions(-) diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index ccef4480efc..78fce723abf 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -302,7 +302,6 @@ fn create_static_invoice_builder<'a>( payment_secret, relative_expiry_secs, recipient.node.list_usable_channels(), - recipient.node.test_get_peers_for_blinded_path(), ) .unwrap() } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index d8d99b66682..93f77b89652 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5441,12 +5441,10 @@ where } fn check_refresh_async_receive_offer_cache(&self, timer_tick_occurred: bool) { - let peers = self.get_peers_for_blinded_path(); let channels = self.list_usable_channels(); let entropy = &*self.entropy_source; let router = &*self.router; let refresh_res = self.flow.check_refresh_async_receive_offer_cache( - peers, channels, entropy, router, @@ -5534,10 +5532,7 @@ where ); } } else { - let reply_path = HeldHtlcReplyPath::ToUs { - payment_id, - peers: self.get_peers_for_blinded_path(), - }; + let reply_path = HeldHtlcReplyPath::ToUs { payment_id }; let enqueue_held_htlc_available_res = self.flow.enqueue_held_htlc_available(invoice, reply_path); if enqueue_held_htlc_available_res.is_err() { @@ -12264,9 +12259,7 @@ macro_rules! create_offer_builder { ($self: ident, $builder: ty) => { /// [`Offer`]: crate::offers::offer::Offer /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest pub fn create_offer_builder(&$self) -> Result<$builder, Bolt12SemanticError> { - let builder = $self.flow.create_offer_builder( - &*$self.entropy_source, $self.get_peers_for_blinded_path() - )?; + let builder = $self.flow.create_offer_builder(&*$self.entropy_source)?; Ok(builder.into()) } @@ -12289,9 +12282,7 @@ macro_rules! create_offer_builder { ($self: ident, $builder: ty) => { where ME::Target: MessageRouter, { - let builder = $self.flow.create_offer_builder_using_router( - router, &*$self.entropy_source, $self.get_peers_for_blinded_path() - )?; + let builder = $self.flow.create_offer_builder_using_router(router, &*$self.entropy_source)?; Ok(builder.into()) } @@ -12345,8 +12336,7 @@ macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { let entropy = &*$self.entropy_source; let builder = $self.flow.create_refund_builder( - entropy, amount_msats, absolute_expiry, - payment_id, $self.get_peers_for_blinded_path() + entropy, amount_msats, absolute_expiry, payment_id )?; let _persistence_guard = PersistenceNotifierGuard::notify_on_drop($self); @@ -12389,8 +12379,7 @@ macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { let entropy = &*$self.entropy_source; let builder = $self.flow.create_refund_builder_using_router( - router, entropy, amount_msats, absolute_expiry, - payment_id, $self.get_peers_for_blinded_path() + router, entropy, amount_msats, absolute_expiry, payment_id )?; let _persistence_guard = PersistenceNotifierGuard::notify_on_drop($self); @@ -12462,8 +12451,7 @@ where pub fn set_paths_to_static_invoice_server( &self, paths_to_static_invoice_server: Vec, ) -> Result<(), ()> { - let peers = self.get_peers_for_blinded_path(); - self.flow.set_paths_to_static_invoice_server(paths_to_static_invoice_server, peers)?; + self.flow.set_paths_to_static_invoice_server(paths_to_static_invoice_server)?; let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); Ok(()) @@ -12643,10 +12631,7 @@ where let invoice_request = builder.build_and_sign()?; let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); - self.flow.enqueue_invoice_request( - invoice_request.clone(), payment_id, nonce, - self.get_peers_for_blinded_path() - )?; + self.flow.enqueue_invoice_request(invoice_request.clone(), payment_id, nonce,)?; let retryable_invoice_request = RetryableInvoiceRequest { invoice_request: invoice_request.clone(), @@ -12701,7 +12686,7 @@ where let invoice = builder.allow_mpp().build_and_sign(secp_ctx)?; - self.flow.enqueue_invoice(invoice.clone(), refund, self.get_peers_for_blinded_path())?; + self.flow.enqueue_invoice(invoice.clone(), refund)?; Ok(invoice) }, @@ -12765,14 +12750,7 @@ where optional_params.payer_note, )?; - self.flow - .enqueue_dns_onion_message( - onion_message, - context, - dns_resolvers, - self.get_peers_for_blinded_path(), - ) - .map_err(|_| ()) + self.flow.enqueue_dns_onion_message(onion_message, context, dns_resolvers).map_err(|_| ()) } /// Gets a payment secret and payment hash for use in an invoice given to a third party wishing @@ -12913,8 +12891,7 @@ where pub fn blinded_paths_for_async_recipient( &self, recipient_id: Vec, relative_expiry: Option, ) -> Result, ()> { - let peers = self.get_peers_for_blinded_path(); - self.flow.blinded_paths_for_async_recipient(recipient_id, relative_expiry, peers) + self.flow.blinded_paths_for_async_recipient(recipient_id, relative_expiry) } pub(super) fn duration_since_epoch(&self) -> Duration { @@ -12948,11 +12925,6 @@ where .collect::>() } - #[cfg(test)] - pub(super) fn test_get_peers_for_blinded_path(&self) -> Vec { - self.get_peers_for_blinded_path() - } - #[cfg(test)] /// Creates multi-hop blinded payment paths for the given `amount_msats` by delegating to /// [`Router::create_blinded_payment_paths`]. @@ -14735,9 +14707,8 @@ where { let RetryableInvoiceRequest { invoice_request, nonce, .. } = retryable_invoice_request; - let peers = self.get_peers_for_blinded_path(); let enqueue_invreq_res = - self.flow.enqueue_invoice_request(invoice_request, payment_id, nonce, peers); + self.flow.enqueue_invoice_request(invoice_request, payment_id, nonce); if enqueue_invreq_res.is_err() { log_warn!( self.logger, @@ -14945,9 +14916,8 @@ where &self, message: OfferPathsRequest, context: AsyncPaymentsContext, responder: Option, ) -> Option<(OfferPaths, ResponseInstruction)> { - let peers = self.get_peers_for_blinded_path(); let (message, reply_path_context) = - match self.flow.handle_offer_paths_request(&message, context, peers) { + match self.flow.handle_offer_paths_request(&message, context) { Some(msg) => msg, None => return None, }; @@ -14965,7 +14935,6 @@ where message, context, responder.clone(), - self.get_peers_for_blinded_path(), self.list_usable_channels(), &*self.entropy_source, &*self.router, diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 3795a076566..7a7214aea0c 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -173,7 +173,6 @@ where /// client, or when the paths change, e.g. if the paths are set to expire at a particular time. pub fn set_paths_to_static_invoice_server( &self, paths_to_static_invoice_server: Vec, - peers: Vec, ) -> Result<(), ()> { let mut cache = self.async_receive_offer_cache.lock().unwrap(); cache.set_paths_to_static_invoice_server(paths_to_static_invoice_server.clone())?; @@ -181,7 +180,7 @@ where // We'll only fail here if no peers are connected yet for us to create reply paths to outbound // offer_paths_requests, so ignore the error. - let _ = self.check_refresh_async_offers(peers, false); + let _ = self.check_refresh_async_offers(false); Ok(()) } @@ -286,7 +285,6 @@ where /// Errors if blinded path creation fails or the provided `recipient_id` is larger than 1KiB. pub fn blinded_paths_for_async_recipient( &self, recipient_id: Vec, relative_expiry: Option, - peers: Vec, ) -> Result, ()> { if recipient_id.len() > 1024 { log_trace!(self.logger, "Async recipient ID exceeds 1024 bytes"); @@ -300,19 +298,18 @@ where recipient_id, path_absolute_expiry, }); - self.create_blinded_paths(peers, context) + self.create_blinded_paths(context) } /// Creates a collection of blinded paths by delegating to /// [`MessageRouter::create_blinded_paths`]. /// /// Errors if the `MessageRouter` errors. - fn create_blinded_paths( - &self, peers: Vec, context: MessageContext, - ) -> Result, ()> { + fn create_blinded_paths(&self, context: MessageContext) -> Result, ()> { let recipient = self.get_our_node_id(); let receive_key = self.get_receive_auth_key(); let secp_ctx = &self.secp_ctx; + let peers = self.peers_cache.lock().unwrap().clone(); self.message_router .create_blinded_paths(recipient, receive_key, context, peers, secp_ctx) @@ -431,8 +428,6 @@ pub enum HeldHtlcReplyPath { ToUs { /// The id of the payment. payment_id: PaymentId, - /// The peers to use when creating this reply path. - peers: Vec, }, /// The reply path to the [`HeldHtlcAvailable`] message should terminate at our next-hop channel /// counterparty, as they are holding our HTLC until they receive the corresponding @@ -603,13 +598,13 @@ where /// /// [`DefaultMessageRouter`]: crate::onion_message::messenger::DefaultMessageRouter pub fn create_offer_builder( - &self, entropy_source: ES, peers: Vec, + &self, entropy_source: ES, ) -> Result, Bolt12SemanticError> where ES::Target: EntropySource, { self.create_offer_builder_intern(&*entropy_source, |_, context, _| { - self.create_blinded_paths(peers, context) + self.create_blinded_paths(context) .map(|paths| paths.into_iter().take(1)) .map_err(|_| Bolt12SemanticError::MissingPaths) }) @@ -624,13 +619,14 @@ where /// /// See [`Self::create_offer_builder`] for more details on usage. pub fn create_offer_builder_using_router( - &self, router: ME, entropy_source: ES, peers: Vec, + &self, router: ME, entropy_source: ES, ) -> Result, Bolt12SemanticError> where ME::Target: MessageRouter, ES::Target: EntropySource, { let receive_key = self.get_receive_auth_key(); + let peers = self.peers_cache.lock().unwrap().clone(); self.create_offer_builder_intern(&*entropy_source, |node_id, context, secp_ctx| { router .create_blinded_paths(node_id, receive_key, context, peers, secp_ctx) @@ -734,7 +730,7 @@ where /// [`RouteParameters::from_payment_params_and_value`]: crate::routing::router::RouteParameters::from_payment_params_and_value pub fn create_refund_builder( &self, entropy_source: ES, amount_msats: u64, absolute_expiry: Duration, - payment_id: PaymentId, peers: Vec, + payment_id: PaymentId, ) -> Result, Bolt12SemanticError> where ES::Target: EntropySource, @@ -742,7 +738,7 @@ where self.create_refund_builder_intern( &*entropy_source, |_, context, _| { - self.create_blinded_paths(peers, context) + self.create_blinded_paths(context) .map(|paths| paths.into_iter().take(1)) .map_err(|_| Bolt12SemanticError::MissingPaths) }, @@ -773,13 +769,14 @@ where /// [`RouteParameters::from_payment_params_and_value`]: crate::routing::router::RouteParameters::from_payment_params_and_value pub fn create_refund_builder_using_router( &self, router: ME, entropy_source: ES, amount_msats: u64, absolute_expiry: Duration, - payment_id: PaymentId, peers: Vec, + payment_id: PaymentId, ) -> Result, Bolt12SemanticError> where ME::Target: MessageRouter, ES::Target: EntropySource, { let receive_key = self.get_receive_auth_key(); + let peers = self.peers_cache.lock().unwrap().clone(); self.create_refund_builder_intern( &*entropy_source, |node_id, context, secp_ctx| { @@ -819,7 +816,7 @@ where pub fn create_static_invoice_builder<'a, ES: Deref, R: Deref>( &self, router: &R, entropy_source: ES, offer: &'a Offer, offer_nonce: Nonce, payment_secret: PaymentSecret, relative_expiry_secs: u32, - usable_channels: Vec, peers: Vec, + usable_channels: Vec, ) -> Result, Bolt12SemanticError> where ES::Target: EntropySource, @@ -860,9 +857,8 @@ where path_absolute_expiry, }); - let async_receive_message_paths = self - .create_blinded_paths(peers, context) - .map_err(|()| Bolt12SemanticError::MissingPaths)?; + let async_receive_message_paths = + self.create_blinded_paths(context).map_err(|()| Bolt12SemanticError::MissingPaths)?; StaticInvoiceBuilder::for_offer_using_derived_keys( offer, @@ -1048,21 +1044,13 @@ where /// /// See [`OffersMessageFlow::create_invoice_request_builder`] for more details. /// - /// # Peers - /// - /// The user must provide a list of [`MessageForwardNode`] that will be used to generate - /// valid reply paths for the counterparty to send back the corresponding [`Bolt12Invoice`] - /// or [`InvoiceError`]. - /// /// [`supports_onion_messages`]: crate::types::features::Features::supports_onion_messages pub fn enqueue_invoice_request( &self, invoice_request: InvoiceRequest, payment_id: PaymentId, nonce: Nonce, - peers: Vec, ) -> Result<(), Bolt12SemanticError> { let context = MessageContext::Offers(OffersContext::OutboundPayment { payment_id, nonce }); - let reply_paths = self - .create_blinded_paths(peers, context) - .map_err(|_| Bolt12SemanticError::MissingPaths)?; + let reply_paths = + self.create_blinded_paths(context).map_err(|_| Bolt12SemanticError::MissingPaths)?; let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap(); if !invoice_request.paths().is_empty() { @@ -1093,23 +1081,16 @@ where /// Enqueues the created [`Bolt12Invoice`] corresponding to a [`Refund`] to be sent /// to the counterparty. /// - /// # Peers - /// - /// The user must provide a list of [`MessageForwardNode`] that will be used to generate valid - /// reply paths for the counterparty to send back the corresponding [`InvoiceError`] if we fail - /// to create blinded reply paths - /// /// [`supports_onion_messages`]: crate::types::features::Features::supports_onion_messages pub fn enqueue_invoice( - &self, invoice: Bolt12Invoice, refund: &Refund, peers: Vec, + &self, invoice: Bolt12Invoice, refund: &Refund, ) -> Result<(), Bolt12SemanticError> { let payment_hash = invoice.payment_hash(); let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash }); - let reply_paths = self - .create_blinded_paths(peers, context) - .map_err(|_| Bolt12SemanticError::MissingPaths)?; + let reply_paths = + self.create_blinded_paths(context).map_err(|_| Bolt12SemanticError::MissingPaths)?; let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap(); @@ -1190,16 +1171,15 @@ where matches!(reply_path_params, HeldHtlcReplyPath::ToUs { .. }); let reply_paths = match reply_path_params { - HeldHtlcReplyPath::ToUs { payment_id, peers } => { + HeldHtlcReplyPath::ToUs { payment_id } => { let context = MessageContext::AsyncPayments(AsyncPaymentsContext::OutboundPayment { payment_id, }); - self.create_blinded_paths(peers, context) - .map_err(|_| { - log_trace!(self.logger, "Failed to create blinded paths when enqueueing held_htlc_available message"); - Bolt12SemanticError::MissingPaths - })? + self.create_blinded_paths(context).map_err(|_| { + log_trace!(self.logger, "Failed to create blinded paths when enqueueing held_htlc_available message"); + Bolt12SemanticError::MissingPaths + })? }, HeldHtlcReplyPath::ToCounterparty { path } => vec![path], }; @@ -1250,21 +1230,12 @@ where } /// Enqueues the created [`DNSSECQuery`] to be sent to the counterparty. - /// - /// # Peers - /// - /// The user must provide a list of [`MessageForwardNode`] that will be used to generate - /// valid reply paths for the counterparty to send back the corresponding response for - /// the [`DNSSECQuery`] message. - /// - /// [`supports_onion_messages`]: crate::types::features::Features::supports_onion_messages #[cfg(feature = "dnssec")] pub fn enqueue_dns_onion_message( &self, message: DNSSECQuery, context: DNSResolverContext, dns_resolvers: Vec, - peers: Vec, ) -> Result<(), Bolt12SemanticError> { let reply_paths = self - .create_blinded_paths(peers, MessageContext::DNSResolver(context)) + .create_blinded_paths(MessageContext::DNSResolver(context)) .map_err(|_| Bolt12SemanticError::MissingPaths)?; let message_params = dns_resolvers @@ -1332,8 +1303,8 @@ where /// /// Errors if we failed to create blinded reply paths when sending an [`OfferPathsRequest`] message. pub fn check_refresh_async_receive_offer_cache( - &self, peers: Vec, usable_channels: Vec, entropy: ES, - router: R, timer_tick_occurred: bool, + &self, usable_channels: Vec, entropy: ES, router: R, + timer_tick_occurred: bool, ) -> Result<(), ()> where ES::Target: EntropySource, @@ -1347,18 +1318,16 @@ where } } - self.check_refresh_async_offers(peers.clone(), timer_tick_occurred)?; + self.check_refresh_async_offers(timer_tick_occurred)?; if timer_tick_occurred { - self.check_refresh_static_invoices(peers, usable_channels, entropy, router); + self.check_refresh_static_invoices(usable_channels, entropy, router); } Ok(()) } - fn check_refresh_async_offers( - &self, peers: Vec, timer_tick_occurred: bool, - ) -> Result<(), ()> { + fn check_refresh_async_offers(&self, timer_tick_occurred: bool) -> Result<(), ()> { let duration_since_epoch = self.duration_since_epoch(); let mut cache = self.async_receive_offer_cache.lock().unwrap(); @@ -1376,7 +1345,7 @@ where .saturating_add(TEMP_REPLY_PATH_RELATIVE_EXPIRY), invoice_slot: needs_new_offer_slot, }); - let reply_paths = match self.create_blinded_paths(peers, context) { + let reply_paths = match self.create_blinded_paths(context) { Ok(paths) => paths, Err(()) => { log_error!( @@ -1408,8 +1377,7 @@ where /// Enqueue onion messages that will used to request invoice refresh from the static invoice /// server, based on the offers provided by the cache. fn check_refresh_static_invoices( - &self, peers: Vec, usable_channels: Vec, entropy: ES, - router: R, + &self, usable_channels: Vec, entropy: ES, router: R, ) where ES::Target: EntropySource, R::Target: Router, @@ -1424,7 +1392,6 @@ where let (invoice, forward_invreq_path) = match self.create_static_invoice_for_server( offer, offer_nonce, - peers.clone(), usable_channels.clone(), &*entropy, &*router, @@ -1455,7 +1422,7 @@ where // Enqueue the new serve_static_invoice messages in a separate loop to avoid holding the offer // cache lock and the pending_async_payments_messages lock at the same time. for (serve_invoice_msg, serve_invoice_path, reply_path_ctx) in serve_static_invoice_msgs { - let reply_paths = match self.create_blinded_paths(peers.clone(), reply_path_ctx) { + let reply_paths = match self.create_blinded_paths(reply_path_ctx) { Ok(paths) => paths, Err(()) => continue, }; @@ -1475,7 +1442,6 @@ where /// Sends out [`OfferPaths`] onion messages in response. pub fn handle_offer_paths_request( &self, request: &OfferPathsRequest, context: AsyncPaymentsContext, - peers: Vec, ) -> Option<(OfferPaths, MessageContext)> { let duration_since_epoch = self.duration_since_epoch(); @@ -1499,7 +1465,7 @@ where invoice_slot: request.invoice_slot, }); - match self.create_blinded_paths(peers, context) { + match self.create_blinded_paths(context) { Ok(paths) => (paths, path_absolute_expiry), Err(()) => { log_error!( @@ -1537,8 +1503,7 @@ where /// fail to create blinded paths. pub fn handle_offer_paths( &self, message: OfferPaths, context: AsyncPaymentsContext, responder: Responder, - peers: Vec, usable_channels: Vec, entropy: ES, - router: R, + usable_channels: Vec, entropy: ES, router: R, ) -> Option<(ServeStaticInvoice, MessageContext)> where ES::Target: EntropySource, @@ -1590,7 +1555,6 @@ where let (invoice, forward_invoice_request_path) = match self.create_static_invoice_for_server( &offer, offer_nonce, - peers, usable_channels, &*entropy, router, @@ -1628,8 +1592,8 @@ where /// Creates a [`StaticInvoice`] and a blinded path for the server to forward invoice requests from /// payers to our node. fn create_static_invoice_for_server( - &self, offer: &Offer, offer_nonce: Nonce, peers: Vec, - usable_channels: Vec, entropy: ES, router: R, + &self, offer: &Offer, offer_nonce: Nonce, usable_channels: Vec, + entropy: ES, router: R, ) -> Result<(StaticInvoice, BlindedMessagePath), ()> where ES::Target: EntropySource, @@ -1665,14 +1629,13 @@ where payment_secret, offer_relative_expiry, usable_channels, - peers.clone(), ) .and_then(|builder| builder.build_and_sign(secp_ctx)) .map_err(|_| ())?; let context = MessageContext::Offers(OffersContext::InvoiceRequest { nonce: offer_nonce }); let forward_invoice_request_path = self - .create_blinded_paths(peers, context) + .create_blinded_paths(context) .and_then(|paths| paths.into_iter().next().ok_or(()))?; Ok((invoice, forward_invoice_request_path)) From bddbd9c3d699b963fea5edfc8b7c920971f238f9 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 10 Sep 2025 12:15:16 -0400 Subject: [PATCH 3/3] Support multi-hop blinded paths for RAA We've been including blinded paths in our RAA messages for async payments purposes, but had to use 1-hop blinded paths because we can't acquire additional peer locks while holding a specific channel's peer lock during the RAA creation process. In the past few commits we started caching the set of peers in the OffersMessageFlow, so we can now generate multi-hop blinded paths during the RAA creation process without needing to acquire additional peer locks. --- lightning/src/ln/async_payments_tests.rs | 84 ++++++++++++++++++++++++ lightning/src/offers/flow.rs | 7 +- lightning/src/util/test_utils.rs | 14 ++++ 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index 78fce723abf..590e7042dd4 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -3285,6 +3285,90 @@ fn async_payment_mpp() { claim_payment_along_route(ClaimAlongRouteArgs::new(sender, expected_route, keysend_preimage)); } +#[test] +fn fallback_to_one_hop_release_htlc_path() { + // Check that if the sender's LSP's message router fails to find a blinded path when creating a + // path for the release_held_htlc message, they will fall back to manually creating a 1-hop + // blinded path. + let chanmon_cfgs = create_chanmon_cfgs(4); + let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); + + let (sender_cfg, recipient_cfg) = (often_offline_node_cfg(), often_offline_node_cfg()); + let mut sender_lsp_cfg = test_default_channel_config(); + sender_lsp_cfg.enable_htlc_hold = true; + let mut invoice_server_cfg = test_default_channel_config(); + invoice_server_cfg.accept_forwards_to_priv_channels = true; + + let node_chanmgrs = create_node_chanmgrs( + 4, + &node_cfgs, + &[Some(sender_cfg), Some(sender_lsp_cfg), Some(invoice_server_cfg), Some(recipient_cfg)], + ); + let nodes = create_network(4, &node_cfgs, &node_chanmgrs); + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 1_000_000, 0); + // Make sure all nodes are at the same block height + let node_max_height = + nodes.iter().map(|node| node.blocks.lock().unwrap().len()).max().unwrap() as u32; + connect_blocks(&nodes[0], node_max_height - nodes[0].best_block_info().1); + connect_blocks(&nodes[1], node_max_height - nodes[1].best_block_info().1); + connect_blocks(&nodes[2], node_max_height - nodes[2].best_block_info().1); + connect_blocks(&nodes[3], node_max_height - nodes[3].best_block_info().1); + let sender = &nodes[0]; + let sender_lsp = &nodes[1]; + let invoice_server = &nodes[2]; + let recipient = &nodes[3]; + + let amt_msat = 5000; + let (static_invoice, peer_node_id, static_invoice_om) = + build_async_offer_and_init_payment(amt_msat, &nodes); + + // Force the sender_lsp's call to MessageRouter::create_blinded_paths to fail so it has to fall + // back to a 1-hop blinded path when creating the paths for its revoke_and_ack message. + sender_lsp.message_router.create_blinded_paths_res_override.lock().unwrap().1 = Some(Err(())); + + let payment_hash = + lock_in_htlc_for_static_invoice(&static_invoice_om, peer_node_id, sender, sender_lsp); + + // Check that we actually had to fall back to a 1-hop path. + assert!(sender_lsp.message_router.create_blinded_paths_res_override.lock().unwrap().0 > 0); + sender_lsp.message_router.create_blinded_paths_res_override.lock().unwrap().1 = None; + + sender_lsp.node.process_pending_htlc_forwards(); + let (peer_id, held_htlc_om) = + extract_held_htlc_available_oms(sender, &[sender_lsp, invoice_server, recipient]) + .pop() + .unwrap(); + recipient.onion_messenger.handle_onion_message(peer_id, &held_htlc_om); + + // The release_htlc OM should go straight to the sender's LSP since they created a 1-hop blinded + // path to themselves for receiving it. + let release_htlc_om = recipient + .onion_messenger + .next_onion_message_for_peer(sender_lsp.node.get_our_node_id()) + .unwrap(); + sender_lsp + .onion_messenger + .handle_onion_message(recipient.node.get_our_node_id(), &release_htlc_om); + + sender_lsp.node.process_pending_htlc_forwards(); + let mut events = sender_lsp.node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let ev = remove_first_msg_event_to_node(&invoice_server.node.get_our_node_id(), &mut events); + check_added_monitors!(sender_lsp, 1); + + let path: &[&Node] = &[invoice_server, recipient]; + let args = PassAlongPathArgs::new(sender_lsp, path, amt_msat, payment_hash, ev); + let claimable_ev = do_pass_along_path(args).unwrap(); + + let route: &[&[&Node]] = &[&[sender_lsp, invoice_server, recipient]]; + let keysend_preimage = extract_payment_preimage(&claimable_ev); + let (res, _) = + claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage)); + assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice))); +} + #[test] fn fail_held_htlcs_when_cfg_unset() { // Test that if we receive a held HTLC but `UserConfig::enable_htlc_hold` is unset, we will fail diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 7a7214aea0c..82e7ddda1ce 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -1214,9 +1214,14 @@ where where ES::Target: EntropySource, { - // In the future, we should support multi-hop paths here. let context = MessageContext::AsyncPayments(AsyncPaymentsContext::ReleaseHeldHtlc { intercept_id }); + if let Ok(mut paths) = self.create_blinded_paths(context.clone()) { + if let Some(path) = paths.pop() { + return path; + } + } + let num_dummy_hops = PADDED_PATH_LENGTH.saturating_sub(1); BlindedMessagePath::new_with_dummy_hops( &[], diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index 769c2a3ed3e..51f56eae660 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -334,6 +334,9 @@ pub enum TestMessageRouterInternal<'a> { pub struct TestMessageRouter<'a> { pub inner: TestMessageRouterInternal<'a>, pub peers_override: Mutex>, + // An override result for Self::create_blinded_paths, plus a tracker for how many times the + // overridden result has been returned. + pub create_blinded_paths_res_override: Mutex<(u8, Option, ()>>)>, } impl<'a> TestMessageRouter<'a> { @@ -346,6 +349,7 @@ impl<'a> TestMessageRouter<'a> { entropy_source, )), peers_override: Mutex::new(Vec::new()), + create_blinded_paths_res_override: Mutex::new((0, None)), } } @@ -358,6 +362,7 @@ impl<'a> TestMessageRouter<'a> { entropy_source, )), peers_override: Mutex::new(Vec::new()), + create_blinded_paths_res_override: Mutex::new((0, None)), } } } @@ -385,6 +390,15 @@ impl<'a> MessageRouter for TestMessageRouter<'a> { &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { + { + let mut res_override = self.create_blinded_paths_res_override.lock().unwrap(); + if let Some(res) = &res_override.1 { + let res = res.clone(); + res_override.0 += 1; + return res; + } + } + let mut peers = peers; { let peers_override = self.peers_override.lock().unwrap();