Skip to content

Commit bd9add5

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 fe11112 commit bd9add5

File tree

4 files changed

+235
-0
lines changed

4 files changed

+235
-0
lines changed

lightning/src/blinded_path/message.rs

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

3737
use core::mem;
3838
use core::ops::Deref;
39+
use core::time::Duration;
3940

4041
/// A blinded path to be used for sending or receiving a message, hiding the identity of the
4142
/// recipient.
@@ -343,6 +344,47 @@ pub enum OffersContext {
343344
/// [`Offer`]: crate::offers::offer::Offer
344345
nonce: Nonce,
345346
},
347+
/// Context used by a [`BlindedMessagePath`] within the [`Offer`] of an async recipient.
348+
///
349+
/// This variant is received by the static invoice server when handling an [`InvoiceRequest`] on
350+
/// behalf of said async recipient.
351+
///
352+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
353+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
354+
StaticInvoiceRequested {
355+
/// An identifier for the async recipient for whom the static invoice server is serving
356+
/// [`StaticInvoice`]s. Used to look up a corresponding [`StaticInvoice`] to return to the payer
357+
/// if the recipient is offline.
358+
///
359+
/// Also useful for the server to rate limit the number of [`InvoiceRequest`]s it will respond
360+
/// to on recipient's behalf.
361+
///
362+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
363+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
364+
recipient_id_nonce: Nonce,
365+
366+
/// A nonce used for authenticating that a received [`InvoiceRequest`] is valid for a preceding
367+
/// [`OfferPaths`] message sent by the static invoice server.
368+
///
369+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
370+
/// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths
371+
nonce: Nonce,
372+
373+
/// Authentication code for the [`InvoiceRequest`].
374+
///
375+
/// Prevents nodes from creating their own blinded path to the static invoice server and causing
376+
/// them to unintentionally hit their database looking for a [`StaticInvoice`] to return.
377+
///
378+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
379+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
380+
hmac: Hmac<Sha256>,
381+
382+
/// The time as duration since the Unix epoch at which this path expires and messages sent over
383+
/// it should be ignored.
384+
///
385+
/// Useful to timeout async recipients that are no longer supported as clients.
386+
path_absolute_expiry: Duration,
387+
},
346388
/// Context used by a [`BlindedMessagePath`] within a [`Refund`] or as a reply path for an
347389
/// [`InvoiceRequest`].
348390
///
@@ -459,6 +501,43 @@ pub enum AsyncPaymentsContext {
459501
/// offer paths if we are no longer configured to accept paths from them.
460502
path_absolute_expiry: core::time::Duration,
461503
},
504+
/// Context used by a reply path to an [`OfferPaths`] message, provided back to the static invoice
505+
/// server in corresponding [`ServeStaticInvoice`] messages.
506+
///
507+
/// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths
508+
/// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice
509+
ServeStaticInvoice {
510+
/// An identifier for the async recipient that is requesting that a [`StaticInvoice`] be served
511+
/// on their behalf.
512+
///
513+
/// Useful as a key to retrieve the invoice when payers send an [`InvoiceRequest`] to the static
514+
/// invoice server. Also useful to rate limit the invoices being persisted on behalf of a
515+
/// particular recipient.
516+
///
517+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
518+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
519+
/// [`Offer::paths`]: crate::offers::offer::Offer::paths
520+
recipient_id_nonce: Nonce,
521+
/// A nonce used for authenticating that a [`ServeStaticInvoice`] message is valid for a preceding
522+
/// [`OfferPaths`] message.
523+
///
524+
/// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice
525+
/// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths
526+
nonce: Nonce,
527+
/// Authentication code for the [`ServeStaticInvoice`] message.
528+
///
529+
/// Prevents nodes from creating their own blinded path to the static invoice server and causing
530+
/// them to persist an unintended [`StaticInvoice`].
531+
///
532+
/// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice
533+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
534+
hmac: Hmac<Sha256>,
535+
/// The time as duration since the Unix epoch at which this path expires and messages sent over
536+
/// it should be ignored.
537+
///
538+
/// Useful to timeout async recipients that are no longer supported as clients.
539+
path_absolute_expiry: core::time::Duration,
540+
},
462541
/// Context used by a reply path to a [`ServeStaticInvoice`] message, provided back to us in
463542
/// corresponding [`StaticInvoicePersisted`] messages.
464543
///
@@ -580,6 +659,12 @@ impl_writeable_tlv_based_enum!(OffersContext,
580659
(1, nonce, required),
581660
(2, hmac, required)
582661
},
662+
(3, StaticInvoiceRequested) => {
663+
(0, recipient_id_nonce, required),
664+
(2, nonce, required),
665+
(4, hmac, required),
666+
(6, path_absolute_expiry, required),
667+
},
583668
);
584669

