{
 "number": 35742,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35742",
 "title": "descriptors: check duplicate keys in all multipath Miniscript branches",
 "author": "yashbhutwala",
 "author_association": "CONTRIBUTOR",
 "created_at": "2026-07-17T20:36:49Z",
 "updated_at": "2026-09-09T11:40:43Z",
 "age_days": 61,
 "draft": false,
 "labels": [
  "Needs rebase",
  "Descriptors"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "5ced9e7cfd69b2f7f19e16908c4069560f0975fc",
 "head_ref": "fix/multipath-miniscript-sanity",
 "head_repo": "yashbhutwala/bitcoin",
 "head_history": [
  {
   "t": "2026-09-05T19:07:42Z",
   "sha": "5ced9e7cfd69b2f7f19e16908c4069560f0975fc"
  }
 ],
 "additions": 39,
 "deletions": 3,
 "changed_files": 2,
 "commit_count": 1,
 "size_bucket": "S",
 "mergeable_state": "dirty",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "l0rinc",
      "url": "https://github.com/bitcoin/bitcoin/pull/35742#issuecomment-5546853919"
     },
     {
      "login": "vicjuma",
      "url": "https://github.com/bitcoin/bitcoin/pull/35742#pullrequestreview-5134577838"
     }
    ]
   },
   "conflicts": [
    {
     "number": 36122,
     "title": "BIP460: CISA for Taproot key path spends",
     "author": "fjahr"
    },
    {
     "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"
    }
   ]
  }
 },
 "acks_parsed": {
  "l0rinc": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-09-04T21:51:10Z",
   "stale": false
  },
  "vicjuma": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-09-07T18:27: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": 1,
  "distinct_reviewers": [
   "Zeegaths",
   "l0rinc",
   "vicjuma"
  ]
 },
 "signals": {
  "needs_rebase": true,
  "ci_failed": false,
  "mergeable_state": "dirty",
  "last_author_activity": "2026-09-05T19:07:42Z",
  "last_reviewer_activity": "2026-09-08T19:51:44Z",
  "last_reviewer": "l0rinc",
  "author_silent_days": 11,
  "waiting_on_author_days": 8,
  "days_since_update": 8
 },
 "refs": {
  "mentioned": [
   22838,
   35629
  ],
  "depends_on": [],
  "fixes": [
   35629
  ],
  "linked_issues": [
   {
    "number": 35629,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "Duplicate-key sanity for multipath Miniscript descriptors only checks branch 0, so nonzero-branch key collisions are accepted"
   }
  ],
  "references": [
   {
    "number": 35629,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "Duplicate-key sanity for multipath Miniscript descriptors only checks branch 0, so nonzero-branch key collisions are accepted"
   },
   {
    "number": 22838,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2024-08-28",
    "title": "descriptors: Be able to specify change and receiving in a single descriptor string"
   }
  ],
  "conflicts": [
   36122,
   35445
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/script/descriptor.cpp"
 ],
 "body": "Fixes #35629.\n\nMultipath Miniscript descriptors performed duplicate-key sanity checking while parsing the template, when key comparison used the first multipath branch. As a result, duplicate keys present only in a later branch were accepted.\n\nSelect each expanded multipath branch and rerun the existing duplicate-key check before constructing the final descriptors. If a branch contains a duplicate, report the offending concrete Miniscript expression.\n\nThe regression test covers:\n\n- a valid two-branch `wsh` control;\n- a duplicate in the second `wsh` branch;\n- a duplicate in the third of three `tr` branches.\n\nTested with:\n\n```\nbuild/bin/test_bitcoin --run_test=descriptor_tests/multipath_miniscript_duplicate_keys\nbuild/bin/test_bitcoin --run_test=descriptor_tests\nbuild/bin/test_bitcoin\n```",
 "commits": [
  {
   "sha": "5ced9e7cfd69b2f7f19e16908c4069560f0975fc",
   "date": "2026-09-05T19:02:14Z",
   "message": "descriptor: check duplicate keys in all multipath branches"
  }
 ],
 "timeline": [
  {
   "t": "2026-08-04T09:05:19Z",
   "kind": "comment",
   "who": "Zeegaths",
   "assoc": "NONE",
   "text": "I ran this on master and on pr-35742,\n\n```\nbitcoin-cli getdescriptorinfo \"wsh(or_i(pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/<0;1>),pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/<2;1>)))\"\n```\n\nand this is what I got:\n\n**On master**\nSame public key is generated\n\n```json\n{\n  \"descriptor\": \"wsh(or_i(pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/0),pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/2)))#kzwg47ay\",\n  \"multipath_expansion\": [\n    \"wsh(or_i(pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/0),pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/2)))#kzwg47ay\",\n    \"wsh(or_i(pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/1),pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/1)))#jkt2efkp\"\n  ],\n  \"checksum\": \"mn7hqkkh\",\n  \"isrange\": false,\n  \"issolvable\": true,\n  \"hasprivatekeys\": false\n}\n```\n\n**On branch**\n\n```\nerror code: -5\nerror message:\nor_i(pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/1),pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/1)) is not sane: contains duplicate public keys\n```\n\nI also checked using `importdescriptors` to observe wallet action.\n\n**On master**, the unsafe branch is added to the wallet:\n\n```\nbitcoin-cli -rpcwallet=testwallet_master importdescriptors '[{\"desc\": \"wsh(or_i(pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/<0;1>),pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/<2;1>)))#mn7hqkkh\", \"timestamp\": \"now\", \"active\": false}]'\n```\n```json\n[\n  {\n    \"success\": true\n  }\n]\n```\n\n**On the PR:**\n\n```json\n[\n  {\n    \"success\": false,\n    \"error\": {\n      \"code\": -5,\n      \"message\": \"or_i(pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/1),pk(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/1)) is not sane: contains duplicate public keys\"\n    }\n  }\n]\n```\n\nThis shows exactly how the PR fixes the silent passing on master"
  },
  {
   "t": "2026-09-04T21:51:10Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Concept ACK, I also found the same issue - please rebase."
  },
  {
   "t": "2026-09-05T19:07:42Z",
   "kind": "force_push",
   "who": "yashbhutwala",
   "commit": "5ced9e7cfd69b2f7f19e16908c4069560f0975fc"
  },
  {
   "t": "2026-09-07T18:26:04Z",
   "kind": "review_comment",
   "who": "vicjuma",
   "assoc": "CONTRIBUTOR",
   "path": "src/script/descriptor.cpp",
   "commit": "5ced9e7cfd69b2f7f19e16908c4069560f0975fc",
   "in_reply_to": null,
   "text": "#22838 :-}. This change aligns with BIP389. Hardcoding the 0th index assumes that there is always a single path/branch that contradicts the BIP imo. Reproduced the error.\n\nBefore PR\n-----------------\n\nAfter PR\n---------------"
  },
  {
   "t": "2026-09-07T18:27:33Z",
   "kind": "review",
   "who": "vicjuma",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "5ced9e7cfd69b2f7f19e16908c4069560f0975fc",
   "text": "Concept ACK"
  },
  {
   "t": "2026-09-08T19:44:35Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/script/descriptor.cpp",
   "commit": "5ced9e7cfd69b2f7f19e16908c4069560f0975fc",
   "in_reply_to": null,
   "text": "Given that sane already checks for duplicates in https://github.com/bitcoin/bitcoin/blob/1fdd208c1ce32ae2385069f39e7d78972213424a/src/script/miniscript.h#L1699, as far as I can tell this can be simplified:\n```suggestion\n            // Recheck duplicate keys after multipath expansion, since parsing checked only branch 0\n            for (size_t i{1}; i < num_multipath; ++i) {\n                parser.m_multipath_index = i;\n                node->DuplicateKeyCheck(parser);\n                if (!node->CheckDuplicateKey()) {\n                    error = *node->FindInsaneSub()->ToString(parser) + \" is not sane: contains duplicate public keys\";\n                    return {};\n                }\n            }\n```"
  },
  {
   "t": "2026-09-08T19:51:44Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "CHANGES_REQUESTED",
   "commit": "5ced9e7cfd69b2f7f19e16908c4069560f0975fc",
   "text": "I suggest splitting this into focused commits so reviewers can understand the old behavior and the problem before assessing the fix: test infrastructure and controls, [characterization of the old behavior](https://github.com/bitcoin/bitcoin/blob/db74d3390a391a2a76d7b4d676342a9d1489059b/doc/developer-notes.md?plain=1#L698-L703) with TODOs explaining why those expectations are wrong, then the fix and updated expectations. See https://github.com/l0rinc/bitcoin/pull/295 for how I split the same change.\n\nWe should also take this opportunity to add coverage for mixed single-path/multipath keys, wildcards, hardened paths with and without private keys, differing origins, nested errors, and valid key sharing across branches and Taproot leaves.\n\nA functional test would demonstrate the wallet impact: these imports currently succeed but prevent reload. With the fix, rejected imports leave the descriptor set unchanged and the wallet reloadable.\n\nEdit: please make the PR description less robotic, show us how you tested this manually for example"
  }
 ],
 "labels_log": [
  {
   "t": "2026-07-17T20:36:53Z",
   "action": "labeled",
   "label": "Descriptors",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-24T11:30:34Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-05T20:10:08Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-09T11:40:42Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-08-02T13:18:22Z",
   "kind": "ready_for_review",
   "who": "yashbhutwala"
  }
 ],
 "text_chars": 5692,
 "text_tokens_estimate": 1423,
 "changed_paths": [
  "src/script/descriptor.cpp",
  "src/test/descriptor_tests.cpp"
 ],
 "files": [
  {
   "path": "src/script/descriptor.cpp",
   "add": 18,
   "del": 3
  },
  {
   "path": "src/test/descriptor_tests.cpp",
   "add": 21,
   "del": 0
  }
 ],
 "test_lines": 21,
 "git": {
  "head": "5ced9e7cfd69b2f7f19e16908c4069560f0975fc",
  "head_matches_backup": true,
  "base": "f0c839ace5a2a5e2651c580c76974c514f1500f8",
  "commits": [
   {
    "sha": "5ced9e7cfd",
    "subject": "descriptor: check duplicate keys in all multipath branches",
    "files": 2,
    "add": 39,
    "del": 3
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "dc012132d1235847",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}