From 5dcb53f7cebdb778bc0f68677b5cb1a246d02eb3 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Mon, 18 Dec 2023 16:06:36 -0500 Subject: [PATCH] tests: Test for concurrent writes with db tx There are occasions where a multi-statement tx is begun in one batch, and a second batch is created which does a normal write (without a multi-statement tx). These should not conflict with each other and all of the data should end up being written to disk. --- test/functional/wallet_descriptor.py | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/functional/wallet_descriptor.py b/test/functional/wallet_descriptor.py index e9321b72e20695..a640d58ac00a3a 100755 --- a/test/functional/wallet_descriptor.py +++ b/test/functional/wallet_descriptor.py @@ -9,7 +9,10 @@ except ImportError: pass +import concurrent.futures + from test_framework.blocktools import COINBASE_MATURITY +from test_framework.descriptors import descsum_create from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, @@ -33,6 +36,38 @@ def skip_test_if_missing_module(self): self.skip_if_no_sqlite() self.skip_if_no_py_sqlite3() + def test_concurrent_writes(self): + self.log.info("Test sqlite concurrent writes are in the correct order") + self.restart_node(0, extra_args=["-unsafesqlitesync=0"]) + self.nodes[0].createwallet(wallet_name="concurrency", blank=True) + wallet = self.nodes[0].get_wallet_rpc("concurrency") + # First import a descriptor that uses hardened dervation so that topping up + # Will require writing a ton to db + wallet.importdescriptors([{"desc":descsum_create("wpkh(tprv8ZgxMBicQKsPeuVhWwi6wuMQGfPKi9Li5GtX35jVNknACgqe3CY4g5xgkfDDJcmtF7o1QnxWDRYw4H5P26PXq7sbcUkEqeR4fg3Kxp2tigg/0h/0h/*h)"), "timestamp": "now", "active": True}]) + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as thread: + topup = thread.submit(wallet.keypoolrefill, newsize=1000) + + # Then while the topup is running, we need to do something that will call + # ChainStateFlushed which will trigger a write to the db, hopefully at the + # same time that the topup still has an open db transaction. + self.nodes[0].cli.gettxoutsetinfo() + assert_equal(topup.result(), None) + + wallet.unloadwallet() + + # Check that everything was written + wallet_db = self.nodes[0].wallets_path / "concurrency" / self.wallet_data_filename + conn = sqlite3.connect(wallet_db) + with conn: + # Retrieve the bestblock_nomerkle record + bestblock_rec = conn.execute("SELECT value FROM main WHERE hex(key) = '1262657374626C6F636B5F6E6F6D65726B6C65'").fetchone()[0] + # Retrieve the number of descriptor cache records + cache_records = conn.execute("SELECT COUNT(key) FROM main WHERE key LIKE '%walletdescriptorcache%'").fetchone()[0] + conn.close() + + assert_equal(bestblock_rec[5:37][::-1].hex(), self.nodes[0].getbestblockhash()) + assert_equal(cache_records, 1000) + def run_test(self): if self.is_bdb_compiled(): # Make a legacy wallet and check it is BDB @@ -240,6 +275,8 @@ def run_test(self): conn.close() assert_raises_rpc_error(-4, "Unexpected legacy entry in descriptor wallet found.", self.nodes[0].loadwallet, "crashme") + self.test_concurrent_writes() + if __name__ == '__main__': WalletDescriptorTest().main ()