Include invoice_slot in OfferPathsRequest message
What changed, and why it matters
This commit changes an internal Lightning protocol message so that a wallet tells a static invoice server which database 'slot' to use when storing an invoice. It is a protocol/API simplification, not a security fix. There is no evidence it prevents or fixes any exploit.
No security action required; review as normal protocol/API refactoring.
Security signals we found
Protocol message field added to an async-payments onion message
No validation, authorization, or cryptographic changes present
No mention of security, vulnerability, CVE, or bug in commit message
Evidence from the diff
The patch adds an invoice_slot: u16 field to OfferPathsRequest and threads the first empty cache slot through AsyncReceiveOfferCache::prune_expired_offers. The goal is to let the static invoice server index invoices by (recipient_id, invoice_slot) instead of sometimes using invoice_id, removing a dual-lookup pattern. The change is purely structural/protocol evolution; no bounds-checking, cryptographic, or authorization logic is modified.
Changed components
lightning/src/offers/async_receive_offer_cache.rslightning/src/offers/flow.rslightning/src/onion_message/async_payments.rsInspect captured patch +29 / −12
diff --git a/lightning/src/offers/async_receive_offer_cache.rs b/lightning/src/offers/async_receive_offer_cache.rs
index 1b1078d..b3e6b83 100644
--- a/lightning/src/offers/async_receive_offer_cache.rs
+++ b/lightning/src/offers/async_receive_offer_cache.rs
@@ -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 the first slot number 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<u16> {
// Remove expired offers from the cache.
let mut offer_was_removed = false;
for offer_opt in self.offers.iter_mut() {
@@ -268,8 +269,14 @@ 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).and_then(|idx| {
+ debug_assert!(idx < MAX_CACHED_OFFERS_TARGET);
+ idx.try_into().ok()
+ })
}
/// Returns whether the new paths we've just received from the static invoice server should be used
diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs
index b6eee42..7c809ad 100644
--- a/lightning/src/offers/flow.rs
+++ b/lightning/src/offers/flow.rs
@@ -1266,11 +1266,11 @@ 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_slot =
+ match cache.prune_expired_offers(duration_since_epoch, timer_tick_occurred) {
+ Some(idx) => idx,
+ 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 {
@@ -1289,7 +1289,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_slot,
+ });
enqueue_onion_message_with_reply_paths(
message,
cache.paths_to_static_invoice_server(),
diff --git a/lightning/src/onion_message/async_payments.rs b/lightning/src/onion_message/async_payments.rs
index 52badd7..1582543 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`].
@@ -233,7 +239,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),
Why this scored 18/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.