Skip to content

Async recipient-side of static invoice server #3618

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

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion fuzz/src/onion_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ use lightning::ln::peer_handler::IgnoringMessageHandler;
use lightning::ln::script::ShutdownScript;
use lightning::offers::invoice::UnsignedBolt12Invoice;
use lightning::onion_message::async_payments::{
AsyncPaymentsMessageHandler, HeldHtlcAvailable, ReleaseHeldHtlc,
AsyncPaymentsMessageHandler, HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ReleaseHeldHtlc,
Copy link
Contributor

Choose a reason for hiding this comment

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

Good explanation in the commit message. Maybe something can be added to highlight the downside of not having a static invoice server. Suppose the user would just hand over their keysend invoice to the sender or publish it on a website, what goes wrong? I suppose something goes wrong when the paths would need to change?

Copy link
Contributor

Choose a reason for hiding this comment

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

Commit message reads very well. Some of that valuable info might even be moved into code comments?

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 think for the part discussing alternative designs, I'm going to address those in a bLIP and we can link to that here. Did you want the high-level flow explanation to go in the docs?

Copy link
Contributor

Choose a reason for hiding this comment

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

For someone new to the code, it is useful to be able to read the high level flow somewhere. The commit message won't be visible anymore. But a link to a blip explaining the high level flow and alternative designs seems fine too.

ServeStaticInvoice, StaticInvoicePersisted,
};
use lightning::onion_message::messenger::{
CustomOnionMessageHandler, Destination, MessageRouter, MessageSendInstructions,
Expand Down Expand Up @@ -124,6 +125,30 @@ impl OffersMessageHandler for TestOffersMessageHandler {
struct TestAsyncPaymentsMessageHandler {}

impl AsyncPaymentsMessageHandler for TestAsyncPaymentsMessageHandler {
fn handle_offer_paths_request(
&self, _message: OfferPathsRequest, _context: AsyncPaymentsContext,
responder: Option<Responder>,
) -> Option<(OfferPaths, ResponseInstruction)> {
let responder = match responder {
Some(resp) => resp,
None => return None,
};
Some((OfferPaths { paths: Vec::new(), paths_absolute_expiry: None }, responder.respond()))
}
fn handle_offer_paths(
&self, _message: OfferPaths, _context: AsyncPaymentsContext, _responder: Option<Responder>,
) -> Option<(ServeStaticInvoice, ResponseInstruction)> {
None
}
fn handle_serve_static_invoice(
&self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext,
_responder: Option<Responder>,
) {
}
fn handle_static_invoice_persisted(
&self, _message: StaticInvoicePersisted, _context: AsyncPaymentsContext,
) {
}
fn handle_held_htlc_available(
&self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext,
responder: Option<Responder>,
Expand Down
95 changes: 95 additions & 0 deletions lightning/src/blinded_path/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ use crate::ln::channelmanager::PaymentId;
use crate::ln::msgs::DecodeError;
use crate::ln::onion_utils;
use crate::offers::nonce::Nonce;
use crate::offers::offer::Offer;
use crate::onion_message::messenger::Responder;
use crate::onion_message::packet::ControlTlvs;
use crate::routing::gossip::{NodeId, ReadOnlyNetworkGraph};
use crate::sign::{EntropySource, NodeSigner, Recipient};
Expand Down Expand Up @@ -404,6 +406,84 @@ 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 used by a reply path to a [`ServeStaticInvoice`] message, provided back to us in
/// corresponding [`StaticInvoicePersisted`] messages.
///
/// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice
/// [`StaticInvoicePersisted`]: crate::onion_message::async_payments::StaticInvoicePersisted
StaticInvoicePersisted {
/// The offer corresponding to the [`StaticInvoice`] that has been persisted. This invoice is
/// now ready to be provided by the static invoice server in response to [`InvoiceRequest`]s.
///
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
offer: Offer,
/// A [`Nonce`] useful for updating the [`StaticInvoice`] that corresponds to the
/// [`AsyncPaymentsContext::StaticInvoicePersisted::offer`], since the offer may be much longer
/// lived than the invoice.
///
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
offer_nonce: Nonce,
/// Useful to determine how far an offer is into its lifespan, to decide whether the offer is
/// expiring soon and we should start building a new one.
offer_created_at: core::time::Duration,
/// A [`Responder`] useful for updating the [`StaticInvoice`] that corresponds to the
/// [`AsyncPaymentsContext::StaticInvoicePersisted::offer`], since the offer may be much longer
/// lived than the invoice.
///
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
update_static_invoice_path: Responder,
/// The time as duration since the Unix epoch at which the [`StaticInvoice`] expires, used to track
/// when we need to generate and persist a new invoice with the static invoice server.
///
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
static_invoice_absolute_expiry: core::time::Duration,
/// A nonce used for authenticating that a [`StaticInvoicePersisted`] message is valid for a
/// preceding [`ServeStaticInvoice`] message.
///
/// [`StaticInvoicePersisted`]: crate::onion_message::async_payments::StaticInvoicePersisted
/// [`ServeStaticInvoice`]: crate::onion_message::async_payments::ServeStaticInvoice
nonce: Nonce,
/// Authentication code for the [`StaticInvoicePersisted`] message.
///
/// Prevents nodes from creating their own blinded path to us and causing us to cache an
/// unintended async receive offer.
///
/// [`StaticInvoicePersisted`]: crate::onion_message::async_payments::StaticInvoicePersisted
hmac: Hmac<Sha256>,
/// The time as duration since the Unix epoch at which this path expires and messages sent over
/// it should be ignored.
///
/// Prevents a static invoice server from causing an async recipient to cache an old offer if
/// the recipient is no longer configured to use that server.
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 @@ -486,6 +566,21 @@ 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),
},
(3, StaticInvoicePersisted) => {
(0, offer, required),
(2, offer_nonce, required),
(4, offer_created_at, required),
(6, update_static_invoice_path, required),
(8, static_invoice_absolute_expiry, required),
(10, nonce, required),
(12, hmac, required),
(14, path_absolute_expiry, required),
},
);

/// Contains a simple nonce for use in a blinded path's context.
Expand Down
112 changes: 110 additions & 2 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ use crate::ln::outbound_payment::{
SendAlongPathArgs, StaleExpiration,
};
use crate::ln::types::ChannelId;
use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache;
use crate::offers::flow::OffersMessageFlow;
use crate::offers::invoice::{
Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder, DEFAULT_RELATIVE_EXPIRY,
Expand All @@ -97,7 +98,8 @@ use crate::offers::parse::Bolt12SemanticError;
use crate::offers::refund::Refund;
use crate::offers::signer;
use crate::onion_message::async_payments::{
AsyncPaymentsMessage, AsyncPaymentsMessageHandler, HeldHtlcAvailable, ReleaseHeldHtlc,
AsyncPaymentsMessage, AsyncPaymentsMessageHandler, HeldHtlcAvailable, OfferPaths,
OfferPathsRequest, ReleaseHeldHtlc, ServeStaticInvoice, StaticInvoicePersisted,
};
use crate::onion_message::dns_resolution::HumanReadableName;
use crate::onion_message::messenger::{
Expand Down Expand Up @@ -5103,7 +5105,24 @@ where
}

#[cfg(async_payments)]
fn check_refresh_async_receive_offers(&self) {
let peers = self.get_peers_for_blinded_path();
let channels = self.list_usable_channels();
let entropy = &*self.entropy_source;
let router = &*self.router;
match self.flow.check_refresh_async_receive_offers(peers, channels, entropy, router) {
Err(()) => {
log_error!(
self.logger,
"Failed to create blinded paths when requesting async receive offer paths"
);
},
Ok(()) => {},
}
}

#[rustfmt::skip]
#[cfg(async_payments)]
fn initiate_async_payment(
&self, invoice: &StaticInvoice, payment_id: PaymentId
) -> Result<(), Bolt12PaymentError> {
Expand Down Expand Up @@ -7047,6 +7066,9 @@ where
duration_since_epoch, &self.pending_events
);

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

// 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 @@ -10586,9 +10608,23 @@ where
#[cfg(c_bindings)]
create_refund_builder!(self, RefundMaybeWithDerivedMetadataBuilder);

/// Retrieve our cached [`Offer`]s for receiving async payments as an often-offline recipient.
/// Will only be set if [`UserConfig::paths_to_static_invoice_server`] is set and we succeeded in
/// interactively building a [`StaticInvoice`] with the static invoice server.
///
/// Useful for posting offers to receive payments later, such as posting an offer on a website.
#[cfg(async_payments)]
Copy link
Contributor

Choose a reason for hiding this comment

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

Definitely looks like too many async payments cfg directives to me.

pub fn get_cached_async_receive_offers(&self) -> Vec<Offer> {
Copy link
Contributor

Choose a reason for hiding this comment

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

These pub fns added, do they need a high level test?

Also curious to see how this is going to be used in ldk node.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is tested in #3628. As discussed offline, I looked into migrating some of the testing to this PR by using mocks for the server. It doesn't look particularly trivial honestly, we'd still need a lot of the code that's in the follow-up for unwrapping + sending messages and creating blinded paths on behalf of the server.

I'm still going to try to get you something to look at on the ldk-node front though 🫡 hoping to have some updates soon.

Copy link
Contributor

Choose a reason for hiding this comment

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

Would the order have mattered here? If the server pr would have been the first, would it have been easier to mock the client? Not asking for this, to be clear. Just curious.

Copy link
Contributor

Choose a reason for hiding this comment

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

Tests in the follow up look good indeed.

self.flow.get_cached_async_receive_offers()
}

/// Create an offer for receiving async payments as an often-offline recipient.
///
/// Because we may be offline when the payer attempts to request an invoice, you MUST:
/// Instead of using this method, it is preferable to set
/// [`UserConfig::paths_to_static_invoice_server`] and retrieve the automatically built offer via
/// [`Self::get_cached_async_receive_offers`].
///
/// If you want to build the [`StaticInvoice`] manually using this method instead, you MUST:
/// 1. Provide at least 1 [`BlindedMessagePath`] terminating at an always-online node that will
/// serve the [`StaticInvoice`] created from this offer on our behalf.
/// 2. Use [`Self::create_static_invoice_builder`] to create a [`StaticInvoice`] from this
Expand All @@ -10605,6 +10641,10 @@ where
/// Creates a [`StaticInvoiceBuilder`] from the corresponding [`Offer`] and [`Nonce`] that were
/// created via [`Self::create_async_receive_offer_builder`]. If `relative_expiry` is unset, the
/// invoice's expiry will default to [`STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY`].
///
/// Instead of using this method to manually build the invoice, it is preferable to set
/// [`UserConfig::paths_to_static_invoice_server`] and retrieve the automatically built offer via
/// [`Self::get_cached_async_receive_offers`].
#[cfg(async_payments)]
#[rustfmt::skip]
pub fn create_static_invoice_builder<'a>(
Expand Down Expand Up @@ -11482,6 +11522,13 @@ where
return NotifyOption::SkipPersistHandleEvents;
//TODO: Also re-broadcast announcement_signatures
});

// While we usually refresh the AsyncReceiveOfferCache on a timer, we also want to start
// interactively building offers as soon as we can after startup. We can't start building offers
// until we have some peer connection(s) to send onion messages over, so as a minor optimization
// refresh the cache when a peer connects.
#[cfg(async_payments)]
self.check_refresh_async_receive_offers();
res
}

