@@ -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 ,
@@ -186,6 +189,112 @@ impl AsyncReceiveOfferCache {
186189 self . offer_paths_request_attempts = 0 ;
187190 self . last_offer_paths_request_timestamp = Duration :: from_secs ( 0 ) ;
188191 }
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+ }
189298}
190299
191300impl Writeable for AsyncReceiveOfferCache {
0 commit comments