#36442 descriptor: Represent multipath descriptors in a single Descriptor object

ReviewabilityReviewsAgreementSize
#36442 achow101 descriptor: Represent multipath descriptors in a single Descriptor objectWaiting on author0Blocked642 + 163 tests
  • Preserve multipath descriptor strings during parsing instead of immediately splitting them
  • Enable RPCs like getdescriptorinfo to return the original unexpanded multipath descriptor
  • Review the logic now; conflicts with master are mechanical and will need a rebase
  • Unaddressed objection: migratewallet marks all imported key origin steps as hardened (fametrano)
  • Suggested adding a release note for changed display formatting of mixed hardening characters (fametrano)
  • Concept approval without stated reasons (w0xlt, Sjors)
  • 642 lines added or modified outside tests
  • 163 lines added or modified in tests
  • 712 lines removed in total
  • 39 files, 8 commits
Utilities (logging, arguments, libraries)#40 of 59
P3 · cleanup
  • P3 because it improves type safety and derivation path parsing across shared BIP32 utilities
  • The changes primarily serve descriptor-layer functionality rather than standalone utility needs
  • Ranking pass: Moved up: it unblocks #36443 and restores round-tripping of multipath descriptor strings.
Wallet#31 of 92
P2 · new feature
  • P2 because it lets wallet RPCs return and round-trip original multipath descriptor strings
  • It unblocks follow-on multipath descriptor functionality
  • Ranking pass: Moved down slightly to head the multipath chain #36442 -> #36443 -> #36444 as a contiguous block.
Reviewability: ReadyStalePaused
Agreement: StrongPositivePositive w/ caveatsNeutralMildDisputedBlockedCrickets
Reviews: current code-review ACKs, then (+stale ACKs) and -NACKs; greener = more ACKs.
Size: lines added or modified outside tests, then in tests; greener = smaller.

Goal

Refactors output descriptor parsing and BIP32 keypath handling so multipath descriptors are represented as a single `Descriptor` object rather than immediately expanding into a vector of separate descriptors. Replaces raw integer derivation vectors with a dedicated `KeyPath` class composed of `KeyPathElement` objects that preserve multipath branches and the original hardening character. Updates `Parse` to return a single `std::unique_ptr<Descriptor>`, providing `GetMultipathExpansion()` and `IsMultipath()` for subsystems that require individual paths.

Problem: Multipath descriptor strings were treated purely as syntactic sugar and discarded during parsing, splitting into multiple distinct descriptors. As a result, the original multipath string could not be preserved, round-tripped, or returned by RPCs like `getdescriptorinfo`.

Evidence of importance:

  • PR description: '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.'
  • test/functional/rpc_getdescriptorinfo.py: asserts that `getdescriptorinfo(desc)["descriptor"]` now returns the unexpanded multipath descriptor rather than `descsum_create(expanded_descs[0])`.

Scope: A wide-reaching refactor that replaces `std::vector<uint32_t>` derivation paths across wallet, descriptor, signing, and BIP32 utility code with custom `KeyPath` and `KeyPathElement` abstractions, affecting call sites across the codebase.

Risk of a mistake: high (wallet, rpc_api). Mistakes in derivation path parsing or child key derivation could cause wallets to generate wrong addresses, misplace keys, or fail to find expected transaction outputs.

Changed since the previous assessment: Rebased or updated since the previous version with no major architectural changes.

Reviewability

Paused · Waiting on author

Open objections from fametrano with no author reply for 9 days; review would repeat what has been asked.

Waiting on (from the thread read):

  • author: address the migratewallet hardening bug reported by fametrano 2026-10-23

Agreement

Blocked

objection that needs a change, nobody has replied (fametrano)

Thread notes: fametrano reported a regression in SingleKeyPathElement::Unserialize causing migratewallet to mark all derivation steps hardened, offered an inline fix, and suggested a release note for descriptor marker display changes.

Objections, suggestions, and questions:

ReviewerTypeHarm, request, or questionStatusMust beAuthor repliedQuote
fametrano (contributor)objection, correctnessmigratewallet turns every step of an imported key's origin path into a hardened step because SingleKeyPathElement::Unserialize assigns a bool to std::optional<char> m_hardenedopenchangedno2026-10-23: 'On this head, `migratewallet` turns every step of an imported key's origin path into a hardened step.'
fametrano (contributor)suggestion, maintenanceConsider adding a release note since descriptors mixing h and ' now print, with their checksum, differently from v31.1openansweredno2026-10-23: 'A descriptor mixing `h` and `'` thus prints, with its checksum, differently from v31.1, which may deserve a release note.'
fametrano (contributor)suggestion, correctnessOnly engage m_hardened for a hardened step so that unhardened steps read from disk stay unhardenedopenchangedno2026-10-23: 'Engaged only for a hardened step, so an unhardened step read from disk stays unhardened:'

