@@ -16,12 +16,15 @@ use crate::io::Read;
1616use crate :: ln:: msgs:: DecodeError ;
1717use crate :: offers:: nonce:: Nonce ;
1818use crate :: offers:: offer:: Offer ;
19- #[ cfg( async_payments) ]
20- use crate :: onion_message:: async_payments:: OfferPaths ;
2119use crate :: onion_message:: messenger:: Responder ;
2220use crate :: prelude:: * ;
2321use crate :: util:: ser:: { Readable , Writeable , Writer } ;
2422use core:: time:: Duration ;
23+ #[ cfg( async_payments) ]
24+ use {
25+ crate :: blinded_path:: message:: AsyncPaymentsContext ,
26+ crate :: onion_message:: async_payments:: OfferPaths ,
27+ } ;
2528
2629struct AsyncReceiveOffer {
2730 offer : Offer ,
@@ -88,6 +91,13 @@ impl AsyncReceiveOfferCache {
8891#[ cfg( async_payments) ]
8992const NUM_CACHED_OFFERS_TARGET : usize = 3 ;
9093
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+
91101// The max number of times we'll attempt to request offer paths or attempt to refresh a static
92102// invoice before giving up.
93103#[ cfg( async_payments) ]
@@ -203,6 +213,110 @@ impl AsyncReceiveOfferCache {
203213 self . offer_paths_request_attempts = 0 ;
204214 self . last_offer_paths_request_timestamp = Duration :: from_secs ( 0 ) ;
205215 }
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+ }
206320}
207321
208322impl Writeable for AsyncReceiveOfferCache {
0 commit comments