@@ -17,12 +17,15 @@ use crate::io::Read;
17
17
use crate :: ln:: msgs:: DecodeError ;
18
18
use crate :: offers:: nonce:: Nonce ;
19
19
use crate :: offers:: offer:: Offer ;
20
- #[ cfg( async_payments) ]
21
- use crate :: onion_message:: async_payments:: OfferPaths ;
22
20
use crate :: onion_message:: messenger:: Responder ;
23
21
use crate :: prelude:: * ;
24
22
use crate :: util:: ser:: { Readable , Writeable , Writer } ;
25
23
use core:: time:: Duration ;
24
+ #[ cfg( async_payments) ]
25
+ use {
26
+ crate :: blinded_path:: message:: AsyncPaymentsContext ,
27
+ crate :: onion_message:: async_payments:: OfferPaths ,
28
+ } ;
26
29
27
30
/// The status of this offer in the cache.
28
31
enum OfferStatus {
@@ -150,6 +153,13 @@ const MAX_CACHED_OFFERS_TARGET: usize = 10;
150
153
#[ cfg( async_payments) ]
151
154
const UNUSED_OFFERS_TARGET : u8 = 3 ;
152
155
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
+
153
163
// The max number of times we'll attempt to request offer paths or attempt to refresh a static
154
164
// invoice before giving up.
155
165
#[ cfg( async_payments) ]
@@ -248,6 +258,110 @@ impl AsyncReceiveOfferCache {
248
258
self . offer_paths_request_attempts = 0 ;
249
259
self . last_offer_paths_request_timestamp = Duration :: from_secs ( 0 ) ;
250
260
}
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
+ }
251
365
}
252
366
253
367
impl Writeable for AsyncReceiveOfferCache {
0 commit comments