@@ -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 ,
@@ -85,6 +88,13 @@ impl AsyncReceiveOfferCache {
8588#[ cfg( async_payments) ]
8689const NUM_CACHED_OFFERS_TARGET : usize = 3 ;
8790
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+
8898// The max number of times we'll attempt to request offer paths or attempt to refresh a static
8999// invoice before giving up.
90100#[ cfg( async_payments) ]
@@ -199,6 +209,110 @@ impl AsyncReceiveOfferCache {
199209 self . offer_paths_request_attempts = 0 ;
200210 self . last_offer_paths_request_timestamp = Duration :: from_secs ( 0 ) ;
201211 }
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+ }
202316}
203317
204318impl Writeable for AsyncReceiveOfferCache {
0 commit comments