585670
impl_writeable_tlv_based_enum!(AsyncPaymentsContext,
@@ -613,6 +698,12 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext,
613698
(2, hmac, required),
614699
(4, path_absolute_expiry, required),
615700
},
701+
(5, ServeStaticInvoice) => {
702+
(0, recipient_id_nonce, required),
703+
(2, nonce, required),
704+
(4, hmac, required),
705+
(6, path_absolute_expiry, required),
706+
},
616707
);
617708

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

lightning/src/ln/channelmanager.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12887,6 +12887,21 @@ where
1288712887
&self, _message: OfferPathsRequest, _context: AsyncPaymentsContext,
1288812888
_responder: Option<Responder>,
1288912889
) -> Option<(OfferPaths, ResponseInstruction)> {
12890+
#[cfg(async_payments)]
12891+
{
12892+
let peers = self.get_peers_for_blinded_path();
12893+
let (message, reply_path_context) = match self.flow.handle_offer_paths_request(
12894+
_context,
12895+
peers,
12896+
&*self.entropy_source,
12897+
) {
12898+
Some(msg) => msg,
12899+
None => return None,
12900+
};
12901+
_responder.map(|resp| (message, resp.respond_with_reply_path(reply_path_context)))
12902+
}
12903+
12904+
#[cfg(not(async_payments))]
1289012905
None
1289112906
}
1289212907

lightning/src/offers/flow.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@ const OFFERS_MESSAGE_REQUEST_LIMIT: usize = 10;
228228
#[cfg(async_payments)]
229229
const TEMP_REPLY_PATH_RELATIVE_EXPIRY: Duration = Duration::from_secs(7200);
230230

231+
// Default to async receive offers and the paths used to update them lasting 1 year.
232+
#[cfg(async_payments)]
233+
const DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY: Duration = Duration::from_secs(365 * 24 * 60 * 60);
234+
231235
impl<MR: Deref> OffersMessageFlow<MR>
232236
where
233237
MR::Target: MessageRouter,
@@ -1314,6 +1318,84 @@ where
13141318
}
13151319
}
13161320

