Skip to content

Commit ed0bf36

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 33291b6 commit ed0bf36

File tree

3 files changed

+65
-30
lines changed

3 files changed

+65
-30
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: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,18 @@ struct AsyncReceiveOffer {
6464
update_static_invoice_path: Responder,
6565
}
6666

67+
impl AsyncReceiveOffer {
68+
/// An offer needs to be refreshed if it is unused and has been cached longer than
69+
/// `OFFER_REFRESH_THRESHOLD`.
70+
fn needs_refresh(&self, duration_since_epoch: Duration) -> bool {
71+
let awhile_ago = duration_since_epoch.saturating_sub(OFFER_REFRESH_THRESHOLD);
72+
match self.status {
73+
OfferStatus::Ready { .. } => self.created_at < awhile_ago,
74+
_ => false,
75+
}
76+
}
77+
}
78+
6779
impl_writeable_tlv_based_enum!(OfferStatus,
6880
(0, Used) => {
6981
(0, invoice_created_at, required),
@@ -283,9 +295,9 @@ impl AsyncReceiveOfferCache {
283295
/// to build a new offer.
284296
pub(super) fn should_build_offer_with_paths(
285297
&self, offer_paths: &[BlindedMessagePath], offer_paths_absolute_expiry_secs: Option<u64>,
286-
duration_since_epoch: Duration,
298+
slot: u16, duration_since_epoch: Duration,
287299
) -> bool {
288-
if self.needs_new_offer_idx(duration_since_epoch).is_none() {
300+
if !self.slot_needs_offer(slot, duration_since_epoch) {
289301
return false;
290302
}
291303

@@ -307,37 +319,51 @@ impl AsyncReceiveOfferCache {
307319
/// until it succeeds, see [`AsyncReceiveOfferCache`] docs.
308320
pub(super) fn cache_pending_offer(
309321
&mut self, offer: Offer, offer_paths_absolute_expiry_secs: Option<u64>, offer_nonce: Nonce,
310-
update_static_invoice_path: Responder, duration_since_epoch: Duration,
311-
) -> Result<u16, ()> {
322+
update_static_invoice_path: Responder, duration_since_epoch: Duration, slot: u16,
323+
) -> Result<(), ()> {
312324
self.prune_expired_offers(duration_since_epoch, false);
313325

314326
if !self.should_build_offer_with_paths(
315327
offer.paths(),
316328
offer_paths_absolute_expiry_secs,
329+
slot,
317330
duration_since_epoch,
318331
) {
319332
return Err(());
320333
}
321334

322-
let idx = match self.needs_new_offer_idx(duration_since_epoch) {
323-
Some(idx) => idx,
324-
None => return Err(()),
325-
};
326-
327-
match self.offers.get_mut(idx) {
328-
Some(offer_opt) => {
329-
*offer_opt = Some(AsyncReceiveOffer {
335+
match self.offers.get_mut(slot as usize) {
336+
Some(slot) => {
337+
*slot = Some(AsyncReceiveOffer {
330338
offer,
331339
created_at: duration_since_epoch,
332340
offer_nonce,
333341
status: OfferStatus::Pending,
334342
update_static_invoice_path,
335-
});
343+
})
344+
},
345+
None => {
346+
debug_assert!(false, "Slot in cache was invalid but should'be been checked above");
347+
return Err(());
336348
},
337-
None => return Err(()),
338349
}
339350

340-
Ok(idx.try_into().map_err(|_| ())?)
351+
Ok(())
352+
}
353+
354+
fn slot_needs_offer(&self, slot: u16, duration_since_epoch: Duration) -> bool {
355+
match self.offers.get(slot as usize) {
356+
Some(Some(offer)) => offer.needs_refresh(duration_since_epoch),
357+
// This slot in the cache was pre-allocated as needing an offer in
358+
// `set_paths_to_static_invoice_server` and is currently vacant
359+
Some(None) => true,
360+
// `slot` is out-of-range. Note that the cache only has `MAX_CACHED_OFFERS_TARGET` slots
361+
// total, so any slots outside of that range are invalid.
362+
None => {
363+
debug_assert!(false, "Got offer paths for a non-existent slot in the cache");
364+
false
365+
},
366+
}
341367
}
342368

343369
/// If we have any empty slots in the cache or offers that can and should be replaced with a fresh
@@ -377,12 +403,11 @@ impl AsyncReceiveOfferCache {
377403

378404
// Filter for unused offers where longer than OFFER_REFRESH_THRESHOLD time has passed since they
379405
// were last updated, so they are stale enough to warrant replacement.
380-
let awhile_ago = duration_since_epoch.saturating_sub(OFFER_REFRESH_THRESHOLD);
381-
self.unused_ready_offers()
382-
.filter(|(_, offer, _)| offer.created_at < awhile_ago)
406+
self.offers_with_idx()
407+
.filter(|(_, offer)| offer.needs_refresh(duration_since_epoch))
383408
// Get the stalest offer and return its index
384-
.min_by(|(_, offer_a, _), (_, offer_b, _)| offer_a.created_at.cmp(&offer_b.created_at))
385-
.map(|(idx, _, _)| idx)
409+
.min_by(|(_, offer_a), (_, offer_b)| offer_a.created_at.cmp(&offer_b.created_at))
410+
.map(|(idx, _)| idx)
386411
}
387412

388413
/// Returns an iterator over (offer_idx, offer)

lightning/src/offers/flow.rs

Lines changed: 11 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_slot,
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.
@@ -1460,6 +1462,7 @@ where
14601462
if !cache.should_build_offer_with_paths(
14611463
&message.paths[..],
14621464
message.paths_absolute_expiry,
1465+
invoice_slot,
14631466
duration_since_epoch,
14641467
) {
14651468
return None;
@@ -1495,18 +1498,16 @@ where
14951498
Err(()) => return None,
14961499
};
14971500

1498-
let res = self.async_receive_offer_cache.lock().unwrap().cache_pending_offer(
1501+
if let Err(()) = self.async_receive_offer_cache.lock().unwrap().cache_pending_offer(
14991502
offer,
15001503
message.paths_absolute_expiry,
15011504
offer_nonce,
15021505
responder,
15031506
duration_since_epoch,
1504-
);
1505-
1506-
let invoice_slot = match res {
1507-
Ok(idx) => idx,
1508-
Err(()) => return None,
1509-
};
1507+
invoice_slot,
1508+
) {
1509+
return None;
1510+
}
15101511

15111512
let reply_path_context = {
15121513
MessageContext::AsyncPayments(AsyncPaymentsContext::StaticInvoicePersisted {

0 commit comments

Comments
 (0)