Skip to content

Commit 808d1dc

Browse files
authored
Merge pull request #3618 from valentinewallace/2025-02-static-inv-server-client
Async recipient-side of static invoice server
2 parents f66d568 + 0608de1 commit 808d1dc

File tree

11 files changed

+1329
-10
lines changed

11 files changed

+1329
-10
lines changed

fuzz/src/onion_message.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ use lightning::ln::peer_handler::IgnoringMessageHandler;
1515
use lightning::ln::script::ShutdownScript;
1616
use lightning::offers::invoice::UnsignedBolt12Invoice;
1717
use lightning::onion_message::async_payments::{
18-
AsyncPaymentsMessageHandler, HeldHtlcAvailable, ReleaseHeldHtlc,
18+
AsyncPaymentsMessageHandler, HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ReleaseHeldHtlc,
19+
ServeStaticInvoice, StaticInvoicePersisted,
1920
};
2021
use lightning::onion_message::messenger::{
2122
CustomOnionMessageHandler, Destination, MessageRouter, MessageSendInstructions,
@@ -124,6 +125,30 @@ impl OffersMessageHandler for TestOffersMessageHandler {
124125
struct TestAsyncPaymentsMessageHandler {}
125126

126127
impl AsyncPaymentsMessageHandler for TestAsyncPaymentsMessageHandler {
128+
fn handle_offer_paths_request(
129+
&self, _message: OfferPathsRequest, _context: AsyncPaymentsContext,
130+
responder: Option<Responder>,
131+
) -> Option<(OfferPaths, ResponseInstruction)> {
132+
let responder = match responder {
133+
Some(resp) => resp,
134+
None => return None,
135+
};
136+
Some((OfferPaths { paths: Vec::new(), paths_absolute_expiry: None }, responder.respond()))
137+
}
138+
fn handle_offer_paths(
139+
&self, _message: OfferPaths, _context: AsyncPaymentsContext, _responder: Option<Responder>,
140+
) -> Option<(ServeStaticInvoice, ResponseInstruction)> {
141+
None
142+
}
143+
fn handle_serve_static_invoice(
144+
&self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext,
145+
_responder: Option<Responder>,
146+
) {
147+
}
148+
fn handle_static_invoice_persisted(
149+
&self, _message: StaticInvoicePersisted, _context: AsyncPaymentsContext,
150+
) {
151+
}
127152
fn handle_held_htlc_available(
128153
&self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext,
129154
responder: Option<Responder>,

lightning/src/blinded_path/message.rs

Lines changed: 42 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::OfferId;
2627
use crate::onion_message::packet::ControlTlvs;
2728
use crate::routing::gossip::{NodeId, ReadOnlyNetworkGraph};
2829
use crate::sign::{EntropySource, NodeSigner, Recipient};
@@ -404,6 +405,40 @@ pub enum OffersContext {
404405
/// [`AsyncPaymentsMessage`]: crate::onion_message::async_payments::AsyncPaymentsMessage
405406
#[derive(Clone, Debug)]
406407
pub enum AsyncPaymentsContext {
408+
/// Context used by a reply path to an [`OfferPathsRequest`], provided back to us as an async
409+
/// recipient in corresponding [`OfferPaths`] messages from the static invoice server.
410+
///
411+
/// [`OfferPathsRequest`]: crate::onion_message::async_payments::OfferPathsRequest
412+
/// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths
413+
OfferPaths {
414+
/// The time as duration since the Unix epoch at which this path expires and messages sent over
415+
/// it should be ignored.
416+
///
417+
/// This avoids the situation where the [`OfferPaths`] message is very delayed and thus
418+
/// outdated.
419+
///
420+
/// [`OfferPaths`]: crate::onion_message::async_payments::OfferPaths
421+
path_absolute_expiry: core::time::Duration,
422+
},
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 id of the offer in the cache corresponding to the [`StaticInvoice`] that has been
430+
/// persisted. This invoice is now ready to be provided by the static invoice server in response
431+
/// to [`InvoiceRequest`]s, so the corresponding offer can be marked as ready to receive
432+
/// payments.
433+
///
434+
/// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice
435+
/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
436+
offer_id: OfferId,
437+
/// The time as duration since the Unix epoch at which this path expires and messages sent over
438+
/// it should be ignored. If we receive confirmation of an invoice over this path after its
439+
/// expiry, it may be outdated and a new invoice update should be sent instead.
440+
path_absolute_expiry: core::time::Duration,
441+
},
407442
/// Context contained within the reply [`BlindedMessagePath`] we put in outbound
408443
/// [`HeldHtlcAvailable`] messages, provided back to us in corresponding [`ReleaseHeldHtlc`]
409444
/// messages.
@@ -486,6 +521,13 @@ impl_writeable_tlv_based_enum!(AsyncPaymentsContext,
486521
(2, hmac, required),
487522
(4, path_absolute_expiry, required),
488523
},
524+
(2, OfferPaths) => {
525+
(0, path_absolute_expiry, required),
526+
},
527+
(3, StaticInvoicePersisted) => {
528+
(0, offer_id, required),
529+
(2, path_absolute_expiry, required),
530+
},
489531
);
490532

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

lightning/src/ln/channelmanager.rs

Lines changed: 140 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ use crate::ln::outbound_payment::{
8686
StaleExpiration,
8787
};
8888
use crate::ln::types::ChannelId;
89+
use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache;
8990
use crate::offers::flow::OffersMessageFlow;
9091
use crate::offers::invoice::{
9192
Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder, DEFAULT_RELATIVE_EXPIRY,
@@ -98,7 +99,8 @@ use crate::offers::parse::Bolt12SemanticError;
9899
use crate::offers::refund::Refund;
99100
use crate::offers::signer;
100101
use crate::onion_message::async_payments::{
101-
AsyncPaymentsMessage, AsyncPaymentsMessageHandler, HeldHtlcAvailable, ReleaseHeldHtlc,
102+
AsyncPaymentsMessage, AsyncPaymentsMessageHandler, HeldHtlcAvailable, OfferPaths,
103+
OfferPathsRequest, ReleaseHeldHtlc, ServeStaticInvoice, StaticInvoicePersisted,
102104
};
103105
use crate::onion_message::dns_resolution::HumanReadableName;
104106
use crate::onion_message::messenger::{
@@ -5246,6 +5248,30 @@ where
52465248
)
52475249
}
52485250

5251+
#[cfg(async_payments)]
5252+
fn check_refresh_async_receive_offer_cache(&self, timer_tick_occurred: bool) {
5253+
let peers = self.get_peers_for_blinded_path();
5254+
let channels = self.list_usable_channels();
5255+
let entropy = &*self.entropy_source;
5256+
let router = &*self.router;
5257+
let refresh_res = self.flow.check_refresh_async_receive_offer_cache(
5258+
peers,
5259+
channels,
5260+
entropy,
5261+
router,
5262+
timer_tick_occurred,
5263+
);
5264+
match refresh_res {
5265+
Err(()) => {
5266+
log_error!(
5267+
self.logger,
5268+
"Failed to create blinded paths when requesting async receive offer paths"
5269+
);
5270+
},
5271+
Ok(()) => {},
5272+
}
5273+
}
5274+
52495275
#[cfg(async_payments)]
52505276
fn initiate_async_payment(
52515277
&self, invoice: &StaticInvoice, payment_id: PaymentId,
@@ -7240,6 +7266,9 @@ where
72407266
duration_since_epoch, &self.pending_events
72417267
);
72427268

7269+
#[cfg(async_payments)]
7270+
self.check_refresh_async_receive_offer_cache(true);
7271+
72437272
// Technically we don't need to do this here, but if we have holding cell entries in a
72447273
// channel that need freeing, it's better to do that here and block a background task
72457274
// than block the message queueing pipeline.
@@ -10999,9 +11028,29 @@ where
1099911028
#[cfg(c_bindings)]
1100011029
create_refund_builder!(self, RefundMaybeWithDerivedMetadataBuilder);
1100111030

11031+
/// Retrieve an [`Offer`] for receiving async payments as an often-offline recipient. Will only
11032+
/// return an offer if [`Self::set_paths_to_static_invoice_server`] was called and we succeeded in
11033+
/// interactively building a [`StaticInvoice`] with the static invoice server.
11034+
///
11035+
/// Useful for posting offers to receive payments later, such as posting an offer on a website.
11036+
#[cfg(async_payments)]
11037+
pub fn get_async_receive_offer(&self) -> Result<Offer, ()> {
11038+
let (offer, needs_persist) = self.flow.get_async_receive_offer()?;
11039+
if needs_persist {
11040+
// We need to re-persist the cache if a fresh offer was just marked as used to ensure we
11041+
// continue to keep this offer's invoice updated and don't replace it with the server.
11042+
let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self);
11043+
}
11044+
Ok(offer)
11045+
}
11046+
1100211047
/// Create an offer for receiving async payments as an often-offline recipient.
1100311048
///
11004-
/// Because we may be offline when the payer attempts to request an invoice, you MUST:
11049+
/// Instead of using this method, it is preferable to call
11050+
/// [`Self::set_paths_to_static_invoice_server`] and retrieve the automatically built offer via
11051+
/// [`Self::get_async_receive_offer`].
11052+
///
11053+
/// If you want to build the [`StaticInvoice`] manually using this method instead, you MUST:
1100511054
/// 1. Provide at least 1 [`BlindedMessagePath`] terminating at an always-online node that will
1100611055
/// serve the [`StaticInvoice`] created from this offer on our behalf.
1100711056
/// 2. Use [`Self::create_static_invoice_builder`] to create a [`StaticInvoice`] from this
@@ -11018,6 +11067,10 @@ where
1101811067
/// Creates a [`StaticInvoiceBuilder`] from the corresponding [`Offer`] and [`Nonce`] that were
1101911068
/// created via [`Self::create_async_receive_offer_builder`]. If `relative_expiry` is unset, the
1102011069
/// invoice's expiry will default to [`STATIC_INVOICE_DEFAULT_RELATIVE_EXPIRY`].
11070+
///
11071+
/// Instead of using this method to manually build the invoice, it is preferable to set
11072+
/// [`Self::set_paths_to_static_invoice_server`] and retrieve the automatically built offer via
11073+
/// [`Self::get_async_receive_offer`].
1102111074
#[cfg(async_payments)]
1102211075
pub fn create_static_invoice_builder<'a>(
1102311076
&self, offer: &'a Offer, offer_nonce: Nonce, relative_expiry: Option<Duration>,
@@ -11053,6 +11106,22 @@ where
1105311106
)
1105411107
}
1105511108

11109+
/// Sets the [`BlindedMessagePath`]s that we will use as an async recipient to interactively build
11110+
/// [`Offer`]s with a static invoice server, so the server can serve [`StaticInvoice`]s to payers
11111+
/// on our behalf when we're offline.
11112+
///
11113+
/// This method only needs to be called once when the server first takes on the recipient as a
11114+
/// client, or when the paths change, e.g. if the paths are set to expire at a particular time.
11115+
#[cfg(async_payments)]
11116+
pub fn set_paths_to_static_invoice_server(
11117+
&self, paths_to_static_invoice_server: Vec<BlindedMessagePath>,
11118+
) -> Result<(), ()> {
11119+
self.flow.set_paths_to_static_invoice_server(paths_to_static_invoice_server)?;
11120+
11121+
let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self);
11122+
Ok(())
11123+
}
11124+
1105611125
/// Pays for an [`Offer`] using the given parameters by creating an [`InvoiceRequest`] and
1105711126
/// enqueuing it to be sent via an onion message. [`ChannelManager`] will pay the actual
1105811127
/// [`Bolt12Invoice`] once it is received.
@@ -11960,6 +12029,13 @@ where
1196012029
return NotifyOption::SkipPersistHandleEvents;
1196112030
//TODO: Also re-broadcast announcement_signatures
1196212031
});
12032+
12033+
// While we usually refresh the AsyncReceiveOfferCache on a timer, we also want to start
12034+
// interactively building offers as soon as we can after startup. We can't start building offers
12035+
// until we have some peer connection(s) to send onion messages over, so as a minor optimization
12036+
// refresh the cache when a peer connects.
12037+
#[cfg(async_payments)]
12038+
self.check_refresh_async_receive_offer_cache(false);
1196312039
res
1196412040
}
1196512041

