Skip to content

Commit e1761f3

Browse files
Check and refresh served static invoices
As an async recipient, we need to interactively build offers and corresponding static invoices, the latter of which an always-online node will serve to payers on our behalf. Offers may be very long-lived and have a longer expiration than their corresponding static invoice. Therefore, persist a fresh invoice with the static invoice server when the current invoice gets close to expiration.
1 parent 7359c1d commit e1761f3

File tree

3 files changed

+120
-6
lines changed

3 files changed

+120
-6
lines changed

lightning/src/ln/channelmanager.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -4918,7 +4918,9 @@ where
49184918

49194919
#[cfg(async_payments)]
49204920
fn check_refresh_async_receive_offers(&self) {
4921-
match self.flow.check_refresh_async_receive_offers(self.get_peers_for_blinded_path()) {
4921+
let peers = self.get_peers_for_blinded_path();
4922+
let channels = self.list_usable_channels();
4923+
match self.flow.check_refresh_async_receive_offers(peers, channels) {
49224924
Err(()) => {
49234925
log_error!(self.logger, "Failed to create blinded paths when requesting async receive offer paths");
49244926
},

lightning/src/offers/async_receive_offer_cache.rs

+58-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::util::ser::{Readable, Writeable, Writer};
2121
use core::time::Duration;
2222
#[cfg(async_payments)]
2323
use {
24-
crate::blinded_path::message::AsyncPaymentsContext,
24+
crate::blinded_path::message::AsyncPaymentsContext, crate::offers::offer::OfferId,
2525
crate::onion_message::async_payments::OfferPaths,
2626
};
2727

@@ -187,6 +187,63 @@ impl AsyncReceiveOfferCache {
187187
self.last_offer_paths_request_timestamp = Duration::from_secs(0);
188188
}
189189

190+
/// Returns an iterator over the list of cached offers where the invoice is expiring soon and we
191+
/// need to send an updated one to the static invoice server.
192+
pub(super) fn offers_needing_invoice_refresh(
193+
&self, duration_since_epoch: Duration,
194+
) -> impl Iterator<Item = (&Offer, Nonce, Duration, &Responder)> {
195+
self.offers.iter().filter_map(move |offer| {
196+
const ONE_DAY: Duration = Duration::from_secs(24 * 60 * 60);
197+
198+
if offer.offer.is_expired_no_std(duration_since_epoch) {
199+
return None;
200+
}
201+
if offer.invoice_update_attempts >= Self::MAX_UPDATE_ATTEMPTS {
202+
return None;
203+
}
204+
205+
let time_until_invoice_expiry =
206+
offer.static_invoice_absolute_expiry.saturating_sub(duration_since_epoch);
207+
let time_until_offer_expiry = offer
208+
.offer
209+
.absolute_expiry()
210+
.unwrap_or_else(|| Duration::from_secs(u64::MAX))
211+
.saturating_sub(duration_since_epoch);
212+
213+
// Update the invoice if it expires in less than a day, as long as the offer has a longer
214+
// expiry than that.
215+
let needs_update = time_until_invoice_expiry < ONE_DAY
216+
&& time_until_offer_expiry > time_until_invoice_expiry;
217+
if needs_update {
218+
Some((
219+
&offer.offer,
220+
offer.offer_nonce,
221+
offer.offer_created_at,
222+
&offer.update_static_invoice_path,
223+
))
224+
} else {
225+
None
226+
}
227+
})
228+
}
229+
230+
/// Indicates that we've sent onion messages attempting to update the static invoice corresponding
231+
/// to the provided offer_id. Calling this method allows the cache to self-limit how many invoice
232+
/// update requests are sent.
233+
///
234+
/// Errors if the offer corresponding to the provided offer_id could not be found.
235+
pub(super) fn increment_invoice_update_attempts(
236+
&mut self, offer_id: OfferId,
237+
) -> Result<(), ()> {
238+
match self.offers.iter_mut().find(|offer| offer.offer.id() == offer_id) {
239+
Some(offer) => {
240+
offer.invoice_update_attempts += 1;
241+
Ok(())
242+
},
243+
None => return Err(()),
244+
}
245+
}
246+
190247
/// Should be called when we receive a [`StaticInvoicePersisted`] message from the static invoice
191248
/// server, which indicates that a new offer was persisted by the server and they are ready to
192249
/// serve the corresponding static invoice to payers on our behalf.

lightning/src/offers/flow.rs

+59-4
Original file line numberDiff line numberDiff line change
@@ -1080,13 +1080,14 @@ where
10801080
core::mem::take(&mut self.pending_dns_onion_messages.lock().unwrap())
10811081
}
10821082

1083-
/// Sends out [`OfferPathsRequest`] onion messages if we are an often-offline recipient and are
1084-
/// configured to interactively build offers and static invoices with a static invoice server.
1083+
/// Sends out [`OfferPathsRequest`] and [`ServeStaticInvoice`] onion messages if we are an
1084+
/// often-offline recipient and are configured to interactively build offers and static invoices
1085+
/// with a static invoice server.
10851086
///
10861087
/// Errors if we failed to create blinded reply paths when sending an [`OfferPathsRequest`] message.
10871088
#[cfg(async_payments)]
10881089
pub(crate) fn check_refresh_async_receive_offers(
1089-
&self, peers: Vec<MessageForwardNode>,
1090+
&self, peers: Vec<MessageForwardNode>, usable_channels: Vec<ChannelDetails>,
10901091
) -> Result<(), ()> {
10911092
// Terminate early if this node does not intend to receive async payments.
10921093
if self.paths_to_static_invoice_server.is_empty() {
@@ -1113,7 +1114,7 @@ where
11131114
path_absolute_expiry: duration_since_epoch
11141115
.saturating_add(REPLY_PATH_RELATIVE_EXPIRY),
11151116
});
1116-
let reply_paths = match self.create_blinded_paths(peers, context) {
1117+
let reply_paths = match self.create_blinded_paths(peers.clone(), context) {
11171118
Ok(paths) => paths,
11181119
Err(()) => {
11191120
return Err(());
@@ -1133,6 +1134,60 @@ where
11331134
);
11341135
}
11351136

1137+
// If a static invoice server has persisted an offer for us but the corresponding invoice is
1138+
// expiring soon, we need to refresh that invoice. Here we create the onion messages that will
1139+
// be used to request invoice refresh, based on the offers provided by the cache.
1140+
let mut serve_static_invoice_messages = Vec::new();
1141+
{
1142+
let cache = self.async_receive_offer_cache.lock().unwrap();
1143+
for offer_and_metadata in cache.offers_needing_invoice_refresh(duration_since_epoch) {
1144+
let (offer, offer_nonce, offer_created_at, update_static_invoice_path) =
1145+
offer_and_metadata;
1146+
let offer_id = offer.id();
1147+
1148+
let (serve_invoice_msg, reply_path_ctx) = match self
1149+
.create_serve_static_invoice_message(
1150+
offer.clone(),
1151+
offer_nonce,
1152+
offer_created_at,
1153+
peers.clone(),
1154+
usable_channels.clone(),
1155+
update_static_invoice_path.clone(),
1156+
) {
1157+
Ok((msg, ctx)) => (msg, ctx),
1158+
Err(()) => continue,
1159+
};
1160+
serve_static_invoice_messages.push((serve_invoice_msg, reply_path_ctx, offer_id));
1161+
}
1162+
}
1163+
1164+
// Enqueue the new serve_static_invoice messages in a separate loop to avoid holding the offer
1165+
// cache lock and the pending_async_payments_messages lock at the same time.
1166+
for (serve_invoice_msg, reply_path_ctx, offer_id) in serve_static_invoice_messages {
1167+
let context = MessageContext::AsyncPayments(reply_path_ctx);
1168+
let reply_paths = match self.create_blinded_paths(peers.clone(), context) {
1169+
Ok(paths) => paths,
1170+
Err(()) => continue,
1171+
};
1172+
1173+
{
1174+
// We can't fail past this point, so indicate to the cache that we've requested an invoice
1175+
// update.
1176+
let mut cache = self.async_receive_offer_cache.lock().unwrap();
1177+
if cache.increment_invoice_update_attempts(offer_id).is_err() {
1178+
continue;
1179+
}
1180+
}
1181+
1182+
let message = AsyncPaymentsMessage::ServeStaticInvoice(serve_invoice_msg);
1183+
enqueue_onion_message_with_reply_paths(
1184+
message,
1185+
&self.paths_to_static_invoice_server,
1186+
reply_paths,
1187+
&mut self.pending_async_payments_messages.lock().unwrap(),
1188+
);
1189+
}
1190+
11361191
Ok(())
11371192
}
11381193

0 commit comments

Comments
 (0)