@@ -10,7 +10,7 @@ mod common;
1010use std:: collections:: HashSet ;
1111use std:: future:: Future ;
1212use std:: str:: FromStr ;
13- use std:: sync:: atomic:: { AtomicBool , Ordering } ;
13+ use std:: sync:: atomic:: { AtomicBool , AtomicUsize , Ordering } ;
1414use std:: sync:: { mpsc, Arc } ;
1515use std:: time:: Duration ;
1616
@@ -24,10 +24,10 @@ use common::{
2424 expect_channel_pending_event, expect_channel_ready_event, expect_channel_ready_events,
2525 expect_event, expect_payment_claimable_event, expect_payment_received_event,
2626 expect_payment_successful_event, expect_splice_negotiated_event, generate_blocks_and_wait,
27- generate_listening_addresses, invalidate_blocks, open_channel, open_channel_push_amt ,
28- open_channel_with_all, premine_and_distribute_funds, premine_blocks, prepare_rbf ,
29- random_chain_source, random_config, setup_bitcoind_and_electrsd, setup_builder, setup_node ,
30- setup_two_nodes, splice_in_with_all, wait_for_block, wait_for_tx, InMemoryStore ,
27+ generate_listening_addresses, invalidate_blocks, open_channel, open_channel_no_wait ,
28+ open_channel_push_amt , open_channel_with_all, premine_and_distribute_funds, premine_blocks,
29+ prepare_rbf , random_chain_source, random_config, setup_bitcoind_and_electrsd, setup_builder,
30+ setup_node , setup_two_nodes, splice_in_with_all, wait_for_block, wait_for_tx, InMemoryStore ,
3131 TestChainSource , TestConfig , TestStoreType , TestSyncStore ,
3232} ;
3333use electrsd:: corepc_node:: { self , Node as BitcoinD } ;
@@ -216,6 +216,164 @@ fn wallet_store_contention_does_not_stall_runtime() {
216216 result. unwrap_or_else ( |e| panic ! ( "wallet contention test failed: {e}" ) ) ;
217217}
218218
219+ #[ derive( Clone ) ]
220+ struct WalletPersistGatedStore {
221+ inner : Arc < InMemoryStore > ,
222+ wallet_write_gate : Arc < tokio:: sync:: RwLock < ( ) > > ,
223+ gate_engaged : Arc < AtomicBool > ,
224+ wallet_writes_completed : Arc < AtomicUsize > ,
225+ }
226+
227+ impl WalletPersistGatedStore {
228+ fn new ( ) -> Self {
229+ Self {
230+ inner : Arc :: new ( InMemoryStore :: new ( ) ) ,
231+ wallet_write_gate : Arc :: new ( tokio:: sync:: RwLock :: new ( ( ) ) ) ,
232+ gate_engaged : Arc :: new ( AtomicBool :: new ( false ) ) ,
233+ wallet_writes_completed : Arc :: new ( AtomicUsize :: new ( 0 ) ) ,
234+ }
235+ }
236+ }
237+
238+ impl KVStore for WalletPersistGatedStore {
239+ fn read (
240+ & self , primary_namespace : & str , secondary_namespace : & str , key : & str ,
241+ ) -> impl Future < Output = Result < Vec < u8 > , lightning:: io:: Error > > + ' static + Send {
242+ KVStore :: read ( & * self . inner , primary_namespace, secondary_namespace, key)
243+ }
244+
245+ fn write (
246+ & self , primary_namespace : & str , secondary_namespace : & str , key : & str , buf : Vec < u8 > ,
247+ ) -> impl Future < Output = Result < ( ) , lightning:: io:: Error > > + ' static + Send {
248+ let inner = Arc :: clone ( & self . inner ) ;
249+ let wallet_write_gate = Arc :: clone ( & self . wallet_write_gate ) ;
250+ let gate_engaged = Arc :: clone ( & self . gate_engaged ) ;
251+ let wallet_writes_completed = Arc :: clone ( & self . wallet_writes_completed ) ;
252+ let primary_namespace = primary_namespace. to_string ( ) ;
253+ let secondary_namespace = secondary_namespace. to_string ( ) ;
254+ let key = key. to_string ( ) ;
255+ async move {
256+ let is_wallet_write = primary_namespace == "bdk_wallet" ;
257+ if is_wallet_write && gate_engaged. load ( Ordering :: Acquire ) {
258+ let _guard = wallet_write_gate. read ( ) . await ;
259+ }
260+ let res =
261+ KVStore :: write ( & * inner, & primary_namespace, & secondary_namespace, & key, buf) . await ;
262+ if is_wallet_write && res. is_ok ( ) {
263+ wallet_writes_completed. fetch_add ( 1 , Ordering :: AcqRel ) ;
264+ }
265+ res
266+ }
267+ }
268+
269+ fn remove (
270+ & self , primary_namespace : & str , secondary_namespace : & str , key : & str , lazy : bool ,
271+ ) -> impl Future < Output = Result < ( ) , lightning:: io:: Error > > + ' static + Send {
272+ KVStore :: remove ( & * self . inner , primary_namespace, secondary_namespace, key, lazy)
273+ }
274+
275+ fn list (
276+ & self , primary_namespace : & str , secondary_namespace : & str ,
277+ ) -> impl Future < Output = Result < Vec < String > , lightning:: io:: Error > > + ' static + Send {
278+ KVStore :: list ( & * self . inner , primary_namespace, secondary_namespace)
279+ }
280+ }
281+
282+ impl PaginatedKVStore for WalletPersistGatedStore {
283+ fn list_paginated (
284+ & self , primary_namespace : & str , secondary_namespace : & str , page_token : Option < PageToken > ,
285+ ) -> impl Future < Output = Result < PaginatedListResponse , lightning:: io:: Error > > + ' static + Send
286+ {
287+ PaginatedKVStore :: list_paginated (
288+ & * self . inner ,
289+ primary_namespace,
290+ secondary_namespace,
291+ page_token,
292+ )
293+ }
294+ }
295+
296+ // LDK invokes the sync `SignerProvider::get_shutdown_scriptpubkey` callback on a runtime worker
297+ // thread while holding channel locks when a node accepts (or opens) a channel. If deriving the
298+ // shutdown script waits on wallet persistence, a contended wallet store wedges the event handler
299+ // while it holds those locks, and other runtime tasks blocking on the same locks can capture the
300+ // remaining workers, deadlocking the runtime. Gate node B's BDK wallet writes and assert the
301+ // channel open still completes, with the revealed address persisted once the store recovers.
302+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
303+ async fn channel_open_completes_while_wallet_persistence_is_stalled ( ) {
304+ let ( bitcoind, electrsd) = setup_bitcoind_and_electrsd ( ) ;
305+ let esplora_url = format ! ( "http://{}" , electrsd. esplora_url. as_ref( ) . unwrap( ) ) ;
306+ let chain_source = TestChainSource :: Esplora ( & electrsd) ;
307+
308+ let config_a = random_config ( ) ;
309+ let node_a = setup_node ( & chain_source, config_a) ;
310+
311+ let config_b = random_config ( ) ;
312+ setup_builder ! ( builder_b, config_b. node_config) ;
313+ let mut sync_config = EsploraSyncConfig :: default ( ) ;
314+ sync_config. background_sync_config = None ;
315+ builder_b. set_chain_source_esplora ( esplora_url, Some ( sync_config) ) ;
316+ let store = WalletPersistGatedStore :: new ( ) ;
317+ let node_b = builder_b. build_with_store ( config_b. node_entropy . into ( ) , store. clone ( ) ) . unwrap ( ) ;
318+ node_b. start ( ) . unwrap ( ) ;
319+
320+ // Fund both nodes so node B passes the anchor reserve check on the accept path.
321+ let address_a = node_a. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
322+ let address_b = node_b. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
323+ premine_and_distribute_funds (
324+ & bitcoind. client ,
325+ & electrsd. client ,
326+ vec ! [ address_a, address_b] ,
327+ Amount :: from_sat ( 5_000_000 ) ,
328+ )
329+ . await ;
330+ node_a. sync_wallets ( ) . unwrap ( ) ;
331+ node_b. sync_wallets ( ) . unwrap ( ) ;
332+
333+ // Stall writes of node B's BDK wallet data before the channel open reaches the accept path.
334+ // The gate guard lives on a plain thread with a deadline: if the gated write wedges the
335+ // runtime (the bug under test captures all workers, so even timers stop firing), the gate
336+ // force-reopens after the event timeouts below have expired, letting them fail the test
337+ // cleanly instead of hanging it.
338+ let ( release_gate_sender, release_gate_receiver) = mpsc:: sync_channel :: < ( ) > ( 1 ) ;
339+ let ( gate_held_sender, gate_held_receiver) = mpsc:: sync_channel :: < ( ) > ( 1 ) ;
340+ let gate = Arc :: clone ( & store. wallet_write_gate ) ;
341+ std:: thread:: spawn ( move || {
342+ let _guard = gate. blocking_write ( ) ;
343+ let _ = gate_held_sender. send ( ( ) ) ;
344+ let _ = release_gate_receiver. recv_timeout ( Duration :: from_secs ( 90 ) ) ;
345+ } ) ;
346+ gate_held_receiver. recv ( ) . unwrap ( ) ;
347+ store. gate_engaged . store ( true , Ordering :: Release ) ;
348+ let wallet_writes_before = store. wallet_writes_completed . load ( Ordering :: Acquire ) ;
349+
350+ // Accepting the channel must not wait on wallet persistence: `open_channel_no_wait` times out
351+ // waiting for the `ChannelPending` events otherwise.
352+ let funding_txo = open_channel_no_wait ( & node_a, & node_b, 500_000 , None , false ) . await ;
353+
354+ // Reopen the gate and verify the deferred persist of the revealed shutdown-script address
355+ // eventually lands.
356+ store. gate_engaged . store ( false , Ordering :: Release ) ;
357+ // The watchdog thread may have force-released the gate already on a slow run.
358+ let _ = release_gate_sender. send ( ( ) ) ;
359+ let persisted = async {
360+ while store. wallet_writes_completed . load ( Ordering :: Acquire ) <= wallet_writes_before {
361+ tokio:: time:: sleep ( Duration :: from_millis ( 50 ) ) . await ;
362+ }
363+ } ;
364+ tokio:: time:: timeout ( Duration :: from_secs ( common:: INTEROP_TIMEOUT_SECS ) , persisted)
365+ . await
366+ . expect ( "timed out waiting for the deferred wallet persist" ) ;
367+
368+ // The channel and both nodes remain fully functional.
369+ wait_for_tx ( & electrsd. client , funding_txo. txid ) . await ;
370+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
371+ node_a. sync_wallets ( ) . unwrap ( ) ;
372+ node_b. sync_wallets ( ) . unwrap ( ) ;
373+ expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
374+ expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
375+ }
376+
219377#[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
220378async fn channel_full_cycle ( ) {
221379 let ( bitcoind, electrsd) = setup_bitcoind_and_electrsd ( ) ;
0 commit comments