test: add coverage for importdescriptors while wallet is rescanning
What changed, and why it matters
This commit only adds a new automated test to Bitcoin Core. It checks that if you try to import wallet descriptors while the wallet is already busy rescanning the blockchain, the second import attempt is rejected with a clear error message. There is no change to production wallet code, so this patch does not fix or introduce any live security issue by itself.
No security action required. Treat as normal test-coverage commit during review.
Security signals we found
No production code changed
Adds regression/behavioral test for concurrent wallet rescan handling
Existing error path (-4 'Wallet is currently rescanning') is exercised, not newly introduced
Evidence from the diff
The diff adds test_rescan_fails_import() to test/functional/wallet_importdescriptors.py. The test creates a wallet, starts a long-running importdescriptors rescan via an xprv with timestamp=0 and range [0,10000], then concurrently submits a second importdescriptors call. It asserts that exactly one call succeeds and the other fails immediately with JSONRPC error code -4 (‘Wallet is currently rescanning. Abort existing rescan or wait.’). This is purely test-coverage expansion; no C++/Python production logic is modified.
Changed components
test/functional/wallet_importdescriptors.pyInspect captured patch +51 / −0
diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py
index 692d042a..85fb38e5 100755
--- a/test/functional/wallet_importdescriptors.py
+++ b/test/functional/wallet_importdescriptors.py
@@ -16,6 +16,7 @@ variants.
and test the values returned."""
import concurrent.futures
+import threading
import time
from test_framework.blocktools import COINBASE_MATURITY
@@ -117,6 +118,55 @@ class ImportDescriptorsTest(BitcoinTestFramework):
wallet=wallet)
wallet.unloadwallet()
+ def test_rescan_fails_import(self):
+ xpriv = "tprv8ZgxMBicQKsPeuVhWwi6wuMQGfPKi9Li5GtX35jVNknACgqe3CY4g5xgkfDDJcmtF7o1QnxWDRYw4H5P26PXq7sbcUkEqeR4fg3Kxp2tigg"
+
+ self.log.info("Test importdescriptors fails when wallet is already rescanning")
+ wallet_name = "rescan_wallet"
+ self.nodes[0].createwallet(wallet_name=wallet_name, blank=True)
+ other_desc = descsum_create("pkh(" + get_generate_key().privkey + ")")
+
+ w_import = self.nodes[0].create_new_rpc_connection(mode="AUTHPROXY") / f"wallet/{wallet_name}"
+
+ with concurrent.futures.ThreadPoolExecutor(max_workers=2) as thread:
+ w_rescan = self.nodes[0].create_new_rpc_connection(mode="AUTHPROXY") / f"wallet/{wallet_name}"
+ w_conflict = self.nodes[0].create_new_rpc_connection(mode="AUTHPROXY") / f"wallet/{wallet_name}"
+ # Use an xprv with timestamp=0 and a large key-range to trigger a slow full rescan that stays in-flight
+ slow_desc = [{"desc": descsum_create("pkh(" + xpriv + "/0h/*h)"),
+ "timestamp": 0, "range": [0, 10000]}]
+ conflicting_desc = [{"desc": descsum_create("pkh(" + xpriv + "/1h/*h)"),
+ "timestamp": 0, "range": [0, 10000]}]
+
+ start = threading.Barrier(3)
+
+ def import_after_barrier(wallet, descriptors):
+ start.wait(timeout=10)
+ return wallet.importdescriptors(descriptors)
+
+ imports = [
+ thread.submit(import_after_barrier, w_rescan, slow_desc),
+ thread.submit(import_after_barrier, w_conflict, conflicting_desc),
+ ]
+ start.wait(timeout=10)
+
+ # One importdescriptor call must hold WalletRescanReserver while the other fails immediately.
+ num_errors = 0
+ num_success = 0
+ for future in concurrent.futures.as_completed(imports, timeout=30 * self.options.timeout_factor):
+ try:
+ assert_equal(future.result(), [{'success': True}])
+ num_success += 1
+ except JSONRPCException as e:
+ assert_equal(e.error["code"], -4)
+ assert_equal(e.error["message"], "Wallet is currently rescanning. Abort existing rescan or wait.")
+ num_errors += 1
+
+ assert_equal(num_success, 1)
+ assert_equal(num_errors, 1)
+
+ # After the rescan finishes, any importdescriptors should succeed.
+ result = w_import.importdescriptors([{"desc": other_desc, "timestamp": "now"}])
+ assert_equal(result[0]['success'], True)
def run_test(self):
self.log.info('Setting up wallets')
@@ -882,6 +932,7 @@ class ImportDescriptorsTest(BitcoinTestFramework):
self.test_import_unused_key()
self.test_import_unused_key_existing()
self.test_import_unused_noprivs()
+ self.test_rescan_fails_import()
if __name__ == '__main__':
ImportDescriptorsTest(__file__).main()
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.