wallet: Improve `HasWalletDescriptor` performance and other canonical descriptor string followups #36230

pull achow101 wants to merge 8 commits into bitcoin:master from achow101:miniscript-desc-id-perf changing 9 files +97 −42
  1. achow101 commented at 12:17 AM on September 12, 2026: member

    Instead of re-computing the canonical descriptor string for every call to HasWalletDescriptor. WalletDescriptor will now compute it once upon construction and cache the hash of that string. The comparison uses a new WalletDescriptor::Equals function which compares the canonical string hashes. The hash is used to avoid holding possibly a large amount of memory for a string that is rarely used. This should fix the performance regression described in #35445 (comment)

    Also addresses several review comments related to documentation and code readability:

  2. DrahtBot added the label Wallet on Sep 12, 2026
  3. DrahtBot commented at 12:17 AM on September 12, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

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

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    ACK polespinasa, Sjors
    Stale ACK rkrux

    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:

    • #35444 (wallet: make descriptor SPKM mutex non-recursive by w0xlt)

    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(descriptor_str, keys, error, true) in src/wallet/walletutil.h

    <sup>2026-09-17 22:30:44</sup>

  4. achow101 added this to the milestone 32.0 on Sep 12, 2026
  5. achow101 force-pushed on Sep 12, 2026
  6. DrahtBot added the label CI failed on Sep 12, 2026
  7. DrahtBot commented at 12:21 AM on September 12, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task i686, no IPC: https://github.com/bitcoin/bitcoin/actions/runs/34661121561/job/103463659758</sub> <sub>LLM reason (✨ experimental): CI failed due to a compiler error: descriptor.cpp has an uninitialized use_apostrophe variable triggering -Werror=maybe-uninitialized during the build.</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>

  8. achow101 renamed this:
    wallet: Imrpove `HasWalletDescriptor` performance and other canonical descriptor string followups
    wallet: Improve `HasWalletDescriptor` performance and other canonical descriptor string followups
    on Sep 12, 2026
  9. DrahtBot removed the label CI failed on Sep 12, 2026
  10. hebasto commented at 11:41 AM on September 12, 2026: member

    Tested 41aab45e17920400b011366d7c257898d999e430 on Alpine:

    $ ./build-pr36230/test/functional/test_runner.py wallet_miniscript
    Temporary test directory at /tmp/test_runner_₿_🏃_20260912_113648
    Remaining jobs: [wallet_miniscript.py]
    ....................................................................................................................................................................................................................................................................................                                                                                                                                                                                                                                                                                    1/1 - wallet_miniscript.py passed, Duration: 144 s
    
    TEST                 | STATUS    | DURATION
    
    wallet_miniscript.py | ✓ Passed  | 144 s
    
    ALL                  | ✓ Passed  | 144 s (accumulated) 
    Runtime: 144 s
    
    

    The test performance issue is resolved. This is also confirmed by the CI:

    wallet_miniscript.py                                                             | ✓ Passed  | 262 s
    
  11. in src/wallet/scriptpubkeyman.cpp:1532 in 27034d39cb
    1529 | @@ -1530,7 +1530,7 @@ bool DescriptorScriptPubKeyMan::HasWalletDescriptor(const WalletDescriptor& desc
    1530 |  {
    1531 |      LOCK(cs_desc_man);
    1532 |      // Compare by using the canonical string to make the hardened indicators consistent for comparison
    


    rkrux commented at 12:11 PM on September 12, 2026:

    In 27034d39cb59c67a80304bf1d1f18a72ccf3aec9 "wallet: Compare descriptors by hash of canonical string"

    This comment can be moved out to become the documentation for Equals.


    polespinasa commented at 9:34 AM on September 15, 2026:

    +1 also we now compare by using the canonical string hash, not the string.

  12. hebasto commented at 12:14 PM on September 12, 2026: member

    This should fix the performance regression described in #35445 (comment)

    Do we need a benchmark for that?

  13. rkrux approved
  14. rkrux commented at 12:16 PM on September 12, 2026: contributor

    lgtm ACK 41aab45e17920400b011366d7c257898d999e430

    I notice a 50% improvement in execution time of the wallet_miniscript.py test on my machine.

  15. sedited added the label Needs Backport (32.x) on Sep 14, 2026
  16. fanquake commented at 9:18 AM on September 14, 2026: member
  17. in src/wallet/walletutil.h:140 in 27034d39cb
     136 | +        CalculateCanonicalHash();
     137 | +    }
     138 |  
     139 |      void UpdateFrom(const WalletDescriptor& other);
     140 | +
     141 | +    bool Equals(const WalletDescriptor& other) const;
    


    Sjors commented at 8:04 AM on September 15, 2026:

    In 27034d39cb59c67a80304bf1d1f18a72ccf3aec9 wallet: Compare descriptors by hash of canonical string: this should document that it only compares the canonical string.


    davidgumberg commented at 6:01 PM on September 16, 2026:

    yeah, and I think it would be wise to change the name from Equals() to something scarier


    achow101 commented at 8:36 PM on September 16, 2026:

    Added a comment and renamed to IsCanonicallyEquivalent


    Faiqonli commented at 6:12 AM on September 17, 2026:

    1GGC7C8mcPeoXxpjAn1duquC2GEHkatpmF

  18. in src/wallet/walletutil.h:70 in 27034d39cb
      65 | @@ -66,6 +66,11 @@ class WalletDescriptor
      66 |      int32_t range_start = 0; // First item in range; start of range, inclusive, i.e. [range_start, range_end). This never changes.
      67 |      int32_t next_index = 0; // Position of the next item to generate
      68 |      int32_t range_end = 0; // Item after the last; end of range, exclusive, i.e. [range_start, range_end). This will increment with each TopUp()
      69 | +
      70 | +    uint256 canonical_hash; // Hash of the canonical string, used as a shortcut for comparing canonical strings
    


    Sjors commented at 8:13 AM on September 15, 2026:

    In 27034d39cb59c67a80304bf1d1f18a72ccf3aec9 wallet: Compare descriptors by hash of canonical string: nit, it's never to late to use m_ (and it would make CalculateCanonicalHash easier to read).


    achow101 commented at 8:36 PM on September 16, 2026:

    Done

  19. in src/script/descriptor.cpp:273 in db58563a34
     270 | +        case StringType::CANONICAL:
     271 | +            // CANONICAL always uses h
     272 | +            use_apostrophe = false;
     273 | +            break;
     274 | +        case StringType::PUBLIC:
     275 | +            use_apostrophe = (!normalized && m_apostrophe) ? true : false;
    


    Sjors commented at 9:11 AM on September 15, 2026:

    In db58563a34949fbcf8bb4e477622400a8b4be5f5 descriptor: Explicitly handle use_apostrophe cases: nit, don't need no ternary.


    davidgumberg commented at 7:09 PM on September 16, 2026:

    no it aint need'nt it


    achow101 commented at 8:36 PM on September 16, 2026:

    Done

  20. Sjors commented at 9:15 AM on September 15, 2026: member

    Code review 41aab45e17920400b011366d7c257898d999e430.

    Two non-blocking questions / suggestions:

    • is there a reason why this cache lives on WalletDescriptor and not Descriptor?
    • an alternative design could be to have a just-in-time (private) CanonicalHash() getter.
  21. in src/wallet/walletutil.cpp:108 in 27034d39cb
     103 | +    CSHA256().Write((unsigned char*)canonical.data(), canonical.size()).Finalize(canonical_hash.begin());
     104 | +}
     105 | +
     106 | +bool WalletDescriptor::Equals(const WalletDescriptor& other) const
     107 | +{
     108 | +    Assert(!canonical_hash.IsNull() && !other.canonical_hash.IsNull());
    


    polespinasa commented at 9:33 AM on September 15, 2026:

    in 27034d39cb59c67a80304bf1d1f18a72ccf3aec9 wallet: Compare descriptors by hash of canonical string

    Why not instead of Assert just compute the hash if it was not already computed and then cache it?


    davidgumberg commented at 6:02 PM on September 16, 2026:

    +1


    davidgumberg commented at 6:05 PM on September 16, 2026:

    I think the best way to do this would be to change CalculateCanonicalHash() so that it returns the cached value if one exists, if not compute the hash and store it,

    Then you also don't have to do it in the constructor and can just lazily do it at comparison time


    davidgumberg commented at 6:17 PM on September 16, 2026:

    <details> <summary> example diff </summary>

    diff --git a/src/wallet/walletutil.cpp b/src/wallet/walletutil.cpp
    index 3bc013c174..ab32a5283b 100644
    --- a/src/wallet/walletutil.cpp
    +++ b/src/wallet/walletutil.cpp
    @@ -97,16 +97,21 @@ void WalletDescriptor::UpdateFrom(const WalletDescriptor& other)
         cache = other.cache;
     }
     
    -void WalletDescriptor::CalculateCanonicalHash()
    +uint256 WalletDescriptor::CalculateCanonicalHash() const
     {
    -    std::string canonical = descriptor->ToCanonicalString();
    -    CSHA256().Write((unsigned char*)canonical.data(), canonical.size()).Finalize(canonical_hash.begin());
    +    if (!m_canonical_hash) {
    +        std::string canonical = descriptor->ToCanonicalString();
    +        uint256 hash;
    +        CSHA256().Write((unsigned char*)canonical.data(), canonical.size()).Finalize(hash.begin());
    +        m_canonical_hash = hash;
    +    }
    +
    +    return m_canonical_hash.value();
     }
     
     bool WalletDescriptor::Equals(const WalletDescriptor& other) const
     {
    -    Assert(!canonical_hash.IsNull() && !other.canonical_hash.IsNull());
    -    return canonical_hash == other.canonical_hash;
    +    return CalculateCanonicalHash() == other.CalculateCanonicalHash();
     }
     
     } // namespace wallet
    diff --git a/src/wallet/walletutil.h b/src/wallet/walletutil.h
    index 6a3a111e6f..9b990cd246 100644
    --- a/src/wallet/walletutil.h
    +++ b/src/wallet/walletutil.h
    @@ -67,9 +67,8 @@ private:
         int32_t next_index = 0; // Position of the next item to generate
         int32_t range_end = 0; // Item after the last; end of range, exclusive, i.e. [range_start, range_end). This will increment with each TopUp()
     
    -    uint256 canonical_hash; // Hash of the canonical string, used as a shortcut for comparing canonical strings
    -
    -    void CalculateCanonicalHash();
    +    mutable std::optional<uint256> m_canonical_hash; // Hash of the canonical string, used as a shortcut for comparing canonical strings
    +    uint256 CalculateCanonicalHash() const;
     
     public:
         std::shared_ptr<Descriptor> descriptor;
    @@ -113,7 +112,6 @@ public:
                 throw std::ios_base::failure("Can't load a multipath descriptor from databases");
             }
             descriptor = std::move(descs.at(0));
    -        CalculateCanonicalHash();
         }
     
         SERIALIZE_METHODS(WalletDescriptor, obj)
    @@ -131,9 +129,7 @@ public:
           range_end(descriptor->IsRange() ? range_end : 1),
           descriptor(descriptor),
           creation_time(creation_time)
    -    {
    -        CalculateCanonicalHash();
    -    }
    +    {}
     
         void UpdateFrom(const WalletDescriptor& other);
    

    polespinasa commented at 6:19 PM on September 16, 2026:

    This would also fix this: #36230 (review)


    davidgumberg commented at 6:29 PM on September 16, 2026:

    The fact that this is being cached also means that there ideally should be some guard against the descriptor mutating during the lifetime of the WalletDescriptor:

         int32_t next_index = 0; // Position of the next item to generate
         int32_t range_end = 0; // Item after the last; end of range, exclusive, i.e. [range_start, range_end). This will increment with each TopUp()
     public:
    -    std::shared_ptr<Descriptor> descriptor;
    +    std::shared_ptr<const Descriptor> descriptor;
         uint64_t creation_time = 0;
         DescriptorCache cache;
    
         void DeserializeDescriptor(const std::string& str)
         {
    +        // This should not be used to mutate a descriptor.
    +        Assume(descriptor == nullptr);
    +
             std::string error;
             FlatSigningProvider keys;
             auto descs = Parse(str, keys, error, true);
    

    achow101 commented at 8:36 PM on September 16, 2026:

    Done

  22. in src/wallet/walletutil.h:127 in 27034d39cb


    polespinasa commented at 9:34 AM on September 15, 2026:

    in wallet: Compare descriptors by hash of canonical string 27034d39cb59c67a80304bf1d1f18a72ccf3aec9

    Default constructor does not initialize the hash. #36230 (review) would fix it tho.


    achow101 commented at 8:36 PM on September 16, 2026:

    I added a commit that removes the default constructor outright. We should do things RAII.

  23. in src/script/descriptor.cpp:537 in db58563a34
     534 | +            use_apostrophe = false;
     535 | +            break;
     536 | +        case StringType::PUBLIC:
     537 | +            use_apostrophe = (!normalized && m_apostrophe) ? true : false;
     538 | +            break;
     539 | +        } // no default case, so the compiler can warn about missing cases
    


    polespinasa commented at 9:48 AM on September 15, 2026:

    in db58563a34949fbcf8bb4e477622400a8b4be5f5 descriptor: Explicitly handle use_apostrophe cases

    The code is duplicated from OriginString. Maybe a helper function?

    <details> <summary>diff</summary>

    $ git diff
    diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp
    index 00028a6eb7..b3f09202cc 100644
    --- a/src/script/descriptor.cpp
    +++ b/src/script/descriptor.cpp
    @@ -249,6 +249,26 @@ public:
    
        /** Whether this PubkeyProvider can always provide a public key without cache or private key arguments */
        virtual bool CanSelfExpand() const = 0;
    +
    +protected:
    +    virtual bool UseApostrophe(StringType type, bool normalized, bool apostrophe) const
    +    {
    +        bool use_apostrophe{false};
    +            switch (type) {
    +            case StringType::COMPAT:
    +                // COMPAT always uses apostrophe to stay compatible with previous versions
    +                use_apostrophe = true;
    +                break;
    +            case StringType::CANONICAL:
    +                // CANONICAL always uses h
    +                use_apostrophe = false;
    +                break;
    +            case StringType::PUBLIC:
    +                use_apostrophe = (!normalized && apostrophe) ? true : false;
    +                break;
    +            } // no default case, so the compiler can warn about missing cases
    +        return use_apostrophe;
    +    }
    };
    
    class OriginPubkeyProvider final : public PubkeyProvider
    @@ -259,20 +279,7 @@ class OriginPubkeyProvider final : public PubkeyProvider
    
        std::string OriginString(StringType type, bool normalized=false) const
        {
    -        bool use_apostrophe{false};
    -        switch (type) {
    -        case StringType::COMPAT:
    -            // COMPAT always uses apostrophe to stay compatible with previous versions
    -            use_apostrophe = true;
    -            break;
    -        case StringType::CANONICAL:
    -            // CANONICAL always uses h
    -            use_apostrophe = false;
    -            break;
    -        case StringType::PUBLIC:
    -            use_apostrophe = (!normalized && m_apostrophe) ? true : false;
    -            break;
    -        } // no default case, so the compiler can warn about missing cases
    +        bool use_apostrophe{UseApostrophe(type, normalized, m_apostrophe)};
            return HexStr(m_origin.fingerprint) + FormatHDKeypath(m_origin.path, use_apostrophe);
        }
    
    @@ -521,20 +528,7 @@ public:
        }
        std::string ToString(StringType type, bool normalized) const
        {
    -        bool use_apostrophe{false};
    -        switch (type) {
    -        case StringType::COMPAT:
    -            // COMPAT always uses apostrophe to stay compatible with previous versions
    -            use_apostrophe = true;
    -            break;
    -        case StringType::CANONICAL:
    -            // CANONICAL always uses h
    -            use_apostrophe = false;
    -            break;
    -        case StringType::PUBLIC:
    -            use_apostrophe = (!normalized && m_apostrophe) ? true : false;
    -            break;
    -        } // no default case, so the compiler can warn about missing cases
    +        bool use_apostrophe{UseApostrophe(type, normalized, m_apostrophe)};
            std::string ret = EncodeExtPubKey(m_root_extkey) + FormatHDKeypath(m_path, /*apostrophe=*/use_apostrophe);
            if (IsRange()) {
                ret += "/*";
    
    

    </details>


    achow101 commented at 8:36 PM on September 16, 2026:

    Done

  24. in src/script/descriptor.h:251 in 814d486160
     247 | @@ -248,7 +248,7 @@ std::unique_ptr<Descriptor> InferDescriptor(const CScript& script, const Signing
     248 |   * Due to the hash's usage in previous versions, the COMPAT string is computed with some quirks.
     249 |   *
     250 |   * The hash is the sha256 of the public descriptor using apostrophes as the hardened indicator, except inside of
     251 | - * Miniscript expressions, where "h" is the hardened indicator.
     252 | + * Miniscript expressions, where the public serializtion is used as provided.
    


    polespinasa commented at 9:53 AM on September 15, 2026:

    in 814d48616002669864c75a335608be7b6249ea86 descriptor, doc: Clarify miniscript CompatDescriptorHash

    serializtion -> serialization

    In commit message:

    use whichever -> uses whichever.


    achow101 commented at 8:37 PM on September 16, 2026:

    Fixed

  25. in test/functional/wallet_backwards_compatibility.py:372 in 41aab45e17 outdated
     370 |                      # Miniscript descriptors imported into node versions other than 31.0 will
     371 | -                    # result in wallets that cannot be loaded into 31.0.
     372 | +                    # result in wallets that cannot be loaded into 31.0 and 31.1.
     373 |                      # These wallets will emit a "Wallet corrupted" error.
     374 | -                    if wallet_name == "miniscript" and n.version == 310000:
     375 | +                    if wallet_name == "miniscript" and (n.version == 310000 or n.version == 310100):
    


    polespinasa commented at 10:01 AM on September 15, 2026:

    in 41aab45e17920400b011366d7c257898d999e430 test: Use 31.1 for wallet back compat instead of 31.0

    Here you add a version for 31.1 and keep 31.0 but in add_nodes you change from 31.0 to 31.1. Maybe keep both in add_nodes or remove 31.0 in here?


    achow101 commented at 8:37 PM on September 16, 2026:

    Added 31.0


    maflcko commented at 6:03 AM on September 17, 2026:

    This is not resolved (CI still fails)

  26. polespinasa commented at 10:01 AM on September 15, 2026: member

    concept ACK 41aab45e17920400b011366d7c257898d999e430

    Left some comments.

  27. in src/wallet/walletutil.cpp:91 in 27034d39cb
      86 | @@ -87,8 +87,8 @@ WalletDescriptor GenerateWalletDescriptor(const CExtPubKey& master_key, const Ou
      87 |  
      88 |  void WalletDescriptor::UpdateFrom(const WalletDescriptor& other)
      89 |  {
      90 | -    if (descriptor->ToCanonicalString() != other.descriptor->ToCanonicalString()) {
      91 | -        return;
      92 | +    if (!Equals(other)) {
      93 | +         return;
    


    davidgumberg commented at 5:58 PM on September 16, 2026:

    nit: whitespace

    I wonder why this wasn't caught by the linter


    achow101 commented at 8:37 PM on September 16, 2026:

    Fixed

  28. achow101 commented at 8:14 PM on September 16, 2026: member

    Do we need a benchmark for that?

    I don't think so

  29. achow101 force-pushed on Sep 16, 2026
  30. DrahtBot added the label CI failed on Sep 16, 2026
  31. in test/get_previous_releases.py:112 in ad0cd36fdf outdated
     115 | +    "f81dd017a551c5af7fa2d6fa67b885077a8353322a8019e8fd538366bae1eff7": {"tag": "v31.1", "archive": "bitcoin-31.1-powerpc64-linux-gnu.tar.gz"},
     116 | +    "8a9213348a111438472653b8bd46c12184c60cc35ce0c2af02b853de4297cf94": {"tag": "v31.1", "archive": "bitcoin-31.1-riscv64-linux-gnu.tar.gz"},
     117 | +    "bc506958d0f387c1ea770bdc7c7192a505fa645ff62cabcc7761fa7eb89e867e": {"tag": "v31.1", "archive": "bitcoin-31.1-x86_64-apple-darwin.tar.gz"},
     118 | +    "b80d9c3e04da78fb6f0569685673418cf686fadba9042d926d13fb87ff503f9e": {"tag": "v31.1", "archive": "bitcoin-31.1-x86_64-linux-gnu.tar.gz"},
     119 | +    "c99ef173471c58e6766d9eebd12e6c35349082eeed3939bc99eed58ef57db587": {"tag": "v31.1", "archive": "bitcoin-31.1-win64.zip"},
     120 |  }
    


    polespinasa commented at 6:44 AM on September 17, 2026:

    The ci fails because some file isn't found. I never checked this previous release code, but I guess this is deleting the 31.0 file so backwards compatibility test cannot find the file. Instead of delete 31.0 and change for 31.1 shouldn't this commit just add 31.1?

  32. bitcoin deleted a comment on Sep 17, 2026
  33. achow101 force-pushed on Sep 17, 2026
  34. DrahtBot removed the label CI failed on Sep 17, 2026
  35. in src/wallet/walletutil.cpp:106 in 489e875874 outdated
     101 | +{
     102 | +    if (!m_canonical_hash) {
     103 | +        m_canonical_hash.emplace();
     104 | +        std::string canonical = descriptor->ToCanonicalString();
     105 | +        CSHA256().Write((unsigned char*)canonical.data(), canonical.size()).Finalize(m_canonical_hash->begin());
     106 | +    }
    


    polespinasa commented at 10:27 AM on September 17, 2026:

    in 489e87587476cc497b31e33f86106b20f89b66f2 wallet: Compare descriptors by hash of canonical string

    This is dead code as is now. Because every constructor pre-computes the canonical hash. Now that the default constructor is deleted I would just drop the GetCanonicalHash from the other constructors and only compute it the first time we need to compare it then store it for future usages.

    Otherwise can just go back to the assume and always pre-compute.

    I prefer first tho.


    davidgumberg commented at 6:19 PM on September 17, 2026:

    +1 to lazily computing it then caching

  36. in test/functional/wallet_backwards_compatibility.py:374 in f9359d06af
     372 |                      # Miniscript descriptors imported into node versions other than 31.0 will
     373 | -                    # result in wallets that cannot be loaded into 31.0.
     374 | +                    # result in wallets that cannot be loaded into 31.0 and 31.1.
     375 |                      # These wallets will emit a "Wallet corrupted" error.
     376 | -                    if wallet_name == "miniscript" and n.version == 310000:
     377 | +                    if wallet_name == "miniscript" and (n.version == 310000 or n.version == 310100):
    


    polespinasa commented at 10:55 AM on September 17, 2026:

    in f9359d06af84b070e93689c624fa149938b7e5a4 test: Add 31.1 to wallet back compat

                        if wallet_name == "miniscript" and n.version in (310000, 310100):
    
  37. in src/script/descriptor.cpp:254 in 838c5ff4b5
     248 | @@ -249,6 +249,26 @@ struct PubkeyProvider
     249 |  
     250 |      /** Whether this PubkeyProvider can always provide a public key without cache or private key arguments */
     251 |      virtual bool CanSelfExpand() const = 0;
     252 | +
     253 | +protected:
     254 | +    bool DetermineApostropheUse(StringType type, bool normalized, bool public_apostrophe) const
    


    polespinasa commented at 11:00 AM on September 17, 2026:

    in 838c5ff4b5686422f5d10ed32938e7380c6483ce descriptor: Explicitly handle use_apostrophe cases

    Static instead of const?

  38. polespinasa commented at 11:09 AM on September 17, 2026: member

    I think the descriptor should be protected from mutating, as David pointed in #36230 (review)

    If not we could store a hash that does not match the current descriptor.

  39. in src/wallet/walletutil.h:121 in 69f779caae outdated
     117 | @@ -118,14 +118,17 @@ class WalletDescriptor
     118 |          SER_READ(obj, obj.DeserializeDescriptor(descriptor_str));
     119 |      }
     120 |  
     121 | -    WalletDescriptor() = default;
     122 | +    WalletDescriptor() = delete;
    


    davidgumberg commented at 6:22 PM on September 17, 2026:

    https://github.com/bitcoin/bitcoin/pull/36230/changes/69f779caaec77adb4c07582fb4f766582a4f6209 (wallet: Remove WalletDescriptor's default constructor)

    after deleting the default constructor descriptor can be made const, this guards against the cached string becoming invalid:

    diff --git a/src/wallet/walletutil.h b/src/wallet/walletutil.h
    index dec2e95099..c3853a2011 100644
    --- a/src/wallet/walletutil.h
    +++ b/src/wallet/walletutil.h
    @@ -72,7 +72,7 @@ private:
         uint256 GetCanonicalHash() const;
     
     public:
    -    std::shared_ptr<Descriptor> descriptor;
    +    const std::shared_ptr<const Descriptor> descriptor;
         uint64_t creation_time = 0;
         DescriptorCache cache;
    
  40. achow101 force-pushed on Sep 17, 2026
  41. wallet: Remove WalletDescriptor's default constructor
    Drop the default constructor and add a factory function to construct a
    WalletDescriptor from stream
    4cc00f7bc0
  42. wallet: Make WalletDescriptor's descriptor const bb5e8320d7
  43. wallet: Compare descriptors by hash of canonical string
    The canonical string comparison was slow because it would compute the
    canonical string for each comparison. This can be sped up by holding the
    canonical string in memory, computed upon construction of
    WalletDescriptor. To reduce memory usage, this string is further hashed
    so that the comparison operates over the hash of the canonical string.
    64abb3e8d3
  44. descriptor: Explicitly handle use_apostrophe cases 405b1d6055
  45. descriptor, doc: Clarify miniscript CompatDescriptorHash
    The hardened indicator for Minscript expressions in
    CompatDescriptorHash uses whichever hardened indicator was originally
    given by the user.
    f0f6dce849
  46. wallet: Document WalletDescriptor::UpdateFrom f6cbcfd98a
  47. test: Simplify miniscript descriptor check in wallet back compat ebf2f69ee8
  48. test: Add 31.1 to wallet back compat 61edcf9253
  49. achow101 force-pushed on Sep 17, 2026
  50. in src/wallet/test/wallet_tests.cpp:593 in 4cc00f7bc0
     588 | @@ -1046,8 +589,8 @@ BOOST_FIXTURE_TEST_CASE(wallet_descriptor_test, BasicTestingSetup)
     589 |      vw << int32_t{1};
     590 |  
     591 |      SpanReader vr{malformed_record};
     592 | -    WalletDescriptor w_desc;
     593 | -    BOOST_CHECK_EXCEPTION(vr >> w_desc, std::ios_base::failure, malformed_descriptor);
     594 | +    std::optional<WalletDescriptor> w_desc;
     595 | +    BOOST_CHECK_EXCEPTION(w_desc.emplace(WalletDescriptor::FromStream(deserialize, vr)), std::ios_base::failure, malformed_descriptor);
    


    polespinasa commented at 7:20 AM on September 18, 2026:

    in 4cc00f7bc04b4c07827728ba5ce1094b338a76a8 wallet: Remove WalletDescriptor's default constructor

    nit: FromStream is what is being tested here, so creating an optional w_desc is unnecessary and emplace is dead code.

    $ git diff
    diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp
    index e515426119..42c0edf789 100644
    --- a/src/wallet/test/wallet_tests.cpp
    +++ b/src/wallet/test/wallet_tests.cpp
    @@ -589,8 +589,7 @@ BOOST_FIXTURE_TEST_CASE(wallet_descriptor_test, BasicTestingSetup)
         vw << int32_t{1};
     
         SpanReader vr{malformed_record};
    -    std::optional<WalletDescriptor> w_desc;
    -    BOOST_CHECK_EXCEPTION(w_desc.emplace(WalletDescriptor::FromStream(deserialize, vr)), std::ios_base::failure, malformed_descriptor);
    +    BOOST_CHECK_EXCEPTION(WalletDescriptor::FromStream(deserialize, vr), std::ios_base::failure, malformed_descriptor);
     }
    
    
  51. in src/wallet/walletutil.h:139 in 64abb3e8d3
     133 | @@ -129,9 +134,13 @@ class WalletDescriptor
     134 |        next_index(next_index),
     135 |        range_end(descriptor->IsRange() ? range_end : 1),
     136 |        descriptor(descriptor),
     137 | -      creation_time(creation_time) {}
     138 | +      creation_time(creation_time)
     139 | +    {}
     140 |  
    


    polespinasa commented at 7:40 AM on September 18, 2026:

    in 64abb3e8d3610449d541bfff2db23b9ace3fa111 wallet: Compare descriptors by hash of canonical string

    nit: added an unnecessary jump line.

  52. in src/wallet/walletutil.cpp:100 in 64abb3e8d3
      96 | @@ -97,4 +97,19 @@ void WalletDescriptor::UpdateFrom(const WalletDescriptor& other)
      97 |      cache = other.cache;
      98 |  }
      99 |  
     100 | +uint256 WalletDescriptor::GetCanonicalHash() const
    


    polespinasa commented at 7:41 AM on September 18, 2026:

    in 64abb3e8d3610449d541bfff2db23b9ace3fa111 wallet: Compare descriptors by hash of canonical string

    Commit message is wrong, it still mentions the previous approach behavior "computed on construction".

  53. in src/wallet/walletutil.h:142 in 64abb3e8d3
     138 | +      creation_time(creation_time)
     139 | +    {}
     140 |  
     141 |      void UpdateFrom(const WalletDescriptor& other);
     142 | +
     143 | +    // Compare by using the canonical string to make the hardened indicators consistent for comparison
    


    polespinasa commented at 7:45 AM on September 18, 2026:

    in 64abb3e8d3610449d541bfff2db23b9ace3fa111 wallet: Compare descriptors by hash of canonical string

    nit:

        // Compare by using the canonical string hash to make the hardened indicators consistent for comparison
    
  54. in src/script/descriptor.h:251 in f0f6dce849
     247 | @@ -248,7 +248,7 @@ std::unique_ptr<Descriptor> InferDescriptor(const CScript& script, const Signing
     248 |   * Due to the hash's usage in previous versions, the COMPAT string is computed with some quirks.
     249 |   *
     250 |   * The hash is the sha256 of the public descriptor using apostrophes as the hardened indicator, except inside of
     251 | - * Miniscript expressions, where "h" is the hardened indicator.
     252 | + * Miniscript expressions, where the public serialization is used as provided.
    


    polespinasa commented at 7:54 AM on September 18, 2026:

    in f0f6dce849634e801ac81705176ca8b96abbaa09 descriptor, doc: Clarify miniscript CompatDescriptorHash

    nit: in commit message Minscript -> Miniscript

  55. in test/functional/wallet_backwards_compatibility.py:371 in 61edcf9253
     366 | @@ -365,11 +367,11 @@ def run_test(self):
     367 |                      continue
     368 |                  # Also try to reopen on master after opening on old
     369 |                  for n in [node, node_master]:
     370 | -                    # 31.0 has a descriptor id calculation incompatibility.
     371 | +                    # 31.0 and 31.1 have a descriptor id calculation incompatibility.
     372 |                      # Miniscript descriptors imported into node versions other than 31.0 will
    


    polespinasa commented at 7:57 AM on September 18, 2026:

    in 61edcf92532502e91c2e17b8c2a23b45f5773936 test: Add 31.1 to wallet back compat

    nit: Missed to add 31.1 in this comment line too.

  56. polespinasa commented at 7:58 AM on September 18, 2026: member

    ACK 61edcf92532502e91c2e17b8c2a23b45f5773936

    Looks good to me. Left a few more nits in case you have to re-touch, feel free to ignore :)

  57. DrahtBot requested review from rkrux on Sep 18, 2026
  58. Sjors commented at 11:04 AM on September 18, 2026: member

    ACK 61edcf92532502e91c2e17b8c2a23b45f5773936

  59. fanquake merged this on Sep 19, 2026
  60. fanquake closed this on Sep 19, 2026

  61. fanquake removed the label Needs Backport (32.x) on Sep 19, 2026
  62. fanquake commented at 4:27 PM on September 19, 2026: member

    Backported to 32.x in #36300.

  63. fanquake referenced this in commit 98b9a497e0 on Sep 19, 2026
  64. fanquake referenced this in commit 655148d0ac on Sep 19, 2026
  65. fanquake referenced this in commit 7fdb8584da on Sep 19, 2026
  66. fanquake referenced this in commit 81e580401d on Sep 19, 2026
  67. fanquake referenced this in commit d17455aae8 on Sep 19, 2026
  68. fanquake referenced this in commit e6c57c0982 on Sep 19, 2026
  69. fanquake referenced this in commit c46dacf923 on Sep 19, 2026
  70. fanquake referenced this in commit 21a947002e on Sep 19, 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-09-20 13:52 UTC

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