descriptor, rpc, wallet: do not echo key material in error messages #36648

pull fametrano wants to merge 1 commits into bitcoin:master from fametrano:descriptor-errors-no-keys changing 11 files +163 −33
  1. fametrano commented at 12:49 PM on October 24, 2026: none

    Some descriptor error messages quote the string the caller sent, and a descriptor can hold a private key (a WIF, an xprv). The reply then carries the key into places the request may not go: an application's error log, an error reporter, a screenshot shared for help. Even the first characters of a key give away part of it.

    This PR stops the quoting.

    Parsing (descriptor.cpp, util/bip32.cpp):

    before after
    Key ' <key>' is invalid due to whitespace Key is invalid due to whitespace
    key '<key>' is not valid key is not valid
    Pubkey '<hex>' is invalid Pubkey is invalid
    Multi threshold '<text>' is not valid same for text of 11 characters or fewer, the length of 4294967295h; Multi threshold is not valid for longer text
    Key path value '<text>' specifies multipath in a section where multipath is not allowed same rule: Key path value specifies multipath in a section where multipath is not allowed for longer text
    Key path value '<text>' is not a valid uint32 same rule: Key path value is not a valid uint32 for longer text
    '<expr>' is not a valid descriptor function '<name>(...' is not a valid descriptor function, where <name> is the text up to (; Not a valid descriptor function if there is no ( or the name is longer than 16 characters

    No key is 11 characters or fewer.

    Callers:

    • Cannot derive script without private keys: '...' (rpc/util.cpp) and Could not add descriptor '...': ... (wallet/imports.cpp) now quote desc->ToString(), the public form with its checksum.
    • Unable to parse descriptor '...': ... (wallet/rpc/spend.cpp) no longer quotes the string: there is no parsed descriptor to print, and the parse error says what is wrong.

    Existing assertions on these messages are updated. New tests check that no part of a key is in the parse errors and the caller errors, including a key with a typo in its last character. They also check the exact text of the caller errors.

    This is one of several PRs from a large audit of Bitcoin Core that I did with the help of LLM tools. The audit produced its evidence all at once, which is why the PRs arrive together. I understand each change fully and will answer review questions myself.

  2. descriptor, rpc, wallet: do not echo key material in error messages
    Several error messages quote text from the descriptor the caller sent,
    and that text can be a private key. Error text is often logged or
    shared where the request is not. Even the first characters of a key
    give away part of it.
    
    Parsing (src/script/descriptor.cpp, src/util/bip32.cpp):
    - "Key '%s' is invalid due to whitespace", "key '%s' is not valid" and
      "Pubkey '%s' is invalid" quoted the key. They quote nothing now.
    - "Multi threshold '%s' is not valid" and the multipath message "Key
      path value '%s' specifies multipath ..." quote their text only if it
      is no longer than 11 characters, the length of "4294967295h". No key
      is that short. Longer text is left out.
    - "Key path value '%s' is not a valid uint32" follows the same rule.
    - "'%s' is not a valid descriptor function" quotes the function name, the
      text up to '(', followed by "(...", as in "'pk(...' is not a valid
      descriptor function". It does so only if the name is 16 characters or
      fewer. Otherwise, and for a bare key with no '(', nothing is quoted,
      and the message reads "Not a valid descriptor function".
    
    Callers (src/rpc/util.cpp, src/wallet/imports.cpp,
    src/wallet/rpc/spend.cpp):
    - "Cannot derive script without private keys" and "Could not add
      descriptor" quote desc->ToString(), the public form, instead of the
      string sent.
    - "Unable to parse descriptor" no longer quotes the string. No parsed
      descriptor exists there, and the parse error says what is wrong.
    
    Tests are updated for the new messages. New tests check that no part of
    a key is in the parse errors and the caller errors, including a key with
    a typo in its last character. They also check the exact text of the
    caller errors.
    a3f26e8343
  3. DrahtBot commented at 12:49 PM on October 24, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    External sites

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

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

    Type Reviewers
    ACK jpk68

    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:

    • #36402 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36402.svg"></sub> (refactor: wallet_importdescriptors to separate run_test() contents into specific functions by nebula-21)
    • #36257 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36257.svg"></sub> (qa: assert_equals -> assert_true/assert_false by hodlinator)
    • #35918 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35918.svg"></sub> (descriptor: Expose key expression parsing by pseudoramdom)

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

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. jpk68 commented at 8:35 PM on October 27, 2026: contributor

    ACK a3f26e834323a129dc2663825ab77053dd2f698f

  5. in src/script/descriptor.cpp:2831 in a3f26e8343
    2827 | +    const std::string_view expr_text{expr.begin(), expr.end()};
    2828 | +    const size_t paren{expr_text.find('(')};
    2829 | +    if (paren == std::string_view::npos || paren == 0 || paren > MAX_FUNCTION_NAME_CHARS) {
    2830 | +        error = "Not a valid descriptor function";
    2831 | +    } else {
    2832 | +        error = strprintf("'%s(...' is not a valid descriptor function", expr_text.substr(0, paren));
    


    uqlidi commented at 5:32 PM on October 28, 2026:

    For wpkh(KEY)x, the error is now 'wpkh(...' is not a valid descriptor function. wpkh is a valid function; the real problem is the trailing x. The old message echoed the full text, so the mistake was visible.

    you can make it skip the key inside the function print the chars that is connected to ).

    somethiing like that:

    Invalid descriptor expression 'wpkh(...)x'
    

    uqlidi commented at 5:38 PM on October 28, 2026:

    suggested diff:

    diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp
    index 3ae2da81d..29dbfa3a7 100644
    --- a/src/script/descriptor.cpp
    +++ b/src/script/descriptor.cpp
    @@ -2828,15 +2828,29 @@ std::vector<std::unique_ptr<DescriptorImpl>> ParseScript(uint32_t& key_exp_index
             error = "A function is needed within P2WSH";
             return {};
         }
    -    // Quote the function name only if it is 16 characters or fewer: every key is longer, and a bare key has no '('
    +    // Show the expression as "name(...)rest", where the arguments are never quoted, the name
    +    // only if it is 16 characters or fewer (every key is longer, and a bare key has no '('),
    +    // and the text after the matching ')' only if it is no longer than MAX_QUOTED_TOKEN_CHARS.
         constexpr size_t MAX_FUNCTION_NAME_CHARS{16};
         const std::string_view expr_text{expr.begin(), expr.end()};
    -    const size_t paren{expr_text.find('(')};
    -    if (paren == std::string_view::npos || paren == 0 || paren > MAX_FUNCTION_NAME_CHARS) {
    +    const size_t open{expr_text.find('(')};
    +    if (open == std::string_view::npos || open == 0 || open > MAX_FUNCTION_NAME_CHARS) {
             error = "Not a valid descriptor function";
    -    } else {
    -        error = strprintf("'%s(...' is not a valid descriptor function", expr_text.substr(0, paren));
    +        return {};
    +    }
    +    std::string shown{strprintf("%s(...", expr_text.substr(0, open))};
    +    int depth{0};
    +    for (size_t i{open}; i < expr_text.size(); ++i) {
    +        if (expr_text[i] == '(') {
    +            ++depth;
    +        } else if (expr_text[i] == ')' && --depth == 0) {
    +            const std::string_view rest{expr_text.substr(i + 1)};
    +            shown += ')';
    +            shown += rest.size() <= MAX_QUOTED_TOKEN_CHARS ? rest : std::string_view{"..."};
    +            break;
    +        }
         }
    +    error = strprintf("Invalid descriptor expression '%s'", shown);
         return {};
     }
    
  6. in src/script/descriptor.cpp:2037 in a3f26e8343
    2033 | @@ -2021,7 +2034,7 @@ std::vector<std::unique_ptr<PubkeyProvider>> ParsePubkeyInner(uint32_t& key_exp_
    2034 |                      return ret;
    2035 |                  }
    2036 |              }
    2037 | -            error = strprintf("Pubkey '%s' is invalid", str);
    2038 | +            error = "Pubkey is invalid";
    


    uqlidi commented at 5:49 PM on October 28, 2026:

    In a descriptor with many keys, the error doesn't say which one failed. For wsh(multi(2,KEY1,KEY2,KEY3)) with a typo in KEY3, the error is only:

    Multi: key is not valid
    

    With 15 keys, the user has to check each one by hand.

    You can print the key's position. key_exp_index already counts the keys parsed before the failing one, so no key text is needed.

    Something like that:

    Multi: Key [#3](/bitcoin-bitcoin/3/) is not valid
    

    Note: the count includes each musig() aggregate key, so a key after a musig() gets a number one higher than a user would count. The test expectations for these messages still need updating.


    uqlidi commented at 5:49 PM on October 28, 2026:

    In a descriptor with many keys, the error doesn't say which one failed. For wsh(multi(2,KEY1,KEY2,KEY3)) with a typo in KEY3, the error is only:

    Multi: key is not valid
    

    With 15 keys, the user has to check each one by hand.

    You can print the key's position. key_exp_index already counts the keys parsed before the failing one, so no key text is needed.

    Something like that:

    Multi: Key [#3](/bitcoin-bitcoin/3/) is not valid
    

    Note: the count includes each musig() aggregate key, so a key after a musig() gets a number one higher than a user would count. The test expectations for these messages still need updating.

  7. in src/wallet/rpc/spend.cpp:632 in a3f26e8343
     628 | @@ -629,7 +629,7 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact
     629 |                  std::vector<CScript> scripts_temp;
     630 |                  auto descs = Parse(desc_str, desc_out, error, true);
     631 |                  if (descs.empty()) {
     632 | -                    throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Unable to parse descriptor '%s': %s", desc_str, error));
     633 | +                    throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Unable to parse descriptor: %s", error));
    


    uqlidi commented at 5:56 PM on October 28, 2026:

    similar to the key index solving_data.descriptors is a list, now that the descriptor is no longer printed, the error doesn't say which one failed:

    Unable to parse descriptor: Missing checksum
    

    You can print its index in the list.

    should be:

    Unable to parse descriptor at index 1: Missing checksum
    

    we could do something like that instead:

    diff --git a/src/wallet/rpc/spend.cpp b/src/wallet/rpc/spend.cpp
    index 383f752dc..0ab874793 100644
    --- a/src/wallet/rpc/spend.cpp
    +++ b/src/wallet/rpc/spend.cpp
    @@ -622,14 +622,15 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact
             }
     
             if (solving_data.exists("descriptors")) {
    -            for (const UniValue& desc_univ : solving_data["descriptors"].get_array().getValues()) {
    -                const std::string& desc_str  = desc_univ.get_str();
    +            const auto& desc_values{solving_data["descriptors"].get_array().getValues()};
    +            for (size_t desc_index{0}; desc_index < desc_values.size(); ++desc_index) {
    +                const std::string& desc_str{desc_values[desc_index].get_str()};
                     FlatSigningProvider desc_out;
                     std::string error;
                     std::vector<CScript> scripts_temp;
                     auto descs = Parse(desc_str, desc_out, error, true);
                     if (descs.empty()) {
    -                    throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Unable to parse descriptor: %s", error));
    +                    throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Unable to parse descriptor at index %u: %s", desc_index, error));
                     }
                     for (auto& desc : descs) {
                         desc->Expand(0, desc_out, scripts_temp, desc_out);
    
  8. in src/util/bip32.cpp:34 in a3f26e8343
      29 | @@ -30,6 +30,8 @@ util::Expected<KeyPathElement, std::string> ParseKeyPathElement(std::span<const
      30 |  
      31 |      const auto number{ToIntegral<uint32_t>(std::string_view{elem.begin(), elem.end()})};
      32 |      if (!number) {
      33 | +        // A value longer than any uint32 may be a key written in the path: do not quote it
      34 | +        if (raw.size() > 11) return util::Unexpected{std::string{"Key path value is not a valid uint32"}};
    


    uqlidi commented at 5:59 PM on October 28, 2026:

    i see you are using the same value of MAX_QUOTED_CHARS in https://github.com/bitcoin/bitcoin/pull/36648/changes#diff-dcb25ae6b5207f65ae6a1cd364c9e62e5a9df69a1cd21b9ed79849cc99c2150bR1880

    you can define it here and reuse it inside descriptor.cpp


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-10-30 15:51 UTC

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