Skip to content

Commit 1bbfd6f

Browse files
committed
Add async offer refresh readiness APIs
Let async recipients refresh receive offers explicitly, wait for readiness, and preserve payment metadata across static-invoice refreshes. Co-Authored-By: HAL 9000
1 parent fd5bbd6 commit 1bbfd6f

3 files changed

Lines changed: 196 additions & 29 deletions

File tree

lightning/src/ln/channelmanager.rs

Lines changed: 90 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5884,30 +5884,95 @@ impl<
58845884
)
58855885
}
58865886

5887-
fn check_refresh_async_receive_offer_cache(&self, timer_tick_occurred: bool) {
5887+
fn check_refresh_async_receive_offer_cache(&self, timer_tick_occurred: bool) -> Result<(), ()> {
5888+
self.check_refresh_async_receive_offer_cache_with_payment_metadata(
5889+
timer_tick_occurred,
5890+
None,
5891+
)
5892+
}
5893+
5894+
fn check_refresh_async_receive_offer_cache_with_payment_metadata(
5895+
&self, timer_tick_occurred: bool, payment_metadata: Option<BTreeMap<u64, Vec<u8>>>,
5896+
) -> Result<(), ()> {
58885897
let peers = self.get_peers_for_blinded_path();
58895898
let channels = self.list_usable_channels();
58905899
let router = &self.router;
5891-
let refresh_res = self.flow.check_refresh_async_receive_offer_cache(
5900+
self.flow.check_refresh_async_receive_offer_cache_with_payment_metadata(
58925901
peers,
58935902
channels,
58945903
router,
58955904
timer_tick_occurred,
5896-
);
5897-
match refresh_res {
5898-
Err(()) => {
5899-
log_error!(
5900-
self.logger,
5901-
"Failed to create blinded paths when requesting async receive offer paths"
5902-
);
5903-
},
5904-
Ok(()) => {},
5905-
}
5905+
payment_metadata,
5906+
)
59065907
}
59075908

59085909
#[cfg(test)]
59095910
pub(crate) fn test_check_refresh_async_receive_offers(&self) {
5910-
self.check_refresh_async_receive_offer_cache(false);
5911+
self.check_refresh_async_receive_offer_cache(false).unwrap();
5912+
}
5913+
5914+
/// Requests fresh async receive offer paths from the configured static invoice server, if any.
5915+
pub fn refresh_async_receive_offers(&self) -> Result<(), ()> {
5916+
self.check_refresh_async_receive_offer_cache(false).map_err(|()| {
5917+
log_error!(
5918+
self.logger,
5919+
"Failed to create blinded paths when requesting async receive offer paths"
5920+
);
5921+
})
5922+
}
5923+
5924+
/// Requests fresh async receive offer paths from the configured static invoice server, if any,
5925+
/// and attaches `payment_metadata` to the resulting BOLT 12 payment contexts.
5926+
///
5927+
/// The metadata is persisted with the async receive offer cache so future static-invoice
5928+
/// refreshes for the same offer continue to include it.
5929+
pub fn refresh_async_receive_offers_with_payment_metadata(
5930+
&self, payment_metadata: BTreeMap<u64, Vec<u8>>,
5931+
) -> Result<(), ()> {
5932+
self.check_refresh_async_receive_offer_cache_with_payment_metadata(
5933+
false,
5934+
Some(payment_metadata),
5935+
)
5936+
.map_err(|()| {
5937+
log_error!(
5938+
self.logger,
5939+
"Failed to create blinded paths when requesting async receive offer paths"
5940+
);
5941+
})
5942+
}
5943+
5944+
/// Returns once an async receive offer is ready after the interactive static-invoice
5945+
/// protocol completes, or immediately if one is already available.
5946+
///
5947+
/// Callers that need a timeout can combine this future with their runtime's timeout
5948+
/// primitive.
5949+
#[cfg_attr(
5950+
feature = "std",
5951+
doc = "Synchronous callers should instead fetch the underlying [`Future`] via [`Self::get_async_receive_offer_ready_future`] and call [`Future::wait_timeout`] on it."
5952+
)]
5953+
///
5954+
/// [`Future`]: crate::util::wakers::Future
5955+
#[cfg_attr(
5956+
feature = "std",
5957+
doc = "[`Future::wait_timeout`]: crate::util::wakers::Future::wait_timeout"
5958+
)]
5959+
pub async fn await_async_receive_offer(&self) -> Result<Offer, ()> {
5960+
if let Ok(offer) = self.get_async_receive_offer() {
5961+
return Ok(offer);
5962+
}
5963+
5964+
self.flow.get_async_receive_offer_ready_future().await;
5965+
self.get_async_receive_offer()
5966+
}
5967+
5968+
/// Returns a [`Future`] that completes when an async receive offer is ready.
5969+
///
5970+
/// See [`OffersMessageFlow::get_async_receive_offer_ready_future`] for details.
5971+
///
5972+
/// [`Future`]: crate::util::wakers::Future
5973+
/// [`OffersMessageFlow::get_async_receive_offer_ready_future`]: crate::offers::flow::OffersMessageFlow::get_async_receive_offer_ready_future
5974+
pub fn get_async_receive_offer_ready_future(&self) -> crate::util::wakers::Future {
5975+
self.flow.get_async_receive_offer_ready_future()
59115976
}
59125977

