Skip to content

Commit b67220b

Browse files
Send static invoice in response to offer paths
As an async recipient, we need to interactively build a static invoice that an always-online node will serve to payers on our behalf. As part of this process, the static invoice server sends us blinded message paths to include in our offer so they'll receive invoice requests from senders trying to pay us while we're offline. On receipt of these paths, create an offer and static invoice and send the invoice back to the server so they can provide the invoice to payers.
1 parent 41f0088 commit b67220b

File tree

3 files changed

+181
-9
lines changed

3 files changed

+181
-9
lines changed

lightning/src/blinded_path/message.rs

+39
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use crate::ln::channelmanager::PaymentId;
2323
use crate::ln::msgs::DecodeError;
2424
use crate::ln::onion_utils;
2525
use crate::offers::nonce::Nonce;
26+
use crate::offers::offer::Offer;
2627
use crate::onion_message::packet::ControlTlvs;
2728
use crate::routing::gossip::{NodeId, ReadOnlyNetworkGraph};
2829
use crate::sign::{EntropySource, NodeSigner, Recipient};
@@ -419,6 +420,38 @@ pub enum AsyncPaymentsContext {
419420
/// is no longer configured to accept paths from them.
420421
path_absolute_expiry: core::time::Duration,
421422
},
423+
/// Context used by a reply path to a [`ServeStaticInvoice`] message, provided back to us in
424+
/// corresponding [`StaticInvoicePersisted`] messages.
425+
///
426+
/// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice
427+
/// [`StaticInvoicePersisted`]: crate::onion_message::async_payments::StaticInvoicePersisted
428+
StaticInvoicePersisted {
429+
/// The offer corresponding to the [`StaticInvoice`] that has been persisted. This invoice is
430+
/// now ready to be provided by the static invoice server in response to [`InvoiceRequest`]s.
431+
///
432+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
433+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
434+
offer: Offer,
435+
/// A nonce used for authenticating that a [`StaticInvoicePersisted`] message is valid for a
436+
/// preceding [`ServeStaticInvoice`] message.
437+
///
438+
/// [`StaticInvoicePersisted`]: crate::onion_message::async_payments::StaticInvoicePersisted
439+
/// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice
440+
nonce: Nonce,
441+
/// Authentication code for the [`StaticInvoicePersisted`] message.
442+
///
443+
/// Prevents nodes from creating their own blinded path to us and causing us to cache an
444+
/// unintended async receive offer.
445+
///
446+
/// [`StaticInvoicePersisted`]: crate::onion_message::async_payments::StaticInvoicePersisted
447+
hmac: Hmac<Sha256>,
448+
/// The time as duration since the Unix epoch at which this path expires and messages sent over
449+
/// it should be ignored.
450+
///
451+
/// Prevents a static invoice server from causing an async recipient to cache an old offer if
452+
/// the recipient is no longer configured to use that server.
453+
path_absolute_expiry: core::time::Duration,
454+
},
422455
/// Context contained within the reply [`BlindedMessagePath`] we put in outbound
423456
/// [`HeldHtlcAvailable`] messages, provided back to us in corresponding [`ReleaseHeldHtlc`]
424457
/// messages.
@@ -506,6 +539,12 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext,
506539
(2, hmac, required),
507540
(4, path_absolute_expiry, required),
508541
},
542+
(3, StaticInvoicePersisted) => {
543+
(0, offer, required),
544+
(2, nonce, required),
545+
(4, hmac, required),
546+
(6, path_absolute_expiry, required),
547+
},
509548
);
510549

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

lightning/src/ln/channelmanager.rs

+113-9
Original file line numberDiff line numberDiff line change
@@ -1516,6 +1516,11 @@ struct AsyncReceiveOffer {
15161516
}
15171517

15181518
impl AsyncReceiveOffer {
1519+
// If we have more than three hours before our offer expires, don't bother requesting new
1520+
// paths.
1521+
#[cfg(async_payments)]
1522+
const OFFER_RELATIVE_EXPIRY_BUFFER: Duration = Duration::from_secs(60 * 60 * 3);
1523+
15191524
/// Removes the offer from our cache if it's expired.
15201525
#[cfg(async_payments)]
15211526
fn check_expire_offer(&mut self, duration_since_epoch: Duration) {
@@ -1526,6 +1531,17 @@ impl AsyncReceiveOffer {
15261531
}
15271532
}
15281533
}
1534+
1535+
#[cfg(async_payments)]
1536+
fn should_refresh_offer(&self, duration_since_epoch: Duration) -> bool {
1537+
if let Some(ref offer) = self.offer {
1538+
let offer_expiry = offer.absolute_expiry().unwrap_or(Duration::MAX);
1539+
if offer_expiry > duration_since_epoch.saturating_add(Self::OFFER_RELATIVE_EXPIRY_BUFFER) {
1540+
return false
1541+
}
1542+
}
1543+
return true
1544+
}
15291545
}
15301546

