Skip to content

Commit be591c1

Browse files
Async receive: update static invoices when stale
Previously, for every one of our async receive offers that are in use, we would send a fresh invoice once on every timer tick/once a minute. It'd be better to check if the invoice is actually kinda-stale before updating it to avoid bombarding the server, so we do so here using the new invoice_created field added in the previous commit.
1 parent 37ab3d3 commit be591c1

File tree

3 files changed

+74
-16
lines changed

3 files changed

+74
-16
lines changed

lightning/src/ln/async_payments_tests.rs

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use crate::ln::outbound_payment::{
2828
PendingOutboundPayment, Retry, TEST_ASYNC_PAYMENT_TIMEOUT_RELATIVE_EXPIRY,
2929
};
3030
use crate::offers::async_receive_offer_cache::{
31-
TEST_MAX_CACHED_OFFERS_TARGET, TEST_MAX_UPDATE_ATTEMPTS,
31+
TEST_INVOICE_REFRESH_THRESHOLD, TEST_MAX_CACHED_OFFERS_TARGET, TEST_MAX_UPDATE_ATTEMPTS,
3232
TEST_MIN_OFFER_PATHS_RELATIVE_EXPIRY_SECS, TEST_OFFER_REFRESH_THRESHOLD,
3333
};
3434
use crate::offers::flow::{
@@ -1715,11 +1715,53 @@ fn offer_cache_round_trip_ser() {
17151715
assert_eq!(cached_offers_pre_ser, cached_offers_post_ser);
17161716
}
17171717

1718+
#[test]
1719+
fn refresh_static_invoices_for_pending_offers() {
1720+
// Check that an invoice for an offer that is pending persistence with the server will be updated
1721+
// every timer tick.
1722+
let chanmon_cfgs = create_chanmon_cfgs(2);
1723+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
1724+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None, None]);
1725+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
1726+
create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0);
1727+
let server = &nodes[0];
1728+
let recipient = &nodes[1];
1729+
1730+
let recipient_id = vec![42; 32];
1731+
let inv_server_paths =
1732+
server.node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap();
1733+
recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap();
1734+
expect_offer_paths_requests(&nodes[1], &[&nodes[0]]);
1735+
1736+
// Set up the recipient to have one offer pending with the static invoice server.
1737+
invoice_flow_up_to_send_serve_static_invoice(server, recipient);
1738+
1739+
// Every timer tick, we'll send a fresh invoice to the server.
1740+
for _ in 0..10 {
1741+
recipient.node.timer_tick_occurred();
1742+
let pending_oms = recipient.onion_messenger.release_pending_msgs();
1743+
pending_oms
1744+
.get(&server.node.get_our_node_id())
1745+
.unwrap()
1746+
.iter()
1747+
.find(|msg| match server.onion_messenger.peel_onion_message(&msg).unwrap() {
1748+
PeeledOnion::AsyncPayments(AsyncPaymentsMessage::ServeStaticInvoice(_), _, _) => {
1749+
true
1750+
},
1751+
PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPathsRequest(_), _, _) => {
1752+
false
1753+
},
1754+
_ => panic!("Unexpected message"),
1755+
})
1756+
.unwrap();
1757+
}
1758+
}
1759+
17181760
#[cfg_attr(feature = "std", ignore)]
17191761
#[test]
1720-
fn refresh_static_invoices() {
1721-
// Check that an invoice for a particular offer stored with the server will be updated once per
1722-
// timer tick.
1762+
fn refresh_static_invoices_for_used_offers() {
1763+
// Check that an invoice for a used offer stored with the server will be updated every
1764+
// INVOICE_REFRESH_THRESHOLD.
17231765
let chanmon_cfgs = create_chanmon_cfgs(3);
17241766
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
17251767

@@ -1744,25 +1786,26 @@ fn refresh_static_invoices() {
17441786
// Set up the recipient to have one offer and an invoice with the static invoice server.
17451787
let flow_res = pass_static_invoice_server_messages(server, recipient, recipient_id.clone());
17461788
let original_invoice = flow_res.invoice;
1747-
// Mark the offer as used so we'll update the invoice on timer tick.
1789+
// Mark the offer as used so we'll update the invoice after INVOICE_REFRESH_THRESHOLD.
17481790
let _offer = recipient.node.get_async_receive_offer().unwrap();
17491791

17501792
// Force the server and recipient to send OMs directly to each other for testing simplicity.
17511793
server.message_router.peers_override.lock().unwrap().push(recipient.node.get_our_node_id());
17521794
recipient.message_router.peers_override.lock().unwrap().push(server.node.get_our_node_id());
17531795

1754-
assert!(recipient
1755-
.onion_messenger
1756-
.next_onion_message_for_peer(server.node.get_our_node_id())
1757-
.is_none());
1796+
// Prior to INVOICE_REFRESH_THRESHOLD, we won't refresh the invoice.
1797+
advance_time_by(TEST_INVOICE_REFRESH_THRESHOLD, recipient);
1798+
recipient.node.timer_tick_occurred();
1799+
expect_offer_paths_requests(&nodes[2], &[&nodes[0], &nodes[1]]);
17581800

1759-
// Check that we'll refresh the invoice on the next timer tick.
1801+
// After INVOICE_REFRESH_THRESHOLD, we will refresh the invoice.
1802+
advance_time_by(Duration::from_secs(1), recipient);
17601803
recipient.node.timer_tick_occurred();
17611804
let pending_oms = recipient.onion_messenger.release_pending_msgs();
17621805
let serve_static_invoice_om = pending_oms
17631806
.get(&server.node.get_our_node_id())
17641807
.unwrap()
1765-
.into_iter()
1808+
.iter()
17661809
.find(|msg| match server.onion_messenger.peel_onion_message(&msg).unwrap() {
17671810
PeeledOnion::AsyncPayments(AsyncPaymentsMessage::ServeStaticInvoice(_), _, _) => true,
17681811
PeeledOnion::AsyncPayments(AsyncPaymentsMessage::OfferPathsRequest(_), _, _) => false,

lightning/src/offers/async_receive_offer_cache.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ const MAX_UPDATE_ATTEMPTS: u8 = 3;
199199
#[cfg(async_payments)]
200200
const OFFER_REFRESH_THRESHOLD: Duration = Duration::from_secs(2 * 60 * 60);
201201

202+
/// Invoices stored with the static invoice server may become stale due to outdated channel and fee
203+
/// info, so they should be updated regularly.
204+
const INVOICE_REFRESH_THRESHOLD: Duration = Duration::from_secs(2 * 60 * 60);
205+
202206
// Require offer paths that we receive to last at least 3 months.
203207
#[cfg(async_payments)]
204208
const MIN_OFFER_PATHS_RELATIVE_EXPIRY_SECS: u64 = 3 * 30 * 24 * 60 * 60;
@@ -210,6 +214,8 @@ pub(crate) const TEST_MAX_UPDATE_ATTEMPTS: u8 = MAX_UPDATE_ATTEMPTS;
210214
#[cfg(all(test, async_payments))]
211215
pub(crate) const TEST_OFFER_REFRESH_THRESHOLD: Duration = OFFER_REFRESH_THRESHOLD;
212216
#[cfg(all(test, async_payments))]
217+
pub(crate) const TEST_INVOICE_REFRESH_THRESHOLD: Duration = INVOICE_REFRESH_THRESHOLD;
218+
#[cfg(all(test, async_payments))]
213219
pub(crate) const TEST_MIN_OFFER_PATHS_RELATIVE_EXPIRY_SECS: u64 =
214220
MIN_OFFER_PATHS_RELATIVE_EXPIRY_SECS;
215221

@@ -416,13 +422,21 @@ impl AsyncReceiveOfferCache {
416422
/// Returns an iterator over the list of cached offers where we need to send an updated invoice to
417423
/// the static invoice server.
418424
pub(super) fn offers_needing_invoice_refresh(
419-
&self,
425+
&self, duration_since_epoch: Duration,
420426
) -> impl Iterator<Item = (&Offer, Nonce, u16, &Responder)> {
421427
// For any offers which are either in use or pending confirmation by the server, we should send
422428
// them a fresh invoice on each timer tick.
423-
self.offers_with_idx().filter_map(|(idx, offer)| {
424-
let needs_invoice_update =
425-
matches!(offer.status, OfferStatus::Used { .. } | OfferStatus::Pending);
429+
self.offers_with_idx().filter_map(move |(idx, offer)| {
430+
let needs_invoice_update = match offer.status {
431+
OfferStatus::Used { invoice_created_at } => {
432+
invoice_created_at.saturating_add(INVOICE_REFRESH_THRESHOLD)
433+
< duration_since_epoch
434+
},
435+
OfferStatus::Pending => true,
436+
// Don't bother updating `Ready` offers' invoices on a timer because the offers themselves
437+
// are regularly rotated anyway.
438+
OfferStatus::Ready { .. } => false,
439+
};
426440
if needs_invoice_update {
427441
let offer_slot = idx.try_into().unwrap_or(u16::MAX);
428442
Some((

lightning/src/offers/flow.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1333,8 +1333,9 @@ where
13331333
{
13341334
let mut serve_static_invoice_msgs = Vec::new();
13351335
{
1336+
let duration_since_epoch = self.duration_since_epoch();
13361337
let cache = self.async_receive_offer_cache.lock().unwrap();
1337-
for offer_and_metadata in cache.offers_needing_invoice_refresh() {
1338+
for offer_and_metadata in cache.offers_needing_invoice_refresh(duration_since_epoch) {
13381339
let (offer, offer_nonce, slot_number, update_static_invoice_path) =
13391340
offer_and_metadata;
13401341

0 commit comments

Comments
 (0)