Avoid repeated persisted async invoice refreshes
What changed, and why it matters
This commit fixes a bug where a Lightning node's timer would keep refreshing and re-sending the same static invoice over and over, instead of waiting for the proper refresh interval. The root cause was using the older (minimum) invoice creation time rather than the newest (maximum) one when deciding if an offer looked stale. This could waste bandwidth, create unnecessary network messages, and potentially cause the node to look unreliable or spammy to peers.
Apply the patch. The change is small, well-tested, and corrects a clear logic error. Monitor for any related timer-driven loops that compare timestamps with min() where max() is semantically required.
Security signals we found
Repeated message generation due to stale timestamp comparison
Resource exhaustion / amplification risk from persistent timer-driven refreshes
Logic bug in state-machine timestamp update (min vs max)
Potential peer reputation degradation from repeated invoice serving
No explicit cryptographic or memory-safety flaw
Evidence from the diff
In rust-lightning’s async receive offer cache, when a refreshed static invoice was persisted, the code updated invoice_created_at with min(new_time, old_time), keeping the oldest timestamp. The refresh threshold is computed relative to this timestamp, so every timer tick would see the offer as stale and enqueue another ServeStaticInvoice request. The fix changes the assignment to max(new_time, old_time), anchoring the threshold to the newest persisted invoice. A regression test confirms that after a successful refresh, a subsequent timer tick does not immediately enqueue another ServeStaticInvoice.
Changed components
lightning/src/offers/async_receive_offer_cache.rslightning/src/ln/async_payments_tests.rsAsyncReceiveOfferCacheServeStaticInvoice message flowasync payments / BOLT 12 offersInspect captured patch +20 / −1
diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs
index 7bd745d..6e8f38f 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 dd96b5d..367cdb6 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 },
}
Why this scored 37/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.