From c6f4d8fc0c54e770f5679176058b7a00c59bf541 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 10 Jun 2026 13:32:49 +0200 Subject: [PATCH] Avoid repeated persisted async invoice refreshes When a used async receive offer's refreshed static invoice is persisted, advance the recorded invoice creation time. This keeps the refresh threshold anchored to the newest invoice instead of making the offer look stale on every timer tick. Add coverage that a used offer does not enqueue another ServeStaticInvoice immediately after the server confirms the refresh. Co-Authored-By: HAL 9000 This finding was discovered by Project Loupe --- lightning/src/ln/async_payments_tests.rs | 19 +++++++++++++++++++ .../src/offers/async_receive_offer_cache.rs | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index 7bd745dab0e..6e8f38f847a 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -2450,6 +2450,25 @@ fn refresh_static_invoices_for_used_offers() { .handle_onion_message(server.node.get_our_node_id(), &invoice_persisted_om); assert_eq!(recipient.node.flow.test_get_async_receive_offers().len(), 1); + // The invoice was just refreshed and persisted. A later timer tick must wait until the next + // refresh threshold before generating another invoice for the same offer. + recipient.node.timer_tick_occurred(); + let pending_oms_after = recipient.onion_messenger.release_pending_msgs(); + let mut extra_serve_invoices = 0; + if let Some(msgs) = pending_oms_after.get(&server.node.get_our_node_id()) { + for msg in msgs { + if let PeeledOnion::AsyncPayments(AsyncPaymentsMessage::ServeStaticInvoice(_), _, _) = + server.onion_messenger.peel_onion_message(&msg).unwrap() + { + extra_serve_invoices += 1; + } + } + } + assert_eq!( + extra_serve_invoices, 0, + "used offer invoice was refreshed again immediately after a successful refresh" + ); + // Remove the peer restriction added above. server.message_router.peers_override.lock().unwrap().clear(); recipient.message_router.peers_override.lock().unwrap().clear(); diff --git a/lightning/src/offers/async_receive_offer_cache.rs b/lightning/src/offers/async_receive_offer_cache.rs index dd96b5d1c42..367cdb68fc8 100644 --- a/lightning/src/offers/async_receive_offer_cache.rs +++ b/lightning/src/offers/async_receive_offer_cache.rs @@ -491,7 +491,7 @@ impl AsyncReceiveOfferCache { match offer.status { OfferStatus::Used { invoice_created_at: ref mut inv_created_at } | OfferStatus::Ready { invoice_created_at: ref mut inv_created_at } => { - *inv_created_at = core::cmp::min(invoice_created_at, *inv_created_at); + *inv_created_at = core::cmp::max(invoice_created_at, *inv_created_at); }, OfferStatus::Pending => offer.status = OfferStatus::Ready { invoice_created_at }, }