Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Client-side of static invoice server #3618

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
31 changes: 31 additions & 0 deletions lightning/src/blinded_path/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,32 @@ pub enum OffersContext {
/// [`AsyncPaymentsMessage`]: crate::onion_message::async_payments::AsyncPaymentsMessage
#[derive(Clone, Debug)]
pub enum AsyncPaymentsContext {
/// Context used by a reply path to an [`OfferPathsRequest`], provided back to us in corresponding
/// [`OfferPaths`] messages.
///
/// [`OfferPathsRequest`]: crate::onion_message::async_payments::OfferPathsRequest
/// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths
OfferPaths {
/// A nonce used for authenticating that an [`OfferPaths`] message is valid for a preceding
/// [`OfferPathsRequest`].
///
/// [`OfferPathsRequest`]: crate::onion_message::async_payments::OfferPathsRequest
/// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths
nonce: Nonce,
/// Authentication code for the [`OfferPaths`] message.
///
/// Prevents nodes from creating their own blinded path to us and causing us to cache an
/// unintended async receive offer.
///
/// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths
hmac: Hmac<Sha256>,
/// The time as duration since the Unix epoch at which this path expires and messages sent over
/// it should be ignored.
///
/// Used to time out a static invoice server from providing offer paths if the async recipient
/// is no longer configured to accept paths from them.
path_absolute_expiry: core::time::Duration,
},
/// Context contained within the reply [`BlindedMessagePath`] we put in outbound
/// [`HeldHtlcAvailable`] messages, provided back to us in corresponding [`ReleaseHeldHtlc`]
/// messages.
Expand Down Expand Up @@ -475,6 +501,11 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext,
(2, hmac, required),
(4, path_absolute_expiry, required),
},
(2, OfferPaths) => {
(0, nonce, required),
(2, hmac, required),
(4, path_absolute_expiry, required),
},
);

/// Contains a simple nonce for use in a blinded path's context.
Expand Down
75 changes: 75 additions & 0 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1515,6 +1515,19 @@ struct AsyncReceiveOffer {
offer_paths_request_attempts: u8,
}

impl AsyncReceiveOffer {
/// Removes the offer from our cache if it's expired.
#[cfg(async_payments)]
fn check_expire_offer(&mut self, duration_since_epoch: Duration) {
if let Some(ref mut offer) = self.offer {
if offer.is_expired_no_std(duration_since_epoch) {
self.offer.take();
self.offer_paths_request_attempts = 0;
}
}
}
}

impl_writeable_tlv_based!(AsyncReceiveOffer, {
(0, offer, option),
(2, offer_paths_request_attempts, (static_value, 0)),
Expand Down Expand Up @@ -2429,6 +2442,8 @@ where
//
// `pending_async_payments_messages`
//
// `async_receive_offer_cache`
//
// `total_consistency_lock`
// |
// |__`forward_htlcs`
Expand Down Expand Up @@ -4849,6 +4864,60 @@ where
)
}

