Skip to content

Commit 548ecd1

Browse files
committed
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.
1 parent 395bcd2 commit 548ecd1

File tree

1 file changed

+40
-0
lines changed

1 file changed

+40
-0
lines changed

test/functional/wallet_descriptor.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
except ImportError:
1010
pass
1111

12+
import concurrent.futures
13+
1214
from test_framework.blocktools import COINBASE_MATURITY
15+
from test_framework.descriptors import descsum_create
1316
from test_framework.test_framework import BitcoinTestFramework
1417
from 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

244284
if __name__ == '__main__':
245285
WalletDescriptorTest().main ()

0 commit comments

Comments
 (0)