@@ -33,6 +33,12 @@ impl_writeable_tlv_based!(PersistedStaticInvoice, {
3333 ( 2 , request_path, required)
3434} ) ;
3535
36+ #[ derive( Debug ) ]
37+ pub ( crate ) enum StaticInvoiceStoreError {
38+ RateLimited ,
39+ Io ( lightning:: io:: Error ) ,
40+ }
41+
3642pub ( crate ) struct StaticInvoiceStore {
3743 kv_store : Arc < DynStore > ,
3844 request_rate_limiter : Mutex < RateLimiter > ,
@@ -62,18 +68,18 @@ impl StaticInvoiceStore {
6268
6369 fn check_rate_limit (
6470 limiter : & Mutex < RateLimiter > , recipient_id : & [ u8 ] ,
65- ) -> Result < ( ) , lightning :: io :: Error > {
71+ ) -> Result < ( ) , StaticInvoiceStoreError > {
6672 let mut limiter = limiter. lock ( ) . expect ( "lock" ) ;
6773 if !limiter. allow ( recipient_id) {
68- Err ( lightning :: io :: Error :: new ( lightning :: io :: ErrorKind :: Other , "Rate limit exceeded" ) )
74+ Err ( StaticInvoiceStoreError :: RateLimited )
6975 } else {
7076 Ok ( ( ) )
7177 }
7278 }
7379
7480 pub ( crate ) async fn handle_static_invoice_requested (
7581 & self , recipient_id : & [ u8 ] , invoice_slot : u16 ,
76- ) -> Result < Option < ( StaticInvoice , BlindedMessagePath ) > , lightning :: io :: Error > {
82+ ) -> Result < Option < ( StaticInvoice , BlindedMessagePath ) > , StaticInvoiceStoreError > {
7783 Self :: check_rate_limit ( & self . request_rate_limiter , & recipient_id) ?;
7884
7985 let ( secondary_namespace, key) = Self :: get_storage_location ( invoice_slot, recipient_id) ;
@@ -97,21 +103,14 @@ impl StaticInvoiceStore {
97103 )
98104 } )
99105 } )
100- . or_else (
101- |e| {
102- if e. kind ( ) == lightning:: io:: ErrorKind :: NotFound {
103- Ok ( None )
104- } else {
105- Err ( e)
106- }
107- } ,
108- )
106+ . or_else ( |e| if e. kind ( ) == lightning:: io:: ErrorKind :: NotFound { Ok ( None ) } else { Err ( e) } )
107+ . map_err ( StaticInvoiceStoreError :: Io )
109108 }
110109
111110 pub ( crate ) async fn handle_persist_static_invoice (
112111 & self , invoice : StaticInvoice , invoice_request_path : BlindedMessagePath , invoice_slot : u16 ,
113112 recipient_id : Vec < u8 > ,
114- ) -> Result < ( ) , lightning :: io :: Error > {
113+ ) -> Result < ( ) , StaticInvoiceStoreError > {
115114 Self :: check_rate_limit ( & self . persist_rate_limiter , & recipient_id) ?;
116115
117116 let ( secondary_namespace, key) = Self :: get_storage_location ( invoice_slot, & recipient_id) ;
@@ -120,7 +119,7 @@ impl StaticInvoiceStore {
120119 PersistedStaticInvoice { invoice, request_path : invoice_request_path } ;
121120
122121 let mut buf = Vec :: new ( ) ;
123- persisted_invoice. write ( & mut buf) ?;
122+ persisted_invoice. write ( & mut buf) . map_err ( StaticInvoiceStoreError :: Io ) ?;
124123
125124 // Static invoices will be persisted at "static_invoices/<sha256(recipient_id)>/<invoice_slot>".
126125 //
@@ -133,6 +132,7 @@ impl StaticInvoiceStore {
133132 buf,
134133 )
135134 . await
135+ . map_err ( StaticInvoiceStoreError :: Io )
136136 }
137137
138138 fn get_storage_location ( invoice_slot : u16 , recipient_id : & [ u8 ] ) -> ( String , String ) {
@@ -146,6 +146,7 @@ impl StaticInvoiceStore {
146146
147147#[ cfg( test) ]
148148mod tests {
149+ use std:: sync:: atomic:: { AtomicBool , Ordering } ;
149150 use std:: sync:: Arc ;
150151 use std:: time:: Duration ;
151152
@@ -154,16 +155,214 @@ mod tests {
154155 use lightning:: blinded_path:: message:: BlindedMessagePath ;
155156 use lightning:: blinded_path:: payment:: { BlindedPayInfo , BlindedPaymentPath } ;
156157 use lightning:: blinded_path:: BlindedHop ;
158+ use lightning:: events:: Event ;
159+ use lightning:: ln:: channelmanager:: PaymentId ;
157160 use lightning:: ln:: inbound_payment:: ExpandedKey ;
158161 use lightning:: offers:: nonce:: Nonce ;
159162 use lightning:: offers:: offer:: OfferBuilder ;
160163 use lightning:: offers:: static_invoice:: { StaticInvoice , StaticInvoiceBuilder } ;
164+ use lightning:: onion_message:: async_payments:: {
165+ AsyncPaymentsMessage , AsyncPaymentsMessageHandler ,
166+ } ;
167+ use lightning:: onion_message:: messenger:: Responder ;
168+ use lightning:: onion_message:: offers:: OffersMessageHandler ;
161169 use lightning:: sign:: EntropySource ;
170+ use lightning:: util:: persist:: KVStore ;
171+ use lightning:: util:: ser:: { Readable , Writeable } ;
172+ use lightning:: util:: wallet_utils:: Wallet as LdkWallet ;
162173 use lightning_types:: features:: BlindedHopFeatures ;
163174
175+ use crate :: builder:: NodeBuilder ;
176+ use crate :: entropy:: NodeEntropy ;
177+ use crate :: event:: EventHandler ;
164178 use crate :: io:: test_utils:: InMemoryStore ;
179+ use crate :: io:: STATIC_INVOICE_STORE_PRIMARY_NAMESPACE ;
180+ use crate :: logger:: { LogRecord , LogWriter , Logger } ;
181+ use crate :: payment:: asynchronous:: rate_limiter:: RateLimiter ;
165182 use crate :: payment:: asynchronous:: static_invoice_store:: StaticInvoiceStore ;
166183 use crate :: types:: { DynStore , DynStoreWrapper } ;
184+ use crate :: { BumpTransactionEventHandler , Node } ;
185+
186+ #[ derive( Default ) ]
187+ struct TestLogWriter {
188+ logged : AtomicBool ,
189+ }
190+
191+ impl LogWriter for TestLogWriter {
192+ fn log ( & self , _record : LogRecord ) {
193+ self . logged . store ( true , Ordering :: Relaxed ) ;
194+ }
195+ }
196+
197+ fn event_handler (
198+ store : StaticInvoiceStore ,
199+ ) -> ( Node , EventHandler < Arc < Logger > > , Arc < TestLogWriter > ) {
200+ let mut builder = NodeBuilder :: new ( ) ;
201+ builder. set_log_facade_logger ( ) ;
202+ #[ cfg( not( feature = "uniffi" ) ) ]
203+ let entropy = NodeEntropy :: from_seed_bytes ( [ 42 ; 64 ] ) ;
204+ #[ cfg( feature = "uniffi" ) ]
205+ let entropy = NodeEntropy :: from_seed_bytes ( vec ! [ 42 ; 64 ] ) . unwrap ( ) ;
206+ let node = builder. build_with_store ( entropy, InMemoryStore :: new ( ) ) . unwrap ( ) ;
207+ let bump_handler = Arc :: new ( BumpTransactionEventHandler :: new (
208+ Arc :: clone ( & node. tx_broadcaster ) ,
209+ Arc :: new ( LdkWallet :: new ( Arc :: clone ( & node. wallet ) , Arc :: clone ( & node. logger ) ) ) ,
210+ Arc :: clone ( & node. keys_manager ) ,
211+ Arc :: clone ( & node. logger ) ,
212+ ) ) ;
213+ let log_writer = Arc :: new ( TestLogWriter :: default ( ) ) ;
214+ let handler = EventHandler :: new (
215+ Arc :: clone ( & node. event_queue ) ,
216+ Arc :: clone ( & node. wallet ) ,
217+ bump_handler,
218+ Arc :: clone ( & node. channel_manager ) ,
219+ Arc :: clone ( & node. connection_manager ) ,
220+ Arc :: clone ( & node. output_sweeper ) ,
221+ Arc :: clone ( & node. network_graph ) ,
222+ Arc :: clone ( & node. liquidity_source ) ,
223+ Arc :: clone ( & node. payment_store ) ,
224+ Arc :: clone ( & node. forwarding_store ) ,
225+ Arc :: clone ( & node. peer_store ) ,
226+ Arc :: clone ( & node. keys_manager ) ,
227+ Some ( store) ,
228+ Arc :: clone ( & node. onion_messenger ) ,
229+ None ,
230+ None ,
231+ Arc :: clone ( & node. runtime ) ,
232+ Arc :: new ( Logger :: new_custom_writer ( log_writer. clone ( ) ) ) ,
233+ Arc :: clone ( & node. config ) ,
234+ ) ;
235+ ( node, handler, log_writer)
236+ }
237+
238+ fn responder ( ) -> Responder {
239+ // Responder has no public constructor, so use its serialized representation.
240+ struct ReplyPath {
241+ path : BlindedMessagePath ,
242+ }
243+ lightning:: impl_writeable_tlv_based!( ReplyPath , { ( 0 , path, required) } ) ;
244+ let bytes = ReplyPath { path : blinded_path ( ) } . encode ( ) ;
245+ Responder :: read ( & mut & bytes[ ..] ) . unwrap ( )
246+ }
247+
248+ fn static_invoice_event ( persist : bool ) -> Event {
249+ if persist {
250+ Event :: PersistStaticInvoice {
251+ invoice : invoice ( ) ,
252+ invoice_request_path : blinded_path ( ) ,
253+ invoice_slot : 1 ,
254+ recipient_id : vec ! [ 1 , 1 , 1 ] ,
255+ invoice_persisted_path : responder ( ) ,
256+ }
257+ } else {
258+ let invoice_request = OfferBuilder :: new ( recipient_pubkey ( ) )
259+ . amount_msats ( 1_000 )
260+ . build ( )
261+ . unwrap ( )
262+ . request_invoice (
263+ & ExpandedKey :: new ( [ 42 ; 32 ] ) ,
264+ Nonce :: from_entropy_source ( & FixedEntropy { } ) ,
265+ & Secp256k1 :: new ( ) ,
266+ PaymentId ( [ 42 ; 32 ] ) ,
267+ )
268+ . unwrap ( )
269+ . build_and_sign ( )
270+ . unwrap ( ) ;
271+ Event :: StaticInvoiceRequested {
272+ recipient_id : vec ! [ 1 , 1 , 1 ] ,
273+ invoice_slot : 0 ,
274+ reply_path : responder ( ) ,
275+ invoice_request,
276+ }
277+ }
278+ }
279+
280+ async fn check_rate_limited_static_invoice_event ( persist : bool ) {
281+ let kv_store: Arc < DynStore > = Arc :: new ( DynStoreWrapper ( InMemoryStore :: new ( ) ) ) ;
282+ let mut store = StaticInvoiceStore :: new ( Arc :: clone ( & kv_store) ) ;
283+ store
284+ . handle_persist_static_invoice ( invoice ( ) , blinded_path ( ) , 0 , vec ! [ 1 , 1 , 1 ] )
285+ . await
286+ . unwrap ( ) ;
287+ // Reject requests deterministically, without depending on refill timing.
288+ * store. request_rate_limiter . get_mut ( ) . unwrap ( ) =
289+ RateLimiter :: new ( 0 , Duration :: from_secs ( 1 ) , Duration :: from_secs ( 600 ) ) ;
290+ * store. persist_rate_limiter . get_mut ( ) . unwrap ( ) =
291+ RateLimiter :: new ( 0 , Duration :: from_secs ( 1 ) , Duration :: from_secs ( 600 ) ) ;
292+ let ( node, handler, log_writer) = event_handler ( store) ;
293+ node. channel_manager . push_pending_event ( static_invoice_event ( persist) ) ;
294+ node. channel_manager . push_pending_event ( Event :: PaymentFailed {
295+ payment_id : PaymentId ( [ 42 ; 32 ] ) ,
296+ payment_hash : None ,
297+ reason : None ,
298+ } ) ;
299+ let handled_next_event = AtomicBool :: new ( false ) ;
300+ node. channel_manager
301+ . process_pending_events_async ( |event| async {
302+ if matches ! ( event, Event :: PaymentFailed { .. } ) {
303+ handled_next_event. store ( true , Ordering :: Relaxed ) ;
304+ Ok ( ( ) )
305+ } else {
306+ handler. handle_event ( event) . await
307+ }
308+ } )
309+ . await ;
310+ assert ! (
311+ handled_next_event. load( Ordering :: Relaxed ) ,
312+ "a rate-limited static invoice event must not delay the next event"
313+ ) ;
314+ assert ! (
315+ !log_writer. logged. load( Ordering :: Relaxed ) ,
316+ "a rate-limited static invoice event must not produce a log message"
317+ ) ;
318+ assert ! ( AsyncPaymentsMessageHandler :: release_pending_messages( & * node. channel_manager)
319+ . is_empty( ) ) ;
320+ assert ! ( OffersMessageHandler :: release_pending_messages( & * node. channel_manager) . is_empty( ) ) ;
321+ let store = StaticInvoiceStore :: new ( kv_store) ;
322+ assert ! ( store. handle_static_invoice_requested( & [ 1 , 1 , 1 ] , 0 ) . await . unwrap( ) . is_some( ) ) ;
323+ assert ! ( store. handle_static_invoice_requested( & [ 1 , 1 , 1 ] , 1 ) . await . unwrap( ) . is_none( ) ) ;
324+ }
325+
326+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
327+ async fn rate_limited_static_invoice_persistence_does_not_replay ( ) {
328+ check_rate_limited_static_invoice_event ( true ) . await ;
329+ }
330+
331+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
332+ async fn rate_limited_static_invoice_request_does_not_replay ( ) {
333+ check_rate_limited_static_invoice_event ( false ) . await ;
334+ }
335+
336+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
337+ async fn static_invoice_persistence_is_acknowledged ( ) {
338+ let kv_store: Arc < DynStore > = Arc :: new ( DynStoreWrapper ( InMemoryStore :: new ( ) ) ) ;
339+ let ( node, handler, _) = event_handler ( StaticInvoiceStore :: new ( Arc :: clone ( & kv_store) ) ) ;
340+ handler. handle_event ( static_invoice_event ( true ) ) . await . unwrap ( ) ;
341+ let messages =
342+ AsyncPaymentsMessageHandler :: release_pending_messages ( & * node. channel_manager ) ;
343+ assert_eq ! ( messages. len( ) , 1 ) ;
344+ assert ! ( matches!( messages[ 0 ] . 0 , AsyncPaymentsMessage :: StaticInvoicePersisted ( _) ) ) ;
345+ let store = StaticInvoiceStore :: new ( kv_store) ;
346+ assert ! ( store. handle_static_invoice_requested( & [ 1 , 1 , 1 ] , 1 ) . await . unwrap( ) . is_some( ) ) ;
347+ }
348+
349+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
350+ async fn static_invoice_read_error_is_replayed ( ) {
351+ let kv_store: Arc < DynStore > = Arc :: new ( DynStoreWrapper ( InMemoryStore :: new ( ) ) ) ;
352+ let ( namespace, key) = StaticInvoiceStore :: get_storage_location ( 0 , & [ 1 , 1 , 1 ] ) ;
353+ KVStore :: write (
354+ & * kv_store,
355+ STATIC_INVOICE_STORE_PRIMARY_NAMESPACE ,
356+ & namespace,
357+ & key,
358+ vec ! [ 0xff ] ,
359+ )
360+ . await
361+ . unwrap ( ) ;
362+ let ( _node, handler, log_writer) = event_handler ( StaticInvoiceStore :: new ( kv_store) ) ;
363+ assert ! ( handler. handle_event( static_invoice_event( false ) ) . await . is_err( ) ) ;
364+ assert ! ( log_writer. logged. load( Ordering :: Relaxed ) ) ;
365+ }
167366
168367 #[ tokio:: test]
169368 async fn static_invoice_store_test ( ) {
0 commit comments