Expand Down Expand Up @@ -12806,6 +12853,62 @@ where
MR::Target: MessageRouter,
L::Target: Logger,
{
fn handle_offer_paths_request(
&self, _message: OfferPathsRequest, _context: AsyncPaymentsContext,
_responder: Option<Responder>,
) -> Option<(OfferPaths, ResponseInstruction)> {
None
}

fn handle_offer_paths(
&self, _message: OfferPaths, _context: AsyncPaymentsContext, _responder: Option<Responder>,
) -> Option<(ServeStaticInvoice, ResponseInstruction)> {
#[cfg(async_payments)]
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe I am still surprised that this lives in channelmanager. Didn't look into the flow refactoring yet, but I thought the goal was to keep high level stuff like offers out of chan mgr?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, from my view this this just calls into the flow so nothing substantial is really living in ChannelManager. The manager still receives offers messages in general because it needs to add to the outbound_payments module when we initiate a payment to an offer, but the bulk of the logic now lives in the OffersMessageFlow.

{
let responder = match _responder {
Some(responder) => responder,
None => return None,
};
let (serve_static_invoice, reply_context) = match self.flow.handle_offer_paths(
_message,
_context,
responder.clone(),
self.get_peers_for_blinded_path(),
self.list_usable_channels(),
&*self.entropy_source,
&*self.router,
) {
Some((msg, ctx)) => (msg, ctx),
None => return None,
};
let response_instructions = responder.respond_with_reply_path(reply_context);
return Some((serve_static_invoice, response_instructions));
}

#[cfg(not(async_payments))]
return None;
}

fn handle_serve_static_invoice(
&self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext,
_responder: Option<Responder>,
) {
}

fn handle_static_invoice_persisted(
&self, _message: StaticInvoicePersisted, _context: AsyncPaymentsContext,
) {
#[cfg(async_payments)]
{
let should_persist = self.flow.handle_static_invoice_persisted(_context);
let _persistence_guard =
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure how this works exactly, but can't the call to notify be skipped completely if not should_persist?

PersistenceNotifierGuard::optionally_notify(self, || match should_persist {
true => NotifyOption::DoPersist,
false => NotifyOption::SkipPersistNoEvents,
});
}
}

#[rustfmt::skip]
fn handle_held_htlc_available(
&self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext,
Expand Down Expand Up @@ -13658,6 +13761,7 @@ where
(15, self.inbound_payment_id_secret, required),
(17, in_flight_monitor_updates, option),
(19, peer_storage_dir, optional_vec),
(21, self.flow.writeable_async_receive_offer_cache(), required),
});

