Skip to content

Commit cc9c671

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 ade9f8e commit cc9c671

File tree

3 files changed

+163
-0
lines changed

3 files changed

+163
-0
lines changed

lightning/src/blinded_path/message.rs

Lines changed: 83 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,41 @@ 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 when surfaced alongside the below `invoice_id` when payers send an
489+
/// [`InvoiceRequest`], to pull the specific static invoice from the database.
490+
///
491+
/// Also useful to rate limit the invoices being persisted on behalf of a particular recipient.
492+
///
493+
/// This id will be provided back to us as the static invoice server via
494+
/// [`OffersContext::StaticInvoiceRequested::recipient_id`].
495+
///
496+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
497+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
498+
recipient_id: Vec<u8>,
499+
/// A random identifier for the specific [`StaticInvoice`] that the recipient is requesting be
500+
/// served on their behalf. Useful when surfaced alongside the above `recipient_id` when payers
501+
/// send an [`InvoiceRequest`], to pull the specific static invoice from the database. This id
502+
/// will be provided back to us as the static invoice server via
503+
/// [`OffersContext::StaticInvoiceRequested::invoice_id`].
504+
///
505+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
506+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
507+
invoice_id: u128,
508+
/// The time as duration since the Unix epoch at which this path expires and messages sent over
509+
/// it should be ignored.
510+
///
511+
/// Useful to timeout async recipients that are no longer supported as clients.
512+
path_absolute_expiry: core::time::Duration,
513+
},
441514
/// Context used by a reply path to a [`ServeStaticInvoice`] message, provided back to us in
442515
/// corresponding [`StaticInvoicePersisted`] messages.
443516
///
@@ -526,6 +599,11 @@ impl_writeable_tlv_based_enum!(OffersContext,
526599
(1, nonce, required),
527600
(2, hmac, required)
528601
},
602+
(3, StaticInvoiceRequested) => {
603+
(0, recipient_id, required),
604+
(2, invoice_id, required),
605+
(4, path_absolute_expiry, required),
606+
},
529607
);
530608

531609
impl_writeable_tlv_based_enum!(AsyncPaymentsContext,
@@ -550,6 +628,11 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext,
550628
(0, recipient_id, required),
551629
(2, path_absolute_expiry, option),
552630
},
631+
(5, ServeStaticInvoice) => {
632+
(0, recipient_id, required),
633+
(2, invoice_id, required),
634+
(4, path_absolute_expiry, required),
635+
},
553636
);
554637

555638
/// 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,
@@ -1315,6 +1319,69 @@ where
13151319
}
13161320
}
13171321

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

0 commit comments

Comments
 (0)