Skip to content

Commit 10874f3

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 9d3d326 commit 10874f3

File tree

4 files changed

+137
-0
lines changed

4 files changed

+137
-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: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ use crate::util::errors::APIError;
9494
#[cfg(async_payments)] use {
9595
crate::offers::offer::Amount,
9696
crate::offers::static_invoice::{DEFAULT_RELATIVE_EXPIRY as STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY, StaticInvoice, StaticInvoiceBuilder},
97+
crate::onion_message::async_payments::REPLY_PATH_RELATIVE_EXPIRY,
9798
};
9899

99100
#[cfg(feature = "dnssec")]
@@ -1515,6 +1516,35 @@ struct AsyncReceiveOffer {
15151516
offer_paths_request_attempts: u8,
15161517
}
15171518

1519+
impl AsyncReceiveOffer {
1520+
// If we have more than three hours before our offer expires, don't bother requesting new
1521+
// paths.
1522+
#[cfg(async_payments)]
1523+
const OFFER_RELATIVE_EXPIRY_BUFFER: Duration = Duration::from_secs(3 * 60 * 60);
1524+
1525+
/// Removes the offer from our cache if it's expired.
1526+
#[cfg(async_payments)]
1527+
fn check_expire_offer(&mut self, duration_since_epoch: Duration) {
1528+
if let Some(ref mut offer) = self.offer {
1529+
if offer.is_expired_no_std(duration_since_epoch) {
1530+
self.offer.take();
1531+
self.offer_paths_request_attempts = 0;
1532+
}
1533+
}
1534+
}
1535+
1536+
#[cfg(async_payments)]
1537+
fn should_refresh_offer(&self, duration_since_epoch: Duration) -> bool {
1538+
if let Some(ref offer) = self.offer {
1539+
let offer_expiry = offer.absolute_expiry().unwrap_or(Duration::MAX);
1540+
if offer_expiry > duration_since_epoch.saturating_add(Self::OFFER_RELATIVE_EXPIRY_BUFFER) {
1541+
return false
1542+
}
1543+
}
1544+
return true
1545+
}
1546+
}
1547+
15181548
impl_writeable_tlv_based!(AsyncReceiveOffer, {
15191549
(0, offer, option),
15201550
(2, offer_paths_request_attempts, (static_value, 0)),
@@ -2432,6 +2462,8 @@ where
24322462
//
24332463
// `pending_async_payments_messages`
24342464
//
2465+
// `async_receive_offer_cache`
2466+
//
24352467
// `total_consistency_lock`
24362468
// |
24372469
// |__`forward_htlcs`
@@ -4852,6 +4884,50 @@ where
48524884
)
48534885
}
48544886

4887+
#[cfg(async_payments)]
4888+
fn check_refresh_async_receive_offer(&self) {
4889+
if self.default_configuration.paths_to_static_invoice_server.is_empty() { return }
4890+
4891+
let expanded_key = &self.inbound_payment_key;
4892+
let entropy = &*self.entropy_source;
4893+
let duration_since_epoch = self.duration_since_epoch();
4894+
4895+
{
4896+
let mut offer_cache = self.async_receive_offer_cache.lock().unwrap();
4897+
offer_cache.check_expire_offer(duration_since_epoch);
4898+
if !offer_cache.should_refresh_offer(duration_since_epoch) {
4899+
return
4900+
}
4901+
4902+
const MAX_ATTEMPTS: u8 = 3;
4903+
if offer_cache.offer_paths_request_attempts > MAX_ATTEMPTS { return }
4904+
}
4905+
4906+
let reply_paths = {
4907+
let nonce = Nonce::from_entropy_source(entropy);
4908+
let context = MessageContext::AsyncPayments(AsyncPaymentsContext::OfferPaths {
4909+
nonce,
4910+
hmac: signer::hmac_for_offer_paths_context(nonce, expanded_key),
4911+
path_absolute_expiry: duration_since_epoch.saturating_add(REPLY_PATH_RELATIVE_EXPIRY),
4912+
});
4913+
match self.create_blinded_paths(context) {
4914+
Ok(paths) => paths,
4915+
Err(()) => {
4916+
log_error!(self.logger, "Failed to create blinded paths when requesting async receive offer paths");
4917+
return
4918+
}
4919+
}
4920+
};
4921+
4922+
4923+
self.async_receive_offer_cache.lock().unwrap().offer_paths_request_attempts += 1;
4924+
let message = AsyncPaymentsMessage::OfferPathsRequest(OfferPathsRequest {});
4925+
queue_onion_message_with_reply_paths(
4926+
message, &self.default_configuration.paths_to_static_invoice_server[..], reply_paths,
4927+
&mut self.pending_async_payments_messages.lock().unwrap()
4928+
);
4929+
}
4930+
48554931
#[cfg(async_payments)]
48564932
fn initiate_async_payment(
48574933
&self, invoice: &StaticInvoice, payment_id: PaymentId
@@ -6801,6 +6877,9 @@ where
68016877
duration_since_epoch, &self.pending_events
68026878
);
68036879

6880+
#[cfg(async_payments)]
6881+
self.check_refresh_async_receive_offer();
6882+
68046883
// Technically we don't need to do this here, but if we have holding cell entries in a
68056884
// channel that need freeing, it's better to do that here and block a background task
68066885
// than block the message queueing pipeline.
@@ -12085,6 +12164,9 @@ where
1208512164
return NotifyOption::SkipPersistHandleEvents;
1208612165
//TODO: Also re-broadcast announcement_signatures
1208712166
});
12167+
12168+
#[cfg(async_payments)]
12169+
self.check_refresh_async_receive_offer();
1208812170
res
1208912171
}
1209012172

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+
}

lightning/src/onion_message/async_payments.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ const INVOICE_PERSISTED_TLV_TYPE: u64 = 65544;
2929
const HELD_HTLC_AVAILABLE_TLV_TYPE: u64 = 72;
3030
const RELEASE_HELD_HTLC_TLV_TYPE: u64 = 74;
3131

32+
// Used to expire reply paths used in exchanging static invoice server onion messages. We expect
33+
// these onion messages to be exchanged quickly, but add some buffer for no-std users who rely on
34+
// block timestamps.
35+
#[cfg(async_payments)]
36+
pub(crate) const REPLY_PATH_RELATIVE_EXPIRY: Duration = Duration::from_secs(2 * 60 * 60);
37+
3238
/// A handler for an [`OnionMessage`] containing an async payments message as its payload.
3339
///
3440
/// [`OnionMessage`]: crate::ln::msgs::OnionMessage

0 commit comments

Comments
 (0)