From 638b41b84fbc1ec75b80b709f31065043fe410a2 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 20 Feb 2025 16:45:58 -0500 Subject: [PATCH 1/9] 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 ade9f8e3550296ca1f54215c50e8dbcfd08904ca Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Mon, 3 Feb 2025 15:26:01 -0800 Subject: [PATCH 2/9] 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 | 28 +++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 99dd4fa667c..8cac9a9a02f 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, + /// An optional field indicating 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: Option, + }, /// 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, option), + }, ); /// 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..6cbafcb918d 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -250,6 +250,34 @@ 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`]. + /// + /// Errors if blinded path creation fails or the provided `recipient_id` is larger than 1KiB. + #[cfg(async_payments)] + pub(crate) fn blinded_paths_for_async_recipient( + &self, recipient_id: Vec, relative_expiry: Option, + peers: Vec, + ) -> Result, ()> { + if recipient_id.len() > 1024 { + return Err(()); + } + + let path_absolute_expiry = + relative_expiry.map(|exp| exp.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 cc9c67162c2081919036e2c804a306ca236d9f71 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Thu, 6 Feb 2025 16:29:42 -0800 Subject: [PATCH 3/9] 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 | 83 +++++++++++++++++++++++++++ lightning/src/ln/channelmanager.rs | 13 +++++ lightning/src/offers/flow.rs | 67 +++++++++++++++++++++ 3 files changed, 163 insertions(+) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 8cac9a9a02f..ba99b283ee4 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,41 @@ 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 when surfaced alongside the below `invoice_id` when payers send an + /// [`InvoiceRequest`], to pull the specific static invoice from the database. + /// + /// 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 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 +599,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 +628,11 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext, (0, recipient_id, required), (2, path_absolute_expiry, option), }, + (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 6cbafcb918d..fdac5453f92 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, @@ -1315,6 +1319,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.unwrap_or(Duration::MAX) { + 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 1ae315100c8b04cbdc828a7dd318706fa70afb15 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 18 Feb 2025 17:30:08 -0500 Subject: [PATCH 4/9] 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 | 50 +++++++++++++++++++++++++++ lightning/src/ln/channelmanager.rs | 32 +++++++++++++++++ lightning/src/offers/flow.rs | 55 ++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+) diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index d01af737c32..e6625a57ae9 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -1582,6 +1582,47 @@ 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 and its metadata are 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 random 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. + /// + /// Note that this id will remain the same for all invoice updates corresponding to a particular + /// offer that the recipient has cached. + 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 +2053,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 +2630,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..1d55e87dc58 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((recipient_id, inv_id)) => (recipient_id, inv_id), + 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 fdac5453f92..36fd73b1b3c 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, }; @@ -234,6 +235,11 @@ where } } +/// The maximum size of a received [`StaticInvoice`] before we'll fail verification in +/// [`OffersMessageFlow::verify_serve_static_invoice_message]. +#[cfg(async_payments)] +pub const MAX_STATIC_INVOICE_SIZE_BYTES: usize = 5 * 1024; + /// Defines the maximum number of [`OffersMessage`] including different reply paths to be sent /// along different paths. /// Sending multiple requests increases the chances of successful delivery in case some @@ -1532,6 +1538,55 @@ 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. + /// + /// Errors if the [`ServeStaticInvoice::invoice`] is expired or larger than + /// [`MAX_STATIC_INVOICE_SIZE_BYTES`], or if blinded path verification fails. + /// + /// [`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(()); + } + if message.invoice.serialized_length() > MAX_STATIC_INVOICE_SIZE_BYTES { + 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 cdf29085050cd8a422e83474ed589b361bfa2ea7 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 19 Feb 2025 19:18:13 -0500 Subject: [PATCH 5/9] 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 e6625a57ae9..399dcb5d3fb 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 random identifier for the invoice. When an `Event::StaticInvoiceRequested` comes in for + /// A random 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. /// @@ -1623,6 +1623,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 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 { @@ -2059,6 +2090,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`. @@ -2633,6 +2669,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 1d55e87dc58..c9c77ada802 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 36fd73b1b3c..8af280c5f57 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -426,6 +426,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, @@ -443,13 +463,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(()), }; @@ -460,7 +495,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, @@ -1070,6 +1105,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 1ce1321b957db8471afefde855a828382ff9eb9d Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Wed, 4 Jun 2025 18:26:10 -0400 Subject: [PATCH 6/9] 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 463e76b99639f8e551c92ba9a236238a9bf3e6be Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Mon, 7 Jul 2025 17:39:50 -0400 Subject: [PATCH 7/9] Test utils: update onion messenger message release Previously, the test-only method OnionMessenger::release_pending_msgs would not release a message if it was pending in e.g. the OffersMessageHandler but not in the messenger's own queue. Update this to pull pending messages from all handlers before releasing pending messages. --- lightning/src/onion_message/messenger.rs | 83 +++++++++++++----------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 3b2566119de..46b25223546 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -1538,7 +1538,9 @@ where } #[cfg(test)] - pub(super) fn release_pending_msgs(&self) -> HashMap> { + pub(crate) fn release_pending_msgs(&self) -> HashMap> { + self.enqueue_messages_from_handlers(); + 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 @@ -1549,6 +1551,47 @@ where msgs } + // Pull pending messages from each onion message handler and enqueue them in the messenger. + fn enqueue_messages_from_handlers(&self) { + // Enqueue any initiating `OffersMessage`s to send. + for (message, instructions) in self.offers_handler.release_pending_messages() { + let _ = self.send_onion_message_internal( + message, + instructions, + format_args!("when sending OffersMessage"), + ); + } + + #[cfg(async_payments)] + { + for (message, instructions) in self.async_payments_handler.release_pending_messages() { + let _ = self.send_onion_message_internal( + message, + instructions, + format_args!("when sending AsyncPaymentsMessage"), + ); + } + } + + // Enqueue any initiating `DNSResolverMessage`s to send. + for (message, instructions) in self.dns_resolver_handler.release_pending_messages() { + let _ = self.send_onion_message_internal( + message, + instructions, + format_args!("when sending DNSResolverMessage"), + ); + } + + // Enqueue any initiating `CustomMessage`s to send. + for (message, instructions) in self.custom_handler.release_pending_custom_messages() { + let _ = self.send_onion_message_internal( + message, + instructions, + format_args!("when sending CustomMessage"), + ); + } + } + fn enqueue_intercepted_event(&self, event: Event) { const MAX_EVENTS_BUFFER_SIZE: usize = (1 << 10) * 256; let mut pending_intercepted_msgs_events = @@ -2104,43 +2147,7 @@ where // enqueued in the handler by users, find a path to the corresponding blinded path's introduction // node, and then enqueue the message for sending to the first peer in the full path. fn next_onion_message_for_peer(&self, peer_node_id: PublicKey) -> Option { - // Enqueue any initiating `OffersMessage`s to send. - for (message, instructions) in self.offers_handler.release_pending_messages() { - let _ = self.send_onion_message_internal( - message, - instructions, - format_args!("when sending OffersMessage"), - ); - } - - #[cfg(async_payments)] - { - for (message, instructions) in self.async_payments_handler.release_pending_messages() { - let _ = self.send_onion_message_internal( - message, - instructions, - format_args!("when sending AsyncPaymentsMessage"), - ); - } - } - - // Enqueue any initiating `DNSResolverMessage`s to send. - for (message, instructions) in self.dns_resolver_handler.release_pending_messages() { - let _ = self.send_onion_message_internal( - message, - instructions, - format_args!("when sending DNSResolverMessage"), - ); - } - - // Enqueue any initiating `CustomMessage`s to send. - for (message, instructions) in self.custom_handler.release_pending_custom_messages() { - let _ = self.send_onion_message_internal( - message, - instructions, - format_args!("when sending CustomMessage"), - ); - } + self.enqueue_messages_from_handlers(); let mut message_recipients = self.message_recipients.lock().unwrap(); message_recipients.get_mut(&peer_node_id).and_then(|buffer| buffer.dequeue_message()) From a9834959267b42018dbd0f88570d8d65ba0bd5c7 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 27 May 2025 13:34:33 -0700 Subject: [PATCH 8/9] 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 | 406 ++++++++++++------ 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 | 5 + lightning/src/util/test_utils.rs | 31 +- 6 files changed, 343 insertions(+), 123 deletions(-) diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index 9b0a0352698..ce9fb541b05 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,130 @@ use bitcoin::secp256k1::Secp256k1; use core::convert::Infallible; use core::time::Duration; +struct StaticInvoiceServerFlowResult { + invoice: StaticInvoice, + invoice_slot: u16, + invoice_id: u128, + + // 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 (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 (invoice, invoice_slot, invoice_id, ack_path) = match events.pop().unwrap() { + Event::PersistStaticInvoice { + invoice, + invoice_persisted_path, + recipient_id: ev_id, + invoice_slot, + invoice_id, + } => { + assert_eq!(recipient_id, ev_id); + (invoice, invoice_slot, invoice_id, invoice_persisted_path) + }, + _ => panic!(), + }; + + // 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); + + // 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, + invoice_slot, + invoice_id, + } +} + // 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 +182,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 +226,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 +397,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 +441,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 +448,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 +471,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 +502,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 +517,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 +532,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 +562,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 +593,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 +600,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 +607,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 +628,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 +646,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 +663,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 +699,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 +716,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 +762,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 +792,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 +809,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 +981,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 +1036,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 +1046,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 +1106,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 +1118,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 +1136,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 +1172,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 +1237,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 +1249,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 8af280c5f57..2759435e0e9 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -247,6 +247,9 @@ pub const MAX_STATIC_INVOICE_SIZE_BYTES: usize = 5 * 1024; /// 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)] @@ -1236,6 +1239,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 46b25223546..31067fad2ed 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, 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 92881539154028efb6b9d26b05127ee86e00e660 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Mon, 30 Jun 2025 18:43:30 -0400 Subject: [PATCH 9/9] Test static invoice server protocol --- lightning/src/ln/async_payments_tests.rs | 785 +++++++++++++++++- lightning/src/ln/channelmanager.rs | 5 + .../src/offers/async_receive_offer_cache.rs | 10 + lightning/src/offers/flow.rs | 9 +- 4 files changed, 807 insertions(+), 2 deletions(-) diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index ce9fb541b05..ab9f172ce1b 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, TEST_OFFER_REFRESH_THRESHOLD, +}; +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; @@ -277,6 +285,12 @@ fn extract_payment_preimage(event: &Event) -> PaymentPreimage { } } +fn advance_time_by(duration: Duration, node: &Node) { + let target_time = (node.node.duration_since_epoch() + duration).as_secs() as u32; + let block = create_dummy_block(node.best_block_hash(), target_time, Vec::new()); + connect_block(node, &block); +} + #[test] fn invalid_keysend_payment_secret() { let chanmon_cfgs = create_chanmon_cfgs(3); @@ -1272,3 +1286,772 @@ 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: + advance_time_by(OFFER_PATHS_REQ_RELATIVE_EXPIRY + Duration::from_secs(1), server); + 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. + advance_time_by(TEST_TEMP_REPLY_PATH_RELATIVE_EXPIRY + Duration::from_secs(1), recipient); + 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_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(_), _, _) + )); + + advance_time_by(TEST_TEMP_REPLY_PATH_RELATIVE_EXPIRY + Duration::from_secs(1), recipient); + 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 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.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; + let mut invoice_ids = new_hash_set(); + for expected_inv_slot in 0..TEST_MAX_CACHED_OFFERS_TARGET { + let flow_res = pass_static_invoice_server_messages(server, recipient, recipient_id.clone()); + assert_eq!(flow_res.invoice_slot, expected_inv_slot as u16); + + offer_paths_req = Some(flow_res.offer_paths_request); + invoice_ids.insert(flow_res.invoice_id); + + // 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 + ); + // Check that all invoice ids are unique. + assert_eq!(invoice_ids.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()); +} + +#[test] +fn offer_cache_round_trip_ser() { + // Check that the async payments offer cache survives round trip serialization within the + // `ChannelManager`. + 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. + for _ in 0..TEST_MAX_CACHED_OFFERS_TARGET { + pass_static_invoice_server_messages(server, recipient, recipient_id.clone()); + // Trigger a cache refresh + recipient.node.timer_tick_occurred(); + } + + // 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 sender = &nodes[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 flow_res = pass_static_invoice_server_messages(server, recipient, recipient_id.clone()); + let original_invoice = flow_res.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 pending_oms = recipient.onion_messenger.release_pending_msgs(); + let serve_static_invoice_om = pending_oms + .get(&server.node.get_our_node_id()) + .unwrap() + .into_iter() + .find(|msg| match server.onion_messenger.peel_onion_message(&msg).unwrap() { + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::ServeStaticInvoice(_), _, _) => true, + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPathsRequest(_), _, _) => false, + _ => panic!("Unexpected message"), + }) + .unwrap(); + + 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_slot, + invoice_id, + invoice_persisted_path, + recipient_id: ev_id, + } => { + assert_ne!(original_invoice, invoice); + assert_eq!(recipient_id, ev_id); + assert_eq!(invoice_slot, flow_res.invoice_slot); + // When we update the invoice corresponding to a specific offer, the invoice_id stays the + // same. + assert_eq!(invoice_id, flow_res.invoice_id); + (invoice, invoice_persisted_path) + }, + _ => panic!(), + }; + 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 = recipient.node.get_async_receive_offer().unwrap(); + let amt_msat = 5000; + let payment_id = PaymentId([1; 32]); + let params = RouteParametersConfig::default(); + sender + .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(), sender, server, recipient, recipient_id).1; + sender + .onion_messenger + .handle_onion_message(recipient.node.get_our_node_id(), &release_held_htlc_om); + + let mut events = sender.node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let ev = remove_first_msg_event_to_node(&server.node.get_our_node_id(), &mut events); + let payment_hash = extract_payment_hash(&ev); + check_added_monitors!(sender, 1); + + let route: &[&[&Node]] = &[&[server, recipient]]; + let args = PassAlongPathArgs::new(sender, 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(sender, 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. + advance_time_by(TEST_DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY + Duration::from_secs(1), server); + + // Check that no Event::PersistStaticInvoice is generated. + 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); +} + +#[cfg_attr(feature = "std", ignore)] +#[test] +fn remove_expired_offer_from_cache() { + 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 recipient = &nodes[1]; + + 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(); + pass_static_invoice_server_messages(&nodes[0], &nodes[1], recipient_id.clone()); + + // We'll be able to retrieve the offer before it expires. + assert!(nodes[1].node.get_async_receive_offer().is_ok()); + + // After the offer expires we'll no longer return it from the API. + advance_time_by(TEST_DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY + Duration::from_secs(1), recipient); + assert!(nodes[1].node.get_async_receive_offer().is_err()); +} + +#[cfg_attr(feature = "std", ignore)] +#[test] +fn refresh_unused_offers() { + // Check that if a user has an unused offer older than TEST_OFFER_REFRESH_THRESHOLD, they will + // replace it with a fresh offer. + 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 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(); + + // First fill up the offer cache. + for _ in 0..(TEST_MAX_CACHED_OFFERS_TARGET - 1) { + // Trigger a cache refresh + recipient.node.timer_tick_occurred(); + + pass_static_invoice_server_messages(server, recipient, recipient_id.clone()); + } + + // Have the last offer expire later than the others. + advance_time_by(Duration::from_secs(1), recipient); + pass_static_invoice_server_messages(server, recipient, recipient_id.clone()); + + // Before the threshold, the recipient will not attempt to update any offers. + advance_time_by(TEST_OFFER_REFRESH_THRESHOLD - Duration::from_secs(2), recipient); + assert!(recipient + .onion_messenger + .release_pending_msgs() + .get(&server.node.get_our_node_id()) + .unwrap() + .is_empty()); + + // After the threshold time passes, the recipient will attempt to replace all of their offers + // (which are all unused) except the last. + advance_time_by(Duration::from_secs(2), recipient); + for expected_invoice_slot in 0..(TEST_MAX_CACHED_OFFERS_TARGET - 1) { + // Trigger a cache refresh + recipient.node.timer_tick_occurred(); + + let flow_res = pass_static_invoice_server_messages(server, recipient, recipient_id.clone()); + assert_eq!(flow_res.invoice_slot, expected_invoice_slot as u16); + } + recipient.node.timer_tick_occurred(); + assert!(recipient + .onion_messenger + .release_pending_msgs() + .get(&server.node.get_our_node_id()) + .unwrap() + .is_empty()); + + // The recipient will update the last offer after the threshold time has passed. + advance_time_by(Duration::from_secs(1), recipient); + recipient.node.timer_tick_occurred(); + let flow_res = pass_static_invoice_server_messages(server, recipient, recipient_id.clone()); + assert_eq!(flow_res.invoice_slot, TEST_MAX_CACHED_OFFERS_TARGET as u16 - 1); + + // If an offer is used, we shouldn't replace it after the threshold time. + let offer = recipient.node.get_async_receive_offer().unwrap(); + advance_time_by(TEST_OFFER_REFRESH_THRESHOLD + Duration::from_secs(100), recipient); + + // All offers besides the used one should be successfully replaced. + for _ in 0..(TEST_MAX_CACHED_OFFERS_TARGET - 1) { + recipient.node.timer_tick_occurred(); + pass_static_invoice_server_messages(server, recipient, recipient_id.clone()); + } + + // The used offer should only get an invoice update. + recipient.node.timer_tick_occurred(); + let mut pending_oms = recipient + .onion_messenger + .release_pending_msgs() + .remove(&server.node.get_our_node_id()) + .unwrap(); + assert_eq!(pending_oms.len(), 1); + let peeled_om = server.onion_messenger.peel_onion_message(&pending_oms[0]).unwrap(); + match peeled_om { + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::ServeStaticInvoice(msg), _, _) => { + assert_eq!(msg.invoice.offer_message_paths(), offer.paths()); + }, + _ => panic!(), + } + + // Check that the used offer is still in the cache + assert!(recipient.node.flow.test_get_async_receive_offers().contains(&offer)); +} + +#[test] +fn invoice_server_is_not_channel_peer() { + // Test that the async recipient's static invoice server does not need to be a channel peer for an + // async payment to successfully complete. + 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, &[None, Some(allow_priv_chan_fwds_cfg), None, None]); + + // Set up a network: + // + // static_invoice_server + // / + // sender -- forwarding_node ---- recipient + // + // So the static invoice server has no channels with the recipient. + + let nodes = create_network(4, &node_cfgs, &node_chanmgrs); + let sender = &nodes[0]; + let forwarding_node = &nodes[1]; + let recipient = &nodes[2]; + let invoice_server = &nodes[3]; + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + create_announced_chan_between_nodes_with_value(&nodes, 0, 3, 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 = + invoice_server.node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + let invoice = + pass_static_invoice_server_messages(invoice_server, recipient, recipient_id.clone()) + .invoice; + + let offer = recipient.node.get_async_receive_offer().unwrap(); + let amt_msat = 5000; + let payment_id = PaymentId([1; 32]); + let params = RouteParametersConfig::default(); + sender + .node + .pay_for_offer(&offer, None, Some(amt_msat), None, payment_id, Retry::Attempts(0), params) + .unwrap(); + + // Do the held_htlc_available --> release_held_htlc dance. + let release_held_htlc_om = + pass_async_payments_oms(invoice.clone(), sender, invoice_server, recipient, recipient_id).1; + sender + .onion_messenger + .handle_onion_message(recipient.node.get_our_node_id(), &release_held_htlc_om); + + // Check that the sender has queued the HTLCs of the async keysend payment. + let mut events = sender.node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let ev = remove_first_msg_event_to_node(&forwarding_node.node.get_our_node_id(), &mut events); + let payment_hash = extract_payment_hash(&ev); + check_added_monitors!(sender, 1); + + let route: &[&[&Node]] = &[&[forwarding_node, recipient]]; + let args = PassAlongPathArgs::new(sender, 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(sender, route, keysend_preimage)); + assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(invoice))); +} diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index c9c77ada802..074c8537e02 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..3a7857ffd0f 100644 --- a/lightning/src/offers/async_receive_offer_cache.rs +++ b/lightning/src/offers/async_receive_offer_cache.rs @@ -194,6 +194,16 @@ 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_OFFER_REFRESH_THRESHOLD: Duration = OFFER_REFRESH_THRESHOLD; +#[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 2759435e0e9..645724e9af2 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -253,12 +253,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,