Skip to content

Commit 705469e

Browse files
committed
Add async offer refresh readiness APIs
To enable suppoort for async payments via LSPS2 JIT channels, we expose explicit async receive-offer refresh and readiness waiting so integrators can sequence external setup before relying on a ready async offer, instead of polling timer ticks. Generated with AI assistance. Co-Authored-By: HAL 9000
1 parent fdeaee8 commit 705469e

3 files changed

Lines changed: 62 additions & 25 deletions

File tree

lightning-liquidity/src/lsps2/router.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,11 @@ impl<R: Router, ES: EntropySource + Send + Sync> LSPS2BOLT12Router<R, ES> {
8989
fn registered_lsps2_params(
9090
&self, payment_context: &PaymentContext,
9191
) -> Vec<LSPS2Bolt12InvoiceParameters> {
92-
// We intentionally only match `Bolt12Offer` here and not `AsyncBolt12Offer`, as LSPS2
93-
// JIT channels are not applicable to async (always-online) BOLT12 offer flows.
92+
// LSPS2 paths are applicable both to normal offers and async offers that resolve via a
93+
// static invoice server. In both cases the intercept SCID lets the LSP intercept the HTLC
94+
// and open the JIT channel before forwarding the payment.
9495
match payment_context {
95-
PaymentContext::Bolt12Offer(_) => {},
96+
PaymentContext::Bolt12Offer(_) | PaymentContext::AsyncBolt12Offer(_) => {},
9697
_ => return Vec::new(),
9798
};
9899

lightning/src/ln/channelmanager.rs

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5802,30 +5802,46 @@ impl<
58025802
)
58035803
}
58045804

