From 84d07e471cb36951ca5b5d9abb303b16a4f8d1d2 Mon Sep 17 00:00:00 2001 From: Pol Espinasa Date: Wed, 29 Apr 2026 13:43:52 +0200 Subject: [PATCH 1/4] test: add coverage for importdescriptor with an encrypted wallet --- test/functional/wallet_importdescriptors.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py index 9a3a1802982..692d042a192 100755 --- a/test/functional/wallet_importdescriptors.py +++ b/test/functional/wallet_importdescriptors.py @@ -762,6 +762,10 @@ class ImportDescriptorsTest(BitcoinTestFramework): self.nodes[0].createwallet("encrypted_wallet", blank=True, passphrase="passphrase") encrypted_wallet = self.nodes[0].get_wallet_rpc("encrypted_wallet") + self.log.info("Wallet must be unlocked to import a descriptor") + assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first.", + encrypted_wallet.importdescriptors, [descriptor]) + descriptor["timestamp"] = 0 descriptor["next_index"] = 0 From ad388bf2541e5d70717de69c05e8fa283fa3f348 Mon Sep 17 00:00:00 2001 From: Pol Espinasa Date: Wed, 29 Apr 2026 13:44:34 +0200 Subject: [PATCH 2/4] test: add coverage for importdescriptors while wallet is rescanning Co-authored-by: w0xlt --- test/functional/wallet_importdescriptors.py | 51 +++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py index 692d042a192..85fb38e5474 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() From d90d7f0a55d3aa63019b8371ea0dd295bdc9d57a Mon Sep 17 00:00:00 2001 From: Pol Espinasa Date: Wed, 29 Apr 2026 21:51:43 +0200 Subject: [PATCH 3/4] test: add coverage for importdescriptors errors when using assumeutxo Co-authored-by: w0xlt --- test/functional/wallet_assumeutxo.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/functional/wallet_assumeutxo.py b/test/functional/wallet_assumeutxo.py index 3f28e36c805..9c647fa22d8 100755 --- a/test/functional/wallet_assumeutxo.py +++ b/test/functional/wallet_assumeutxo.py @@ -215,12 +215,18 @@ class AssumeutxoTest(BitcoinTestFramework): wallet_name = "w1" n1.createwallet(wallet_name, disable_private_keys=True) key = get_generate_key() - time = n1.getblockchaininfo()['time'] + block_info = n1.getblockchaininfo() + time = block_info["time"] + def expected_rescan_error(timestamp): + return f"Rescan failed for descriptor with timestamp {timestamp}. There was an error reading a block from time {time}, which is after or within 7200 seconds of key creation, and could contain transactions pertaining to the desc. As a result, transactions and coins using this desc may not appear in the wallet. This error is likely caused by an in-progress assumeutxo background sync. Check logs or getchainstates RPC for assumeutxo background sync progress and try again later." timestamp = 0 - expected_error_message = f"Rescan failed for descriptor with timestamp {timestamp}. There was an error reading a block from time {time}, which is after or within 7200 seconds of key creation, and could contain transactions pertaining to the desc. As a result, transactions and coins using this desc may not appear in the wallet. This error is likely caused by an in-progress assumeutxo background sync. Check logs or getchainstates RPC for assumeutxo background sync progress and try again later." result = self.import_descriptor(n1, wallet_name, key, timestamp) assert_equal(result[0]['error']['code'], -1) - assert_equal(result[0]['error']['message'], expected_error_message) + assert_equal(result[0]['error']['message'], expected_rescan_error(timestamp)) + now_timestamp = block_info["mediantime"] + result = self.import_descriptor(n1, wallet_name, get_generate_key(), "now") + assert_equal(result[0]['error']['code'], -1) + assert_equal(result[0]['error']['message'], expected_rescan_error(now_timestamp)) self.log.info("Test that rescanning blocks from before the snapshot fails when blocks are not available from the background sync yet") w1 = n1.get_wallet_rpc(wallet_name) From ed11dd6a5fe75c6bc2f21d43da75fa25bd77e7e2 Mon Sep 17 00:00:00 2001 From: Pol Espinasa Date: Mon, 8 Jun 2026 11:41:07 +0200 Subject: [PATCH 4/4] test: add coverage for importdescriptors when manually interrupting a wallet rescan Co-authored-by: w0xlt --- test/functional/wallet_importdescriptors.py | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py index 85fb38e5474..f9429079bbd 100755 --- a/test/functional/wallet_importdescriptors.py +++ b/test/functional/wallet_importdescriptors.py @@ -168,6 +168,33 @@ class ImportDescriptorsTest(BitcoinTestFramework): result = w_import.importdescriptors([{"desc": other_desc, "timestamp": "now"}]) assert_equal(result[0]['success'], True) + self.log.info("Aborting an importdescriptors rescan should fail the RPC call") + wallet_name = "abort_import_wallet" + self.nodes[0].createwallet(wallet_name, blank=True) + + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as thread: + w_import = self.nodes[0].create_new_rpc_connection(mode="AUTHPROXY") / f"wallet/{wallet_name}" + abort_rpc = self.nodes[0].create_new_rpc_connection(mode="AUTHPROXY") / f"wallet/{wallet_name}" + descriptor = [{"desc": descsum_create("pkh(" + xpriv + "/2h/*h)"), + "timestamp": 0, "range": [0, 4000]}] + + importing = thread.submit(w_import.importdescriptors, descriptor) + + # Keep trying because an abort before ScanForWalletTransactions starts + # is reset when the scan loop begins. + abort_succeeded = False + abort_deadline = time.time() + 30 * self.options.timeout_factor + while not importing.done() and time.time() < abort_deadline: + abort_succeeded = abort_rpc.abortrescan() or abort_succeeded + + assert_equal(abort_succeeded, True) + try: + importing.result(timeout=30 * self.options.timeout_factor) + raise AssertionError("importdescriptors unexpectedly succeeded") + except JSONRPCException as e: + assert_equal(e.error["code"], -1) + assert_equal(e.error["message"], "Rescan aborted by user.") + def run_test(self): self.log.info('Setting up wallets') self.nodes[0].createwallet(wallet_name='w0', disable_private_keys=False)