Expose async offer cache API to OffersMessageFlow users
What changed, and why it matters
This commit widens the visibility of several internal Lightning payment-offer APIs from crate-internal to public. It is a routine API exposure change so that non-ChannelManager users can reuse the async-payment offer flow. There is no direct security bug in the diff, but making previously internal state-management methods public slightly enlarges the attack surface and places more responsibility on callers to persist state correctly.
Treat as a normal API-expansion commit. Reviewers of downstream consumers should verify that callers persist the async receive offer cache when instructed and do not bypass intended state-management invariants. No immediate security patch is required.
Security signals we found
API visibility widened from pub(crate) to pub for offer-cache management
New public module exposure: async_receive_offer_cache
Documentation updated to emphasize required persistence of async receive offer cache
No new unsafe code, cryptographic changes, or input-validation changes observed
Evidence from the diff
The patch changes visibility of multiple methods in lightning/src/offers/flow.rs and the async_receive_offer_cache module from pub(crate) to pub, and makes async_receive_offer_cache a public module. It also adjusts documentation to reference writeable_async_receive_offer_cache for persistence. The code itself does not introduce new logic, cryptographic operations, or memory-unsafe changes. The primary security-relevant aspect is expanded API surface: callers outside the crate can now invoke methods that manage AsyncReceiveOfferCache, set blinded paths, enqueue static invoices, and refresh offers. Misuse (e.g., failing to persist the cache when the returned bool indicates it is needed) could lead to stale or lost async-payment state, but that is a caller-responsibility issue rather than a vulnerability in the library.
Changed components
lightning/src/offers/flow.rslightning/src/offers/async_receive_offer_cache.rslightning/src/offers/mod.rsInspect captured patch +26 / −19
diff --git a/lightning/src/offers/async_receive_offer_cache.rs b/lightning/src/offers/async_receive_offer_cache.rs
index 3a7857f..d8a45cb 100644
--- a/lightning/src/offers/async_receive_offer_cache.rs
+++ b/lightning/src/offers/async_receive_offer_cache.rs
@@ -153,7 +153,7 @@ impl AsyncReceiveOfferCache {
///
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
#[cfg(async_payments)]
- pub fn set_paths_to_static_invoice_server(
+ pub(crate) fn set_paths_to_static_invoice_server(
&mut self, paths_to_static_invoice_server: Vec<BlindedMessagePath>,
) -> Result<(), ()> {
if paths_to_static_invoice_server.is_empty() {
@@ -211,7 +211,7 @@ impl AsyncReceiveOfferCache {
///
// We need to re-persist the cache if a fresh offer was just marked as used to ensure we continue
// to keep this offer's invoice updated and don't replace it with the server.
- pub fn get_async_receive_offer(
+ pub(crate) fn get_async_receive_offer(
&mut self, duration_since_epoch: Duration,
) -> Result<(Offer, bool), ()> {
self.prune_expired_offers(duration_since_epoch, false);
diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs
index cd3f95e..dde7d58 100644
--- a/lightning/src/offers/flow.rs
+++ b/lightning/src/offers/flow.rs
@@ -153,8 +153,9 @@ where
/// If we are an async recipient, on startup we'll interactively build offers and static invoices
/// with an always-online node that will serve static invoices on our behalf. Once the offer is
/// built and the static invoice is confirmed as persisted by the server, the underlying
- /// [`AsyncReceiveOfferCache`] should be persisted so we remember the offers we've built.
- pub(crate) fn with_async_payments_offers_cache(
+ /// [`AsyncReceiveOfferCache`] should be persisted using
+ /// [`Self::writeable_async_receive_offer_cache`] so we remember the offers we've built.
+ pub fn with_async_payments_offers_cache(
mut self, async_receive_offer_cache: AsyncReceiveOfferCache,
) -> Self {
self.paths_to_static_invoice_server =
@@ -170,7 +171,7 @@ where
/// This method only needs to be called once when the server first takes on the recipient as a
/// client, or when the paths change, e.g. if the paths are set to expire at a particular time.
#[cfg(async_payments)]
- pub(crate) fn set_paths_to_static_invoice_server(
+ 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
@@ -286,7 +287,7 @@ where
///
/// Errors if blinded path creation fails or the provided `recipient_id` is larger than 1KiB.
#[cfg(async_payments)]
- pub(crate) fn blinded_paths_for_async_recipient(
+ pub fn blinded_paths_for_async_recipient(
&self, recipient_id: Vec<u8>, relative_expiry: Option<Duration>,
peers: Vec<MessageForwardNode>,
) -> Result<Vec<BlindedMessagePath>, ()> {
@@ -1113,9 +1114,11 @@ where
Ok(())
}
- /// Forwards a [`StaticInvoice`] over the provided `responder`.
+ /// Forwards a [`StaticInvoice`] over the provided [`Responder`] in response to an
+ /// [`InvoiceRequest`] that we as a static invoice server received on behalf of an often-offline
+ /// recipient.
#[cfg(async_payments)]
- pub(crate) fn enqueue_static_invoice(
+ pub fn enqueue_static_invoice(
&self, invoice: StaticInvoice, responder: Responder,
) -> Result<(), Bolt12SemanticError> {
let duration_since_epoch = self.duration_since_epoch();
@@ -1226,8 +1229,11 @@ where
/// Retrieve an [`Offer`] for receiving async payments as an often-offline recipient. Will only
/// return an offer if [`Self::set_paths_to_static_invoice_server`] was called and we succeeded in
/// interactively building a [`StaticInvoice`] with the static invoice server.
+ ///
+ /// Returns the requested offer as well as a bool indicating whether the cache needs to be
+ /// persisted using [`Self::writeable_async_receive_offer_cache`].
#[cfg(async_payments)]
- pub(crate) fn get_async_receive_offer(&self) -> Result<(Offer, bool), ()> {
+ pub fn get_async_receive_offer(&self) -> Result<(Offer, bool), ()> {
let mut cache = self.async_receive_offer_cache.lock().unwrap();
cache.get_async_receive_offer(self.duration_since_epoch())
}
@@ -1249,7 +1255,7 @@ where
///
/// Errors if we failed to create blinded reply paths when sending an [`OfferPathsRequest`] message.
#[cfg(async_payments)]
- pub(crate) fn check_refresh_async_receive_offer_cache<ES: Deref, R: Deref>(
+ pub fn check_refresh_async_receive_offer_cache<ES: Deref, R: Deref>(
&self, peers: Vec<MessageForwardNode>, usable_channels: Vec<ChannelDetails>, entropy: ES,
router: R, timer_tick_occurred: bool,
) -> Result<(), ()>
@@ -1385,7 +1391,7 @@ where
/// wants us (the static invoice server) to serve [`StaticInvoice`]s to payers on their behalf.
/// Sends out [`OfferPaths`] onion messages in response.
#[cfg(async_payments)]
- pub(crate) fn handle_offer_paths_request<ES: Deref>(
+ pub fn handle_offer_paths_request<ES: Deref>(
&self, context: AsyncPaymentsContext, peers: Vec<MessageForwardNode>, entropy_source: ES,
) -> Option<(OfferPaths, MessageContext)>
where
@@ -1448,7 +1454,7 @@ where
/// Returns `None` if we have enough offers cached already, verification of `message` fails, or we
/// fail to create blinded paths.
#[cfg(async_payments)]
- pub(crate) fn handle_offer_paths<ES: Deref, R: Deref>(
+ pub fn handle_offer_paths<ES: Deref, R: Deref>(
&self, message: OfferPaths, context: AsyncPaymentsContext, responder: Responder,
peers: Vec<MessageForwardNode>, usable_channels: Vec<ChannelDetails>, entropy: ES,
router: R,
@@ -1599,7 +1605,7 @@ where
/// the static invoice from the database.
///
/// Errors if the [`ServeStaticInvoice::invoice`] is expired or larger than
- /// [`MAX_STATIC_INVOICE_SIZE_BYTES`], or if blinded path verification fails.
+ /// [`MAX_STATIC_INVOICE_SIZE_BYTES`].
///
/// [`ServeStaticInvoice::invoice`]: crate::onion_message::async_payments::ServeStaticInvoice::invoice
#[cfg(async_payments)]
@@ -1641,17 +1647,18 @@ where
}
/// Handles an incoming [`StaticInvoicePersisted`] onion message from the static invoice server.
- /// Returns a bool indicating whether the async receive offer cache needs to be re-persisted.
+ /// Returns a bool indicating whether the async receive offer cache needs to be re-persisted using
+ /// [`Self::writeable_async_receive_offer_cache`].
///
/// [`StaticInvoicePersisted`]: crate::onion_message::async_payments::StaticInvoicePersisted
#[cfg(async_payments)]
- pub(crate) fn handle_static_invoice_persisted(&self, context: AsyncPaymentsContext) -> bool {
+ pub fn handle_static_invoice_persisted(&self, context: AsyncPaymentsContext) -> bool {
let mut cache = self.async_receive_offer_cache.lock().unwrap();
cache.static_invoice_persisted(context, self.duration_since_epoch())
}
- /// Get the `AsyncReceiveOfferCache` for persistence.
- pub(crate) fn writeable_async_receive_offer_cache(&self) -> impl Writeable + '_ {
- &self.async_receive_offer_cache
+ /// Get the [`AsyncReceiveOfferCache`] for persistence.
+ pub fn writeable_async_receive_offer_cache(&self) -> impl Writeable + '_ {
+ self.async_receive_offer_cache.encode()
}
}
diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs
index b603dee..5b5cf6c 100644
--- a/lightning/src/offers/mod.rs
+++ b/lightning/src/offers/mod.rs
@@ -16,7 +16,7 @@
pub mod offer;
pub mod flow;
-pub(crate) mod async_receive_offer_cache;
+pub mod async_receive_offer_cache;
pub mod invoice;
pub mod invoice_error;
mod invoice_macros;
Why this scored 20/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.