Skip to content

Commit 5010109

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 06cf5a0 commit 5010109

File tree

4 files changed

+163
-8
lines changed

4 files changed

+163
-8
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5142,7 +5142,17 @@ where
51425142
#[cfg(async_payments)]
51435143
fn check_refresh_async_receive_offers(&self, timer_tick_occurred: bool) {
51445144
let peers = self.get_peers_for_blinded_path();
5145-
match self.flow.check_refresh_async_receive_offers(peers, timer_tick_occurred) {
5145+
let channels = self.list_usable_channels();
5146+
let entropy = &*self.entropy_source;
5147+
let router = &*self.router;
5148+
let refresh_res = self.flow.check_refresh_async_receive_offers(
5149+
peers,
5150+
channels,
5151+
entropy,
5152+
router,
5153+
timer_tick_occurred,
5154+
);
5155+
match refresh_res {
51465156
Err(()) => {
51475157
log_error!(
51485158
self.logger,

lightning/src/offers/async_receive_offer_cache.rs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use crate::util::ser::{Readable, Writeable, Writer};
2323
use core::time::Duration;
2424
#[cfg(async_payments)]
2525
use {
26-
crate::blinded_path::message::AsyncPaymentsContext,
26+
crate::blinded_path::message::AsyncPaymentsContext, crate::offers::offer::OfferId,
2727
crate::onion_message::async_payments::OfferPaths,
2828
};
2929

@@ -259,6 +259,63 @@ impl AsyncReceiveOfferCache {
259259
self.last_offer_paths_request_timestamp = Duration::from_secs(0);
260260
}
261261

262+
/// Returns an iterator over the list of cached offers where the invoice is expiring soon and we
263+
/// need to send an updated one to the static invoice server.
264+
pub(super) fn offers_needing_invoice_refresh(
265+
&self, duration_since_epoch: Duration,
266+
) -> impl Iterator<Item = (&Offer, Nonce, Duration, &Responder)> {
267+
self.offers.iter().filter_map(move |offer| {
268+
const ONE_DAY: Duration = Duration::from_secs(24 * 60 * 60);
269+
270+
if offer.offer.is_expired_no_std(duration_since_epoch) {
271+
return None;
272+
}
273+
if offer.invoice_update_attempts >= MAX_UPDATE_ATTEMPTS {
274+
return None;
275+
}
276+
277+
let time_until_invoice_expiry =
278+
offer.static_invoice_absolute_expiry.saturating_sub(duration_since_epoch);
279+
let time_until_offer_expiry = offer
280+
.offer
281+
.absolute_expiry()
282+
.unwrap_or_else(|| Duration::from_secs(u64::MAX))
283+
.saturating_sub(duration_since_epoch);
284+
285+
// Update the invoice if it expires in less than a day, as long as the offer has a longer
286+
// expiry than that.
287+
let needs_update = time_until_invoice_expiry < ONE_DAY
288+
&& time_until_offer_expiry > time_until_invoice_expiry;
289+
if needs_update {
290+
Some((
291+
&offer.offer,
292+
offer.offer_nonce,
293+
offer.offer_created_at,
294+
&offer.update_static_invoice_path,
295+
))
296+
} else {
297+
None
298+
}
299+
})
300+
}
301+
302+
/// Indicates that we've sent onion messages attempting to update the static invoice corresponding
303+
/// to the provided offer_id. Calling this method allows the cache to self-limit how many invoice
304+
/// update requests are sent.
305+
///
306+
/// Errors if the offer corresponding to the provided offer_id could not be found.
307+
pub(super) fn increment_invoice_update_attempts(
308+
&mut self, offer_id: OfferId,
309+
) -> Result<(), ()> {
310+
match self.offers.iter_mut().find(|offer| offer.offer.id() == offer_id) {
311+
Some(offer) => {
312+
offer.invoice_update_attempts += 1;
313+
Ok(())
314+
},
315+
None => return Err(()),
316+
}
317+
}
318+
262319
/// Should be called when we receive a [`StaticInvoicePersisted`] message from the static invoice
263320
/// server, which indicates that a new offer was persisted by the server and they are ready to
264321
/// serve the corresponding static invoice to payers on our behalf.

lightning/src/offers/flow.rs

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,8 +1128,9 @@ where
11281128
core::mem::take(&mut self.pending_dns_onion_messages.lock().unwrap())
11291129
}
11301130

1131-
/// Sends out [`OfferPathsRequest`] onion messages if we are an often-offline recipient and are
1132-
/// configured to interactively build offers and static invoices with a static invoice server.
1131+
/// Sends out [`OfferPathsRequest`] and [`ServeStaticInvoice`] onion messages if we are an
1132+
/// often-offline recipient and are configured to interactively build offers and static invoices
1133+
/// with a static invoice server.
11331134
///
11341135
/// # Usage
11351136
///
@@ -1141,9 +1142,14 @@ where
11411142
///
11421143
/// Errors if we failed to create blinded reply paths when sending an [`OfferPathsRequest`] message.
11431144
#[cfg(async_payments)]
1144-
pub(crate) fn check_refresh_async_receive_offers(
1145-
&self, peers: Vec<MessageForwardNode>, timer_tick_occurred: bool,
1146-
) -> Result<(), ()> {
1145+
pub(crate) fn check_refresh_async_receive_offers<ES: Deref, R: Deref>(
1146+
&self, peers: Vec<MessageForwardNode>, usable_channels: Vec<ChannelDetails>, entropy: ES,
1147+
router: R, timer_tick_occurred: bool,
1148+
) -> Result<(), ()>
1149+
where
1150+
ES::Target: EntropySource,
1151+
R::Target: Router,
1152+
{
11471153
// Terminate early if this node does not intend to receive async payments.
11481154
if self.paths_to_static_invoice_server.is_empty() {
11491155
return Ok(());
@@ -1165,7 +1171,7 @@ where
11651171
path_absolute_expiry: duration_since_epoch
11661172
.saturating_add(TEMP_REPLY_PATH_RELATIVE_EXPIRY),
11671173
});
1168-
let reply_paths = match self.create_blinded_paths(peers, context) {
1174+
let reply_paths = match self.create_blinded_paths(peers.clone(), context) {
11691175
Ok(paths) => paths,
11701176
Err(()) => {
11711177
return Err(());
@@ -1187,9 +1193,85 @@ where
11871193
);
11881194
}
11891195

1196+
self.check_refresh_static_invoices(peers, usable_channels, entropy, router);
1197+
11901198
Ok(())
11911199
}
11921200

1201+
/// If a static invoice server has persisted an offer for us but the corresponding invoice is
1202+
/// expiring soon, we need to refresh that invoice. Here we enqueue the onion messages that will
1203+
/// be used to request invoice refresh, based on the offers provided by the cache.
1204+
#[cfg(async_payments)]
1205+
fn check_refresh_static_invoices<ES: Deref, R: Deref>(
1206+
&self, peers: Vec<MessageForwardNode>, usable_channels: Vec<ChannelDetails>, entropy: ES,
1207+
router: R,
1208+
) where
1209+
ES::Target: EntropySource,
1210+
R::Target: Router,
1211+
{
1212+
let duration_since_epoch = self.duration_since_epoch();
1213+
1214+
let mut serve_static_invoice_messages = Vec::new();
1215+
{
1216+
let cache = self.async_receive_offer_cache.lock().unwrap();
1217+
for offer_and_metadata in cache.offers_needing_invoice_refresh(duration_since_epoch) {
1218+
let (offer, offer_nonce, offer_created_at, update_static_invoice_path) =
1219+
offer_and_metadata;
1220+
let offer_id = offer.id();
1221+
1222+
let (serve_invoice_msg, reply_path_ctx) = match self
1223+
.create_serve_static_invoice_message(
1224+
offer.clone(),
1225+
offer_nonce,
1226+
offer_created_at,
1227+
peers.clone(),
1228+
usable_channels.clone(),
1229+
update_static_invoice_path.clone(),
1230+
&*entropy,
1231+
&*router,
1232+
) {
1233+
Ok((msg, ctx)) => (msg, ctx),
1234+
Err(()) => continue,
1235+
};
1236+
serve_static_invoice_messages.push((
1237+
serve_invoice_msg,
1238+
update_static_invoice_path.clone(),
1239+
reply_path_ctx,
1240+
offer_id,
1241+
));
1242+
}
1243+
}
1244+
1245+
// Enqueue the new serve_static_invoice messages in a separate loop to avoid holding the offer
1246+
// cache lock and the pending_async_payments_messages lock at the same time.
1247+
for (serve_invoice_msg, serve_invoice_path, reply_path_ctx, offer_id) in
1248+
serve_static_invoice_messages
1249+
{
1250+
let context = MessageContext::AsyncPayments(reply_path_ctx);
1251+
let reply_paths = match self.create_blinded_paths(peers.clone(), context) {
1252+
Ok(paths) => paths,
1253+
Err(()) => continue,
1254+
};
1255+
1256+
{
1257+
// We can't fail past this point, so indicate to the cache that we've requested an invoice
1258+
// update.
1259+
let mut cache = self.async_receive_offer_cache.lock().unwrap();
1260+
if cache.increment_invoice_update_attempts(offer_id).is_err() {
1261+
continue;
1262+
}
1263+
}
1264+
1265+
let message = AsyncPaymentsMessage::ServeStaticInvoice(serve_invoice_msg);
1266+
enqueue_onion_message_with_reply_paths(
1267+
message,
1268+
&[serve_invoice_path.into_reply_path()],
1269+
reply_paths,
1270+
&mut self.pending_async_payments_messages.lock().unwrap(),
1271+
);
1272+
}
1273+
}
1274+
11931275
/// Handles an incoming [`OfferPaths`] message from the static invoice server, sending out
11941276
/// [`ServeStaticInvoice`] onion messages in response if we want to use the paths we've received
11951277
/// to build and cache an async receive offer.

lightning/src/onion_message/messenger.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,12 @@ impl Responder {
432432
context: Some(context),
433433
}
434434
}
435+
436+
/// Converts a [`Responder`] into its inner [`BlindedMessagePath`].
437+
#[cfg(async_payments)]
438+
pub(crate) fn into_reply_path(self) -> BlindedMessagePath {
439+
self.reply_path
440+
}
435441
}
436442

437443
/// Instructions for how and where to send the response to an onion message.

0 commit comments

Comments
 (0)