5805-
fn check_refresh_async_receive_offer_cache(&self, timer_tick_occurred: bool) {
5805+
fn check_refresh_async_receive_offer_cache(&self, timer_tick_occurred: bool) -> Result<(), ()> {
58065806
let peers = self.get_peers_for_blinded_path();
58075807
let channels = self.list_usable_channels();
58085808
let router = &self.router;
5809-
let refresh_res = self.flow.check_refresh_async_receive_offer_cache(
5809+
self.flow.check_refresh_async_receive_offer_cache(
58105810
peers,
58115811
channels,
58125812
router,
58135813
timer_tick_occurred,
5814-
);
5815-
match refresh_res {
5816-
Err(()) => {
5817-
log_error!(
5818-
self.logger,
5819-
"Failed to create blinded paths when requesting async receive offer paths"
5820-
);
5821-
},
5822-
Ok(()) => {},
5823-
}
5814+
)
58245815
}
58255816

58265817
#[cfg(test)]
58275818
pub(crate) fn test_check_refresh_async_receive_offers(&self) {
5828-
self.check_refresh_async_receive_offer_cache(false);
5819+
self.check_refresh_async_receive_offer_cache(false).unwrap();
5820+
}
5821+
5822+
/// Requests fresh async receive offer paths from the configured static invoice server, if any.
5823+
pub fn refresh_async_receive_offers(&self) -> Result<(), ()> {
5824+
self.check_refresh_async_receive_offer_cache(false).map_err(|()| {
5825+
log_error!(
5826+
self.logger,
5827+
"Failed to create blinded paths when requesting async receive offer paths"
5828+
);
5829+
})
5830+
}
5831+
5832+
/// Waits for an async receive offer to become ready after the interactive static-invoice
5833+
/// protocol completes.
5834+
#[cfg(feature = "std")]
5835+
pub fn await_async_receive_offer(&self, max_wait: Duration) -> Result<Offer, ()> {
5836+
if let Ok(offer) = self.get_async_receive_offer() {
5837+
return Ok(offer);
5838+
}
5839+
5840+
if !self.flow.wait_for_async_receive_offer_ready(max_wait) {
5841+
return Err(());
5842+
}
5843+
5844+
self.get_async_receive_offer()
58295845
}
58305846

58315847
/// Should be called after handling an [`Event::PersistStaticInvoice`], where the `Responder`
@@ -8920,7 +8936,12 @@ impl<
89208936
self.pending_outbound_payments
89218937
.remove_stale_payments(duration_since_epoch, &self.pending_events);
89228938

8923-
self.check_refresh_async_receive_offer_cache(true);
8939+
let _ = self.check_refresh_async_receive_offer_cache(true).map_err(|()| {
8940+
log_error!(
8941+
self.logger,
8942+
"Failed to create blinded paths when requesting async receive offer paths"
8943+
);
8944+
});
89248945

89258946
if self.check_free_holding_cells() {
89268947
// While we try to ensure we clear holding cells immediately, its possible we miss
@@ -15619,7 +15640,12 @@ impl<
1561915640
// interactively building offers as soon as we can after startup. We can't start building offers
1562015641
// until we have some peer connection(s) to receive onion messages over, so as a minor optimization
1562115642
// refresh the cache when a peer connects.
15622-
self.check_refresh_async_receive_offer_cache(false);
15643+
let _ = self.check_refresh_async_receive_offer_cache(false).map_err(|()| {
15644+
log_error!(
15645+
self.logger,
15646+
"Failed to create blinded paths when requesting async receive offer paths"
15647+
);
15648+
});
1562315649
res
1562415650
}
1562515651

lightning/src/offers/flow.rs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ use crate::sync::{Mutex, RwLock};
6161
use crate::types::payment::{PaymentHash, PaymentSecret};
6262
use crate::util::logger::Logger;
6363
use crate::util::ser::Writeable;
64+
#[cfg(feature = "std")]
65+
use crate::util::wakers::Notifier;
6466

6567
#[cfg(feature = "dnssec")]
6668
use {
@@ -93,6 +95,8 @@ pub struct OffersMessageFlow<MR: MessageRouter, L: Logger> {
9395

9496
pending_async_payments_messages: Mutex<Vec<(AsyncPaymentsMessage, MessageSendInstructions)>>,
9597
async_receive_offer_cache: Mutex<AsyncReceiveOfferCache>,
98+
#[cfg(feature = "std")]
99+
async_receive_offer_ready_notifier: Notifier,
96100

97101
#[cfg(feature = "dnssec")]
98102
pub(crate) hrn_resolver: OMNameResolver,
@@ -132,6 +136,8 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
132136
pending_dns_onion_messages: Mutex::new(Vec::new()),
133137

134138
async_receive_offer_cache: Mutex::new(AsyncReceiveOfferCache::new()),
139+
#[cfg(feature = "std")]
140+
async_receive_offer_ready_notifier: Notifier::new(),
135141

136142
logger,
137143
}
@@ -1422,7 +1428,6 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
14221428
Some(idx) => idx,
14231429
None => return Ok(()),
14241430
};
1425-
14261431
// If we need new offers, send out offer paths request messages to the static invoice server.
14271432
let context = MessageContext::AsyncPayments(AsyncPaymentsContext::OfferPaths {
14281433
path_absolute_expiry: duration_since_epoch
@@ -1536,7 +1541,6 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
15361541
},
15371542
_ => return None,
15381543
};
1539-
15401544
// Create the blinded paths that will be included in the async recipient's offer.
15411545
let (offer_paths, paths_expiry) = {
15421546
let path_absolute_expiry =
@@ -1598,7 +1602,6 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
15981602
},
15991603
_ => return None,
16001604
};
1601-
16021605
{
16031606
// Only respond with `ServeStaticInvoice` if we actually need a new offer built.
16041607
let mut cache = self.async_receive_offer_cache.lock().unwrap();
@@ -1630,7 +1633,6 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
16301633
return None;
16311634
},
16321635
};
1633-
16341636
let (invoice, forward_invoice_request_path) = match self.create_static_invoice_for_server(
16351637
&offer,
16361638
offer_nonce,
@@ -1644,7 +1646,6 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
16441646
return None;
16451647
},
16461648
};
1647-
16481649
if let Err(()) = self.async_receive_offer_cache.lock().unwrap().cache_pending_offer(
16491650
offer,
16501651
message.paths_absolute_expiry,
@@ -1656,7 +1657,6 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
16561657
log_error!(self.logger, "Failed to cache pending offer");
16571658
return None;
16581659
}
1659-
16601660
let reply_path_context = {
16611661
MessageContext::AsyncPayments(AsyncPaymentsContext::StaticInvoicePersisted {
16621662
offer_id,
@@ -1772,7 +1772,17 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> {
17721772
/// [`StaticInvoicePersisted`]: crate::onion_message::async_payments::StaticInvoicePersisted
17731773
pub fn handle_static_invoice_persisted(&self, context: AsyncPaymentsContext) -> bool {
17741774
let mut cache = self.async_receive_offer_cache.lock().unwrap();
1775-
cache.static_invoice_persisted(context)
1775+
let updated = cache.static_invoice_persisted(context);
1776+
#[cfg(feature = "std")]
1777+
if updated {
1778+
self.async_receive_offer_ready_notifier.notify();
1779+
}
1780+
updated
1781+
}
1782+
1783+
#[cfg(feature = "std")]
1784+
pub(crate) fn wait_for_async_receive_offer_ready(&self, max_wait: Duration) -> bool {
1785+
self.async_receive_offer_ready_notifier.get_future().wait_timeout(max_wait)
17761786
}
17771787

17781788
/// Get the encoded [`AsyncReceiveOfferCache`] for persistence.

0 commit comments

Comments
 (0)