De-dedup paths between flow and async offer cache
What changed, and why it matters
This commit is a small internal cleanup in the Lightning Dev Kit's code for handling 'offers' (a way to request payments). It removes a duplicate copy of a list of network paths used to contact a static invoice server. Previously the same list was stored in two places, which created a risk that the two copies could become inconsistent. Now the code keeps a single copy in the async receive offer cache and reads it from there. There is no direct security bug being fixed, but the change prevents a future maintenance issue that could lead to incorrect or stale routing paths being used.
No urgent action required. Treat as a normal code-quality refactor. Reviewers may want to confirm that no other code paths still rely on the removed `paths_to_static_invoice_server` field in `OffersMessageFlow` and that the cache lock is not held longer than necessary after the change.
Security signals we found
Eliminates duplicated authoritative state that could drift out of sync
Reduces lock interactions by consolidating path access through the cache
Changes API from returning cloned Vec to borrowed slice, reducing copying
No explicit security fix, CVE, or vulnerability description in commit or references
Evidence from the diff
The patch de-duplicates paths_to_static_invoice_server by removing the duplicate Mutex<Vec<BlindedMessagePath>> field from OffersMessageFlow and instead accessing the paths through AsyncReceiveOfferCache. The cache’s accessor is changed from returning an owned Vec<BlindedMessagePath> to returning a slice &[BlindedMessagePath], avoiding an unnecessary clone. The flow now locks the cache once, uses the paths directly, and drops the lock explicitly. This eliminates the possibility of the in-memory copy and the persisted cache copy diverging. No vulnerability, exploit primitive, or externally reported security issue is present in the materials.
Changed components
lightning/src/offers/async_receive_offer_cache.rslightning/src/offers/flow.rsOffersMessageFlowAsyncReceiveOfferCacheasync payments / BOLT 12 offers path handlingInspect captured patch +11 / −24
diff --git a/lightning/src/offers/async_receive_offer_cache.rs b/lightning/src/offers/async_receive_offer_cache.rs
index d8a45cb..94ff3ad 100644
--- a/lightning/src/offers/async_receive_offer_cache.rs
+++ b/lightning/src/offers/async_receive_offer_cache.rs
@@ -143,8 +143,8 @@ impl AsyncReceiveOfferCache {
}
}
- pub(super) fn paths_to_static_invoice_server(&self) -> Vec<BlindedMessagePath> {
- self.paths_to_static_invoice_server.clone()
+ pub(super) fn paths_to_static_invoice_server(&self) -> &[BlindedMessagePath] {
+ &self.paths_to_static_invoice_server[..]
}
/// Sets the [`BlindedMessagePath`]s that we will use as an async recipient to interactively build
diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs
index 6bc9f3c..57baac9 100644
--- a/lightning/src/offers/flow.rs
+++ b/lightning/src/offers/flow.rs
@@ -104,9 +104,6 @@ where
pending_async_payments_messages: Mutex<Vec<(AsyncPaymentsMessage, MessageSendInstructions)>>,
async_receive_offer_cache: Mutex<AsyncReceiveOfferCache>,
- /// Blinded paths used to request offer paths from the static invoice server, if we are an async
- /// recipient.
- paths_to_static_invoice_server: Mutex<Vec<BlindedMessagePath>>,
#[cfg(feature = "dnssec")]
pub(crate) hrn_resolver: OMNameResolver,
@@ -146,7 +143,6 @@ where
pending_dns_onion_messages: Mutex::new(Vec::new()),
async_receive_offer_cache: Mutex::new(AsyncReceiveOfferCache::new()),
- paths_to_static_invoice_server: Mutex::new(Vec::new()),
}
}
@@ -158,8 +154,6 @@ where
pub fn with_async_payments_offers_cache(
mut self, async_receive_offer_cache: AsyncReceiveOfferCache,
) -> Self {
- self.paths_to_static_invoice_server =
- Mutex::new(async_receive_offer_cache.paths_to_static_invoice_server());
self.async_receive_offer_cache = Mutex::new(async_receive_offer_cache);
self
}
@@ -174,15 +168,9 @@ where
pub fn set_paths_to_static_invoice_server(
&self, paths_to_static_invoice_server: Vec<BlindedMessagePath>,
) -> Result<(), ()> {
- // Store the paths in the async receive cache so they are persisted with the cache, but also
- // store them in-memory in the `OffersMessageFlow` so the flow has access to them when building
- // onion messages to send to the static invoice server, without introducing undesirable lock
- // dependencies with the cache.
- *self.paths_to_static_invoice_server.lock().unwrap() =
- paths_to_static_invoice_server.clone();
-
let mut cache = self.async_receive_offer_cache.lock().unwrap();
- cache.set_paths_to_static_invoice_server(paths_to_static_invoice_server)
+ cache.set_paths_to_static_invoice_server(paths_to_static_invoice_server.clone())?;
+ Ok(())
}
/// Gets the node_id held by this [`OffersMessageFlow`]`
@@ -1264,7 +1252,8 @@ where
R::Target: Router,
{
// Terminate early if this node does not intend to receive async payments.
- if self.paths_to_static_invoice_server.lock().unwrap().is_empty() {
+ let mut cache = self.async_receive_offer_cache.lock().unwrap();
+ if cache.paths_to_static_invoice_server().is_empty() {
return Ok(());
}
@@ -1272,11 +1261,8 @@ where
// Update the cache to remove expired offers, and check to see whether we need new offers to be
// interactively built with the static invoice server.
- let needs_new_offers = self
- .async_receive_offer_cache
- .lock()
- .unwrap()
- .prune_expired_offers(duration_since_epoch, timer_tick_occurred);
+ let needs_new_offers =
+ cache.prune_expired_offers(duration_since_epoch, timer_tick_occurred);
// If we need new offers, send out offer paths request messages to the static invoice server.
if needs_new_offers {
@@ -1292,18 +1278,19 @@ where
};
// We can't fail past this point, so indicate to the cache that we've requested new offers.
- self.async_receive_offer_cache.lock().unwrap().new_offers_requested();
+ cache.new_offers_requested();
let mut pending_async_payments_messages =
self.pending_async_payments_messages.lock().unwrap();
let message = AsyncPaymentsMessage::OfferPathsRequest(OfferPathsRequest {});
enqueue_onion_message_with_reply_paths(
message,
- &self.paths_to_static_invoice_server.lock().unwrap()[..],
+ cache.paths_to_static_invoice_server(),
reply_paths,
&mut pending_async_payments_messages,
);
}
+ core::mem::drop(cache);
if timer_tick_occurred {
self.check_refresh_static_invoices(
Why this scored 21/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.