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