Skip to content

Commit 06cf5a0

Browse files
Cache offer on StaticInvoicePersisted onion message
As an async recipient, we need to interactively build a static invoice that an always-online node will serve on our behalf. Once this invoice is built and persisted by the static invoice server, they will send us a confirmation onion message. At this time, cache the corresponding offer and mark it as ready to receive async payments.
1 parent 4f8d985 commit 06cf5a0

File tree

4 files changed

+167
-2
lines changed

4 files changed

+167
-2
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13077,6 +13077,13 @@ where
1307713077
fn handle_static_invoice_persisted(
1307813078
&self, _message: StaticInvoicePersisted, _context: AsyncPaymentsContext,
1307913079
) {
13080+
#[cfg(async_payments)]
13081+
{
13082+
let should_persist = self.flow.handle_static_invoice_persisted(_context);
13083+
if should_persist {
13084+
let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self);
13085+
}
13086+
}
1308013087
}
1308113088

1308213089
fn handle_held_htlc_available(

lightning/src/offers/async_receive_offer_cache.rs

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@ use crate::io::Read;
1717
use crate::ln::msgs::DecodeError;
1818
use crate::offers::nonce::Nonce;
1919
use crate::offers::offer::Offer;
20-
#[cfg(async_payments)]
21-
use crate::onion_message::async_payments::OfferPaths;
2220
use crate::onion_message::messenger::Responder;
2321
use crate::prelude::*;
2422
use crate::util::ser::{Readable, Writeable, Writer};
2523
use core::time::Duration;
24+
#[cfg(async_payments)]
25+
use {
26+
crate::blinded_path::message::AsyncPaymentsContext,
27+
crate::onion_message::async_payments::OfferPaths,
28+
};
2629

2730
/// The status of this offer in the cache.
2831
enum OfferStatus {
@@ -150,6 +153,13 @@ const MAX_CACHED_OFFERS_TARGET: usize = 10;
150153
#[cfg(async_payments)]
151154
const UNUSED_OFFERS_TARGET: u8 = 3;
152155

156+
// Refuse to store offers if they will exceed the maximum cache size or the maximum number of
157+
// offers.
158+
#[cfg(async_payments)]
159+
const MAX_CACHE_SIZE: usize = (1 << 10) * 70; // 70KiB
160+
#[cfg(async_payments)]
161+
const MAX_OFFERS: usize = 100;
162+
153163
// The max number of times we'll attempt to request offer paths or attempt to refresh a static
154164
// invoice before giving up.
155165
#[cfg(async_payments)]
@@ -248,6 +258,110 @@ impl AsyncReceiveOfferCache {
248258
self.offer_paths_request_attempts = 0;
249259
self.last_offer_paths_request_timestamp = Duration::from_secs(0);
250260
}
261+
262+
/// Should be called when we receive a [`StaticInvoicePersisted`] message from the static invoice
263+
/// server, which indicates that a new offer was persisted by the server and they are ready to
264+
/// serve the corresponding static invoice to payers on our behalf.
265+
///
266+
/// Returns a bool indicating whether an offer was added/updated and re-persistence of the cache
267+
/// is needed.
268+
pub(super) fn static_invoice_persisted(
269+
&mut self, context: AsyncPaymentsContext, duration_since_epoch: Duration,
270+
) -> bool {
271+
let (
272+
candidate_offer,
273+
candidate_offer_nonce,
274+
offer_created_at,
275+
update_static_invoice_path,
276+
static_invoice_absolute_expiry,
277+
) = match context {
278+
AsyncPaymentsContext::StaticInvoicePersisted {
279+
offer,
280+
offer_nonce,
281+
offer_created_at,
282+
update_static_invoice_path,
283+
static_invoice_absolute_expiry,
284+
..
285+
} => (
286+
offer,
287+
offer_nonce,
288+
offer_created_at,
289+
update_static_invoice_path,
290+
static_invoice_absolute_expiry,
291+
),
292+
_ => return false,
293+
};
294+
295+
if candidate_offer.is_expired_no_std(duration_since_epoch) {
296+
return false;
297+
}
298+
if static_invoice_absolute_expiry < duration_since_epoch {
299+
return false;
300+
}
301+
302+
// If the candidate offer is known, either this is a duplicate message or we updated the
303+
// corresponding static invoice that is stored with the server.
304+
if let Some(existing_offer) =
305+
self.offers.iter_mut().find(|cached_offer| cached_offer.offer == candidate_offer)
306+
{
307+
// The blinded path used to update the static invoice corresponding to an offer should never
308+
// change because we reuse the same path every time we update.
309+
debug_assert_eq!(existing_offer.update_static_invoice_path, update_static_invoice_path);
310+
debug_assert_eq!(existing_offer.offer_nonce, candidate_offer_nonce);
311+
312+
let needs_persist =
313+
existing_offer.static_invoice_absolute_expiry != static_invoice_absolute_expiry;
314+
315+
// Since this is the most recent update we've received from the static invoice server, assume
316+
// that the invoice that was just persisted is the only invoice that the server has stored
317+
// corresponding to this offer.
318+
existing_offer.static_invoice_absolute_expiry = static_invoice_absolute_expiry;
319+
existing_offer.invoice_update_attempts = 0;
320+
321+
return needs_persist;
322+
}
323+
324+
let candidate_offer = AsyncReceiveOffer {
325+
offer: candidate_offer,
326+
offer_nonce: candidate_offer_nonce,
327+
offer_created_at,
328+
update_static_invoice_path,
329+
static_invoice_absolute_expiry,
330+
invoice_update_attempts: 0,
331+
};
332+
333+
// If we have room in the cache, go ahead and add this new offer so we have more options. We
334+
// should generally never get close to the cache limit because we limit the number of requests
335+
// for offer persistence that are sent to begin with.
336+
let candidate_cache_size =
337+
self.serialized_length().saturating_add(candidate_offer.serialized_length());
338+
if self.offers.len() < MAX_OFFERS && candidate_cache_size <= MAX_CACHE_SIZE {
339+
self.offers.push(candidate_offer);
340+
return true;
341+
}
342+
343+
// Swap out our lowest expiring offer for this candidate offer if needed. Otherwise we'd be
344+
// risking a situation where all of our existing offers expire soon but we still ignore this one
345+
// even though it's fresh.
346+
const NEVER_EXPIRES: Duration = Duration::from_secs(u64::MAX);
347+
let (soonest_expiring_offer_idx, soonest_offer_expiry) = self
348+
.offers
349+
.iter()
350+
.map(|offer| offer.offer.absolute_expiry().unwrap_or(NEVER_EXPIRES))
351+
.enumerate()
352+
.min_by(|(_, offer_exp_a), (_, offer_exp_b)| offer_exp_a.cmp(offer_exp_b))
353+
.unwrap_or_else(|| {
354+
debug_assert!(false);
355+
(0, NEVER_EXPIRES)
356+
});
357+
358+
if soonest_offer_expiry < candidate_offer.offer.absolute_expiry().unwrap_or(NEVER_EXPIRES) {
359+
self.offers[soonest_expiring_offer_idx] = candidate_offer;
360+
return true;
361+
}
362+
363+
false
364+
}
251365
}
252366

253367
impl Writeable for AsyncReceiveOfferCache {

lightning/src/offers/flow.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,6 +1338,39 @@ where
13381338
Ok((msg, reply_path_context))
13391339
}
13401340

1341+
/// Handles an incoming [`StaticInvoicePersisted`] onion message from the static invoice server.
1342+
/// Returns a bool indicating whether the async receive offer cache needs to be re-persisted.
1343+
///
1344+
/// [`StaticInvoicePersisted`]: crate::onion_message::async_payments::StaticInvoicePersisted
1345+
#[cfg(async_payments)]
1346+
pub(crate) fn handle_static_invoice_persisted(&self, context: AsyncPaymentsContext) -> bool {
1347+
let expanded_key = &self.inbound_payment_key;
1348+
let duration_since_epoch = self.duration_since_epoch();
1349+
1350+
if let AsyncPaymentsContext::StaticInvoicePersisted {
1351+
nonce,
1352+
hmac,
1353+
path_absolute_expiry,
1354+
..
1355+
} = context
1356+
{
1357+
if let Err(()) =
1358+
signer::verify_static_invoice_persisted_context(nonce, hmac, expanded_key)
1359+
{
1360+
return false;
1361+
}
1362+
1363+
if duration_since_epoch > path_absolute_expiry {
1364+
return false;
1365+
}
1366+
} else {
1367+
return false;
1368+
}
1369+
1370+
let mut cache = self.async_receive_offer_cache.lock().unwrap();
1371+
cache.static_invoice_persisted(context, duration_since_epoch)
1372+
}
1373+
13411374
/// Get the `AsyncReceiveOfferCache` for persistence.
13421375
pub(crate) fn writeable_async_receive_offer_cache(&self) -> impl Writeable + '_ {
13431376
&self.async_receive_offer_cache

lightning/src/offers/signer.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,3 +570,14 @@ pub(crate) fn verify_held_htlc_available_context(
570570
Err(())
571571
}
572572
}
573+
574+
#[cfg(async_payments)]
575+
pub(crate) fn verify_static_invoice_persisted_context(
576+
nonce: Nonce, hmac: Hmac<Sha256>, expanded_key: &ExpandedKey,
577+
) -> Result<(), ()> {
578+
if hmac_for_static_invoice_persisted_context(nonce, expanded_key) == hmac {
579+
Ok(())
580+
} else {
581+
Err(())
582+
}
583+
}

0 commit comments

Comments
 (0)