diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 142fe99fd86..954247d936d 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -353,7 +353,7 @@ pub enum OffersContext { 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 + /// [`OffersContext::StaticInvoiceRequested::invoice_slot`] 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`]. /// @@ -364,15 +364,15 @@ pub enum OffersContext { /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest recipient_id: Vec, - /// A random unique identifier for a specific [`StaticInvoice`] that the recipient previously + /// The slot number 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`]. + /// provided via [`AsyncPaymentsContext::ServeStaticInvoice::invoice_slot`]. /// /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest - invoice_id: u128, + invoice_slot: u16, /// The time as duration since the Unix epoch at which this path expires and messages sent over /// it should be ignored. @@ -448,6 +448,14 @@ pub enum AsyncPaymentsContext { /// [`OfferPathsRequest`]: crate::onion_message::async_payments::OfferPathsRequest /// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths OfferPaths { + /// The "slot" in the static invoice server's database that the invoice corresponding to these + /// offer paths should go into, originally set by us in [`OfferPathsRequest::invoice_slot`]. This + /// value allows us as the recipient to replace a specific invoice that is stored by the server, + /// which is useful for limiting the number of invoices stored by the server while also keeping + /// all the invoices persisted with the server fresh. + /// + /// [`OfferPathsRequest::invoice_slot`]: crate::onion_message::async_payments::OfferPathsRequest::invoice_slot + invoice_slot: u16, /// The time as duration since the Unix epoch at which this path expires and messages sent over /// it should be ignored. /// @@ -466,7 +474,7 @@ pub enum AsyncPaymentsContext { /// 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 + /// Useful when surfaced alongside the below `invoice_slot` 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. @@ -477,15 +485,15 @@ pub enum AsyncPaymentsContext { /// [`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 + /// The slot number 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`]. + /// [`OffersContext::StaticInvoiceRequested::invoice_slot`]. /// /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest - invoice_id: u128, + invoice_slot: u16, /// The time as duration since the Unix epoch at which this path expires and messages sent over /// it should be ignored. /// @@ -559,7 +567,7 @@ impl_writeable_tlv_based_enum!(OffersContext, }, (3, StaticInvoiceRequested) => { (0, recipient_id, required), - (2, invoice_id, required), + (2, invoice_slot, required), (4, path_absolute_expiry, required), }, ); @@ -573,6 +581,7 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext, }, (2, OfferPaths) => { (0, path_absolute_expiry, required), + (2, invoice_slot, required), }, (3, StaticInvoicePersisted) => { (0, offer_id, required), @@ -584,7 +593,7 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext, }, (5, ServeStaticInvoice) => { (0, recipient_id, required), - (2, invoice_id, required), + (2, invoice_slot, required), (4, path_absolute_expiry, required), }, ); diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 66424984e4d..30c928297a8 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -1661,18 +1661,11 @@ pub enum Event { /// [`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. + /// and can be used alongside the `invoice_slot` to retrieve the invoice from the database. /// ///[`ChannelManager::blinded_paths_for_async_recipient`]: crate::ln::channelmanager::ChannelManager::blinded_paths_for_async_recipient 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, + /// Once the [`StaticInvoice`] and `invoice_slot` 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. /// @@ -1688,7 +1681,7 @@ pub enum Event { /// 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 + /// matches the below `recipient_id` and `invoice_slot`, 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 @@ -1697,13 +1690,13 @@ pub enum Event { /// [`ChannelManager::send_static_invoice`]: crate::ln::channelmanager::ChannelManager::send_static_invoice StaticInvoiceRequested { /// An identifier for the recipient previously surfaced in - /// [`Event::PersistStaticInvoice::recipient_id`]. Useful when paired with the `invoice_id` to + /// [`Event::PersistStaticInvoice::recipient_id`]. Useful when paired with the `invoice_slot` 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 + /// The slot number for the invoice being requested, previously surfaced in + /// [`Event::PersistStaticInvoice::invoice_slot`]. Useful when paired with the `recipient_id` to /// retrieve the [`StaticInvoice`] requested by the payer. - invoice_id: u128, + invoice_slot: u16, /// 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. /// diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index d868eeeda80..2fa5dee8a84 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -66,7 +66,6 @@ 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, @@ -148,16 +147,15 @@ fn pass_static_invoice_server_messages( // 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() { + let (invoice, invoice_slot, 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) + (invoice, invoice_slot, invoice_persisted_path) }, _ => panic!(), }; @@ -183,7 +181,6 @@ fn pass_static_invoice_server_messages( static_invoice_persisted_message: invoice_persisted_om, invoice, invoice_slot, - invoice_id, } } @@ -209,7 +206,7 @@ fn pass_async_payments_oms( 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 } => { + Event::StaticInvoiceRequested { recipient_id: ev_id, invoice_slot: _, reply_path } => { assert_eq!(recipient_id, ev_id); reply_path }, @@ -573,7 +570,7 @@ fn ignore_unexpected_static_invoice() { 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 } => { + Event::StaticInvoiceRequested { recipient_id: ev_id, invoice_slot: _, reply_path } => { assert_eq!(recipient_id, ev_id); reply_path }, @@ -1626,13 +1623,13 @@ fn limit_serve_static_invoice_requests() { // 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(); + let mut invoice_slots = 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); + invoice_slots.insert(flow_res.invoice_slot); // Trigger a cache refresh recipient.node.timer_tick_occurred(); @@ -1641,8 +1638,8 @@ fn limit_serve_static_invoice_requests() { 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); + // Check that all invoice slot numbers are unique. + assert_eq!(invoice_slots.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. @@ -1822,16 +1819,15 @@ fn refresh_static_invoices_for_used_offers() { 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 + // When we update the invoice corresponding to a specific offer, the invoice_slot stays the // same. - assert_eq!(invoice_id, flow_res.invoice_id); + assert_eq!(invoice_slot, flow_res.invoice_slot); (invoice, invoice_persisted_path) }, _ => panic!(), diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index e4614351ee9..352bd90a8a1 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -14317,11 +14317,9 @@ where let invoice_request = match self.flow.verify_invoice_request(invoice_request, context) { Ok(InvreqResponseInstructions::SendInvoice(invoice_request)) => invoice_request, - Ok(InvreqResponseInstructions::SendStaticInvoice { - recipient_id: _recipient_id, invoice_id: _invoice_id - }) => { + Ok(InvreqResponseInstructions::SendStaticInvoice { recipient_id, invoice_slot }) => { self.pending_events.lock().unwrap().push_back((Event::StaticInvoiceRequested { - recipient_id: _recipient_id, invoice_id: _invoice_id, reply_path: responder + recipient_id, invoice_slot, reply_path: responder }, None)); return None @@ -14443,29 +14441,28 @@ where L::Target: Logger, { fn handle_offer_paths_request( - &self, _message: OfferPathsRequest, _context: AsyncPaymentsContext, - _responder: Option, + &self, message: OfferPathsRequest, context: AsyncPaymentsContext, + responder: Option, ) -> Option<(OfferPaths, ResponseInstruction)> { 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) { + match self.flow.handle_offer_paths_request(&message, context, peers) { Some(msg) => msg, None => return None, }; - _responder.map(|resp| (message, resp.respond_with_reply_path(reply_path_context))) + responder.map(|resp| (message, resp.respond_with_reply_path(reply_path_context))) } fn handle_offer_paths( - &self, _message: OfferPaths, _context: AsyncPaymentsContext, _responder: Option, + &self, message: OfferPaths, context: AsyncPaymentsContext, responder: Option, ) -> Option<(ServeStaticInvoice, ResponseInstruction)> { - let responder = match _responder { + let responder = match responder { Some(responder) => responder, None => return None, }; let (serve_static_invoice, reply_context) = match self.flow.handle_offer_paths( - _message, - _context, + message, + context, responder.clone(), self.get_peers_for_blinded_path(), self.list_usable_channels(), @@ -14484,27 +14481,26 @@ where } fn handle_serve_static_invoice( - &self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext, - _responder: Option, + &self, message: ServeStaticInvoice, context: AsyncPaymentsContext, + responder: Option, ) { - let responder = match _responder { + 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), + let (recipient_id, invoice_slot) = + match self.flow.verify_serve_static_invoice_message(&message, context) { + Ok((recipient_id, inv_slot)) => (recipient_id, inv_slot), 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, + invoice: message.invoice, + invoice_slot, recipient_id, - invoice_id, invoice_persisted_path: responder, }, None, @@ -14512,24 +14508,24 @@ where } fn handle_static_invoice_persisted( - &self, _message: StaticInvoicePersisted, _context: AsyncPaymentsContext, + &self, _message: StaticInvoicePersisted, context: AsyncPaymentsContext, ) { - let should_persist = self.flow.handle_static_invoice_persisted(_context); + let should_persist = self.flow.handle_static_invoice_persisted(context); if should_persist { let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); } } fn handle_held_htlc_available( - &self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext, - _responder: Option, + &self, _message: HeldHtlcAvailable, context: AsyncPaymentsContext, + responder: Option, ) -> Option<(ReleaseHeldHtlc, ResponseInstruction)> { - self.flow.verify_inbound_async_payment_context(_context).ok()?; - return _responder.map(|responder| (ReleaseHeldHtlc {}, responder.respond())); + self.flow.verify_inbound_async_payment_context(context).ok()?; + return responder.map(|responder| (ReleaseHeldHtlc {}, responder.respond())); } - fn handle_release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) { - let payment_id = match _context { + fn handle_release_held_htlc(&self, _message: ReleaseHeldHtlc, context: AsyncPaymentsContext) { + let payment_id = match context { AsyncPaymentsContext::OutboundPayment { payment_id } => payment_id, _ => return, }; diff --git a/lightning/src/offers/async_receive_offer_cache.rs b/lightning/src/offers/async_receive_offer_cache.rs index 1b1078dbb23..402759650bb 100644 --- a/lightning/src/offers/async_receive_offer_cache.rs +++ b/lightning/src/offers/async_receive_offer_cache.rs @@ -46,7 +46,7 @@ enum OfferStatus { Pending, } -#[derive(Clone)] +#[derive(Clone, PartialEq)] struct AsyncReceiveOffer { offer: Offer, /// The time as duration since the Unix epoch at which this offer was created. Useful when @@ -246,10 +246,11 @@ impl AsyncReceiveOfferCache { .ok_or(()) } - /// Remove expired offers from the cache, returning whether new offers are needed. + /// Remove expired offers from the cache, returning an index of the slot in the cache that needs a + /// new offer, if any exist. pub(super) fn prune_expired_offers( &mut self, duration_since_epoch: Duration, force_reset_request_attempts: bool, - ) -> bool { + ) -> Option { // Remove expired offers from the cache. let mut offer_was_removed = false; for offer_opt in self.offers.iter_mut() { @@ -268,8 +269,11 @@ impl AsyncReceiveOfferCache { self.reset_offer_paths_request_attempts() } - self.needs_new_offer_idx(duration_since_epoch).is_some() - && self.offer_paths_request_attempts < MAX_UPDATE_ATTEMPTS + if self.offer_paths_request_attempts >= MAX_UPDATE_ATTEMPTS { + return None; + } + + self.needs_new_offer_idx(duration_since_epoch) } /// Returns whether the new paths we've just received from the static invoice server should be used @@ -300,8 +304,8 @@ impl AsyncReceiveOfferCache { /// until it succeeds, see [`AsyncReceiveOfferCache`] docs. pub(super) fn cache_pending_offer( &mut self, offer: Offer, offer_paths_absolute_expiry_secs: Option, offer_nonce: Nonce, - update_static_invoice_path: Responder, duration_since_epoch: Duration, - ) -> Result { + update_static_invoice_path: Responder, duration_since_epoch: Duration, cache_slot: u16, + ) -> Result<(), ()> { self.prune_expired_offers(duration_since_epoch, false); if !self.should_build_offer_with_paths( @@ -312,35 +316,34 @@ impl AsyncReceiveOfferCache { return Err(()); } - let idx = match self.needs_new_offer_idx(duration_since_epoch) { - Some(idx) => idx, - None => return Err(()), - }; - - match self.offers.get_mut(idx) { - Some(offer_opt) => { - *offer_opt = Some(AsyncReceiveOffer { - offer, - created_at: duration_since_epoch, - offer_nonce, - status: OfferStatus::Pending, - update_static_invoice_path, - }); - }, - None => return Err(()), + let slot_needs_new_offer = self.offers.get(cache_slot as usize) == Some(&None) + || self + .unused_offers_needing_refresh(duration_since_epoch) + .find(|(idx, _)| *idx == cache_slot as usize) + .is_some(); + if !slot_needs_new_offer { + return Err(()); } - Ok(idx.try_into().map_err(|_| ())?) + self.offers[cache_slot as usize] = Some(AsyncReceiveOffer { + offer, + created_at: duration_since_epoch, + offer_nonce, + status: OfferStatus::Pending, + update_static_invoice_path, + }); + Ok(()) } /// If we have any empty slots in the cache or offers that can and should be replaced with a fresh /// offer, here we return the index of the slot that needs a new offer. The index is used for - /// setting [`ServeStaticInvoice::invoice_slot`] when sending the corresponding new static invoice - /// to the server, so the server knows which existing persisted invoice is being replaced, if any. + /// setting [`OfferPathsRequest::invoice_slot`] when requesting offer paths from the server, so + /// the server can include the slot in the offer paths and reply paths that they create in + /// response. /// /// Returns `None` if the cache is full and no offers can currently be replaced. /// - /// [`ServeStaticInvoice::invoice_slot`]: crate::onion_message::async_payments::ServeStaticInvoice::invoice_slot + /// [`OfferPathsRequest::invoice_slot`]: crate::onion_message::async_payments::OfferPathsRequest::invoice_slot fn needs_new_offer_idx(&self, duration_since_epoch: Duration) -> Option { // If we have any empty offer slots, return the first one we find let empty_slot_idx_opt = self.offers.iter().position(|offer_opt| offer_opt.is_none()); @@ -368,14 +371,9 @@ impl AsyncReceiveOfferCache { return None; } - // Filter for unused offers where longer than OFFER_REFRESH_THRESHOLD time has passed since they - // were last updated, so they are stale enough to warrant replacement. - let awhile_ago = duration_since_epoch.saturating_sub(OFFER_REFRESH_THRESHOLD); - self.unused_ready_offers() - .filter(|(_, offer, _)| offer.created_at < awhile_ago) - // Get the stalest offer and return its index - .min_by(|(_, offer_a, _), (_, offer_b, _)| offer_a.created_at.cmp(&offer_b.created_at)) - .map(|(idx, _, _)| idx) + self.unused_offers_needing_refresh(duration_since_epoch) + .min_by(|(_, offer_a), (_, offer_b)| offer_a.created_at.cmp(&offer_b.created_at)) + .map(|(idx, _)| idx) } /// Returns an iterator over (offer_idx, offer) @@ -414,10 +412,10 @@ impl AsyncReceiveOfferCache { /// the static invoice server. pub(super) fn offers_needing_invoice_refresh( &self, duration_since_epoch: Duration, - ) -> impl Iterator { + ) -> impl Iterator { // For any offers which are either in use or pending confirmation by the server, we should send // them a fresh invoice on each timer tick. - self.offers_with_idx().filter_map(move |(idx, offer)| { + self.offers_with_idx().filter_map(move |(_, offer)| { let needs_invoice_update = match offer.status { OfferStatus::Used { invoice_created_at } => { invoice_created_at.saturating_add(INVOICE_REFRESH_THRESHOLD) @@ -429,13 +427,22 @@ impl AsyncReceiveOfferCache { OfferStatus::Ready { .. } => false, }; if needs_invoice_update { - let offer_slot = idx.try_into().unwrap_or(u16::MAX); - Some(( - &offer.offer, - offer.offer_nonce, - offer_slot, - &offer.update_static_invoice_path, - )) + Some((&offer.offer, offer.offer_nonce, &offer.update_static_invoice_path)) + } else { + None + } + }) + } + + // Filter for unused offers where longer than OFFER_REFRESH_THRESHOLD time has passed since they + // were last updated, so they are stale enough to warrant replacement. + fn unused_offers_needing_refresh( + &self, duration_since_epoch: Duration, + ) -> impl Iterator { + let awhile_ago = duration_since_epoch.saturating_sub(OFFER_REFRESH_THRESHOLD); + self.unused_ready_offers().filter_map(move |(idx, offer, _)| { + if offer.created_at < awhile_ago { + Some((idx, offer)) } else { None } diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index b6eee429ca2..7ecb595c50e 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -395,7 +395,7 @@ pub enum InvreqResponseInstructions { /// 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 + /// [`StaticInvoice`] corresponding to the `recipient_id` and `invoice_slot` and calling /// `OffersMessageFlow::enqueue_static_invoice`. /// /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice @@ -404,8 +404,8 @@ pub enum InvreqResponseInstructions { /// /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice recipient_id: Vec, - /// An identifier for the specific invoice being requested by the payer. - invoice_id: u128, + /// The slot number for the specific invoice being requested by the payer. + invoice_slot: u16, }, } @@ -435,7 +435,7 @@ where Some(OffersContext::InvoiceRequest { nonce }) => Some(nonce), Some(OffersContext::StaticInvoiceRequested { recipient_id, - invoice_id, + invoice_slot, path_absolute_expiry, }) => { if path_absolute_expiry < self.duration_since_epoch() { @@ -444,7 +444,7 @@ where return Ok(InvreqResponseInstructions::SendStaticInvoice { recipient_id, - invoice_id, + invoice_slot, }); }, _ => return Err(()), @@ -1266,16 +1266,17 @@ where // Update the cache to remove expired offers, and check to see whether we need new offers to be // interactively built with the static invoice server. - let needs_new_offers = - cache.prune_expired_offers(duration_since_epoch, timer_tick_occurred); - if !needs_new_offers { - return Ok(()); - } + let needs_new_offer_idx: u16 = + match cache.prune_expired_offers(duration_since_epoch, timer_tick_occurred) { + Some(idx) => idx.try_into().map_err(|_| ())?, + None => return Ok(()), + }; // If we need new offers, send out offer paths request messages to the static invoice server. let context = MessageContext::AsyncPayments(AsyncPaymentsContext::OfferPaths { path_absolute_expiry: duration_since_epoch .saturating_add(TEMP_REPLY_PATH_RELATIVE_EXPIRY), + invoice_slot: needs_new_offer_idx, }); let reply_paths = match self.create_blinded_paths(peers, context) { Ok(paths) => paths, @@ -1289,7 +1290,9 @@ where let mut pending_async_payments_messages = self.pending_async_payments_messages.lock().unwrap(); - let message = AsyncPaymentsMessage::OfferPathsRequest(OfferPathsRequest {}); + let message = AsyncPaymentsMessage::OfferPathsRequest(OfferPathsRequest { + invoice_slot: needs_new_offer_idx, + }); enqueue_onion_message_with_reply_paths( message, cache.paths_to_static_invoice_server(), @@ -1314,8 +1317,7 @@ where let duration_since_epoch = self.duration_since_epoch(); let cache = self.async_receive_offer_cache.lock().unwrap(); for offer_and_metadata in cache.offers_needing_invoice_refresh(duration_since_epoch) { - let (offer, offer_nonce, slot_number, update_static_invoice_path) = - offer_and_metadata; + let (offer, offer_nonce, update_static_invoice_path) = offer_and_metadata; let (invoice, forward_invreq_path) = match self.create_static_invoice_for_server( offer, @@ -1339,7 +1341,6 @@ where let serve_invoice_message = ServeStaticInvoice { invoice, forward_invoice_request_path: forward_invreq_path, - invoice_slot: slot_number, }; serve_static_invoice_msgs.push(( serve_invoice_message, @@ -1370,12 +1371,10 @@ 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. - pub fn handle_offer_paths_request( - &self, context: AsyncPaymentsContext, peers: Vec, entropy_source: ES, - ) -> Option<(OfferPaths, MessageContext)> - where - ES::Target: EntropySource, - { + pub fn handle_offer_paths_request( + &self, request: &OfferPathsRequest, context: AsyncPaymentsContext, + peers: Vec, + ) -> Option<(OfferPaths, MessageContext)> { let duration_since_epoch = self.duration_since_epoch(); let recipient_id = match context { @@ -1388,10 +1387,6 @@ where _ => 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 = @@ -1399,7 +1394,7 @@ where let context = MessageContext::Offers(OffersContext::StaticInvoiceRequested { recipient_id: recipient_id.clone(), path_absolute_expiry, - invoice_id, + invoice_slot: request.invoice_slot, }); match self.create_blinded_paths(peers, context) { @@ -1416,7 +1411,7 @@ where duration_since_epoch.saturating_add(DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY); MessageContext::AsyncPayments(AsyncPaymentsContext::ServeStaticInvoice { recipient_id, - invoice_id, + invoice_slot: request.invoice_slot, path_absolute_expiry, }) }; @@ -1442,14 +1437,15 @@ where R::Target: Router, { let duration_since_epoch = self.duration_since_epoch(); - match context { - AsyncPaymentsContext::OfferPaths { path_absolute_expiry } => { + let invoice_slot = match context { + AsyncPaymentsContext::OfferPaths { invoice_slot, path_absolute_expiry } => { if duration_since_epoch > path_absolute_expiry { return None; } + invoice_slot }, _ => return None, - } + }; { // Only respond with `ServeStaticInvoice` if we actually need a new offer built. @@ -1493,18 +1489,16 @@ where Err(()) => return None, }; - let res = self.async_receive_offer_cache.lock().unwrap().cache_pending_offer( + if let Err(()) = self.async_receive_offer_cache.lock().unwrap().cache_pending_offer( offer, message.paths_absolute_expiry, offer_nonce, responder, duration_since_epoch, - ); - - let invoice_slot = match res { - Ok(idx) => idx, - Err(()) => return None, - }; + invoice_slot, + ) { + return None; + } let reply_path_context = { MessageContext::AsyncPayments(AsyncPaymentsContext::StaticInvoicePersisted { @@ -1513,8 +1507,7 @@ where }) }; - let serve_invoice_message = - ServeStaticInvoice { invoice, forward_invoice_request_path, invoice_slot }; + let serve_invoice_message = ServeStaticInvoice { invoice, forward_invoice_request_path }; Some((serve_invoice_message, reply_path_context)) } @@ -1576,7 +1569,7 @@ where /// 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 + /// On success, returns `(recipient_id, invoice_slot)` for use in persisting and later retrieving /// the static invoice from the database. /// /// Errors if the [`ServeStaticInvoice::invoice`] is expired or larger than @@ -1585,7 +1578,7 @@ where /// [`ServeStaticInvoice::invoice`]: crate::onion_message::async_payments::ServeStaticInvoice::invoice pub fn verify_serve_static_invoice_message( &self, message: &ServeStaticInvoice, context: AsyncPaymentsContext, - ) -> Result<(Vec, u128), ()> { + ) -> Result<(Vec, u16), ()> { if message.invoice.is_expired_no_std(self.duration_since_epoch()) { return Err(()); } @@ -1595,14 +1588,14 @@ where match context { AsyncPaymentsContext::ServeStaticInvoice { recipient_id, - invoice_id, + invoice_slot, path_absolute_expiry, } => { if self.duration_since_epoch() > path_absolute_expiry { return Err(()); } - return Ok((recipient_id, invoice_id)); + return Ok((recipient_id, invoice_slot)); }, _ => return Err(()), }; diff --git a/lightning/src/onion_message/async_payments.rs b/lightning/src/onion_message/async_payments.rs index 52badd71394..877af435bb4 100644 --- a/lightning/src/onion_message/async_payments.rs +++ b/lightning/src/onion_message/async_payments.rs @@ -131,7 +131,13 @@ pub enum AsyncPaymentsMessage { /// /// [`Offer::paths`]: crate::offers::offer::Offer::paths #[derive(Clone, Debug)] -pub struct OfferPathsRequest {} +pub struct OfferPathsRequest { + /// The "slot" in the static invoice server's database that this invoice should go into. This + /// allows us as the recipient to replace a specific invoice that is stored by the server, which + /// is useful for limiting the number of invoices stored by the server while also keeping all the + /// invoices persisted with the server fresh. + pub invoice_slot: u16, +} /// [`BlindedMessagePath`]s to be included in an async recipient's [`Offer::paths`], sent by a /// static invoice server in response to an [`OfferPathsRequest`]. @@ -166,11 +172,6 @@ pub struct ServeStaticInvoice { /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice pub forward_invoice_request_path: BlindedMessagePath, - /// The "slot" in the static invoice server's database that this invoice should go into. This - /// allows recipients to replace a specific invoice that is stored by the server, which is useful - /// for limiting the number of invoices stored by the server while also keeping all the invoices - /// persisted with the server fresh. - pub invoice_slot: u16, } /// Confirmation from a static invoice server that a [`StaticInvoice`] was persisted and the @@ -233,7 +234,9 @@ impl OnionMessageContents for ReleaseHeldHtlc { } } -impl_writeable_tlv_based!(OfferPathsRequest, {}); +impl_writeable_tlv_based!(OfferPathsRequest, { + (0, invoice_slot, required), +}); impl_writeable_tlv_based!(OfferPaths, { (0, paths, required_vec), @@ -243,7 +246,6 @@ impl_writeable_tlv_based!(OfferPaths, { impl_writeable_tlv_based!(ServeStaticInvoice, { (0, invoice, required), (2, forward_invoice_request_path, required), - (4, invoice_slot, required), }); impl_writeable_tlv_based!(StaticInvoicePersisted, {});