From 638b41b84fbc1ec75b80b709f31065043fe410a2 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 20 Feb 2025 16:45:58 -0500 Subject: [PATCH 1/8] BOLT 12 {Static}Invoices: expose more is_expired methods In upcoming commits, we need to check whether a static invoice or its underlying offer is expired in no-std builds. Here we expose the methods to do so. The methods could instead be kept private to the crate, but they seem potentially useful. --- lightning/src/offers/invoice.rs | 4 ++++ lightning/src/offers/invoice_macros.rs | 5 +++++ lightning/src/offers/static_invoice.rs | 17 ++++++++++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index eacc2a8914b..7aa3df4ba39 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -1223,6 +1223,10 @@ impl InvoiceContents { is_expired(self.created_at(), self.relative_expiry()) } + fn is_expired_no_std(&self, duration_since_epoch: Duration) -> bool { + self.created_at().saturating_add(self.relative_expiry()) < duration_since_epoch + } + fn payment_hash(&self) -> PaymentHash { self.fields().payment_hash } diff --git a/lightning/src/offers/invoice_macros.rs b/lightning/src/offers/invoice_macros.rs index af3c2a6155e..1ac6e40b896 100644 --- a/lightning/src/offers/invoice_macros.rs +++ b/lightning/src/offers/invoice_macros.rs @@ -131,6 +131,11 @@ macro_rules! invoice_accessors_common { ($self: ident, $contents: expr, $invoice $contents.is_expired() } + /// Whether the invoice has expired given the current time as duration since the Unix epoch. + pub fn is_expired_no_std(&$self, duration_since_epoch: Duration) -> bool { + $contents.is_expired_no_std(duration_since_epoch) + } + /// Fallback addresses for paying the invoice on-chain, in order of most-preferred to /// least-preferred. pub fn fallbacks(&$self) -> Vec
{ diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index 08170fda867..8fa5790161e 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -395,6 +395,18 @@ impl StaticInvoice { self.signature } + /// Whether the [`Offer`] that this invoice is based on is expired. + #[cfg(feature = "std")] + pub fn is_offer_expired(&self) -> bool { + self.contents.is_expired() + } + + /// Whether the [`Offer`] that this invoice is based on is expired, given the current time as + /// duration since the Unix epoch. + pub fn is_offer_expired_no_std(&self, duration_since_epoch: Duration) -> bool { + self.contents.is_offer_expired_no_std(duration_since_epoch) + } + #[allow(unused)] // TODO: remove this once we remove the `async_payments` cfg flag pub(crate) fn is_from_same_offer(&self, invreq: &InvoiceRequest) -> bool { let invoice_offer_tlv_stream = @@ -411,7 +423,6 @@ impl InvoiceContents { self.offer.is_expired() } - #[cfg(not(feature = "std"))] fn is_offer_expired_no_std(&self, duration_since_epoch: Duration) -> bool { self.offer.is_expired_no_std(duration_since_epoch) } @@ -528,6 +539,10 @@ impl InvoiceContents { is_expired(self.created_at(), self.relative_expiry()) } + fn is_expired_no_std(&self, duration_since_epoch: Duration) -> bool { + self.created_at().saturating_add(self.relative_expiry()) < duration_since_epoch + } + fn fallbacks(&self) -> Vec
{ let chain = self.chain(); self.fallbacks From 272360281b9839e62b50f0e1b74f61277e098562 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Mon, 3 Feb 2025 15:26:01 -0800 Subject: [PATCH 2/8] Util for blinded paths to configure an async recipient As part of serving static invoices to payers on behalf of often-offline recipients, these recipients need a way to contact the static invoice server to retrieve blinded paths to include in their offers. Add a utility to create blinded paths for this purpose as a static invoice server. The recipient will be configured with the resulting paths and use them to request offer paths on startup. --- lightning/src/blinded_path/message.rs | 22 +++++++++++++++++++++ lightning/src/ln/channelmanager.rs | 28 +++++++++++++++++++++++++++ lightning/src/offers/flow.rs | 22 +++++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 99dd4fa667c..f68c4e0b5e1 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -405,6 +405,24 @@ pub enum OffersContext { /// [`AsyncPaymentsMessage`]: crate::onion_message::async_payments::AsyncPaymentsMessage #[derive(Clone, Debug)] pub enum AsyncPaymentsContext { + /// Context used by a [`BlindedMessagePath`] provided out-of-band to an async recipient, where the + /// context is provided back to the static invoice server in corresponding [`OfferPathsRequest`]s. + /// + /// [`OfferPathsRequest`]: crate::onion_message::async_payments::OfferPathsRequest + OfferPathsRequest { + /// An identifier for the async recipient that is requesting blinded paths to include in their + /// [`Offer::paths`]. This ID will be surfaced when the async recipient eventually sends a + /// corresponding [`ServeStaticInvoice`] message, and can be used to rate limit the recipient. + /// + /// [`Offer::paths`]: crate::offers::offer::Offer::paths + /// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice + recipient_id: Vec, + /// The time as duration since the Unix epoch at which this path expires and messages sent over + /// it should be ignored. + /// + /// Useful to timeout async recipients that are no longer supported as clients. + path_absolute_expiry: core::time::Duration, + }, /// Context used by a reply path to an [`OfferPathsRequest`], provided back to us as an async /// recipient in corresponding [`OfferPaths`] messages from the static invoice server. /// @@ -528,6 +546,10 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext, (0, offer_id, required), (2, path_absolute_expiry, required), }, + (4, OfferPathsRequest) => { + (0, recipient_id, required), + (2, path_absolute_expiry, required), + }, ); /// Contains a simple nonce for use in a blinded path's context. diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 8a904a90e64..a7e6f96d808 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -11499,6 +11499,34 @@ where inbound_payment::get_payment_preimage(payment_hash, payment_secret, expanded_key) } + /// [`BlindedMessagePath`]s for an async recipient to communicate with this node and interactively + /// build [`Offer`]s and [`StaticInvoice`]s for receiving async payments. + /// + /// ## Usage + /// 1. Static invoice server calls [`Self::blinded_paths_for_async_recipient`] + /// 2. Static invoice server communicates the resulting paths out-of-band to the async recipient, + /// who calls [`Self::set_paths_to_static_invoice_server`] to configure themselves with these + /// paths + /// 3. Async recipient automatically sends [`OfferPathsRequest`]s over the configured paths, and + /// uses the resulting paths from the server's [`OfferPaths`] response to build their async + /// receive offer + /// + /// If `relative_expiry` is unset, the [`BlindedMessagePath`]s will never expire. + /// + /// Returns the paths that the recipient should be configured with via + /// [`Self::set_paths_to_static_invoice_server`]. + /// + /// The provided `recipient_id` must uniquely identify the recipient, and will be surfaced later + /// when the recipient provides us with a static invoice to persist and serve to payers on their + /// behalf. + #[cfg(async_payments)] + 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) + } + #[cfg(any(test, async_payments))] pub(super) fn duration_since_epoch(&self) -> Duration { #[cfg(not(feature = "std"))] diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 0990251c311..29ccbf9a0bb 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -250,6 +250,28 @@ impl OffersMessageFlow where MR::Target: MessageRouter, { + /// [`BlindedMessagePath`]s for an async recipient to communicate with this node and interactively + /// build [`Offer`]s and [`StaticInvoice`]s for receiving async payments. + /// + /// If `relative_expiry` is unset, the [`BlindedMessagePath`]s will never expire. + /// + /// Returns the paths that the recipient should be configured with via + /// [`Self::set_paths_to_static_invoice_server`]. + #[cfg(async_payments)] + pub(crate) fn blinded_paths_for_async_recipient( + &self, recipient_id: Vec, relative_expiry: Option, + peers: Vec, + ) -> Result, ()> { + let path_absolute_expiry = + relative_expiry.unwrap_or(Duration::MAX).saturating_add(self.duration_since_epoch()); + + let context = MessageContext::AsyncPayments(AsyncPaymentsContext::OfferPathsRequest { + recipient_id, + path_absolute_expiry, + }); + self.create_blinded_paths(peers, context) + } + /// Creates a collection of blinded paths by delegating to [`MessageRouter`] based on /// the path's intended lifetime. /// From c4e6bd2d25d97d210473666f1d46221478d665b6 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 6 Feb 2025 16:29:42 -0800 Subject: [PATCH 3/8] Send offer paths in response to requests As part of serving static invoices to payers on behalf of often-offline recipients, we need to provide the async recipient with blinded message paths to include in their offers. Support responding to inbound requests for offer paths from async recipients. --- lightning/src/blinded_path/message.rs | 80 +++++++++++++++++++++++++++ lightning/src/ln/channelmanager.rs | 13 +++++ lightning/src/offers/flow.rs | 67 ++++++++++++++++++++++ 3 files changed, 160 insertions(+) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index f68c4e0b5e1..795841d4f7e 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -35,6 +35,7 @@ use bitcoin::hashes::sha256::Hash as Sha256; use core::mem; use core::ops::Deref; +use core::time::Duration; /// A blinded path to be used for sending or receiving a message, hiding the identity of the /// recipient. @@ -342,6 +343,43 @@ pub enum OffersContext { /// [`Offer`]: crate::offers::offer::Offer nonce: Nonce, }, + /// Context used by a [`BlindedMessagePath`] within the [`Offer`] of an async recipient. + /// + /// This variant is received by the static invoice server when handling an [`InvoiceRequest`] on + /// behalf of said async recipient. + /// + /// [`Offer`]: crate::offers::offer::Offer + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + StaticInvoiceRequested { + /// An identifier for the async recipient for whom we as a static invoice server are serving + /// [`StaticInvoice`]s. Used paired with the + /// [`OffersContext::StaticInvoiceRequested::invoice_id`] when looking up a corresponding + /// [`StaticInvoice`] to return to the payer if the recipient is offline. This id was previously + /// provided via [`AsyncPaymentsContext::ServeStaticInvoice::recipient_id`]. + /// + /// Also useful for rate limiting the number of [`InvoiceRequest`]s we will respond to on + /// recipient's behalf. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + recipient_id: Vec, + + /// A random unique identifier for a specific [`StaticInvoice`] that the recipient previously + /// requested be served on their behalf. Useful when paired with the + /// [`OffersContext::StaticInvoiceRequested::recipient_id`] to pull that specific invoice from + /// the database when payers send an [`InvoiceRequest`]. This id was previously + /// provided via [`AsyncPaymentsContext::ServeStaticInvoice::invoice_id`]. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + invoice_id: u128, + + /// The time as duration since the Unix epoch at which this path expires and messages sent over + /// it should be ignored. + /// + /// Useful to timeout async recipients that are no longer supported as clients. + path_absolute_expiry: Duration, + }, /// Context used by a [`BlindedMessagePath`] within a [`Refund`] or as a reply path for an /// [`InvoiceRequest`]. /// @@ -438,6 +476,38 @@ pub enum AsyncPaymentsContext { /// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths path_absolute_expiry: core::time::Duration, }, + /// Context used by a reply path to an [`OfferPaths`] message, provided back to us as the static + /// invoice server in corresponding [`ServeStaticInvoice`] messages. + /// + /// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths + /// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice + ServeStaticInvoice { + /// An identifier for the async recipient that is requesting that a [`StaticInvoice`] be served + /// on their behalf. + /// + /// Useful for retrieving the invoice when payers send an [`InvoiceRequest`] to us as the static + /// invoice server. Also useful to rate limit the invoices being persisted on behalf of a + /// particular recipient. This id will be provided back to us as the static invoice server via + /// [`OffersContext::StaticInvoiceRequested::recipient_id`] + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + recipient_id: Vec, + /// A random unique identifier for the specific [`StaticInvoice`] that the recipient is + /// requesting be served on their behalf. Useful when surfaced alongside the above + /// `recipient_id` when payers send an [`InvoiceRequest`], to pull the specific static invoice + /// from the database. This id will be provided back to us as the static invoice server via + /// [`OffersContext::StaticInvoiceRequested::invoice_id`] + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + invoice_id: u128, + /// The time as duration since the Unix epoch at which this path expires and messages sent over + /// it should be ignored. + /// + /// Useful to timeout async recipients that are no longer supported as clients. + path_absolute_expiry: core::time::Duration, + }, /// Context used by a reply path to a [`ServeStaticInvoice`] message, provided back to us in /// corresponding [`StaticInvoicePersisted`] messages. /// @@ -526,6 +596,11 @@ impl_writeable_tlv_based_enum!(OffersContext, (1, nonce, required), (2, hmac, required) }, + (3, StaticInvoiceRequested) => { + (0, recipient_id, required), + (2, invoice_id, required), + (4, path_absolute_expiry, required), + }, ); impl_writeable_tlv_based_enum!(AsyncPaymentsContext, @@ -550,6 +625,11 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext, (0, recipient_id, required), (2, path_absolute_expiry, required), }, + (5, ServeStaticInvoice) => { + (0, recipient_id, required), + (2, invoice_id, required), + (4, path_absolute_expiry, required), + }, ); /// Contains a simple nonce for use in a blinded path's context. diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index a7e6f96d808..8a2162a9745 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -13482,6 +13482,19 @@ where &self, _message: OfferPathsRequest, _context: AsyncPaymentsContext, _responder: Option, ) -> Option<(OfferPaths, ResponseInstruction)> { + #[cfg(async_payments)] + { + let peers = self.get_peers_for_blinded_path(); + let entropy = &*self.entropy_source; + let (message, reply_path_context) = + match self.flow.handle_offer_paths_request(_context, peers, entropy) { + Some(msg) => msg, + None => return None, + }; + _responder.map(|resp| (message, resp.respond_with_reply_path(reply_path_context))) + } + + #[cfg(not(async_payments))] None } diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 29ccbf9a0bb..39f8e60bc64 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -246,6 +246,10 @@ const OFFERS_MESSAGE_REQUEST_LIMIT: usize = 10; #[cfg(async_payments)] const TEMP_REPLY_PATH_RELATIVE_EXPIRY: Duration = Duration::from_secs(7200); +// Default to async receive offers and the paths used to update them lasting one year. +#[cfg(async_payments)] +const DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY: Duration = Duration::from_secs(365 * 24 * 60 * 60); + impl OffersMessageFlow where MR::Target: MessageRouter, @@ -1309,6 +1313,69 @@ where } } + /// Handles an incoming [`OfferPathsRequest`] onion message from an often-offline recipient who + /// wants us (the static invoice server) to serve [`StaticInvoice`]s to payers on their behalf. + /// Sends out [`OfferPaths`] onion messages in response. + #[cfg(async_payments)] + pub(crate) fn handle_offer_paths_request( + &self, context: AsyncPaymentsContext, peers: Vec, entropy_source: ES, + ) -> Option<(OfferPaths, MessageContext)> + where + ES::Target: EntropySource, + { + let duration_since_epoch = self.duration_since_epoch(); + + let recipient_id = match context { + AsyncPaymentsContext::OfferPathsRequest { recipient_id, path_absolute_expiry } => { + if duration_since_epoch > path_absolute_expiry { + return None; + } + recipient_id + }, + _ => return None, + }; + + let mut random_bytes = [0u8; 16]; + random_bytes.copy_from_slice(&entropy_source.get_secure_random_bytes()[..16]); + let invoice_id = u128::from_be_bytes(random_bytes); + + // Create the blinded paths that will be included in the async recipient's offer. + let (offer_paths, paths_expiry) = { + let path_absolute_expiry = + duration_since_epoch.saturating_add(DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY); + let context = OffersContext::StaticInvoiceRequested { + recipient_id: recipient_id.clone(), + path_absolute_expiry, + invoice_id, + }; + match self.create_blinded_paths_using_absolute_expiry( + context, + Some(path_absolute_expiry), + peers, + ) { + Ok(paths) => (paths, path_absolute_expiry), + Err(()) => return None, + } + }; + + // Create a reply path so that the recipient can respond to our offer_paths message with the + // static invoice that they create. This path will also be used by the recipient to update said + // invoice. + let reply_path_context = { + let path_absolute_expiry = + duration_since_epoch.saturating_add(DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY); + MessageContext::AsyncPayments(AsyncPaymentsContext::ServeStaticInvoice { + recipient_id, + invoice_id, + path_absolute_expiry, + }) + }; + + let offer_paths_om = + OfferPaths { paths: offer_paths, paths_absolute_expiry: Some(paths_expiry.as_secs()) }; + return Some((offer_paths_om, reply_path_context)); + } + /// Handles an incoming [`OfferPaths`] message from the static invoice server, sending out /// [`ServeStaticInvoice`] onion messages in response if we've built a new async receive offer and /// need the corresponding [`StaticInvoice`] to be persisted by the static invoice server. From f8859f754e859465d14c54d47c67ad5f4d936d01 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 18 Feb 2025 17:30:08 -0500 Subject: [PATCH 4/8] Static invoice server: persist invoices once built As part of serving static invoices to payers on behalf of often-offline recipients, the recipient will send us the final static invoice once it's done being interactively built. We will then persist this invoice and confirm to them that the corresponding offer is ready to be used for async payments. Surface an event once the invoice is received and expose an API to tell the recipient that it's ready for payments. --- lightning/src/events/mod.rs | 47 ++++++++++++++++++++++++++++++ lightning/src/ln/channelmanager.rs | 32 ++++++++++++++++++++ lightning/src/offers/flow.rs | 44 ++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index d01af737c32..17dd1a62be6 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -1582,6 +1582,44 @@ pub enum Event { /// onion messages. peer_node_id: PublicKey, }, + /// As a static invoice server, we received a [`StaticInvoice`] from an async recipient that wants + /// us to serve the invoice to payers on their behalf when they are offline. This event will only + /// be generated if we previously created paths using + /// [`ChannelManager::blinded_paths_for_async_recipient`] and the recipient was configured with + /// them via [`ChannelManager::set_paths_to_static_invoice_server`]. + /// + /// [`ChannelManager::blinded_paths_for_async_recipient`]: crate::ln::channelmanager::ChannelManager::blinded_paths_for_async_recipient + /// [`ChannelManager::set_paths_to_static_invoice_server`]: crate::ln::channelmanager::ChannelManager::set_paths_to_static_invoice_server + #[cfg(async_payments)] + PersistStaticInvoice { + /// The invoice that should be persisted and later provided to payers when handling a future + /// `Event::StaticInvoiceRequested`. + invoice: StaticInvoice, + /// Useful for the recipient to replace a specific invoice stored by us as the static invoice + /// server. + /// + /// When this invoice is persisted, this slot number should be included so if we receive another + /// [`Event::PersistStaticInvoice`] containing the same slot number we can swap the existing + /// invoice out for the new one. + invoice_slot: u16, + /// An identifier for the recipient, originally provided to + /// [`ChannelManager::blinded_paths_for_async_recipient`]. + /// + /// When an `Event::StaticInvoiceRequested` comes in for the invoice, this id will be surfaced + /// and can be used alongside the `invoice_id` to retrieve the invoice from the database. + recipient_id: Vec, + /// A unique identifier for the invoice. When an `Event::StaticInvoiceRequested` comes in for + /// the invoice, this id will be surfaced and can be used alongside the `recipient_id` to + /// retrieve the invoice from the database. + invoice_id: u128, + /// Once the [`StaticInvoice`], `invoice_slot` and `invoice_id` are persisted, + /// [`ChannelManager::static_invoice_persisted`] should be called with this responder to confirm + /// to the recipient that their [`Offer`] is ready to be used for async payments. + /// + /// [`ChannelManager::static_invoice_persisted`]: crate::ln::channelmanager::ChannelManager::static_invoice_persisted + /// [`Offer`]: crate::offers::offer::Offer + invoice_persisted_path: Responder, + }, } impl Writeable for Event { @@ -2012,6 +2050,12 @@ impl Writeable for Event { (8, former_temporary_channel_id, required), }); }, + #[cfg(async_payments)] + &Event::PersistStaticInvoice { .. } => { + 45u8.write(writer)?; + // No need to write these events because we can just restart the static invoice negotiation + // on startup. + }, // Note that, going forward, all new events must only write data inside of // `write_tlv_fields`. Versions 0.0.101+ will ignore odd-numbered events that write // data via `write_tlv_fields`. @@ -2583,6 +2627,9 @@ impl MaybeReadable for Event { former_temporary_channel_id: former_temporary_channel_id.0.unwrap(), })) }, + // Note that we do not write a length-prefixed TLV for PersistStaticInvoice events. + #[cfg(async_payments)] + 45u8 => Ok(None), // Versions prior to 0.0.100 did not ignore odd types, instead returning InvalidValue. // Version 0.0.100 failed to properly ignore odd types, possibly resulting in corrupt // reads. diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 8a2162a9745..611d4ef75a1 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5272,6 +5272,13 @@ where } } + /// Should be called after handling an [`Event::PersistStaticInvoice`], where the `Responder` + /// comes from [`Event::PersistStaticInvoice::invoice_persisted_path`]. + #[cfg(async_payments)] + pub fn static_invoice_persisted(&self, invoice_persisted_path: Responder) { + self.flow.static_invoice_persisted(invoice_persisted_path); + } + #[cfg(async_payments)] fn initiate_async_payment( &self, invoice: &StaticInvoice, payment_id: PaymentId, @@ -13535,6 +13542,31 @@ where &self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext, _responder: Option, ) { + #[cfg(async_payments)] + { + let responder = match _responder { + Some(resp) => resp, + None => return, + }; + + let (recipient_id, invoice_id) = + match self.flow.verify_serve_static_invoice_message(&_message, _context) { + Ok(nonce) => nonce, + Err(()) => return, + }; + + let mut pending_events = self.pending_events.lock().unwrap(); + pending_events.push_back(( + Event::PersistStaticInvoice { + invoice: _message.invoice, + invoice_slot: _message.invoice_slot, + recipient_id, + invoice_id, + invoice_persisted_path: responder, + }, + None, + )); + } } fn handle_static_invoice_persisted( diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 39f8e60bc64..61e4a32715c 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -68,6 +68,7 @@ use { crate::offers::static_invoice::{StaticInvoice, StaticInvoiceBuilder}, crate::onion_message::async_payments::{ HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ServeStaticInvoice, + StaticInvoicePersisted, }, crate::onion_message::messenger::Responder, }; @@ -1526,6 +1527,49 @@ where Ok((invoice, forward_invoice_request_path)) } + /// Verifies an incoming [`ServeStaticInvoice`] onion message from an often-offline recipient who + /// wants us as a static invoice server to serve the [`ServeStaticInvoice::invoice`] to payers on + /// their behalf. + /// + /// On success, returns (recipient_id, invoice_id) for use in persisting and later retrieving + /// the static invoice from the database. + /// + /// [`ServeStaticInvoice::invoice`]: crate::onion_message::async_payments::ServeStaticInvoice::invoice + #[cfg(async_payments)] + pub fn verify_serve_static_invoice_message( + &self, message: &ServeStaticInvoice, context: AsyncPaymentsContext, + ) -> Result<(Vec, u128), ()> { + if message.invoice.is_expired_no_std(self.duration_since_epoch()) { + return Err(()); + } + match context { + AsyncPaymentsContext::ServeStaticInvoice { + recipient_id, + invoice_id, + path_absolute_expiry, + } => { + if self.duration_since_epoch() > path_absolute_expiry { + return Err(()); + } + + return Ok((recipient_id, invoice_id)); + }, + _ => return Err(()), + }; + } + + /// Indicates that a [`ServeStaticInvoice::invoice`] has been persisted and is ready to be served + /// to payers on behalf of an often-offline recipient. This method must be called after persisting + /// a [`StaticInvoice`] to confirm to the recipient that their corresponding [`Offer`] is ready to + /// receive async payments. + #[cfg(async_payments)] + pub fn static_invoice_persisted(&self, responder: Responder) { + let mut pending_async_payments_messages = + self.pending_async_payments_messages.lock().unwrap(); + let message = AsyncPaymentsMessage::StaticInvoicePersisted(StaticInvoicePersisted {}); + pending_async_payments_messages.push((message, responder.respond().into_instructions())); + } + /// Handles an incoming [`StaticInvoicePersisted`] onion message from the static invoice server. /// Returns a bool indicating whether the async receive offer cache needs to be re-persisted. /// From 96b4070b31143529e7c126fa52b4fc7d79e39dc9 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 19 Feb 2025 19:18:13 -0500 Subject: [PATCH 5/8] Static invoice server: forward static invoices to payers Here we implement serving static invoices to payers on behalf of often-offline recipients. These recipients previously encoded blinded paths terminating at our node in their offer, so we receive invoice requests on their behalf. Handle those inbound invreqs by retrieving a static invoice we previously persisted on behalf of the payee, and forward it to the payer as a reply to their invreq. --- lightning/src/events/mod.rs | 45 +++++++++++++++++++++-- lightning/src/ln/channelmanager.rs | 22 ++++++++++- lightning/src/offers/flow.rs | 59 +++++++++++++++++++++++++++++- 3 files changed, 119 insertions(+), 7 deletions(-) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 17dd1a62be6..6b9a52c2ccc 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -1593,7 +1593,7 @@ pub enum Event { #[cfg(async_payments)] PersistStaticInvoice { /// The invoice that should be persisted and later provided to payers when handling a future - /// `Event::StaticInvoiceRequested`. + /// [`Event::StaticInvoiceRequested`]. invoice: StaticInvoice, /// Useful for the recipient to replace a specific invoice stored by us as the static invoice /// server. @@ -1605,10 +1605,10 @@ pub enum Event { /// An identifier for the recipient, originally provided to /// [`ChannelManager::blinded_paths_for_async_recipient`]. /// - /// When an `Event::StaticInvoiceRequested` comes in for the invoice, this id will be surfaced + /// When an [`Event::StaticInvoiceRequested`] comes in for the invoice, this id will be surfaced /// and can be used alongside the `invoice_id` to retrieve the invoice from the database. recipient_id: Vec, - /// A unique identifier for the invoice. When an `Event::StaticInvoiceRequested` comes in for + /// A unique identifier for the invoice. When an [`Event::StaticInvoiceRequested`] comes in for /// the invoice, this id will be surfaced and can be used alongside the `recipient_id` to /// retrieve the invoice from the database. invoice_id: u128, @@ -1620,6 +1620,37 @@ pub enum Event { /// [`Offer`]: crate::offers::offer::Offer invoice_persisted_path: Responder, }, + /// As a static invoice server, we received an [`InvoiceRequest`] on behalf of an often-offline + /// recipient for whom we are serving [`StaticInvoice`]s. + /// + /// This event will only be generated if we previously created paths using + /// [`ChannelManager::blinded_paths_for_async_recipient`] and the recipient was configured with + /// them via [`ChannelManager::set_paths_to_static_invoice_server`]. + /// + /// If we previously persisted a [`StaticInvoice`] from an [`Event::PersistStaticInvoice`] that + /// matches the below `recipient_id` and `invoice_id`, that invoice should be retrieved now + /// and forwarded to the payer via [`ChannelManager::send_static_invoice`]. + /// + /// [`ChannelManager::blinded_paths_for_async_recipient`]: crate::ln::channelmanager::ChannelManager::blinded_paths_for_async_recipient + /// [`ChannelManager::set_paths_to_static_invoice_server`]: crate::ln::channelmanager::ChannelManager::set_paths_to_static_invoice_server + /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest + /// [`ChannelManager::send_static_invoice`]: crate::ln::channelmanager::ChannelManager::send_static_invoice + #[cfg(async_payments)] + StaticInvoiceRequested { + /// An identifier for the recipient previously surfaced in + /// [`Event::PersistStaticInvoice::recipient_id`]. Useful when paired with the `invoice_id` to + /// retrieve the [`StaticInvoice`] requested by the payer. + recipient_id: Vec, + /// A random unique identifier for the invoice being requested, previously surfaced in + /// [`Event::PersistStaticInvoice::invoice_id`]. Useful when paired with the `recipient_id` to + /// retrieve the [`StaticInvoice`] requested by the payer. + invoice_id: u128, + /// The path over which the [`StaticInvoice`] will be sent to the payer, which should be + /// provided to [`ChannelManager::send_static_invoice`] along with the invoice. + /// + /// [`ChannelManager::send_static_invoice`]: crate::ln::channelmanager::ChannelManager::send_static_invoice + reply_path: Responder, + }, } impl Writeable for Event { @@ -2056,6 +2087,11 @@ impl Writeable for Event { // No need to write these events because we can just restart the static invoice negotiation // on startup. }, + #[cfg(async_payments)] + &Event::StaticInvoiceRequested { .. } => { + 47u8.write(writer)?; + // Never write StaticInvoiceRequested events as buffered onion messages aren't serialized. + }, // Note that, going forward, all new events must only write data inside of // `write_tlv_fields`. Versions 0.0.101+ will ignore odd-numbered events that write // data via `write_tlv_fields`. @@ -2630,6 +2666,9 @@ impl MaybeReadable for Event { // Note that we do not write a length-prefixed TLV for PersistStaticInvoice events. #[cfg(async_payments)] 45u8 => Ok(None), + // Note that we do not write a length-prefixed TLV for StaticInvoiceRequested events. + #[cfg(async_payments)] + 47u8 => Ok(None), // Versions prior to 0.0.100 did not ignore odd types, instead returning InvalidValue. // Version 0.0.100 failed to properly ignore odd types, possibly resulting in corrupt // reads. diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 611d4ef75a1..40480fd9b43 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -87,7 +87,7 @@ use crate::ln::outbound_payment::{ }; use crate::ln::types::ChannelId; use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache; -use crate::offers::flow::OffersMessageFlow; +use crate::offers::flow::{InvreqResponseInstructions, OffersMessageFlow}; use crate::offers::invoice::{ Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder, DEFAULT_RELATIVE_EXPIRY, }; @@ -5279,6 +5279,14 @@ where self.flow.static_invoice_persisted(invoice_persisted_path); } + /// Forwards a [`StaticInvoice`] in response to an [`Event::StaticInvoiceRequested`]. + #[cfg(async_payments)] + pub fn send_static_invoice( + &self, invoice: StaticInvoice, responder: Responder, + ) -> Result<(), Bolt12SemanticError> { + self.flow.enqueue_static_invoice(invoice, responder) + } + #[cfg(async_payments)] fn initiate_async_payment( &self, invoice: &StaticInvoice, payment_id: PaymentId, @@ -13355,7 +13363,17 @@ where }; let invoice_request = match self.flow.verify_invoice_request(invoice_request, context) { - Ok(invoice_request) => invoice_request, + Ok(InvreqResponseInstructions::SendInvoice(invoice_request)) => invoice_request, + Ok(InvreqResponseInstructions::SendStaticInvoice { + recipient_id: _recipient_id, invoice_id: _invoice_id + }) => { + #[cfg(async_payments)] + self.pending_events.lock().unwrap().push_back((Event::StaticInvoiceRequested { + recipient_id: _recipient_id, invoice_id: _invoice_id, reply_path: responder + }, None)); + + return None + }, Err(_) => return None, }; diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 61e4a32715c..99724a3996f 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -415,6 +415,26 @@ fn enqueue_onion_message_with_reply_paths( }); } +/// Instructions for how to respond to an `InvoiceRequest`. +pub enum InvreqResponseInstructions { + /// We are the recipient of this payment, and a [`Bolt12Invoice`] should be sent in response to + /// the invoice request since it is now verified. + SendInvoice(VerifiedInvoiceRequest), + /// We are a static invoice server and should respond to this invoice request by retrieving the + /// [`StaticInvoice`] corresponding to the `recipient_id` and `invoice_id` and calling + /// `OffersMessageFlow::enqueue_static_invoice`. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + SendStaticInvoice { + /// An identifier for the async recipient for whom we are serving [`StaticInvoice`]s. + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + recipient_id: Vec, + /// An identifier for the specific invoice being requested by the payer. + invoice_id: u128, + }, +} + impl OffersMessageFlow where MR::Target: MessageRouter, @@ -432,13 +452,28 @@ where /// - The verification process (via recipient context data or metadata) fails. pub fn verify_invoice_request( &self, invoice_request: InvoiceRequest, context: Option, - ) -> Result { + ) -> Result { let secp_ctx = &self.secp_ctx; let expanded_key = &self.inbound_payment_key; let nonce = match context { None if invoice_request.metadata().is_some() => None, Some(OffersContext::InvoiceRequest { nonce }) => Some(nonce), + #[cfg(async_payments)] + Some(OffersContext::StaticInvoiceRequested { + recipient_id, + invoice_id, + path_absolute_expiry, + }) => { + if path_absolute_expiry < self.duration_since_epoch() { + return Err(()); + } + + return Ok(InvreqResponseInstructions::SendStaticInvoice { + recipient_id, + invoice_id, + }); + }, _ => return Err(()), }; @@ -449,7 +484,7 @@ where None => invoice_request.verify_using_metadata(expanded_key, secp_ctx), }?; - Ok(invoice_request) + Ok(InvreqResponseInstructions::SendInvoice(invoice_request)) } /// Verifies a [`Bolt12Invoice`] using the provided [`OffersContext`] or the invoice's payer metadata, @@ -1059,6 +1094,26 @@ where Ok(()) } + /// Forwards a [`StaticInvoice`] over the provided `responder`. + #[cfg(async_payments)] + pub(crate) fn enqueue_static_invoice( + &self, invoice: StaticInvoice, responder: Responder, + ) -> Result<(), Bolt12SemanticError> { + let duration_since_epoch = self.duration_since_epoch(); + if invoice.is_expired_no_std(duration_since_epoch) { + return Err(Bolt12SemanticError::AlreadyExpired); + } + if invoice.is_offer_expired_no_std(duration_since_epoch) { + return Err(Bolt12SemanticError::AlreadyExpired); + } + + let mut pending_offers_messages = self.pending_offers_messages.lock().unwrap(); + let message = OffersMessage::StaticInvoice(invoice); + pending_offers_messages.push((message, responder.respond().into_instructions())); + + Ok(()) + } + /// Enqueues `held_htlc_available` onion messages to be sent to the payee via the reply paths /// contained within the provided [`StaticInvoice`]. /// From c02a1a672905a1034cc89bd7cd81292a7db568a5 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 4 Jun 2025 18:26:10 -0400 Subject: [PATCH 6/8] Async payments tests: stop hardcoding keysend bytes We're about to add a bunch more async payments tests, so take this opportunity to clean up the existing tests by no longer hardcoding the keysend payment preimage bytes ahead of time. This previously caused an MPP test to spuriously fail because all the session_privs were the same, and is generally not ideal. Also add a few comments to an existing test and a few more trivial cleanups. --- lightning/src/ln/async_payments_tests.rs | 88 ++++++++++++------------ 1 file changed, 43 insertions(+), 45 deletions(-) diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index a956f2ebae2..9b0a0352698 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -11,7 +11,9 @@ use crate::blinded_path::message::{MessageContext, OffersContext}; use crate::blinded_path::payment::PaymentContext; use crate::blinded_path::payment::{AsyncBolt12OfferContext, BlindedPaymentTlvs}; use crate::chain::channelmonitor::{HTLC_FAIL_BACK_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS}; -use crate::events::{Event, HTLCHandlingFailureType, PaidBolt12Invoice, PaymentFailureReason}; +use crate::events::{ + Event, HTLCHandlingFailureType, PaidBolt12Invoice, PaymentFailureReason, PaymentPurpose, +}; use crate::ln::blinded_payment_tests::{fail_blinded_htlc_backwards, get_blinded_route_parameters}; use crate::ln::channelmanager::{PaymentId, RecipientOnionFields}; use crate::ln::functional_test_utils::*; @@ -128,6 +130,25 @@ fn create_static_invoice( (offer, static_invoice) } +fn extract_payment_hash(event: &MessageSendEvent) -> PaymentHash { + match event { + MessageSendEvent::UpdateHTLCs { ref updates, .. } => { + updates.update_add_htlcs[0].payment_hash + }, + _ => panic!(), + } +} + +fn extract_payment_preimage(event: &Event) -> PaymentPreimage { + match event { + Event::PaymentClaimable { + purpose: PaymentPurpose::Bolt12OfferPayment { payment_preimage, .. }, + .. + } => payment_preimage.unwrap(), + _ => panic!(), + } +} + #[test] fn invalid_keysend_payment_secret() { let chanmon_cfgs = create_chanmon_cfgs(3); @@ -215,6 +236,7 @@ fn static_invoice_unknown_required_features() { create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + // Manually construct a static invoice so we can set unknown required features. let blinded_paths_to_always_online_node = nodes[1] .message_router .create_blinded_paths( @@ -237,6 +259,8 @@ fn static_invoice_unknown_required_features() { .build_and_sign(&secp_ctx) .unwrap(); + // Initiate payment to the offer corresponding to the manually-constructed invoice that has + // unknown required features. let amt_msat = 5000; let payment_id = PaymentId([1; 32]); let params = RouteParametersConfig::default(); @@ -264,6 +288,8 @@ fn static_invoice_unknown_required_features() { ) .unwrap(); + // Check that paying the static invoice fails as expected with + // `PaymentFailureReason::UnknownRequiredFeatures`. let static_invoice_om = nodes[1] .onion_messenger .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) @@ -404,12 +430,6 @@ fn async_receive_flow_success() { create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); - // Set the random bytes so we can predict the payment preimage and hash. - let hardcoded_random_bytes = [42; 32]; - let keysend_preimage = PaymentPreimage(hardcoded_random_bytes); - let payment_hash: PaymentHash = keysend_preimage.into(); - *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(hardcoded_random_bytes); - let relative_expiry = Duration::from_secs(1000); let (offer, static_invoice) = create_static_invoice(&nodes[1], &nodes[2], Some(relative_expiry), &secp_ctx); @@ -433,6 +453,7 @@ fn async_receive_flow_success() { let mut events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + let payment_hash = extract_payment_hash(&ev); check_added_monitors!(nodes[0], 1); // Receiving a duplicate release_htlc message doesn't result in duplicate payment. @@ -442,9 +463,9 @@ fn async_receive_flow_success() { assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; - let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) - .with_payment_preimage(keysend_preimage); - do_pass_along_path(args); + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev); + let claimable_ev = do_pass_along_path(args).unwrap(); + let keysend_preimage = extract_payment_preimage(&claimable_ev); let res = claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage)); assert!(res.is_some()); @@ -556,9 +577,6 @@ fn async_receive_mpp() { let (offer, static_invoice) = create_static_invoice(&nodes[1], &nodes[3], None, &secp_ctx); - // In other tests we hardcode the sender's random bytes so we can predict the keysend preimage to - // check later in the test, but that doesn't work for MPP because it causes the session_privs for - // the different MPP parts to not be unique. let amt_msat = 15_000_000; let payment_id = PaymentId([1; 32]); let params = RouteParametersConfig::default(); @@ -593,8 +611,8 @@ fn async_receive_mpp() { let args = PassAlongPathArgs::new(&nodes[0], expected_route[1], amt_msat, payment_hash, ev); let claimable_ev = do_pass_along_path(args).unwrap(); let keysend_preimage = match claimable_ev { - crate::events::Event::PaymentClaimable { - purpose: crate::events::PaymentPurpose::Bolt12OfferPayment { payment_preimage, .. }, + Event::PaymentClaimable { + purpose: PaymentPurpose::Bolt12OfferPayment { payment_preimage, .. }, .. } => payment_preimage.unwrap(), _ => panic!(), @@ -643,13 +661,6 @@ fn amount_doesnt_match_invreq() { connect_blocks(&nodes[3], 4 * CHAN_CONFIRM_DEPTH + 1 - nodes[3].best_block_info().1); let (offer, static_invoice) = create_static_invoice(&nodes[1], &nodes[3], None, &secp_ctx); - - // Set the random bytes so we can predict the payment preimage and hash. - let hardcoded_random_bytes = [42; 32]; - let keysend_preimage = PaymentPreimage(hardcoded_random_bytes); - let payment_hash: PaymentHash = keysend_preimage.into(); - *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(hardcoded_random_bytes); - let amt_msat = 5000; let payment_id = PaymentId([1; 32]); let params = RouteParametersConfig::default(); @@ -696,10 +707,10 @@ fn amount_doesnt_match_invreq() { let mut ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); assert!(matches!( ev, MessageSendEvent::UpdateHTLCs { ref updates, .. } if updates.update_add_htlcs.len() == 1)); + let payment_hash = extract_payment_hash(&ev); let route: &[&[&Node]] = &[&[&nodes[1], &nodes[3]]]; let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) - .with_payment_preimage(keysend_preimage) .without_claimable_event() .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }); do_pass_along_path(args); @@ -725,9 +736,9 @@ fn amount_doesnt_match_invreq() { ev, MessageSendEvent::UpdateHTLCs { ref updates, .. } if updates.update_add_htlcs.len() == 1)); check_added_monitors!(nodes[0], 1); let route: &[&[&Node]] = &[&[&nodes[2], &nodes[3]]]; - let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) - .with_payment_preimage(keysend_preimage); - do_pass_along_path(args); + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev); + let claimable_ev = do_pass_along_path(args).unwrap(); + let keysend_preimage = extract_payment_preimage(&claimable_ev); claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage)); } @@ -882,12 +893,6 @@ fn invalid_async_receive_with_retry( .build_and_sign(&secp_ctx) .unwrap(); - // Set the random bytes so we can predict the payment preimage and hash. - let hardcoded_random_bytes = [42; 32]; - let keysend_preimage = PaymentPreimage(hardcoded_random_bytes); - let payment_hash: PaymentHash = keysend_preimage.into(); - *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(hardcoded_random_bytes); - let params = RouteParametersConfig::default(); nodes[0] .node @@ -906,10 +911,10 @@ fn invalid_async_receive_with_retry( let mut ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); assert!(matches!( ev, MessageSendEvent::UpdateHTLCs { ref updates, .. } if updates.update_add_htlcs.len() == 1)); + let payment_hash = extract_payment_hash(&ev); let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; - let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) - .with_payment_preimage(keysend_preimage); + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev); do_pass_along_path(args); // Fail the HTLC backwards to enable us to more easily modify the now-Retryable outbound to test @@ -935,7 +940,6 @@ fn invalid_async_receive_with_retry( check_added_monitors!(nodes[0], 1); let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) - .with_payment_preimage(keysend_preimage) .without_claimable_event() .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }); do_pass_along_path(args); @@ -949,9 +953,9 @@ fn invalid_async_receive_with_retry( let mut ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); check_added_monitors!(nodes[0], 1); let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; - let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) - .with_payment_preimage(keysend_preimage); - do_pass_along_path(args); + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev); + let claimable_ev = do_pass_along_path(args).unwrap(); + let keysend_preimage = extract_payment_preimage(&claimable_ev); claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage)); } @@ -1031,12 +1035,6 @@ fn expired_static_invoice_payment_path() { 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); - // Set the random bytes so we can predict the payment preimage and hash. - let hardcoded_random_bytes = [42; 32]; - let keysend_preimage = PaymentPreimage(hardcoded_random_bytes); - let payment_hash: PaymentHash = keysend_preimage.into(); - *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(hardcoded_random_bytes); - // Hardcode the blinded payment path returned by the router so we can expire it via mining blocks. let (_, static_invoice_expired_paths) = create_static_invoice(&nodes[1], &nodes[2], None, &secp_ctx); @@ -1097,11 +1095,11 @@ fn expired_static_invoice_payment_path() { let mut events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + let payment_hash = extract_payment_hash(&ev); check_added_monitors!(nodes[0], 1); let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) - .with_payment_preimage(keysend_preimage) .without_claimable_event() .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }); do_pass_along_path(args); From dcebe45fd140db47b971e6ce80892ee64854f5d5 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 27 May 2025 13:34:33 -0700 Subject: [PATCH 7/8] Adapt async payments tests for static invoice server We were manually creating the static invoice in tests, but now we can use the static invoice server protocol to interactively build the invoice. --- lightning/src/ln/async_payments_tests.rs | 412 ++++++++++++------ lightning/src/ln/functional_test_utils.rs | 1 + .../src/offers/async_receive_offer_cache.rs | 15 + lightning/src/offers/flow.rs | 8 + lightning/src/onion_message/messenger.rs | 7 +- lightning/src/util/test_utils.rs | 31 +- 6 files changed, 350 insertions(+), 124 deletions(-) diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index 9b0a0352698..ac01dc01185 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -25,12 +25,15 @@ use crate::ln::offers_tests; use crate::ln::onion_utils::LocalHTLCFailureReason; use crate::ln::outbound_payment::PendingOutboundPayment; use crate::ln::outbound_payment::Retry; +use crate::offers::flow::TEST_OFFERS_MESSAGE_REQUEST_LIMIT; use crate::offers::invoice_request::InvoiceRequest; use crate::offers::nonce::Nonce; use crate::offers::offer::Offer; use crate::offers::static_invoice::StaticInvoice; use crate::onion_message::async_payments::{AsyncPaymentsMessage, AsyncPaymentsMessageHandler}; -use crate::onion_message::messenger::{Destination, MessageRouter, MessageSendInstructions}; +use crate::onion_message::messenger::{ + Destination, MessageRouter, MessageSendInstructions, PeeledOnion, +}; use crate::onion_message::offers::OffersMessage; use crate::onion_message::packet::ParsedOnionMessageContents; use crate::prelude::*; @@ -47,6 +50,136 @@ use bitcoin::secp256k1::Secp256k1; use core::convert::Infallible; use core::time::Duration; +struct StaticInvoiceServerFlowResult { + invoice: StaticInvoice, + + // Returning messages that were sent along the way allows us to test handling duplicate messages. + offer_paths_request: msgs::OnionMessage, + static_invoice_persisted_message: msgs::OnionMessage, +} + +// Go through the flow of interactively building a `StaticInvoice`, returning the +// AsyncPaymentsMessage::ServeStaticInvoice that has yet to be provided to the server node. +// Assumes that the sender and recipient are only peers with each other. +// +// Returns (offer_paths_req, serve_static_invoice) +fn invoice_flow_up_to_send_serve_static_invoice( + server: &Node, recipient: &Node, +) -> (msgs::OnionMessage, msgs::OnionMessage) { + // First provide an OfferPathsRequest from the recipient to the server. + recipient.node.timer_tick_occurred(); + let offer_paths_req = loop { + let msg = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + // Ignore any messages that are updating the static invoice stored with the server here + if matches!( + server.onion_messenger.peel_onion_message(&msg).unwrap(), + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPathsRequest(_), _, _) + ) { + break msg; + } + }; + server.onion_messenger.handle_onion_message(recipient.node.get_our_node_id(), &offer_paths_req); + + // Check that the right number of requests were queued and that they were only queued for the + // server node. + let mut pending_oms = recipient.onion_messenger.release_pending_msgs(); + let mut offer_paths_req_msgs = pending_oms.remove(&server.node.get_our_node_id()).unwrap(); + assert!(offer_paths_req_msgs.len() <= TEST_OFFERS_MESSAGE_REQUEST_LIMIT); + for (_, msgs) in pending_oms { + assert!(msgs.is_empty()); + } + + // The server responds with OfferPaths. + let offer_paths = server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .unwrap(); + recipient.onion_messenger.handle_onion_message(server.node.get_our_node_id(), &offer_paths); + + // Only one OfferPaths response should be queued. + let mut pending_oms = server.onion_messenger.release_pending_msgs(); + for (_, msgs) in pending_oms { + assert!(msgs.is_empty()); + } + + // After receiving the offer paths, the recipient constructs the static invoice and sends + // ServeStaticInvoice to the server. + let serve_static_invoice_om = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + (offer_paths_req, serve_static_invoice_om) +} + +// Go through the flow of interactively building a `StaticInvoice` and storing it with the static +// invoice server, returning the invoice and messages that were exchanged along the way at the end. +fn pass_static_invoice_server_messages( + server: &Node, recipient: &Node, recipient_id: Vec, +) -> StaticInvoiceServerFlowResult { + // Force the server and recipient to send OMs directly to each other for testing simplicity. + server.message_router.peers_override.lock().unwrap().push(recipient.node.get_our_node_id()); + recipient.message_router.peers_override.lock().unwrap().push(server.node.get_our_node_id()); + + let num_cached_offers_before_flow = recipient.node.flow.test_get_async_receive_offers().len(); + + let (offer_paths_req, serve_static_invoice_om) = + invoice_flow_up_to_send_serve_static_invoice(server, recipient); + server + .onion_messenger + .handle_onion_message(recipient.node.get_our_node_id(), &serve_static_invoice_om); + + // Upon handling the ServeStaticInvoice message, the server's node surfaces an event indicating + // that the static invoice should be persisted. + let mut events = server.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let (static_invoice, ack_path) = match events.pop().unwrap() { + Event::PersistStaticInvoice { + invoice, + invoice_persisted_path, + recipient_id: ev_id, + invoice_slot: _, // TODO: do something with this? + invoice_id: _, // TODO: do something with this? + } => { + assert_eq!(recipient_id, ev_id); + (invoice, invoice_persisted_path) + }, + _ => panic!(), + }; + assert_eq!( + recipient.node.flow.test_get_async_receive_offers().len(), + num_cached_offers_before_flow + ); + + // Once the static invoice is persisted, the server needs to call `static_invoice_persisted` with + // the reply path to the ServeStaticInvoice message, to tell the recipient that their offer is + // ready to be used for async payments. + server.node.static_invoice_persisted(ack_path); + let invoice_persisted_om = server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .unwrap(); + recipient + .onion_messenger + .handle_onion_message(server.node.get_our_node_id(), &invoice_persisted_om); + assert_eq!( + recipient.node.flow.test_get_async_receive_offers().len(), + num_cached_offers_before_flow + 1 + ); + + // Remove the peer restriction added above. + server.message_router.peers_override.lock().unwrap().clear(); + recipient.message_router.peers_override.lock().unwrap().clear(); + + StaticInvoiceServerFlowResult { + offer_paths_request: offer_paths_req, + static_invoice_persisted_message: invoice_persisted_om, + invoice: static_invoice, + } +} + // Goes through the async receive onion message flow, returning the final release_held_htlc OM. // // Assumes the held_htlc_available message will be sent: @@ -55,28 +188,30 @@ use core::time::Duration; // Returns: (held_htlc_available_om, release_held_htlc_om) fn pass_async_payments_oms( static_invoice: StaticInvoice, sender: &Node, always_online_recipient_counterparty: &Node, - recipient: &Node, + recipient: &Node, recipient_id: Vec, ) -> (msgs::OnionMessage, msgs::OnionMessage) { let sender_node_id = sender.node.get_our_node_id(); let always_online_node_id = always_online_recipient_counterparty.node.get_our_node_id(); - // Don't forward the invreq since we don't support retrieving the static invoice from the - // recipient's LSP yet, instead manually construct the response. let invreq_om = sender.onion_messenger.next_onion_message_for_peer(always_online_node_id).unwrap(); - let invreq_reply_path = - offers_tests::extract_invoice_request(always_online_recipient_counterparty, &invreq_om).1; - always_online_recipient_counterparty .onion_messenger - .send_onion_message( - ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( - static_invoice, - )), - MessageSendInstructions::WithoutReplyPath { - destination: Destination::BlindedPath(invreq_reply_path), - }, - ) + .handle_onion_message(sender_node_id, &invreq_om); + + let mut events = always_online_recipient_counterparty.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let reply_path = match events.pop().unwrap() { + Event::StaticInvoiceRequested { recipient_id: ev_id, invoice_id: _, reply_path } => { + assert_eq!(recipient_id, ev_id); + reply_path + }, + _ => panic!(), + }; + + always_online_recipient_counterparty + .node + .send_static_invoice(static_invoice, reply_path) .unwrap(); let static_invoice_om = always_online_recipient_counterparty .onion_messenger @@ -97,10 +232,9 @@ fn pass_async_payments_oms( .onion_messenger .handle_onion_message(always_online_node_id, &held_htlc_available_om_1_2); - ( - held_htlc_available_om_1_2, - recipient.onion_messenger.next_onion_message_for_peer(sender_node_id).unwrap(), - ) + let release_held_htlc = + recipient.onion_messenger.next_onion_message_for_peer(sender_node_id).unwrap(); + (held_htlc_available_om_1_2, release_held_htlc) } fn create_static_invoice( @@ -269,8 +403,8 @@ fn static_invoice_unknown_required_features() { .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), params) .unwrap(); - // Don't forward the invreq since we don't support retrieving the static invoice from the - // recipient's LSP yet, instead manually construct the response. + // Don't forward the invreq since the invoice was created outside of the normal flow, instead + // manually construct the response. let invreq_om = nodes[0] .onion_messenger .next_onion_message_for_peer(nodes[1].node.get_our_node_id()) @@ -313,7 +447,6 @@ fn static_invoice_unknown_required_features() { fn ignore_unexpected_static_invoice() { // Test that we'll ignore unexpected static invoices, invoices that don't match our invoice // request, and duplicate invoices. - let secp_ctx = Secp256k1::new(); let chanmon_cfgs = create_chanmon_cfgs(3); let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); @@ -321,9 +454,21 @@ fn ignore_unexpected_static_invoice() { create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + let recipient_id = vec![42; 32]; + let inv_server_paths = + nodes[1].node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + nodes[2].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + // Initiate payment to the sender's intended offer. - let (offer, valid_static_invoice) = - create_static_invoice(&nodes[1], &nodes[2], None, &secp_ctx); + let valid_static_invoice = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()).invoice; + let offer = nodes[2].node.get_async_receive_offer().unwrap(); + + // Create a static invoice to be sent over the reply path containing the original payment_id, but + // the static invoice corresponds to a different offer than was originally paid. + let unexpected_static_invoice = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()).invoice; + let amt_msat = 5000; let payment_id = PaymentId([1; 32]); let params = RouteParametersConfig::default(); @@ -332,30 +477,24 @@ fn ignore_unexpected_static_invoice() { .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), params) .unwrap(); - // Don't forward the invreq since we don't support retrieving the static invoice from the - // recipient's LSP yet, instead manually construct the responses below. let invreq_om = nodes[0] .onion_messenger .next_onion_message_for_peer(nodes[1].node.get_our_node_id()) .unwrap(); - let invreq_reply_path = offers_tests::extract_invoice_request(&nodes[1], &invreq_om).1; + nodes[1].onion_messenger.handle_onion_message(nodes[0].node.get_our_node_id(), &invreq_om); - // Create a static invoice to be sent over the reply path containing the original payment_id, but - // the static invoice corresponds to a different offer than was originally paid. - let unexpected_static_invoice = create_static_invoice(&nodes[1], &nodes[2], None, &secp_ctx).1; + let mut events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let reply_path = match events.pop().unwrap() { + Event::StaticInvoiceRequested { recipient_id: ev_id, invoice_id: _, reply_path } => { + assert_eq!(recipient_id, ev_id); + reply_path + }, + _ => panic!(), + }; - // Check that we'll ignore the unexpected static invoice. - nodes[1] - .onion_messenger - .send_onion_message( - ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( - unexpected_static_invoice, - )), - MessageSendInstructions::WithoutReplyPath { - destination: Destination::BlindedPath(invreq_reply_path.clone()), - }, - ) - .unwrap(); + // Check that the sender will ignore the unexpected static invoice. + nodes[1].node.send_static_invoice(unexpected_static_invoice, reply_path.clone()).unwrap(); let unexpected_static_invoice_om = nodes[1] .onion_messenger .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) @@ -369,17 +508,7 @@ fn ignore_unexpected_static_invoice() { // A valid static invoice corresponding to the correct offer will succeed and cause us to send a // held_htlc_available onion message. - nodes[1] - .onion_messenger - .send_onion_message( - ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( - valid_static_invoice.clone(), - )), - MessageSendInstructions::WithoutReplyPath { - destination: Destination::BlindedPath(invreq_reply_path.clone()), - }, - ) - .unwrap(); + nodes[1].node.send_static_invoice(valid_static_invoice.clone(), reply_path.clone()).unwrap(); let static_invoice_om = nodes[1] .onion_messenger .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) @@ -394,17 +523,7 @@ fn ignore_unexpected_static_invoice() { .all(|(msg, _)| matches!(msg, AsyncPaymentsMessage::HeldHtlcAvailable(_)))); // Receiving a duplicate invoice will have no effect. - nodes[1] - .onion_messenger - .send_onion_message( - ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( - valid_static_invoice, - )), - MessageSendInstructions::WithoutReplyPath { - destination: Destination::BlindedPath(invreq_reply_path), - }, - ) - .unwrap(); + nodes[1].node.send_static_invoice(valid_static_invoice, reply_path).unwrap(); let dup_static_invoice_om = nodes[1] .onion_messenger .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) @@ -419,23 +538,29 @@ fn ignore_unexpected_static_invoice() { #[test] fn async_receive_flow_success() { // Test that an always-online sender can successfully pay an async receiver. - let secp_ctx = Secp256k1::new(); + let chanmon_cfgs = create_chanmon_cfgs(3); let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let mut allow_priv_chan_fwds_cfg = test_default_channel_config(); allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg), None]); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); - let relative_expiry = Duration::from_secs(1000); - let (offer, static_invoice) = - create_static_invoice(&nodes[1], &nodes[2], Some(relative_expiry), &secp_ctx); - assert!(static_invoice.invoice_features().supports_basic_mpp()); - assert_eq!(static_invoice.relative_expiry(), relative_expiry); + let recipient_id = vec![42; 32]; + let inv_server_paths = + nodes[1].node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + nodes[2].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + let invoice_flow_res = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()); + let static_invoice = invoice_flow_res.invoice; + assert!(static_invoice.invoice_features().supports_basic_mpp()); + let offer = nodes[2].node.get_async_receive_offer().unwrap(); let amt_msat = 5000; let payment_id = PaymentId([1; 32]); let params = RouteParametersConfig::default(); @@ -443,8 +568,14 @@ fn async_receive_flow_success() { .node .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), params) .unwrap(); - let release_held_htlc_om = - pass_async_payments_oms(static_invoice.clone(), &nodes[0], &nodes[1], &nodes[2]).1; + let release_held_htlc_om = pass_async_payments_oms( + static_invoice.clone(), + &nodes[0], + &nodes[1], + &nodes[2], + recipient_id, + ) + .1; nodes[0] .onion_messenger .handle_onion_message(nodes[2].node.get_our_node_id(), &release_held_htlc_om); @@ -468,7 +599,6 @@ fn async_receive_flow_success() { let keysend_preimage = extract_payment_preimage(&claimable_ev); let res = claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage)); - assert!(res.is_some()); assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice))); } @@ -476,7 +606,6 @@ fn async_receive_flow_success() { #[test] fn expired_static_invoice_fail() { // Test that if we receive an expired static invoice we'll fail the payment. - let secp_ctx = Secp256k1::new(); let chanmon_cfgs = create_chanmon_cfgs(3); let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); @@ -484,10 +613,14 @@ fn expired_static_invoice_fail() { create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); - const INVOICE_EXPIRY_SECS: u32 = 10; - let relative_expiry = Duration::from_secs(INVOICE_EXPIRY_SECS as u64); - let (offer, static_invoice) = - create_static_invoice(&nodes[1], &nodes[2], Some(relative_expiry), &secp_ctx); + let recipient_id = vec![42; 32]; + let inv_server_paths = + nodes[1].node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + nodes[2].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + let static_invoice = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()).invoice; + let offer = nodes[2].node.get_async_receive_offer().unwrap(); let amt_msat = 5000; let payment_id = PaymentId([1; 32]); @@ -501,20 +634,16 @@ fn expired_static_invoice_fail() { .onion_messenger .next_onion_message_for_peer(nodes[1].node.get_our_node_id()) .unwrap(); - let invreq_reply_path = offers_tests::extract_invoice_request(&nodes[1], &invreq_om).1; - // TODO: update to not manually send here when we add support for being the recipient's - // always-online counterparty - nodes[1] - .onion_messenger - .send_onion_message( - ParsedOnionMessageContents::::Offers(OffersMessage::StaticInvoice( - static_invoice, - )), - MessageSendInstructions::WithoutReplyPath { - destination: Destination::BlindedPath(invreq_reply_path), - }, - ) - .unwrap(); + nodes[1].onion_messenger.handle_onion_message(nodes[0].node.get_our_node_id(), &invreq_om); + + let mut events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let reply_path = match events.pop().unwrap() { + Event::StaticInvoiceRequested { reply_path, .. } => reply_path, + _ => panic!(), + }; + + nodes[1].node.send_static_invoice(static_invoice.clone(), reply_path).unwrap(); let static_invoice_om = nodes[1] .onion_messenger .next_onion_message_for_peer(nodes[0].node.get_our_node_id()) @@ -523,7 +652,7 @@ fn expired_static_invoice_fail() { // Wait until the static invoice expires before providing it to the sender. let block = create_dummy_block( nodes[0].best_block_hash(), - nodes[0].node.duration_since_epoch().as_secs() as u32 + INVOICE_EXPIRY_SECS + 1, + (static_invoice.created_at() + static_invoice.relative_expiry()).as_secs() as u32 + 1u32, Vec::new(), ); connect_block(&nodes[0], &block); @@ -540,17 +669,18 @@ fn expired_static_invoice_fail() { }, _ => panic!(), } - // The sender doesn't reply with InvoiceError right now because the always-online node doesn't - // currently provide them with a reply path to do so. + // TODO: the sender doesn't reply with InvoiceError right now because the always-online node + // doesn't currently provide them with a reply path to do so. } #[test] fn async_receive_mpp() { - let secp_ctx = Secp256k1::new(); let chanmon_cfgs = create_chanmon_cfgs(4); let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); + let mut allow_priv_chan_fwds_cfg = test_default_channel_config(); allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; + let node_chanmgrs = create_node_chanmgrs( 4, &node_cfgs, @@ -575,7 +705,14 @@ fn async_receive_mpp() { connect_blocks(&nodes[2], 4 * CHAN_CONFIRM_DEPTH + 1 - nodes[2].best_block_info().1); connect_blocks(&nodes[3], 4 * CHAN_CONFIRM_DEPTH + 1 - nodes[3].best_block_info().1); - let (offer, static_invoice) = create_static_invoice(&nodes[1], &nodes[3], None, &secp_ctx); + let recipient_id = vec![42; 32]; + let inv_server_paths = + nodes[1].node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + nodes[3].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + let static_invoice = + pass_static_invoice_server_messages(&nodes[1], &nodes[3], recipient_id.clone()).invoice; + let offer = nodes[3].node.get_async_receive_offer().unwrap(); let amt_msat = 15_000_000; let payment_id = PaymentId([1; 32]); @@ -585,7 +722,7 @@ fn async_receive_mpp() { .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(1), params) .unwrap(); let release_held_htlc_om_3_0 = - pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[3]).1; + pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[3], recipient_id).1; nodes[0] .onion_messenger .handle_onion_message(nodes[3].node.get_our_node_id(), &release_held_htlc_om_3_0); @@ -631,6 +768,7 @@ fn amount_doesnt_match_invreq() { let secp_ctx = Secp256k1::new(); let chanmon_cfgs = create_chanmon_cfgs(4); let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); + let mut allow_priv_chan_fwds_cfg = test_default_channel_config(); allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; // Make one blinded path's fees slightly higher so they are tried in a deterministic order. @@ -660,7 +798,15 @@ fn amount_doesnt_match_invreq() { connect_blocks(&nodes[2], 4 * CHAN_CONFIRM_DEPTH + 1 - nodes[2].best_block_info().1); connect_blocks(&nodes[3], 4 * CHAN_CONFIRM_DEPTH + 1 - nodes[3].best_block_info().1); - let (offer, static_invoice) = create_static_invoice(&nodes[1], &nodes[3], None, &secp_ctx); + let recipient_id = vec![42; 32]; + let inv_server_paths = + nodes[1].node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + nodes[3].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + let static_invoice = + pass_static_invoice_server_messages(&nodes[1], &nodes[3], recipient_id.clone()).invoice; + let offer = nodes[3].node.get_async_receive_offer().unwrap(); + let amt_msat = 5000; let payment_id = PaymentId([1; 32]); let params = RouteParametersConfig::default(); @@ -669,7 +815,7 @@ fn amount_doesnt_match_invreq() { .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(1), params) .unwrap(); let release_held_htlc_om_3_0 = - pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[3]).1; + pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[3], recipient_id).1; // Replace the invoice request contained within outbound_payments before sending so the invreq // amount doesn't match the onion amount when the HTLC gets to the recipient. @@ -841,15 +987,25 @@ fn invalid_async_receive_with_retry( let secp_ctx = Secp256k1::new(); let chanmon_cfgs = create_chanmon_cfgs(3); let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let mut allow_priv_chan_fwds_cfg = test_default_channel_config(); allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg), None]); - let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + let recipient_id = vec![42; 32]; + let inv_server_paths = + nodes[1].node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + nodes[2].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + // Set the random bytes so we can predict the offer nonce. + let hardcoded_random_bytes = [42; 32]; + *nodes[2].keys_manager.override_random_bytes.lock().unwrap() = Some(hardcoded_random_bytes); + // Ensure all nodes start at the same height. connect_blocks(&nodes[0], 2 * CHAN_CONFIRM_DEPTH + 1 - nodes[0].best_block_info().1); connect_blocks(&nodes[1], 2 * CHAN_CONFIRM_DEPTH + 1 - nodes[1].best_block_info().1); @@ -886,12 +1042,9 @@ fn invalid_async_receive_with_retry( } nodes[2].router.expect_blinded_payment_paths(static_invoice_paths); - let static_invoice = nodes[2] - .node - .create_static_invoice_builder(&offer, offer_nonce, None) - .unwrap() - .build_and_sign(&secp_ctx) - .unwrap(); + let static_invoice = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()).invoice; + let offer = nodes[2].node.get_async_receive_offer().unwrap(); let params = RouteParametersConfig::default(); nodes[0] @@ -899,7 +1052,7 @@ fn invalid_async_receive_with_retry( .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(2), params) .unwrap(); let release_held_htlc_om_2_0 = - pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[2]).1; + pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[2], recipient_id).1; nodes[0] .onion_messenger .handle_onion_message(nodes[2].node.get_our_node_id(), &release_held_htlc_om_2_0); @@ -959,12 +1112,11 @@ fn invalid_async_receive_with_retry( claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage)); } -#[cfg(not(feature = "std"))] +#[cfg_attr(feature = "std", ignore)] #[test] fn expired_static_invoice_message_path() { // Test that if we receive a held_htlc_available message over an expired blinded path, we'll // ignore it. - let secp_ctx = Secp256k1::new(); let chanmon_cfgs = create_chanmon_cfgs(3); let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); @@ -972,13 +1124,14 @@ fn expired_static_invoice_message_path() { create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); - const INVOICE_EXPIRY_SECS: u32 = 10; - let (offer, static_invoice) = create_static_invoice( - &nodes[1], - &nodes[2], - Some(Duration::from_secs(INVOICE_EXPIRY_SECS as u64)), - &secp_ctx, - ); + let recipient_id = vec![42; 32]; + let inv_server_paths = + nodes[1].node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + nodes[2].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + let static_invoice = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()).invoice; + let offer = nodes[2].node.get_async_receive_offer().unwrap(); let amt_msat = 5000; let payment_id = PaymentId([1; 32]); @@ -989,13 +1142,18 @@ fn expired_static_invoice_message_path() { .unwrap(); // While the invoice is unexpired, respond with release_held_htlc. - let (held_htlc_available_om, _release_held_htlc_om) = - pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[2]); + let (held_htlc_available_om, _release_held_htlc_om) = pass_async_payments_oms( + static_invoice.clone(), + &nodes[0], + &nodes[1], + &nodes[2], + recipient_id, + ); // After the invoice is expired, ignore inbound held_htlc_available messages over the path. let path_absolute_expiry = crate::ln::inbound_payment::calculate_absolute_expiry( nodes[2].node.duration_since_epoch().as_secs(), - INVOICE_EXPIRY_SECS, + static_invoice.relative_expiry().as_secs() as u32, ); let block = create_dummy_block( nodes[2].best_block_hash(), @@ -1020,14 +1178,21 @@ fn expired_static_invoice_payment_path() { let secp_ctx = Secp256k1::new(); let chanmon_cfgs = create_chanmon_cfgs(3); let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let mut allow_priv_chan_fwds_cfg = test_default_channel_config(); allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg), None]); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + let recipient_id = vec![42; 32]; + let inv_server_paths = + nodes[1].node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + nodes[2].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + // Make sure all nodes are at the same block height in preparation for CLTV timeout things. let node_max_height = nodes.iter().map(|node| node.blocks.lock().unwrap().len()).max().unwrap() as u32; @@ -1078,7 +1243,10 @@ fn expired_static_invoice_payment_path() { ); connect_blocks(&nodes[2], final_max_cltv_expiry - nodes[2].best_block_info().1); - let (offer, static_invoice) = create_static_invoice(&nodes[1], &nodes[2], None, &secp_ctx); + let static_invoice = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()).invoice; + let offer = nodes[2].node.get_async_receive_offer().unwrap(); + let amt_msat = 5000; let payment_id = PaymentId([1; 32]); let params = RouteParametersConfig::default(); @@ -1087,7 +1255,7 @@ fn expired_static_invoice_payment_path() { .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), params) .unwrap(); let release_held_htlc_om = - pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[2]).1; + pass_async_payments_oms(static_invoice, &nodes[0], &nodes[1], &nodes[2], recipient_id).1; nodes[0] .onion_messenger .handle_onion_message(nodes[2].node.get_our_node_id(), &release_held_htlc_om); diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 245479e1df8..4761420d9dc 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -1331,6 +1331,7 @@ macro_rules! reload_node { _reload_node(&$node, $new_config, &chanman_encoded, $monitors_encoded); $node.node = &$new_channelmanager; $node.onion_messenger.set_offers_handler(&$new_channelmanager); + $node.onion_messenger.set_async_payments_handler(&$new_channelmanager); }; ($node: expr, $chanman_encoded: expr, $monitors_encoded: expr, $persister: ident, $new_chain_monitor: ident, $new_channelmanager: ident) => { reload_node!( diff --git a/lightning/src/offers/async_receive_offer_cache.rs b/lightning/src/offers/async_receive_offer_cache.rs index dd10fb099c2..4203408326d 100644 --- a/lightning/src/offers/async_receive_offer_cache.rs +++ b/lightning/src/offers/async_receive_offer_cache.rs @@ -461,6 +461,21 @@ impl AsyncReceiveOfferCache { false } + + #[cfg(test)] + pub(super) fn test_get_payable_offers(&self) -> Vec { + self.offers_with_idx() + .filter_map(|(_, offer)| { + if matches!(offer.status, OfferStatus::Ready { .. }) + || matches!(offer.status, OfferStatus::Used) + { + Some(offer.offer.clone()) + } else { + None + } + }) + .collect() + } } impl Writeable for AsyncReceiveOfferCache { diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 99724a3996f..39c1a5bb319 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -242,6 +242,9 @@ where /// even if multiple invoices are received. const OFFERS_MESSAGE_REQUEST_LIMIT: usize = 10; +#[cfg(all(async_payments, test))] +pub(crate) const TEST_OFFERS_MESSAGE_REQUEST_LIMIT: usize = OFFERS_MESSAGE_REQUEST_LIMIT; + /// The default relative expiry for reply paths where a quick response is expected and the reply /// path is single-use. #[cfg(async_payments)] @@ -1225,6 +1228,11 @@ where cache.get_async_receive_offer(self.duration_since_epoch()) } + #[cfg(all(test, async_payments))] + pub(crate) fn test_get_async_receive_offers(&self) -> Vec { + self.async_receive_offer_cache.lock().unwrap().test_get_payable_offers() + } + /// Sends out [`OfferPathsRequest`] and [`ServeStaticInvoice`] onion messages if we are an /// often-offline recipient and are configured to interactively build offers and static invoices /// with a static invoice server. diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 3b2566119de..f5762b04310 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -1324,6 +1324,11 @@ where self.offers_handler = offers_handler; } + #[cfg(any(test, feature = "_test_utils"))] + pub fn set_async_payments_handler(&mut self, async_payments_handler: APH) { + self.async_payments_handler = async_payments_handler; + } + /// Sends an [`OnionMessage`] based on its [`MessageSendInstructions`]. pub fn send_onion_message( &self, contents: T, instructions: MessageSendInstructions, @@ -1538,7 +1543,7 @@ where } #[cfg(test)] - pub(super) fn release_pending_msgs(&self) -> HashMap> { + pub(crate) fn release_pending_msgs(&self) -> HashMap> { let mut message_recipients = self.message_recipients.lock().unwrap(); let mut msgs = new_hash_map(); // We don't want to disconnect the peers by removing them entirely from the original map, so we diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index fecdb830fe0..e0869bf4364 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -317,13 +317,17 @@ pub struct TestMessageRouter<'a> { &'a TestLogger, &'a TestKeysInterface, >, + pub peers_override: Mutex>, } impl<'a> TestMessageRouter<'a> { pub fn new( network_graph: Arc>, entropy_source: &'a TestKeysInterface, ) -> Self { - Self { inner: DefaultMessageRouter::new(network_graph, entropy_source) } + Self { + inner: DefaultMessageRouter::new(network_graph, entropy_source), + peers_override: Mutex::new(Vec::new()), + } } } @@ -331,6 +335,13 @@ impl<'a> MessageRouter for TestMessageRouter<'a> { fn find_path( &self, sender: PublicKey, peers: Vec, destination: Destination, ) -> Result { + let mut peers = peers; + { + let peers_override = self.peers_override.lock().unwrap(); + if !peers_override.is_empty() { + peers = peers_override.clone(); + } + } self.inner.find_path(sender, peers, destination) } @@ -338,6 +349,13 @@ impl<'a> MessageRouter for TestMessageRouter<'a> { &self, recipient: PublicKey, context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { + let mut peers = peers; + { + let peers_override = self.peers_override.lock().unwrap(); + if !peers_override.is_empty() { + peers = peers_override.clone(); + } + } self.inner.create_blinded_paths(recipient, context, peers, secp_ctx) } @@ -345,6 +363,17 @@ impl<'a> MessageRouter for TestMessageRouter<'a> { &self, recipient: PublicKey, context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { + let mut peers = peers; + { + let peers_override = self.peers_override.lock().unwrap(); + if !peers_override.is_empty() { + peers = peers_override + .clone() + .iter() + .map(|pk| MessageForwardNode { node_id: *pk, short_channel_id: None }) + .collect(); + } + } self.inner.create_compact_blinded_paths(recipient, context, peers, secp_ctx) } } From 685a364becda604bb495426e2b780aa3bf709437 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Mon, 30 Jun 2025 18:43:30 -0400 Subject: [PATCH 8/8] Test static invoice server protocol --- lightning/src/ln/async_payments_tests.rs | 639 +++++++++++++++++- lightning/src/ln/channelmanager.rs | 5 + .../src/offers/async_receive_offer_cache.rs | 8 + lightning/src/offers/flow.rs | 9 +- 4 files changed, 659 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index ac01dc01185..c9d799fa241 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -25,7 +25,14 @@ use crate::ln::offers_tests; use crate::ln::onion_utils::LocalHTLCFailureReason; use crate::ln::outbound_payment::PendingOutboundPayment; use crate::ln::outbound_payment::Retry; -use crate::offers::flow::TEST_OFFERS_MESSAGE_REQUEST_LIMIT; +use crate::offers::async_receive_offer_cache::{ + TEST_MAX_CACHED_OFFERS_TARGET, TEST_MAX_UPDATE_ATTEMPTS, + TEST_MIN_OFFER_PATHS_RELATIVE_EXPIRY_SECS, +}; +use crate::offers::flow::{ + TEST_DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY, TEST_OFFERS_MESSAGE_REQUEST_LIMIT, + TEST_TEMP_REPLY_PATH_RELATIVE_EXPIRY, +}; use crate::offers::invoice_request::InvoiceRequest; use crate::offers::nonce::Nonce; use crate::offers::offer::Offer; @@ -42,6 +49,7 @@ use crate::sign::NodeSigner; use crate::sync::Mutex; use crate::types::features::Bolt12InvoiceFeatures; use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; +use crate::util::ser::Writeable; use bitcoin::constants::ChainHash; use bitcoin::network::Network; use bitcoin::secp256k1; @@ -1278,3 +1286,632 @@ fn expired_static_invoice_payment_path() { 1, ); } + +#[cfg_attr(feature = "std", ignore)] +#[test] +fn ignore_expired_offer_paths_request() { + // Ignore an incoming `OfferPathsRequest` if it is sent over a blinded path that is expired. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let server = &nodes[0]; + let recipient = &nodes[1]; + + const OFFER_PATHS_REQ_RELATIVE_EXPIRY: Duration = Duration::from_secs(60 * 60); + let recipient_id = vec![42; 32]; + let inv_server_paths = server + .node + .blinded_paths_for_async_recipient(recipient_id, Some(OFFER_PATHS_REQ_RELATIVE_EXPIRY)) + .unwrap(); + recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + // Retrieve the offer paths request, and check that before the path that the recipient was + // configured with expires the server will respond to it, and after the config path expires they + // won't. + recipient.node.timer_tick_occurred(); + let offer_paths_req = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + assert!(matches!( + server.onion_messenger.peel_onion_message(&offer_paths_req).unwrap(), + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPathsRequest(_), _, _) + )); + recipient.onion_messenger.release_pending_msgs(); // Ignore redundant paths requests + + // Prior to the config path expiry the server will respond with offer_paths: + server.onion_messenger.handle_onion_message(recipient.node.get_our_node_id(), &offer_paths_req); + let offer_paths = server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .unwrap(); + assert!(matches!( + recipient.onion_messenger.peel_onion_message(&offer_paths).unwrap(), + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPaths(_), _, _) + )); + server.onion_messenger.release_pending_msgs(); // Ignore redundant offer_paths + + // After the config path expiry the offer paths request will be ignored: + let configured_path_absolute_expiry = + (server.node.duration_since_epoch() + OFFER_PATHS_REQ_RELATIVE_EXPIRY).as_secs() as u32; + let block = create_dummy_block( + server.best_block_hash(), + configured_path_absolute_expiry + 1u32, + Vec::new(), + ); + connect_block(&server, &block); + connect_block(&recipient, &block); + + server.onion_messenger.handle_onion_message(recipient.node.get_our_node_id(), &offer_paths_req); + assert!(server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .is_none()); +} + +#[cfg_attr(feature = "std", ignore)] +#[test] +fn ignore_expired_offer_paths_message() { + // If the recipient receives an offer_paths message over an expired reply path, it should be ignored. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let server = &nodes[0]; + let recipient = &nodes[1]; + + let recipient_id = vec![42; 32]; + let inv_server_paths = + server.node.blinded_paths_for_async_recipient(recipient_id, None).unwrap(); + recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + // First retrieve the offer_paths_request and corresponding offer_paths response from the server. + recipient.node.timer_tick_occurred(); + let offer_paths_req = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + recipient.onion_messenger.release_pending_msgs(); // Ignore redundant paths requests + server.onion_messenger.handle_onion_message(recipient.node.get_our_node_id(), &offer_paths_req); + let offer_paths = server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .unwrap(); + assert!(matches!( + recipient.onion_messenger.peel_onion_message(&offer_paths).unwrap(), + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPaths(_), _, _) + )); + + // Prior to expiry of the offer_paths_request reply path, the recipient will respond to + // offer_paths with serve_static_invoice. + recipient.onion_messenger.handle_onion_message(server.node.get_our_node_id(), &offer_paths); + let serve_static_invoice = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + assert!(matches!( + server.onion_messenger.peel_onion_message(&serve_static_invoice).unwrap(), + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::ServeStaticInvoice(_), _, _) + )); + + // Manually advance time for the recipient so they will perceive the offer_paths message as being + // sent over an expired reply path, and not respond with serve_static_invoice. + let offer_paths_request_reply_path_exp = + (recipient.node.duration_since_epoch() + TEST_TEMP_REPLY_PATH_RELATIVE_EXPIRY).as_secs(); + let block = create_dummy_block( + recipient.best_block_hash(), + offer_paths_request_reply_path_exp as u32 + 1u32, + Vec::new(), + ); + connect_block(&recipient, &block); + + recipient.onion_messenger.handle_onion_message(server.node.get_our_node_id(), &offer_paths); + assert!(recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .is_none()); +} + +#[cfg_attr(feature = "std", ignore)] +#[test] +fn ignore_expired_serve_static_invoice_message() { + // If the server receives a serve_static_invoice message over an expired reply path, it should be + // ignored. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let server = &nodes[0]; + let recipient = &nodes[1]; + + let recipient_id = vec![42; 32]; + let inv_server_paths = + server.node.blinded_paths_for_async_recipient(recipient_id, None).unwrap(); + recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + // First retrieve the serve_static_invoice message. + recipient.node.timer_tick_occurred(); + let serve_static_invoice = invoice_flow_up_to_send_serve_static_invoice(server, recipient).1; + + // Manually advance time for the server so they will perceive the serve_static_invoice message as being + // sent over an expired reply path, and not respond with serve_static_invoice. + let block = create_dummy_block( + server.best_block_hash(), + (server.node.duration_since_epoch() + TEST_DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY).as_secs() + as u32 + 1u32, + Vec::new(), + ); + connect_block(&server, &block); + + server + .onion_messenger + .handle_onion_message(recipient.node.get_our_node_id(), &serve_static_invoice); + assert!(server.node.get_and_clear_pending_events().is_empty()); + assert!(server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .is_none()); +} + +#[cfg_attr(feature = "std", ignore)] +#[test] +fn ignore_expired_static_invoice_persisted_message() { + // If the recipient receives a static_invoice_persisted message over an expired reply path, it + // should be ignored. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let server = &nodes[0]; + let recipient = &nodes[1]; + + let recipient_id = vec![42; 32]; + let inv_server_paths = + server.node.blinded_paths_for_async_recipient(recipient_id, None).unwrap(); + recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + // Exchange messages until we can extract the final static_invoice_persisted OM. + recipient.node.timer_tick_occurred(); + let serve_static_invoice = invoice_flow_up_to_send_serve_static_invoice(server, recipient).1; + server + .onion_messenger + .handle_onion_message(recipient.node.get_our_node_id(), &serve_static_invoice); + let mut events = server.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let ack_path = match events.pop().unwrap() { + Event::PersistStaticInvoice { invoice_persisted_path, .. } => invoice_persisted_path, + _ => panic!(), + }; + + server.node.static_invoice_persisted(ack_path); + let invoice_persisted = server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .unwrap(); + assert!(matches!( + recipient.onion_messenger.peel_onion_message(&invoice_persisted).unwrap(), + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::StaticInvoicePersisted(_), _, _) + )); + + let block = create_dummy_block( + recipient.best_block_hash(), + (recipient.node.duration_since_epoch() + TEST_TEMP_REPLY_PATH_RELATIVE_EXPIRY).as_secs() + as u32 + 1u32, + Vec::new(), + ); + connect_block(&server, &block); + connect_block(&recipient, &block); + recipient + .onion_messenger + .handle_onion_message(server.node.get_our_node_id(), &invoice_persisted); + assert!(recipient.node.get_async_receive_offer().is_err()); +} + +#[test] +fn limit_offer_paths_requests() { + // Limit the number of offer_paths_requests sent to the server if they aren't responding. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let server = &nodes[0]; + let recipient = &nodes[1]; + + let recipient_id = vec![42; 32]; + let inv_server_paths = + server.node.blinded_paths_for_async_recipient(recipient_id, None).unwrap(); + recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + // Up to TEST_MAX_UPDATE_ATTEMPTS offer_paths_requests are allowed to be sent out before the async + // recipient should give up. + for _ in 0..TEST_MAX_UPDATE_ATTEMPTS { + recipient.node.test_check_refresh_async_receive_offers(); + let offer_paths_req = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + assert!(matches!( + server.onion_messenger.peel_onion_message(&offer_paths_req).unwrap(), + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPathsRequest(_), _, _) + )); + recipient.onion_messenger.release_pending_msgs(); // Ignore redundant paths requests + } + + // After the recipient runs out of attempts to request offer paths, they will give up until the + // next timer tick. + recipient.node.test_check_refresh_async_receive_offers(); + assert!(recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .is_none()); + + // On the next timer tick, more offer paths requests should be allowed to go through. + recipient.node.timer_tick_occurred(); + let offer_paths_req = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + assert!(matches!( + server.onion_messenger.peel_onion_message(&offer_paths_req).unwrap(), + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPathsRequest(_), _, _) + )); +} + +#[test] +fn limit_serve_static_invoice_requests() { + // If we have enough async receive offers cached already, the recipient should stop sending out + // offer_paths_requests. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let persister; + let chain_monitor; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let payee_node_deserialized; + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let chan_id = + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0).0.channel_id; + let server = &nodes[0]; + let recipient = &nodes[1]; + + let recipient_id = vec![42; 32]; + let inv_server_paths = + server.node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + // Build the target number of offers interactively with the static invoice server. + let mut offer_paths_req = None; + for _ in 0..TEST_MAX_CACHED_OFFERS_TARGET { + let flow_res = pass_static_invoice_server_messages(server, recipient, recipient_id.clone()); + offer_paths_req = Some(flow_res.offer_paths_request); + + // Trigger a cache refresh + recipient.node.timer_tick_occurred(); + } + assert_eq!( + recipient.node.flow.test_get_async_receive_offers().len(), + TEST_MAX_CACHED_OFFERS_TARGET + ); + + // Force allowing more offer paths request attempts so we can check that the recipient will not + // attempt to build any further offers. + recipient.node.timer_tick_occurred(); + assert!(recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .is_none()); + + // If the recipient now receives new offer_paths, they should not attempt to build new offers as + // they already have enough. + server + .onion_messenger + .handle_onion_message(recipient.node.get_our_node_id(), &offer_paths_req.unwrap()); + let offer_paths = server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .unwrap(); + recipient.onion_messenger.handle_onion_message(server.node.get_our_node_id(), &offer_paths); + assert!(recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .is_none()); + + // Check that round trip serialization of the ChannelManager will result in identical stored + // offers. + let cached_offers_pre_ser = recipient.node.flow.test_get_async_receive_offers(); + let config = test_default_channel_config(); + let serialized_monitor = get_monitor!(recipient, chan_id).encode(); + reload_node!( + nodes[1], + config, + recipient.node.encode(), + &[&serialized_monitor], + persister, + chain_monitor, + payee_node_deserialized + ); + let recipient = &nodes[1]; + let cached_offers_post_ser = recipient.node.flow.test_get_async_receive_offers(); + assert_eq!(cached_offers_pre_ser, cached_offers_post_ser); +} + +#[cfg_attr(feature = "std", ignore)] +#[test] +fn refresh_static_invoices() { + // Check that an invoice for a particular offer stored with the server will be updated once per + // timer tick. + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + + let mut allow_priv_chan_fwds_cfg = test_default_channel_config(); + allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; + let node_chanmgrs = + create_node_chanmgrs(3, &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg), None]); + + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + let server = &nodes[1]; + let recipient = &nodes[2]; + + let recipient_id = vec![42; 32]; + let inv_server_paths = + server.node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + // Set up the recipient to have one offer and an invoice with the static invoice server. + let original_invoice = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()).invoice; + // Mark the offer as used so we'll update the invoice on timer tick. + let _offer = recipient.node.get_async_receive_offer().unwrap(); + + // Force the server and recipient to send OMs directly to each other for testing simplicity. + server.message_router.peers_override.lock().unwrap().push(recipient.node.get_our_node_id()); + recipient.message_router.peers_override.lock().unwrap().push(server.node.get_our_node_id()); + + assert!(recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .is_none()); + + // Check that we'll refresh the invoice on the next timer tick. + recipient.node.timer_tick_occurred(); + let serve_static_invoice_om = loop { + let msg = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + match server.onion_messenger.peel_onion_message(&msg).unwrap() { + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::ServeStaticInvoice(_), _, _) => { + break msg; + }, + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPathsRequest(_), _, _) => {}, + _ => panic!("Unexpected message"), + } + }; + server + .onion_messenger + .handle_onion_message(recipient.node.get_our_node_id(), &serve_static_invoice_om); + let mut events = server.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let (updated_invoice, ack_path) = match events.pop().unwrap() { + Event::PersistStaticInvoice { + invoice, + invoice_persisted_path, + recipient_id: ev_id, + .. + } => { + assert_eq!(recipient_id, ev_id); + (invoice, invoice_persisted_path) + }, + _ => panic!(), + }; + assert_ne!(original_invoice, updated_invoice); + server.node.static_invoice_persisted(ack_path); + let invoice_persisted_om = server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .unwrap(); + recipient + .onion_messenger + .handle_onion_message(server.node.get_our_node_id(), &invoice_persisted_om); + assert_eq!(recipient.node.flow.test_get_async_receive_offers().len(), 1); + + // Remove the peer restriction added above. + server.message_router.peers_override.lock().unwrap().clear(); + recipient.message_router.peers_override.lock().unwrap().clear(); + + // Complete a payment to the new invoice. + let offer = nodes[2].node.get_async_receive_offer().unwrap(); + let amt_msat = 5000; + let payment_id = PaymentId([1; 32]); + let params = RouteParametersConfig::default(); + nodes[0] + .node + .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), params) + .unwrap(); + + let release_held_htlc_om = pass_async_payments_oms( + updated_invoice.clone(), + &nodes[0], + &nodes[1], + &nodes[2], + recipient_id, + ) + .1; + nodes[0] + .onion_messenger + .handle_onion_message(nodes[2].node.get_our_node_id(), &release_held_htlc_om); + + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + let payment_hash = extract_payment_hash(&ev); + check_added_monitors!(nodes[0], 1); + + let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev); + let claimable_ev = do_pass_along_path(args).unwrap(); + let keysend_preimage = extract_payment_preimage(&claimable_ev); + let res = + claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage)); + assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(updated_invoice))); +} + +#[cfg_attr(feature = "std", ignore)] +#[test] +fn ignore_expired_static_invoice() { + // If a server receives an expired static invoice to persist, they should ignore it and not + // generate an event. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let server = &nodes[0]; + let recipient = &nodes[1]; + let recipient_id = vec![42; 32]; + let inv_server_paths = + server.node.blinded_paths_for_async_recipient(recipient_id, None).unwrap(); + recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + let (_, serve_static_invoice_om) = + invoice_flow_up_to_send_serve_static_invoice(server, recipient); + + // Advance time for the server so that by the time it receives the serve_static_invoice message, + // the invoice within has expired. + let block = create_dummy_block( + server.best_block_hash(), + server.node.duration_since_epoch().as_secs() as u32 + + TEST_DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY.as_secs() as u32 + + 1, + Vec::new(), + ); + connect_block(server, &block); + + server + .onion_messenger + .handle_onion_message(recipient.node.get_our_node_id(), &serve_static_invoice_om); + let mut events = server.node.get_and_clear_pending_events(); + assert!(events.is_empty()); +} + +#[test] +fn ignore_offer_paths_expiry_too_soon() { + // Recipents should ignore received offer_paths that expire too soon. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + let server = &nodes[0]; + let recipient = &nodes[1]; + let recipient_id = vec![42; 32]; + let inv_server_paths = + server.node.blinded_paths_for_async_recipient(recipient_id, None).unwrap(); + recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + // Get a legit offer_paths message from the server. + recipient.node.timer_tick_occurred(); + let offer_paths_req = recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .unwrap(); + recipient.onion_messenger.release_pending_msgs(); + server.onion_messenger.handle_onion_message(recipient.node.get_our_node_id(), &offer_paths_req); + let offer_paths = server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .unwrap(); + + // Get the blinded path use when manually sending the modified offer_paths message to the + // recipient. + let offer_paths_req_reply_path = + match server.onion_messenger.peel_onion_message(&offer_paths_req) { + Ok(PeeledOnion::AsyncPayments( + AsyncPaymentsMessage::OfferPathsRequest(_), + _, + reply_path, + )) => reply_path.unwrap(), + _ => panic!(), + }; + + // Modify the offer_paths message from the server to indicate that the offer paths expire too + // soon. + let (mut offer_paths_unwrapped, ctx) = match recipient + .onion_messenger + .peel_onion_message(&offer_paths) + { + Ok(PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPaths(msg), ctx, _)) => (msg, ctx), + _ => panic!(), + }; + let too_soon_expiry_secs = recipient + .node + .duration_since_epoch() + .as_secs() + .saturating_add(TEST_MIN_OFFER_PATHS_RELATIVE_EXPIRY_SECS - 1); + offer_paths_unwrapped.paths_absolute_expiry = Some(too_soon_expiry_secs); + + // Deliver the expired paths to the recipient and make sure they don't construct a + // serve_static_invoice message in response. + server + .onion_messenger + .send_onion_message( + ParsedOnionMessageContents::::AsyncPayments( + AsyncPaymentsMessage::OfferPaths(offer_paths_unwrapped), + ), + MessageSendInstructions::WithReplyPath { + destination: Destination::BlindedPath(offer_paths_req_reply_path), + // This context isn't used because the recipient doesn't reply to the message + context: MessageContext::AsyncPayments(ctx), + }, + ) + .unwrap(); + let offer_paths_expiry_too_soon = server + .onion_messenger + .next_onion_message_for_peer(recipient.node.get_our_node_id()) + .unwrap(); + recipient + .onion_messenger + .handle_onion_message(server.node.get_our_node_id(), &offer_paths_expiry_too_soon); + assert!(recipient + .onion_messenger + .next_onion_message_for_peer(server.node.get_our_node_id()) + .is_none()); +} + +#[test] +fn ignore_duplicate_offer() { + // Test that if an async receiver gets notified that the invoice for an offer was persisted twice, + // they won't cache the offer twice. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None, None]); + + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + + let recipient_id = vec![42; 32]; + let inv_server_paths = + nodes[0].node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + nodes[1].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + + let invoice_flow_res = + pass_static_invoice_server_messages(&nodes[0], &nodes[1], recipient_id.clone()); + let static_invoice = invoice_flow_res.invoice; + assert!(static_invoice.invoice_features().supports_basic_mpp()); + assert!(nodes[1].node.get_async_receive_offer().is_ok()); + + // Check that the recipient will ignore duplicate offers received. + nodes[1].onion_messenger.handle_onion_message( + nodes[1].node.get_our_node_id(), + &invoice_flow_res.static_invoice_persisted_message, + ); + assert_eq!(nodes[1].node.flow.test_get_async_receive_offers().len(), 1); +} diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 40480fd9b43..e5a58109f5c 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -5272,6 +5272,11 @@ where } } + #[cfg(all(test, async_payments))] + pub(crate) fn test_check_refresh_async_receive_offers(&self) { + self.check_refresh_async_receive_offer_cache(false); + } + /// Should be called after handling an [`Event::PersistStaticInvoice`], where the `Responder` /// comes from [`Event::PersistStaticInvoice::invoice_persisted_path`]. #[cfg(async_payments)] diff --git a/lightning/src/offers/async_receive_offer_cache.rs b/lightning/src/offers/async_receive_offer_cache.rs index 4203408326d..f0d7a7ff4fe 100644 --- a/lightning/src/offers/async_receive_offer_cache.rs +++ b/lightning/src/offers/async_receive_offer_cache.rs @@ -194,6 +194,14 @@ const OFFER_REFRESH_THRESHOLD: Duration = Duration::from_secs(2 * 60 * 60); #[cfg(async_payments)] const MIN_OFFER_PATHS_RELATIVE_EXPIRY_SECS: u64 = 3 * 30 * 24 * 60 * 60; +#[cfg(all(test, async_payments))] +pub(crate) const TEST_MAX_CACHED_OFFERS_TARGET: usize = MAX_CACHED_OFFERS_TARGET; +#[cfg(all(test, async_payments))] +pub(crate) const TEST_MAX_UPDATE_ATTEMPTS: u8 = MAX_UPDATE_ATTEMPTS; +#[cfg(all(test, async_payments))] +pub(crate) const TEST_MIN_OFFER_PATHS_RELATIVE_EXPIRY_SECS: u64 = + MIN_OFFER_PATHS_RELATIVE_EXPIRY_SECS; + #[cfg(async_payments)] impl AsyncReceiveOfferCache { /// Retrieve a cached [`Offer`] for receiving async payments as an often-offline recipient, as diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 39c1a5bb319..fbf3779dac5 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -248,12 +248,19 @@ pub(crate) const TEST_OFFERS_MESSAGE_REQUEST_LIMIT: usize = OFFERS_MESSAGE_REQUE /// The default relative expiry for reply paths where a quick response is expected and the reply /// path is single-use. #[cfg(async_payments)] -const TEMP_REPLY_PATH_RELATIVE_EXPIRY: Duration = Duration::from_secs(7200); +const TEMP_REPLY_PATH_RELATIVE_EXPIRY: Duration = Duration::from_secs(2 * 60 * 60); + +#[cfg(all(async_payments, test))] +pub(crate) const TEST_TEMP_REPLY_PATH_RELATIVE_EXPIRY: Duration = TEMP_REPLY_PATH_RELATIVE_EXPIRY; // Default to async receive offers and the paths used to update them lasting one year. #[cfg(async_payments)] const DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY: Duration = Duration::from_secs(365 * 24 * 60 * 60); +#[cfg(all(async_payments, test))] +pub(crate) const TEST_DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY: Duration = + DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY; + impl OffersMessageFlow where MR::Target: MessageRouter,