Skip to content

Commit 14b6ff6

Browse files
Cache pending offer in specific invoice slot
When we as an async recipient receive offer paths from the static invoice server, we create an offer and cache it, retrying persisting a corresponding invoice with the server until it succeeds. In the initially-merged version of this protocol, we would put this cached offer in any slot in the cache that needed an offer at the time the offer paths were received. However, in the last commit we started requesting offer paths for a specific slot in the cache, as part of eliminating the use of the invoice_id field in the overall protocol. As a result, here we put the cached offer in the specific cache slot that the original OfferPathsRequest indicated, rather than any slot that could use a new offer.
1 parent 93644d2 commit 14b6ff6

File tree

3 files changed

+55
-38
lines changed

3 files changed

+55
-38
lines changed

lightning/src/blinded_path/message.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,14 @@ pub enum AsyncPaymentsContext {
448448
/// [`OfferPathsRequest`]: crate::onion_message::async_payments::OfferPathsRequest
449449
/// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths
450450
OfferPaths {
451+
/// The "slot" in the static invoice server's database that the invoice corresponding to these
452+
/// offer paths should go into, originally set by us in [`OfferPathsRequest::invoice_slot`]. This
453+
/// value allows us as the recipient to replace a specific invoice that is stored by the server,
454+
/// which is useful for limiting the number of invoices stored by the server while also keeping
455+
/// all the invoices persisted with the server fresh.
456+
///
457+
/// [`OfferPathsRequest::invoice_slot`]: crate::onion_message::async_payments::OfferPathsRequest::invoice_slot
458+
invoice_slot: u16,
451459
/// The time as duration since the Unix epoch at which this path expires and messages sent over
452460
/// it should be ignored.
453461
///
@@ -573,6 +581,7 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext,
573581
},
574582
(2, OfferPaths) => {
575583
(0, path_absolute_expiry, required),
584+
(2, invoice_slot, required),
576585
},
577586
(3, StaticInvoicePersisted) => {
578587
(0, offer_id, required),

lightning/src/offers/async_receive_offer_cache.rs

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ enum OfferStatus {
4646
Pending,
4747
}
4848

49-
#[derive(Clone)]
49+
#[derive(Clone, PartialEq)]
5050
struct AsyncReceiveOffer {
5151
offer: Offer,
5252
/// The time as duration since the Unix epoch at which this offer was created. Useful when
@@ -304,8 +304,8 @@ impl AsyncReceiveOfferCache {
304304
/// until it succeeds, see [`AsyncReceiveOfferCache`] docs.
305305
pub(super) fn cache_pending_offer(
306306
&mut self, offer: Offer, offer_paths_absolute_expiry_secs: Option<u64>, offer_nonce: Nonce,
307-
update_static_invoice_path: Responder, duration_since_epoch: Duration,
308-
) -> Result<u16, ()> {
307+
update_static_invoice_path: Responder, duration_since_epoch: Duration, cache_slot: u16,
308+
) -> Result<(), ()> {
309309
self.prune_expired_offers(duration_since_epoch, false);
310310

311311
if !self.should_build_offer_with_paths(
@@ -316,25 +316,23 @@ impl AsyncReceiveOfferCache {
316316
return Err(());
317317
}
318318

319-
let idx = match self.needs_new_offer_idx(duration_since_epoch) {
320-
Some(idx) => idx,
321-
None => return Err(()),
322-
};
323-
324-
match self.offers.get_mut(idx) {
325-
Some(offer_opt) => {
326-
*offer_opt = Some(AsyncReceiveOffer {
327-
offer,
328-
created_at: duration_since_epoch,
329-
offer_nonce,
330-
status: OfferStatus::Pending,
331-
update_static_invoice_path,
332-
});
333-
},
334-
None => return Err(()),
319+
let slot_needs_new_offer = self.offers.get(cache_slot as usize) == Some(&None)
320+
|| self
321+
.unused_offers_needing_refresh(duration_since_epoch)
322+
.find(|(idx, _)| *idx == cache_slot as usize)
323+
.is_some();
324+
if !slot_needs_new_offer {
325+
return Err(());
335326
}
336327

337-
Ok(idx.try_into().map_err(|_| ())?)
328+
self.offers[cache_slot as usize] = Some(AsyncReceiveOffer {
329+
offer,
330+
created_at: duration_since_epoch,
331+
offer_nonce,
332+
status: OfferStatus::Pending,
333+
update_static_invoice_path,
334+
});
335+
Ok(())
338336
}
339337

340338
/// If we have any empty slots in the cache or offers that can and should be replaced with a fresh
@@ -372,14 +370,9 @@ impl AsyncReceiveOfferCache {
372370
return None;
373371
}
374372

375-
// Filter for unused offers where longer than OFFER_REFRESH_THRESHOLD time has passed since they
376-
// were last updated, so they are stale enough to warrant replacement.
377-
let awhile_ago = duration_since_epoch.saturating_sub(OFFER_REFRESH_THRESHOLD);
378-
self.unused_ready_offers()
379-
.filter(|(_, offer, _)| offer.created_at < awhile_ago)
380-
// Get the stalest offer and return its index
381-
.min_by(|(_, offer_a, _), (_, offer_b, _)| offer_a.created_at.cmp(&offer_b.created_at))
382-
.map(|(idx, _, _)| idx)
373+
self.unused_offers_needing_refresh(duration_since_epoch)
374+
.min_by(|(_, offer_a), (_, offer_b)| offer_a.created_at.cmp(&offer_b.created_at))
375+
.map(|(idx, _)| idx)
383376
}
384377

385378
/// Returns an iterator over (offer_idx, offer)
@@ -446,6 +439,21 @@ impl AsyncReceiveOfferCache {
446439
})
447440
}
448441

442+
// Filter for unused offers where longer than OFFER_REFRESH_THRESHOLD time has passed since they
443+
// were last updated, so they are stale enough to warrant replacement.
444+
fn unused_offers_needing_refresh(
445+
&self, duration_since_epoch: Duration,
446+
) -> impl Iterator<Item = (usize, &AsyncReceiveOffer)> {
447+
let awhile_ago = duration_since_epoch.saturating_sub(OFFER_REFRESH_THRESHOLD);
448+
self.unused_ready_offers().filter_map(move |(idx, offer, _)| {
449+
if offer.created_at < awhile_ago {
450+
Some((idx, offer))
451+
} else {
452+
None
453+
}
454+
})
455+
}
456+
449457
/// Should be called when we receive a [`StaticInvoicePersisted`] message from the static invoice
450458
/// server, which indicates that a new offer was persisted by the server and they are ready to
451459
/// serve the corresponding static invoice to payers on our behalf.

lightning/src/offers/flow.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,6 +1276,7 @@ where
12761276
let context = MessageContext::AsyncPayments(AsyncPaymentsContext::OfferPaths {
12771277
path_absolute_expiry: duration_since_epoch
12781278
.saturating_add(TEMP_REPLY_PATH_RELATIVE_EXPIRY),
1279+
invoice_slot: needs_new_offer_idx,
12791280
});
12801281
let reply_paths = match self.create_blinded_paths(peers, context) {
12811282
Ok(paths) => paths,
@@ -1444,14 +1445,15 @@ where
14441445
R::Target: Router,
14451446
{
14461447
let duration_since_epoch = self.duration_since_epoch();
1447-
match context {
1448-
AsyncPaymentsContext::OfferPaths { path_absolute_expiry } => {
1448+
let invoice_slot = match context {
1449+
AsyncPaymentsContext::OfferPaths { invoice_slot, path_absolute_expiry } => {
14491450
if duration_since_epoch > path_absolute_expiry {
14501451
return None;
14511452
}
1453+
invoice_slot
14521454
},
14531455
_ => return None,
1454-
}
1456+
};
14551457

14561458
{
14571459
// Only respond with `ServeStaticInvoice` if we actually need a new offer built.
@@ -1495,18 +1497,16 @@ where
14951497
Err(()) => return None,
14961498
};
14971499

1498-
let res = self.async_receive_offer_cache.lock().unwrap().cache_pending_offer(
1500+
if let Err(()) = self.async_receive_offer_cache.lock().unwrap().cache_pending_offer(
14991501
offer,
15001502
message.paths_absolute_expiry,
15011503
offer_nonce,
15021504
responder,
15031505
duration_since_epoch,
1504-
);
1505-
1506-
let invoice_slot = match res {
1507-
Ok(idx) => idx,
1508-
Err(()) => return None,
1509-
};
1506+
invoice_slot,
1507+
) {
1508+
return None;
1509+
}
15101510

15111511
let reply_path_context = {
15121512
MessageContext::AsyncPayments(AsyncPaymentsContext::StaticInvoicePersisted {

0 commit comments

Comments
 (0)