Skip to content

Commit 63ddee8

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 10874f3 commit 63ddee8

File tree

3 files changed

+161
-0
lines changed

3 files changed

+161
-0
lines changed

lightning/src/blinded_path/message.rs

Lines changed: 39 additions & 0 deletions
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

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

lightning/src/offers/signer.rs

Lines changed: 29 additions & 0 deletions
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)