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