Ok(())
Expand Down Expand Up @@ -14222,6 +14326,7 @@ where
let mut decode_update_add_htlcs: Option<HashMap<u64, Vec<msgs::UpdateAddHTLC>>> = None;
let mut inbound_payment_id_secret = None;
let mut peer_storage_dir: Option<Vec<(PublicKey, Vec<u8>)>> = None;
let mut async_receive_offer_cache: AsyncReceiveOfferCache = AsyncReceiveOfferCache::new();
read_tlv_fields!(reader, {
(1, pending_outbound_payments_no_retry, option),
(2, pending_intercepted_htlcs, option),
Expand All @@ -14239,6 +14344,7 @@ where
(15, inbound_payment_id_secret, option),
(17, in_flight_monitor_updates, option),
(19, peer_storage_dir, optional_vec),
(21, async_receive_offer_cache, (default_value, async_receive_offer_cache)),
Copy link
Contributor

Choose a reason for hiding this comment

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

This cache seems so far away from channel manager core responsibilities. Can't it be persisted elsewhere / separately?

I also thought that for fixing the force closures, we wanted to move away from channel mgr persistence completely and reconstruct everything from the monitors.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, it crossed my mind that this might come up when we address removing ChannelManager persistence :( Will start an offline discussion for this next week, not sure what the best solution is. We could have a separate method to retrieve the cache for persistence. Don't think this should be a blocker since it's an optional field that can easily be removed, but it'd be nice to have a path forward.

});
let mut decode_update_add_htlcs = decode_update_add_htlcs.unwrap_or_else(|| new_hash_map());
let peer_storage_dir: Vec<(PublicKey, Vec<u8>)> = peer_storage_dir.unwrap_or_else(Vec::new);
Expand Down Expand Up @@ -14925,6 +15031,8 @@ where
chain_hash, best_block, our_network_pubkey,
highest_seen_timestamp, expanded_inbound_key,
secp_ctx.clone(), args.message_router
).with_async_payments_offers_cache(
async_receive_offer_cache, &args.default_config.paths_to_static_invoice_server[..]
);

