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