@@ -13374,6 +13450,64 @@ where
1337413450
MR::Target: MessageRouter,
1337513451
L::Target: Logger,
1337613452
{
13453+
fn handle_offer_paths_request(
13454+
&self, _message: OfferPathsRequest, _context: AsyncPaymentsContext,
13455+
_responder: Option<Responder>,
13456+
) -> Option<(OfferPaths, ResponseInstruction)> {
13457+
None
13458+
}
13459+
13460+
fn handle_offer_paths(
13461+
&self, _message: OfferPaths, _context: AsyncPaymentsContext, _responder: Option<Responder>,
13462+
) -> Option<(ServeStaticInvoice, ResponseInstruction)> {
13463+
#[cfg(async_payments)]
13464+
{
13465+
let responder = match _responder {
13466+
Some(responder) => responder,
13467+
None => return None,
13468+
};
13469+
let (serve_static_invoice, reply_context) = match self.flow.handle_offer_paths(
13470+
_message,
13471+
_context,
13472+
responder.clone(),
13473+
self.get_peers_for_blinded_path(),
13474+
self.list_usable_channels(),
13475+
&*self.entropy_source,
13476+
&*self.router,
13477+
) {
13478+
Some((msg, ctx)) => (msg, ctx),
13479+
None => return None,
13480+
};
13481+
13482+
// We cached a new pending offer, so persist the cache.
13483+
let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self);
13484+
13485+
let response_instructions = responder.respond_with_reply_path(reply_context);
13486+
return Some((serve_static_invoice, response_instructions));
13487+
}
13488+
13489+
#[cfg(not(async_payments))]
13490+
return None;
13491+
}
13492+
13493+
fn handle_serve_static_invoice(
13494+
&self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext,
13495+
_responder: Option<Responder>,
13496+
) {
13497+
}
13498+
13499+
fn handle_static_invoice_persisted(
13500+
&self, _message: StaticInvoicePersisted, _context: AsyncPaymentsContext,
13501+
) {
13502+
#[cfg(async_payments)]
13503+
{
13504+
let should_persist = self.flow.handle_static_invoice_persisted(_context);
13505+
if should_persist {
13506+
let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self);
13507+
}
13508+
}
13509+
}
13510+
1337713511
fn handle_held_htlc_available(
1337813512
&self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext,
1337913513
_responder: Option<Responder>,
@@ -14239,6 +14373,7 @@ where
1423914373
(15, self.inbound_payment_id_secret, required),
1424014374
(17, in_flight_monitor_updates, option),
1424114375
(19, peer_storage_dir, optional_vec),
14376+
(21, self.flow.writeable_async_receive_offer_cache(), required),
1424214377
});
1424314378

