Skip to content

Commit 41f0088

Browse files
Check and refresh async receive offer
As an async recipient, we need to interactively build a static invoice that an always-online node will serve to payers on our behalf. At the start of this process, we send a request for paths to include in our offer to the always-online node on startup and refresh the cached offer when it expires.
1 parent e99e05b commit 41f0088

File tree

3 files changed

+124
-0
lines changed

3 files changed

+124
-0
lines changed

lightning/src/blinded_path/message.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,32 @@ pub enum OffersContext {
393393
/// [`AsyncPaymentsMessage`]: crate::onion_message::async_payments::AsyncPaymentsMessage
394394
#[derive(Clone, Debug)]
395395
pub enum AsyncPaymentsContext {
396+
/// Context used by a reply path to an [`OfferPathsRequest`], provided back to us in corresponding
397+
/// [`OfferPaths`] messages.
398+
///
399+
/// [`OfferPathsRequest`]: crate::onion_message::async_payments::OfferPathsRequest
400+
/// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths
401+
OfferPaths {
402+
/// A nonce used for authenticating that an [`OfferPaths`] message is valid for a preceding
403+
/// [`OfferPathsRequest`].
404+
///
405+
/// [`OfferPathsRequest`]: crate::onion_message::async_payments::OfferPathsRequest
406+
/// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths
407+
nonce: Nonce,
408+
/// Authentication code for the [`OfferPaths`] message.
409+
///
410+
/// Prevents nodes from creating their own blinded path to us and causing us to cache an
411+
/// unintended async receive offer.
412+
///
413+
/// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths
414+
hmac: Hmac<Sha256>,
415+
/// The time as duration since the Unix epoch at which this path expires and messages sent over
416+
/// it should be ignored.
417+
///
418+
/// Used to time out a static invoice server from providing offer paths if the async recipient
419+
/// is no longer configured to accept paths from them.
420+
path_absolute_expiry: core::time::Duration,
421+
},
396422
/// Context contained within the reply [`BlindedMessagePath`] we put in outbound
397423
/// [`HeldHtlcAvailable`] messages, provided back to us in corresponding [`ReleaseHeldHtlc`]
398424
/// messages.
@@ -475,6 +501,11 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext,
475501
(2, hmac, required),
476502
(4, path_absolute_expiry, required),
477503
},
504+
(2, OfferPaths) => {
505+
(0, nonce, required),
506+
(2, hmac, required),
507+
(4, path_absolute_expiry, required),
508+
},
478509
);
479510

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

lightning/src/ln/channelmanager.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,6 +1515,19 @@ struct AsyncReceiveOffer {
15151515
offer_paths_request_attempts: u8,
15161516
}
15171517

1518+
impl AsyncReceiveOffer {
1519+
/// Removes the offer from our cache if it's expired.
1520+
#[cfg(async_payments)]
1521+
fn check_expire_offer(&mut self, duration_since_epoch: Duration) {
1522+
if let Some(ref mut offer) = self.offer {
1523+
if offer.is_expired_no_std(duration_since_epoch) {
1524+
self.offer.take();
1525+
self.offer_paths_request_attempts = 0;
1526+
}
1527+
}
1528+
}
1529+
}
1530+
15181531
impl_writeable_tlv_based!(AsyncReceiveOffer, {
15191532
(0, offer, option),
15201533
(2, offer_paths_request_attempts, (static_value, 0)),
@@ -2429,6 +2442,8 @@ where
24292442
//
24302443
// `pending_async_payments_messages`
24312444
//
2445+
// `async_receive_offer_cache`
2446+
//
24322447
// `total_consistency_lock`
24332448
// |
24342449
// |__`forward_htlcs`
@@ -4849,6 +4864,60 @@ where
48494864
)
48504865
}
48514866

