Skip to content

Commit a26550b

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 ac6e640 commit a26550b

File tree

3 files changed

+184
-0
lines changed

3 files changed

+184
-0
lines changed

lightning/src/blinded_path/message.rs

Lines changed: 91 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.
@@ -331,6 +332,47 @@ pub enum OffersContext {
331332
/// [`Offer`]: crate::offers::offer::Offer
332333
nonce: Nonce,
333334
},
335+
/// Context used by a [`BlindedMessagePath`] within the [`Offer`] of an async recipient on behalf
336+
/// of whom we are serving [`StaticInvoice`]s.
337+
///
338+
/// This variant is intended to be received when handling an [`InvoiceRequest`] on behalf of said
339+
/// async recipient.
340+
///
341+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
342+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
343+
StaticInvoiceRequested {
344+
/// An identifier for the async recipient for whom we are serving [`StaticInvoice`]s. Used to
345+
/// look up a corresponding [`StaticInvoice`] to return to the payer if the recipient is offline.
346+
///
347+
/// Also useful to rate limit the number of [`InvoiceRequest`]s we will respond to on
348+
/// recipient's behalf.
349+
///
350+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
351+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
352+
recipient_id_nonce: Nonce,
353+
354+
/// A nonce used for authenticating that a received [`InvoiceRequest`] is valid for a preceding
355+
/// [`OfferPaths`] message that we sent.
356+
///
357+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
358+
/// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths
359+
nonce: Nonce,
360+
361+
/// Authentication code for the [`InvoiceRequest`].
362+
///
363+
/// Prevents nodes from creating their own blinded path to us and causing us to unintentionally
364+
/// hit our database looking for a [`StaticInvoice`] to return.
365+
///
366+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
367+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
368+
hmac: Hmac<Sha256>,
369+
370+
/// The time as duration since the Unix epoch at which this path expires and messages sent over
371+
/// it should be ignored.
372+
///
373+
/// Useful to timeout async recipients that are no longer supported as clients.
374+
path_absolute_expiry: Duration,
375+
},
334376
/// Context used by a [`BlindedMessagePath`] within a [`Refund`] or as a reply path for an
335377
/// [`InvoiceRequest`].
336378
///
@@ -448,6 +490,43 @@ pub enum AsyncPaymentsContext {
448490
/// is no longer configured to accept paths from them.
449491
path_absolute_expiry: core::time::Duration,
450492
},
493+
/// Context used by a reply path to an [`OfferPaths`] message, provided back to us in
494+
/// corresponding [`ServeStaticInvoice`] messages.
495+
///
496+
/// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths
497+
/// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice
498+
ServeStaticInvoice {
499+
/// An identifier for the async recipient that is requesting that a [`StaticInvoice`] be served
500+
/// on their behalf.
501+
///
502+
/// Useful as a key to retrieve the invoice when payers send an [`InvoiceRequest`] over the
503+
/// paths that we previously created for the recipient's [`Offer::paths`]. Also useful to rate
504+
/// limit the invoices being persisted on behalf of a particular recipient.
505+
///
506+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
507+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
508+
/// [`Offer::paths`]: crate::offers::offer::Offer::paths
509+
recipient_id_nonce: Nonce,
510+
/// A nonce used for authenticating that a [`ServeStaticInvoice`] message is valid for a preceding
511+
/// [`OfferPaths`] message.
512+
///
513+
/// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice
514+
/// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths
515+
nonce: Nonce,
516+
/// Authentication code for the [`ServeStaticInvoice`] message.
517+
///
518+
/// Prevents nodes from creating their own blinded path to us and causing us to persist an
519+
/// unintended [`StaticInvoice`].
520+
///
521+
/// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice
522+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
523+
hmac: Hmac<Sha256>,
524+
/// The time as duration since the Unix epoch at which this path expires and messages sent over
525+
/// it should be ignored.
526+
///
527+
/// Useful to timeout async recipients that are no longer supported as clients.
528+
path_absolute_expiry: core::time::Duration,
529+
},
451530
/// Context used by a reply path to a [`ServeStaticInvoice`] message, provided back to us in
452531
/// corresponding [`StaticInvoicePersisted`] messages.
453532
///
@@ -549,6 +628,12 @@ impl_writeable_tlv_based_enum!(OffersContext,
549628
(1, nonce, required),
550629
(2, hmac, required)
551630
},
631+
(3, StaticInvoiceRequested) => {
632+
(0, recipient_id_nonce, required),
633+
(2, nonce, required),
634+
(4, hmac, required),
635+
(6, path_absolute_expiry, required),
636+
},
552637
);
553638