1424414379
Ok(())
@@ -14818,6 +14953,7 @@ where
1481814953
let mut decode_update_add_htlcs: Option<HashMap<u64, Vec<msgs::UpdateAddHTLC>>> = None;
1481914954
let mut inbound_payment_id_secret = None;
1482014955
let mut peer_storage_dir: Option<Vec<(PublicKey, Vec<u8>)>> = None;
14956+
let mut async_receive_offer_cache: AsyncReceiveOfferCache = AsyncReceiveOfferCache::new();
1482114957
read_tlv_fields!(reader, {
1482214958
(1, pending_outbound_payments_no_retry, option),
1482314959
(2, pending_intercepted_htlcs, option),
@@ -14835,6 +14971,7 @@ where
1483514971
(15, inbound_payment_id_secret, option),
1483614972
(17, in_flight_monitor_updates, option),
1483714973
(19, peer_storage_dir, optional_vec),
14974+
(21, async_receive_offer_cache, (default_value, async_receive_offer_cache)),
1483814975
});
1483914976
let mut decode_update_add_htlcs = decode_update_add_htlcs.unwrap_or_else(|| new_hash_map());
1484014977
let peer_storage_dir: Vec<(PublicKey, Vec<u8>)> = peer_storage_dir.unwrap_or_else(Vec::new);
@@ -15521,7 +15658,7 @@ where
1552115658
chain_hash, best_block, our_network_pubkey,
1552215659
highest_seen_timestamp, expanded_inbound_key,
1552315660
secp_ctx.clone(), args.message_router
15524-
);
15661+
).with_async_payments_offers_cache(async_receive_offer_cache);
1552515662