4867+
#[cfg(async_payments)]
4868+
fn check_refresh_async_receive_offer(&self) {
4869+
if self.default_configuration.paths_to_static_invoice_server.is_empty() { return }
4870+
4871+
let expanded_key = &self.inbound_payment_key;
4872+
let entropy = &*self.entropy_source;
4873+
let duration_since_epoch = self.duration_since_epoch();
4874+
4875+
{
4876+
let mut offer_cache = self.async_receive_offer_cache.lock().unwrap();
4877+
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+
}
4887+
}
4888+
4889+
const MAX_ATTEMPTS: u8 = 3;
4890+
if offer_cache.offer_paths_request_attempts > MAX_ATTEMPTS { return }
4891+
}
4892+
4893+
let reply_paths = {
4894+
// We expect the static invoice server to respond quickly to our request for offer paths, but
4895+
// add some buffer for no-std users that rely on block timestamps.
4896+
const REPLY_PATH_RELATIVE_EXPIRY: Duration = Duration::from_secs(2 * 60 * 60);
4897+
let nonce = Nonce::from_entropy_source(entropy);
4898+
let context = MessageContext::AsyncPayments(AsyncPaymentsContext::OfferPaths {
4899+
nonce,
4900+
hmac: signer::hmac_for_offer_paths_context(nonce, expanded_key),
4901+
path_absolute_expiry: duration_since_epoch.saturating_add(REPLY_PATH_RELATIVE_EXPIRY),
4902+
});
4903+
match self.create_blinded_paths(context) {
4904+
Ok(paths) => paths,
4905+
Err(()) => {
4906+
log_error!(self.logger, "Failed to create blinded paths when requesting async receive offer paths");
4907+
return
4908+
}
4909+
}
4910+
};
4911+
4912+
4913+
self.async_receive_offer_cache.lock().unwrap().offer_paths_request_attempts += 1;
4914+
let message = AsyncPaymentsMessage::OfferPathsRequest(OfferPathsRequest {});
4915+
queue_onion_message_with_reply_paths(
4916+
message, &self.default_configuration.paths_to_static_invoice_server[..], reply_paths,
4917+
&mut self.pending_async_payments_messages.lock().unwrap()
4918+
);
4919+
}
4920+
48524921
#[cfg(async_payments)]
48534922
fn initiate_async_payment(
48544923
&self, invoice: &StaticInvoice, payment_id: PaymentId
@@ -6798,6 +6867,9 @@ where
67986867
duration_since_epoch, &self.pending_events
67996868
);
68006869

6870+
#[cfg(async_payments)]
6871+
self.check_refresh_async_receive_offer();
6872+
68016873
// Technically we don't need to do this here, but if we have holding cell entries in a
68026874
// channel that need freeing, it's better to do that here and block a background task
68036875
// than block the message queueing pipeline.
@@ -12082,6 +12154,9 @@ where
1208212154
return NotifyOption::SkipPersistHandleEvents;
1208312155
//TODO: Also re-broadcast announcement_signatures
1208412156
});
12157+
12158+
#[cfg(async_payments)]
12159+
self.check_refresh_async_receive_offer();
1208512160
res
1208612161
}
1208712162

lightning/src/offers/signer.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ const PAYMENT_TLVS_HMAC_INPUT: &[u8; 16] = &[8; 16];
5555
#[cfg(async_payments)]
5656
const ASYNC_PAYMENTS_HELD_HTLC_HMAC_INPUT: &[u8; 16] = &[9; 16];
5757

58+
// HMAC input used in `AsyncPaymentsContext::OfferPaths` to authenticate inbound offer_paths onion
59+
// messages.
60+
#[cfg(async_payments)]
61+
const ASYNC_PAYMENTS_OFFER_PATHS_INPUT: &[u8; 16] = &[10; 16];
62+
5863
/// Message metadata which possibly is derived from [`MetadataMaterial`] such that it can be
5964
/// verified.
6065
#[derive(Clone)]
@@ -555,3 +560,16 @@ pub(crate) fn verify_held_htlc_available_context(
555560
Err(())
556561
}
557562
}
563+
564+
#[cfg(async_payments)]
565+
pub(crate) fn hmac_for_offer_paths_context(
566+
nonce: Nonce, expanded_key: &ExpandedKey,
567+
) -> Hmac<Sha256> {
568+
const IV_BYTES: &[u8; IV_LEN] = b"LDK Offer Paths~";
569+
let mut hmac = expanded_key.hmac_for_offer();
570+
hmac.input(IV_BYTES);
571+
hmac.input(&nonce.0);
572+
hmac.input(ASYNC_PAYMENTS_OFFER_PATHS_INPUT);
573+
574+
Hmac::from_engine(hmac)
575+
}

0 commit comments

Comments
 (0)