Refresh async static invoices after channel changes#4753
Conversation
Async receive offers currently decide static invoice refreshes from timer-based freshness only. Channel changes need a separate selector so callers can rebuild server-side invoices without waiting for the normal age threshold. Add a cache helper that returns used and pending offers for forced invoice refresh. Ready offers stay on the existing offer rotation path because they have not been returned to the application yet. AI-assisted: planning and writing commit Co-Authored-By: OpenAI Codex <codex@openai.com>
The cache can now identify offers whose static invoices should be rebuilt immediately, but callers still need one canonical path that turns those offers into ServeStaticInvoice messages. Thread a forced-refresh entry point through OffersMessageFlow and ChannelManager while reusing the existing static invoice construction code. This keeps the later channel-change behavior focused on deciding when to refresh, not how to rebuild invoices. AI-assisted: planning and writing commit Co-Authored-By: OpenAI Codex <codex@openai.com>
Static invoices contain blinded payment paths built from the recipient's current channels and fees. When channels become usable, close, or require a local fee update, cached async receive offers can otherwise keep serving stale payment instructions until the normal refresh threshold passes. Mark those channel changes as requiring a forced async static invoice refresh, then process the refresh after channel locks are released. The deferred flag avoids rebuilding invoices while holding peer/channel locks, since invoice rebuilding needs a fresh usable-channel snapshot. AI-assisted: planning and writing commit Co-Authored-By: OpenAI Codex <codex@openai.com>
Used async receive offers may already be published by the application, so a new usable channel should refresh the static invoice stored by the server without waiting for the normal age threshold. Add a focused async payments test that marks an offer used, opens another channel to the static invoice server, and asserts that a replacement ServeStaticInvoice is sent for the same server slot. AI-assisted: planning and writing commit Co-Authored-By: OpenAI Codex <codex@openai.com>
|
I've assigned @wpaulino as a reviewer! |
| self.check_refresh_async_receive_offer_cache(true); | ||
| if should_refresh_static_invoices { | ||
| self.mark_async_receive_static_invoice_refresh_pending(); | ||
| } | ||
| self.process_pending_async_receive_static_invoice_refresh(); |
There was a problem hiding this comment.
On a timer tick where a fee update occurred, this double-sends ServeStaticInvoice messages. check_refresh_async_receive_offer_cache(true) (line 9167) already runs the timer-driven static invoice refresh via check_refresh_static_invoices(..., false), which sends Pending offers (always) and stale Used offers. The forced refresh here then runs check_refresh_static_invoices(..., true), which selects Pending (always) and Used (always) — so any Pending (and stale Used) offer gets two ServeStaticInvoice messages and produces two PersistStaticInvoice events on the server in the same tick.
static_invoice_persisted is idempotent so this isn't a correctness bug, but it's wasteful onion traffic / server churn. Consider skipping the forced refresh on timer ticks (the timer path already covers the relevant offers), or deferring the should_refresh_static_invoices trigger to the next tick.
| if chan_needs_persist == NotifyOption::DoPersist { | ||
| should_persist = NotifyOption::DoPersist; | ||
| should_refresh_static_invoices = true; |
There was a problem hiding this comment.
Tying the forced refresh to any fee change means a full, unconditional refresh of all Used/Pending static invoices whenever update_channel_fee returns DoPersist. Commitment feerate estimates can fluctuate frequently, so this bypasses the INVOICE_REFRESH_THRESHOLD rate-limiting that normally throttles Used-offer refreshes and can generate a steady stream of ServeStaticInvoice messages even when payment paths are otherwise unchanged. Consider whether a fee delta should be debounced or only trigger when it materially affects the encoded invoice paths.
SummaryThe PR is mostly sound on lock-safety (the deferred-refresh pattern correctly releases per-peer/persistence locks before refreshing, and
Cross-cutting note: when the last usable channel closes, |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4753 +/- ##
==========================================
+ Coverage 84.55% 86.96% +2.41%
==========================================
Files 137 161 +24
Lines 77617 111673 +34056
Branches 77617 111673 +34056
==========================================
+ Hits 65627 97119 +31492
- Misses 9948 12050 +2102
- Partials 2042 2504 +462
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Implements an async receive follow-up from #4135: refresh async offers/static invoices when a channel opens, closes, or fees change.
Static invoices are built from the recipient's current payment paths. If local channel state changes, an already-published async receive offer can otherwise continue serving stale server-side invoices until the normal refresh threshold passes.
This PR adds a forced refresh path for async receive static invoices. The existing timer-based refresh behavior remains unchanged; channel changes simply add an immediate refresh trigger for affected offers.
The existing invoice construction flow remains the single source of truth. Forced refresh changes when invoices are refreshed, not how they are built. The refresh itself is deferred until after channel locks are released, preserving the existing lock-safety model while ensuring invoices are updated promptly.