Support:

  • w0xlt (contributor) (Concept ACK, 2026-10-08): (no reason given) [not substantive]
  • Sjors (member) (Concept ACK, 2026-10-21): (no reason given) [not substantive]

Participants:

  • w0xlt (contributor, support, 1 statements 2026-10-08 to 2026-10-08): Concept ACK
  • Sjors (member, support, 1 statements 2026-10-21 to 2026-10-21): Concept ACK
  • fametrano (contributor, objection, 2 statements 2026-10-23 to 2026-10-23): reported migratewallet hardening regression and suggested a release note

Dependencies

Enables:

Files

322 lines under test/bench/ci.

  • src/script/descriptor.cpp +263/-410
  • src/util/bip32.cpp +150/-28
  • src/test/bip32_tests.cpp +77/-79
  • src/util/bip32.h +111/-10
  • src/test/descriptor_tests.cpp +45/-37
  • src/rpc/output_script.cpp +14/-19
  • src/wallet/wallet.cpp +18/-14
  • src/wallet/imports.cpp +13/-12
  • src/wallet/scriptpubkeyman.cpp +13/-11
  • src/wallet/walletdb.cpp +12/-10
  • src/rpc/util.cpp +8/-8
  • src/test/fuzz/parse_hd_keypath.cpp +8/-6
  • src/test/psbt_tests.cpp +5/-5
  • src/wallet/test/util.cpp +4/-6
  • src/wallet/rpc/spend.cpp +4/-4
  • src/wallet/test/fuzz/scriptpubkeyman.cpp +4/-4
  • src/wallet/walletutil.h +4/-4
  • src/key.cpp +4/-3
  • src/qt/test/wallettests.cpp +3/-4
  • src/rpc/mining.cpp +3/-4
  • src/script/descriptor.h +6/-1
  • src/wallet/export.cpp +4/-3
  • src/wallet/test/walletload_tests.cpp +4/-3
  • src/script/sign.cpp +3/-3
  • src/wallet/test/psbt_wallet_tests.cpp +2/-3
  • src/bench/descriptors.cpp +2/-2
  • src/bench/wallet_encrypt.cpp +2/-2
  • src/bench/wallet_ismine.cpp +2/-2
  • src/test/fuzz/descriptor_parse.cpp +2/-2
  • src/wallet/rpc/wallet.cpp +2/-2
  • src/wallet/walletutil.cpp +2/-2
  • src/key.h +2/-1
  • src/rpc/util.h +2/-1
  • src/script/keyorigin.h +2/-1
  • src/wallet/test/wallet_tests.cpp +1/-2
  • src/psbt.h +1/-1
  • src/test/fuzz/util/wallet.h +1/-1
  • src/wallet/rpc/addresses.cpp +1/-1
  • test/functional/rpc_getdescriptorinfo.py +1/-1

Card

The short text the ranking pass compares across PRs.

Represents multipath descriptors natively in a single Descriptor object rather than immediately expanding them during parsing. Previously, multipath syntax was discarded upon parsing as syntactic sugar, which prevented RPCs like getdescriptorinfo from returning or round-tripping the original multipath descriptor string. Derivation path parsing is refactored into KeyPath and KeyPathElement classes to retain multipath steps and hardening formats, while callers requiring individual paths use GetMultipathExpansion. This change enables downstream work in #36443.

Data

dossier JSON · extract JSON · model ledger, generated 2026-10-24T01:18, confidence high, input hash 60931fd990203586
thread read through 2026-10-23 (4 statements), head 74915efa6e
labels: Needs rebase, Descriptors, CI failed

Categories

Utilities (logging, arguments, libraries)

P3 · cleanup (ranking pass: Moved up: it unblocks #36443 and restores round-tripping of multipath descriptor strings.)

Replaces raw vector<uint32_t> derivation paths with structured KeyPath abstractions in shared BIP32 utilities. This improves type safety and derivation path parsing across consumers, though the primary motivation is descriptor-layer functionality.

Membership: Substantially refactors src/util/bip32.cpp and src/util/bip32.h (+261/-38) to introduce structured KeyPath and KeyPathElement primitives.

Factors: security/stability 0, bug 0, performance 0, user value 2, leverage 2

Wallet

P2 · new feature (ranking pass: Moved down slightly to head the multipath chain #36442 -> #36443 -> #36444 as a contiguous block.)

Preserves multipath descriptor strings instead of prematurely expanding and discarding them during parsing. As stated in the description, 'Once we finish parsing such a string, the multipath elements are lost... This means that we are unable to return the multipath string for such descriptors'; representing them in a single object resolves this limitation and unblocks follow-on work like #36443.

Membership: Has the Descriptors label, heavily alters src/script/descriptor.cpp, and updates wallet storage and scriptpubkeyman handling for multipath descriptors.

Factors: security/stability 0, bug 0, performance 0, user value 2, leverage 2