@@ -16,12 +16,15 @@ use crate::io::Read;
16
16
use crate :: ln:: msgs:: DecodeError ;
17
17
use crate :: offers:: nonce:: Nonce ;
18
18
use crate :: offers:: offer:: Offer ;
19
- #[ cfg( async_payments) ]
20
- use crate :: onion_message:: async_payments:: OfferPaths ;
21
19
use crate :: onion_message:: messenger:: Responder ;
22
20
use crate :: prelude:: * ;
23
21
use crate :: util:: ser:: { Readable , Writeable , Writer } ;
24
22
use core:: time:: Duration ;
23
+ #[ cfg( async_payments) ]
24
+ use {
25
+ crate :: blinded_path:: message:: AsyncPaymentsContext ,
26
+ crate :: onion_message:: async_payments:: OfferPaths ,
27
+ } ;
25
28
26
29
struct AsyncReceiveOffer {
27
30
offer : Offer ,
@@ -88,6 +91,13 @@ impl AsyncReceiveOfferCache {
88
91
#[ cfg( async_payments) ]
89
92
const NUM_CACHED_OFFERS_TARGET : usize = 3 ;
90
93
94
+ // Refuse to store offers if they will exceed the maximum cache size or the maximum number of
95
+ // offers.
96
+ #[ cfg( async_payments) ]
97
+ const MAX_CACHE_SIZE : usize = ( 1 << 10 ) * 70 ; // 70KiB
98
+ #[ cfg( async_payments) ]
99
+ const MAX_OFFERS : usize = 100 ;
100
+
91
101
// The max number of times we'll attempt to request offer paths or attempt to refresh a static
92
102
// invoice before giving up.
93
103
#[ cfg( async_payments) ]
@@ -203,6 +213,110 @@ impl AsyncReceiveOfferCache {
203
213
self . offer_paths_request_attempts = 0 ;
204
214
self . last_offer_paths_request_timestamp = Duration :: from_secs ( 0 ) ;
205
215
}
216
+
217
+ /// Should be called when we receive a [`StaticInvoicePersisted`] message from the static invoice
218
+ /// server, which indicates that a new offer was persisted by the server and they are ready to
219
+ /// serve the corresponding static invoice to payers on our behalf.
220
+ ///
221
+ /// Returns a bool indicating whether an offer was added/updated and re-persistence of the cache
222
+ /// is needed.
223
+ pub ( super ) fn static_invoice_persisted (
224
+ & mut self , context : AsyncPaymentsContext , duration_since_epoch : Duration ,
225
+ ) -> bool {
226
+ let (
227
+ candidate_offer,
228
+ candidate_offer_nonce,
229
+ offer_created_at,
230
+ update_static_invoice_path,
231
+ static_invoice_absolute_expiry,
232
+ ) = match context {
233
+ AsyncPaymentsContext :: StaticInvoicePersisted {
234
+ offer,
235
+ offer_nonce,
236
+ offer_created_at,
237
+ update_static_invoice_path,
238
+ static_invoice_absolute_expiry,
239
+ ..
240
+ } => (
241
+ offer,
242
+ offer_nonce,
243
+ offer_created_at,
244
+ update_static_invoice_path,
245
+ static_invoice_absolute_expiry,
246
+ ) ,
247
+ _ => return false ,
248
+ } ;
249
+
250
+ if candidate_offer. is_expired_no_std ( duration_since_epoch) {
251
+ return false ;
252
+ }
253
+ if static_invoice_absolute_expiry < duration_since_epoch {
254
+ return false ;
255
+ }
256
+
257
+ // If the candidate offer is known, either this is a duplicate message or we updated the
258
+ // corresponding static invoice that is stored with the server.
259
+ if let Some ( existing_offer) =
260
+ self . offers . iter_mut ( ) . find ( |cached_offer| cached_offer. offer == candidate_offer)
261
+ {
262
+ // The blinded path used to update the static invoice corresponding to an offer should never
263
+ // change because we reuse the same path every time we update.
264
+ debug_assert_eq ! ( existing_offer. update_static_invoice_path, update_static_invoice_path) ;
265
+ debug_assert_eq ! ( existing_offer. offer_nonce, candidate_offer_nonce) ;
266
+
267
+ let needs_persist =
268
+ existing_offer. static_invoice_absolute_expiry != static_invoice_absolute_expiry;
269
+
270
+ // Since this is the most recent update we've received from the static invoice server, assume
271
+ // that the invoice that was just persisted is the only invoice that the server has stored
272
+ // corresponding to this offer.
273
+ existing_offer. static_invoice_absolute_expiry = static_invoice_absolute_expiry;
274
+ existing_offer. invoice_update_attempts = 0 ;
275
+
276
+ return needs_persist;
277
+ }
278
+
279
+ let candidate_offer = AsyncReceiveOffer {
280
+ offer : candidate_offer,
281
+ offer_nonce : candidate_offer_nonce,
282
+ offer_created_at,
283
+ update_static_invoice_path,
284
+ static_invoice_absolute_expiry,
285
+ invoice_update_attempts : 0 ,
286
+ } ;
287
+
288
+ // If we have room in the cache, go ahead and add this new offer so we have more options. We
289
+ // should generally never get close to the cache limit because we limit the number of requests
290
+ // for offer persistence that are sent to begin with.
291
+ let candidate_cache_size =
292
+ self . serialized_length ( ) . saturating_add ( candidate_offer. serialized_length ( ) ) ;
293
+ if self . offers . len ( ) < MAX_OFFERS && candidate_cache_size <= MAX_CACHE_SIZE {
294
+ self . offers . push ( candidate_offer) ;
295
+ return true ;
296
+ }
297
+
298
+ // Swap out our lowest expiring offer for this candidate offer if needed. Otherwise we'd be
299
+ // risking a situation where all of our existing offers expire soon but we still ignore this one
300
+ // even though it's fresh.
301
+ const NEVER_EXPIRES : Duration = Duration :: from_secs ( u64:: MAX ) ;
302
+ let ( soonest_expiring_offer_idx, soonest_offer_expiry) = self
303
+ . offers
304
+ . iter ( )
305
+ . map ( |offer| offer. offer . absolute_expiry ( ) . unwrap_or ( NEVER_EXPIRES ) )
306
+ . enumerate ( )
307
+ . min_by ( |( _, offer_exp_a) , ( _, offer_exp_b) | offer_exp_a. cmp ( offer_exp_b) )
308
+ . unwrap_or_else ( || {
309
+ debug_assert ! ( false ) ;
310
+ ( 0 , NEVER_EXPIRES )
311
+ } ) ;
312
+
313
+ if soonest_offer_expiry < candidate_offer. offer . absolute_expiry ( ) . unwrap_or ( NEVER_EXPIRES ) {
314
+ self . offers [ soonest_expiring_offer_idx] = candidate_offer;
315
+ return true ;
316
+ }
317
+
318
+ false
319
+ }
206
320
}
207
321
208
322
impl Writeable for AsyncReceiveOfferCache {
0 commit comments