{
 "number": 36325,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36325",
 "title": "Contrib: add multisig wizard",
 "author": "rxbryan",
 "author_association": "FIRST_TIME_CONTRIBUTOR",
 "created_at": "2026-09-24T12:21:15Z",
 "updated_at": "2026-10-29T05:37:29Z",
 "description_hash": "a7f484f67e4e386b",
 "age_days": 38,
 "draft": false,
 "labels": [
  "Scripts and tools"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "2803e1518bb22394a80bac94e2435bb3982d0cde",
 "head_ref": "contrib_multisig",
 "head_repo": "rxbryan/bitcoin",
 "head_history": [
  {
   "t": "2026-09-24T21:29:15Z",
   "sha": "feb2f590909839f5768470a321aafeba0022dc57"
  },
  {
   "t": "2026-09-24T21:37:02Z",
   "sha": "5f0da0f148a8b7906b5bcb9db971a778a6aaf61e"
  },
  {
   "t": "2026-09-25T07:04:38Z",
   "sha": "2803e1518bb22394a80bac94e2435bb3982d0cde"
  }
 ],
 "additions": 710,
 "deletions": 0,
 "changed_files": 4,
 "commit_count": 5,
 "size_bucket": "L",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "BenWestgate",
      "url": "https://github.com/bitcoin/bitcoin/pull/36325#issuecomment-6488924478"
     }
    ],
    "concept_nack": [
     {
      "login": "pseudoramdom",
      "url": "https://github.com/bitcoin/bitcoin/pull/36325#issuecomment-6373344413"
     }
    ]
   },
   "conflicts": []
  }
 },
 "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": [
   "BenWestgate",
   "kevkevinpal",
   "pseudoramdom",
   "sedited"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-10-05T21:08:05Z",
  "last_reviewer_activity": "2026-10-29T05:37:14Z",
  "last_reviewer": "BenWestgate",
  "author_silent_days": 27,
  "waiting_on_author_days": 3,
  "days_since_update": 3
 },
 "refs": {
  "mentioned": [
   35377,
   35645,
   35918,
   36133,
   36143
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 35645,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "Multisig Wizard tracking issue"
   },
   {
    "number": 35377,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "wallet: Allow importing of descriptors without private keys when the wallet has the private keys"
   },
   {
    "number": 36133,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "wallet: store multipath descriptor"
   },
   {
    "number": 35918,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "descriptor: Expose key expression parsing"
   },
   {
    "number": 36143,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "descriptor: add CreateMultisigDescriptor()"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "contrib/multisig/README.md"
 ],
 "body": "Per the discussion in #35645, this PR adds a Python utility that performs the multisig setup process, so that RPC and interface work is shaped by what that flow actually needs.\n\nIt covers wallet creation, key generation at the BIP 87 path, collecting cosigner keys, assembling a `wsh(sortedmulti())` or `tr(musig(),sortedmulti_a())` descriptor, verifying it, and importing it.\n\n### Gaps that need new functionality in C++\n- `importdescriptors` rejects a descriptor with no private keys when the wallet has private keys enabled. See #35377.\n- The wallet stores a multipath descriptor as two descriptors, so listdescriptors never shows the descriptor that was shared. See #36133.\n- The wallet labels addresses, not keys, so cosigner names live in the setup and are lost on import.\n- There is no RPC for validating a key expression, so the key is wrapped in `pkh()` for `getdescriptorinfo` to parse. See #35918, which exposes `ParsePubkey()`.\n- `createmultisig` only takes plain public keys, not extended keys with origins, and cannot build a ranged or taproot multisig descriptor.\n- Nothing reports what a descriptor commits to, or whether this wallet holds one of its keys, before it is imported.\n\nPart of #35645.",
 "commits": [
  {
   "sha": "6feb6063bd2fdfbe6afb6347bccf883601032eba",
   "date": "2026-09-25T07:02:28Z",
   "message": "contrib: add multisig wizard\n\nFollowing the discussion on #36143, this commit adds a python\nutility implementing the multisig setup flow tracked in #35645\nusing available RPCs and implements work arounds for missing RPC\nor wallet features.\n\nAdds wallet creation and key generation step.\n\nThe wallet cannot store a name for a key, so cosigner names live\nin the setup and are lost on import."
  },
  {
   "sha": "6cfa28e1c879310fbb1fff130cb53c22ed0afa21",
   "date": "2026-09-25T07:02:29Z",
   "message": "contrib: validate cosigner key expressions\n\nCheck a key expression before recording it, and reject private keys,\nderivation paths, multipath derivations, keys without origin information\nand duplicates.\n\nThe wizard also requires the BIP 87 path, which the descriptor layer does\nnot."
  },
  {
   "sha": "154fd8922f9c5d9411ac78027dbcf98db52b7e64",
   "date": "2026-09-25T07:02:29Z",
   "message": "contrib: assemble wsh and taproot multisig descriptors\n\nBuilds the ranged, multipath descriptor from the collected keys and\na call to getdescriptorinfo checks the resulting descriptor and\nsupplys the checksum.\n\nFor taproot the key path is a MuSig2 aggregate of all n keys\nrather than a fixed or variable NUMS point: it cannot bypass the\npolicy, since spending that way needs every signer, and it avoids\nthe fingerprinting using a well-known internal key would cause."
  },
  {
   "sha": "039db5c3e3108c1bb7961b4a93d3f55040dd045a",
   "date": "2026-09-25T07:02:30Z",
   "message": "contrib: analyze and verify a multisig descriptor\n\nanalyze_descriptor reports what the descriptor requires to spend.\nWhich cosigner we are, and the first receive address,\n\nAccepted shapes are wsh(sortedmulti(...)) and, for taproot,\ntr(KEY,sortedmulti_a(...)) where KEY is either musig() of exactly\nthe keys in the leaf, so that spending by key path needs\nevery cosigner, or the NUMS point, which is provably unspendable.\nAnything else is rejected.\n\nNames are local, so any cosigner we have no name for is reported\nby fingerprint."
  },
  {
   "sha": "2803e1518bb22394a80bac94e2435bb3982d0cde",
   "date": "2026-09-25T07:02:30Z",
   "message": "contrib: add import multisig descriptor step\n\nDuring the import process, the participant swaps their own key\nexpression in the descriptor for the private one since\nimportdescriptors reject a descriptor with no private keys when\nthe wallet has private keys enabled. See #35377"
  }
 ],
 "timeline": [
  {
   "t": "2026-09-24T21:29:15Z",
   "kind": "force_push",
   "who": "rxbryan",
   "commit": "feb2f590909839f5768470a321aafeba0022dc57",
   "id": "push:feb2f590909839f5768470a321aafeba0022dc57"
  },
  {
   "t": "2026-09-24T21:37:02Z",
   "kind": "force_push",
   "who": "rxbryan",
   "commit": "5f0da0f148a8b7906b5bcb9db971a778a6aaf61e",
   "id": "push:5f0da0f148a8b7906b5bcb9db971a778a6aaf61e"
  },
  {
   "t": "2026-09-25T07:04:38Z",
   "kind": "force_push",
   "who": "rxbryan",
   "commit": "2803e1518bb22394a80bac94e2435bb3982d0cde",
   "id": "push:2803e1518bb22394a80bac94e2435bb3982d0cde"
  },
  {
   "t": "2026-09-25T07:43:50Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "Do we need this in the repository? Seems fine to me if it were just an external script.",
   "id": "c:5828804605",
   "url": "https://github.com/bitcoin/bitcoin/pull/36325#issuecomment-5828804605",
   "hash": "46659d9cf6d5bfad",
   "edited": null
  },
  {
   "t": "2026-09-26T08:33:23Z",
   "kind": "comment",
   "who": "rxbryan",
   "assoc": "NONE",
   "text": "Aside from being a demonstration of the multisig setup flow, the utility is intended to become a command-line equivalent of the GUI wizard that ships with Core so users can set up multisig without hand-assembling descriptors.\nThe workarounds in it will eventually be superseded as functionality is moved to C++.\n\nWhether that belongs in the repo is worth settling first.",
   "id": "c:5844636792",
   "url": "https://github.com/bitcoin/bitcoin/pull/36325#issuecomment-5844636792",
   "hash": "8b8d902a69d0240d",
   "edited": null
  },
  {
   "t": "2026-09-29T23:44:11Z",
   "kind": "review_comment",
   "who": "BenWestgate",
   "assoc": "CONTRIBUTOR",
   "path": "contrib/multisig/README.md",
   "commit": "2803e1518bb22394a80bac94e2435bb3982d0cde",
   "in_reply_to": null,
   "text": "```suggestion\nRPCs and implements workarounds for missing RPC or wallet features.\n```",
   "id": "rc:4139434264",
   "url": "https://github.com/bitcoin/bitcoin/pull/36325#discussion_r4139434264",
   "hash": "b7cfabe62b874ca3",
   "edited": "2026-09-30T00:18:39Z"
  },
  {
   "t": "2026-09-29T23:45:00Z",
   "kind": "review_comment",
   "who": "BenWestgate",
   "assoc": "CONTRIBUTOR",
   "path": "contrib/multisig/README.md",
   "commit": "2803e1518bb22394a80bac94e2435bb3982d0cde",
   "in_reply_to": null,
   "text": "```suggestion\nA coordinator that does not contribute keys needs a watch-only wallet.\n```",
   "id": "rc:4139438995",
   "url": "https://github.com/bitcoin/bitcoin/pull/36325#discussion_r4139438995",
   "hash": "589cdf527e11ac3e",
   "edited": "2026-09-30T00:18:39Z"
  },
  {
   "t": "2026-09-30T00:18:39Z",
   "kind": "review",
   "who": "BenWestgate",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "2803e1518bb22394a80bac94e2435bb3982d0cde",
   "text": "cACK, only _cursory_ code review of `wizard.py` but the interface and usage looks correct.\n\nAs for @sedited wondering if we need this in the repository:\nFor years, Core having user unfriendly multisig has been exploited by scammers to promote alternative multisig wallets with substantially less scrutiny than this repository.",
   "id": "r:5359830788",
   "url": "https://github.com/bitcoin/bitcoin/pull/36325#pullrequestreview-5359830788",
   "hash": "23dcfc1a89dd8dbd",
   "edited": "2026-10-04T06:29:16Z"
  },
  {
   "t": "2026-10-03T17:00:47Z",
   "kind": "comment",
   "who": "kevkevinpal",
   "assoc": "CONTRIBUTOR",
   "text": "I'm not sure we want this in this repository.\n\nLooking at the other scripts in `contrib` I don't see any that handle wallet utilities. If this were to be added it would make more sense to add it to the Bitcoin Core wallet.\n\nFrom what I understand in the discussion in https://github.com/bitcoin/bitcoin/issues/35645, this would not be used, as once the work in the GUI and wallet has finished, that would be preferred. The script is still valuable, but we might not want it with the contrib scripts.\n\n[quoted text omitted]\nI don't think the goal of Bitcoin Core is to be \"non-technologist\" friendly, and also adding a new script to the repo does not guarantee that it will be maintained. It is just another piece of code the Bitcoin Core maintainers will need to worry about forever.\n\n---\n\nTo me, it makes the most sense to keep this change on your own fork and share it with the group in the thread so they can still use it without merging it into this repo, where it will need to be maintained",
   "id": "c:5971349539",
   "url": "https://github.com/bitcoin/bitcoin/pull/36325#issuecomment-5971349539",
   "hash": "fc7d4f7f79371f4c",
   "edited": null
  },
  {
   "t": "2026-10-05T00:12:33Z",
   "kind": "comment",
   "who": "BenWestgate",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\n@rxbryan are you willing to maintain this script (including bug fixing) until the RPC obsoletes it?\n\n[quoted text omitted]\n@Sjors said in https://github.com/bitcoin/bitcoin/pull/36143#issuecomment-5508988125 a wallet c++ implementation is premature and he preferred to:\n[quoted text omitted]\n\nThe script serves as a functional to-do checklist for multisig, with its workarounds deleted as wallet PRs land.\n\nThe advantage of merging it into this repo is better scrutiny and CI against bugs for adventurous users who'd like to use it on mainnet.",
   "id": "c:5985919355",
   "url": "https://github.com/bitcoin/bitcoin/pull/36325#issuecomment-5985919355",
   "hash": "2ba89b7fed61fc63",
   "edited": null
  },
  {
   "t": "2026-10-05T21:08:05Z",
   "kind": "comment",
   "who": "rxbryan",
   "assoc": "NONE",
   "text": "[quoted text omitted]\nYes. I'll maintain it, we have enough test cases to force a maintenance contract on whoever modifies code touching it.",
   "id": "c:6002949265",
   "url": "https://github.com/bitcoin/bitcoin/pull/36325#issuecomment-6002949265",
   "hash": "558716f2af3a8197",
   "edited": null
  },
  {
   "t": "2026-10-23T19:45:25Z",
   "kind": "comment",
   "who": "pseudoramdom",
   "assoc": "MEMBER",
   "text": "Thanks for putting this together and listing the gaps.\n\nThat said, I'm leaning Concept NACK on merging this into the repo.\nThe goal of #35645 is for the wallet to handle multisig setup by itself - a GUI wizard on top of `interfaces::Wallet`, plus the RPCs behind it. Like you said that work would supersede this script.  Merging this would mean reviewing ~700 lines (script and functional test) whose job is to work around things we plan to fix. We'd then have to review again each time one of those fixes lands and removes a workaround. I'd rather prefer we direct our resources at implementing items in #35645, since they reach both RPC and GUI users.\n\nAlso putting a script in `contrib/` makes it look like a recommended tool and that's hard to undo once people use it with real funds.\n\nWhat we can do\n- We can track this separately in a separate fork/gist and have it linked from #35645, so people testing the backend PRs can use it. As @sedited mentioned, it works fine as an external script.\n- We can add the gaps from the description to #35645, so the Core backend work tracks them directly. Some are already there (#35377, #35918, `createmultisigdescriptor`, `analyzeDescriptor`)",
   "id": "c:6373344413",
   "url": "https://github.com/bitcoin/bitcoin/pull/36325#issuecomment-6373344413",
   "hash": "0d21357a2a4c0a44",
   "edited": null
  },
  {
   "t": "2026-10-23T20:27:35Z",
   "kind": "comment",
   "who": "BenWestgate",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\n Even I, supporting the idea of merging this into the repo due to the demand for a well reviewed multisig setup, did not fully code review it due to it's length and my expectation that it would receive cNACKs.\n\n What's the absolute smallest it could be made to maximize the number of people willing to review it and minimize the time it takes them?  For example were it 100-300 lines I likely would have code reviewed despite my expectations.\n\n[quoted text omitted]\n Could you modularize it so that each time one of our workarounds lands, we just delete a function or module and replace it with the new RPC?  So it's a +1/-?? diff to review?  I think that would maximizes the chances of support and review interest.\n\n[quoted text omitted]\nAgreed, if the goal of this script is to disappear that means we want a terminal or CLI wizard for multisig so that it is eventually replaced by something in C++, I haven't seen that talked about if that is the goal, although I would cACK that also since it's better than nothing and no GUI.",
   "id": "c:6374070423",
   "url": "https://github.com/bitcoin/bitcoin/pull/36325#issuecomment-6374070423",
   "hash": "0ad0a8e43fcda71c",
   "edited": "2026-10-23T20:28:33Z"
  },
  {
   "t": "2026-10-29T05:37:14Z",
   "kind": "comment",
   "who": "BenWestgate",
   "assoc": "CONTRIBUTOR",
   "text": "tACK 2803e1518\nAll wizard steps run on an offline node for 2-of-3 `wsh(sortedmulti)` at `m/87h/1h/0h`: `create_wallet`, `generate_key`, `add_cosigner`, `assemble`, `verify` and `import_descriptor`, for three key wallets and a watch-only coordinator. The watch-only wallet was restored online and created a PSBT. Two key wallets signed it offline, and `testmempoolaccept` allowed the result.\n\n### Review nits\n\n1. **`import_descriptor` uses `\"timestamp\": \"now\"`.** In `importdescriptors`, \"now\" means the best block's time. An offline node has only the genesis block, so the wallet's birth time is genesis. Restoring that wallet's backup on a pruned online node then fails with `Prune: last wallet synchronisation goes beyond pruned data. You need to -reindex`. The same descriptor imported with the current time restored fine on the same pruned node. Suggested fix: pass `int(time.time())`, or let the caller give a timestamp. On an online node the result is the same as \"now\".\n2. **No backup step.** After `import_descriptor`, each participant has to back up their key wallet and the descriptor. A key wallet alone can't rebuild the multisig. The wizard's docstrings don't mention this. It may belong in the README rather than the code.",
   "id": "c:6488924478",
   "url": "https://github.com/bitcoin/bitcoin/pull/36325#issuecomment-6488924478",
   "hash": "7c6760713e2870aa",
   "edited": "2026-10-29T05:37:29Z"
  }
 ],
 "labels_log": [
  {
   "t": "2026-09-24T12:21:19Z",
   "action": "labeled",
   "label": "Scripts and tools",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-26T18:28:25Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-26T19:35:09Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-09-24T21:29:46Z",
   "kind": "ready_for_review",
   "who": "rxbryan"
  }
 ],
 "text_chars": 9332,
 "text_tokens_estimate": 2333,
 "changed_paths": [
  "contrib/multisig/README.md",
  "contrib/multisig/wizard.py",
  "test/functional/test_runner.py",
  "test/functional/tool_multisig_wizard.py"
 ],
 "files": [
  {
   "path": "contrib/multisig/README.md",
   "add": 11,
   "del": 0
  },
  {
   "path": "contrib/multisig/wizard.py",
   "add": 348,
   "del": 0
  },
  {
   "path": "test/functional/test_runner.py",
   "add": 1,
   "del": 0
  },
  {
   "path": "test/functional/tool_multisig_wizard.py",
   "add": 350,
   "del": 0
  }
 ],
 "test_lines": 710,
 "git": {
  "head": "2803e1518bb22394a80bac94e2435bb3982d0cde",
  "head_matches_backup": true,
  "base": "5c726f2058a4d5ac2c7d0dc8753d4ff125d03f15",
  "patch_id": "9b0e8407cf5254cb",
  "commits": [
   {
    "sha": "6feb6063bd",
    "subject": "contrib: add multisig wizard",
    "files": 4,
    "add": 159,
    "del": 0
   },
   {
    "sha": "6cfa28e1c8",
    "subject": "contrib: validate cosigner key expressions",
    "files": 2,
    "add": 112,
    "del": 0
   },
   {
    "sha": "154fd8922f",
    "subject": "contrib: assemble wsh and taproot multisig descriptors",
    "files": 2,
    "add": 94,
    "del": 0
   },
   {
    "sha": "039db5c3e3",
    "subject": "contrib: analyze and verify a multisig descriptor",
    "files": 2,
    "add": 241,
    "del": 3
   },
   {
    "sha": "2803e1518b",
    "subject": "contrib: add import multisig descriptor step",
    "files": 2,
    "add": 107,
    "del": 0
   }
  ],
  "patch_truncated": false
 },
 "patch_id": "9b0e8407cf5254cb",
 "input_hash": "4b8f6024520a6837",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}