Skip to content

Commit c4e6bd2

Browse files
Send offer paths in response to requests
As part of serving static invoices to payers on behalf of often-offline recipients, we need to provide the async recipient with blinded message paths to include in their offers. Support responding to inbound requests for offer paths from async recipients.
1 parent 2723602 commit c4e6bd2

File tree

3 files changed

+160
-0
lines changed

3 files changed

+160
-0
lines changed

lightning/src/blinded_path/message.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ use bitcoin::hashes::sha256::Hash as Sha256;
3535

3636
use core::mem;
3737
use core::ops::Deref;
38+
use core::time::Duration;
3839

3940
/// A blinded path to be used for sending or receiving a message, hiding the identity of the
4041
/// recipient.
@@ -342,6 +343,43 @@ pub enum OffersContext {
342343
/// [`Offer`]: crate::offers::offer::Offer
343344
nonce: Nonce,
344345
},
346+
/// Context used by a [`BlindedMessagePath`] within the [`Offer`] of an async recipient.
347+
///
348+
/// This variant is received by the static invoice server when handling an [`InvoiceRequest`] on
349+
/// behalf of said async recipient.
350+
///
351+
/// [`Offer`]: crate::offers::offer::Offer
352+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
353+
StaticInvoiceRequested {
354+
/// An identifier for the async recipient for whom we as a static invoice server are serving
355+
/// [`StaticInvoice`]s. Used paired with the
356+
/// [`OffersContext::StaticInvoiceRequested::invoice_id`] when looking up a corresponding
357+
/// [`StaticInvoice`] to return to the payer if the recipient is offline. This id was previously
358+
/// provided via [`AsyncPaymentsContext::ServeStaticInvoice::recipient_id`].
359+
///
360+
/// Also useful for rate limiting the number of [`InvoiceRequest`]s we will respond to on
361+
/// recipient's behalf.
362+
///
363+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
364+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
365+
recipient_id: Vec<u8>,
366+
367+
/// A random unique identifier for a specific [`StaticInvoice`] that the recipient previously
368+
/// requested be served on their behalf. Useful when paired with the
369+
/// [`OffersContext::StaticInvoiceRequested::recipient_id`] to pull that specific invoice from
370+
/// the database when payers send an [`InvoiceRequest`]. This id was previously
371+
/// provided via [`AsyncPaymentsContext::ServeStaticInvoice::invoice_id`].
372+
///
373+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
374+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
375+
invoice_id: u128,
376+
377+
/// The time as duration since the Unix epoch at which this path expires and messages sent over
378+
/// it should be ignored.
379+
///
380+
/// Useful to timeout async recipients that are no longer supported as clients.
381+
path_absolute_expiry: Duration,
382+
},
345383
/// Context used by a [`BlindedMessagePath`] within a [`Refund`] or as a reply path for an
346384
/// [`InvoiceRequest`].
347385
///
@@ -438,6 +476,38 @@ pub enum AsyncPaymentsContext {
438476
/// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths
439477
path_absolute_expiry: core::time::Duration,
440478
},
479+
/// Context used by a reply path to an [`OfferPaths`] message, provided back to us as the static
480+
/// invoice server in corresponding [`ServeStaticInvoice`] messages.
481+
///
482+
/// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths
483+
/// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice
484+
ServeStaticInvoice {
485+
/// An identifier for the async recipient that is requesting that a [`StaticInvoice`] be served
486+
/// on their behalf.
487+
///
488+
/// Useful for retrieving the invoice when payers send an [`InvoiceRequest`] to us as the static
489+
/// invoice server. Also useful to rate limit the invoices being persisted on behalf of a
490+
/// particular recipient. This id will be provided back to us as the static invoice server via
491+
/// [`OffersContext::StaticInvoiceRequested::recipient_id`]
492+
///
493+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
494+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
495+
recipient_id: Vec<u8>,
496+
/// A random unique identifier for the specific [`StaticInvoice`] that the recipient is
497+
/// requesting be served on their behalf. Useful when surfaced alongside the above
498+
/// `recipient_id` when payers send an [`InvoiceRequest`], to pull the specific static invoice
499+
/// from the database. This id will be provided back to us as the static invoice server via
500+
/// [`OffersContext::StaticInvoiceRequested::invoice_id`]
501+
///
502+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
503+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
504+
invoice_id: u128,
505+
/// The time as duration since the Unix epoch at which this path expires and messages sent over
506+
/// it should be ignored.
507+
///
508+
/// Useful to timeout async recipients that are no longer supported as clients.
509+
path_absolute_expiry: core::time::Duration,
510+
},
441511
/// Context used by a reply path to a [`ServeStaticInvoice`] message, provided back to us in
442512
/// corresponding [`StaticInvoicePersisted`] messages.
443513
///
@@ -526,6 +596,11 @@ impl_writeable_tlv_based_enum!(OffersContext,
526596
(1, nonce, required),
527597
(2, hmac, required)
528598
},
599+
(3, StaticInvoiceRequested) => {
600+
(0, recipient_id, required),
601+
(2, invoice_id, required),
602+
(4, path_absolute_expiry, required),
603+
},
529604
);
530605