#[cfg(async_payments)]
fn check_refresh_async_receive_offer(&self) {
Copy link
Contributor

@joostjager joostjager Mar 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My view on channelmanager is that it manages the channels. Relatively low level operations. I think that async offers live on a higher level, a level above basic channel management. Shouldn't this be reflected in code, by implementing it outside of channel manager?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I believe this will move into OffersMessageFlow once #3639 lands!

if self.default_configuration.paths_to_static_invoice_server.is_empty() { return }

let expanded_key = &self.inbound_payment_key;
let entropy = &*self.entropy_source;
let duration_since_epoch = self.duration_since_epoch();

{
let mut offer_cache = self.async_receive_offer_cache.lock().unwrap();
offer_cache.check_expire_offer(duration_since_epoch);

if let Some(ref offer) = offer_cache.offer {
// If we have more than three hours before our offer expires, don't bother requesting new
// paths.
const PATHS_EXPIRY_BUFFER: Duration = Duration::from_secs(60 * 60 * 3);
let offer_expiry = offer.absolute_expiry().unwrap_or(Duration::MAX);
if offer_expiry > duration_since_epoch.saturating_add(PATHS_EXPIRY_BUFFER) {
return
}
}

const MAX_ATTEMPTS: u8 = 3;
if offer_cache.offer_paths_request_attempts > MAX_ATTEMPTS { return }
}

let reply_paths = {
// We expect the static invoice server to respond quickly to our request for offer paths, but
// add some buffer for no-std users that rely on block timestamps.
const REPLY_PATH_RELATIVE_EXPIRY: Duration = Duration::from_secs(2 * 60 * 60);
let nonce = Nonce::from_entropy_source(entropy);
let context = MessageContext::AsyncPayments(AsyncPaymentsContext::OfferPaths {
nonce,
hmac: signer::hmac_for_offer_paths_context(nonce, expanded_key),
path_absolute_expiry: duration_since_epoch.saturating_add(REPLY_PATH_RELATIVE_EXPIRY),
});
match self.create_blinded_paths(context) {
Ok(paths) => paths,
Err(()) => {
log_error!(self.logger, "Failed to create blinded paths when requesting async receive offer paths");
return
}
}
};


self.async_receive_offer_cache.lock().unwrap().offer_paths_request_attempts += 1;
let message = AsyncPaymentsMessage::OfferPathsRequest(OfferPathsRequest {});
queue_onion_message_with_reply_paths(
message, &self.default_configuration.paths_to_static_invoice_server[..], reply_paths,
&mut self.pending_async_payments_messages.lock().unwrap()
);
}

#[cfg(async_payments)]
fn initiate_async_payment(
&self, invoice: &StaticInvoice, payment_id: PaymentId
Expand Down Expand Up @@ -6798,6 +6867,9 @@ where
duration_since_epoch, &self.pending_events
);

#[cfg(async_payments)]
self.check_refresh_async_receive_offer();

// Technically we don't need to do this here, but if we have holding cell entries in a
// channel that need freeing, it's better to do that here and block a background task
// than block the message queueing pipeline.
Expand Down Expand Up @@ -12082,6 +12154,9 @@ where
return NotifyOption::SkipPersistHandleEvents;
//TODO: Also re-broadcast announcement_signatures
});

#[cfg(async_payments)]
self.check_refresh_async_receive_offer();
res
}

Expand Down
18 changes: 18 additions & 0 deletions lightning/src/offers/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ const PAYMENT_TLVS_HMAC_INPUT: &[u8; 16] = &[8; 16];
#[cfg(async_payments)]
const ASYNC_PAYMENTS_HELD_HTLC_HMAC_INPUT: &[u8; 16] = &[9; 16];

// HMAC input used in `AsyncPaymentsContext::OfferPaths` to authenticate inbound offer_paths onion
// messages.
#[cfg(async_payments)]
const ASYNC_PAYMENTS_OFFER_PATHS_INPUT: &[u8; 16] = &[10; 16];

/// Message metadata which possibly is derived from [`MetadataMaterial`] such that it can be
/// verified.
#[derive(Clone)]
Expand Down Expand Up @@ -555,3 +560,16 @@ pub(crate) fn verify_held_htlc_available_context(
Err(())
}
}

#[cfg(async_payments)]
pub(crate) fn hmac_for_offer_paths_context(
nonce: Nonce, expanded_key: &ExpandedKey,
) -> Hmac<Sha256> {
const IV_BYTES: &[u8; IV_LEN] = b"LDK Offer Paths~";
let mut hmac = expanded_key.hmac_for_offer();
hmac.input(IV_BYTES);
hmac.input(&nonce.0);
hmac.input(ASYNC_PAYMENTS_OFFER_PATHS_INPUT);
Comment on lines +573 to +577
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to include path_absolute_expiry?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought the nonce/IV was sufficient but I'm not certain. @TheBlueMatt would it be an improvement to commit to the expiry in the hmac? IIUC the path still can't be re-purposed...


Hmac::from_engine(hmac)
}