1321+
/// Handles an incoming [`OfferPathsRequest`] onion message from an often-offline recipient who
1322+
/// wants us (the static invoice server) to serve [`StaticInvoice`]s to payers on their behalf.
1323+
/// Sends out [`OfferPaths`] onion messages in response.
1324+
#[cfg(async_payments)]
1325+
pub(crate) fn handle_offer_paths_request<ES: Deref>(
1326+
&self, context: AsyncPaymentsContext, peers: Vec<MessageForwardNode>, entropy: ES,
1327+
) -> Option<(OfferPaths, MessageContext)>
1328+
where
1329+
ES::Target: EntropySource,
1330+
{
1331+
let expanded_key = &self.inbound_payment_key;
1332+
let duration_since_epoch = self.duration_since_epoch();
1333+
1334+
// First verify the message context to make sure we created the blinded path that this message
1335+
// was received over.
1336+
let recipient_id_nonce = match context {
1337+
AsyncPaymentsContext::OfferPathsRequest {
1338+
recipient_id_nonce,
1339+
hmac,
1340+
path_absolute_expiry,
1341+
} => {
1342+
if let Err(()) = signer::verify_offer_paths_request_context(
1343+
recipient_id_nonce,
1344+
hmac,
1345+
expanded_key,
1346+
) {
1347+
return None;
1348+
}
1349+
if duration_since_epoch > path_absolute_expiry {
1350+
return None;
1351+
}
1352+
recipient_id_nonce
1353+
},
1354+
_ => return None,
1355+
};
1356+
1357+
// Next create the blinded paths that will be included in the async recipient's offer.
1358+
let (offer_paths, paths_expiry) = {
1359+
let path_absolute_expiry =
1360+
duration_since_epoch.saturating_add(DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY);
1361+
let nonce = Nonce::from_entropy_source(&*entropy);
1362+
let hmac = signer::hmac_for_async_recipient_invreq_context(nonce, expanded_key);
1363+
let context = OffersContext::StaticInvoiceRequested {
1364+
recipient_id_nonce,
1365+
nonce,
1366+
hmac,
1367+
path_absolute_expiry,
1368+
};
1369+
match self.create_blinded_paths_using_absolute_expiry(
1370+
context,
1371+
Some(path_absolute_expiry),
1372+
peers,
1373+
) {
1374+
Ok(paths) => (paths, path_absolute_expiry),
1375+
Err(()) => return None,
1376+
}
1377+
};
1378+
1379+
// Finally create a reply path so that the recipient can respond to our offer_paths message with
1380+
// the static invoice that they create, that corresponds to the offer containing our paths.
1381+
let reply_path_context = {
1382+
let nonce = Nonce::from_entropy_source(entropy);
1383+
let path_absolute_expiry =
1384+
duration_since_epoch.saturating_add(DEFAULT_ASYNC_RECEIVE_OFFER_EXPIRY);
1385+
let hmac = signer::hmac_for_serve_static_invoice_context(nonce, expanded_key);
1386+
MessageContext::AsyncPayments(AsyncPaymentsContext::ServeStaticInvoice {
1387+
nonce,
1388+
recipient_id_nonce,
1389+
hmac,
1390+
path_absolute_expiry,
1391+
})
1392+
};
1393+
1394+
let offer_paths_om =
1395+
OfferPaths { paths: offer_paths, paths_absolute_expiry: Some(paths_expiry) };
1396+
return Some((offer_paths_om, reply_path_context));
1397+
}
1398+
13171399
/// Handles an incoming [`OfferPaths`] message from the static invoice server, sending out
13181400
/// [`ServeStaticInvoice`] onion messages in response if we want to use the paths we've received
13191401
/// to build and cache an async receive offer.

lightning/src/offers/signer.rs

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

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

612+
#[cfg(async_payments)]
613+
pub(crate) fn verify_offer_paths_request_context(
614+
nonce: Nonce, hmac: Hmac<Sha256>, expanded_key: &ExpandedKey,
615+
) -> Result<(), ()> {
616+
if hmac_for_offer_paths_request_context(nonce, expanded_key) == hmac {
617+
Ok(())
618+
} else {
619+
Err(())
620+
}
621+
}
622+
602623
#[cfg(async_payments)]
603624
pub(crate) fn hmac_for_offer_paths_context(
604625
nonce: Nonce, expanded_key: &ExpandedKey,
@@ -623,6 +644,19 @@ pub(crate) fn verify_offer_paths_context(
623644
}
624645
}
625646

647+
#[cfg(async_payments)]
648+
pub(crate) fn hmac_for_serve_static_invoice_context(
649+
nonce: Nonce, expanded_key: &ExpandedKey,
650+
) -> Hmac<Sha256> {
651+
const IV_BYTES: &[u8; IV_LEN] = b"LDK Serve Inv~~~";
652+
let mut hmac = expanded_key.hmac_for_offer();
653+
hmac.input(IV_BYTES);
654+
hmac.input(&nonce.0);
655+
hmac.input(ASYNC_PAYMENTS_SERVE_STATIC_INVOICE_INPUT);
656+
657+
Hmac::from_engine(hmac)
658+
}
659+
626660
#[cfg(async_payments)]
627661
pub(crate) fn hmac_for_static_invoice_persisted_context(
628662
nonce: Nonce, expanded_key: &ExpandedKey,
@@ -646,3 +680,16 @@ pub(crate) fn verify_static_invoice_persisted_context(
646680
Err(())
647681
}
648682
}
683+
684+
#[cfg(async_payments)]
685+
pub(crate) fn hmac_for_async_recipient_invreq_context(
686+
nonce: Nonce, expanded_key: &ExpandedKey,
687+
) -> Hmac<Sha256> {
688+
const IV_BYTES: &[u8; IV_LEN] = b"LDK Async Invreq";
689+
let mut hmac = expanded_key.hmac_for_offer();
690+
hmac.input(IV_BYTES);
691+
hmac.input(&nonce.0);
692+
hmac.input(ASYNC_PAYMENTS_INVREQ);
693+
694+
Hmac::from_engine(hmac)
695+
}

0 commit comments

Comments
 (0)