let channel_manager = ChannelManager {
Expand Down
2 changes: 1 addition & 1 deletion lightning/src/ln/inbound_payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ pub fn create_from_hash(
}

#[cfg(async_payments)]
pub(super) fn create_for_spontaneous_payment(
pub(crate) fn create_for_spontaneous_payment(
keys: &ExpandedKey, min_value_msat: Option<u64>, invoice_expiry_delta_secs: u32,
current_time: u64, min_final_cltv_expiry_delta: Option<u16>,
) -> Result<PaymentSecret, ()> {
Expand Down
19 changes: 18 additions & 1 deletion lightning/src/ln/peer_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use crate::util::ser::{VecWriter, Writeable, Writer};
use crate::ln::peer_channel_encryptor::{PeerChannelEncryptor, NextNoiseStep, MessageBuf, MSG_BUF_ALLOC_SIZE};
use crate::ln::wire;
use crate::ln::wire::{Encode, Type};
use crate::onion_message::async_payments::{AsyncPaymentsMessageHandler, HeldHtlcAvailable, ReleaseHeldHtlc};
use crate::onion_message::async_payments::{AsyncPaymentsMessageHandler, HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ServeStaticInvoice, ReleaseHeldHtlc, StaticInvoicePersisted};
use crate::onion_message::dns_resolution::{DNSResolverMessageHandler, DNSResolverMessage, DNSSECProof, DNSSECQuery};
use crate::onion_message::messenger::{CustomOnionMessageHandler, Responder, ResponseInstruction, MessageSendInstructions};
use crate::onion_message::offers::{OffersMessage, OffersMessageHandler};
Expand Down Expand Up @@ -150,6 +150,23 @@ impl OffersMessageHandler for IgnoringMessageHandler {
}
}
impl AsyncPaymentsMessageHandler for IgnoringMessageHandler {
fn handle_offer_paths_request(
&self, _message: OfferPathsRequest, _context: AsyncPaymentsContext, _responder: Option<Responder>,
) -> Option<(OfferPaths, ResponseInstruction)> {
None
}
fn handle_offer_paths(
&self, _message: OfferPaths, _context: AsyncPaymentsContext, _responder: Option<Responder>,
) -> Option<(ServeStaticInvoice, ResponseInstruction)> {
None
}
fn handle_serve_static_invoice(
&self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext,
_responder: Option<Responder>,
) {}
fn handle_static_invoice_persisted(
&self, _message: StaticInvoicePersisted, _context: AsyncPaymentsContext,
) {}
fn handle_held_htlc_available(
&self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext,
_responder: Option<Responder>,
Expand Down
Loading
Loading