test: Add importdescriptors rpc error coverage #35179

pull polespinasa wants to merge 4 commits into bitcoin:master from polespinasa:2026-04-29-testaddimportdescriptorsrpccoverage changing 2 files +91 −3
  1. polespinasa commented at 11:55 AM on April 29, 2026: member

    The current tests for importdescriptors RPC do not check for cases where RPC errors should be thrown.

    This PR adds coverage for importing a descriptor when the wallet is encrypted , for importing a descriptor while the wallet is rescanning and importing a descriptor while using assumeutxo

    For context, this lack of coverage was found while implementing #34861 when a reviewer found that this was being silently broken in the PR.

    I am not sure if the "rescanning test" approach is the optimal solution, I am open to suggestions.

  2. DrahtBot added the label Tests on Apr 29, 2026
  3. DrahtBot commented at 11:55 AM on April 29, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/35179.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK w0xlt
    Stale ACK ViniciusCestarii, achow101, rkrux, Bicaru20

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #35377 (wallet: Allow importing of descriptors without private keys when the wallet has the private keys by achow101)
    • #31668 (Added rescan option for import descriptors by saikiran57)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. polespinasa force-pushed on Apr 29, 2026
  5. DrahtBot added the label CI failed on Apr 29, 2026
  6. polespinasa force-pushed on Apr 29, 2026
  7. polespinasa force-pushed on Apr 29, 2026
  8. polespinasa force-pushed on Apr 29, 2026
  9. DrahtBot removed the label CI failed on Apr 29, 2026
  10. in test/functional/wallet_importdescriptors.py:846 in 9b06a8e069 outdated
     841 | +            # which should hold WalletRescanReserver for a few seconds.
     842 | +            with self.nodes[0].assert_debug_log(
     843 | +                expected_msgs=["Rescan started from block 0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206... (slow variant inspecting all blocks)"],
     844 | +                timeout=10,
     845 | +            ):
     846 | +                thread.submit(make_proxy().importdescriptors,
    


    junbyjun1238 commented at 5:17 PM on April 29, 2026:

    Question: Should the submitted import future be checked here?

    The second RPC call is made after the rescan-start log is observed, so this does exercise the "already rescanning" error path. But the background importdescriptors future is not stored or checked, and leaving the ThreadPoolExecutor context waits for it to finish before abortrescan() is called. So the later abortrescan() call likely does not actually abort this rescan.

    Would it be clearer to either assert the background import result, or move the abort/check inside the executor block if the intention is to stop the rescan early?


    polespinasa commented at 5:53 PM on April 29, 2026:

    But the background importdescriptors future is not stored or checked

    It doesn't matter, it is not what this test is doing. There are plenty of tests testing "normal" imports. This test only wants to make sure that if a rescan is going on, trying to import a descriptor to that wallet will fail.

    You are right about the abortrescan(), before it was inside the thread, I guess I moved it by mistake, but given the test is pretty fast, it can just be deleted.

  11. polespinasa force-pushed on Apr 29, 2026
  12. in test/functional/wallet_importdescriptors.py:836 in 9b06a8e069 outdated
     831 | +        wallet_name = "rescan_wallet"
     832 | +        self.nodes[0].createwallet(wallet_name=wallet_name, blank=True)
     833 | +        w_rescan = self.nodes[0].get_wallet_rpc(wallet_name)
     834 | +
     835 | +        def make_proxy():
     836 | +            return AuthServiceProxy(rpc_url(self.nodes[0].datadir_path, self.nodes[0].index, self.nodes[0].chain, self.nodes[0].rpchost) + "/wallet/" + wallet_name)
    


    ViniciusCestarii commented at 5:55 PM on April 29, 2026:

    nit: the testing framework has an util function for this:

            def make_proxy():
                return get_rpc_proxy(f"{self.nodes[0].url}/wallet/{wallet_name}", self.nodes[0].index)
    

    polespinasa commented at 7:12 PM on April 29, 2026:

    I could use get_rpc_proxy but there's no much difference because I cannot use self.nodes[0].url it returns None if --usecli is used.

    If I use get_rpc_proxy the diff would look like:

    <details> <summary>diff</summary>

    $ git diff
    diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py
    index cd18bf4474..ef31bf5eec 100755
    --- a/test/functional/wallet_importdescriptors.py
    +++ b/test/functional/wallet_importdescriptors.py
    @@ -23,10 +23,10 @@ from test_framework.blocktools import COINBASE_MATURITY
     from test_framework.test_framework import BitcoinTestFramework
     from test_framework.descriptors import descsum_create
     from test_framework.script import SEQUENCE_LOCKTIME_TYPE_FLAG
    -from test_framework.authproxy import AuthServiceProxy
     from test_framework.util import (
         assert_equal,
         assert_raises_rpc_error,
    +    get_rpc_proxy,
         rpc_url,
     )
     from test_framework.wallet_util import (
    @@ -833,7 +833,8 @@ class ImportDescriptorsTest(BitcoinTestFramework):
             w_rescan = self.nodes[0].get_wallet_rpc(wallet_name)
     
             def make_proxy():
    -            return AuthServiceProxy(rpc_url(self.nodes[0].datadir_path, self.nodes[0].index, self.nodes[0].chain, self.nodes[0].rpchost) + "/wallet/" + wallet_name)
    +            return get_rpc_proxy(rpc_url(self.nodes[0].datadir_path, self.nodes[0].index, self.nodes[0].chain, self.nodes[0].rpchost) + f"/wallet/{wallet_name}",
    +             self.nodes[0].index)
     
             with concurrent.futures.ThreadPoolExecutor(max_workers=1) as thread:
                 slow_desc = descsum_create("pkh(" + xpriv + "/1h/*h)")
    
    

    </details>

    Which looks even a bit longer so it's less readable. I will let as is unless someone suggest a cleaner way of doing it.


    ViniciusCestarii commented at 7:43 PM on April 29, 2026:

    Cool, didn't know about the --usecli implication. Leaving as-is makes sense

  13. ViniciusCestarii commented at 6:22 PM on April 29, 2026: contributor

    tACK 5e23ea4007b1664c97de4e65c25204f7c6aaa06d

    The rescanning test looks fine. Line 724 has used the same pattern to prevent race condition since dbeca79

  14. polespinasa force-pushed on Apr 29, 2026
  15. DrahtBot added the label CI failed on Apr 29, 2026
  16. polespinasa force-pushed on Apr 29, 2026
  17. polespinasa force-pushed on Apr 29, 2026
  18. polespinasa commented at 8:25 PM on April 29, 2026: member

    Added another test for importdescriptors when using assumeutxo

  19. polespinasa force-pushed on Apr 29, 2026
  20. DrahtBot removed the label CI failed on Apr 29, 2026
  21. in test/functional/wallet_importdescriptors.py:836 in efeb39996a
     831 | +        wallet_name = "rescan_wallet"
     832 | +        self.nodes[0].createwallet(wallet_name=wallet_name, blank=True)
     833 | +        w_rescan = self.nodes[0].get_wallet_rpc(wallet_name)
     834 | +
     835 | +        def make_proxy():
     836 | +            return AuthServiceProxy(rpc_url(self.nodes[0].datadir_path, self.nodes[0].index, self.nodes[0].chain, self.nodes[0].rpchost) + "/wallet/" + wallet_name)
    


    maflcko commented at 9:14 AM on May 6, 2026:

    you are side-stepping the timeout and the coverage. This may be fine, but I think it is cleaner to have a function here. Maybe faab8e28db0bf205451ff3d30f269167b73f24c0 (if you want to use the rpc), or alternatively, use the cli(), like the other tests that spin up a new thread and need a new rpc socket for that thread.


    polespinasa commented at 9:31 AM on May 6, 2026:

    Done, used cli()

  22. polespinasa force-pushed on May 6, 2026
  23. polespinasa commented at 9:32 AM on May 6, 2026: member

    Force pushed to address comments and rebased on top of master

  24. achow101 commented at 11:09 PM on May 28, 2026: member

    ACK cb50adad6d6cf936350f748f3b7e618b2df0037a

  25. DrahtBot requested review from ViniciusCestarii on May 28, 2026
  26. sedited requested review from maflcko on Jun 3, 2026
  27. in test/functional/wallet_importdescriptors.py:854 in db04594f4f outdated
     849 | +                wallet_cli().importdescriptors,
     850 | +                [{"desc": slow_desc, "timestamp": "now"}])
     851 | +
     852 | +        # After the rescan is finished, importdescriptors should succeed with a fresh descriptor.
     853 | +        result = w_rescan.importdescriptors([{"desc": descsum_create("pkh(" + get_generate_key().privkey + ")"), "timestamp": "now"}])
     854 | +        assert_equal(result[0]['success'], True)
    


    rkrux commented at 3:11 PM on June 3, 2026:

    Please extract this in a function? run_test is already 800 lines long.


    polespinasa commented at 6:59 PM on June 3, 2026:

    Feels weird, when the whole test is structured as a one-function. I guess a follow-up could be to organize a bit the whole test and create different test functions.


    rkrux commented at 10:09 AM on June 4, 2026:

    Refactoring like this can be done simultaneously with the other work. I doubt a separate PR just for this kind of refactoring would garner reviews.

  28. in test/functional/wallet_importdescriptors.py:843 in db04594f4f outdated
     838 | +            # importdescriptors with timestamp=0 triggers key derivation + rescan,
     839 | +            # which should hold WalletRescanReserver for a few seconds.
     840 | +            with self.nodes[0].assert_debug_log(
     841 | +                expected_msgs=["Rescan started from block 0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206... (slow variant inspecting all blocks)"],
     842 | +                timeout=10,
     843 | +            ):
    


    rkrux commented at 3:13 PM on June 3, 2026:

    Is this assert debug log necessary? Reservation happens pretty early in the execution of the RPC. https://github.com/bitcoin/bitcoin/blob/7c2718a4b82df709ee06380209747577514acbf0/src/wallet/rpc/backup.cpp#L378-L390

    This assert is only delaying the calling of the next RPC.


    polespinasa commented at 6:55 PM on June 3, 2026:

    I don't think it matters at all if it delays a bit the call of the RPC.

    I like it because is self-explanatory of what we are doing. Happy to drop it in case other reviewers also consider is worth to drop.


    rkrux commented at 9:56 AM on June 4, 2026:

    It makes the test do more than necessary when it asserts for the message in the debug log. But fine to keep it if you prefer it here.

  29. in test/functional/wallet_importdescriptors.py:850 in db04594f4f
     845 | +                    [{"desc": slow_desc, "timestamp": 0, "active": True, "range": [0, 4000], "next_index": 0}])
     846 | +
     847 | +            # while WalletRescanReserver is held trying to import a descriptor should fail.
     848 | +            assert_raises_rpc_error(-4, "Wallet is currently rescanning. Abort existing rescan or wait.",
     849 | +                wallet_cli().importdescriptors,
     850 | +                [{"desc": slow_desc, "timestamp": "now"}])
    


    rkrux commented at 3:14 PM on June 3, 2026:

    Nit: Seems a bit odd that the same descriptor is being imported again, use the other one?

    @@ -835,22 +835,19 @@ class ImportDescriptorsTest(BitcoinTestFramework):
     
             with concurrent.futures.ThreadPoolExecutor(max_workers=1) as thread:
                 slow_desc = descsum_create("pkh(" + xpriv + "/1h/*h)")
    +            other_desc = descsum_create("pkh(" + get_generate_key().privkey + ")")
                 # importdescriptors with timestamp=0 triggers key derivation + rescan,
                 # which should hold WalletRescanReserver for a few seconds.
    
                 assert_raises_rpc_error(-4, "Wallet is currently rescanning. Abort existing rescan or wait.",
                     wallet_cli().importdescriptors,
    -                [{"desc": slow_desc, "timestamp": "now"}])
    +                [{"desc": other_desc, "timestamp": "now"}])
     
             # After the rescan is finished, importdescriptors should succeed with a fresh descriptor.
    -        result = w_rescan.importdescriptors([{"desc": descsum_create("pkh(" + get_generate_key().privkey + ")"), "timestamp": "now"}])
    +        result = w_rescan.importdescriptors([{"desc": other_desc, "timestamp": "now"}])
             assert_equal(result[0]['success'], True)
    

    polespinasa commented at 6:48 PM on June 3, 2026:

    It does not matter, but picked it so it does not cause confusion.


    rkrux commented at 10:09 AM on June 4, 2026:

    From a functionality point of view, it's odd to see that the same descriptor is being imported with a different timestamp immediately. I understand that this is a test but importing a separate descriptor seems more natural and less contrived.

  30. in test/functional/wallet_importdescriptors.py:838 in db04594f4f
     833 | +        def wallet_cli():
     834 | +            return self.nodes[0].cli(f"-rpcwallet={wallet_name}")
     835 | +
     836 | +        with concurrent.futures.ThreadPoolExecutor(max_workers=1) as thread:
     837 | +            slow_desc = descsum_create("pkh(" + xpriv + "/1h/*h)")
     838 | +            # importdescriptors with timestamp=0 triggers key derivation + rescan,
    


    rkrux commented at 3:15 PM on June 3, 2026:

    importdescriptors with timestamp=0 triggers key derivation + rescan

    I don't believe timestamp=0 is the reason for derivation - isn't it the presence of 4000 range that causes the (multiple) derivations?


    polespinasa commented at 7:08 PM on June 3, 2026:

    you are right, poor writing from my part here, fixed in the next push. Thanks!

  31. rkrux commented at 3:17 PM on June 3, 2026: contributor

    Concept ACK cb50adad6d6cf936350f748f3b7e618b2df0037a

  32. w0xlt commented at 6:34 PM on June 3, 2026: contributor

    lgtm ACK cb50adad6d6cf936350f748f3b7e618b2df0037a

  33. DrahtBot requested review from rkrux on Jun 3, 2026
  34. polespinasa force-pushed on Jun 3, 2026
  35. polespinasa requested review from w0xlt on Jun 3, 2026
  36. in test/functional/wallet_importdescriptors.py:848 in fa1d727224 outdated
     843 | +            with self.nodes[0].assert_debug_log(
     844 | +                expected_msgs=["Rescan started from block 0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206... (slow variant inspecting all blocks)"],
     845 | +                timeout=10,
     846 | +            ):
     847 | +                thread.submit(wallet_cli().importdescriptors,
     848 | +                    [{"desc": slow_desc, "timestamp": 0, "active": True, "range": [0, 4000], "next_index": 0}])
    


    Bicaru20 commented at 7:37 PM on June 3, 2026:

    Small nit: I don't think next_index is needed here to held WalletRescanReserver. It could cause some confussion.

                        [{"desc": slow_desc, "timestamp": 0, "active": True, "range": [0, 4000]}])
    

    polespinasa commented at 7:57 PM on June 3, 2026:

    Will keep as it is, if a range descriptor is set (like this case) next_index specifies the next index to generate addresses. We are forcing, and documenting, that we want to start generating addresses from the beginning.

  37. polespinasa commented at 7:51 PM on June 3, 2026: member

    The CI failure seems unrelated. Probably with a CI re-run would pass.

  38. Bicaru20 commented at 7:53 PM on June 3, 2026: none

    ACK fa1d727224fd1aa53aae1985dee082a9ff291137. Run and passed the tests. Left a small nit in the code, feel free to ignore it if it is not relevant.

  39. DrahtBot requested review from achow101 on Jun 3, 2026
  40. DrahtBot added the label CI failed on Jun 3, 2026
  41. rkrux commented at 10:10 AM on June 4, 2026: contributor

    lgtm ACK fa1d727

    I like the test in the second commit.

  42. fanquake closed this on Jun 4, 2026

  43. fanquake reopened this on Jun 4, 2026

  44. DrahtBot removed the label CI failed on Jun 4, 2026
  45. in test/functional/wallet_importdescriptors.py:847 in fa1d727224
     842 | +
     843 | +            with self.nodes[0].assert_debug_log(
     844 | +                expected_msgs=["Rescan started from block 0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206... (slow variant inspecting all blocks)"],
     845 | +                timeout=10,
     846 | +            ):
     847 | +                thread.submit(wallet_cli().importdescriptors,
    


    w0xlt commented at 6:30 AM on June 6, 2026:

    The test starts a background importdescriptors call but never checks its Future, so any exception or unexpected result from that background RPC can be missed. Storing the future and calling result() makes the test fail if the slow import does not actually complete successfully.

    diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py
    index e2b9914fa8..45c7264e0f 100755
    --- a/test/functional/wallet_importdescriptors.py
    +++ b/test/functional/wallet_importdescriptors.py
    @@ -844,13 +844,14 @@ class ImportDescriptorsTest(BitcoinTestFramework):
                     expected_msgs=["Rescan started from block 0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206... (slow variant inspecting all blocks)"],
                     timeout=10,
                 ):
    -                thread.submit(wallet_cli().importdescriptors,
    +                importing = thread.submit(wallet_cli().importdescriptors,
                         [{"desc": slow_desc, "timestamp": 0, "active": True, "range": [0, 4000], "next_index": 0}])
     
                 # while WalletRescanReserver is held trying to import a descriptor should fail.
                 assert_raises_rpc_error(-4, "Wallet is currently rescanning. Abort existing rescan or wait.",
                     wallet_cli().importdescriptors,
                     [{"desc": other_desc, "timestamp": "now"}])
    +            assert_equal(importing.result(), [{"success": True}])
     
             # After the rescan is finished, importdescriptors should succeed.
             result = w_rescan.importdescriptors([{"desc": other_desc, "timestamp": "now"}])
    

    polespinasa commented at 7:07 AM on June 8, 2026:

    good idea, picked

  46. DrahtBot requested review from w0xlt on Jun 6, 2026
  47. polespinasa force-pushed on Jun 8, 2026
  48. polespinasa commented at 7:08 AM on June 8, 2026: member

    Force pushed to add a success check in the second commit after the "slow import".

    $ git diff fa1d727224fd1aa53aae1985dee082a9ff291137..9ee3f49f9f16a4d07c0e0a3f9facbd503cfd1434
    diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py
    index e2b9914fa8..45c7264e0f 100755
    --- a/test/functional/wallet_importdescriptors.py
    +++ b/test/functional/wallet_importdescriptors.py
    @@ -844,13 +844,14 @@ class ImportDescriptorsTest(BitcoinTestFramework):
                     expected_msgs=["Rescan started from block 0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206... (slow variant inspecting all blocks)"],
                     timeout=10,
                 ):
    -                thread.submit(wallet_cli().importdescriptors,
    +                importing = thread.submit(wallet_cli().importdescriptors,
                         [{"desc": slow_desc, "timestamp": 0, "active": True, "range": [0, 4000], "next_index": 0}])
     
                 # while WalletRescanReserver is held trying to import a descriptor should fail.
                 assert_raises_rpc_error(-4, "Wallet is currently rescanning. Abort existing rescan or wait.",
                     wallet_cli().importdescriptors,
                     [{"desc": other_desc, "timestamp": "now"}])
    +            assert_equal(importing.result(), [{"success": True}])
     
             # After the rescan is finished, importdescriptors should succeed.
             result = w_rescan.importdescriptors([{"desc": other_desc, "timestamp": "now"}])
    
    
  49. DrahtBot added the label CI failed on Jun 8, 2026
  50. polespinasa force-pushed on Jun 8, 2026
  51. polespinasa force-pushed on Jun 8, 2026
  52. polespinasa force-pushed on Jun 8, 2026
  53. polespinasa force-pushed on Jun 8, 2026
  54. polespinasa commented at 9:54 AM on June 8, 2026: member

    Added another commit with more test coverage. It ensures that if the user manually aborts the wallet rescaning, importdescriptors will fail.

  55. in test/functional/wallet_importdescriptors.py:1 in b2ce3e6c2f outdated


    rkrux commented at 10:28 AM on June 8, 2026:

    Now that this PR adds two long(er) test cases, reconsider splitting them into separate functions? I don't see why we should continue bloating run_test.


    rkrux commented at 10:34 AM on June 8, 2026:

    Re: #35179 (review)

    I missed it in my previous review that few test cases are already split into separate functions here: https://github.com/bitcoin/bitcoin/blob/5f33da9aa30ff80876f2f450b4370c446555bd71/test/functional/wallet_importdescriptors.py#L878-L880

    So it makes the case stronger for not having new tests* in the run_test function.


    polespinasa commented at 12:54 PM on June 8, 2026:

    done

  56. in test/functional/wallet_importdescriptors.py:897 in b2ce3e6c2f
     892 | +            # importdescriptors with range=[0, 4000] triggers derivation of 4001 keys,
     893 | +            # and timestamp=0 triggers a full rescan from genesis —
     894 | +            # together these hold WalletRescanReserver for a few seconds.
     895 | +
     896 | +            with self.nodes[0].busy_wait_for_debug_log(
     897 | +                expected_msgs=[b"Rescan started from block 0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206... (slow variant inspecting all blocks)"],
    


    rkrux commented at 10:56 AM on June 8, 2026:

    In 90a38d6adc3991162e5b04f9a6a6b5fefd0fa654 "test: add coverage for importdescriptors while wallet is rescanning"

    While the above code comment helps to some extent, it's usually not a good idea to hardcode test environment specific data in assertions, instead it can be retrieved it dynamically from the test env itself.

    diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py
    index 90b59e7af7..d6a8ccfbfb 100755
    --- a/test/functional/wallet_importdescriptors.py
    +++ b/test/functional/wallet_importdescriptors.py
    @@ -893,10 +893,8 @@ class ImportDescriptorsTest(BitcoinTestFramework):
                 # and timestamp=0 triggers a full rescan from genesis —
                 # together these hold WalletRescanReserver for a few seconds.
     
    -            with self.nodes[0].busy_wait_for_debug_log(
    -                expected_msgs=[b"Rescan started from block 0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206... (slow variant inspecting all blocks)"],
    -                timeout=10,
    -            ):
    +            log_msg = f"Rescan started from block {self.nodes[0].getblockhash(0)}... (slow variant inspecting all blocks)"
    +            with self.nodes[0].busy_wait_for_debug_log(expected_msgs=[log_msg.encode("utf-8")], timeout=10):
                     importing = thread.submit(wallet_cli(wallet_name).importdescriptors,
                         [{"desc": slow_desc, "timestamp": 0, "active": True, "range": [0, 4000], "next_index": 0}])
     
    
    

    polespinasa commented at 11:13 AM on June 8, 2026:

    done

  57. rkrux commented at 10:57 AM on June 8, 2026: contributor

    Code review b2ce3e6

    I like these test cases that touch upon parallel rescanning related RPCs, this portion of the code is usually not tested. So, this PR is a good addition in this regard.

  58. DrahtBot removed the label CI failed on Jun 8, 2026
  59. in test/functional/wallet_importdescriptors.py:887 in b2ce3e6c2f
     882 | +        wallet_name = "rescan_wallet"
     883 | +        self.nodes[0].createwallet(wallet_name=wallet_name, blank=True)
     884 | +        w_rescan = self.nodes[0].get_wallet_rpc(wallet_name)
     885 | +
     886 | +        def wallet_cli(wallet):
     887 | +            return self.nodes[0].cli(f"-rpcwallet={wallet}")
    


    maflcko commented at 11:26 AM on June 8, 2026:

    nit: It could be a bit confusing to call the cli, when the reason is not obvious. Maybe call create_new_rpc_connection, which documents that it "Create[s] an additional RPC connection, likely to be used in a new thread." Also, using create_new_rpc_connection would be more consistent with the general idea that tests consistently use the cli when passed --usecli, and consistently not use it when not passed this option (unless the test is specifically cli-specific.

    But just a nit


    polespinasa commented at 12:55 PM on June 8, 2026:

    Didn't know about create_new_rpc_connection, tried to apply your suggestion.

  60. polespinasa force-pushed on Jun 8, 2026
  61. polespinasa force-pushed on Jun 8, 2026
  62. DrahtBot added the label CI failed on Jun 8, 2026
  63. polespinasa requested review from Bicaru20 on Jun 8, 2026
  64. polespinasa requested review from rkrux on Jun 8, 2026
  65. polespinasa closed this on Jun 8, 2026

  66. polespinasa reopened this on Jun 8, 2026

  67. test: add coverage for importdescriptor with an encrypted wallet 84d07e471c
  68. polespinasa force-pushed on Jun 8, 2026
  69. DrahtBot removed the label CI failed on Jun 8, 2026
  70. Bicaru20 commented at 5:41 PM on June 8, 2026: none

    re-ACK a788dce1bd. Run the tests, everything looks good to me.

  71. in test/functional/wallet_importdescriptors.py:143 in a788dce1bd outdated
     138 | +            with self.nodes[0].busy_wait_for_debug_log(expected_msgs=[log_msg.encode("utf-8")], timeout=10):
     139 | +                rescanning = thread.submit(w_rescan.importdescriptors, slow_desc)
     140 | +
     141 | +            # Rescan is confirmed started; WalletRescanReserver is held.
     142 | +            # importdescriptors on the same wallet must fail immediately.
     143 | +            assert_raises_rpc_error(-4, "Wallet is currently rescanning. Abort existing rescan or wait.",
    


    maflcko commented at 5:30 AM on June 9, 2026:

    Pretty sure this is racy. There is no guarantee that the thread still runs? See also the existing test further down (c.f. dbeca792a9980085d00be0f9d78187ca3a4d7cdc)

    (Same below)


    w0xlt commented at 7:39 AM on June 9, 2026:

    There seem to be two separate races.

    For the abort test, fAbortRescan is reset after the “Rescan started...” log, so abortrescan() could set the flag before the rescan thread clears it. We could move the reset before the log.

    For the “already rescanning” test, the log only proves the rescan started, not that it is still running. We could also wait until getwalletinfo()["scanning"] shows active progress before sending the second RPC.

    It can make the test more robust.

    <details> <summary>diff</summary>

    diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
    index bb79dd471e..8bc8ef0b0e 100644
    --- a/src/wallet/wallet.cpp
    +++ b/src/wallet/wallet.cpp
    @@ -1881,10 +1881,10 @@ CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_bloc
         std::unique_ptr<FastWalletRescanFilter> fast_rescan_filter;
         if (chain().hasBlockFilterIndex(BlockFilterType::BASIC)) fast_rescan_filter = std::make_unique<FastWalletRescanFilter>(*this);
     
    +    fAbortRescan = false;
         WalletLogPrintf("Rescan started from block %s... (%s)\n", start_block.ToString(),
                         fast_rescan_filter ? "fast variant using block filters" : "slow variant inspecting all blocks");
     
    -    fAbortRescan = false;
         ShowProgress(strprintf("[%s] %s", DisplayName(), _("Rescanning…")), 0); // show rescan progress in GUI as dialog or on splashscreen, if rescan required on startup (e.g. due to corruption)
         uint256 tip_hash = WITH_LOCK(cs_wallet, return GetLastBlockHash());
         uint256 end_hash = tip_hash;
    diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py
    index 929bc62a28..a13b86a851 100755
    --- a/test/functional/wallet_importdescriptors.py
    +++ b/test/functional/wallet_importdescriptors.py
    @@ -128,6 +128,15 @@ class ImportDescriptorsTest(BitcoinTestFramework):
     
             w_import = self.nodes[0].create_new_rpc_connection(mode="AUTHPROXY") / f"wallet/{wallet_name}"
     
    +        def wait_for_rescan_to_advance(wallet_rpc, future):
    +            def rescan_has_progress():
    +                if future.done():
    +                    raise AssertionError("background importdescriptors rescan finished before the concurrent RPC could be tested")
    +                scanning = wallet_rpc.getwalletinfo()["scanning"]
    +                return scanning is not False and scanning["progress"] > 0
    +
    +            self.wait_until(rescan_has_progress, timeout=10)
    +
             with concurrent.futures.ThreadPoolExecutor(max_workers=1) as thread:
                 w_rescan = self.nodes[0].create_new_rpc_connection(mode="AUTHPROXY") / f"wallet/{wallet_name}"
                 # Use an xprv with timestamp=0 to trigger a slow full rescan that stays in-flight
    @@ -137,8 +146,9 @@ class ImportDescriptorsTest(BitcoinTestFramework):
     
                 with self.nodes[0].busy_wait_for_debug_log(expected_msgs=[log_msg.encode("utf-8")], timeout=10):
                     rescanning = thread.submit(w_rescan.importdescriptors, slow_desc)
    +            wait_for_rescan_to_advance(w_import, rescanning)
     
    -            # Rescan is confirmed started; WalletRescanReserver is held.
    +            # Rescan has advanced and WalletRescanReserver is still held.
                 # importdescriptors on the same wallet must fail immediately.
                 assert_raises_rpc_error(-4, "Wallet is currently rescanning. Abort existing rescan or wait.",
                     w_import.importdescriptors,
    @@ -161,10 +171,10 @@ class ImportDescriptorsTest(BitcoinTestFramework):
     
                 with self.nodes[0].busy_wait_for_debug_log(expected_msgs=[log_msg.encode("utf-8")], timeout=10):
                     importing = thread.submit(w_import.importdescriptors, descriptor)
    -
    -            # busy_wait_for_debug_log only returns after the expected message has
    -            # appeared, so the rescan has definitely started; abort it now.
                 abort_rpc = self.nodes[0].create_new_rpc_connection(mode="AUTHPROXY") / f"wallet/{wallet_name}"
    +            wait_for_rescan_to_advance(abort_rpc, importing)
    +
    +            # The rescan has advanced and is still in progress; abort it now.
                 abort_rpc.abortrescan()
     
                 try:
    

    </details>


    polespinasa commented at 6:06 PM on June 9, 2026:

    Marking as resolved to clean the comment log for reviewers. Refer to #35179 (comment) for an answer here :)

  72. polespinasa force-pushed on Jun 9, 2026
  73. polespinasa commented at 5:48 PM on June 9, 2026: member

    cc @maflcko

    As @w0xlt said there are two race conditions.

    1. is fixed by the last push as the previous log checked for a log line that is set before setting fAbortRescan=false so there could be the case were abortrescan() was called before setting fAbortRescan=false which would reset its value. By checking that getwaletinfo returns a dict object in the scanning key we are sure we entered the rescan loop. See that this only applies for the abortrescan test, as the importdescriptor rpc call does not check for a rescan to be running but for the wallet to be reserved.

    <details>

    
    $ git diff a788dce1bdcad44daf66f3bd58e8e34a30f33097..6fac1f19cfc6f1accbf7d47962cd08b432da9167
    diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py
    index 929bc62a28..6dcb040a93 100755
    --- a/test/functional/wallet_importdescriptors.py
    +++ b/test/functional/wallet_importdescriptors.py
    @@ -158,13 +158,11 @@ class ImportDescriptorsTest(BitcoinTestFramework):
                 w_import = 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)
     
    -            with self.nodes[0].busy_wait_for_debug_log(expected_msgs=[log_msg.encode("utf-8")], timeout=10):
    -                importing = thread.submit(w_import.importdescriptors, descriptor)
    -
    -            # busy_wait_for_debug_log only returns after the expected message has
    -            # appeared, so the rescan has definitely started; abort it now.
                 abort_rpc = self.nodes[0].create_new_rpc_connection(mode="AUTHPROXY") / f"wallet/{wallet_name}"
    +            # Wait until getwalletinfo reports scanning=dict (scanning=bool if not scanning).
    +            self.wait_until(lambda: isinstance(abort_rpc.getwalletinfo().get("scanning"), dict), timeout=10)
                 abort_rpc.abortrescan()
     
                 try:
    
    

    </details>

    1. There is another race condition that I think cannot be fixed, but it is unlikely and can be done more unlikely to be hit. It is finishing the rescan before calling abortrescan() or importdescriptors() (depending on the test). I would say that is unlikely to happen as the scanning range is quite big and should not be faster than just doing a simple rpc call. I would say to leave it as is and, in case we see failures, just maybe mine some more blocks to make that range even bigger?
  74. polespinasa force-pushed on Jun 9, 2026
  75. DrahtBot added the label CI failed on Jun 9, 2026
  76. maflcko commented at 6:57 PM on June 9, 2026: member

    2. I would say to leave it as is and, in case we see failures

    Well, CI seems to fail:

    https://github.com/bitcoin/bitcoin/actions/runs/27225874552/job/80393544442?pr=35179#step:10:311

    2026-06-09T18:26:44.043083Z TestFramework (INFO): Test importdescriptors fails when wallet is already rescanning
    
    2026-06-09T18:26:44.678802Z TestFramework (ERROR): Unexpected exception:
    
    Traceback (most recent call last):
    
      File "D:\a\bitcoin\bitcoin\test\functional\test_framework\test_framework.py", line 144, in main
    
        self.run_test()
    
        ~~~~~~~~~~~~~^^
    
      File "D:\a\bitcoin\bitcoin/test/functional/wallet_importdescriptors.py", line 939, in run_test
    
        self.test_rescan_fails_import()
    
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
    
      File "D:\a\bitcoin\bitcoin/test/functional/wallet_importdescriptors.py", line 143, in test_rescan_fails_import
    
        assert_raises_rpc_error(-4, "Wallet is currently rescanning. Abort existing rescan or wait.",
    
        ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    
            w_import.importdescriptors,
    
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^
    
            [{"desc": other_desc, "timestamp": "now"}])
    
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    
      File "D:\a\bitcoin\bitcoin\test\functional\test_framework\util.py", line 171, in assert_raises_rpc_error
    
        assert try_rpc(code, message, fun, *args, **kwds), "No exception raised"
    
               ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    
    AssertionError: No exception raised
    
  77. polespinasa force-pushed on Jun 9, 2026
  78. polespinasa commented at 7:34 PM on June 9, 2026: member

    Well, CI seems to fail

    I need github to include some crying emoji to react to comments :)

    According to claude the test failing was the windows one as i/o is slow there and rescan might end before the log can be actually read. Tried to fix it by changing the approach similarly to what I did for the other test to use getwalletinfo, also I increased the range of the descriptor to slow down a bit more the rescan. Hopefully now it does not fail.

  79. maflcko commented at 10:23 AM on June 10, 2026: member

    Well, it is still failing?

    I don't have a better suggestion, but ideally tests are passing on any platform. Some users are running on SBCs with slower IO (or similar). So ignoring them doesn't seem user-friendly. Also, intermittent CI errors are annoying to deal with in this repo. Finally, the tests shouldn't fail when run on antithesis/bedrock-style systems.

    You'll have to properly fix this, as explained in #35179 (review)

  80. test: add coverage for importdescriptors while wallet is rescanning
    Co-authored-by: w0xlt <woltx@protonmail.com>
    ad388bf254
  81. test: add coverage for importdescriptors errors when using assumeutxo
    Co-authored-by: w0xlt <woltx@protonmail.com>
    d90d7f0a55
  82. test: add coverage for importdescriptors when manually interrupting a wallet rescan
    Co-authored-by: w0xlt <woltx@protonmail.com>
    ed11dd6a5f
  83. polespinasa force-pushed on Jun 10, 2026
  84. polespinasa commented at 2:24 PM on June 10, 2026: member

    New approach :)

    Last force push changes the importdescriptor test. Instead of calling importdescriptor and after that call it again and check that the second fails, calls both at the same time and creates an on-purpose race condition between them, the test checks that one must succeed and the other must fails. We don't care which succeeds or fails.

    For the abortscan test, call abort scan until importdescriptor finishes. This fixes a race condition where abortscan could be too fast for the rescan to start. Then assert that importdescriptors was interrupted and did not succeed.

  85. maflcko commented at 2:57 PM on June 10, 2026: member

    It is still more than one thread, so they can race. I don't want to block this, but IIUC there is no scheduler guarantee about execution.

    Sure, this is adding some test coverage. But if the test can intermittently fail (now or in the future), I wonder if it is worth it. No strong opinion, just my thought.

  86. polespinasa commented at 3:08 PM on June 10, 2026: member

    It is still more than one thread, so they can race. I don't want to block this, but IIUC there is no scheduler guarantee about execution.

    I don't think this can be done in a single thread. If anyone comes with an idea on how to do it in a single thread I would love to try implement it.

    Sure, this is adding some test coverage. But if the test can intermittently fail (now or in the future), I wonder if it is worth it. No strong opinion, just my thought.

    It totally make sense, and the PR is structured this way so tests can easily be dropped in case they are not desired. Only commit 2 and 4 have multiple threads.

    For now what I am trying to do is come with the approach that is most likely (or will always) to succeed and not cause random failures. I would say that, if we come with a non-failure version this tests are worth (due to all changes this part of the code is having now). And if they cause failures in the future, dropping them is easy and not a big deal.

  87. DrahtBot removed the label CI failed on Jun 10, 2026
  88. w0xlt commented at 8:54 PM on June 10, 2026: contributor

    ACK ed11dd6a5fe75c6bc2f21d43da75fa25bd77e7e2

    Two notes:

    1. While concurrent-imports test's remaining race, it requires delaying one thread for the entire duration of the other call (~1.2s on machine). So the risk of spurious CI failures should be negligible.

    2. The abort test still has a race window (caused by the wallet's rescan-abort handling, not by the test itself), as mentioned before, but it can be addressed in a follow-up PR.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-06-11 10:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me