{
 "number": 35377,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35377",
 "title": "wallet: Allow importing of descriptors without private keys when the wallet has the private keys",
 "author": "achow101",
 "author_association": "MEMBER",
 "created_at": "2026-05-25T23:55:32Z",
 "updated_at": "2026-09-17T00:51:57Z",
 "age_days": 114,
 "draft": true,
 "labels": [
  "Wallet"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "eed498d498971af30fef1aa52d816ed06b6bd051",
 "head_ref": "importdescriptors-without-priv",
 "head_repo": "achow101/bitcoin",
 "head_history": [
  {
   "t": "2026-05-26T18:15:18Z",
   "sha": "2e6d8d0b8b619dfacd1718501b3795cf8742d6da"
  },
  {
   "t": "2026-06-13T02:22:11Z",
   "sha": "726006c4473b9ca9edd202f80707f3f6b7c67440"
  },
  {
   "t": "2026-06-19T21:51:48Z",
   "sha": "53551be44abb7d73b9bbd603f430def2f110ffec"
  },
  {
   "t": "2026-06-27T01:32:02Z",
   "sha": "a0a14954e867ca8eba9e57e4759f0c9c05a41ea3"
  },
  {
   "t": "2026-07-08T21:13:40Z",
   "sha": "8791d4f71b9ee142dbf3b55b2be761ea19b8913e"
  },
  {
   "t": "2026-07-31T06:25:07Z",
   "sha": "5f328673c18f2f3615a18a99e1a62f4230d7d85c"
  },
  {
   "t": "2026-08-05T23:04:06Z",
   "sha": "2e5f5fcd1d6ac437e9d94f4eb29d85d54a382042"
  },
  {
   "t": "2026-08-05T23:23:58Z",
   "sha": "69ebe6b55b7e153aaff36dfe83c2f8aabeb72933"
  },
  {
   "t": "2026-08-08T02:36:00Z",
   "sha": "63534675460f697f2ec1f2582bc85cc8a40517f1"
  },
  {
   "t": "2026-08-26T21:02:26Z",
   "sha": "8d439772a792d4863c7932793884f2c2fd05dc7d"
  },
  {
   "t": "2026-09-14T20:50:50Z",
   "sha": "eed498d498971af30fef1aa52d816ed06b6bd051"
  }
 ],
 "additions": 830,
 "deletions": 283,
 "changed_files": 18,
 "commit_count": 16,
 "size_bucket": "XL",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "rkrux",
      "url": "https://github.com/bitcoin/bitcoin/pull/35377#issuecomment-4630564472"
     },
     {
      "login": "polespinasa",
      "url": "https://github.com/bitcoin/bitcoin/pull/35377#issuecomment-4647841954"
     },
     {
      "login": "Sjors",
      "url": "https://github.com/bitcoin/bitcoin/pull/35377#issuecomment-5482654715"
     }
    ]
   },
   "conflicts": [
    {
     "number": 36257,
     "title": "qa: assert_equals -> assert_true/assert_false",
     "author": "hodlinator"
    },
    {
     "number": 36236,
     "title": "wallet, rpc: add verify_balance option to importdescriptors",
     "author": "musaHaruna"
    },
    {
     "number": 36143,
     "title": "descriptor: add CreateMultisigDescriptor()",
     "author": "rxbryan"
    },
    {
     "number": 36133,
     "title": "wallet: store multipath descriptor",
     "author": "Sjors"
    },
    {
     "number": 35998,
     "title": "wallet: Handle or explicitly ignore `WalletBatch` write failures",
     "author": "achow101"
    },
    {
     "number": 35989,
     "title": "wallet: fix crash on importdescriptors with a range ending at 2^31-1",
     "author": "shuv-amp"
    },
    {
     "number": 35752,
     "title": "wallet: make encryption state updates atomic",
     "author": "l0rinc"
    },
    {
     "number": 35619,
     "title": "test:  ExtendedPrivateKey follow-ups",
     "author": "rkrux"
    },
    {
     "number": 34520,
     "title": "refactor: Add [[nodiscard]] to functions returning bool+mutable ref",
     "author": "maflcko"
    },
    {
     "number": 31668,
     "title": "Added rescan option for import descriptors",
     "author": "saikiran57"
    }
   ]
  }
 },
 "acks_parsed": {
  "Sjors": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-08-31T18:24:53Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 1,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "Sjors",
   "polespinasa",
   "rkrux"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-14T20:50:50Z",
  "last_reviewer_activity": "2026-09-02T18:00:41Z",
  "last_reviewer": "Sjors",
  "author_silent_days": 2,
  "waiting_on_author_days": 0,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [
   27336,
   34861,
   36133
  ],
  "depends_on": [
   34861
  ],
  "fixes": [
   27336
  ],
  "linked_issues": [
   {
    "number": 27336,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "Should be able to import an xpub descriptor to a privkey-enabled wallet if the wallet has the privkeys"
   }
  ],
  "references": [
   {
    "number": 34861,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-09-17",
    "title": "wallet: Add importdescriptors interface"
   },
   {
    "number": 27336,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "Should be able to import an xpub descriptor to a privkey-enabled wallet if the wallet has the privkeys"
   },
   {
    "number": 36133,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "wallet: store multipath descriptor"
   }
  ],
  "conflicts": [
   36257,
   36236,
   36143,
   36133,
   35998,
   35989,
   35752,
   35619,
   34520,
   31668
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [],
 "body": "Currently importing a descriptor to a wallet that has private keys requires the descriptor to include the private keys. This is not ideal as it means exposing private key material. This PR makes it so that the wallet will lookup and substitute the private keys for their respective public keys in such descriptors, thus enabling importing of public descriptors into wallets with private keys.\n\nThe underlying mechanism is that the wallet retrieves all of the pubkeys from the descriptor and checks to see if any of them have private keys in any `ScriptPubKeyMan`. Additionally, if the descriptor has a xpub with key origin info, we will check if any xprvs known to the wallet have a matching fingerprint and derive to the specified xpub. If so, the origin + xpub are replaced with the single xprv with the origin derivation path prepended to the key expression's derivation path.\n\nPossible future work is to allow the xpub substitution to work when the wallet has some child in the key origin, i.e. the wallet xprv has a key origin that is a prefix of the key origin specified for an xpub in the descriptor. Currently this kind of substitution is not being done, only root master xprvs will be substituted.\n\nIf a wallet does not have the private keys for a public descriptor, the import is still disallowed.\n\nThis PR is based on #34861 to avoid an annoying rebase.\n\nCloses #27336",
 "commits": [
  {
   "sha": "380d3ae0deab72c2652b3290bfe00195cca4f8e9",
   "date": "2026-09-14T18:07:30Z",
   "message": "wallet: rpc: Use std::optional in GetImportTimestamp"
  },
  {
   "sha": "90485103c67d2454efa10743b1a2e6d844f74c58",
   "date": "2026-09-14T18:07:31Z",
   "message": "wallet: lower the minimum timestamp to 0\n\nTimestamp 0 is allowed and it does not seem a reason to have a minimum timestamp > 0\nother than some carried value from legacy wallet.\n\nLowering simplifies importdescriptors because, before this change, if a timestamp 0 was set\nby the user it would be overwritten by the minimum timestamp, but error messages should still\nreport timestamp 0.\n\nNegative timestamps are now invalid and throw."
  },
  {
   "sha": "7375124f4c9a7d998bf6e0f1eed3975f0ea761e3",
   "date": "2026-09-14T18:07:31Z",
   "message": "wallet: refactor: make is_ranged no longer an optional"
  },
  {
   "sha": "f1f61af357275f277c169e895d39699f918ef7aa",
   "date": "2026-09-14T18:07:31Z",
   "message": "wallet: Add ImportDescriptorRequest structs\n\nImportDescriptorRquest will replace the UniValue-based interface of ProcessDescriptorImport(),\nwhich will allow to extract the functionality to other interfaces such as the GUI, etc,\nin a subsequent commit."
  },
  {
   "sha": "975215618505e2d62c17cb63ce94ca2a72e79ffc",
   "date": "2026-09-14T18:07:31Z",
   "message": "wallet: rpc: refactor: Extract UniValue processing from ProcessDescriptorImport\n\nAdds a new function ProcessUniValueDescriptor that translates from UniValue to ImportDescriptorRequest.\nRefactors ProcessDescriptorImport to work with ImportDescriptorRequest objects instead of UniValue.\nRefactors RPCMethod importdescriptors() to use ProcessUniValueDescriptor.\n\nThe commit introduces a small behavior change to make label handling more consistent.\nPreviously, an unset label on a descriptor that cannot have a label would eventually be converted\nto an empty string. However, if the user explicitly set the label to an empty string,\nthe importdescriptors RPC call would fail because the descriptor does not support labels.\nThis inconsistency does not make sense, since an empty string (or null) effectively means that\nno label was set. With this change, null labels and empty strings are treated as valid and are\nconsidered equivalent to having no label at all."
  },
  {
   "sha": "04c73a91d2cc4165b90dc43b8932b128b8e1923f",
   "date": "2026-09-14T18:07:32Z",
   "message": "wallet: Add ImportError struct and new WalletError codes\n\nImportError struct will substitute the UniValue based response of ProcessDescriptorImport\nin a subsequent commit. This will allow other interfaces such as the GUI to consume those function.\n\nAdded an extra constructors that will be useful in following commits.\n\nAlso add new error codes to the WalletError codes enum, this is necessary in order to propagate\nerror codes to the new interfaces and to translate errors to the rpc wallet error codes."
  },
  {
   "sha": "48d3d717fb6e3f61acbaf867404e51a1cac77e78",
   "date": "2026-09-14T18:07:32Z",
   "message": "wallet, util: Add HandleWalletErrorCode\n\nHelper function to translate from WalletErrorCode to RPC error codes."
  },
  {
   "sha": "a40ee4ec26590c4e485f5bb0155d90b4438ee590",
   "date": "2026-09-14T18:07:32Z",
   "message": "wallet: rpc: refactor: ProcessDescriptorImport returns ImportDescriptorResult\n\nMake ProcessDescriptorImport return a ImportDescriptorResult object instead of a UniValue.\n\nThis commit gets rid of a try-catch that generates a big diff of identation changes,\nit is recommended to review it with --ignore-space-change"
  },
  {
   "sha": "c9650d8f0e2f664b4f4362a42e8dfc7efc82af18",
   "date": "2026-09-14T18:07:32Z",
   "message": "wallet: rename ProcessDescriptorImport to ImportDescriptor and add ProcessDescriptorsImport\n\nRename ProcessDescriptorImport() to ImportDescriptor().\nImportDescriptor() takes a single descriptor request and imports it into the wallet.\n\nAdd ProcessDescriptorsImport() (descriptors in plural) to handle wallet locking and rescanning\nover a vector of ImportDescriptorRequest items. It is also in charge of collecting all responses\nof imports from ImportDescriptor.\n\nThis allows a next commit to extract ImportDescriptor() and\nProcessDescriptorsImport() out of the RPC code so they can be used by\nother future interfaces."
  },
  {
   "sha": "a9cd985d0a54877ef3ba9b844f725a2d3065f1bb",
   "date": "2026-09-14T18:07:33Z",
   "message": "wallet: Move ImportDescriptor and ProcessDescriptorsImport to imports.cpp\n\nExtract ImportDescriptor() and ProcessDescriptorsImport() out of backup.cpp and move them into imports.cpp so other future interfaces\ncan use them without needing to know about RPC code.\n\nThe commit can be reviewed with the --color-moved=dimmed-zebra option for an easier review."
  },
  {
   "sha": "3be5f401370ee18dc263149c8a23a33cba9baef5",
   "date": "2026-09-14T18:21:14Z",
   "message": "wallet: add CheckDescriptorRangeBounds\n\nCo-authored-by: w0xlt <woltx@protonmail.com>\n\nProcessUniValueDescriptor() checks for the descriptor range correctness, but since a\nfuture commit adds an interface to import descriptors, those checks must be extracted\nfrom RPC code. This commit adds a helper function to check ranges in descriptors and\nchecks for the range inside ImportDescriptor().\nProcessUniValueDescriptor keeps also the check as other RPC calls other than importdescriptors\nalso needs to check for ranges. This introduces a double check on the RPC path,\nbut the cost of it is minimal and can be accepted."
  },
  {
   "sha": "2c249dbdf5c9d8c7dd71ac90ea29a8b52ec4b2ba",
   "date": "2026-09-14T18:21:15Z",
   "message": "wallet: Add an importDescriptors() interface for the wallet"
  },
  {
   "sha": "159a873c7a6c752a32d98154512dd02a4e945ada",
   "date": "2026-09-14T20:50:37Z",
   "message": "descriptor: Implement SubstituteMasterExtPubs\n\nSubstituteMasterExtPubs replaces Origin + BIP32 inside of a descriptor\nwhen a master xprv is provided that matches the origin and derives the\nBIP32 xpub."
  },
  {
   "sha": "540bc1adf20c8fd202153fcde25e9d108dee84c3",
   "date": "2026-09-14T20:50:37Z",
   "message": "wallet: Substitute known keys when importing a descriptor\n\nWhen an imported descriptor contains pubkeys for which the wallet knows\nthe private keys, substitute those pubkeys for the privkeys so that the\ndescriptor can be imported. This allows such descriptors without private keys\nto be imported into wallets with private keys enabled."
  },
  {
   "sha": "7475cf580768c8acea119bdfbb5ea49c11ec80ac",
   "date": "2026-09-14T20:50:38Z",
   "message": "descriptor, musig: Return participants in GetPubKeys"
  },
  {
   "sha": "eed498d498971af30fef1aa52d816ed06b6bd051",
   "date": "2026-09-14T20:50:38Z",
   "message": "test: Test importdescriptors with descriptors without privkeys\n\nTest that importdescriptors can import descriptors that don't have\nprivate keys, but the wallet already has the private key for them."
  }
 ],
 "timeline": [
  {
   "t": "2026-05-26T18:15:18Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "2e6d8d0b8b619dfacd1718501b3795cf8742d6da"
  },
  {
   "t": "2026-06-05T10:28:24Z",
   "kind": "comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "text": "Definite Concept ACK 2e6d8d0 because it allows the users to not deal with private keys manually."
  },
  {
   "t": "2026-06-08T10:15:16Z",
   "kind": "comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "text": "seems like a good idea, concept ACK\n\nWill review after #34861 is merged"
  },
  {
   "t": "2026-06-13T02:22:11Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "726006c4473b9ca9edd202f80707f3f6b7c67440"
  },
  {
   "t": "2026-06-19T21:51:48Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "53551be44abb7d73b9bbd603f430def2f110ffec"
  },
  {
   "t": "2026-06-27T01:32:02Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "a0a14954e867ca8eba9e57e4759f0c9c05a41ea3"
  },
  {
   "t": "2026-07-08T21:13:40Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "8791d4f71b9ee142dbf3b55b2be761ea19b8913e"
  },
  {
   "t": "2026-07-31T06:25:07Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "5f328673c18f2f3615a18a99e1a62f4230d7d85c"
  },
  {
   "t": "2026-08-05T23:04:06Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "2e5f5fcd1d6ac437e9d94f4eb29d85d54a382042"
  },
  {
   "t": "2026-08-05T23:23:58Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "69ebe6b55b7e153aaff36dfe83c2f8aabeb72933"
  },
  {
   "t": "2026-08-08T02:36:00Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "63534675460f697f2ec1f2582bc85cc8a40517f1"
  },
  {
   "t": "2026-08-26T21:02:26Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "8d439772a792d4863c7932793884f2c2fd05dc7d"
  },
  {
   "t": "2026-08-31T18:24:53Z",
   "kind": "comment",
   "who": "Sjors",
   "assoc": "MEMBER",
   "text": "Concept ACK, I've been using some version of this in https://github.com/Sjors/bitcoin/pull/91. I'll try to switch to exactly this PR after https://github.com/bitcoin/bitcoin/pull/34861 lands and you've rebased."
  },
  {
   "t": "2026-09-02T18:00:41Z",
   "kind": "comment",
   "who": "Sjors",
   "assoc": "MEMBER",
   "text": "An alternative approach, using bits of #36133, is to give `Parse` access to the wallet's extended keys: [2026/09/parse-known-keys](https://github.com/Sjors/bitcoin/commits/2026/09/parse-known-keys/) (2 commits)\n\nAlthough it works fine when you give it all keys, I further simplified it by only considering hd keys."
  },
  {
   "t": "2026-09-14T20:50:50Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "eed498d498971af30fef1aa52d816ed06b6bd051"
  }
 ],
 "labels_log": [
  {
   "t": "2026-05-25T23:55:36Z",
   "action": "labeled",
   "label": "Wallet",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-26T10:21:12Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-26T19:42:02Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-12T00:04:40Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-13T03:24:03Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-13T04:22:24Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-19T23:04:38Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-26T23:02:09Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-27T03:30:19Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-03T11:21:53Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-08T22:24:52Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-14T09:19:08Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-06T00:49:26Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-06T01:23:06Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-08T03:56:29Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-11T11:58:07Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-26T22:40:06Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-14T13:31:56Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-14T22:41:28Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 7298,
 "text_tokens_estimate": 1824,
 "changed_paths": [
  "src/script/descriptor.cpp",
  "src/script/descriptor.h",
  "src/wallet/imports.cpp",
  "src/wallet/test/walletload_tests.cpp",
  "src/wallet/wallet.cpp",
  "src/wallet/wallet.h",
  "test/functional/wallet_importdescriptors.py"
 ],
 "files": [
  {
   "path": "src/script/descriptor.cpp",
   "add": 80,
   "del": 15
  },
  {
   "path": "src/script/descriptor.h",
   "add": 5,
   "del": 0
  },
  {
   "path": "src/wallet/imports.cpp",
   "add": 24,
   "del": 0
  },
  {
   "path": "src/wallet/test/walletload_tests.cpp",
   "add": 1,
   "del": 0
  },
  {
   "path": "src/wallet/wallet.cpp",
   "add": 31,
   "del": 0
  },
  {
   "path": "src/wallet/wallet.h",
   "add": 3,
   "del": 0
  },
  {
   "path": "test/functional/wallet_importdescriptors.py",
   "add": 67,
   "del": 4
  }
 ],
 "test_lines": 72,
 "git": {
  "head": "eed498d498971af30fef1aa52d816ed06b6bd051",
  "head_matches_backup": true,
  "base": "2c249dbdf5c9d8c7dd71ac90ea29a8b52ec4b2ba",
  "commits": [
   {
    "sha": "159a873c7a",
    "subject": "descriptor: Implement SubstituteMasterExtPubs",
    "files": 3,
    "add": 77,
    "del": 14
   },
   {
    "sha": "540bc1adf2",
    "subject": "wallet: Substitute known keys when importing a descriptor",
    "files": 3,
    "add": 58,
    "del": 0
   },
   {
    "sha": "7475cf5807",
    "subject": "descriptor, musig: Return participants in GetPubKeys",
    "files": 2,
    "add": 9,
    "del": 3
   },
   {
    "sha": "eed498d498",
    "subject": "test: Test importdescriptors with descriptors without privkeys",
    "files": 1,
    "add": 67,
    "del": 2
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "bbe5dc58d3337a71",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}