{
 "number": 36648,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36648",
 "title": "descriptor, rpc, wallet: do not echo key material in error messages",
 "author": "fametrano",
 "author_association": "FIRST_TIME_CONTRIBUTOR",
 "created_at": "2026-10-24T12:49:24Z",
 "updated_at": "2026-11-01T19:55:29Z",
 "description_hash": "674f20d989c33524",
 "age_days": 8,
 "draft": false,
 "labels": [
  "CI failed"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "a0217c4781f1be9eb6d2300df53656b6d6907379",
 "head_ref": "descriptor-errors-no-keys",
 "head_repo": "fametrano/bitcoin",
 "head_history": [
  {
   "t": "2026-11-01T18:51:13Z",
   "sha": "a0217c4781f1be9eb6d2300df53656b6d6907379"
  }
 ],
 "additions": 225,
 "deletions": 62,
 "changed_files": 11,
 "commit_count": 1,
 "size_bucket": "M",
 "mergeable_state": "unstable",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "stale_ack": [
     {
      "login": "jpk68",
      "url": "https://github.com/bitcoin/bitcoin/pull/36648#issuecomment-6452939364"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "jpk68": {
   "kind": "ack",
   "hash": "a3f26e834323a129dc2663825ab77053dd2f698f",
   "t": "2026-10-27T20:35:57Z",
   "stale": true
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 1,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "jpk68",
   "uqlidi"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": true,
  "mergeable_state": "unstable",
  "last_author_activity": "2026-11-01T18:57:50Z",
  "last_reviewer_activity": "2026-10-28T17:59:38Z",
  "last_reviewer": "uqlidi",
  "author_silent_days": 0,
  "waiting_on_author_days": 0,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/script/descriptor.cpp",
  "src/util/bip32.cpp",
  "src/wallet/rpc/spend.cpp"
 ],
 "body": "Some descriptor error messages quote the string the caller sent, and a descriptor can hold a private key. The error then carries the key, or part of it, into places the request may not go: an application's log, an error reporter, a screenshot shared for help.\n\nThis PR stops the quoting. A failing key is named by its position, a failing entry of `solving_data.descriptors` by its index, and a function name is quoted only where the parser accepts it. The commit message lists each changed error.\n\n| before | after |\n| --- | --- |\n| `key '<key>' is not valid` | `key is not valid` |\n| `Multi: key '<key>' is not valid` | `Multi: key 3: key is not valid` |\n| `'wpkh(<key>)x' is not a valid descriptor function` | `Unexpected text after the closing ')' of wpkh()` |\n| `'<start of a key>(<rest>' is not a valid descriptor function` | `Not a valid descriptor expression` |\n| `Unable to parse descriptor '<descriptor>': Missing checksum` | `Unable to parse descriptor at index 1: Missing checksum` |\n| `Could not add descriptor '<descriptor>': ...` | the same, quoting the public form of the descriptor |",
 "commits": [
  {
   "sha": "a0217c4781f1be9eb6d2300df53656b6d6907379",
   "date": "2026-11-01T17:36:47Z",
   "message": "descriptor, rpc, wallet: do not echo key material in error messages\n\nSeveral error messages quote text from the descriptor the caller sent,\nand that text can be a private key. Error text is often logged or\nshared where the request is not. Even the first characters of a key\ngive away part of it.\n\nParsing (src/script/descriptor.cpp, src/util/bip32.cpp):\n- \"Key '%s' is invalid due to whitespace\", \"key '%s' is not valid\" and\n  \"Pubkey '%s' is invalid\" quoted the key. They quote nothing now.\n- A key that fails inside multi(), sortedmulti(), multi_a(),\n  sortedmulti_a(), musig() or a miniscript is named by its position, as\n  in \"Multi: key 3: key is not valid\" and \"Miniscript: key 2: key is not\n  valid\".\n- \"Multi threshold '%s' is not valid\", \"Key path value '%s' specifies\n  multipath ...\" and \"Key path value '%s' is not a valid uint32\" quote\n  nothing now. A stray ',' or '/' inside a key can leave a short\n  fragment of it as the threshold or a path value.\n- \"Provided checksum '%s' does not match computed checksum '%s'\" no\n  longer quotes the provided checksum, \"Key origin start '[ character\n  expected but not found, got '%c' instead\" no longer quotes the\n  character, and \"Fingerprint '%s' is not hex\" no longer quotes the\n  fingerprint. A stray '#', ']' or '[' inside a key puts part of it\n  there.\n- \"'%s' is not a valid descriptor function\" no longer quotes the\n  expression. A stray '(' inside a key leaves its first characters where\n  a function name is expected, and a stray ')' leaves its last\n  characters after the closing ')'. The message quotes the function name\n  only where it is one of the descriptor and miniscript functions the\n  parser accepts, as in \"Not a valid pk() expression\" and \"Unexpected\n  text after the closing ')' of wpkh()\". Otherwise it reads \"Not a valid\n  descriptor expression\", or \"Unexpected text after the closing ')'\".\n\nCallers (src/rpc/util.cpp, src/wallet/imports.cpp,\nsrc/wallet/rpc/spend.cpp):\n- \"Cannot derive script without private keys\" and \"Could not add\n  descriptor\" quote desc->ToString(), the public form, instead of the\n  string sent.\n- \"Unable to parse descriptor\" no longer quotes the string, and gives\n  its index in solving_data.descriptors instead. No parsed descriptor\n  exists there, and the parse error says what is wrong.\n\nTests are updated for the new messages. New tests check that no part of\na key is in the parse errors and the caller errors, including a key with\na typo in its last character or a stray ',', '/', '(', ')', '#', '[' or\n']' inside it. They also check the exact text of the caller errors."
  }
 ],
 "timeline": [
  {
   "t": "2026-10-27T20:35:57Z",
   "kind": "comment",
   "who": "jpk68",
   "assoc": "CONTRIBUTOR",
   "text": "ACK a3f26e834323a129dc2663825ab77053dd2f698f",
   "id": "c:6452939364",
   "url": "https://github.com/bitcoin/bitcoin/pull/36648#issuecomment-6452939364",
   "hash": "674175625cb07fe8",
   "edited": null
  },
  {
   "t": "2026-10-28T17:32:48Z",
   "kind": "review_comment",
   "who": "uqlidi",
   "assoc": "NONE",
   "path": "src/script/descriptor.cpp",
   "commit": "a3f26e834323a129dc2663825ab77053dd2f698f",
   "in_reply_to": null,
   "text": "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.\n\nyou can make it skip the key inside the function print the chars that is connected to `)`.\n\nsomethiing like that:\n\n```\nInvalid descriptor expression 'wpkh(...)x'\n```",
   "id": "rc:4403942538",
   "url": "https://github.com/bitcoin/bitcoin/pull/36648#discussion_r4403942538",
   "hash": "3f59536e2dc4bff5",
   "edited": "2026-10-28T18:00:08Z"
  },
  {
   "t": "2026-10-28T17:38:20Z",
   "kind": "review_comment",
   "who": "uqlidi",
   "assoc": "NONE",
   "path": "src/script/descriptor.cpp",
   "commit": "a3f26e834323a129dc2663825ab77053dd2f698f",
   "in_reply_to": 4403942538,
   "text": "suggested diff:\n```diff\ndiff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp\nindex 3ae2da81d..29dbfa3a7 100644\n--- a/src/script/descriptor.cpp\n+++ b/src/script/descriptor.cpp\n@@ -2828,15 +2828,29 @@ std::vector<std::unique_ptr<DescriptorImpl>> ParseScript(uint32_t& key_exp_index\n         error = \"A function is needed within P2WSH\";\n         return {};\n     }\n-    // Quote the function name only if it is 16 characters or fewer: every key is longer, and a bare key has no '('\n+    // Show the expression as \"name(...)rest\", where the arguments are never quoted, the name\n+    // only if it is 16 characters or fewer (every key is longer, and a bare key has no '('),\n+    // and the text after the matching ')' only if it is no longer than MAX_QUOTED_TOKEN_CHARS.\n     constexpr size_t MAX_FUNCTION_NAME_CHARS{16};\n     const std::string_view expr_text{expr.begin(), expr.end()};\n-    const size_t paren{expr_text.find('(')};\n-    if (paren == std::string_view::npos || paren == 0 || paren > MAX_FUNCTION_NAME_CHARS) {\n+    const size_t open{expr_text.find('(')};\n+    if (open == std::string_view::npos || open == 0 || open > MAX_FUNCTION_NAME_CHARS) {\n         error = \"Not a valid descriptor function\";\n-    } else {\n-        error = strprintf(\"'%s(...' is not a valid descriptor function\", expr_text.substr(0, paren));\n+        return {};\n+    }\n+    std::string shown{strprintf(\"%s(...\", expr_text.substr(0, open))};\n+    int depth{0};\n+    for (size_t i{open}; i < expr_text.size(); ++i) {\n+        if (expr_text[i] == '(') {\n+            ++depth;\n+        } else if (expr_text[i] == ')' && --depth == 0) {\n+            const std::string_view rest{expr_text.substr(i + 1)};\n+            shown += ')';\n+            shown += rest.size() <= MAX_QUOTED_TOKEN_CHARS ? rest : std::string_view{\"...\"};\n+            break;\n+        }\n     }\n+    error = strprintf(\"Invalid descriptor expression '%s'\", shown);\n     return {};\n }\n```",
   "id": "rc:4404002803",
   "url": "https://github.com/bitcoin/bitcoin/pull/36648#discussion_r4404002803",
   "hash": "927137a291ba4c57",
   "edited": "2026-10-28T18:00:08Z"
  },
  {
   "t": "2026-10-28T17:49:16Z",
   "kind": "review_comment",
   "who": "uqlidi",
   "assoc": "NONE",
   "path": "src/script/descriptor.cpp",
   "commit": "a0217c4781f1be9eb6d2300df53656b6d6907379",
   "in_reply_to": null,
   "text": "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:\n```\nMulti: key is not valid\n```\nWith 15 keys, the user has to check each one by hand.\n\nYou can print the key's position. `key_exp_index` already counts the keys parsed before the failing one, so no key text is needed.\n\nSomething like that:\n```\nMulti: Key #3 is not valid\n```\nNote: 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.",
   "id": "rc:4404122186",
   "url": "https://github.com/bitcoin/bitcoin/pull/36648#discussion_r4404122186",
   "hash": "18d3a10c6a2a5f16",
   "edited": "2026-10-28T18:00:08Z"
  },
  {
   "t": "2026-10-28T17:49:19Z",
   "kind": "review_comment",
   "who": "uqlidi",
   "assoc": "NONE",
   "path": "src/script/descriptor.cpp",
   "commit": "a0217c4781f1be9eb6d2300df53656b6d6907379",
   "in_reply_to": null,
   "text": "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:\n```\nMulti: key is not valid\n```\nWith 15 keys, the user has to check each one by hand.\n\nYou can print the key's position. `key_exp_index` already counts the keys parsed before the failing one, so no key text is needed.\n\nSomething like that:\n```\nMulti: Key #3 is not valid\n```\nNote: 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.",
   "id": "rc:4404122555",
   "url": "https://github.com/bitcoin/bitcoin/pull/36648#discussion_r4404122555",
   "hash": "18d3a10c6a2a5f16",
   "edited": "2026-10-28T18:00:08Z"
  },
  {
   "t": "2026-10-28T17:56:32Z",
   "kind": "review_comment",
   "who": "uqlidi",
   "assoc": "NONE",
   "path": "src/wallet/rpc/spend.cpp",
   "commit": "a3f26e834323a129dc2663825ab77053dd2f698f",
   "in_reply_to": null,
   "text": "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:\n```\nUnable to parse descriptor: Missing checksum\n```\nYou can print its index in the list.\n\nshould be:\n```\nUnable to parse descriptor at index 1: Missing checksum\n```\n\nwe could do something like that instead:\n```diff\ndiff --git a/src/wallet/rpc/spend.cpp b/src/wallet/rpc/spend.cpp\nindex 383f752dc..0ab874793 100644\n--- a/src/wallet/rpc/spend.cpp\n+++ b/src/wallet/rpc/spend.cpp\n@@ -622,14 +622,15 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact\n         }\n\n         if (solving_data.exists(\"descriptors\")) {\n-            for (const UniValue& desc_univ : solving_data[\"descriptors\"].get_array().getValues()) {\n-                const std::string& desc_str  = desc_univ.get_str();\n+            const auto& desc_values{solving_data[\"descriptors\"].get_array().getValues()};\n+            for (size_t desc_index{0}; desc_index < desc_values.size(); ++desc_index) {\n+                const std::string& desc_str{desc_values[desc_index].get_str()};\n                 FlatSigningProvider desc_out;\n                 std::string error;\n                 std::vector<CScript> scripts_temp;\n                 auto descs = Parse(desc_str, desc_out, error, true);\n                 if (descs.empty()) {\n-                    throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf(\"Unable to parse descriptor: %s\", error));\n+                    throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf(\"Unable to parse descriptor at index %u: %s\", desc_index, error));\n                 }\n                 for (auto& desc : descs) {\n                     desc->Expand(0, desc_out, scripts_temp, desc_out);\n```",
   "id": "rc:4404196067",
   "url": "https://github.com/bitcoin/bitcoin/pull/36648#discussion_r4404196067",
   "hash": "865c9fcb1473f5a7",
   "edited": "2026-10-28T18:00:50Z"
  },
  {
   "t": "2026-10-28T17:59:38Z",
   "kind": "review_comment",
   "who": "uqlidi",
   "assoc": "NONE",
   "path": "src/util/bip32.cpp",
   "commit": "a3f26e834323a129dc2663825ab77053dd2f698f",
   "in_reply_to": null,
   "text": "i see you are using the same value of `MAX_QUOTED_CHARS` in https://github.com/bitcoin/bitcoin/pull/36648/changes#diff-dcb25ae6b5207f65ae6a1cd364c9e62e5a9df69a1cd21b9ed79849cc99c2150bR1880\n\nyou can define it here and reuse it inside descriptor.cpp",
   "id": "rc:4404226504",
   "url": "https://github.com/bitcoin/bitcoin/pull/36648#discussion_r4404226504",
   "hash": "a189aec0efe71f8f",
   "edited": "2026-10-28T18:00:08Z"
  },
  {
   "t": "2026-11-01T18:51:13Z",
   "kind": "force_push",
   "who": "fametrano",
   "commit": "a0217c4781f1be9eb6d2300df53656b6d6907379",
   "id": "push:a0217c4781f1be9eb6d2300df53656b6d6907379"
  },
  {
   "t": "2026-11-01T18:52:03Z",
   "kind": "review_comment",
   "who": "fametrano",
   "assoc": "NONE",
   "path": "src/script/descriptor.cpp",
   "commit": "a3f26e834323a129dc2663825ab77053dd2f698f",
   "in_reply_to": 4403942538,
   "text": "Done: `wpkh(KEY)x` now gives `Unexpected text after the closing ')' of wpkh()`, which quotes the name only because it is a function the parser accepts, and never the text after the `)`, which can be the end of a key.",
   "id": "rc:4436906602",
   "url": "https://github.com/bitcoin/bitcoin/pull/36648#discussion_r4436906602",
   "hash": "2109f1ccde22d3ae",
   "edited": "2026-11-01T18:56:51Z"
  },
  {
   "t": "2026-11-01T18:52:38Z",
   "kind": "review_comment",
   "who": "fametrano",
   "assoc": "NONE",
   "path": "src/script/descriptor.cpp",
   "commit": "a0217c4781f1be9eb6d2300df53656b6d6907379",
   "in_reply_to": 4404122186,
   "text": "Done: the error gives the key's position in its list, counted from 1, as in `Multi: key 3: key is not valid`, and a key after a `musig()` is not counted one higher.",
   "id": "rc:4436908363",
   "url": "https://github.com/bitcoin/bitcoin/pull/36648#discussion_r4436908363",
   "hash": "90812e6f5487d302",
   "edited": "2026-11-01T18:57:13Z"
  },
  {
   "t": "2026-11-01T18:52:40Z",
   "kind": "review_comment",
   "who": "fametrano",
   "assoc": "NONE",
   "path": "src/script/descriptor.cpp",
   "commit": "a0217c4781f1be9eb6d2300df53656b6d6907379",
   "in_reply_to": 4404122555,
   "text": "Done, as in the other copy of this comment: `Multi: key 3: key is not valid`, counted within the list.",
   "id": "rc:4436908470",
   "url": "https://github.com/bitcoin/bitcoin/pull/36648#discussion_r4436908470",
   "hash": "6dc7ebae2ef7cd1f",
   "edited": "2026-11-01T18:57:15Z"
  },
  {
   "t": "2026-11-01T18:52:42Z",
   "kind": "review_comment",
   "who": "fametrano",
   "assoc": "NONE",
   "path": "src/wallet/rpc/spend.cpp",
   "commit": "a3f26e834323a129dc2663825ab77053dd2f698f",
   "in_reply_to": 4404196067,
   "text": "Done with your loop: `Unable to parse descriptor at index 1: Missing checksum`.",
   "id": "rc:4436908572",
   "url": "https://github.com/bitcoin/bitcoin/pull/36648#discussion_r4436908572",
   "hash": "cb1e2fe4cc69c302",
   "edited": "2026-11-01T18:57:18Z"
  },
  {
   "t": "2026-11-01T18:52:44Z",
   "kind": "review_comment",
   "who": "fametrano",
   "assoc": "NONE",
   "path": "src/util/bip32.cpp",
   "commit": "a3f26e834323a129dc2663825ab77053dd2f698f",
   "in_reply_to": 4404226504,
   "text": "`MAX_QUOTED_CHARS` is gone: no error quotes text by length any more, so there is nothing left to share with `bip32.cpp`.",
   "id": "rc:4436908721",
   "url": "https://github.com/bitcoin/bitcoin/pull/36648#discussion_r4436908721",
   "hash": "ffa758ceac6c431c",
   "edited": "2026-11-01T18:57:20Z"
  },
  {
   "t": "2026-11-01T18:57:50Z",
   "kind": "comment",
   "who": "fametrano",
   "assoc": "NONE",
   "text": "@uqlidi Thanks, I addressed all four points; the replies are inline.",
   "id": "c:6562416965",
   "url": "https://github.com/bitcoin/bitcoin/pull/36648#issuecomment-6562416965",
   "hash": "ad633e206a929ffe",
   "edited": null
  }
 ],
 "labels_log": [
  {
   "t": "2026-11-01T19:55:05Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 10104,
 "text_tokens_estimate": 2526,
 "changed_paths": [
  "src/rpc/util.cpp",
  "src/script/descriptor.cpp",
  "src/test/descriptor_tests.cpp",
  "src/util/bip32.cpp",
  "src/wallet/imports.cpp",
  "src/wallet/rpc/spend.cpp",
  "test/functional/rpc_getdescriptorinfo.py",
  "test/functional/rpc_scantxoutset.py",
  "test/functional/wallet_fundrawtransaction.py",
  "test/functional/wallet_importdescriptors.py",
  "test/functional/wallet_miniscript.py"
 ],
 "files": [
  {
   "path": "src/rpc/util.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/script/descriptor.cpp",
   "add": 27,
   "del": 6
  },
  {
   "path": "src/test/descriptor_tests.cpp",
   "add": 59,
   "del": 10
  },
  {
   "path": "src/util/bip32.cpp",
   "add": 2,
   "del": 0
  },
  {
   "path": "src/wallet/imports.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/wallet/rpc/spend.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "test/functional/rpc_getdescriptorinfo.py",
   "add": 32,
   "del": 6
  },
  {
   "path": "test/functional/rpc_scantxoutset.py",
   "add": 18,
   "del": 0
  },
  {
   "path": "test/functional/wallet_fundrawtransaction.py",
   "add": 12,
   "del": 1
  },
  {
   "path": "test/functional/wallet_importdescriptors.py",
   "add": 9,
   "del": 6
  },
  {
   "path": "test/functional/wallet_miniscript.py",
   "add": 1,
   "del": 1
  }
 ],
 "test_lines": 155,
 "git": {
  "head": "a3f26e834323a129dc2663825ab77053dd2f698f",
  "head_matches_backup": true,
  "base": "a773e1e33b349dc819b5a0edc1c7a4839dc73b48",
  "patch_id": "b3bf271d1f852e6f",
  "commits": [
   {
    "sha": "a3f26e8343",
    "subject": "descriptor, rpc, wallet: do not echo key material in error messages",
    "files": 11,
    "add": 163,
    "del": 33
   }
  ],
  "patch_truncated": true
 },
 "patch_id": "b3bf271d1f852e6f",
 "input_hash": "084245c8a5706472",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}