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