{
 "number": 36332,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36332",
 "title": "sign: add musig2 data after validating musig2 involvement",
 "author": "achow101",
 "author_association": "MEMBER",
 "created_at": "2026-09-24T20:49:42Z",
 "updated_at": "2026-10-22T09:20:02Z",
 "description_hash": "cd6935d9b2ced4f8",
 "age_days": 38,
 "draft": false,
 "labels": [],
 "milestone": null,
 "base": "master",
 "head_sha": "bca240a0e0a5b229ffab71760511cb746046212f",
 "head_ref": "musig2-no-extra-leafs",
 "head_repo": "achow101/bitcoin",
 "head_history": [],
 "additions": 68,
 "deletions": 18,
 "changed_files": 2,
 "commit_count": 3,
 "size_bucket": "S",
 "mergeable_state": "unknown",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "vicjuma",
      "url": "https://github.com/bitcoin/bitcoin/pull/36332#pullrequestreview-5323241566"
     }
    ],
    "concept_ack": [
     {
      "login": "Bicaru20",
      "url": "https://github.com/bitcoin/bitcoin/pull/36332#pullrequestreview-5331628059"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "vicjuma": {
   "kind": "ack",
   "hash": "bca240a0e0a5b229ffab71760511cb746046212f",
   "t": "2026-09-25T22:52:29Z",
   "stale": false
  },
  "Bicaru20": {
   "kind": "concept_ack",
   "hash": "bca240a0e0a5b229ffab71760511cb746046212f",
   "t": "2026-09-27T19:05:30Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 1,
  "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": [
   "Bicaru20",
   "Zeegaths",
   "fametrano",
   "polespinasa",
   "vicjuma"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "unknown",
  "last_author_activity": "2026-09-24T20:37:23Z",
  "last_reviewer_activity": "2026-10-01T21:28:30Z",
  "last_reviewer": "fametrano",
  "author_silent_days": 38,
  "waiting_on_author_days": 31,
  "days_since_update": 10
 },
 "refs": {
  "mentioned": [
   36323
  ],
  "depends_on": [],
  "fixes": [
   36323
  ],
  "linked_issues": [
   {
    "number": 36323,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "wallet/psbt/musig2: overpopulating participant leaf_hashes in utxoupdatepsbt and descriptorprocesspsbt"
   }
  ],
  "references": [
   {
    "number": 36323,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "wallet/psbt/musig2: overpopulating participant leaf_hashes in utxoupdatepsbt and descriptorprocesspsbt"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/script/sign.cpp",
  "test/functional/rpc_psbt.py"
 ],
 "body": "Previously, signing was assuming that MuSig2 information from the given `SigningProvider` was all from the descriptor that is relevant to the input being signed. However, in both `descriptorprocesspsbt` and `utxoupdatepsbt`, this is not true as information from all provided descriptors is put into the `SigningProvider` that is passed to `SignTaproot`. Consequently, if one of the given descriptors involved a MuSig2, the MuSig2 information would be accidentally added to the `SignatureData` which results in those fields appearing in the resulting PSBT.\n\nBoth the filling of aggregate pubkey information as well as participant BIP 32 derivation paths is moved to after the aggregate pubkey check inside of `SignMuSig2`. This ensures that information about only the aggregates pertaining to the particular script being signed are included in the resulting PSBT.\n\nFixes #36323",
 "commits": [
  {
   "sha": "60fce07641312234b48695fe4c77ca5e619d00e0",
   "date": "2026-09-24T20:36:52Z",
   "message": "sign: Fill musig participant derivation paths after checking aggregate\n\nIf the given SigningProvider contains information for more than one\ndescriptor, we should fill participant derivation paths only after we\nare sure the current input being signed involves the current aggregate\npubkey."
  },
  {
   "sha": "eab30978ea846ff8cc4a8c7f7bbefc452abd2f11",
   "date": "2026-09-24T20:37:21Z",
   "message": "sign: Fill aggregate pubkeys after determining involvement\n\nWhen attempting to sign an input with MuSig2, we should only be filling\nin aggregate pubkey information after checking whether the aggregate is\nrelevant to the input."
  },
  {
   "sha": "bca240a0e0a5b229ffab71760511cb746046212f",
   "date": "2026-09-24T20:37:23Z",
   "message": "test: Check musig2 psbt fields when updating with multiple descriptors\n\nWhen updating a PSBT using multiple descriptors, musig2 fields were\naccidentally being added when they should not have been. Check that\nthese fields are being correctly ommitted."
  }
 ],
 "timeline": [
  {
   "t": "2026-09-25T22:19:26Z",
   "kind": "review_comment",
   "who": "vicjuma",
   "assoc": "CONTRIBUTOR",
   "path": "src/script/sign.cpp",
   "commit": "bca240a0e0a5b229ffab71760511cb746046212f",
   "in_reply_to": null,
   "text": "Looking at the `taproot_bip32_derivs` before and after the change with the `descriptorprocesspsbt` the unnecessary fields are being omitted as this PR intends\n\n**Before:**\n\nkeys not specifically assigned to the script are present with `\"path\": \"m\",`\n\n**After:**\n\nkeys not specifically assigned to the script are excluded",
   "id": "rc:4109087318",
   "url": "https://github.com/bitcoin/bitcoin/pull/36332#discussion_r4109087318",
   "hash": "00773a5b63bc78a4",
   "edited": "2026-09-25T22:52:29Z"
  },
  {
   "t": "2026-09-25T22:29:18Z",
   "kind": "review_comment",
   "who": "vicjuma",
   "assoc": "CONTRIBUTOR",
   "path": "src/script/sign.cpp",
   "commit": "bca240a0e0a5b229ffab71760511cb746046212f",
   "in_reply_to": null,
   "text": "nit:\nCalled on every signing attempt, is it optimal to avoid repeated lookups\n\n```console\nLEAF1=$(./bitcoin-cli -rpcwallet=$LEAF_WALLET getaddressinfo \"$LEAF1_ADDR\" | jq -r '.pubkey')\n\nLEAF2=$(./bitcoin-cli -rpcwallet=$LEAF_WALLET getaddressinfo \"$LEAF2_ADDR\" | jq -r '.pubkey')\n\nLEAF3=$(./bitcoin-cli -rpcwallet=$LEAF_WALLET getaddressinfo \"$LEAF3_ADDR\" | jq -r '.pubkey')\n\nCAROL_DESC=\"tr(tpubDCwXwcLsjrohrJvaPFgWCwg3CFYjjLPYS37y3nncwkBEByZGA5k2oZ5WMxa25gUWzatAH6MyRPqQRvKtCH9ocNZAN6aVZ3YqwRYV8GtdsRS/0/*,{pk($LEAF1),{pk($LEAF2),pk($LEAF3)}})\"\n```\nGetting ~4 hits with the above descriptor\n\n```cpp\n LogInfo(\"MUSIG2_LOOKUP_HIT\\n\");\n std::map<CPubKey, std::vector<CPubKey>> agg_keys = provider.GetAllMuSig2ParticipantPubkeys();\n```\n\n[Reproduced](https://github.com/vicjuma/pr-36332-used-commands/blob/main/pr-36332.txt)",
   "id": "rc:4109136155",
   "url": "https://github.com/bitcoin/bitcoin/pull/36332#discussion_r4109136155",
   "hash": "0f6b4e96e09d16b9",
   "edited": "2026-09-26T06:43:00Z"
  },
  {
   "t": "2026-09-25T22:52:29Z",
   "kind": "review",
   "who": "vicjuma",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "bca240a0e0a5b229ffab71760511cb746046212f",
   "text": "ACK bca240a0e0a5b229ffab71760511cb746046212f",
   "id": "r:5323241566",
   "url": "https://github.com/bitcoin/bitcoin/pull/36332#pullrequestreview-5323241566",
   "hash": "b3c4e4a00b916f01",
   "edited": null
  },
  {
   "t": "2026-09-27T18:21:03Z",
   "kind": "comment",
   "who": "Zeegaths",
   "assoc": "NONE",
   "text": "I ran the RPCs on both versions, and this is what I got:\n\non master: All leaf hashes are populated\n\non PR: The leaf hashes are empty on 2/3 keys\n\nThe fourth entry is the taproot aggregate, which is always empty",
   "id": "c:5858509383",
   "url": "https://github.com/bitcoin/bitcoin/pull/36332#issuecomment-5858509383",
   "hash": "9d8dd5f91e6f2726",
   "edited": "2026-09-27T18:21:29Z"
  },
  {
   "t": "2026-09-27T18:55:08Z",
   "kind": "review_comment",
   "who": "Bicaru20",
   "assoc": "CONTRIBUTOR",
   "path": "src/script/sign.cpp",
   "commit": "bca240a0e0a5b229ffab71760511cb746046212f",
   "in_reply_to": 4109136155,
   "text": "I agree this could be improved. I can think of doing two things:\n\n- Create the `agg_keys` in `SignTaproot` so we only call `GetAllMuSig2ParticipantPubkeys` once.\n- Leave the insert of ` sigdata.musig2_pubkeys` as it was in `SignTaproot` and use `insert_or_assign` to delete the irrelevant aggregated pubkeys from sigdata. I've tried to implement this last option:\n\n```diff\n--- a/src/script/sign.cpp\n+++ b/src/script/sign.cpp\n@@ -291,10 +291,7 @@ static bool SignMuSig2(const BaseSignatureCreator& creator, SignatureData& sigda\n         agg_info = misc_pk_it->second.second;\n     }\n\n-    std::map<CPubKey, std::vector<CPubKey>> agg_keys = provider.GetAllMuSig2ParticipantPubkeys();\n-    agg_keys.insert(sigdata.musig2_pubkeys.begin(), sigdata.musig2_pubkeys.end());\n-\n-    for (const auto& [agg_pub, part_pks] : agg_keys) {\n+    for (const auto& [agg_pub, part_pks] : sigdata.musig2_pubkeys) {\n         if (part_pks.empty()) continue;\n\n         // The pubkey in the script may not be the actual aggregate of the participants, but derived from it.\n@@ -321,7 +318,7 @@ static bool SignMuSig2(const BaseSignatureCreator& creator, SignatureData& sigda\n         }\n\n         // Aggregate is now relevant, add to sigdata\n-        sigdata.musig2_pubkeys.emplace(agg_pub, part_pks);\n+        sigdata.musig2_pubkeys.insert_or_assign(agg_pub, part_pks);\n\n         // Fill participant derivation path info\n         for (const auto& part_pk : part_pks) {\n@@ -573,6 +570,9 @@ static bool SignTaproot(const SigningProvider& provider, const BaseSignatureCrea\n     if (provider.GetTaprootBuilder(output, builder)) {\n         sigdata.tr_builder = builder;\n     }\n+    if (auto agg_keys = provider.GetAllMuSig2ParticipantPubkeys(); !agg_keys.empty()) {\n+        sigdata.musig2_pubkeys.insert(agg_keys.begin(), agg_keys.end());\n+    }\n```\n</detail>",
   "id": "rc:4116528878",
   "url": "https://github.com/bitcoin/bitcoin/pull/36332#discussion_r4116528878",
   "hash": "f57cc5fb2ddd5e48",
   "edited": "2026-09-27T18:55:09Z"
  },
  {
   "t": "2026-09-27T19:05:14Z",
   "kind": "review_comment",
   "who": "Bicaru20",
   "assoc": "CONTRIBUTOR",
   "path": "test/functional/rpc_psbt.py",
   "commit": "bca240a0e0a5b229ffab71760511cb746046212f",
   "in_reply_to": null,
   "text": "nit: I think it would make the test easy to follow if the condition check directly the descriptor. Sth like this:\n```diff\n--- a/test/functional/rpc_psbt.py\n+++ b/test/functional/rpc_psbt.py\n@@ -315,12 +315,10 @@ class PSBTTest(BitcoinTestFramework):\n         musig_descriptor = descsum_create(\n             f\"tr(musig([11111111]{part_a},[22222222]{part_b}),pk([33333333]{leaf_key}))\"\n         )\n+        not_musig_descriptor = descsum_create(f\"tr([33333333]{leaf_key},pk([33333333]{leaf_key}))\")\n         # Also retain known origins when the aggregate matches neither the\n         # internal key nor the script key of the output being updated.\n-        for i, descriptor in enumerate([\n-            musig_descriptor,\n-            descsum_create(f\"tr([33333333]{leaf_key},pk([33333333]{leaf_key}))\"),\n-        ]):\n+        for descriptor in [musig_descriptor, not_musig_descriptor]:\n             descriptors = [musig_descriptor, descriptor]\n             address = node.deriveaddresses(descriptor)[0]\n             script = bytes.fromhex(node.validateaddress(address)[\"scriptPubKey\"])\n@@ -341,13 +339,13 @@ class PSBTTest(BitcoinTestFramework):\n                         entry[\"pubkey\"]: entry\n                         for entry in decoded[scope][0][\"taproot_bip32_derivs\"]\n                     }\n-                    if i == 1:\n+                    if descriptor == not_musig_descriptor:\n                         assert \"musig_participant_pubkeys\" not in decoded[scope][0]\n\n                     # Only the script key is involved in the leaf (BIP371/373).\n                     assert_equal(len(origins[leaf_key[2:]][\"leaf_hashes\"]), 1)\n\n-                    if i == 0:\n+                    if descriptor == musig_descriptor:\n                         for participant, fingerprint in ((part_a, \"11111111\"), (part_b, \"22222222\")):\n                             assert_equal(origins[participant[2:]][\"leaf_hashes\"], [])\n                             assert_equal(origins[participant[2:]][\"master_fingerprint\"], fingerprint)\n```",
   "id": "rc:4116560626",
   "url": "https://github.com/bitcoin/bitcoin/pull/36332#discussion_r4116560626",
   "hash": "ffa21b5989f50134",
   "edited": "2026-09-27T19:05:30Z"
  },
  {
   "t": "2026-09-27T19:05:30Z",
   "kind": "review",
   "who": "Bicaru20",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "bca240a0e0a5b229ffab71760511cb746046212f",
   "text": "Concept ACK bca240a0e0a5b229ffab71760511cb746046212f. I agree with @vicjuma that in `sign.cpp` the code could be optimize to avoid repited calls pn `GetAllMuSig2ParticipantPubkeys`.\n\n- 60fce07641312234b48695fe4c77ca5e619d00e0: It changes the location of the loop that fills participant derivation path info, so we fill them after we check that the current inputs uses the aggregate pubkey.\n\n- eab30978ea846ff8cc4a8c7f7bbefc452abd2f11: Before this, we added the aggregate pubkey information before we even called `SignMuSig2`. This commits modifies when the `sigdata.musig2_pubkeys` are filled passing inside the `SignMuSig2` function. This way, inside the function we only add the aggregate pubkey information to sigdata if it is relevant to the current input.\n\n- bca240a0e0a5b229ffab71760511cb746046212f: Test to check that musig2 fields are not accidentally being added",
   "id": "r:5331628059",
   "url": "https://github.com/bitcoin/bitcoin/pull/36332#pullrequestreview-5331628059",
   "hash": "e127cd2f01a91cac",
   "edited": null
  },
  {
   "t": "2026-09-28T17:41:40Z",
   "kind": "review_comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "path": "test/functional/rpc_psbt.py",
   "commit": "bca240a0e0a5b229ffab71760511cb746046212f",
   "in_reply_to": null,
   "text": "in bca240a0e0a5b229ffab71760511cb746046212f test: Check musig2 psbt fields when updating with multiple descriptors\n\nThis assert is wrong\n```suggestion\n                        assert \"musig2_participant_pubkeys\" not in decoded[scope][0]\n```",
   "id": "rc:4125261587",
   "url": "https://github.com/bitcoin/bitcoin/pull/36332#discussion_r4125261587",
   "hash": "22ad40be7d669d7d",
   "edited": "2026-09-28T17:44:41Z"
  },
  {
   "t": "2026-09-28T17:43:38Z",
   "kind": "review_comment",
   "who": "polespinasa",
   "assoc": "MEMBER",
   "path": "test/functional/rpc_psbt.py",
   "commit": "bca240a0e0a5b229ffab71760511cb746046212f",
   "in_reply_to": null,
   "text": "in bca240a0e0a5b229ffab71760511cb746046212f test: Check musig2 psbt fields when updating with multiple descriptors\n\nMaybe assert here than the fields are present?\nConsider adding\n```python\n\"musig2_participant_pubkeys\" in decoded[scope][0]\n```",
   "id": "rc:4125278894",
   "url": "https://github.com/bitcoin/bitcoin/pull/36332#discussion_r4125278894",
   "hash": "8d7c6e54ceb1d40f",
   "edited": "2026-09-28T17:44:41Z"
  },
  {
   "t": "2026-10-01T21:28:29Z",
   "kind": "comment",
   "who": "fametrano",
   "assoc": "CONTRIBUTOR",
   "text": "Tested bca240a0e0: the fix works. I will review again once the assert polespinasa flagged is fixed.\n\nI ran `utxoupdatepsbt` and `descriptorprocesspsbt` on master and on this PR with the issue's descriptor and four variants: musig() in a leaf, ranged participants, a BIP328-derived aggregate, and a key in two aggregates. On master a participant gets every leaf hash. With the PR it gets only the hashes of leaves that contain it or an aggregate it belongs to.\n\n@Zeegaths if your descriptor has the issue's shape, `tr(musig(A,B),pk(C))`, the two keys that lose the hash are A and B. They are only in the key path, so the empty lists are what the issue asks for.",
   "id": "c:5940991797",
   "url": "https://github.com/bitcoin/bitcoin/pull/36332#issuecomment-5940991797",
   "hash": "f38fb9bef637ae74",
   "edited": "2026-10-22T09:19:57Z"
  },
  {
   "t": "2026-10-01T21:28:30Z",
   "kind": "review_comment",
   "who": "fametrano",
   "assoc": "CONTRIBUTOR",
   "path": "test/functional/rpc_psbt.py",
   "commit": "bca240a0e0a5b229ffab71760511cb746046212f",
   "in_reply_to": 4125261587,
   "text": "Agreed: `decodepsbt` writes `musig2_participant_pubkeys`, so the assert as written can never fail. With eab30978ea reverted the unrelated aggregate is back, and the test still passes. With your suggestion it fails on that assert, and it passes on the PR.",
   "id": "rc:4160674221",
   "url": "https://github.com/bitcoin/bitcoin/pull/36332#discussion_r4160674221",
   "hash": "8359f1bc8ae14a38",
   "edited": null
  }
 ],
 "labels_log": [],
 "state_log": [],
 "text_chars": 9213,
 "text_tokens_estimate": 2303,
 "changed_paths": [
  "src/script/sign.cpp",
  "test/functional/rpc_psbt.py"
 ],
 "files": [
  {
   "path": "src/script/sign.cpp",
   "add": 20,
   "del": 18
  },
  {
   "path": "test/functional/rpc_psbt.py",
   "add": 48,
   "del": 0
  }
 ],
 "test_lines": 48,
 "git": {
  "head": "bca240a0e0a5b229ffab71760511cb746046212f",
  "head_matches_backup": true,
  "base": "bfdcd9797cd1a1345bdaf7ee7ef48f94028262da",
  "patch_id": "01365eb3f0e89245",
  "commits": [
   {
    "sha": "60fce07641",
    "subject": "sign: Fill musig participant derivation paths after checking aggregate",
    "files": 1,
    "add": 13,
    "del": 13
   },
   {
    "sha": "eab30978ea",
    "subject": "sign: Fill aggregate pubkeys after determining involvement",
    "files": 1,
    "add": 7,
    "del": 5
   },
   {
    "sha": "bca240a0e0",
    "subject": "test: Check musig2 psbt fields when updating with multiple descriptors",
    "files": 1,
    "add": 48,
    "del": 0
   }
  ],
  "patch_truncated": false
 },
 "patch_id": "01365eb3f0e89245",
 "input_hash": "697efa84bd36d127",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}