Skip to content

Commit 1c0e029

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 fb7e467 commit 1c0e029

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
@@ -1518,54 +1518,6 @@ fn ignore_expired_offer_paths_message() {
15181518
.is_none());
15191519
}
15201520

1521-
#[cfg_attr(feature = "std", ignore)]
1522-
#[test]
1523-
fn ignore_expired_invoice_persisted_message() {
1524-
// If the recipient receives a static_invoice_persisted message over an expired reply path, it
1525-
// should be ignored.
1526-
let chanmon_cfgs = create_chanmon_cfgs(2);
1527-
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
1528-
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
1529-
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
1530-
create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
1531-
let server = &nodes[0];
1532-
let recipient = &nodes[1];
1533-
1534-
let recipient_id = vec![42; 32];
1535-
let inv_server_paths =
1536-
server.node.blinded_paths_for_async_recipient(recipient_id, None).unwrap();
1537-
recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap();
1538-
1539-
// Exchange messages until we can extract the final static_invoice_persisted OM.
1540-
recipient.node.timer_tick_occurred();
1541-
let serve_static_invoice = invoice_flow_up_to_send_serve_static_invoice(server, recipient).1;
1542-
server
1543-
.onion_messenger
1544-
.handle_onion_message(recipient.node.get_our_node_id(), &serve_static_invoice);
1545-
let mut events = server.node.get_and_clear_pending_events();
1546-
assert_eq!(events.len(), 1);
1547-
let ack_path = match events.pop().unwrap() {
1548-
Event::PersistStaticInvoice { invoice_persisted_path, .. } => invoice_persisted_path,
1549-
_ => panic!(),
1550-
};
1551-
1552-
server.node.static_invoice_persisted(ack_path);
1553-
let invoice_persisted = server
1554-
.onion_messenger
1555-
.next_onion_message_for_peer(recipient.node.get_our_node_id())
1556-
.unwrap();
1557-
assert!(matches!(
1558-
recipient.onion_messenger.peel_onion_message(&invoice_persisted).unwrap(),
1559-
PeeledOnion::AsyncPayments(AsyncPaymentsMessage::StaticInvoicePersisted(_), _, _)
1560-
));
1561-
1562-
advance_time_by(TEST_TEMP_REPLY_PATH_RELATIVE_EXPIRY + Duration::from_secs(1), recipient);
1563-
recipient
1564-
.onion_messenger
1565-
.handle_onion_message(server.node.get_our_node_id(), &invoice_persisted);
1566-
assert!(recipient.node.get_async_receive_offer().is_err());
1567-
}
1568-
15691521
#[test]
15701522
fn limit_offer_paths_requests() {
15711523
// 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
@@ -1342,7 +1342,6 @@ where
13421342
ES::Target: EntropySource,
13431343
R::Target: Router,
13441344
{
1345-
let duration_since_epoch = self.duration_since_epoch();
13461345
let mut serve_static_invoice_msgs = Vec::new();
13471346
{
13481347
let cache = self.async_receive_offer_cache.lock().unwrap();
@@ -1363,10 +1362,8 @@ where
13631362
};
13641363

13651364
let reply_path_context = {
1366-
let path_absolute_expiry =
1367-
duration_since_epoch.saturating_add(TEMP_REPLY_PATH_RELATIVE_EXPIRY);
13681365
MessageContext::AsyncPayments(AsyncPaymentsContext::StaticInvoicePersisted {
1369-
path_absolute_expiry,
1366+
invoice_created_at: invoice.created_at(),
13701367
offer_id: offer.id(),
13711368
})
13721369
};
@@ -1544,11 +1541,9 @@ where
15441541
};
15451542

15461543
let reply_path_context = {
1547-
let path_absolute_expiry =
1548-
duration_since_epoch.saturating_add(TEMP_REPLY_PATH_RELATIVE_EXPIRY);
15491544
MessageContext::AsyncPayments(AsyncPaymentsContext::StaticInvoicePersisted {
15501545
offer_id,
1551-
path_absolute_expiry,
1546+
invoice_created_at: invoice.created_at(),
15521547
})
15531548
};
15541549

@@ -1669,7 +1664,7 @@ where
16691664
#[cfg(async_payments)]
16701665
pub fn handle_static_invoice_persisted(&self, context: AsyncPaymentsContext) -> bool {
16711666
let mut cache = self.async_receive_offer_cache.lock().unwrap();
1672-
cache.static_invoice_persisted(context, self.duration_since_epoch())
1667+
cache.static_invoice_persisted(context)
16731668
}
16741669

16751670
/// Get the [`AsyncReceiveOfferCache`] for persistence.

0 commit comments

Comments
 (0)