554639
impl_writeable_tlv_based_enum!(AsyncPaymentsContext,
@@ -578,6 +663,12 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext,
578663
(2, hmac, required),
579664
(4, path_absolute_expiry, required),
580665
},
666+
(5, ServeStaticInvoice) => {
667+
(0, recipient_id_nonce, required),
668+
(2, nonce, required),
669+
(4, hmac, required),
670+
(6, path_absolute_expiry, required),
671+
},
581672
);
582673

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

lightning/src/ln/channelmanager.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12702,6 +12702,56 @@ where
1270212702
&self, _message: OfferPathsRequest, _context: AsyncPaymentsContext,
1270312703
_responder: Option<Responder>,
1270412704
) -> Option<(OfferPaths, ResponseInstruction)> {
12705+
#[cfg(async_payments)] {
12706+
let entropy = &*self.entropy_source;
12707+
let expanded_key = &self.inbound_payment_key;
12708+
let duration_since_epoch = self.duration_since_epoch();
12709+
12710+
let recipient_id_nonce = match _context {
12711+
AsyncPaymentsContext::OfferPathsRequest { recipient_id_nonce, hmac, path_absolute_expiry } => {
12712+
if let Err(()) = signer::verify_offer_paths_request_context(
12713+
recipient_id_nonce, hmac, expanded_key
12714+
) { return None }
12715+
if duration_since_epoch > path_absolute_expiry {
12716+
return None }
12717+
recipient_id_nonce
12718+
},
12719+
_ => return None
12720+
};
12721+
12722+
let (offer_paths, paths_expiry) = {
12723+
// TODO: support longer-lived offers
12724+
const OFFER_PATH_EXPIRY: Duration = Duration::from_secs(30 * 24 * 60 * 60);
12725+
let path_absolute_expiry =
12726+
duration_since_epoch.saturating_add(OFFER_PATH_EXPIRY);
12727+
let nonce = Nonce::from_entropy_source(entropy);
12728+
let hmac = signer::hmac_for_async_recipient_invreq_context(nonce, expanded_key);
12729+
let context = OffersContext::StaticInvoiceRequested {
12730+
recipient_id_nonce, nonce, hmac, path_absolute_expiry
12731+
};
12732+
match self.create_blinded_paths_using_absolute_expiry(
12733+
context, Some(path_absolute_expiry)
12734+
) {
12735+
Ok(paths) => (paths, path_absolute_expiry),
12736+
Err(()) => return None,
12737+
}
12738+
};
12739+
12740+
let reply_path_context = {
12741+
let nonce = Nonce::from_entropy_source(entropy);
12742+
let path_absolute_expiry = duration_since_epoch.saturating_add(REPLY_PATH_RELATIVE_EXPIRY);
12743+
let hmac = signer::hmac_for_serve_static_invoice_context(nonce, expanded_key);
12744+
MessageContext::AsyncPayments(AsyncPaymentsContext::ServeStaticInvoice {
12745+
nonce, recipient_id_nonce, hmac, path_absolute_expiry
12746+
})
12747+
};
12748+
12749+
let offer_paths_om = OfferPaths { paths: offer_paths, paths_absolute_expiry: Some(paths_expiry) };
12750+
return _responder
12751+
.map(|responder| (offer_paths_om, responder.respond_with_reply_path(reply_path_context)))
12752+
}
12753+
12754+
#[cfg(not(async_payments))]
1270512755
None
1270612756
}
1270712757

