99except ImportError :
1010 pass
1111
12+ import concurrent .futures
13+
1214from test_framework .blocktools import COINBASE_MATURITY
15+ from test_framework .descriptors import descsum_create
1316from test_framework .test_framework import BitcoinTestFramework
1417from test_framework .util import (
1518 assert_equal ,
@@ -33,6 +36,41 @@ def skip_test_if_missing_module(self):
3336 self .skip_if_no_sqlite ()
3437 self .skip_if_no_py_sqlite3 ()
3538
39+ def test_concurrent_writes (self ):
40+ self .log .info ("Test sqlite concurrent writes are in the correct order" )
41+ self .restart_node (0 , extra_args = ["-unsafesqlitesync=0" ])
42+ self .nodes [0 ].createwallet (wallet_name = "concurrency" , blank = True )
43+ wallet = self .nodes [0 ].get_wallet_rpc ("concurrency" )
44+ # First import a descriptor that uses hardened dervation so that topping up
45+ # Will require writing a ton to db
46+ wallet .importdescriptors ([{"desc" :descsum_create ("wpkh(tprv8ZgxMBicQKsPeuVhWwi6wuMQGfPKi9Li5GtX35jVNknACgqe3CY4g5xgkfDDJcmtF7o1QnxWDRYw4H5P26PXq7sbcUkEqeR4fg3Kxp2tigg/0h/0h/*h)" ), "timestamp" : "now" , "active" : True }])
47+ with concurrent .futures .ThreadPoolExecutor (max_workers = 1 ) as thread :
48+ topup = thread .submit (wallet .keypoolrefill , newsize = 1000 )
49+
50+ # Then while the topup is running, we need to do something that will call
51+ # ChainStateFlushed which will trigger a write to the db, hopefully at the
52+ # same time that the topup still has an open db transaction.
53+ self .nodes [0 ].cli .gettxoutsetinfo ()
54+ assert_equal (topup .result (), None )
55+
56+ wallet .unloadwallet ()
57+
58+ # Check that everything was written
59+ wallet_db = self .nodes [0 ].wallets_path / "concurrency" / self .wallet_data_filename
60+ conn = sqlite3 .connect (wallet_db )
61+ with conn :
62+ # Retrieve the bestblock_nomerkle record
63+ bestblock_rec = conn .execute ("SELECT value FROM main WHERE hex(key) = '1262657374626C6F636B5F6E6F6D65726B6C65'" ).fetchone ()[0 ]
64+ # Retrieve the number of descriptor cache records
65+ # Since we store binary data, sqlite's comparison operators don't work everywhere
66+ # so just retrieve all records and process them ourselves.
67+ db_keys = conn .execute ("SELECT key FROM main" ).fetchall ()
68+ cache_records = len ([k [0 ] for k in db_keys if b"walletdescriptorcache" in k [0 ]])
69+ conn .close ()
70+
71+ assert_equal (bestblock_rec [5 :37 ][::- 1 ].hex (), self .nodes [0 ].getbestblockhash ())
72+ assert_equal (cache_records , 1000 )
73+
3674 def run_test (self ):
3775 if self .is_bdb_compiled ():
3876 # Make a legacy wallet and check it is BDB
@@ -240,6 +278,8 @@ def run_test(self):
240278 conn .close ()
241279 assert_raises_rpc_error (- 4 , "Unexpected legacy entry in descriptor wallet found." , self .nodes [0 ].loadwallet , "crashme" )
242280
281+ self .test_concurrent_writes ()
282+
243283
244284if __name__ == '__main__' :
245285 WalletDescriptorTest ().main ()
0 commit comments