wallet: Construct ScriptPubKeyMans with all data rather than loaded progressively #28333

pull achow101 wants to merge 10 commits into bitcoin:master from achow101:raii-spkm changing 12 files +253 −171
  1. achow101 commented at 2:55 AM on August 24, 2023: member

    Instead of constructing ScriptPubKeyMans with no data, and then loading data as we find it, we should gather everything first and then load it all on construction. If there actually is no data and we want to setup generation, then that should also occur in a constructor rather than afterwards.

    This change is only applied to DescriptorScriptPubKeyMan and ExternalSignerScriptPubKeyMan, and should be done for any ScriptPubKeyMans added in the future. I don't think it's really worth it to do this for LegacyScriptPubKeyMan since it would make loading performance worse (or cause layer violations) and it's (supposed to be) going away soon.

  2. DrahtBot commented at 2:55 AM on August 24, 2023: 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/28333.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK polespinasa
    Concept ACK murchandamus
    Approach ACK w0xlt
    Stale ACK rkrux, adyshimony, davidgumberg

    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:

    • #34909 (wallet, refactor: modularise wallet by extracting out legacy wallet migration by rkrux)
    • #34193 (wallet: make migration more robust against failures by furszy)
    • #33112 (wallet: relax external_signer flag constraints, add musig2 test (partial) by Sjors)
    • #32861 (Have createwalletdescriptor auto-detect an unused(KEY) by Sjors)

    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-->

    LLM Linter (✨ experimental)

    Possible places where named args for integral literals may be used (e.g. func(x, /*named_arg=*/0) in C++, and func(x, named_arg=0) in Python):

    • Parse(desc_str, provider, error, false) in src/wallet/scriptpubkeyman.cpp

    Possible places where comparison-specific test macros should replace generic comparisons:

    • [test/functional/wallet_descriptor.py] assert len(key_rows) >= 1 -> use assert_greater_than_or_equal(len(key_rows), 1)

    <sup>2026-04-29 21:19:49</sup>

  3. DrahtBot added the label Wallet on Aug 24, 2023
  4. in src/wallet/wallet.cpp:None in ca729a7d60 outdated
     390 | @@ -391,40 +391,18 @@ std::shared_ptr<CWallet> CreateWallet(WalletContext& context, const std::string&
     391 |          return nullptr;
     392 |      }
     393 |  
     394 | +    // Unset the blank flag if not specified by the user
     395 | +    if (!create_blank) {
    


    S3RK commented at 7:00 AM on August 28, 2023:

    Is this change of behaviour or not?


    achow101 commented at 5:56 PM on August 29, 2023:

    It is not.

    EncryptWallet does not end up calling the SetupGeneration functions when the blank flag is set. Previously, we would set those up manually after encrypting. While making this PR, I realized that we could just unset the blank flag before calling EncryptWallet so that it can do the setup for us and we don't have to do it again afterwards.

  5. unknown approved
  6. DrahtBot added the label CI failed on Sep 3, 2023
  7. DrahtBot removed the label CI failed on Sep 5, 2023
  8. DrahtBot added the label CI failed on Nov 25, 2023
  9. maflcko commented at 1:35 PM on November 27, 2023: member
    wallet/test/fuzz/scriptpubkeyman.cpp: In lambda function:
    wallet/test/fuzz/scriptpubkeyman.cpp:114:67: error: ‘void wallet::DescriptorScriptPubKeyMan::AddDescriptorKey(const CKey&, const CPubKey&)’ is private within this context
      114 |                 spk_manager->AddDescriptorKey(key, key.GetPubKey());
          |                                             
    
  10. achow101 force-pushed on Nov 27, 2023
  11. DrahtBot removed the label CI failed on Nov 27, 2023
  12. DrahtBot added the label Needs rebase on Nov 28, 2023
  13. achow101 force-pushed on Nov 28, 2023
  14. DrahtBot removed the label Needs rebase on Nov 28, 2023
  15. achow101 force-pushed on Dec 7, 2023
  16. DrahtBot added the label Needs rebase on Dec 8, 2023
  17. achow101 force-pushed on Dec 8, 2023
  18. DrahtBot removed the label Needs rebase on Dec 8, 2023
  19. DrahtBot added the label CI failed on Jan 12, 2024
  20. DrahtBot added the label Needs rebase on Feb 20, 2024
  21. achow101 force-pushed on Feb 20, 2024
  22. DrahtBot removed the label Needs rebase on Feb 20, 2024
  23. DrahtBot added the label Needs rebase on Mar 29, 2024
  24. achow101 force-pushed on Mar 29, 2024
  25. DrahtBot removed the label Needs rebase on Mar 29, 2024
  26. achow101 force-pushed on Apr 3, 2024
  27. achow101 commented at 10:41 PM on April 3, 2024: member

    Added a commit so that the fuzzer will skip the type of inputs that was causing the previous failure. This should be safe as that failure is not reachable in normal usage. It can only be reached if the user's wallet is corrupted, and in that case, the runtime error would propagate up to them so there would not be a crash.

  28. DrahtBot removed the label CI failed on Apr 4, 2024
  29. DrahtBot added the label Needs rebase on Jun 3, 2024
  30. achow101 force-pushed on Jun 4, 2024
  31. DrahtBot removed the label Needs rebase on Jun 5, 2024
  32. DrahtBot added the label Needs rebase on Jul 11, 2024
  33. achow101 force-pushed on Jul 22, 2024
  34. DrahtBot removed the label Needs rebase on Jul 22, 2024
  35. DrahtBot added the label Needs rebase on Aug 12, 2024
  36. achow101 force-pushed on Aug 29, 2024
  37. DrahtBot removed the label Needs rebase on Aug 29, 2024
  38. achow101 requested review from murchandamus on Oct 15, 2024
  39. DrahtBot added the label Needs rebase on Oct 24, 2024
  40. achow101 force-pushed on Oct 28, 2024
  41. DrahtBot added the label CI failed on Oct 28, 2024
  42. DrahtBot commented at 7:24 PM on October 28, 2024: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Debug: https://github.com/bitcoin/bitcoin/runs/32177937035</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  43. achow101 force-pushed on Oct 28, 2024
  44. DrahtBot removed the label Needs rebase on Oct 28, 2024
  45. DrahtBot removed the label CI failed on Oct 28, 2024
  46. DrahtBot added the label Needs rebase on Apr 23, 2025
  47. achow101 force-pushed on Apr 24, 2025
  48. DrahtBot removed the label Needs rebase on Apr 24, 2025
  49. DrahtBot added the label Needs rebase on Apr 25, 2025
  50. achow101 force-pushed on Apr 25, 2025
  51. DrahtBot removed the label Needs rebase on Apr 25, 2025
  52. DrahtBot added the label Needs rebase on May 7, 2025
  53. achow101 force-pushed on May 7, 2025
  54. DrahtBot removed the label Needs rebase on May 7, 2025
  55. DrahtBot added the label Needs rebase on May 19, 2025
  56. achow101 force-pushed on May 20, 2025
  57. DrahtBot removed the label Needs rebase on May 20, 2025
  58. in src/wallet/scriptpubkeyman.h:None in 2cee4def97 outdated
     308 | @@ -317,22 +309,33 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan
     309 |      // Fetch the SigningProvider for a given index and optionally include private keys. Called by the above functions.
     310 |      std::unique_ptr<FlatSigningProvider> GetSigningProvider(int32_t index, bool include_private = false) const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
     311 |  
     312 | +    void SetCache(const DescriptorCache& cache);
     313 | +
     314 | +    void AddDescriptorKey(const CKey& key, const CPubKey &pubkey);
     315 | +    void UpdateWithSigningProvider(WalletBatch& batch, const FlatSigningProvider& signing_provider) EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
     316 | +
     317 | +    //! Setup descriptors based on the given CExtkey
    


    DrahtBot commented at 9:35 AM on May 21, 2025:

    CExtkey -> CExtKey [typographical error in type name]

  59. DrahtBot added the label Needs rebase on May 21, 2025
  60. achow101 force-pushed on May 21, 2025
  61. DrahtBot removed the label Needs rebase on May 21, 2025
  62. DrahtBot added the label Needs rebase on Jun 14, 2025
  63. achow101 force-pushed on Jun 14, 2025
  64. DrahtBot removed the label Needs rebase on Jun 14, 2025
  65. rkrux commented at 10:03 AM on July 11, 2025: contributor

    I seem to be getting onboard with this idea as I can notice readability improvements on a cursory glance. Do you anticipate any performance improvements as well?

    The last sentence related to legacy SPKMs of the last paragraph in the PR description can be removed now.

    Will do full review soon.

  66. in src/wallet/wallet.cpp:None in 32dd0e3525 outdated
     424 | +    if (!create_blank) {
     425 | +        wallet->UnsetWalletFlag(WALLET_FLAG_BLANK_WALLET);
     426 | +    }
     427 | +
     428 |      // Encrypt the wallet
     429 |      if (!passphrase.empty() && !(wallet_creation_flags & WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
    


    rkrux commented at 10:49 AM on July 15, 2025:

    In 32dd0e35251973f9bdc2114e70c800624894d489 "wallet: Consolidate generation setup callers into one function"

    While this flow is being cleaned up. There is a check specially for this flag above on line 400 that forbids this situation and the secondary check seems redundant here now.

    - if (!passphrase.empty() && !(wallet_creation_flags & WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
    + if (!passphrase.empty()) {
    

    achow101 commented at 9:44 PM on July 24, 2025:

    Done

  67. in src/wallet/wallet.cpp:None in 32dd0e3525 outdated
     419 | @@ -420,30 +420,18 @@ std::shared_ptr<CWallet> CreateWallet(WalletContext& context, const std::string&
     420 |          return nullptr;
     421 |      }
     422 |  
     423 | +    // Unset the blank flag if not specified by the user
     424 | +    if (!create_blank) {
    


    rkrux commented at 10:54 AM on July 15, 2025:

    In https://github.com/bitcoin/bitcoin/commit/32dd0e35251973f9bdc2114e70c800624894d489 "wallet: Consolidate generation setup callers into one function"

    Fine as-is but perhaps this block can be put inside the if block below because from what I see if not set by the user, this flag can only be set in case of passphrase not being empty on line 388. This wrapped inside a tighter constraint such as the one below looks better to me.


    achow101 commented at 9:24 PM on July 24, 2025:

    The blank flag needs to be unset before EncryptWallet so that EncryptWallet can generate the new keys after encrypting. Otherwise, the newly created wallet will be encrypted but blank, which is not what we want in this scenario.


    rkrux commented at 7:28 AM on July 25, 2025:

    I had something like this in mind and I should have left a diff earlier for clarity. It seemed to me that only for encrypted wallets the blank flag was set by the code & hence, believed this flag unsetting could be nested inside the below condition.

    diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
    index bd1b36b4a9..5a3ecc1d19 100644
    --- a/src/wallet/wallet.cpp
    +++ b/src/wallet/wallet.cpp
    @@ -381,9 +381,6 @@ std::shared_ptr<CWallet> CreateWallet(WalletContext& context, const std::string&
         Assert(wallet_creation_flags & WALLET_FLAG_DESCRIPTORS);
         options.require_format = DatabaseFormat::SQLITE;
     
    -    // Indicate that the wallet is actually supposed to be blank and not just blank to make it encrypted
    -    bool create_blank = (wallet_creation_flags & WALLET_FLAG_BLANK_WALLET);
    -
         // Born encrypted wallets need to be created blank first.
         if (!passphrase.empty()) {
             wallet_creation_flags |= WALLET_FLAG_BLANK_WALLET;
    @@ -420,13 +417,10 @@ std::shared_ptr<CWallet> CreateWallet(WalletContext& context, const std::string&
             return nullptr;
         }
     
    -    // Unset the blank flag if not specified by the user
    -    if (!create_blank) {
    -        wallet->UnsetWalletFlag(WALLET_FLAG_BLANK_WALLET);
    -    }
    -
         // Encrypt the wallet
         if (!passphrase.empty()) {
    +        // Unset the blank flag that was purposefully set above for encrypted wallets
    +        wallet->UnsetWalletFlag(WALLET_FLAG_BLANK_WALLET);
             if (!wallet->EncryptWallet(passphrase)) {
                 error = Untranslated("Error: Wallet created but failed to encrypt.");
                 status = DatabaseStatus::FAILED_ENCRYPT;
    
    

    But I check now that this fails the test for intentionally blank encrypted wallets. So, this suggestion is incorrect and need not be implemented.

    def test_encrypted(self):
            self.log.info("Test createwalletdescriptor with encrypted wallets")
            def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
            self.nodes[0].createwallet("encrypted", blank=True, passphrase="pass")
    
  68. in src/wallet/wallet.cpp:None in 32dd0e3525 outdated
    3585 | +        (IsWalletFlagSet(WALLET_FLAG_BLANK_WALLET) || IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS))) {
    3586 | +        return true;
    3587 | +    }
    3588 | +    SetupDescriptorScriptPubKeyMans();
    3589 | +    return true;
    3590 | +}
    


    rkrux commented at 11:01 AM on July 15, 2025:

    In https://github.com/bitcoin/bitcoin/commit/32dd0e35251973f9bdc2114e70c800624894d489 "wallet: Consolidate generation setup callers into one function"

    This never seems to return false. Do we need to check for a falsy value while calling it like done above? Maybe not even return a bool? Just a thrown exception that is bubbled up.


    achow101 commented at 9:44 PM on July 24, 2025:

    Made void

  69. in src/wallet/walletdb.cpp:None in fdba094a67 outdated
     901 |          });
     902 |          result = std::max(result, key_res.m_result);
     903 |          num_keys = key_res.m_records;
     904 |  
     905 |          // Get encrypted keys
     906 | +        std::map<CKeyID, std::pair<CPubKey, std::vector<unsigned char>>> ckeys;
    


    rkrux commented at 11:23 AM on July 15, 2025:

    In fdba094a67906cc89918661e24c9c9a5e3f4cb0b "wallet: Load everything into DescSPKM on construction"

    - std::map<CKeyID, std::pair<CPubKey, std::vector<unsigned char>>> ckeys;
    + CryptedKeyMap ckeys;
    

    achow101 commented at 9:44 PM on July 24, 2025:

    Done

  70. in src/wallet/walletdb.cpp:None in fdba094a67 outdated
     859 | -        spk_man->SetCache(cache);
     860 | +        // Set the cache to the WalletDescriptor
     861 | +        desc.cache = cache;
     862 |  
     863 |          // Get unencrypted keys
     864 | +        std::map<CKeyID, CKey> keys;
    


    rkrux commented at 11:24 AM on July 15, 2025:

    In fdba094a67906cc89918661e24c9c9a5e3f4cb0b "wallet: Load everything into DescSPKM on construction"

    - std::map<CKeyID, CKey> keys;
    + KeyMap keys;
    

    achow101 commented at 9:44 PM on July 24, 2025:

    Done

  71. in src/wallet/scriptpubkeyman.h:None in fdba094a67 outdated
     172 | @@ -173,15 +173,18 @@ static const std::unordered_set<OutputType> LEGACY_OUTPUT_TYPES {
     173 |      OutputType::BECH32,
     174 |  };
     175 |  
     176 | +using WatchOnlySet = std::set<CScript>;
     177 | +using WatchKeyMap = std::map<CKeyID, CPubKey>;
     178 | +using KeyMap = std::map<CKeyID, CKey>;
     179 | +using CryptedKeyMap = std::map<CKeyID, std::pair<CPubKey, std::vector<unsigned char>>>;
     180 | +using ScriptPubKeyMap = std::map<CScript, int32_t>; // Map of scripts to descriptor range index
     181 | +using PubKeyMap = std::map<CPubKey, int32_t>; // Map of pubkeys involved in scripts to descriptor range index
    


    rkrux commented at 11:26 AM on July 15, 2025:

    In fdba094a67906cc89918661e24c9c9a5e3f4cb0b "wallet: Load everything into DescSPKM on construction"

    Some of them are now being used in wallet.cpp & external_signer_scriptpubkeyman.h and I suggested using couple in walletdb.cpp too. Maybe they can now go in a utility file like walletutil.h?


    achow101 commented at 9:44 PM on July 24, 2025:

    I think leaving them here is fine.

  72. in src/wallet/scriptpubkeyman.h:None in fdba094a67 outdated
     172 | @@ -173,15 +173,18 @@ static const std::unordered_set<OutputType> LEGACY_OUTPUT_TYPES {
     173 |      OutputType::BECH32,
     174 |  };
     175 |  
     176 | +using WatchOnlySet = std::set<CScript>;
     177 | +using WatchKeyMap = std::map<CKeyID, CPubKey>;
    


    rkrux commented at 11:32 AM on July 15, 2025:

    In https://github.com/bitcoin/bitcoin/commit/fdba094a67906cc89918661e24c9c9a5e3f4cb0b "wallet: Load everything into DescSPKM on construction"

    I feel these 2 can be kept inside LegacyDataSPKM itself, they are not used anywhere else, and I don't suppose they will be as well in the future.


    achow101 commented at 9:45 PM on July 24, 2025:

    Done

  73. in src/wallet/scriptpubkeyman.h:None in 93c3729ad5 outdated
     331 | @@ -324,10 +332,8 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan
     332 |      DescriptorScriptPubKeyMan(WalletBatch& batch, WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const FlatSigningProvider& provider);
     333 |      //! Create a DescriptorScriptPubKeyMan from existing data (i.e. during loading)
     334 |      DescriptorScriptPubKeyMan(WalletStorage& storage, const uint256& id, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys);
     335 | -    DescriptorScriptPubKeyMan(WalletStorage& storage, int64_t keypool_size)
     336 | -        :   ScriptPubKeyMan(storage),
     337 | -            m_keypool_size(keypool_size)
     338 | -        {}
     339 | +    //! Create an automatically generated DescriptorScriptPubKeyMan
    


    rkrux commented at 11:53 AM on July 15, 2025:

    In 93c3729ad5adc02f6d87ec8daa1a1952d7c41009 "wallet: Setup new autogenerated descriptors on construction"

    - //! Create an automatically generated DescriptorScriptPubKeyMan
    + //! Create an automatically generated DescriptorScriptPubKeyMan (i.e. during wallet or descriptor creation)
    

    achow101 commented at 9:45 PM on July 24, 2025:

    Done

  74. in src/wallet/scriptpubkeyman.h:None in 93c3729ad5 outdated
     331 | @@ -324,10 +332,8 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan
     332 |      DescriptorScriptPubKeyMan(WalletBatch& batch, WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const FlatSigningProvider& provider);
    


    rkrux commented at 11:56 AM on July 15, 2025:

    In https://github.com/bitcoin/bitcoin/commit/93c3729ad5adc02f6d87ec8daa1a1952d7c41009 "wallet: Setup new autogenerated descriptors on construction"

    This will make storage the first argument in all the 4 constructors and this consistency would be nice for readability imo.

    - DescriptorScriptPubKeyMan(WalletBatch& batch, WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const FlatSigningProvider& provider);
    + DescriptorScriptPubKeyMan(WalletStorage& storage, WalletBatch& batch, WalletDescriptor& descriptor, int64_t keypool_size, const FlatSigningProvider& provider);
    

    achow101 commented at 9:45 PM on July 24, 2025:

    Done

  75. rkrux commented at 12:19 PM on July 15, 2025: contributor

    Concept ACK 93c3729ad5adc02f6d87ec8daa1a1952d7c41009

    This PR makes DescriptorScriptPubKeyMan class RAII compliant by adding 4 constructors that are responsible for allocating all the resources (eg: memory) required by this class during initialisation itself. I feel that it improves the readability quite a lot as the reader can easily gauge what all is needed by this class at one place and there is no need for multiple scattered class-write function calls like the one below that used to appear before.

    - WITH_LOCK(desc_spk_man->cs_desc_man, desc_spk_man->AddDescriptorKeyWithDB(batch, key, key.GetPubKey()));
    - desc_spk_man->TopUpWithDB(batch);
    

    I noticed that this style now allows us to add more error checking that makes the code more robust imo.

    Error: Wallet contains both unencrypted and encrypted keys
    

    Perhaps a Refactoring label is required in the PR?

  76. achow101 force-pushed on Jul 24, 2025
  77. rkrux approved
  78. rkrux commented at 7:48 AM on July 25, 2025: contributor

    ACK 8e9de762f66f3e99e02944006999fe5afb1896e4

    Thanks for addressing comments.

    git range-diff 93c3729...8e9de76
    
  79. achow101 requested review from davidgumberg on Oct 22, 2025
  80. DrahtBot added the label Needs rebase on Dec 27, 2025
  81. achow101 force-pushed on Jan 2, 2026
  82. DrahtBot removed the label Needs rebase on Jan 2, 2026
  83. in src/wallet/scriptpubkeyman.cpp:861 in 3c4f53d5e1 outdated
     856 | +
     857 | +DescriptorScriptPubKeyMan::DescriptorScriptPubKeyMan(WalletStorage& storage, WalletBatch& batch, int64_t keypool_size, const CExtKey& master_key, OutputType addr_type, bool internal)
     858 | +    :   ScriptPubKeyMan(storage),
     859 | +        m_keypool_size(keypool_size)
     860 | +{
     861 | +    SetupDescriptorGeneration(batch, master_key, addr_type, internal);
    


    w0xlt commented at 2:54 AM on February 12, 2026:

    nit: SetupDescriptorGeneration return value is being silently ignored.

        Assume(SetupDescriptorGeneration(batch, master_key, addr_type, internal));
    

    polespinasa commented at 10:25 AM on February 18, 2026:

    Wouldn't Assume here abort the program if this is hit:

    
        // Ignore when there is already a descriptor
        if (m_wallet_descriptor.descriptor) {
            return false;
        }
    

    It doesn't look like a reason to abort the program. It was already ignored in the past before this changes https://github.com/bitcoin/bitcoin/blob/59e10a5463dc940ecd88e6961737f634416de6d5/src/wallet/wallet.cpp#L3607

    Maybe could just be a void. I tested it and doesn't seem to break anything.

    <details>

    $ git diff
    diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp
    index 8a1c0a45a5..60302110ad 100644
    --- a/src/wallet/scriptpubkeyman.cpp
    +++ b/src/wallet/scriptpubkeyman.cpp
    @@ -1132,14 +1132,14 @@ bool DescriptorScriptPubKeyMan::AddDescriptorKeyWithDB(WalletBatch& batch, const
         }
     }
     
    -bool DescriptorScriptPubKeyMan::SetupDescriptorGeneration(WalletBatch& batch, const CExtKey& master_key, OutputType addr_type, bool internal)
    +void DescriptorScriptPubKeyMan::SetupDescriptorGeneration(WalletBatch& batch, const CExtKey& master_key, OutputType addr_type, bool internal)
     {
         LOCK(cs_desc_man);
         assert(m_storage.IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
     
         // Ignore when there is already a descriptor
         if (m_wallet_descriptor.descriptor) {
    -        return false;
    +        return;
         }
     
         m_wallet_descriptor = GenerateWalletDescriptor(master_key.Neuter(), addr_type, internal);
    @@ -1156,7 +1156,6 @@ bool DescriptorScriptPubKeyMan::SetupDescriptorGeneration(WalletBatch& batch, co
         TopUpWithDB(batch);
     
         m_storage.UnsetBlankWalletFlag(batch);
    -    return true;
     }
     
     bool DescriptorScriptPubKeyMan::IsHDEnabled() const
    diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h
    index 00dd6eed4d..f4e1c8b053 100644
    --- a/src/wallet/scriptpubkeyman.h
    +++ b/src/wallet/scriptpubkeyman.h
    @@ -357,7 +357,7 @@ public:
         bool IsHDEnabled() const override;
     
         //! Setup descriptors based on the given CExtkey
    -    bool SetupDescriptorGeneration(WalletBatch& batch, const CExtKey& master_key, OutputType addr_type, bool internal);
    +    void SetupDescriptorGeneration(WalletBatch& batch, const CExtKey& master_key, OutputType addr_type, bool internal);
     
         bool HavePrivateKeys() const override;
         bool HasPrivKey(const CKeyID& keyid) const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
    
    

    <\details>


    achow101 commented at 10:32 PM on March 23, 2026:

    Changed to void

  84. in src/wallet/scriptpubkeyman.cpp:848 in 3c4f53d5e1 outdated
     843 | +    m_map_keys(keys),
     844 | +    m_map_crypted_keys(ckeys),
     845 | +    m_keypool_size(keypool_size),
     846 | +    m_wallet_descriptor(descriptor)
     847 | +{
     848 | +    if (!m_map_keys.empty() && !m_map_crypted_keys.empty()) {
    


    w0xlt commented at 3:02 AM on February 12, 2026:

    nit: Consistency with the other constructor bodies.

        LOCK(cs_desc_man);
        if (!m_map_keys.empty() && !m_map_crypted_keys.empty()) {
    

    achow101 commented at 10:09 PM on March 23, 2026:

    I don't think that "consistency" is a good reason to be acquiring the lock when it is entirely unnecessary. The other constructors need to acquire the lock because they call functions that do AssertLockHeld.

  85. w0xlt commented at 3:03 AM on February 12, 2026: contributor

    Approach ACK

  86. adyshimony commented at 12:32 AM on February 14, 2026: none

    ACK 3c4f53d5e1ea30d9a2a4bd1d34501ce419d2aebf

    I agree this is a clearer and safer approach. Reviewed and tested.

    Tests: /build/bin/test_bitcoin /build/test/functional/test_runner.py wallet_createwallet.py wallet_encryption.py wallet_descriptor.py wallet_importdescriptors.py wallet_signer.py wallet_blank.py wallet_keypool_topup.py

  87. DrahtBot requested review from w0xlt on Feb 14, 2026
  88. DrahtBot requested review from rkrux on Feb 14, 2026
  89. rkrux commented at 2:33 PM on February 16, 2026: contributor

    Reviewing again.

  90. in src/wallet/wallet.cpp:428 in 6d299761a2 outdated
     421 | @@ -422,30 +422,18 @@ std::shared_ptr<CWallet> CreateWallet(WalletContext& context, const std::string&
     422 |          return nullptr;
     423 |      }
     424 |  
     425 | +    // Unset the blank flag if not specified by the user
     426 | +    if (!create_blank) {
     427 | +        wallet->UnsetWalletFlag(WALLET_FLAG_BLANK_WALLET);
     428 | +    }
    


    polespinasa commented at 9:18 AM on February 18, 2026:

    Just a question, in 6d299761a28ec299f21897c4fac57ba3bfc0762f

    Why could the wallet have the flag set and the user not specify it?


    murchandamus commented at 4:51 PM on March 13, 2026:

    This is probably out-of-scope for this PR, but I find the existence of create_blank confusing in general. I’m wondering whether we could avoid surfacing create_blank to the user in general. From what I understand, every wallet starts blank and every user flow needs to add some descriptor to the wallet before it becomes usable for wallet purposes. Wouldn’t it make more sense to optionally allow the creation of a new wallet when you import a descriptor instead of putting the onus on the user to know that they need to create a blank wallet first?

    Then users would either:

    • Create a new wallet (with SPKMs)
    • Import a descriptor to an existing wallet
    • Import a descriptor to create a new wallet that only contains the descriptor

    This way it would always be obvious whether we use custom SPKMs or need to generate fresh ones and blank wallets would be an implementation detail rather than a user interface option. Would there be any other reason why users would want to create blank wallets? (Ignoring for a moment that it would change the user flow existing users have come to expect.)


    achow101 commented at 10:15 PM on March 23, 2026:

    Why could the wallet have the flag set and the user not specify it?

    The flag can be set without the user having done so because that's how born-encrypted wallets are implemented. We need to tell wallet creation to not generate any descriptors (or keys, back when this was originally implemented) so that the wallet can then be encrypted, and the descriptors generated afterwards. Since creating a wallet with no descriptors in it is also useful to users, this was made into a wallet flag so that users could also explicitly create such a wallet.

    Wouldn’t it make more sense to optionally allow the creation of a new wallet when you import a descriptor instead of putting the onus on the user to know that they need to create a blank wallet first?

    Sure, but that's way out of scope.


    davidgumberg commented at 10:21 PM on April 22, 2026:

    I think born encrypted as an overloaded meaning of the blank flag is kind of a dangerous footgun and a bit hard to comprehend, I haven't checked but I think I recall that this was a lot easier to implement the way it is now in an earlier version of the wallet code, but today it would be a pretty small diff that I think is in scope here to fix this: https://github.com/davidgumberg/bitcoin/commit/130af745309e19934388de2ca1c684b687f9f625


    achow101 commented at 10:33 PM on April 23, 2026:

    Pulled in that commit

  91. in src/wallet/walletdb.cpp:795 in 82fbca46e7 outdated
     790 | -
     791 | -        // Prior to doing anything with this spkm, verify ID compatibility
     792 | -        if (id != spkm.GetID()) {
     793 | -            strErr = "The descriptor ID calculated by the wallet differs from the one in DB";
     794 | -            return DBErrors::CORRUPT;
     795 | -        }
    


    polespinasa commented at 9:50 AM on February 18, 2026:

    In 82fbca46e7bfcc5cec52b8b5902adb55ea538d46

    Why move this check at the end inside pwallet->LoadDescriptorScriptPubKeyMan(id, desc, keys, ckeys); ? Wouldn't we avoid doing extra useless job if keeping this here?


    achow101 commented at 10:32 PM on March 23, 2026:

    Good point, moved the ID check to after deserializing the WalletDescriptor

  92. in src/wallet/scriptpubkeyman.cpp:606 in 32fb9ffdd1 outdated
     602 | @@ -603,9 +603,8 @@ std::optional<MigrationData> LegacyDataSPKM::MigrateToDescriptor()
     603 |          WalletDescriptor w_desc(std::move(descs.at(0)), creation_time, 0, 0, 0);
     604 |  
     605 |          // Make the DescriptorScriptPubKeyMan and get the scriptPubKeys
     606 | -        auto desc_spk_man = std::make_unique<DescriptorScriptPubKeyMan>(m_storage, w_desc, /*keypool_size=*/0);
     607 | -        WITH_LOCK(desc_spk_man->cs_desc_man, desc_spk_man->AddDescriptorKeyWithDB(batch, key, key.GetPubKey()));
     608 | -        desc_spk_man->TopUpWithDB(batch);
     609 | +        keys.keys.emplace(key.GetPubKey().GetID(), key);
    


    polespinasa commented at 9:57 AM on February 18, 2026:

    in 32fb9ffdd1ee0ffbad6a65640ebef2e90b09991e

    nit: I know this is not this PR fault but keys.keys is not clear and makes this difficult to understand. Probably FlatSigningProvider variable should have another name?


    achow101 commented at 10:22 PM on March 23, 2026:

    Will leave for a different PR


    davidgumberg commented at 1:29 AM on April 23, 2026:

    nit: Given that the offending is being added here, I think it's in scope for this PR, but if you refuse I will still ACK :)


    achow101 commented at 9:13 PM on April 23, 2026:

    The line that creates keys is not in this PR.


    davidgumberg commented at 9:25 PM on April 23, 2026:

    Sure, but the confusing line is being added in this PR, and every user of keys is being touched here anyways.


    achow101 commented at 10:33 PM on April 23, 2026:

    Fine, done.

  93. polespinasa commented at 10:30 AM on February 18, 2026: member

    Approach ACK This is much more clean and clear than before.

    code reviewed lgtm 3c4f53d5e1ea30d9a2a4bd1d34501ce419d2aebf

    I just have some questions and left some non-blocking nits before ACKing it.

  94. in src/wallet/wallet.cpp:3601 in 6d299761a2 outdated
    3593 | @@ -3609,6 +3594,17 @@ void CWallet::SetupDescriptorScriptPubKeyMans()
    3594 |      }
    3595 |  }
    3596 |  
    3597 | +void CWallet::SetupWalletGeneration()
    3598 | +{
    3599 | +    LOCK(cs_wallet);
    3600 | +    // Skip if blank or no privkeys
    3601 | +    if (!IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER) &&
    


    murchandamus commented at 5:26 PM on March 13, 2026:

    Wouldn’t WALLET_FLAG_EXTERNAL_SIGNER indicate that there are no privkeys? So, why don’t we skip on WALLET_FLAG_EXTERNAL_SIGNER true?

    Is the comment here wrong?


    achow101 commented at 10:30 PM on March 23, 2026:

    We don't skip because Setup for external signers invokes the external signer to get descriptors.

    Updated the comment to be more explicit

  95. murchandamus commented at 6:22 PM on March 13, 2026: member

    Concept ACK

  96. achow101 force-pushed on Mar 23, 2026
  97. in src/wallet/wallet.cpp:433 in 5767a58f5e outdated
     433 | +    if (!create_blank) {
     434 | +        wallet->UnsetWalletFlag(WALLET_FLAG_BLANK_WALLET);
     435 | +    }
     436 | +
     437 |      // Encrypt the wallet
     438 | -    if (!passphrase.empty() && !(wallet_creation_flags & WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
    


    davidgumberg commented at 7:55 PM on April 22, 2026:

    In commit https://github.com/bitcoin/bitcoin/pull/28333/changes/5767a58f5e6001d6a59505cef3c6796db98fdf69 (wallet: Consolidate generation setup callers into one function):

    Just a note for reviewers: this is removed because it's redundant with the check above:

    https://github.com/bitcoin/bitcoin/blob/8a05adc5f81da483fb4e1b3592acb3e274014298/src/wallet/wallet.cpp#L409


    polespinasa commented at 4:40 PM on April 27, 2026:

    I don't think this is a good idea. Someone could call CreateNew with is_encrypted=true and WALLET_FLAG_DISABLE_PRIVATE_KEYS set.

    I think a safer approach is to add an assertion to ensure it cannot happen. assert(!passphrase.empty() || !(wallet_creation_flags & WALLET_FLAG_DISABLE_PRIVATE_KEYS))


    achow101 commented at 8:59 PM on April 27, 2026:

    CreateNew isn't exposed to the world, it's hard for someone to just call it. There's already a check above, before CreateNew for this condition, that's why this can be dropped.

  98. in src/wallet/wallet.cpp:455 in 5767a58f5e outdated
     456 | -                wallet->SetupDescriptorScriptPubKeyMans();
     457 | -            }
     458 | -
     459 | -            // Relock the wallet
     460 | -            wallet->Lock();
     461 | -        }
    


    davidgumberg commented at 7:58 PM on April 22, 2026:

    In commit https://github.com/bitcoin/bitcoin/pull/28333/changes/5767a58f5e6001d6a59505cef3c6796db98fdf69 (wallet: Consolidate generation setup callers into one function):

    For reviewers: This can be dropped because all of this is performed in EncryptWallet().

  99. in src/wallet/wallet.cpp:454 in 5767a58f5e outdated
     455 | -                LOCK(wallet->cs_wallet);
     456 | -                wallet->SetupDescriptorScriptPubKeyMans();
     457 | -            }
     458 | -
     459 | -            // Relock the wallet
     460 | -            wallet->Lock();
    


    davidgumberg commented at 10:36 PM on April 22, 2026:

    In https://github.com/bitcoin/bitcoin/pull/28333/changes/5767a58f5e6001d6a59505cef3c6796db98fdf69#r3127213366 (wallet: Consolidate generation setup callers into one function):

    This commit is almost a strict refactor but there are two small behavior changes:

    1. The case where a wallet is encrypted and then fails to unlock no longer has an explicit check / error message in create.
    • This seems unlikely, but maybe as a sanity check we should keep the check that unlocking works.
    • Code smell that no tests break when this behavior is changed.
    1. Before the born-encrypted wallet was unlocked, locked, then unlocked, then setup descriptors, then locked, now the first no-op unlock, lock is deleted.
    • These should be 100% equivalent in effect, but it is different behavior just to be thorough.

    refactor: prefix maybe?


    achow101 commented at 10:33 PM on April 23, 2026:

    Code smell that no tests break when this behavior is changed.

    Because it is untestable. There's no way to contrive a test where unlocking immediately after locking results in a failure. The only way that this can happen is if on-disk data is immediately corrupted.

    I changed the Unlock sanity check in EncryptWallet to return false if it fails to preserve the sanity check. But as with before, this is not testable.

    refactor: prefix maybe?

    Done


    davidgumberg commented at 1:49 AM on April 24, 2026:

    Because it is untestable. There's no way to contrive a test where unlocking immediately after locking results in a failure. The only way that this can happen is if on-disk data is immediately corrupted.

    Yeah that makes sense.

    I changed the Unlock sanity check in EncryptWallet to return false if it fails to preserve the sanity check. But as with before, this is not testable.

    I think this was missed


    achow101 commented at 7:48 PM on April 24, 2026:

    Oops, done

  100. in src/wallet/walletdb.cpp:775 in 0f294cbb6a outdated
     770 | @@ -771,10 +771,8 @@ static DBErrors LoadDescriptorWalletRecords(CWallet* pwallet, DatabaseBatch& bat
     771 |              strErr = strprintf("%s\nDetails: %s", strErr, e.what());
     772 |              return DBErrors::UNKNOWN_DESCRIPTOR;
     773 |          }
     774 | -        DescriptorScriptPubKeyMan& spkm = pwallet->LoadDescriptorScriptPubKeyMan(id, desc);
     775 |  
     776 | -        // Prior to doing anything with this spkm, verify ID compatibility
     777 | -        if (id != spkm.GetID()) {
     778 | +        if (id != desc.id) {
    


    davidgumberg commented at 11:05 PM on April 22, 2026:

    In commit https://github.com/bitcoin/bitcoin/pull/28333/changes/0f294cbb6a255e7c7f72985feffb08a6a27f0458 (wallet: Load everything into DescSPKM on construction):

    Note for other reviewers, this diff could have been done outside of any of the changes in this PR as the id was already computed above at deserialization:

    https://github.com/bitcoin/bitcoin/blob/8a05adc5f81da483fb4e1b3592acb3e274014298/src/wallet/walletutil.h#L86

  101. in src/wallet/scriptpubkeyman.cpp:832 in 0f294cbb6a outdated
     827 | +    m_map_crypted_keys(ckeys),
     828 | +    m_keypool_size(keypool_size),
     829 | +    m_wallet_descriptor(descriptor)
     830 | +{
     831 | +    if (!m_map_keys.empty() && !m_map_crypted_keys.empty()) {
     832 | +        throw std::runtime_error("Error: Wallet contains both unencrypted and encrypted keys");
    


    davidgumberg commented at 1:20 AM on April 23, 2026:

    achow101 commented at 10:33 PM on April 23, 2026:

    Pulled in the commit


    polespinasa commented at 4:50 PM on April 27, 2026:

    in f4894a8b5cf133ef1958348c820b8f6b1fb37ad5 wallet: Load everything into DescSPKM on construction

    nit: Seems that we don't add Error: prefix in front of runtime errors (have only seen 2-3 examples in wallet code). Probably is better to drop it for consistency.

    $ git grep "std::runtime_error" | grep "Error:"
    src/util/subprocess.h:class CalledProcessError: public std::runtime_error
    src/util/subprocess.h:class OSError: public std::runtime_error
    src/wallet/scriptpubkeyman.cpp:        throw std::runtime_error("Error: Wallet contains both unencrypted and encrypted keys");
    src/wallet/scriptpubkeyman.cpp:            throw std::runtime_error("Error: Unable to expand wallet descriptor from cache");
    src/wallet/scriptpubkeyman.cpp:                throw std::runtime_error(strprintf("Error: Already loaded script at index %d as being at index %d", i, m_map_script_pub_keys[script]));
    src/wallet/wallet.cpp:        })) throw std::runtime_error("Error: cannot process db transaction for descriptors setup");
    src/wallet/wallet.cpp:        if (!batch.TxnBegin()) throw std::runtime_error("Error: cannot create db transaction for descriptors import");
    src/wallet/wallet.cpp:        if (!batch.TxnCommit()) throw std::runtime_error("Error: cannot commit db transaction for descriptors import");
    

    achow101 commented at 1:03 AM on April 28, 2026:

    Done

  102. in src/wallet/scriptpubkeyman.cpp:834 in 0f294cbb6a
     829 | +    m_wallet_descriptor(descriptor)
     830 | +{
     831 | +    if (!m_map_keys.empty() && !m_map_crypted_keys.empty()) {
     832 | +        throw std::runtime_error("Error: Wallet contains both unencrypted and encrypted keys");
     833 | +    }
     834 | +    SetCache(m_wallet_descriptor.cache);
    


    davidgumberg commented at 6:25 PM on April 23, 2026:

    Maybe to ~enforce that callers are actually setting up the cache add the cache as an argument to the constructor?

    <details>

    <summary>

    diff

    </summary>

    diff --git a/src/wallet/external_signer_scriptpubkeyman.h b/src/wallet/external_signer_scriptpubkeyman.h
    index c0bdb037d8..6c181500d3 100644
    --- a/src/wallet/external_signer_scriptpubkeyman.h
    +++ b/src/wallet/external_signer_scriptpubkeyman.h
    @@ -16,8 +16,8 @@ namespace wallet {
     class ExternalSignerScriptPubKeyMan : public DescriptorScriptPubKeyMan
     {
       public:
    -  ExternalSignerScriptPubKeyMan(WalletStorage& storage, const uint256& id, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys)
    -      :   DescriptorScriptPubKeyMan(storage, id, descriptor, keypool_size, keys, ckeys)
    +  ExternalSignerScriptPubKeyMan(WalletStorage& storage, const uint256& id, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys, const DescriptorCache& cache)
    +      :   DescriptorScriptPubKeyMan(storage, id, descriptor, keypool_size, keys, ckeys, cache)
           {}
       ExternalSignerScriptPubKeyMan(WalletStorage& storage, int64_t keypool_size)
           :   DescriptorScriptPubKeyMan(storage, keypool_size)
    diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp
    index cf0d0033e5..a49cc99f1d 100644
    --- a/src/wallet/scriptpubkeyman.cpp
    +++ b/src/wallet/scriptpubkeyman.cpp
    @@ -821,7 +821,7 @@ bool LegacyDataSPKM::DeleteRecordsWithDB(WalletBatch& batch)
         return batch.EraseRecords(DBKeys::LEGACY_TYPES);
     }
     
    -DescriptorScriptPubKeyMan::DescriptorScriptPubKeyMan(WalletStorage& storage, const uint256& id, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys)
    +DescriptorScriptPubKeyMan::DescriptorScriptPubKeyMan(WalletStorage& storage, const uint256& id, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys, const DescriptorCache& cache)
         : ScriptPubKeyMan(storage),
         m_map_keys(keys),
         m_map_crypted_keys(ckeys),
    diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h
    index 096e4a57b4..42be4d8f39 100644
    --- a/src/wallet/scriptpubkeyman.h
    +++ b/src/wallet/scriptpubkeyman.h
    @@ -330,7 +330,7 @@ public:
                 m_wallet_descriptor(descriptor)
             {}
         //! Create a DescriptorScriptPubKeyMan from existing data (i.e. during loading)
    -    DescriptorScriptPubKeyMan(WalletStorage& storage, const uint256& id, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys);
    +    DescriptorScriptPubKeyMan(WalletStorage& storage, const uint256& id, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys, const DescriptorCache& cache);
         DescriptorScriptPubKeyMan(WalletStorage& storage, int64_t keypool_size)
             :   ScriptPubKeyMan(storage),
                 m_keypool_size(keypool_size)
    diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp
    index 2eb3242835..3b02856cbb 100644
    --- a/src/wallet/wallet.cpp
    +++ b/src/wallet/wallet.cpp
    @@ -3546,13 +3546,13 @@ void CWallet::ConnectScriptPubKeyManNotifiers()
         }
     }
     
    -void CWallet::LoadDescriptorScriptPubKeyMan(uint256 id, WalletDescriptor& desc, const KeyMap& keys, const CryptedKeyMap& ckeys)
    +void CWallet::LoadDescriptorScriptPubKeyMan(uint256 id, WalletDescriptor& desc, const KeyMap& keys, const CryptedKeyMap& ckeys, const DescriptorCache& cache)
     {
         DescriptorScriptPubKeyMan* spk_manager;
         if (IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER)) {
    -        spk_manager = new ExternalSignerScriptPubKeyMan(*this, id, desc, m_keypool_size, keys, ckeys);
    +        spk_manager = new ExternalSignerScriptPubKeyMan(*this, id, desc, m_keypool_size, keys, ckeys, cache);
         } else {
    -        spk_manager = new DescriptorScriptPubKeyMan(*this, id, desc, m_keypool_size, keys, ckeys);
    +        spk_manager = new DescriptorScriptPubKeyMan(*this, id, desc, m_keypool_size, keys, ckeys, cache);
         }
         AddScriptPubKeyMan(id, std::unique_ptr<ScriptPubKeyMan>(spk_manager));
     }
    diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h
    index df2659739e..7dec6d72dd 100644
    --- a/src/wallet/wallet.h
    +++ b/src/wallet/wallet.h
    @@ -1004,7 +1004,7 @@ public:
         void ConnectScriptPubKeyManNotifiers();
     
         //! Instantiate a descriptor ScriptPubKeyMan from the WalletDescriptor and load it
    -    void LoadDescriptorScriptPubKeyMan(uint256 id, WalletDescriptor& desc, const KeyMap& keys, const CryptedKeyMap& ckeys);
    +    void LoadDescriptorScriptPubKeyMan(uint256 id, WalletDescriptor& desc, const KeyMap& keys, const CryptedKeyMap& ckeys, const DescriptorCache& cache);
     
         //! Adds the active ScriptPubKeyMan for the specified type and internal. Writes it to the wallet file
         //! [@param](/bitcoin-bitcoin/contributor/param/)[in] id The unique id for the ScriptPubKeyMan
    diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp
    index b9ca9214e2..ca15df0043 100644
    --- a/src/wallet/walletdb.cpp
    +++ b/src/wallet/walletdb.cpp
    @@ -831,9 +831,6 @@ static DBErrors LoadDescriptorWalletRecords(CWallet* pwallet, DatabaseBatch& bat
             });
             result = std::max(result, lh_cache_res.m_result);
     
    -        // Set the cache to the WalletDescriptor
    -        desc.cache = cache;
    -
             // Get unencrypted keys
             KeyMap keys;
             prefix = PrefixStream(DBKeys::WALLETDESCRIPTORKEY, id);
    @@ -901,7 +898,7 @@ static DBErrors LoadDescriptorWalletRecords(CWallet* pwallet, DatabaseBatch& bat
             num_ckeys = ckey_res.m_records;
     
             try {
    -            pwallet->LoadDescriptorScriptPubKeyMan(id, desc, keys, ckeys);
    +            pwallet->LoadDescriptorScriptPubKeyMan(id, desc, keys, ckeys, cache);
             } catch (std::runtime_error& e) {
                 strErr = e.what();
                 return DBErrors::CORRUPT;
    

    </details>

    Alternatively... drop the argument from SetCache() since this is it's only caller and it uses DSPKM state anyways:

    <details>

    <summary>

    diff

    </summary>

    diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp
    index cf0d0033e5..67bdb5597d 100644
    --- a/src/wallet/scriptpubkeyman.cpp
    +++ b/src/wallet/scriptpubkeyman.cpp
    @@ -831,7 +831,7 @@ DescriptorScriptPubKeyMan::DescriptorScriptPubKeyMan(WalletStorage& storage, con
         if (!m_map_keys.empty() && !m_map_crypted_keys.empty()) {
             throw std::runtime_error("Error: Wallet contains both unencrypted and encrypted keys");
         }
    -    SetCache(m_wallet_descriptor.cache);
    +    SetCache();
     }
     
     util::Result<CTxDestination> DescriptorScriptPubKeyMan::GetNewDestination(const OutputType type)
    @@ -1439,11 +1439,10 @@ uint256 DescriptorScriptPubKeyMan::GetID() const
         return m_wallet_descriptor.id;
     }
     
    -void DescriptorScriptPubKeyMan::SetCache(const DescriptorCache& cache)
    +void DescriptorScriptPubKeyMan::SetCache()
     {
         LOCK(cs_desc_man);
         std::set<CScript> new_spks;
    -    m_wallet_descriptor.cache = cache;
         for (int32_t i = m_wallet_descriptor.range_start; i < m_wallet_descriptor.range_end; ++i) {
             FlatSigningProvider out_keys;
             std::vector<CScript> scripts_temp;
    diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h
    index 096e4a57b4..8cc99310bf 100644
    --- a/src/wallet/scriptpubkeyman.h
    +++ b/src/wallet/scriptpubkeyman.h
    @@ -314,7 +314,7 @@ private:
         // Fetch the SigningProvider for a given index and optionally include private keys. Called by the above functions.
         std::unique_ptr<FlatSigningProvider> GetSigningProvider(int32_t index, bool include_private = false) const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
     
    -    void SetCache(const DescriptorCache& cache);
    +    void SetCache();
     
     protected:
         WalletDescriptor m_wallet_descriptor GUARDED_BY(cs_desc_man);
    

    </details>


    achow101 commented at 10:33 PM on April 23, 2026:

    Taken the second suggestion

  103. in src/wallet/test/fuzz/scriptpubkeyman.cpp:71 in f398d80938 outdated
      69 | @@ -70,6 +70,11 @@ static std::optional<std::pair<WalletDescriptor, FlatSigningProvider>> CreateWal
      70 |      std::vector<std::unique_ptr<Descriptor>> parsed_descs = Parse(desc_str.value(), keys, error, false);
      71 |      if (parsed_descs.empty()) return std::nullopt;
    


    davidgumberg commented at 6:35 PM on April 23, 2026:

    In https://github.com/bitcoin/bitcoin/pull/28333/changes/f398d809382631b86f21c7cd793fb7b6362dbdcf (fuzz: Skip adding descriptor to wallet if it cannot be expanded):

    Context for reviewers for why this commit is here:

    #28333 (comment) #28333 (comment)

    In the next commit, https://github.com/bitcoin/bitcoin/pull/28333/changes/94e0aa68d0bd1e2cc88e3fe9c05a7dac81b9f285 (wallet: include keys when constructing ,DescriptorSPKM during import) the DSPKM constructor used by AddWalletDescriptor() will now call UpdateWithSigningProvider:

    https://github.com/bitcoin/bitcoin/blob/94e0aa68d0bd1e2cc88e3fe9c05a7dac81b9f285/src/wallet/scriptpubkeyman.cpp#L821-L829

    which throws when top up fails:

    https://github.com/bitcoin/bitcoin/blob/94e0aa68d0bd1e2cc88e3fe9c05a7dac81b9f285/src/wallet/scriptpubkeyman.cpp#L1608-L1611

  104. in src/wallet/scriptpubkeyman.cpp:839 in 94e0aa68d0 outdated
     834 | +    m_wallet_descriptor(descriptor)
     835 | +{
     836 | +    LOCK(cs_desc_man);
     837 | +    UpdateWithSigningProvider(batch, provider);
     838 | +}
     839 | +
    


    davidgumberg commented at 9:55 PM on April 23, 2026:

    pure-style-take-if-you-like-otherwise-feel-free-to-disregard-nit:

    Maybe one constructor that takes a std::optional<WalletBatch> instead of two that differ by one line.


    achow101 commented at 10:36 PM on April 23, 2026:

    Disregarding :)

  105. in src/wallet/scriptpubkeyman.cpp:1610 in 94e0aa68d0 outdated
    1605 | +        AddDescriptorKeyWithDB(batch, key, key.GetPubKey());
    1606 | +    }
    1607 | +
    1608 | +    // Top up key pool, to generate scriptPubKeys
    1609 | +    if (!TopUpWithDB(batch)) {
    1610 | +        throw std::runtime_error("Could not top up scriptPubKeys");
    


    davidgumberg commented at 10:19 PM on April 23, 2026:

    Although unreachable today, since this is part of the DSPKM API it should have a test, maybe e.g.:

    https://github.com/davidgumberg/bitcoin/commit/ec12b6b61f5b8615b2d2d86a05c23f01e3af5740

    <details>

    <summary>diff</summary>

    diff --git a/src/wallet/test/scriptpubkeyman_tests.cpp b/src/wallet/test/scriptpubkeyman_tests.cpp
    index 9c62436d6c..b3237b92da 100644
    --- a/src/wallet/test/scriptpubkeyman_tests.cpp
    +++ b/src/wallet/test/scriptpubkeyman_tests.cpp
    @@ -4,6 +4,7 @@
     
     #include <key.h>
     #include <key_io.h>
    +#include <test/util/common.h>
     #include <test/util/setup_common.h>
     #include <script/solver.h>
     #include <wallet/scriptpubkeyman.h>
    @@ -37,5 +38,17 @@ BOOST_AUTO_TEST_CASE(DescriptorScriptPubKeyManTests)
         BOOST_CHECK(signprov_keypath_nums_h == nullptr);
     }
     
    +BOOST_AUTO_TEST_CASE(DescriptorSPKMTopUpFailureThrows)
    +{
    +    // Attempting to construct a DescriptorSPKM that cannot be topped up should throw.
    +    CExtKey extkey;
    +    extkey.SetSeed(std::array<std::byte, 32>{});
    +    CWallet keystore(m_node.chain.get(), "", CreateMockableWalletDatabase());
    +    BOOST_CHECK_EXCEPTION(
    +        CreateDescriptor(keystore, "wpkh(" + EncodeExtPubKey(extkey.Neuter()) +
    +"/*h)", /*success=*/true),
    +        std::runtime_error, HasReason("Could not top up scriptPubKeys"));
    +}
    +
     BOOST_AUTO_TEST_SUITE_END()
     } // namespace wallet
    

    </details>


    achow101 commented at 10:37 PM on April 23, 2026:

    Taken

  106. achow101 force-pushed on Apr 23, 2026
  107. DrahtBot added the label CI failed on Apr 23, 2026
  108. achow101 force-pushed on Apr 23, 2026
  109. DrahtBot removed the label CI failed on Apr 23, 2026
  110. davidgumberg commented at 1:52 AM on April 24, 2026: contributor

    crACK https://github.com/bitcoin/bitcoin/commit/6eb2db367785eacd8ed250a854d66c48544ce5d8

    This PR RAII-ify's DSPKM's so that they're ready to use after construction, it's a nice cleanup.

    One small nit remains: #28333 (review)

  111. DrahtBot requested review from polespinasa on Apr 24, 2026
  112. DrahtBot requested review from murchandamus on Apr 24, 2026
  113. achow101 force-pushed on Apr 24, 2026
  114. in src/wallet/scriptpubkeyman.cpp:1442 in f4894a8b5c
    1438 | @@ -1426,11 +1439,10 @@ uint256 DescriptorScriptPubKeyMan::GetID() const
    1439 |      return m_wallet_descriptor.id;
    1440 |  }
    1441 |  
    1442 | -void DescriptorScriptPubKeyMan::SetCache(const DescriptorCache& cache)
    1443 | +void DescriptorScriptPubKeyMan::SetCache()
    


    rkrux commented at 10:30 AM on April 25, 2026:

    In f4894a8b5cf133ef1958348c820b8f6b1fb37ad5 "wallet: Load everything into DescSPKM on construction"

    diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp
    index 9fc5a9d1cf..9b43c892fd 100644
    --- a/src/wallet/scriptpubkeyman.cpp
    +++ b/src/wallet/scriptpubkeyman.cpp
    @@ -847,7 +847,7 @@ DescriptorScriptPubKeyMan::DescriptorScriptPubKeyMan(WalletStorage& storage, con
         if (!m_map_keys.empty() && !m_map_crypted_keys.empty()) {
             throw std::runtime_error("Error: Wallet contains both unencrypted and encrypted keys");
         }
    -    SetCache();
    +    Load();
     }
     
     DescriptorScriptPubKeyMan::DescriptorScriptPubKeyMan(WalletStorage& storage, WalletBatch& batch, int64_t keypool_size, const CExtKey& master_key, OutputType addr_type, bool internal)
    @@ -1461,7 +1461,7 @@ uint256 DescriptorScriptPubKeyMan::GetID() const
         return m_wallet_descriptor.id;
     }
     
    -void DescriptorScriptPubKeyMan::SetCache()
    +void DescriptorScriptPubKeyMan::Load()
     {
         LOCK(cs_desc_man);
         std::set<CScript> new_spks;
    diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h
    index 15f8179b45..e0dc58f020 100644
    --- a/src/wallet/scriptpubkeyman.h
    +++ b/src/wallet/scriptpubkeyman.h
    @@ -314,7 +314,7 @@ private:
         // Fetch the SigningProvider for a given index and optionally include private keys. Called by the above functions.
         std::unique_ptr<FlatSigningProvider> GetSigningProvider(int32_t index, bool include_private = false) const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
     
    -    void SetCache();
    +    void Load();
     
         void AddDescriptorKey(const CKey& key, const CPubKey &pubkey);
         void UpdateWithSigningProvider(WalletBatch& batch, const FlatSigningProvider& signing_provider) EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man);
    
    

    The name SetCache for this method seems outdated now because neither does it accept nor does it set the (descriptor) cache anymore, and it only sets the private members of DescriptorScriptPubKeyMan. Though it can be argued that these internal members of the class are sort of a cache but I am not inclined towards that because the term cache is usually reserved for something that's explicitly designed for quicker and frequent access and using cache for most private members (if not all) seems like a stretch.

    More importantly, this method is called from only one constructor that's called during loading the descriptor records from the wallet database, so I believe a simple Load() would be more pertinent.

    I do notice that there is a wallet TopUpCallback method call at the end of this method that signals to me that this method is doing more than what's necessary and therefore believe that call could be moved one level up in the constructor calling this method but that would be unrelated changes that needn't be done in this PR.


    achow101 commented at 1:01 AM on April 28, 2026:

    Done

  115. in src/wallet/wallet.cpp:868 in aac8533725
     868 | -            SetupDescriptorScriptPubKeyMans();
     869 | +        if (!Unlock(strWalletPassphrase)) {
     870 | +            return false;
     871 |          }
     872 | +
     873 | +        // Generate new descriptors or seed if not blank or disable private keys
    


    rkrux commented at 12:00 PM on April 27, 2026:

    In aac8533725112e0330da87f32b63386a369f8a9a "wallet: Consolidate generation setup callers into one function"

    if not blank or disable private keys

    This phrase in the comment is leaking the internals of the below method here, which seems unnecessary at the least and confusing at the most. Remove it?

    - // Generate new descriptors or seed if not blank or disable private keys
    

    achow101 commented at 1:01 AM on April 28, 2026:

    Removed

  116. in src/wallet/wallet.cpp:388 in 03993ad475
     384 | @@ -385,18 +385,12 @@ std::shared_ptr<CWallet> CreateWallet(WalletContext& context, const std::string&
     385 |  
     386 |      uint64_t wallet_creation_flags = options.create_flags;
     387 |      const SecureString& passphrase = options.create_passphrase;
     388 | +    bool is_encrypted = !passphrase.empty();
    


    rkrux commented at 12:05 PM on April 27, 2026:

    In 03993ad4756d4d4e3058a11428204ed1e630431f "refactor: wallet: Don't reuse WALLET_BLANK flag for born-encrypted wallets."

    Make the name more explicit? The name is_encrypted (not this variable) can be true for also the wallets that are encrypted post creation.

    - bool is_encrypted = !passphrase.empty();
    + bool born_encrypted = !passphrase.empty(); 
    

    polespinasa commented at 4:32 PM on April 27, 2026:

    In 03993ad4756d4d4e3058a11428204ed1e630431f refactor: wallet: Don't reuse WALLET_BLANK flag for born-encrypted wallets.

    feel-free-to-ignore-nit: I suggest use another name like should_encrypt as at creation time wallet is not encrypted yet, it will be.


    achow101 commented at 1:01 AM on April 28, 2026:

    Renamed


    achow101 commented at 1:03 AM on April 28, 2026:

    Renamed to born_encrypted

  117. in src/wallet/test/util.cpp:1 in 03993ad475 outdated


    rkrux commented at 12:07 PM on April 27, 2026:

    In 03993ad "refactor: wallet: Don't reuse WALLET_BLANK flag for born-encrypted wallets."

    Comment about the commit - Seems fine at first glance to not use the flag but the message doesn't mention the reason.


    rkrux commented at 12:24 PM on April 27, 2026:

    In 03993ad "refactor: wallet: Don't reuse WALLET_BLANK flag for born-encrypted wallets."

    This needs to be inside the is_encrypted conditional as well, I suppose. Seems like a no-op for encrypted wallet creation flow.


    achow101 commented at 9:02 PM on April 27, 2026:

    It does not need to be, it's a no-op when the wallet has no descriptors. It's a no-op for blank wallets too.


    achow101 commented at 1:01 AM on April 28, 2026:

    Expanded the commit message

  118. in src/wallet/wallet.cpp:3082 in 03993ad475
    3078 | @@ -3090,7 +3079,10 @@ std::shared_ptr<CWallet> CWallet::CreateNew(WalletContext& context, const std::s
    3079 |          // Only descriptor wallets can be created
    3080 |          assert(walletInstance->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
    3081 |  
    3082 | -        walletInstance->SetupWalletGeneration();
    3083 | +        // Born encrypted wallets need to be created blank first.
    


    rkrux commented at 12:20 PM on April 27, 2026:

    In 03993ad4756d4d4e3058a11428204ed1e630431f "refactor: wallet: Don't reuse WALLET_BLANK flag for born-encrypted wallets."

    blank first.

    Now that the usage of blank flag is removed in born encrypted wallets, consider removing the blank terminology as well.

    - // Born encrypted wallets need to be created blank first.
    + // Born encrypted wallets will have their keys generated later.
    

    achow101 commented at 1:02 AM on April 28, 2026:

    Done

  119. rkrux commented at 1:00 PM on April 27, 2026: contributor

    Paused review at f4894a8b5cf133ef1958348c820b8f6b1fb37ad5.

    Nice to see more activity on this PR, moving it closer to merge.

    However, the PR seems to have changed a bit from last year when I reviewed it and I'm unable to place the second commit in the context of this PR refactor: wallet: Don't reuse WALLET_BLANK flag for born-encrypted wallets. now - it seems fine to do but going through it seems like a deviation while reviewing the PR that's supposed to construct SPKMs with all data rather than loaded progressively.

    I see the second commit was added recently (https://github.com/bitcoin/bitcoin/pull/28333#discussion_r3134291830) but I really feel it should be its own PR. I ended up adding suggestions in this commit but I don't see a reason why they should be addressed in this PR that's supposed to do something else.

  120. in src/wallet/wallet.cpp:3666 in aac8533725 outdated
    3661 | +{
    3662 | +    LOCK(cs_wallet);
    3663 | +    // We only want to call Setup if we are not blank,
    3664 | +    // have private keys (not having private keys implies blank),
    3665 | +    // or need to get descriptors from an external signer.
    3666 | +    if (!IsWalletFlagSet(WALLET_FLAG_EXTERNAL_SIGNER) &&
    


    polespinasa commented at 4:26 PM on April 27, 2026:

    In aac8533725112e0330da87f32b63386a369f8a9a wallet: Consolidate generation setup callers into one function

    This comment doesn’t exactly reflect the condition. The logic skips setup for non-external signers when the wallet is blank or has private keys disabled. Suggest rewording the comment to match the code behavior.

    // Skip setup for non-external-signer wallets that are either blank
    // or have private keys disabled (not having private keys implies blank).
    

    achow101 commented at 1:02 AM on April 28, 2026:

    Done

  121. in src/wallet/wallet.h:1037 in aac8533725 outdated
    1032 | @@ -1039,6 +1033,9 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
    1033 |      //! Create new seed and default DescriptorScriptPubKeyMans for this wallet
    1034 |      void SetupOwnDescriptorScriptPubKeyMans(WalletBatch& batch) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
    1035 |  
    1036 | +    //! Setup new descriptors or seed for new address generation
    1037 | +    void SetupWalletGeneration();
    


    polespinasa commented at 4:28 PM on April 27, 2026:

    in aac8533 wallet: Consolidate generation setup callers into one function Missing EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) ?


    achow101 commented at 1:08 AM on April 28, 2026:

    This function was written as acquiring the lock, so EXCLUSIVE_LOCKS_REQUIRED is incorrect for that. However, since all callers take the lock beforehand, I changed it to use AssertLockHeld which does require the annotation.

  122. in src/wallet/scriptpubkeyman.cpp:824 in f4894a8b5c
     820 | @@ -821,6 +821,19 @@ bool LegacyDataSPKM::DeleteRecordsWithDB(WalletBatch& batch)
     821 |      return batch.EraseRecords(DBKeys::LEGACY_TYPES);
     822 |  }
     823 |  
     824 | +DescriptorScriptPubKeyMan::DescriptorScriptPubKeyMan(WalletStorage& storage, const uint256& id, WalletDescriptor& descriptor, int64_t keypool_size, const KeyMap& keys, const CryptedKeyMap& ckeys)
    


    polespinasa commented at 4:44 PM on April 27, 2026:

    in f4894a8b5cf133ef1958348c820b8f6b1fb37ad5 wallet: Load everything into DescSPKM on construction

    id is never used, can be dropped here and in ExternalSignerScriptPubKeyMan


    achow101 commented at 1:03 AM on April 28, 2026:

    Dropped

  123. in src/wallet/walletdb.cpp:897 in f4894a8b5c
     893 | @@ -896,12 +894,19 @@ static DBErrors LoadDescriptorWalletRecords(CWallet* pwallet, DatabaseBatch& bat
     894 |              std::vector<unsigned char> privkey;
     895 |              value >> privkey;
     896 |  
     897 | -            spk_man->AddCryptedKey(pubkey.GetID(), pubkey, privkey);
     898 | +            ckeys[pubkey.GetID()] = make_pair(pubkey, privkey);
    


    polespinasa commented at 4:55 PM on April 27, 2026:

    in f4894a8b5cf133ef1958348c820b8f6b1fb37ad5 wallet: Load everything into DescSPKM on construction

    nit: ckeys[pubkey.GetID()] = std::make_pair(pubkey, privkey);


    achow101 commented at 1:04 AM on April 28, 2026:

    Done

  124. in test/functional/wallet_descriptor.py:281 in 110628b0fe
     276 | +        wallet_db = self.nodes[0].wallets_path / wallet_name / self.wallet_data_filename
     277 | +        conn = sqlite3.connect(wallet_db)
     278 | +        with conn:
     279 | +            key_prefix = ser_string(b"walletdescriptorkey")
     280 | +            ckey_prefix = ser_string(b"walletdescriptorckey")
     281 | +            rows = conn.execute('Select key, value FROM main').fetchall()
    


    polespinasa commented at 4:57 PM on April 27, 2026:

    in 110628b0fee6cb66b3194ef082a3dfa7f9356fbc test: wallet: Check that loading wallet with both unencrypted and encrypted keys fails.

    nit: should be SELECT, it's more standard style


    achow101 commented at 1:04 AM on April 28, 2026:

    Done

  125. in test/functional/wallet_descriptor.py:288 in 110628b0fe outdated
     283 | +            # Test the test, want to be sure there is at least one unencrypted key.
     284 | +            assert len(key_rows) >= 1
     285 | +            k, v = key_rows[0]
     286 | +            conn.execute('INSERT INTO main VALUES(?, ?)', (k.replace(key_prefix, ckey_prefix), v))
     287 | +        conn.close()
     288 | +        with self.nodes[0].assert_debug_log(["Wallet contains both unencrypted and encrypted keys"]):
    


    polespinasa commented at 4:59 PM on April 27, 2026:

    in 110628b0fee6cb66b3194ef082a3dfa7f9356fbc test: wallet: Check that loading wallet with both unencrypted and encrypted keys fails.

    As mentioned in https://github.com/bitcoin/bitcoin/pull/28333/changes/f4894a8b5cf133ef1958348c820b8f6b1fb37ad5#r3148895417

    Here the Error: prefix is not present. (Should be removed in the constructor)

  126. in src/wallet/test/fuzz/scriptpubkeyman.cpp:76 in 804dc85f30 outdated
      69 | @@ -70,6 +70,11 @@ static std::optional<std::pair<WalletDescriptor, FlatSigningProvider>> CreateWal
      70 |      std::vector<std::unique_ptr<Descriptor>> parsed_descs = Parse(desc_str.value(), keys, error, false);
      71 |      if (parsed_descs.empty()) return std::nullopt;
      72 |  
      73 | +    FlatSigningProvider out_keys;
      74 | +    std::vector<CScript> scripts_temp;
      75 | +    DescriptorCache temp_cache;
      76 | +    if (!parsed_descs.at(0)->Expand(0, keys, scripts_temp, out_keys, &temp_cache)) return std::nullopt;
    


    polespinasa commented at 5:03 PM on April 27, 2026:

    in 804dc85f30a692531765eb0a8a9148a3e7afa3ff fuzz: Skip adding descriptor to wallet if it cannot be expanded

    nit: out_keys, scripts_temp and temp_cache are populated but then discarded which can be a bit confusing, maybe a small comment with something like: // Only check whether Expand succeeds; discard other outputs can be helpfull?


    achow101 commented at 1:04 AM on April 28, 2026:

    Added a comment

  127. in src/wallet/scriptpubkeyman.cpp:1609 in fcffb36faf outdated
    1604 | +        AddDescriptorKeyWithDB(batch, key, key.GetPubKey());
    1605 | +    }
    1606 | +
    1607 | +    // Top up key pool, to generate scriptPubKeys
    1608 | +    if (!TopUpWithDB(batch)) {
    1609 | +        throw std::runtime_error("Could not top up scriptPubKeys");
    


    polespinasa commented at 5:12 PM on April 27, 2026:

    in fcffb36faf90c82c47eb9076e7abbd9fbc70bee6 wallet: include keys when constructing DescriptorSPKM during import

    I am not really familiar with error handling, but the error type changed here. I think this way it cannot be translated? Which is a problem as this is propagated till importdescriptor RPC.

    not sure of this tbh feel free to ignore if I am mistaken


    achow101 commented at 9:21 PM on April 27, 2026:

    It doesn't matter for RPCs as the output to RPC is never translated. This could be an issue for the GUI once import descriptors in the GUI is implemented. However, I'm pretty sure that this condition can't be hit in practice as it would mean that the descriptor cannot be expanded even when the private keys are available. This is checked for within importdescriptors before we get to this point.


    polespinasa commented at 9:32 PM on April 27, 2026:

    It doesn't matter for RPCs as the output to RPC is never translated

    didn't know it, make sense then :)


    polespinasa commented at 7:09 AM on April 28, 2026:

    Even if it can't be hit, I think it is never catch?

  128. in src/wallet/scriptpubkeyman.h:331 in fcffb36faf
     331 | -        :   ScriptPubKeyMan(storage),
     332 | -            m_keypool_size(keypool_size),
     333 | -            m_wallet_descriptor(descriptor)
     334 | -        {}
     335 | +    DescriptorScriptPubKeyMan(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const FlatSigningProvider& provider);
     336 | +    DescriptorScriptPubKeyMan(WalletStorage& storage, WalletBatch& batch, WalletDescriptor& descriptor, int64_t keypool_size, const FlatSigningProvider& provider);
    


    polespinasa commented at 5:13 PM on April 27, 2026:

    in fcffb36faf90c82c47eb9076e7abbd9fbc70bee6 wallet: include keys when constructing DescriptorSPKM during import

    nit: maybe document a bit more the constructors, when to use each?


    achow101 commented at 1:04 AM on April 28, 2026:

    Added comments

  129. in src/wallet/wallet.cpp:3557 in d946ecf3b1
    3553 | @@ -3554,7 +3554,7 @@ void CWallet::LoadDescriptorScriptPubKeyMan(uint256 id, WalletDescriptor& desc,
    3554 |  DescriptorScriptPubKeyMan& CWallet::SetupDescriptorScriptPubKeyMan(WalletBatch& batch, const CExtKey& master_key, const OutputType& output_type, bool internal)
    3555 |  {
    3556 |      AssertLockHeld(cs_wallet);
    3557 | -    auto spk_manager = std::unique_ptr<DescriptorScriptPubKeyMan>(new DescriptorScriptPubKeyMan(*this, m_keypool_size));
    3558 | +    auto spk_manager = std::unique_ptr<DescriptorScriptPubKeyMan>(new DescriptorScriptPubKeyMan(*this, batch, m_keypool_size, master_key, output_type, internal));
    


    polespinasa commented at 5:25 PM on April 27, 2026:

    in d946ecf3b1930e94dbcbf3a4470cffa1ec6d0a03 wallet: Setup new autogenerated descriptors on construction

    In the old code SetupDescriptorGeneration() was called after the HasEncryption() && IsLocked() check, but now it is called before (inside DescriptorScriptPubKeyMan()). This means that the batch already received unencrypted key writes before the encryption check.


    achow101 commented at 1:05 AM on April 28, 2026:

    Moved this down after the encryption check.

    Also dropped the unnecessary CheckDecryptionKeys and Encrypt.

  130. in src/wallet/scriptpubkeyman.cpp:1177 in d946ecf3b1
    1173 |  {
    1174 |      LOCK(cs_desc_man);
    1175 |      assert(m_storage.IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
    1176 |  
    1177 |      // Ignore when there is already a descriptor
    1178 |      if (m_wallet_descriptor.descriptor) {
    


    polespinasa commented at 5:30 PM on April 27, 2026:

    in d946ecf3b1930e94dbcbf3a4470cffa1ec6d0a03 wallet: Setup new autogenerated descriptors on construction

    SetupDescriptorGeneration is a private function that is only called from a constructor. I think this can never happen because of that. Maybe changing it to assert is better.


    achow101 commented at 1:05 AM on April 28, 2026:

    Done

  131. in src/wallet/test/scriptpubkeyman_tests.cpp:49 in 83c810a7ae
      44 | +    CExtKey extkey;
      45 | +    extkey.SetSeed(std::array<std::byte, 32>{});
      46 | +    CWallet keystore(m_node.chain.get(), "", CreateMockableWalletDatabase());
      47 | +    BOOST_CHECK_EXCEPTION(
      48 | +        CreateDescriptor(keystore, "wpkh(" + EncodeExtPubKey(extkey.Neuter()) +
      49 | +"/*h)", /*success=*/true),
    


    polespinasa commented at 5:33 PM on April 27, 2026:

    in 83c810a7ae3f3f016c807d4efabe4b3264c6b5ff test: wallet: Constructing a DSPKM that can't TopUp() throws.

    nit: you are breaking a string with a line jump.


    achow101 commented at 1:05 AM on April 28, 2026:

    Fixed

  132. polespinasa changes_requested
  133. polespinasa commented at 5:34 PM on April 27, 2026: member

    reviewed 83c810a7ae3f3f016c807d4efabe4b3264c6b5ff

    left some comments

  134. in src/wallet/scriptpubkeyman.h:338 in 83c810a7ae
     342 | -    DescriptorScriptPubKeyMan(WalletStorage& storage, int64_t keypool_size)
     343 | -        :   ScriptPubKeyMan(storage),
     344 | -            m_keypool_size(keypool_size)
     345 | -        {}
     346 | +    //! Create a new DescriptorScriptPubKeyMan from an existing descriptor (i.e. from an import)
     347 | +    DescriptorScriptPubKeyMan(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const FlatSigningProvider& provider);
    


    pseudoramdom commented at 7:45 PM on April 27, 2026:

    (I’m new here and not very familiar with the project conventions, so feel free to disregard if this does not fit the local style)

    f4894a8b5cf133ef1958348c820b8f6b1fb37ad5 These constructor overloads seem to be doing more than just creating a DescriptorScriptPubKeyMan. Some of them import keys, write wallet records, top up scripts, or change wallet state. This makes the call sites look like simple object construction even though different overloads have different side effects and failure modes.

    Could these be named factory functions like CreateForImport, CreateGenerated, or LoadFromDatabase make the intent and error handling clearer?

    static util::Result<std::unique_ptr<DescriptorScriptPubKeyMan>> CreateForImport(
              WalletStorage& storage,
              WalletDescriptor descriptor,
              int64_t keypool_size,
              const FlatSigningProvider& provider);
    

    Alternatively, would it help to just add the named factory wrappers?


    achow101 commented at 1:06 AM on April 28, 2026:

    The constructors are useful for inheriting parent behavior that is useful in children. However, using factory functions does seem like it is easier to read rather than the overloads, so I've reimplemented most of the constructors in that way, and made the remaining be wrappers around the (now private) constructors.

  135. achow101 force-pushed on Apr 28, 2026
  136. achow101 force-pushed on Apr 28, 2026
  137. DrahtBot added the label CI failed on Apr 28, 2026
  138. in src/wallet/scriptpubkeyman.h:172 in fb184af3ed
     165 | @@ -166,14 +166,18 @@ static const std::unordered_set<OutputType> LEGACY_OUTPUT_TYPES {
     166 |      OutputType::BECH32,
     167 |  };
     168 |  
     169 | +using KeyMap = std::map<CKeyID, CKey>;
     170 | +using CryptedKeyMap = std::map<CKeyID, std::pair<CPubKey, std::vector<unsigned char>>>;
     171 | +using ScriptPubKeyMap = std::map<CScript, int32_t>; // Map of scripts to descriptor range index
     172 | +using PubKeyMap = std::map<CPubKey, int32_t>; // Map of pubkeys involved in scripts to descriptor range index
    


    polespinasa commented at 6:59 AM on April 28, 2026:

    in fb184af3edd22670d5f1f0c9d4df7dd5240c51a7 wallet: Load everything into DescSPKM on construction

    The last two definitions are done inside wallet namespace but only used inside the class. Maybe can be moved inside it to not pollute the namespace.


    rkrux commented at 8:48 AM on April 28, 2026:

    but only used inside the class

    The first two are used in multiple places.


    polespinasa commented at 11:07 AM on April 28, 2026:

    but only used inside the class

    The first two are used in multiple places.

    You're right, I fixed the comment


    achow101 commented at 7:53 PM on April 28, 2026:

    Done

  139. in src/wallet/scriptpubkeyman.h:178 in fb184af3ed
     174 |  // Manages the data for a LegacyScriptPubKeyMan.
     175 |  // This is the minimum necessary to load a legacy wallet so that it can be migrated.
     176 |  class LegacyDataSPKM : public ScriptPubKeyMan, public FillableSigningProvider
     177 |  {
     178 | -private:
     179 | +protected:
    


    polespinasa commented at 7:01 AM on April 28, 2026:

    in fb184af3edd22670d5f1f0c9d4df7dd5240c51a7 wallet: Load everything into DescSPKM on construction

    Maybe an explanation on why change this to protected in the commit message?


    rkrux commented at 11:09 AM on April 28, 2026:

    This change seems unnecessary because it builds fine without it - most likely outdated diff.


    achow101 commented at 7:54 PM on April 28, 2026:

    Reverted

  140. in src/wallet/test/scriptpubkeyman_tests.cpp:48 in dcd91fd710 outdated
      43 | +    // Attempting to construct a DescriptorSPKM that cannot be topped up should throw.
      44 | +    CExtKey extkey;
      45 | +    extkey.SetSeed(std::array<std::byte, 32>{});
      46 | +    CWallet keystore(m_node.chain.get(), "", CreateMockableWalletDatabase());
      47 | +    BOOST_CHECK_EXCEPTION(
      48 | +        CreateDescriptor(keystore, "wpkh(" + EncodeExtPubKey(extkey.Neuter()) + "/*h)", /*success=*/true),
    


    polespinasa commented at 7:06 AM on April 28, 2026:

    in dcd91fd710ac10d184b18121f5044f37ca29fa6b test: wallet: Constructing a DSPKM that can't TopUp() throws.

    This is a bit confusing, we have *success=*/true but at the same time we expect it to throw an exception.

    Also maybe a comment saying why this has to throw would help. I guess is because we require priv keys when using /*h.


    achow101 commented at 7:54 PM on April 28, 2026:

    Expanded the comment above.

  141. polespinasa commented at 7:06 AM on April 28, 2026: member

    Thanks for addressing comments :)

    Just three more things I just saw and I will ack :P

  142. in src/wallet/wallet.h:874 in 2318f5dce1
     870 | @@ -871,7 +871,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
     871 |      static bool LoadWalletArgs(std::shared_ptr<CWallet> wallet, const WalletContext& context, bilingual_str& error, std::vector<bilingual_str>& warnings);
     872 |  
     873 |      /* Initializes, creates and returns a new CWallet; returns a null pointer in case of an error */
     874 | -    static std::shared_ptr<CWallet> CreateNew(WalletContext& context, const std::string& name, std::unique_ptr<WalletDatabase> database, uint64_t wallet_creation_flags, bilingual_str& error, std::vector<bilingual_str>& warnings);
     875 | +    static std::shared_ptr<CWallet> CreateNew(WalletContext& context, const std::string& name, std::unique_ptr<WalletDatabase> database, uint64_t wallet_creation_flags, bool is_encrypted, bilingual_str& error, std::vector<bilingual_str>& warnings);
    


    rkrux commented at 11:09 AM on April 28, 2026:

    In 2318f5dce1b1349738c5adc0a34dec6fd45938d0 "refactor: wallet: Don't reuse WALLET_BLANK flag for born-encrypted wallets."

    To fix the clang tidy error: https://github.com/bitcoin/bitcoin/actions/runs/25028250329/job/73304196483?pr=28333

    -    static std::shared_ptr<CWallet> CreateNew(WalletContext& context, const std::string& name, std::unique_ptr<WalletDatabase> database, uint64_t wallet_creation_flags, bool is_encrypted, bilingual_str& error, std::vector<bilingual_str>& warnings);
    +    static std::shared_ptr<CWallet> CreateNew(WalletContext& context, const std::string& name, std::unique_ptr<WalletDatabase> database, uint64_t wallet_creation_flags, bool born_encrypted, bilingual_str& error, std::vector<bilingual_str>& warnings);
    

    achow101 commented at 7:54 PM on April 28, 2026:

    Done

  143. in src/wallet/scriptpubkeyman.h:307 in b1f8254961
     302 | @@ -303,6 +303,13 @@ class DescriptorScriptPubKeyMan : public ScriptPubKeyMan
     303 |       */
     304 |      mutable std::map<uint256, MuSig2SecNonce> m_musig2_secnonces;
     305 |  
     306 | +    //! Create a new DescriptorScriptPubKeyMan from an existing descriptor (i.e. from an import)
     307 | +    DescriptorScriptPubKeyMan(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const FlatSigningProvider& provider)
    


    rkrux commented at 11:14 AM on April 28, 2026:

    In b1f82549616b37d1d24747fc28dd2b9872ff57b7 "wallet: include keys when constructing DescriptorSPKM during import"

    provider is not needed in this constructor.

    diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp
    index c4400b0c06..473ad2d7d7 100644
    --- a/src/wallet/scriptpubkeyman.cpp
    +++ b/src/wallet/scriptpubkeyman.cpp
    @@ -820,7 +820,7 @@ bool LegacyDataSPKM::DeleteRecordsWithDB(WalletBatch& batch)
     
     std::unique_ptr<DescriptorScriptPubKeyMan> DescriptorScriptPubKeyMan::CreateFromImport(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const FlatSigningProvider& provider)
     {
    -    auto spkm = std::unique_ptr<DescriptorScriptPubKeyMan>(new DescriptorScriptPubKeyMan(storage, descriptor, keypool_size, provider));
    +    auto spkm = std::unique_ptr<DescriptorScriptPubKeyMan>(new DescriptorScriptPubKeyMan(storage, descriptor, keypool_size));
         LOCK(spkm->cs_desc_man);
         WalletBatch batch(storage.GetDatabase());
         spkm->UpdateWithSigningProvider(batch, provider);
    @@ -829,7 +829,7 @@ std::unique_ptr<DescriptorScriptPubKeyMan> DescriptorScriptPubKeyMan::CreateFrom
     
     std::unique_ptr<DescriptorScriptPubKeyMan> DescriptorScriptPubKeyMan::CreateFromMigration(WalletStorage& storage, WalletBatch& batch, WalletDescriptor& descriptor, int64_t keypool_size, const FlatSigningProvider& provider)
     {
    -    auto spkm = std::unique_ptr<DescriptorScriptPubKeyMan>(new DescriptorScriptPubKeyMan(storage, descriptor, keypool_size, provider));
    +    auto spkm = std::unique_ptr<DescriptorScriptPubKeyMan>(new DescriptorScriptPubKeyMan(storage, descriptor, keypool_size));
         LOCK(spkm->cs_desc_man);
         spkm->UpdateWithSigningProvider(batch, provider);
         return spkm;
    diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h
    index 3cae66b2e8..621630c948 100644
    --- a/src/wallet/scriptpubkeyman.h
    +++ b/src/wallet/scriptpubkeyman.h
    @@ -304,7 +304,7 @@ private:
         mutable std::map<uint256, MuSig2SecNonce> m_musig2_secnonces;
     
         //! Create a new DescriptorScriptPubKeyMan from an existing descriptor (i.e. from an import)
    -    DescriptorScriptPubKeyMan(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size, const FlatSigningProvider& provider)
    +    DescriptorScriptPubKeyMan(WalletStorage& storage, WalletDescriptor& descriptor, int64_t keypool_size)
             : ScriptPubKeyMan(storage),
             m_keypool_size(keypool_size),
             m_wallet_descriptor(descriptor)
    
    

    achow101 commented at 7:55 PM on April 28, 2026:

    Done

  144. achow101 force-pushed on Apr 28, 2026
  145. achow101 force-pushed on Apr 28, 2026
  146. DrahtBot removed the label CI failed on Apr 28, 2026
  147. in src/wallet/scriptpubkeyman.cpp:1614 in 406a4d3b5b
    1609 | +{
    1610 | +    AssertLockHeld(cs_desc_man);
    1611 | +    // Add the private keys to the descriptor
    1612 | +    for (const auto& entry : signing_provider.keys) {
    1613 | +        const CKey& key = entry.second;
    1614 | +        AddDescriptorKeyWithDB(batch, key, key.GetPubKey());
    


    w0xlt commented at 9:10 PM on April 28, 2026:

    If the result of AddDescriptorKeyWithDB is ignored, the operation could look successful while the wallet had not actually persisted the supplied private key.

            if (!AddDescriptorKeyWithDB(batch, key, key.GetPubKey())) {
                    throw std::runtime_error(std::string(__func__) + ": writing descriptor private key failed");
            }
    

    Test for this:

    diff --git a/src/wallet/test/scriptpubkeyman_tests.cpp b/src/wallet/test/scriptpubkeyman_tests.cpp
    index 74389a5496..d1b63d0b02 100644
    --- a/src/wallet/test/scriptpubkeyman_tests.cpp
    +++ b/src/wallet/test/scriptpubkeyman_tests.cpp
    @@ -50,5 +50,26 @@ BOOST_AUTO_TEST_CASE(desc_spkm_topup_fail)
             std::runtime_error, HasReason("Could not top up scriptPubKeys"));
     }
     
    +BOOST_AUTO_TEST_CASE(desc_spkm_privkey_write_fail)
    +{
    +    CWallet keystore(m_node.chain.get(), "", CreateMockableWalletDatabase());
    +    keystore.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
    +    BOOST_REQUIRE(keystore.EncryptWallet(SecureString{"passphrase"}));
    +    BOOST_REQUIRE(keystore.IsLocked());
    +
    +    const auto key = GenerateRandomKey();
    +    FlatSigningProvider keys;
    +    std::string error;
    +    auto parsed_descs = Parse("wpkh(" + EncodeSecret(key) + ")", keys, error, /*require_checksum=*/false);
    +    BOOST_REQUIRE_EQUAL(parsed_descs.size(), 1);
    +
    +    WalletDescriptor w_desc(std::move(parsed_descs.at(0)), /*creation_time=*/1, /*range_start=*/0, /*range_end=*/1, /*next_index=*/0);
    +
    +    LOCK(keystore.cs_wallet);
    +    BOOST_CHECK_EXCEPTION(
    +        keystore.AddWalletDescriptor(w_desc, keys, /*label=*/"", /*internal=*/false),
    +        std::runtime_error, HasReason("writing descriptor private key failed"));
    +}
    +
     BOOST_AUTO_TEST_SUITE_END()
     } // namespace wallet
    

    achow101 commented at 12:24 AM on April 29, 2026:

    Fixed. I don't think a test is necessary.

  148. achow101 force-pushed on Apr 29, 2026
  149. DrahtBot added the label CI failed on Apr 29, 2026
  150. DrahtBot commented at 1:49 AM on April 29, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task macOS native, fuzz: https://github.com/bitcoin/bitcoin/actions/runs/25084663233/job/73497385306</sub> <sub>LLM reason (✨ experimental): CI failed because the spkm_migration fuzz target crashed with UpdateWithSigningProvider: writing descriptor private key failed (exit code 1).</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  151. wallet migration, fuzz: Migrate hd seed once
    If a wallet has multiple HD chains that have the same seed, we should
    only migrate that seed a single time.
    
    This fixes a fuzz crash that occurs once the return value of
    AddDescriptorKeyWithDB is checked during descriptor construction.
    0301c758ea
  152. wallet: Consolidate generation setup callers into one function cd912c4e10
  153. refactor: wallet: Don't reuse WALLET_BLANK flag for born-encrypted wallets.
    With the split between LoadWallet and CreateNew, it's no longer
    necessary to utilize the blank flag to prevent the wallet from having
    descriptors automatically being generated. Instead, CreateNew can take a
    separate parameter to indicate whether the wallet is to be born
    encrypted and therefore should not have any keys generated.
    f713fd1725
  154. wallet: Load everything into DescSPKM on construction
    Instead of creating a DescSPKM that is then progressively loaded, we
    should instead create it all at once in a factory function when loading.
    80b0c25992
  155. test: wallet: Check that loading wallet with both unencrypted and encrypted keys fails. 8be5ee554b
  156. fuzz: Skip adding descriptor to wallet if it cannot be expanded 6538f69135
  157. wallet: include keys when constructing DescriptorSPKM during import
    When importing a descriptor, all of the descriptor data should be
    provided at the same time in the constructor.
    aa4f7823aa
  158. wallet: Construct ExternalSignerSPKM with the new descriptor
    Instead of constructing then setting the descriptor with
    SetupDescriptor, just pass in that descriptor to the constructor.
    e20aaff70f
  159. wallet: Setup new autogenerated descriptors on construction
    Instead of having a caller use SetupDescriptorGeneration, just have a
    constructor that takes those arguments and sets up the descriptor with
    the autogenerated key.
    32946e0291
  160. test: wallet: Constructing a DSPKM that can't TopUp() throws. 451fdd26a4
  161. achow101 force-pushed on Apr 29, 2026
  162. achow101 commented at 9:19 PM on April 29, 2026: member

    Added a commit to fix the fuzz crash.

  163. DrahtBot removed the label CI failed on Apr 29, 2026
  164. polespinasa commented at 10:15 PM on April 29, 2026: member

    ACK 451fdd26a4f638e4f68f076ba2a891222cea380c

    thanks for addressing all comments, the code lgtm :)

  165. DrahtBot requested review from davidgumberg on Apr 29, 2026
  166. DrahtBot requested review from w0xlt on Apr 29, 2026
  167. DrahtBot requested review from rkrux on Apr 29, 2026

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-05-09 15:13 UTC

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