Skip to content

Commit 0506fab

Browse files
Replace unused async offers based on creation time
Previously, we were refreshing unused async receive offers based on when the corresponding invoice was confirmed as persisted. But the time when the invoice is persisted doesn't really tell us when the offer became stale, since messages may be delayed. Given we want to always have the freshest possible offer, we'd like to replace an unused offer with a new one every few hours based on the offer's age, regardless of when it was persisted by the server, which we now do.
1 parent 2d478dc commit 0506fab

File tree

1 file changed

+23
-35
lines changed

1 file changed

+23
-35
lines changed

lightning/src/offers/async_receive_offer_cache.rs

Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,7 @@ enum OfferStatus {
3535
/// have a maximally fresh offer. We always want to have at least 1 offer in this state,
3636
/// preferably a few so we can respond to user requests for new offers without returning the same
3737
/// one multiple times. Returning a new offer each time is better for privacy.
38-
Ready {
39-
/// If this offer's invoice has been persisted for some time, it's safe to replace to ensure we
40-
/// always have the freshest possible offer available when the user goes to pull an offer from
41-
/// the cache.
42-
invoice_confirmed_persisted_at: Duration,
43-
},
38+
Ready,
4439
/// This offer's invoice is not yet confirmed as persisted by the static invoice server, so it is
4540
/// not yet ready to receive payments.
4641
Pending,
@@ -49,6 +44,9 @@ enum OfferStatus {
4944
#[derive(Clone)]
5045
struct AsyncReceiveOffer {
5146
offer: Offer,
47+
/// The time as duration since the Unix epoch at which this offer was created. Useful when
48+
/// refreshing unused offers.
49+
created_at: Duration,
5250
/// Whether this offer is used, ready for use, or pending invoice persistence with the static
5351
/// invoice server.
5452
status: OfferStatus,
@@ -63,9 +61,7 @@ struct AsyncReceiveOffer {
6361

6462
impl_writeable_tlv_based_enum!(OfferStatus,
6563
(0, Used) => {},
66-
(1, Ready) => {
67-
(0, invoice_confirmed_persisted_at, required),
68-
},
64+
(1, Ready) => {},
6965
(2, Pending) => {},
7066
);
7167

@@ -74,6 +70,7 @@ impl_writeable_tlv_based!(AsyncReceiveOffer, {
7470
(2, offer_nonce, required),
7571
(4, status, required),
7672
(6, update_static_invoice_path, required),
73+
(8, created_at, required),
7774
});
7875

7976
/// If we are an often-offline recipient, we'll want to interactively build offers and static
@@ -179,9 +176,9 @@ const MAX_CACHED_OFFERS_TARGET: usize = 10;
179176
#[cfg(async_payments)]
180177
const MAX_UPDATE_ATTEMPTS: u8 = 3;
181178

182-
// If we have an offer that is replaceable and its invoice was confirmed as persisted more than 2
183-
// hours ago, we can go ahead and refresh it because we always want to have the freshest offer
184-
// possible when a user goes to retrieve a cached offer.
179+
// If we have an offer that is replaceable and is more than 2 hours old, we can go ahead and refresh
180+
// it because we always want to have the freshest offer possible when a user goes to retrieve a
181+
// cached offer.
185182
//
186183
// We avoid replacing unused offers too quickly -- this prevents the case where we send multiple
187184
// invoices from different offers competing for the same slot to the server, messages are received
@@ -216,14 +213,11 @@ impl AsyncReceiveOfferCache {
216213
) -> Result<(Offer, bool), ()> {
217214
self.prune_expired_offers(duration_since_epoch, false);
218215

219-
// Find the freshest unused offer, where "freshness" is based on when the invoice was confirmed
220-
// persisted by the server. See `OfferStatus::Ready`.
216+
// Find the freshest unused offer. See `OfferStatus::Ready`.
221217
let newest_unused_offer_opt = self
222-
.unused_offers()
223-
.max_by(|(_, _, persisted_at_a), (_, _, persisted_at_b)| {
224-
persisted_at_a.cmp(&persisted_at_b)
225-
})
226-
.map(|(idx, offer, _)| (idx, offer.offer.clone()));
218+
.unused_ready_offers()
219+
.max_by(|(_, offer_a), (_, offer_b)| offer_a.created_at.cmp(&offer_b.created_at))
220+
.map(|(idx, offer)| (idx, offer.offer.clone()));
227221
if let Some((idx, newest_ready_offer)) = newest_unused_offer_opt {
228222
self.offers[idx].as_mut().map(|offer| offer.status = OfferStatus::Used);
229223
return Ok((newest_ready_offer, true));
@@ -316,6 +310,7 @@ impl AsyncReceiveOfferCache {
316310
Some(offer_opt) => {
317311
*offer_opt = Some(AsyncReceiveOffer {
318312
offer,
313+
created_at: duration_since_epoch,
319314
offer_nonce,
320315
status: OfferStatus::Pending,
321316
update_static_invoice_path,
@@ -365,15 +360,11 @@ impl AsyncReceiveOfferCache {
365360
// Filter for unused offers where longer than OFFER_REFRESH_THRESHOLD time has passed since they
366361
// were last updated, so they are stale enough to warrant replacement.
367362
let awhile_ago = duration_since_epoch.saturating_sub(OFFER_REFRESH_THRESHOLD);
368-
self.unused_offers()
369-
.filter(|(_, _, invoice_confirmed_persisted_at)| {
370-
*invoice_confirmed_persisted_at < awhile_ago
371-
})
363+
self.unused_ready_offers()
364+
.filter(|(_, offer)| offer.created_at < awhile_ago)
372365
// Get the stalest offer and return its index
373-
.min_by(|(_, _, persisted_at_a), (_, _, persisted_at_b)| {
374-
persisted_at_a.cmp(&persisted_at_b)
375-
})
376-
.map(|(idx, _, _)| idx)
366+
.min_by(|(_, offer_a), (_, offer_b)| offer_a.created_at.cmp(&offer_b.created_at))
367+
.map(|(idx, _)| idx)
377368
}
378369

379370
/// Returns an iterator over (offer_idx, offer)
@@ -387,13 +378,11 @@ impl AsyncReceiveOfferCache {
387378
})
388379
}
389380

390-
/// Returns an iterator over (offer_idx, offer, invoice_confirmed_persisted_at)
391-
/// where all returned offers are [`OfferStatus::Ready`]
392-
fn unused_offers(&self) -> impl Iterator<Item = (usize, &AsyncReceiveOffer, Duration)> {
381+
/// Returns an iterator over (offer_idx, offer) where all returned offers are
382+
/// [`OfferStatus::Ready`]
383+
fn unused_ready_offers(&self) -> impl Iterator<Item = (usize, &AsyncReceiveOffer)> {
393384
self.offers_with_idx().filter_map(|(idx, offer)| match offer.status {
394-
OfferStatus::Ready { invoice_confirmed_persisted_at } => {
395-
Some((idx, offer, invoice_confirmed_persisted_at))
396-
},
385+
OfferStatus::Ready => Some((idx, offer)),
397386
_ => None,
398387
})
399388
}
@@ -464,8 +453,7 @@ impl AsyncReceiveOfferCache {
464453
return false;
465454
}
466455

467-
offer.status =
468-
OfferStatus::Ready { invoice_confirmed_persisted_at: duration_since_epoch };
456+
offer.status = OfferStatus::Ready;
469457
return true;
470458
}
471459

0 commit comments

Comments
 (0)