Skip to content

Commit 37ab3d3

Browse files
Async recipient: track static invoice creation time
Start tracking invoice creation time in cached async offers. This field will be used in the next commit to start only updating static invoices for Used offers every few hours instead of once a minute. We also remove the blinded path context StaticInvoicePersisted::path_absolute_expiry field here, replacing it with the new invoice_created_at field. We don't actually want to terminate early if the reply path a bit stale like we did before, since we want to use the invoice_created_at field regardless to drive a faster refresh of the invoice.
1 parent 0506fab commit 37ab3d3

File tree

4 files changed

+52
-96
lines changed

4 files changed

+52
-96
lines changed

lightning/src/blinded_path/message.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -506,10 +506,9 @@ pub enum AsyncPaymentsContext {
506506
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
507507
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
508508
offer_id: OfferId,
509-
/// The time as duration since the Unix epoch at which this path expires and messages sent over
510-
/// it should be ignored. If we receive confirmation of an invoice over this path after its
511-
/// expiry, it may be outdated and a new invoice update should be sent instead.
512-
path_absolute_expiry: core::time::Duration,
509+
/// The time as duration since the Unix epoch at which the invoice corresponding to this path
510+
/// was created. Useful to know when an invoice needs replacement.
511+
invoice_created_at: core::time::Duration,
513512
},
514513
/// Context contained within the reply [`BlindedMessagePath`] we put in outbound
515514
/// [`HeldHtlcAvailable`] messages, provided back to us in corresponding [`ReleaseHeldHtlc`]
@@ -577,7 +576,7 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext,
577576
},
578577
(3, StaticInvoicePersisted) => {
579578
(0, offer_id, required),
580-
(2, path_absolute_expiry, required),
579+
(2, invoice_created_at, required),
581580
},
582581
(4, OfferPathsRequest) => {
583582
(0, recipient_id, required),

lightning/src/ln/async_payments_tests.rs

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1554,54 +1554,6 @@ fn ignore_expired_offer_paths_message() {
15541554
.is_none());
15551555
}
15561556

