{
 "number": 36442,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36442",
 "title": "descriptor: Represent multipath descriptors in a single Descriptor object",
 "author": "achow101",
 "author_association": "MEMBER",
 "created_at": "2026-10-05T22:14:49Z",
 "updated_at": "2026-10-23T04:50:03Z",
 "description_hash": "f4ec10929e00a5d2",
 "age_days": 27,
 "draft": false,
 "labels": [
  "Needs rebase",
  "Descriptors",
  "CI failed"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "74915efa6e19b1d7b51ba0842726904e4f46e399",
 "head_ref": "multipath-first-class",
 "head_repo": "achow101/bitcoin",
 "head_history": [
  {
   "t": "2026-10-05T22:32:59Z",
   "sha": "3752814f45e40751628629fd7cf50c7be3635c2f"
  },
  {
   "t": "2026-10-07T17:54:44Z",
   "sha": "b7980165b2b9801851944c6100ec9fd477085686"
  },
  {
   "t": "2026-10-07T21:47:48Z",
   "sha": "74915efa6e19b1d7b51ba0842726904e4f46e399"
  }
 ],
 "additions": 805,
 "deletions": 712,
 "changed_files": 39,
 "commit_count": 8,
 "size_bucket": "XL",
 "mergeable_state": "dirty",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "w0xlt",
      "url": "https://github.com/bitcoin/bitcoin/pull/36442#issuecomment-6066053885"
     },
     {
      "login": "Sjors",
      "url": "https://github.com/bitcoin/bitcoin/pull/36442#issuecomment-6307917577"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "w0xlt": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-10-08T18:05:38Z",
   "stale": false
  },
  "Sjors": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-10-21T04:17:33Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 2,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "Sjors",
   "fametrano",
   "w0xlt"
  ]
 },
 "signals": {
  "needs_rebase": true,
  "ci_failed": true,
  "mergeable_state": "dirty",
  "last_author_activity": "2026-10-07T21:47:48Z",
  "last_reviewer_activity": "2026-10-23T04:50:03Z",
  "last_reviewer": "fametrano",
  "author_silent_days": 25,
  "waiting_on_author_days": 9,
  "days_since_update": 9
 },
 "refs": {
  "mentioned": [],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [
   36443
  ],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/util/bip32.h"
 ],
 "body": "Currently, multipath descriptor strings treat the multipath specifiers as syntactic sugar. Once we finish parsing such a string, the multipath elements are lost as the result is the multiple distinct descriptors that the multipath is a shorthand for. This means that we are unable to return the multipath string for such descriptors.\n\nThis PR allows descriptors to actually be multipath where the multipath specifiers are represented inside of the paths of `BIP32PubkeyProvider`s. This allows for the multipath string to be produced.\n\nIn order to achieve this, a lot of derivation path parsing is refactored. `KeyPath` is promoted to being its own class (which subclasses `std::vector`), while the individual derivation indexes become a `KeyPathElement` object so that the multipath elements can be represented. Furthermore, hardened derivation characters are now being stored within `KeyPathElement`s so each element knows what the original hardened character was. This also cleans up a little bit of the descriptor implementation as we no longer have to carry around `bool apostrophe` in a bunch of places.\n\nLastly, `Parse` now returns a single `Descriptor` object, rather than a vector of them. If the descriptor is a multipath descriptor, the expansion can be retrieved using `GetMultipathExpansion()`. The wallet uses this and still must not store multipath descriptors.",
 "commits": [
  {
   "sha": "2ff1009dbe6655e50f02659e45536a1f8377b26a",
   "date": "2026-10-07T17:36:52Z",
   "message": "util: Make KeyPathElement class with private members"
  },
  {
   "sha": "9f3a86f4b1811c94bb297424d9aa77b40a76500a",
   "date": "2026-10-07T21:32:36Z",
   "message": "util: Change all key path operations to operate with std::vector<KeyPathElement>"
  },
  {
   "sha": "7851cfa16c2df992e842318b620afb1acf1600d1",
   "date": "2026-10-07T21:32:37Z",
   "message": "util: Make KeyPath a subclass of std::vector and not only a typedef\n\nWe will want to have member functions on KeyPath later, so make it a\nclass now."
  },
  {
   "sha": "e29d6efd191d9a5acccd1b7c1b1cfbeb4b2097eb",
   "date": "2026-10-07T21:47:36Z",
   "message": "util: Store hardened character in KeyPathElement"
  },
  {
   "sha": "b84641e4c6bcd916de4fbc88d0822f00f4c26ed5",
   "date": "2026-10-07T21:47:36Z",
   "message": "util: Make KeyPathElement handle multipaths\n\nMoves all of the multipath parsing into ParseHDKeypath, and let\nKeyPathElement be a multipath element."
  },
  {
   "sha": "177874ccd39f6a50fc51cc2bd89fc46076005e86",
   "date": "2026-10-07T21:47:36Z",
   "message": "descriptor: Get the multipath expansion from a multipath descriptor"
  },
  {
   "sha": "d689425c0cdba2b1ec0269e61ae6a9f84defc6d9",
   "date": "2026-10-07T21:47:36Z",
   "message": "descriptor: Produce multipath descriptor in ParseScript\n\nInternally, descriptor parsing will produce multipath descriptor objects\nrather than std::vector<DescriptorImpl>. This single multipath\ndescriptor becomes std::vector<Descriptor> within Parse. Future commits\nwill return the single multipath descriptor object for all descriptor parsing."
  },
  {
   "sha": "74915efa6e19b1d7b51ba0842726904e4f46e399",
   "date": "2026-10-07T21:47:36Z",
   "message": "descriptor: Return multipath descriptor from Parse\n\nInstead of a std::vector<std::unique_ptr<Descriptor>>, Parse can return\na single std::unique_ptr<Descriptor> that contains the multipath\ninformation if the descriptor was multipath. Callers that\nrequire the multipath expansion use GetMultipathExpansion() to get the\nexpanded descriptors."
  }
 ],
 "timeline": [
  {
   "t": "2026-10-05T22:32:59Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "3752814f45e40751628629fd7cf50c7be3635c2f",
   "id": "push:3752814f45e40751628629fd7cf50c7be3635c2f"
  },
  {
   "t": "2026-10-07T17:54:44Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "b7980165b2b9801851944c6100ec9fd477085686",
   "id": "push:b7980165b2b9801851944c6100ec9fd477085686"
  },
  {
   "t": "2026-10-07T21:47:48Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "74915efa6e19b1d7b51ba0842726904e4f46e399",
   "id": "push:74915efa6e19b1d7b51ba0842726904e4f46e399"
  },
  {
   "t": "2026-10-08T18:05:38Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "Concept ACK",
   "id": "c:6066053885",
   "url": "https://github.com/bitcoin/bitcoin/pull/36442#issuecomment-6066053885",
   "hash": "e8334cc43879b73c",
   "edited": null
  },
  {
   "t": "2026-10-21T04:17:33Z",
   "kind": "comment",
   "who": "Sjors",
   "assoc": "MEMBER",
   "text": "Concept ACK.\n\nSorry for the pingback noise. I've now adjusted my [merge skill](https://github.com/Sjors/skills/blob/master/merge/SKILL.md) to stop pinging the main repo from fork PRs.",
   "id": "c:6307917577",
   "url": "https://github.com/bitcoin/bitcoin/pull/36442#issuecomment-6307917577",
   "hash": "e7592d4dee550dd5",
   "edited": "2026-10-21T04:48:31Z"
  },
  {
   "t": "2026-10-23T04:50:03Z",
   "kind": "review",
   "who": "fametrano",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "74915efa6e19b1d7b51ba0842726904e4f46e399",
   "text": "On this head, `migratewallet` turns every step of an imported key's origin path into a hardened step.\n\n`SingleKeyPathElement::Unserialize` assigns a `bool` to `std::optional<char> m_hardened`. The optional is then always engaged, so `ChildNumber()` sets the hardened bit on every step read from disk. It came with \"util: Store hardened character in KeyPathElement\". A v29.4 legacy watch-only wallet holding `wpkh([d34db33f/84h/0h/0h/0/5]<pubkey>)` migrates to `84h/0h/0h/0/5` on v31.1 and to `84h/0h/0h/0h/5h` on this head. With the change suggested inline, the migrated descriptors match v31.1.\n\nThe multipath round trip agrees with btclib-wallet, a BIP389 implementation I maintain. On 25 descriptors (wpkh, sh-wsh multisig, tr trees, miniscript, musig, steps of three and five elements), `getdescriptorinfo` keeps each input's paths and markers, and its `multipath_expansion` and `deriveaddresses` match btclib-wallet. Both refuse the same 8 malformed ones. As the description says, markers are now kept as written; v31.1 printed one per key. A descriptor mixing `h` and `'` thus prints, with its checksum, differently from v31.1, which may deserve a release note. I can share the scripts and outputs.",
   "id": "r:5624021942",
   "url": "https://github.com/bitcoin/bitcoin/pull/36442#pullrequestreview-5624021942",
   "hash": "537a1e93f6c4f3f4",
   "edited": null
  },
  {
   "t": "2026-10-23T04:50:03Z",
   "kind": "review_comment",
   "who": "fametrano",
   "assoc": "CONTRIBUTOR",
   "path": "src/util/bip32.h",
   "commit": "74915efa6e19b1d7b51ba0842726904e4f46e399",
   "in_reply_to": null,
   "text": "Engaged only for a hardened step, so an unhardened step read from disk stays unhardened:\n\n```suggestion\n        m_hardened = (num & BIP32_HARDENED_FLAG) ? std::optional<char>{'h'} : std::nullopt;\n```",
   "id": "rc:4356192920",
   "url": "https://github.com/bitcoin/bitcoin/pull/36442#discussion_r4356192920",
   "hash": "7055172dffc6e7be",
   "edited": null
  }
 ],
 "labels_log": [
  {
   "t": "2026-10-05T22:14:53Z",
   "action": "labeled",
   "label": "Descriptors",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-05T22:35:17Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-07T13:58:27Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-07T19:19:45Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-21T14:01:34Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 4284,
 "text_tokens_estimate": 1071,
 "changed_paths": [
  "src/bench/descriptors.cpp",
  "src/bench/wallet_encrypt.cpp",
  "src/bench/wallet_ismine.cpp",
  "src/key.cpp",
  "src/key.h",
  "src/psbt.h",
  "src/qt/test/wallettests.cpp",
  "src/rpc/mining.cpp",
  "src/rpc/output_script.cpp",
  "src/rpc/util.cpp",
  "src/rpc/util.h",
  "src/script/descriptor.cpp",
  "src/script/descriptor.h",
  "src/script/keyorigin.h",
  "src/script/sign.cpp",
  "src/test/bip32_tests.cpp",
  "src/test/descriptor_tests.cpp",
  "src/test/fuzz/descriptor_parse.cpp",
  "src/test/fuzz/parse_hd_keypath.cpp",
  "src/test/fuzz/util/wallet.h",
  "src/test/psbt_tests.cpp",
  "src/util/bip32.cpp",
  "src/util/bip32.h",
  "src/wallet/export.cpp",
  "src/wallet/imports.cpp",
  "src/wallet/rpc/addresses.cpp",
  "src/wallet/rpc/spend.cpp",
  "src/wallet/rpc/wallet.cpp",
  "src/wallet/scriptpubkeyman.cpp",
  "src/wallet/test/fuzz/scriptpubkeyman.cpp",
  "src/wallet/test/psbt_wallet_tests.cpp",
  "src/wallet/test/util.cpp",
  "src/wallet/test/wallet_tests.cpp",
  "src/wallet/test/walletload_tests.cpp",
  "src/wallet/wallet.cpp",
  "src/wallet/walletdb.cpp",
  "src/wallet/walletutil.cpp",
  "src/wallet/walletutil.h",
  "test/functional/rpc_getdescriptorinfo.py"
 ],
 "files": [
  {
   "path": "src/bench/descriptors.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/bench/wallet_encrypt.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/bench/wallet_ismine.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/key.cpp",
   "add": 4,
   "del": 3
  },
  {
   "path": "src/key.h",
   "add": 2,
   "del": 1
  },
  {
   "path": "src/psbt.h",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/qt/test/wallettests.cpp",
   "add": 3,
   "del": 4
  },
  {
   "path": "src/rpc/mining.cpp",
   "add": 3,
   "del": 4
  },
  {
   "path": "src/rpc/output_script.cpp",
   "add": 14,
   "del": 19
  },
  {
   "path": "src/rpc/util.cpp",
   "add": 8,
   "del": 8
  },
  {
   "path": "src/rpc/util.h",
   "add": 2,
   "del": 1
  },
  {
   "path": "src/script/descriptor.cpp",
   "add": 263,
   "del": 410
  },
  {
   "path": "src/script/descriptor.h",
   "add": 6,
   "del": 1
  },
  {
   "path": "src/script/keyorigin.h",
   "add": 2,
   "del": 1
  },
  {
   "path": "src/script/sign.cpp",
   "add": 3,
   "del": 3
  },
  {
   "path": "src/test/bip32_tests.cpp",
   "add": 77,
   "del": 79
  },
  {
   "path": "src/test/descriptor_tests.cpp",
   "add": 45,
   "del": 37
  },
  {
   "path": "src/test/fuzz/descriptor_parse.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/test/fuzz/parse_hd_keypath.cpp",
   "add": 8,
   "del": 6
  },
  {
   "path": "src/test/fuzz/util/wallet.h",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/test/psbt_tests.cpp",
   "add": 5,
   "del": 5
  },
  {
   "path": "src/util/bip32.cpp",
   "add": 150,
   "del": 28
  },
  {
   "path": "src/util/bip32.h",
   "add": 111,
   "del": 10
  },
  {
   "path": "src/wallet/export.cpp",
   "add": 4,
   "del": 3
  },
  {
   "path": "src/wallet/imports.cpp",
   "add": 13,
   "del": 12
  },
  {
   "path": "src/wallet/rpc/addresses.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/wallet/rpc/spend.cpp",
   "add": 4,
   "del": 4
  },
  {
   "path": "src/wallet/rpc/wallet.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/wallet/scriptpubkeyman.cpp",
   "add": 13,
   "del": 11
  },
  {
   "path": "src/wallet/test/fuzz/scriptpubkeyman.cpp",
   "add": 4,
   "del": 4
  },
  {
   "path": "src/wallet/test/psbt_wallet_tests.cpp",
   "add": 2,
   "del": 3
  },
  {
   "path": "src/wallet/test/util.cpp",
   "add": 4,
   "del": 6
  },
  {
   "path": "src/wallet/test/wallet_tests.cpp",
   "add": 1,
   "del": 2
  },
  {
   "path": "src/wallet/test/walletload_tests.cpp",
   "add": 4,
   "del": 3
  },
  {
   "path": "src/wallet/wallet.cpp",
   "add": 18,
   "del": 14
  },
  {
   "path": "src/wallet/walletdb.cpp",
   "add": 12,
   "del": 10
  },
  {
   "path": "src/wallet/walletutil.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/wallet/walletutil.h",
   "add": 4,
   "del": 4
  },
  {
   "path": "test/functional/rpc_getdescriptorinfo.py",
   "add": 1,
   "del": 1
  }
 ],
 "test_lines": 322,
 "git": {
  "head": "74915efa6e19b1d7b51ba0842726904e4f46e399",
  "head_matches_backup": true,
  "base": "9dfde64cc3262329051fd05fffe40eecc786a99f",
  "patch_id": "ebfa12b40aaebc23",
  "commits": [
   {
    "sha": "2ff1009dbe",
    "subject": "util: Make KeyPathElement class with private members",
    "files": 2,
    "add": 11,
    "del": 5
   },
   {
    "sha": "9f3a86f4b1",
    "subject": "util: Change all key path operations to operate with std::vector<KeyPathElement>",
    "files": 15,
    "add": 186,
    "del": 141
   },
   {
    "sha": "7851cfa16c",
    "subject": "util: Make KeyPath a subclass of std::vector and not only a typedef",
    "files": 5,
    "add": 16,
    "del": 12
   },
   {
    "sha": "e29d6efd19",
    "subject": "util: Store hardened character in KeyPathElement",
    "files": 7,
    "add": 99,
    "del": 71
   },
   {
    "sha": "b84641e4c6",
    "subject": "util: Make KeyPathElement handle multipaths",
    "files": 3,
    "add": 171,
    "del": 75
   },
   {
    "sha": "177874ccd3",
    "subject": "descriptor: Get the multipath expansion from a multipath descriptor",
    "files": 5,
    "add": 81,
    "del": 1
   },
   {
    "sha": "d689425c0c",
    "subject": "descriptor: Produce multipath descriptor in ParseScript",
    "files": 4,
    "add": 181,
    "del": 343
   },
   {
    "sha": "74915efa6e",
    "subject": "descriptor: Return multipath descriptor from Parse",
    "files": 26,
    "add": 147,
    "del": 151
   }
  ],
  "patch_truncated": true
 },
 "patch_id": "ebfa12b40aaebc23",
 "input_hash": "b0de96c7ad892f68",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}