15311547
impl_writeable_tlv_based!(AsyncReceiveOffer, {
@@ -4875,15 +4891,8 @@ where
48754891
{
48764892
let mut offer_cache = self.async_receive_offer_cache.lock().unwrap();
48774893
offer_cache.check_expire_offer(duration_since_epoch);
4878-
4879-
if let Some(ref offer) = offer_cache.offer {
4880-
// If we have more than three hours before our offer expires, don't bother requesting new
4881-
// paths.
4882-
const PATHS_EXPIRY_BUFFER: Duration = Duration::from_secs(60 * 60 * 3);
4883-
let offer_expiry = offer.absolute_expiry().unwrap_or(Duration::MAX);
4884-
if offer_expiry > duration_since_epoch.saturating_add(PATHS_EXPIRY_BUFFER) {
4885-
return
4886-
}
4894+
if !offer_cache.should_refresh_offer(duration_since_epoch) {
4895+
return
48874896
}
48884897

48894898
const MAX_ATTEMPTS: u8 = 3;
@@ -12647,6 +12656,101 @@ where
1264712656
fn handle_offer_paths(
1264812657
&self, _message: OfferPaths, _context: AsyncPaymentsContext, _responder: Option<Responder>,
1264912658
) -> Option<(ServeStaticInvoice, ResponseInstruction)> {
12659+
#[cfg(async_payments)] {
12660+
let expanded_key = &self.inbound_payment_key;
12661+
let entropy = &*self.entropy_source;
12662+
let secp_ctx = &self.secp_ctx;
12663+
let duration_since_epoch = self.duration_since_epoch();
12664+
12665+
match _context {
12666+
AsyncPaymentsContext::OfferPaths { nonce, hmac, path_absolute_expiry } => {
12667+
if let Err(()) = signer::verify_offer_paths_context(nonce, hmac, expanded_key) {
12668+
return None
12669+
}
12670+
if duration_since_epoch > path_absolute_expiry { return None }
12671+
},
12672+
_ => return None
12673+
}
12674+
12675+
if !self.async_receive_offer_cache.lock().unwrap().should_refresh_offer(duration_since_epoch) {
12676+
return None
12677+
}
12678+
12679+
// Require at least two hours before we'll need to start the process of creating a new offer.
12680+
const MIN_OFFER_PATHS_RELATIVE_EXPIRY: Duration =
12681+
Duration::from_secs(2 * 60 * 60).saturating_add(AsyncReceiveOffer::OFFER_RELATIVE_EXPIRY_BUFFER);
12682+
let min_offer_paths_absolute_expiry =
12683+
duration_since_epoch.saturating_add(MIN_OFFER_PATHS_RELATIVE_EXPIRY);
12684+
let offer_paths_absolute_expiry =
12685+
_message.paths_absolute_expiry.unwrap_or(Duration::from_secs(u64::MAX));
12686+
if offer_paths_absolute_expiry < min_offer_paths_absolute_expiry {
12687+
log_error!(self.logger, "Received offer paths with too-soon absolute Unix epoch expiry: {}", offer_paths_absolute_expiry.as_secs());
12688+
return None
12689+
}
12690+
12691+
// Expire the offer at the same time as the static invoice so we automatically refresh both
12692+
// at the same time.
12693+
let offer_and_invoice_absolute_expiry = Duration::from_secs(core::cmp::min(
12694+
offer_paths_absolute_expiry.as_secs(),
12695+
duration_since_epoch.saturating_add(STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY).as_secs()
12696+
));
12697+
12698+
let (offer, offer_nonce) = {
12699+
let (offer_builder, offer_nonce) =
12700+
match self.create_async_receive_offer_builder(_message.paths) {
12701+
Ok((builder, nonce)) => (builder, nonce),
12702+
Err(e) => {
12703+
log_error!(self.logger, "Failed to create offer builder when replying to OfferPaths message: {:?}", e);
12704+
return None
12705+
},
12706+
};
12707+
match offer_builder.absolute_expiry(offer_and_invoice_absolute_expiry).build() {
12708+
Ok(offer) => (offer, offer_nonce),
12709+
Err(e) => {
12710+
log_error!(self.logger, "Failed to build offer when replying to OfferPaths message: {:?}", e);
12711+
return None
12712+
},
12713+
}
12714+
};
12715+
12716+
let static_invoice = {
12717+
let invoice_res = self.create_static_invoice_builder(
12718+
&offer, offer_nonce, Some(offer_and_invoice_absolute_expiry)
12719+
).and_then(|builder| builder.build_and_sign(secp_ctx));
12720+
match invoice_res {
12721+
Ok(invoice) => invoice,
12722+
Err(e) => {
12723+
log_error!(self.logger, "Failed to create static invoice when replying to OfferPaths message: {:?}", e);
12724+
return None
12725+
},
12726+
}
12727+
};
12728+
12729+
let invoice_persisted_paths = {
12730+
// We expect the static invoice server to respond quickly, but add some buffer for no-std
12731+
// users that rely on block timestamps.
12732+
const PATH_RELATIVE_EXPIRY: Duration = Duration::from_secs(2 * 60 * 60);
12733+
12734+
let nonce = Nonce::from_entropy_source(entropy);
12735+
let hmac = signer::hmac_for_static_invoice_persisted_context(nonce, expanded_key);
12736+
let context = MessageContext::AsyncPayments(AsyncPaymentsContext::StaticInvoicePersisted {
12737+
offer, nonce, hmac,
12738+
path_absolute_expiry: duration_since_epoch.saturating_add(PATH_RELATIVE_EXPIRY)
12739+
});
12740+
match self.create_blinded_paths(context) {
12741+
Ok(paths) => paths,
12742+
Err(()) => {
12743+
log_error!(self.logger, "Failed to create blinded paths when replying to OfferPaths message");
12744+
return None
12745+
},
12746+
}
12747+
};
12748+
12749+
let reply = ServeStaticInvoice { invoice: static_invoice, invoice_persisted_paths };
12750+
return _responder.map(|responder| (reply, responder.respond()))
12751+
}
12752+
12753+
#[cfg(not(async_payments))]
1265012754
None
1265112755
}
1265212756

lightning/src/offers/signer.rs

+29
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ const ASYNC_PAYMENTS_HELD_HTLC_HMAC_INPUT: &[u8; 16] = &[9; 16];
6060
#[cfg(async_payments)]
6161
const ASYNC_PAYMENTS_OFFER_PATHS_INPUT: &[u8; 16] = &[10; 16];
6262

63+
// HMAC input used in `AsyncPaymentsContext::StaticInvoicePersisted` to authenticate inbound
64+
// static_invoice_persisted onion messages.
65+
#[cfg(async_payments)]
66+
const ASYNC_PAYMENTS_STATIC_INV_PERSISTED_INPUT: &[u8; 16] = &[11; 16];
67+
6368
/// Message metadata which possibly is derived from [`MetadataMaterial`] such that it can be
6469
/// verified.
6570
#[derive(Clone)]
@@ -573,3 +578,27 @@ pub(crate) fn hmac_for_offer_paths_context(
573578

574579
Hmac::from_engine(hmac)
575580
}
581+
582+
#[cfg(async_payments)]
583+
pub(crate) fn verify_offer_paths_context(
584+
nonce: Nonce, hmac: Hmac<Sha256>, expanded_key: &ExpandedKey,
585+
) -> Result<(), ()> {
586+
if hmac_for_offer_paths_context(nonce, expanded_key) == hmac {
587+
Ok(())
588+
} else {
589+
Err(())
590+
}
591+
}
592+
593+
#[cfg(async_payments)]
594+
pub(crate) fn hmac_for_static_invoice_persisted_context(
595+
nonce: Nonce, expanded_key: &ExpandedKey,
596+
) -> Hmac<Sha256> {
597+
const IV_BYTES: &[u8; IV_LEN] = b"LDK InvPersisted";
598+
let mut hmac = expanded_key.hmac_for_offer();
599+
hmac.input(IV_BYTES);
600+
hmac.input(&nonce.0);
601+
hmac.input(ASYNC_PAYMENTS_STATIC_INV_PERSISTED_INPUT);
602+
603+
Hmac::from_engine(hmac)
604+
}

0 commit comments

Comments
 (0)