59135978
/// Should be called after handling an [`Event::PersistStaticInvoice`], where the `Responder`
@@ -9129,7 +9194,12 @@ impl<
91299194
self.pending_outbound_payments
91309195
.remove_stale_payments(duration_since_epoch, &self.pending_events);
91319196

9132-
self.check_refresh_async_receive_offer_cache(true);
9197+
let _ = self.check_refresh_async_receive_offer_cache(true).map_err(|()| {
9198+
log_error!(
9199+
self.logger,
9200+
"Failed to create blinded paths when requesting async receive offer paths"
9201+
);
9202+
});
91339203

91349204
if self.check_free_holding_cells() {
91359205
// While we try to ensure we clear holding cells immediately, its possible we miss
@@ -16055,7 +16125,12 @@ impl<
1605516125
// interactively building offers as soon as we can after startup. We can't start building offers
1605616126
// until we have some peer connection(s) to receive onion messages over, so as a minor optimization
1605716127
// refresh the cache when a peer connects.
16058-
self.check_refresh_async_receive_offer_cache(false);
16128+
let _ = self.check_refresh_async_receive_offer_cache(false).map_err(|()| {
16129+
log_error!(
16130+
self.logger,
16131+
"Failed to create blinded paths when requesting async receive offer paths"
16132+
);
16133+
});
1605916134
res
1606016135
}
1606116136

lightning/src/offers/async_receive_offer_cache.rs

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
//! server as an async recipient. The static invoice server will serve the resulting invoices to
1212
//! payers on our behalf when we're offline.
1313
14+
use alloc::collections::BTreeMap;
15+
1416
use crate::blinded_path::message::{AsyncPaymentsContext, BlindedMessagePath};
1517
use crate::io;
1618
use crate::io::Read;
@@ -19,7 +21,7 @@ use crate::offers::nonce::Nonce;
1921
use crate::offers::offer::Offer;
2022
use crate::onion_message::messenger::Responder;
2123
use crate::prelude::*;
22-
use crate::util::ser::{Readable, Writeable, Writer};
24+
use crate::util::ser::{BigSizeKeyedMap, Readable, Writeable, Writer};
2325
use core::time::Duration;
2426

2527
/// The status of this offer in the cache.
@@ -62,6 +64,7 @@ struct AsyncReceiveOffer {
6264
/// payment paths become otherwise outdated.
6365
offer_nonce: Nonce,
6466
update_static_invoice_path: Responder,
67+
payment_metadata: Option<BTreeMap<u64, Vec<u8>>>,
6568
}
6669

6770
impl AsyncReceiveOffer {
@@ -92,6 +95,7 @@ impl_ser_tlv_based!(AsyncReceiveOffer, {
9295
(4, status, required),
9396
(6, update_static_invoice_path, required),
9497
(8, created_at, required),
98+
(10, payment_metadata, (option, encoding: (BTreeMap<u64, Vec<u8>>, BigSizeKeyedMap))),
9599
});
96100

97101
/// If we are an often-offline recipient, we'll want to interactively build offers and static
@@ -147,6 +151,8 @@ pub struct AsyncReceiveOfferCache {
147151
/// Blinded paths used to request offer paths from the static invoice server.
148152
#[allow(unused)] // TODO: remove when we get rid of async payments cfg flag
149153
paths_to_static_invoice_server: Vec<BlindedMessagePath>,
154+
/// Payment metadata associated with offer-path requests in flight.
155+
pending_offer_payment_metadata: BTreeMap<u16, Option<BTreeMap<u64, Vec<u8>>>>,
150156
}
151157

152158
impl AsyncReceiveOfferCache {
@@ -158,6 +164,7 @@ impl AsyncReceiveOfferCache {
158164
offers: Vec::new(),
159165
offer_paths_request_attempts: 0,
160166
paths_to_static_invoice_server: Vec::new(),
167+
pending_offer_payment_metadata: BTreeMap::new(),
161168
}
162169
}
163170

@@ -320,6 +327,7 @@ impl AsyncReceiveOfferCache {
320327
pub(super) fn cache_pending_offer(
321328
&mut self, offer: Offer, offer_paths_absolute_expiry_secs: Option<u64>, offer_nonce: Nonce,
322329
update_static_invoice_path: Responder, duration_since_epoch: Duration, slot: u16,
330+
payment_metadata: Option<BTreeMap<u64, Vec<u8>>>,
323331
) -> Result<(), ()> {
324332
self.prune_expired_offers(duration_since_epoch, false);
325333

@@ -340,13 +348,15 @@ impl AsyncReceiveOfferCache {
340348
offer_nonce,
341349
status: OfferStatus::Pending,
342350
update_static_invoice_path,
351+
payment_metadata,
343352
})
344353
},
345354
None => {
346355
debug_assert!(false, "Slot in cache was invalid but should'be been checked above");
347356
return Err(());
348357
},
349358
}
359+
self.pending_offer_payment_metadata.remove(&slot);
350360

