{
 "number": 35918,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35918",
 "title": "descriptor: Expose key expression parsing",
 "author": "pseudoramdom",
 "author_association": "CONTRIBUTOR",
 "created_at": "2026-08-06T18:44:09Z",
 "updated_at": "2026-08-28T17:10:28Z",
 "age_days": 41,
 "draft": false,
 "labels": [
  "Needs rebase",
  "Descriptors"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "93600f9f0360bf48a1f616f0f5db3c9ce985ce4b",
 "head_ref": "parse-key-expression-v2",
 "head_repo": "pseudoramdom/bitcoin",
 "head_history": [
  {
   "t": "2026-08-11T19:29:09Z",
   "sha": "93600f9f0360bf48a1f616f0f5db3c9ce985ce4b"
  }
 ],
 "additions": 302,
 "deletions": 86,
 "changed_files": 3,
 "commit_count": 3,
 "size_bucket": "M",
 "mergeable_state": "dirty",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "w0xlt",
      "url": "https://github.com/bitcoin/bitcoin/pull/35918#pullrequestreview-4931348036"
     }
    ]
   },
   "conflicts": [
    {
     "number": 35742,
     "title": "descriptors: check duplicate keys in all multipath Miniscript branches",
     "author": "yashbhutwala"
    },
    {
     "number": 35445,
     "title": "wallet, descriptor: Revert `StringType::COMPAT` for Miniscript expressions and drop the concept of a Descriptor ID that can be validated",
     "author": "achow101"
    },
    {
     "number": 34697,
     "title": "descriptor: fix musig() duplicate key checks and doubled PSBT origin paths",
     "author": "shuv-amp"
    }
   ]
  }
 },
 "acks_parsed": {
  "w0xlt": {
   "kind": "ack",
   "hash": "93600f9f0360bf48a1f616f0f5db3c9ce985ce4b",
   "t": "2026-08-13T20:32:32Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 1,
  "stale_ack": 0,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "uqlidi",
   "w0xlt"
  ]
 },
 "signals": {
  "needs_rebase": true,
  "ci_failed": false,
  "mergeable_state": "dirty",
  "last_author_activity": "2026-08-11T19:29:09Z",
  "last_reviewer_activity": "2026-08-28T17:05:22Z",
  "last_reviewer": "uqlidi",
  "author_silent_days": 36,
  "waiting_on_author_days": 19,
  "days_since_update": 19
 },
 "refs": {
  "mentioned": [],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [],
  "conflicts": [
   35742,
   35445,
   34697
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/script/descriptor.cpp"
 ],
 "body": "This PR exposes a utility function to parse and validate a standalone descriptor key expression.\nThis is particularly useful in GUI multisig setup flows where the UI accepts a pasted key expression. The GUI can use this parser to validate input, inspect the parsed key,  and apply setup-specific policy i.e. reject private keys, uncompressed keys etc. (Multisig Wizard tracking issue: https://github.com/bitcoin/bitcoin/issues/35645)\n\n`PubkeyProvider` is the existing internal representation of a parsed key expression. This PR exposes its interface in `descriptor.h` and adds a public `ParsePubkey()` overload that returns the providers produced by the existing parser.\n\nKey changes\n- public overload uses P2TR key rules, permitting compressed, x-only, extended, and MuSig key expressions while rejecting uncompressed keys.\n- Add `GetOriginInfo()` for retrieving explicitly provided key origin information.\n\n---\nAlso, while I'm here, I noticed that `musig` parser accepted trailing characters apart from `/`.\n- Add a fix & test to reject unexpected trailing characters after a `musig()` expression",
 "commits": [
  {
   "sha": "33af6fdbc18408782ce956dfadbaae1634e155f7",
   "date": "2026-08-11T19:12:39Z",
   "message": "descriptor: Reject trailing text after musig()\n\nThe musig() parser split the input at the closing parenthesis without\nvalidating the remaining text. This allowed unexpected characters after\nthe expression to be ignored.\nAdd a requirement for nonempty char after musig() to begin with '/'."
  },
  {
   "sha": "296898cebd2a8bbd00f6f49b720bc0ad911bd855",
   "date": "2026-08-11T19:21:04Z",
   "message": "descriptor: move PubkeyProvider to descriptor.h\n\nExpose the abstract interface by moving to descriptor.h to allow other\nmodules to consume parsed key expressions.\nMostly a move-only commit. Implementation of operator< is retained in\ndescriptor.cpp"
  },
  {
   "sha": "93600f9f0360bf48a1f616f0f5db3c9ce985ce4b",
   "date": "2026-08-11T19:21:56Z",
   "message": "descriptor: Add public ParsePubKey overload\n\nAdd an overload for parsing standalone key expressions using P2TR key\nrules.\nAlso, expose explicitly provided origin information through GetOriginInfo(),\nallowing callers to inspect the fingerprint and derivation path without\nre-parsing the expression."
  }
 ],
 "timeline": [
  {
   "t": "2026-08-11T19:29:09Z",
   "kind": "force_push",
   "who": "pseudoramdom",
   "commit": "93600f9f0360bf48a1f616f0f5db3c9ce985ce4b"
  },
  {
   "t": "2026-08-13T20:32:32Z",
   "kind": "review",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "93600f9f0360bf48a1f616f0f5db3c9ce985ce4b",
   "text": "ACK 93600f9f0360bf48a1f616f0f5db3c9ce985ce4b"
  },
  {
   "t": "2026-08-28T17:05:22Z",
   "kind": "review_comment",
   "who": "uqlidi",
   "assoc": "NONE",
   "path": "src/script/descriptor.cpp",
   "commit": "93600f9f0360bf48a1f616f0f5db3c9ce985ce4b",
   "in_reply_to": null,
   "text": "```suggestion\nstd::vector<std::unique_ptr<PubkeyProvider>> ParsePubkey(std::string_view key_expression, ParseScriptContext ctx, FlatSigningProvider& out, std::string& error)\n{\n    uint32_t key_exp_index{0};\n    const std::span<const char> expression{key_expression};\n    return ParsePubkey(key_exp_index, expression, ctx, out, error);\n}\n```\n\nI see you hardcoded it for p2tr. shouldn't be for all script types? Why are you making the overload follow the p2tr rules only?"
  }
 ],
 "labels_log": [
  {
   "t": "2026-08-06T18:44:13Z",
   "action": "labeled",
   "label": "Descriptors",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-11T11:55:20Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-11T21:10:48Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-24T11:29:09Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-08-07T06:08:44Z",
   "kind": "closed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-07T06:08:47Z",
   "kind": "reopened",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-11T19:29:19Z",
   "kind": "ready_for_review",
   "who": "pseudoramdom"
  }
 ],
 "text_chars": 2490,
 "text_tokens_estimate": 622,
 "changed_paths": [
  "src/script/descriptor.cpp",
  "src/script/descriptor.h",
  "src/test/descriptor_tests.cpp"
 ],
 "files": [
  {
   "path": "src/script/descriptor.cpp",
   "add": 32,
   "del": 86
  },
  {
   "path": "src/script/descriptor.h",
   "add": 85,
   "del": 0
  },
  {
   "path": "src/test/descriptor_tests.cpp",
   "add": 185,
   "del": 0
  }
 ],
 "test_lines": 185,
 "git": {
  "head": "93600f9f0360bf48a1f616f0f5db3c9ce985ce4b",
  "head_matches_backup": true,
  "base": "512dc9af1b13247680d46d815aba0c090dad8b88",
  "commits": [
   {
    "sha": "33af6fdbc1",
    "subject": "descriptor: Reject trailing text after musig()",
    "files": 2,
    "add": 5,
    "del": 0
   },
   {
    "sha": "296898cebd",
    "subject": "descriptor: move PubkeyProvider to descriptor.h",
    "files": 2,
    "add": 91,
    "del": 86
   },
   {
    "sha": "93600f9f03",
    "subject": "descriptor: Add public ParsePubKey overload",
    "files": 3,
    "add": 206,
    "del": 0
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "db1bf449d8f668a6",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}