1557-
#[cfg_attr(feature = "std", ignore)]
1558-
#[test]
1559-
fn ignore_expired_invoice_persisted_message() {
1560-
// If the recipient receives a static_invoice_persisted message over an expired reply path, it
1561-
// should be ignored.
1562-
let chanmon_cfgs = create_chanmon_cfgs(2);
1563-
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
1564-
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
1565-
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
1566-
create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
1567-
let server = &nodes[0];
1568-
let recipient = &nodes[1];
1569-
1570-
let recipient_id = vec![42; 32];
1571-
let inv_server_paths =
1572-
server.node.blinded_paths_for_async_recipient(recipient_id, None).unwrap();
1573-
recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap();
1574-
1575-
// Exchange messages until we can extract the final static_invoice_persisted OM.
1576-
recipient.node.timer_tick_occurred();
1577-
let serve_static_invoice = invoice_flow_up_to_send_serve_static_invoice(server, recipient).1;
1578-
server
1579-
.onion_messenger
1580-
.handle_onion_message(recipient.node.get_our_node_id(), &serve_static_invoice);
1581-
let mut events = server.node.get_and_clear_pending_events();
1582-
assert_eq!(events.len(), 1);
1583-
let ack_path = match events.pop().unwrap() {
1584-
Event::PersistStaticInvoice { invoice_persisted_path, .. } => invoice_persisted_path,
1585-
_ => panic!(),
1586-
};
1587-
1588-
server.node.static_invoice_persisted(ack_path);
1589-
let invoice_persisted = server
1590-
.onion_messenger
1591-
.next_onion_message_for_peer(recipient.node.get_our_node_id())
1592-
.unwrap();
1593-
assert!(matches!(
1594-
recipient.onion_messenger.peel_onion_message(&invoice_persisted).unwrap(),
1595-
PeeledOnion::AsyncPayments(AsyncPaymentsMessage::StaticInvoicePersisted(_), _, _)
1596-
));
1597-
1598-
advance_time_by(TEST_TEMP_REPLY_PATH_RELATIVE_EXPIRY + Duration::from_secs(1), recipient);
1599-
recipient
1600-
.onion_messenger
1601-
.handle_onion_message(server.node.get_our_node_id(), &invoice_persisted);
1602-
assert!(recipient.node.get_async_receive_offer().is_err());
1603-
}
1604-
16051557
#[test]
16061558
fn limit_offer_paths_requests() {
16071559
// Limit the number of offer_paths_requests sent to the server if they aren't responding.

lightning/src/offers/async_receive_offer_cache.rs

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,20 @@ use crate::blinded_path::message::AsyncPaymentsContext;
3030
enum OfferStatus {
3131
/// This offer has been returned to the user from the cache, so it needs to be stored until it
3232
/// expires and its invoice needs to be kept updated.
33-
Used,
33+
Used {
34+
/// The creation time of the invoice that was last confirmed as persisted by the server. Useful
35+
/// to know when the invoice needs refreshing.
36+
invoice_created_at: Duration,
37+
},
3438
/// This offer has not yet been returned to the user, and is safe to replace to ensure we always
3539
/// have a maximally fresh offer. We always want to have at least 1 offer in this state,
3640
/// preferably a few so we can respond to user requests for new offers without returning the same
3741
/// one multiple times. Returning a new offer each time is better for privacy.
38-
Ready,
42+
Ready {
43+
/// The creation time of the invoice that was last confirmed as persisted by the server. Useful
44+
/// to know when the invoice needs refreshing.
45+
invoice_created_at: Duration,
46+
},
3947
/// This offer's invoice is not yet confirmed as persisted by the static invoice server, so it is
4048
/// not yet ready to receive payments.
4149
Pending,
@@ -60,8 +68,12 @@ struct AsyncReceiveOffer {
6068
}
6169

6270
impl_writeable_tlv_based_enum!(OfferStatus,
63-
(0, Used) => {},
64-
(1, Ready) => {},
71+
(0, Used) => {
72+
(0, invoice_created_at, required),
73+
},
74+
(1, Ready) => {
75+
(0, invoice_created_at, required),
76+
},
6577
(2, Pending) => {},
6678
);
6779

@@ -216,16 +228,18 @@ impl AsyncReceiveOfferCache {
216228
// Find the freshest unused offer. See `OfferStatus::Ready`.
217229
let newest_unused_offer_opt = self
218230
.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()));
221-
if let Some((idx, newest_ready_offer)) = newest_unused_offer_opt {
222-
self.offers[idx].as_mut().map(|offer| offer.status = OfferStatus::Used);
231+
.max_by(|(_, offer_a, _), (_, offer_b, _)| offer_a.created_at.cmp(&offer_b.created_at))
232+
.map(|(idx, offer, invoice_created_at)| (idx, offer.offer.clone(), invoice_created_at));
233+
if let Some((idx, newest_ready_offer, invoice_created_at)) = newest_unused_offer_opt {
234+
self.offers[idx]
235+
.as_mut()
236+
.map(|offer| offer.status = OfferStatus::Used { invoice_created_at });
223237
return Ok((newest_ready_offer, true));
224238
}
225239

226240
// If no unused offers are available, return the used offer with the latest absolute expiry
227241
self.offers_with_idx()
228-
.filter(|(_, offer)| matches!(offer.status, OfferStatus::Used))
242+
.filter(|(_, offer)| matches!(offer.status, OfferStatus::Used { .. }))
229243
.max_by(|a, b| {
230244
let abs_expiry_a = a.1.offer.absolute_expiry().unwrap_or(Duration::MAX);
231245
let abs_expiry_b = b.1.offer.absolute_expiry().unwrap_or(Duration::MAX);
@@ -338,9 +352,9 @@ impl AsyncReceiveOfferCache {
338352
}
339353

340354
// If all of our offers are already used or pending, then none are available to be replaced
341-
let no_replaceable_offers = self
342-
.offers_with_idx()
343-
.all(|(_, offer)| matches!(offer.status, OfferStatus::Used | OfferStatus::Pending));
355+
let no_replaceable_offers = self.offers_with_idx().all(|(_, offer)| {
356+
matches!(offer.status, OfferStatus::Used { .. } | OfferStatus::Pending)
357+
});
344358
if no_replaceable_offers {
345359
return None;
346360
}
@@ -350,7 +364,7 @@ impl AsyncReceiveOfferCache {
350364
let num_payable_offers = self
351365
.offers_with_idx()
352366
.filter(|(_, offer)| {
353-
matches!(offer.status, OfferStatus::Used | OfferStatus::Ready { .. })
367+
matches!(offer.status, OfferStatus::Used { .. } | OfferStatus::Ready { .. })
354368
})
355369
.count();
356370
if num_payable_offers <= 1 {
@@ -361,10 +375,10 @@ impl AsyncReceiveOfferCache {
361375
// were last updated, so they are stale enough to warrant replacement.
362376
let awhile_ago = duration_since_epoch.saturating_sub(OFFER_REFRESH_THRESHOLD);
363377
self.unused_ready_offers()
364-
.filter(|(_, offer)| offer.created_at < awhile_ago)
378+
.filter(|(_, offer, _)| offer.created_at < awhile_ago)
365379
// Get the stalest offer and return its index
366-
.min_by(|(_, offer_a), (_, offer_b)| offer_a.created_at.cmp(&offer_b.created_at))
367-
.map(|(idx, _)| idx)
380+
.min_by(|(_, offer_a, _), (_, offer_b, _)| offer_a.created_at.cmp(&offer_b.created_at))
381+
.map(|(idx, _, _)| idx)
368382
}
369383

370384
/// Returns an iterator over (offer_idx, offer)
@@ -378,11 +392,11 @@ impl AsyncReceiveOfferCache {
378392
})
379393
}
380394

381-
/// Returns an iterator over (offer_idx, offer) where all returned offers are
395+
/// Returns an iterator over (offer_idx, offer, invoice_created_at) where all returned offers are
382396
/// [`OfferStatus::Ready`]
383-
fn unused_ready_offers(&self) -> impl Iterator<Item = (usize, &AsyncReceiveOffer)> {
397+
fn unused_ready_offers(&self) -> impl Iterator<Item = (usize, &AsyncReceiveOffer, Duration)> {
384398
self.offers_with_idx().filter_map(|(idx, offer)| match offer.status {
385-
OfferStatus::Ready => Some((idx, offer)),
399+
OfferStatus::Ready { invoice_created_at } => Some((idx, offer, invoice_created_at)),
386400
_ => None,
387401
})
388402
}
@@ -408,7 +422,7 @@ impl AsyncReceiveOfferCache {
408422
// them a fresh invoice on each timer tick.
409423
self.offers_with_idx().filter_map(|(idx, offer)| {
410424
let needs_invoice_update =
411-
offer.status == OfferStatus::Used || offer.status == OfferStatus::Pending;
425+
matches!(offer.status, OfferStatus::Used { .. } | OfferStatus::Pending);
412426
if needs_invoice_update {
413427
let offer_slot = idx.try_into().unwrap_or(u16::MAX);
414428
Some((
@@ -431,29 +445,25 @@ impl AsyncReceiveOfferCache {
431445
/// is needed.
432446
///
433447
/// [`StaticInvoicePersisted`]: crate::onion_message::async_payments::StaticInvoicePersisted
434-
pub(super) fn static_invoice_persisted(
435-
&mut self, context: AsyncPaymentsContext, duration_since_epoch: Duration,
436-
) -> bool {
437-
let offer_id = match context {
438-
AsyncPaymentsContext::StaticInvoicePersisted { path_absolute_expiry, offer_id } => {
439-
if duration_since_epoch > path_absolute_expiry {
440-
return false;
441-
}
442-
offer_id
448+
pub(super) fn static_invoice_persisted(&mut self, context: AsyncPaymentsContext) -> bool {
449+
let (invoice_created_at, offer_id) = match context {
450+
AsyncPaymentsContext::StaticInvoicePersisted { invoice_created_at, offer_id } => {
451+
(invoice_created_at, offer_id)
443452
},
444453
_ => return false,
445454
};
446455

447456
let mut offers = self.offers.iter_mut();
448457
let offer_entry = offers.find(|o| o.as_ref().map_or(false, |o| o.offer.id() == offer_id));
449458
if let Some(Some(ref mut offer)) = offer_entry {
450-
if offer.status == OfferStatus::Used {
451-
// We succeeded in updating the invoice for a used offer, no re-persistence of the cache
452-
// needed
453-
return false;
459+
match offer.status {
460+
OfferStatus::Used { invoice_created_at: ref mut inv_created_at }
461+
| OfferStatus::Ready { invoice_created_at: ref mut inv_created_at } => {
462+
*inv_created_at = core::cmp::min(invoice_created_at, *inv_created_at);
463+
},
464+
OfferStatus::Pending => offer.status = OfferStatus::Ready { invoice_created_at },
454465
}
455466

456-
offer.status = OfferStatus::Ready;
457467
return true;
458468
}
459469

@@ -465,7 +475,7 @@ impl AsyncReceiveOfferCache {
465475
self.offers_with_idx()
466476
.filter_map(|(_, offer)| {
467477
if matches!(offer.status, OfferStatus::Ready { .. })
468-
|| matches!(offer.status, OfferStatus::Used)
478+
|| matches!(offer.status, OfferStatus::Used { .. })
469479
{
470480
Some(offer.offer.clone())
471481
} else {

lightning/src/offers/flow.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1331,7 +1331,6 @@ where
13311331
ES::Target: EntropySource,
13321332
R::Target: Router,
13331333
{
1334-
let duration_since_epoch = self.duration_since_epoch();
13351334
let mut serve_static_invoice_msgs = Vec::new();
13361335
{
13371336
let cache = self.async_receive_offer_cache.lock().unwrap();
@@ -1352,10 +1351,8 @@ where
13521351
};
13531352

13541353
let reply_path_context = {
1355-
let path_absolute_expiry =
1356-
duration_since_epoch.saturating_add(TEMP_REPLY_PATH_RELATIVE_EXPIRY);
13571354
MessageContext::AsyncPayments(AsyncPaymentsContext::StaticInvoicePersisted {
1358-
path_absolute_expiry,
1355+
invoice_created_at: invoice.created_at(),
13591356
offer_id: offer.id(),
13601357
})
13611358
};
@@ -1533,11 +1530,9 @@ where
15331530
};
15341531

15351532
let reply_path_context = {
1536-
let path_absolute_expiry =
1537-
duration_since_epoch.saturating_add(TEMP_REPLY_PATH_RELATIVE_EXPIRY);
15381533
MessageContext::AsyncPayments(AsyncPaymentsContext::StaticInvoicePersisted {
15391534
offer_id,
1540-
path_absolute_expiry,
1535+
invoice_created_at: invoice.created_at(),
15411536
})
15421537
};
15431538

@@ -1658,7 +1653,7 @@ where
16581653
#[cfg(async_payments)]
16591654
pub fn handle_static_invoice_persisted(&self, context: AsyncPaymentsContext) -> bool {
16601655
let mut cache = self.async_receive_offer_cache.lock().unwrap();
1661-
cache.static_invoice_persisted(context, self.duration_since_epoch())
1656+
cache.static_invoice_persisted(context)
16621657
}
16631658

16641659
/// Get the encoded [`AsyncReceiveOfferCache`] for persistence.

0 commit comments

Comments
 (0)