531606
impl_writeable_tlv_based_enum!(AsyncPaymentsContext,
@@ -550,6 +625,11 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext,
550625
(0, recipient_id, required),
551626
(2, path_absolute_expiry, required),
552627
},
628+
(5, ServeStaticInvoice) => {
629+
(0, recipient_id, required),
630+
(2, invoice_id, required),
631+
(4, path_absolute_expiry, required),
632+
},
553633
);
554634

555635
/// Contains a simple nonce for use in a blinded path's context.

lightning/src/ln/channelmanager.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13482,6 +13482,19 @@ where
1348213482
&self, _message: OfferPathsRequest, _context: AsyncPaymentsContext,
1348313483
_responder: Option<Responder>,
1348413484
) -> Option<(OfferPaths, ResponseInstruction)> {
13485+
#[cfg(async_payments)]
13486+
{
13487+
let peers = self.get_peers_for_blinded_path();
13488+
let entropy = &*self.entropy_source;
13489+
let (message, reply_path_context) =
13490+
match self.flow.handle_offer_paths_request(_context, peers, entropy) {
13491+
Some(msg) => msg,
13492+
None => return None,
13493+
};
13494+
_responder.map(|resp| (message, resp.respond_with_reply_path(reply_path_context)))
13495+
}
13496+
13497+
#[cfg(not(async_payments))]
1348513498
None
1348613499
}
1348713500

lightning/src/offers/flow.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,10 @@ const OFFERS_MESSAGE_REQUEST_LIMIT: usize = 10;
246246
#[cfg(async_payments)]
247247
const TEMP_REPLY_PATH_RELATIVE_EXPIRY: Duration = Duration::from_secs(7200);
248248

249+
// Default to async receive offers and the paths used to update them lasting one year.
250+
#[cfg(async_payments)]
251+
const DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY: Duration = Duration::from_secs(365 * 24 * 60 * 60);
252+
249253
impl<MR: Deref> OffersMessageFlow<MR>
250254
where
251255
MR::Target: MessageRouter,
@@ -1309,6 +1313,69 @@ where
13091313
}
13101314
}
13111315

1316+
/// Handles an incoming [`OfferPathsRequest`] onion message from an often-offline recipient who
1317+
/// wants us (the static invoice server) to serve [`StaticInvoice`]s to payers on their behalf.
1318+
/// Sends out [`OfferPaths`] onion messages in response.
1319+
#[cfg(async_payments)]
1320+
pub(crate) fn handle_offer_paths_request<ES: Deref>(
1321+
&self, context: AsyncPaymentsContext, peers: Vec<MessageForwardNode>, entropy_source: ES,
1322+
) -> Option<(OfferPaths, MessageContext)>
1323+
where
1324+
ES::Target: EntropySource,
1325+
{
1326+
let duration_since_epoch = self.duration_since_epoch();
1327+
1328+
let recipient_id = match context {
1329+
AsyncPaymentsContext::OfferPathsRequest { recipient_id, path_absolute_expiry } => {
1330+
if duration_since_epoch > path_absolute_expiry {
1331+
return None;
1332+
}
1333+
recipient_id
1334+
},
1335+
_ => return None,
1336+
};
1337+
1338+
let mut random_bytes = [0u8; 16];
1339+
random_bytes.copy_from_slice(&entropy_source.get_secure_random_bytes()[..16]);
1340+
let invoice_id = u128::from_be_bytes(random_bytes);
1341+
1342+
// Create the blinded paths that will be included in the async recipient's offer.
1343+
let (offer_paths, paths_expiry) = {
1344+
let path_absolute_expiry =
1345+
duration_since_epoch.saturating_add(DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY);
1346+
let context = OffersContext::StaticInvoiceRequested {
1347+
recipient_id: recipient_id.clone(),
1348+
path_absolute_expiry,
1349+
invoice_id,
1350+
};
1351+
match self.create_blinded_paths_using_absolute_expiry(
1352+
context,
1353+
Some(path_absolute_expiry),
1354+
peers,
1355+
) {
1356+
Ok(paths) => (paths, path_absolute_expiry),
1357+
Err(()) => return None,
1358+
}
1359+
};
1360+
1361+
// Create a reply path so that the recipient can respond to our offer_paths message with the
1362+
// static invoice that they create. This path will also be used by the recipient to update said
1363+
// invoice.
1364+
let reply_path_context = {
1365+
let path_absolute_expiry =
1366+
duration_since_epoch.saturating_add(DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY);
1367+
MessageContext::AsyncPayments(AsyncPaymentsContext::ServeStaticInvoice {
1368+
recipient_id,
1369+
invoice_id,
1370+
path_absolute_expiry,
1371+
})
1372+
};
1373+
1374+
let offer_paths_om =
1375+
OfferPaths { paths: offer_paths, paths_absolute_expiry: Some(paths_expiry.as_secs()) };
1376+
return Some((offer_paths_om, reply_path_context));
1377+
}
1378+
13121379
/// Handles an incoming [`OfferPaths`] message from the static invoice server, sending out
13131380
/// [`ServeStaticInvoice`] onion messages in response if we've built a new async receive offer and
13141381
/// need the corresponding [`StaticInvoice`] to be persisted by the static invoice server.

0 commit comments

Comments
 (0)