351361
Ok(())
352362
}
@@ -433,8 +443,11 @@ impl AsyncReceiveOfferCache {
433443

434444
// Indicates that onion messages requesting new offer paths have been sent to the static invoice
435445
// server. Calling this method allows the cache to self-limit how many requests are sent.
436-
pub(super) fn new_offers_requested(&mut self) {
446+
pub(super) fn new_offers_requested(
447+
&mut self, slot: u16, payment_metadata: Option<BTreeMap<u64, Vec<u8>>>,
448+
) {
437449
self.offer_paths_request_attempts += 1;
450+
self.pending_offer_payment_metadata.insert(slot, payment_metadata);
438451
}
439452

440453
/// Called on timer tick (roughly once per minute) to allow another [`MAX_UPDATE_ATTEMPTS`] offer
@@ -447,7 +460,7 @@ impl AsyncReceiveOfferCache {
447460
/// the static invoice server.
448461
pub(super) fn offers_needing_invoice_refresh(
449462
&self, duration_since_epoch: Duration,
450-
) -> impl Iterator<Item = (&Offer, Nonce, &Responder)> {
463+
) -> impl Iterator<Item = (&Offer, Nonce, &Responder, Option<BTreeMap<u64, Vec<u8>>>)> {
451464
// For any offers which are either in use or pending confirmation by the server, we should send
452465
// them a fresh invoice on each timer tick.
453466
self.offers_with_idx().filter_map(move |(_, offer)| {
@@ -462,13 +475,44 @@ impl AsyncReceiveOfferCache {
462475
OfferStatus::Ready { .. } => false,
463476
};
464477
if needs_invoice_update {
465-
Some((&offer.offer, offer.offer_nonce, &offer.update_static_invoice_path))
478+
Some((
479+
&offer.offer,
480+
offer.offer_nonce,
481+
&offer.update_static_invoice_path,
482+
offer.payment_metadata.clone(),
483+
))
466484
} else {
467485
None
468486
}
469487
})
470488
}
471489

490+
/// Returns the payment metadata that should be attached to a replacement offer in `slot`.
491+
pub(super) fn payment_metadata_for_new_offer(
492+
&self, slot: u16, payment_metadata: Option<BTreeMap<u64, Vec<u8>>>,
493+
) -> Option<BTreeMap<u64, Vec<u8>>> {
494+
payment_metadata
495+
.or_else(|| self.pending_offer_payment_metadata.get(&slot).cloned().flatten())
496+
.or_else(|| {
497+
self.offers
498+
.get(slot as usize)
499+
.and_then(|offer| offer.as_ref())
500+
.and_then(|offer| offer.payment_metadata.clone())
501+
})
502+
}
503+
504+
/// Returns the payment metadata associated with an incoming offer-path response for `slot`.
505+
pub(super) fn payment_metadata_for_offer_slot(
506+
&self, slot: u16,
507+
) -> Option<BTreeMap<u64, Vec<u8>>> {
508+
self.pending_offer_payment_metadata.get(&slot).cloned().flatten().or_else(|| {
509+
self.offers
510+
.get(slot as usize)
511+
.and_then(|offer| offer.as_ref())
512+
.and_then(|offer| offer.payment_metadata.clone())
513+
})
514+
}
515+
472516
/// Should be called when we receive a [`StaticInvoicePersisted`] message from the static invoice
473517
/// server, which indicates that a new offer was persisted by the server and they are ready to
474518
/// serve the corresponding static invoice to payers on our behalf.
@@ -536,6 +580,11 @@ impl Readable for AsyncReceiveOfferCache {
536580
(2, paths_to_static_invoice_server, required_vec),
537581
});
538582
let offers: Vec<Option<AsyncReceiveOffer>> = offers;
539-
Ok(Self { offers, offer_paths_request_attempts: 0, paths_to_static_invoice_server })
583+
Ok(Self {
584+
offers,
585+
offer_paths_request_attempts: 0,
586+
paths_to_static_invoice_server,
587+
pending_offer_payment_metadata: BTreeMap::new(),
588+
})
540589
}
541590
}

0 commit comments

Comments
 (0)