1552615663
let channel_manager = ChannelManager {
1552715664
chain_hash,

lightning/src/ln/inbound_payment.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ pub fn create_from_hash(
214214
}
215215

216216
#[cfg(async_payments)]
217-
pub(super) fn create_for_spontaneous_payment(
217+
pub(crate) fn create_for_spontaneous_payment(
218218
keys: &ExpandedKey, min_value_msat: Option<u64>, invoice_expiry_delta_secs: u32,
219219
current_time: u64, min_final_cltv_expiry_delta: Option<u16>,
220220
) -> Result<PaymentSecret, ()> {

lightning/src/ln/peer_handler.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ use crate::ln::types::ChannelId;
3131
use crate::ln::wire;
3232
use crate::ln::wire::{Encode, Type};
3333
use crate::onion_message::async_payments::{
34-
AsyncPaymentsMessageHandler, HeldHtlcAvailable, ReleaseHeldHtlc,
34+
AsyncPaymentsMessageHandler, HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ReleaseHeldHtlc,
35+
ServeStaticInvoice, StaticInvoicePersisted,
3536
};
3637
use crate::onion_message::dns_resolution::{
3738
DNSResolverMessage, DNSResolverMessageHandler, DNSSECProof, DNSSECQuery,
@@ -212,6 +213,26 @@ impl OffersMessageHandler for IgnoringMessageHandler {
212213
}
213214
}
214215
impl AsyncPaymentsMessageHandler for IgnoringMessageHandler {
216+
fn handle_offer_paths_request(
217+
&self, _message: OfferPathsRequest, _context: AsyncPaymentsContext,
218+
_responder: Option<Responder>,
219+
) -> Option<(OfferPaths, ResponseInstruction)> {
220+
None
221+
}
222+
fn handle_offer_paths(
223+
&self, _message: OfferPaths, _context: AsyncPaymentsContext, _responder: Option<Responder>,
224+
) -> Option<(ServeStaticInvoice, ResponseInstruction)> {
225+
None
226+
}
227+
fn handle_serve_static_invoice(
228+
&self, _message: ServeStaticInvoice, _context: AsyncPaymentsContext,
229+
_responder: Option<Responder>,
230+
) {
231+
}
232+
fn handle_static_invoice_persisted(
233+
&self, _message: StaticInvoicePersisted, _context: AsyncPaymentsContext,
234+
) {
235+
}
215236
fn handle_held_htlc_available(
216237
&self, _message: HeldHtlcAvailable, _context: AsyncPaymentsContext,
217238
_responder: Option<Responder>,

0 commit comments

Comments
 (0)