Skip to content

Commit 4127d9e

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 21d474a commit 4127d9e

File tree

4 files changed

+223
-0
lines changed

4 files changed

+223
-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 on behalf
348+
/// of whom we are serving [`StaticInvoice`]s.
349+
///
350+
/// This variant is intended to be received when handling an [`InvoiceRequest`] on behalf of said
351+
/// async recipient.
352+
///
353+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
354+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
355+
StaticInvoiceRequested {
356+
/// An identifier for the async recipient for whom we are serving [`StaticInvoice`]s. Used to
357+
/// look up a corresponding [`StaticInvoice`] to return to the payer if the recipient is offline.
358+
///
359+
/// Also useful to rate limit the number of [`InvoiceRequest`]s we will respond to on
360+
/// 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 that we sent.
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 us and causing us to unintentionally
376+
/// hit our 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
///
@@ -460,6 +502,43 @@ pub enum AsyncPaymentsContext {
460502
/// is no longer configured to accept paths from them.
461503
path_absolute_expiry: core::time::Duration,
462504
},
505+
/// Context used by a reply path to an [`OfferPaths`] message, provided back to us in
506+
/// corresponding [`ServeStaticInvoice`] messages.
507+
///
508+
/// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths
509+
/// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice
510+
ServeStaticInvoice {
511+
/// An identifier for the async recipient that is requesting that a [`StaticInvoice`] be served
512+
/// on their behalf.
513+
///
514+
/// Useful as a key to retrieve the invoice when payers send an [`InvoiceRequest`] over the
515+
/// paths that we previously created for the recipient's [`Offer::paths`]. Also useful to rate
516+
/// limit the invoices being persisted on behalf of a particular recipient.
517+
///
518+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
519+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
520+
/// [`Offer::paths`]: crate::offers::offer::Offer::paths
521+
recipient_id_nonce: Nonce,
522+
/// A nonce used for authenticating that a [`ServeStaticInvoice`] message is valid for a preceding
523+
/// [`OfferPaths`] message.
524+
///
525+
/// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice
526+
/// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths
527+
nonce: Nonce,
528+
/// Authentication code for the [`ServeStaticInvoice`] message.
529+
///
530+
/// Prevents nodes from creating their own blinded path to us and causing us to persist an
531+
/// unintended [`StaticInvoice`].
532+
///
533+
/// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice
534+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
535+
hmac: Hmac<Sha256>,
536+
/// The time as duration since the Unix epoch at which this path expires and messages sent over
537+
/// it should be ignored.
538+
///
539+
/// Useful to timeout async recipients that are no longer supported as clients.
540+
path_absolute_expiry: core::time::Duration,
541+
},
463542
/// Context used by a reply path to a [`ServeStaticInvoice`] message, provided back to us in
464543
/// corresponding [`StaticInvoicePersisted`] messages.
465544
///
@@ -581,6 +660,12 @@ impl_writeable_tlv_based_enum!(OffersContext,
581660
(1, nonce, required),
582661
(2, hmac, required)
583662
},
663+
(3, StaticInvoiceRequested) => {
664+
(0, recipient_id_nonce, required),
665+
(2, nonce, required),
666+
(4, hmac, required),
667+
(6, path_absolute_expiry, required),
668+
},
584669
);
585670

586671
impl_writeable_tlv_based_enum!(AsyncPaymentsContext,
@@ -614,6 +699,12 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext,
614699
(2, hmac, required),
615700
(4, path_absolute_expiry, required),
616701
},
702+
(5, ServeStaticInvoice) => {
703+
(0, recipient_id_nonce, required),
704+
(2, nonce, required),
705+
(4, hmac, required),
706+
(6, path_absolute_expiry, required),
707+
},
617708
);
618709

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

lightning/src/ln/channelmanager.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12472,6 +12472,18 @@ where
1247212472
&self, _message: OfferPathsRequest, _context: AsyncPaymentsContext,
1247312473
_responder: Option<Responder>,
1247412474
) -> Option<(OfferPaths, ResponseInstruction)> {
12475+
#[cfg(async_payments)] {
12476+
let peers = self.get_peers_for_blinded_path();
12477+
let (message, reply_path_context) = match self.flow.handle_offer_paths_request(
12478+
_context, peers, &*self.entropy_source
12479+
) {
12480+
Some(msg) => msg,
12481+
None => return None,
12482+
};
12483+
_responder.map(|resp| (message, resp.respond_with_reply_path(reply_path_context)))
12484+
}
12485+
12486+
#[cfg(not(async_payments))]
1247512487
None
1247612488
}
1247712489

