Skip to content

Commit 15f9f64

Browse files
Refresh served static invoices on each timer tick
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. We want the static invoice server to always have the freshest possible invoice available, so on each timer tick for every offer that is either currently in use or pending confirmation, send them a new invoice.
1 parent c22f122 commit 15f9f64

File tree

4 files changed

+130
-8
lines changed

4 files changed

+130
-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: 25 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

2525
/// The status of this offer in the cache.
26-
#[derive(Clone)]
26+
#[derive(Clone, PartialEq)]
2727
enum OfferStatus {
2828
/// This offer has been returned to the user from the cache, so it needs to be stored until it
2929
/// expires and its invoice needs to be kept updated.
@@ -317,6 +317,30 @@ impl AsyncReceiveOfferCache {
317317
self.last_offer_paths_request_timestamp = Duration::from_secs(0);
318318
}
319319

320+
/// Returns an iterator over the list of cached offers where we need to send an updated invoice to
321+
/// the static invoice server.
322+
pub(super) fn offers_needing_invoice_refresh(
323+
&self,
324+
) -> impl Iterator<Item = (&Offer, Nonce, u8, &Responder)> {
325+
// For any offers which are either in use or pending confirmation by the server, we should send
326+
// them a fresh invoice on each timer tick.
327+
self.offers_with_idx().filter_map(|(idx, offer)| {
328+
let needs_invoice_update =
329+
offer.status == OfferStatus::Used || offer.status == OfferStatus::Pending;
330+
if needs_invoice_update {
331+
let offer_slot = idx.try_into().unwrap_or(u8::MAX);
332+
Some((
333+
&offer.offer,
334+
offer.offer_nonce,
335+
offer_slot,
336+
&offer.update_static_invoice_path,
337+
))
338+
} else {
339+
None
340+
}
341+
})
342+
}
343+
320344
/// Should be called when we receive a [`StaticInvoicePersisted`] message from the static invoice
321345
/// server, which indicates that a new offer was persisted by the server and they are ready to
322346
/// 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
@@ -1127,8 +1127,9 @@ where
11271127
core::mem::take(&mut self.pending_dns_onion_messages.lock().unwrap())
11281128
}
11291129

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

1197+
if timer_tick_occurred {
1198+
self.check_refresh_static_invoices(peers, usable_channels, entropy, router);
1199+
}
1200+
11911201
Ok(())
11921202
}
11931203

1204+
/// Enqueue onion messages that will used to request invoice refresh from the static invoice
1205+
/// server, based on the offers provided by the cache.
1206+
#[cfg(async_payments)]
1207+
fn check_refresh_static_invoices<ES: Deref, R: Deref>(
1208+
&self, peers: Vec<MessageForwardNode>, usable_channels: Vec<ChannelDetails>, entropy: ES,
1209+
router: R,
1210+
) where
1211+
ES::Target: EntropySource,
1212+
R::Target: Router,
1213+
{
1214+
let duration_since_epoch = self.duration_since_epoch();
1215+
1216+
let mut serve_static_invoice_messages = Vec::new();
1217+
{
1218+
let cache = self.async_receive_offer_cache.lock().unwrap();
1219+
for offer_and_metadata in cache.offers_needing_invoice_refresh() {
1220+
let (offer, offer_nonce, offer_slot, update_static_invoice_path) =
1221+
offer_and_metadata;
1222+
1223+
let (invoice, forward_invreq_path) = match self.create_static_invoice_for_server(
1224+
offer,
1225+
offer_nonce,
1226+
peers.clone(),
1227+
usable_channels.clone(),
1228+
&*entropy,
1229+
&*router,
1230+
) {
1231+
Ok((invoice, path)) => (invoice, path),
1232+
Err(()) => continue,
1233+
};
1234+
1235+
let reply_path_context = {
1236+
let path_absolute_expiry =
1237+
duration_since_epoch.saturating_add(TEMP_REPLY_PATH_RELATIVE_EXPIRY);
1238+
MessageContext::AsyncPayments(AsyncPaymentsContext::StaticInvoicePersisted {
1239+
path_absolute_expiry,
1240+
offer_slot,
1241+
})
1242+
};
1243+
1244+
let serve_invoice_message = ServeStaticInvoice {
1245+
invoice,
1246+
forward_invoice_request_path: forward_invreq_path,
1247+
invoice_slot: offer_slot,
1248+
};
1249+
serve_static_invoice_messages.push((
1250+
serve_invoice_message,
1251+
update_static_invoice_path.clone(),
1252+
reply_path_context,
1253+
));
1254+
}
1255+
}
1256+
1257+
// Enqueue the new serve_static_invoice messages in a separate loop to avoid holding the offer
1258+
// cache lock and the pending_async_payments_messages lock at the same time.
1259+
for (serve_invoice_msg, serve_invoice_path, reply_path_ctx) in serve_static_invoice_messages
1260+
{
1261+
let reply_paths = match self.create_blinded_paths(peers.clone(), reply_path_ctx) {
1262+
Ok(paths) => paths,
1263+
Err(()) => continue,
1264+
};
1265+
1266+
let message = AsyncPaymentsMessage::ServeStaticInvoice(serve_invoice_msg);
1267+
enqueue_onion_message_with_reply_paths(
1268+
message,
1269+
&[serve_invoice_path.into_blinded_path()],
1270+
reply_paths,
1271+
&mut self.pending_async_payments_messages.lock().unwrap(),
1272+
);
1273+
}
1274+
}
1275+
11941276
/// Handles an incoming [`OfferPaths`] message from the static invoice server, sending out
11951277
/// [`ServeStaticInvoice`] onion messages in response if we want to use the paths we've received
11961278
/// 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_blinded_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)