{
 "number": 36154,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36154",
 "title": "wallet: fill PSBT_GLOBAL_XPUB for descriptors with more than one key",
 "author": "jeanpablojp",
 "author_association": "CONTRIBUTOR",
 "created_at": "2026-09-02T22:53:06Z",
 "updated_at": "2026-09-09T11:30:30Z",
 "age_days": 14,
 "draft": false,
 "labels": [
  "Wallet",
  "Needs rebase"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "92bf732c4ee0e7096aa3f203ebcafcdf9366ff1a",
 "head_ref": "psbt-global-xpubs",
 "head_repo": "jeanpablojp/bitcoin",
 "head_history": [
  {
   "t": "2026-09-02T23:13:10Z",
   "sha": "a586e55058e34f928c46d28d84bd4ba5f7f0b4b6"
  },
  {
   "t": "2026-09-03T01:25:27Z",
   "sha": "92bf732c4ee0e7096aa3f203ebcafcdf9366ff1a"
  }
 ],
 "additions": 336,
 "deletions": 36,
 "changed_files": 13,
 "commit_count": 2,
 "size_bucket": "M",
 "mergeable_state": "dirty",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {},
   "conflicts": [
    {
     "number": 36167,
     "title": "[RFC] Enable `-Wunused`",
     "author": "fanquake"
    },
    {
     "number": 36143,
     "title": "descriptor: add CreateMultisigDescriptor()",
     "author": "rxbryan"
    },
    {
     "number": 36133,
     "title": "wallet: store multipath descriptor",
     "author": "Sjors"
    },
    {
     "number": 36122,
     "title": "BIP460: CISA for Taproot key path spends",
     "author": "fjahr"
    },
    {
     "number": 36013,
     "title": "test: Descriptor roundtrip and raw()/ addr() coverage",
     "author": "pablomartin4btc"
    },
    {
     "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": 35370,
     "title": "rpc: add key-origin modes to PSBT processing RPCs",
     "author": "junbyjun1238"
    },
    {
     "number": 35041,
     "title": "descriptor: speed-up `Parse` (xpub/xpriv) in ~30%",
     "author": "brunoerg"
    },
    {
     "number": 34520,
     "title": "refactor: Add [[nodiscard]] to functions returning bool+mutable ref",
     "author": "maflcko"
    }
   ]
  }
 },
 "acks_parsed": {},
 "acks_tally": {
  "ack": 0,
  "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": [
   "Sjors",
   "achow101"
  ]
 },
 "signals": {
  "needs_rebase": true,
  "ci_failed": false,
  "mergeable_state": "dirty",
  "last_author_activity": "2026-09-03T21:03:48Z",
  "last_reviewer_activity": "2026-09-07T22:48:07Z",
  "last_reviewer": "achow101",
  "author_silent_days": 13,
  "waiting_on_author_days": 9,
  "days_since_update": 8
 },
 "refs": {
  "mentioned": [
   27583
  ],
  "depends_on": [],
  "fixes": [
   27583
  ],
  "linked_issues": [
   {
    "number": 27583,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "psbt: set global_xpubs (at least for multisig descriptors)"
   }
  ],
  "references": [
   {
    "number": 27583,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "psbt: set global_xpubs (at least for multisig descriptors)"
   }
  ],
  "conflicts": [
   36167,
   36143,
   36133,
   36122,
   36013,
   35445,
   35370,
   35041,
   34520
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/script/descriptor.cpp"
 ],
 "body": "Closes #27583.\n\n`PSBT_GLOBAL_XPUB` already serializes, deserializes and merges when PSBTs are combined. Nothing writes it, so a PSBT produced by the wallet never carries it, and a signer that only has the file cannot rebuild the multisig from it.\n\nA signer with no registered policy to work from needs it: HWI's BitBox02 driver builds the multisig out of the PSBT's extended keys and refuses to sign without them. Today Specter [fills the field itself](https://github.com/cryptoadvance/specter-desktop/blob/8843e2f0640a9adc129f9ab6afe5ecb9632e7c5b/src/cryptoadvance/specter/wallet/wallet.py#L1866-L1881) after getting the PSBT from Core, and only when the wallet has more than one key.\n\nBefore and after with the BitBox02 and Trezor simulators\n\nA 2-of-3 `wsh(sortedmulti(...))` on regtest with the simulator as one co-signer, signed through HWI 3.2.0. The \"before\" file is the same PSBT with its `PSBT_GLOBAL_XPUB` records removed, so nothing else differs.\n\n| | without the field | with the field |\n|---|---|---|\n| BitBox02 | refuses, `This BitBox02 is not one of the cosigners` | signs |\n| Trezor T | signs, co-signer nodes sent with zeroed chain codes and no derivation paths | signs, nodes sent with the real chain codes and paths |\n\nWhat `parse_multisig` hands the Trezor for the change output:\n\n```\nwithout: global xpubs=0  zeroed chain codes=3/3  depths=[0, 0, 0]  paths=[[], [], []]\nwith   : global xpubs=3  zeroed chain codes=0/3  depths=[4, 4, 4]  paths=[[1, 0], [1, 0], [1, 0]]\n```\n\n`Descriptor` gains an accessor for its extended keys, each with the origin taken at the deepest hardened step, and `DescriptorScriptPubKeyMan::FillPSBT` writes them when the descriptor contributed to the PSBT. That covers `walletcreatefundedpsbt`, `walletprocesspsbt`, `send`, `sendall`, `psbtbumpfee` and the GUI send dialog, which all go through `CWallet::FillPSBT`.\n\nThe field is only written for descriptors with two or more extended keys. With one, the signer derives its own key from its seed, so the entry would add nothing and only cost space. The field is not small. On a 2-of-3 it adds 291 bytes, taking the PSBT from 933 to 1224. `bip32derivs` is respected. That same PSBT already carries the full `witness_script` and `bip32_derivs`, so what is added is the chain code of each account.\n\nThere are unit tests for the accessor, and on the wallet side a functional test with a wallet whose internal descriptor uses different keys from its external one, the only arrangement that can show it publishing on its own.\n\n`descriptorprocesspsbt` is left out, since it signs from the descriptors it is given without going through the wallet, and I intend to cover it as a follow-up. `musig()` contributes nothing, since derivation is applied to the aggregate key.\n\nThe `decodepsbt` output changes, and a release note is included.",
 "commits": [
  {
   "sha": "42d5635587dbe79790bca2b5228609fd9b352ab2",
   "date": "2026-09-02T22:18:23Z",
   "message": "refactor: extract the last hardened xpub lookup in BIP32PubkeyProvider\n\nToNormalizedString walks the key path backwards to the last hardened\nstep, and then fetches the extended public key sitting at that step,\nfrom the descriptor cache when it is cached and by deriving it\notherwise. Both halves are useful on their own, so pull them out as\nLastHardenedIndex() and GetLastHardenedExtPubKey().\n\nNo behaviour change."
  },
  {
   "sha": "92bf732c4ee0e7096aa3f203ebcafcdf9366ff1a",
   "date": "2026-09-03T01:17:10Z",
   "message": "wallet: fill PSBT_GLOBAL_XPUB for descriptors with more than one key\n\nBIP 174 lets a PSBT carry the extended public keys the inputs and\noutputs derive from, so that a signer holding no wallet state can\nrebuild the script from the PSBT alone. Hardware signers read the\nfield, but the wallet never writes it, so a PSBT it produces never\ncarries it.\n\nGive Descriptor an accessor for the extended public keys it is built\nfrom, each with the origin of the key itself, taken at the deepest\nhardened step so that the unhardened children used in the transaction\ncan be derived from it, and have DescriptorScriptPubKeyMan::FillPSBT\nwrite them when the descriptor contributed to the PSBT. That covers\nevery RPC that builds a PSBT from the wallet, walletcreatefundedpsbt,\nwalletprocesspsbt, send, sendall and psbtbumpfee, as well as the GUI\nsend dialog, since all of them go through CWallet::FillPSBT.\n\nOnly descriptors with more than one key publish: for a single key the\nfield says nothing the per-input derivation paths do not already say,\nand every PSBT the wallet produces would grow for nothing. bip32derivs\nis respected, so the field can be turned off with the paths."
  }
 ],
 "timeline": [
  {
   "t": "2026-09-02T23:13:10Z",
   "kind": "force_push",
   "who": "jeanpablojp",
   "commit": "a586e55058e34f928c46d28d84bd4ba5f7f0b4b6"
  },
  {
   "t": "2026-09-03T01:25:27Z",
   "kind": "force_push",
   "who": "jeanpablojp",
   "commit": "92bf732c4ee0e7096aa3f203ebcafcdf9366ff1a"
  },
  {
   "t": "2026-09-03T06:34:59Z",
   "kind": "comment",
   "who": "Sjors",
   "assoc": "MEMBER",
   "text": "@jeanpablojp in the RP description, can you point to the source code in Specter where it adds the global xpub? This might be useful for those who want to verify that it's still needed. Similarly, it's useful to provide a before and after example flow with HWI (and e.g. a Trezor / simulator).\n\n(It's also better to not to tag people in the PR description, because IIUC that text ends up in the merge commit, which then keeps triggering notifications as other projects backport it.)"
  },
  {
   "t": "2026-09-03T21:03:48Z",
   "kind": "comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "text": "Both are in the description now.\n\nThe Specter link points at `Wallet.fill_psbt`, pinned to a commit so the line numbers don't drift.\n\nOn the flow: I ran the Trezor T simulator first, and it signs the same PSBT with or without the field, same signature, same prompts, same screens. What differs is only the data HWI hands it, since without the field `parse_multisig` fills the co-signer nodes with zeroed chain codes and no derivation paths. The device that does show a difference is the BitBox02: it refuses with `This BitBox02 is not one of the cosigners` and signs once the field is there. Both runs are in the collapsed block.\n\nI had written that the Trezor needed the field. That was wrong, and the description is corrected.\n\nAlso dropped the tag, thanks for the note about the merge commit."
  },
  {
   "t": "2026-09-04T07:08:04Z",
   "kind": "comment",
   "who": "Sjors",
   "assoc": "MEMBER",
   "text": "Thanks. It's useful to know that Trezor doesn't seem to need this, but BitBox02 and perhaps other devices do.\n\nI suspect that once we support the new `registerdescriptor` command in HWI, and pass that registration back to HWI when signing, none of the devices will need the global xpub. That's because HWI can construct everything it needs from the descriptor, which itself contains the xpubs.\n\nBut providing global xpubs may still be useful for flows without HWI."
  },
  {
   "t": "2026-09-07T22:38:22Z",
   "kind": "comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "text": "Please shorten your description. We don't need to know the exact details about every single device that needs to know about the global xpub.\n\nWhy is this restricted to only multisigs? Just because that is the described use case in the original issue does not mean that it is the only use case. PSBTs and Descriptors are generic objects, we should avoid doing things that are specific to one type of descriptor or psbt."
  },
  {
   "t": "2026-09-07T22:40:40Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/script/descriptor.cpp",
   "commit": "92bf732c4ee0e7096aa3f203ebcafcdf9366ff1a",
   "in_reply_to": null,
   "text": "In 92bf732c4ee0e7096aa3f203ebcafcdf9366ff1a \"wallet: fill PSBT_GLOBAL_XPUB for descriptors with more than one key\"\n\nThis should return the map rather than taking it as an output parameter.\n\nDo not provide `SigningProvider`, either the last hardened key is already cached, or it does not exist. There is no need to take a `SigningProvider` here, otherwise the wallet wouldn't function."
  },
  {
   "t": "2026-09-07T22:43:32Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/script/descriptor.cpp",
   "commit": "92bf732c4ee0e7096aa3f203ebcafcdf9366ff1a",
   "in_reply_to": null,
   "text": "In 92bf732c4ee0e7096aa3f203ebcafcdf9366ff1a \"wallet: fill PSBT_GLOBAL_XPUB for descriptors with more than one key\"\n\nWe should not make assumptions about what the caller is going to use the xpubs for. This should return the root and it's fingerprint for any key where we cannot do the normalized key thing."
  },
  {
   "t": "2026-09-07T22:46:43Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/script/descriptor.cpp",
   "commit": "92bf732c4ee0e7096aa3f203ebcafcdf9366ff1a",
   "in_reply_to": null,
   "text": "In 92bf732c4ee0e7096aa3f203ebcafcdf9366ff1a \"wallet: fill PSBT_GLOBAL_XPUB for descriptors with more than one key\"\n\nIt is not necessary to determine what the last hardened index is, just ask the cache for it, and if the cache doesn't have that key, return the root."
  },
  {
   "t": "2026-09-07T22:48:07Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/script/descriptor.cpp",
   "commit": "92bf732c4ee0e7096aa3f203ebcafcdf9366ff1a",
   "in_reply_to": null,
   "text": "In 92bf732c4ee0e7096aa3f203ebcafcdf9366ff1a \"wallet: fill PSBT_GLOBAL_XPUB for descriptors with more than one key\"\n\nThis is incorrect. It should traverse the participants and get their xpubs."
  }
 ],
 "labels_log": [
  {
   "t": "2026-09-02T22:53:11Z",
   "action": "labeled",
   "label": "Wallet",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-02T23:14:45Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-03T02:32:54Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-09T11:30:29Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-09-03T00:51:25Z",
   "kind": "convert_to_draft",
   "who": "jeanpablojp"
  },
  {
   "t": "2026-09-03T02:22:17Z",
   "kind": "ready_for_review",
   "who": "jeanpablojp"
  }
 ],
 "text_chars": 7783,
 "text_tokens_estimate": 1945,
 "changed_paths": [
  "doc/release-notes-36154.md",
  "src/key_io.cpp",
  "src/key_io.h",
  "src/script/descriptor.cpp",
  "src/script/descriptor.h",
  "src/test/descriptor_tests.cpp",
  "src/test/fuzz/descriptor_parse.cpp",
  "src/wallet/rpc/spend.cpp",
  "src/wallet/scriptpubkeyman.cpp",
  "src/wallet/scriptpubkeyman.h",
  "src/wallet/test/psbt_wallet_tests.cpp",
  "src/wallet/test/walletload_tests.cpp",
  "test/functional/wallet_multisig_descriptor_psbt.py"
 ],
 "files": [
  {
   "path": "doc/release-notes-36154.md",
   "add": 9,
   "del": 0
  },
  {
   "path": "src/key_io.cpp",
   "add": 7,
   "del": 0
  },
  {
   "path": "src/key_io.h",
   "add": 6,
   "del": 0
  },
  {
   "path": "src/script/descriptor.cpp",
   "add": 96,
   "del": 33
  },
  {
   "path": "src/script/descriptor.h",
   "add": 14,
   "del": 0
  },
  {
   "path": "src/test/descriptor_tests.cpp",
   "add": 98,
   "del": 0
  },
  {
   "path": "src/test/fuzz/descriptor_parse.cpp",
   "add": 6,
   "del": 0
  },
  {
   "path": "src/wallet/rpc/spend.cpp",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/wallet/scriptpubkeyman.cpp",
   "add": 37,
   "del": 0
  },
  {
   "path": "src/wallet/scriptpubkeyman.h",
   "add": 4,
   "del": 0
  },
  {
   "path": "src/wallet/test/psbt_wallet_tests.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/wallet/test/walletload_tests.cpp",
   "add": 1,
   "del": 0
  },
  {
   "path": "test/functional/wallet_multisig_descriptor_psbt.py",
   "add": 55,
   "del": 0
  }
 ],
 "test_lines": 162,
 "git": {
  "head": "92bf732c4ee0e7096aa3f203ebcafcdf9366ff1a",
  "head_matches_backup": true,
  "base": "b811aeabad94ef48cd0f0fb1d2fcc456594aeedb",
  "commits": [
   {
    "sha": "42d5635587",
    "subject": "refactor: extract the last hardened xpub lookup in BIP32PubkeyProvider",
    "files": 1,
    "add": 40,
    "del": 33
   },
   {
    "sha": "92bf732c4e",
    "subject": "wallet: fill PSBT_GLOBAL_XPUB for descriptors with more than one key",
    "files": 13,
    "add": 296,
    "del": 3
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "c454aff394cbb90c",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}