lightning/src/offers/signer.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,16 @@ const ASYNC_PAYMENTS_STATIC_INV_PERSISTED_INPUT: &[u8; 16] = &[11; 16];
6969
#[cfg(async_payments)]
7070
const ASYNC_PAYMENTS_OFFER_PATHS_REQUEST_INPUT: &[u8; 16] = &[12; 16];
7171

72+
/// HMAC input used in `OffersContext::StaticInvoiceRequested` to authenticate inbound invoice
73+
/// requests that are being serviced on behalf of async recipients.
74+
#[cfg(async_payments)]
75+
const ASYNC_PAYMENTS_INVREQ: &[u8; 16] = &[13; 16];
76+
77+
/// HMAC input used in `AsyncPaymentsContext::ServeStaticInvoice` to authenticate inbound
78+
/// serve_static_invoice onion messages.
79+
#[cfg(async_payments)]
80+
const ASYNC_PAYMENTS_SERVE_STATIC_INVOICE_INPUT: &[u8; 16] = &[14; 16];
81+
7282
/// Message metadata which possibly is derived from [`MetadataMaterial`] such that it can be
7383
/// verified.
7484
#[derive(Clone)]
@@ -583,6 +593,13 @@ pub(crate) fn hmac_for_offer_paths_request_context(
583593
Hmac::from_engine(hmac)
584594
}
585595

596+
#[cfg(async_payments)]
597+
pub(crate) fn verify_offer_paths_request_context(
598+
nonce: Nonce, hmac: Hmac<Sha256>, expanded_key: &ExpandedKey,
599+
) -> Result<(), ()> {
600+
if hmac_for_offer_paths_request_context(nonce, expanded_key) == hmac { Ok(()) } else { Err(()) }
601+
}
602+
586603
#[cfg(async_payments)]
587604
pub(crate) fn hmac_for_offer_paths_context(
588605
nonce: Nonce, expanded_key: &ExpandedKey,
@@ -607,6 +624,19 @@ pub(crate) fn verify_offer_paths_context(
607624
}
608625
}
609626

627+
#[cfg(async_payments)]
628+
pub(crate) fn hmac_for_serve_static_invoice_context(
629+
nonce: Nonce, expanded_key: &ExpandedKey,
630+
) -> Hmac<Sha256> {
631+
const IV_BYTES: &[u8; IV_LEN] = b"LDK Serve Inv~~~";
632+
let mut hmac = expanded_key.hmac_for_offer();
633+
hmac.input(IV_BYTES);
634+
hmac.input(&nonce.0);
635+
hmac.input(ASYNC_PAYMENTS_SERVE_STATIC_INVOICE_INPUT);
636+
637+
Hmac::from_engine(hmac)
638+
}
639+
610640
#[cfg(async_payments)]
611641
pub(crate) fn hmac_for_static_invoice_persisted_context(
612642
nonce: Nonce, expanded_key: &ExpandedKey,
@@ -630,3 +660,16 @@ pub(crate) fn verify_static_invoice_persisted_context(
630660
Err(())
631661
}
632662
}
663+
664+
#[cfg(async_payments)]
665+
pub(crate) fn hmac_for_async_recipient_invreq_context(
666+
nonce: Nonce, expanded_key: &ExpandedKey,
667+
) -> Hmac<Sha256> {
668+
const IV_BYTES: &[u8; IV_LEN] = b"LDK Async Invreq";
669+
let mut hmac = expanded_key.hmac_for_offer();
670+
hmac.input(IV_BYTES);
671+
hmac.input(&nonce.0);
672+
hmac.input(ASYNC_PAYMENTS_INVREQ);
673+
674+
Hmac::from_engine(hmac)
675+
}

0 commit comments

Comments
 (0)