lightning/src/offers/flow.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1295,6 +1295,79 @@ where
12951295
}
12961296
}
12971297

1298+
/// Handles an incoming [`OfferPathsRequest`] onion message from an often-offline recipient who
1299+
/// wants us to serve [`StaticInvoice`]s to payers on their behalf. Sends out [`OfferPaths`] onion
1300+
/// messages in response.
1301+
#[cfg(async_payments)]
1302+
pub(crate) fn handle_offer_paths_request<ES: Deref>(
1303+
&self, context: AsyncPaymentsContext, peers: Vec<MessageForwardNode>, entropy: ES,
1304+
) -> Option<(OfferPaths, MessageContext)>
1305+
where
1306+
ES::Target: EntropySource,
1307+
{
1308+
let expanded_key = &self.inbound_payment_key;
1309+
let duration_since_epoch = self.duration_since_epoch();
1310+
1311+
let recipient_id_nonce = match context {
1312+
AsyncPaymentsContext::OfferPathsRequest {
1313+
recipient_id_nonce,
1314+
hmac,
1315+
path_absolute_expiry,
1316+
} => {
1317+
if let Err(()) = signer::verify_offer_paths_request_context(
1318+
recipient_id_nonce,
1319+
hmac,
1320+
expanded_key,
1321+
) {
1322+
return None;
1323+
}
1324+
if duration_since_epoch > path_absolute_expiry {
1325+
return None;
1326+
}
1327+
recipient_id_nonce
1328+
},
1329+
_ => return None,
1330+
};
1331+
1332+
// Default to offers and the paths used to update them lasting 1 year.
1333+
const OFFER_PATH_EXPIRY: Duration = Duration::from_secs(365 * 24 * 60 * 60);
1334+
let (offer_paths, paths_expiry) = {
1335+
let path_absolute_expiry = duration_since_epoch.saturating_add(OFFER_PATH_EXPIRY);
1336+
let nonce = Nonce::from_entropy_source(&*entropy);
1337+
let hmac = signer::hmac_for_async_recipient_invreq_context(nonce, expanded_key);
1338+
let context = OffersContext::StaticInvoiceRequested {
1339+
recipient_id_nonce,
1340+
nonce,
1341+
hmac,
1342+
path_absolute_expiry,
1343+
};
1344+
match self.create_blinded_paths_using_absolute_expiry(
1345+
context,
1346+
Some(path_absolute_expiry),
1347+
peers,
1348+
) {
1349+
Ok(paths) => (paths, path_absolute_expiry),
1350+
Err(()) => return None,
1351+
}
1352+
};
1353+
1354+
let reply_path_context = {
1355+
let nonce = Nonce::from_entropy_source(entropy);
1356+
let path_absolute_expiry = duration_since_epoch.saturating_add(OFFER_PATH_EXPIRY);
1357+
let hmac = signer::hmac_for_serve_static_invoice_context(nonce, expanded_key);
1358+
MessageContext::AsyncPayments(AsyncPaymentsContext::ServeStaticInvoice {
1359+
nonce,
1360+
recipient_id_nonce,
1361+
hmac,
1362+
path_absolute_expiry,
1363+
})
1364+
};
1365+
1366+
let offer_paths_om =
1367+
OfferPaths { paths: offer_paths, paths_absolute_expiry: Some(paths_expiry) };
1368+
return Some((offer_paths_om, reply_path_context));
1369+
}
1370+
12981371
/// Handles an incoming [`OfferPaths`] message from the static invoice server, sending out
12991372
/// [`ServeStaticInvoice`] onion messages in response if we want to use the paths we've received
13001373
/// 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
@@ -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)]
@@ -598,6 +608,17 @@ pub(crate) fn hmac_for_offer_paths_request_context(
598608
Hmac::from_engine(hmac)
599609
}
600610

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

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

0 commit comments

Comments
 (0)