sign: add musig2 data after validating musig2 involvement #36332

pull achow101 wants to merge 3 commits into bitcoin:master from achow101:musig2-no-extra-leafs changing 2 files +68 −18
  1. achow101 commented at 8:49 PM on September 24, 2026: member

    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.

    Both 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.

    Fixes #36323

  2. sign: Fill musig participant derivation paths after checking aggregate
    If the given SigningProvider contains information for more than one
    descriptor, we should fill participant derivation paths only after we
    are sure the current input being signed involves the current aggregate
    pubkey.
    60fce07641
  3. sign: Fill aggregate pubkeys after determining involvement
    When attempting to sign an input with MuSig2, we should only be filling
    in aggregate pubkey information after checking whether the aggregate is
    relevant to the input.
    eab30978ea
  4. test: Check musig2 psbt fields when updating with multiple descriptors
    When updating a PSBT using multiple descriptors, musig2 fields were
    accidentally being added when they should not have been. Check that
    these fields are being correctly ommitted.
    bca240a0e0
  5. DrahtBot commented at 8:49 PM on September 24, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/36332.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    ACK vicjuma
    Concept ACK Bicaru20

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  6. in src/script/sign.cpp:323 in bca240a0e0
     319 | @@ -330,6 +320,22 @@ static bool SignMuSig2(const BaseSignatureCreator& creator, SignatureData& sigda
     320 |              plain_pub = extpub.pubkey;
     321 |          }
     322 |  
     323 | +        // Aggregate is now relevant, add to sigdata
    


    vicjuma commented at 10:19 PM on September 25, 2026:

    Looking at the taproot_bip32_derivs before and after the change with the descriptorprocesspsbt the unnecessary fields are being omitted as this PR intends

    Before:

    <img width="3832" height="1896" alt="Image" src="https://github.com/user-attachments/assets/a9ebd910-e324-4868-bda2-42ec50923c73" />

    keys not specifically assigned to the script are present with "path": "m",

    After:

    <img width="3828" height="1684" alt="Image" src="https://github.com/user-attachments/assets/9ce9b6ca-7f1a-4e85-9148-1ada1770f7eb" />

    keys not specifically assigned to the script are excluded

  7. in src/script/sign.cpp:294 in bca240a0e0
     290 | @@ -291,21 +291,11 @@ static bool SignMuSig2(const BaseSignatureCreator& creator, SignatureData& sigda
     291 |          agg_info = misc_pk_it->second.second;
     292 |      }
     293 |  
     294 | -    for (const auto& [agg_pub, part_pks] : sigdata.musig2_pubkeys) {
     295 | -        if (part_pks.empty()) continue;
     296 | +    std::map<CPubKey, std::vector<CPubKey>> agg_keys = provider.GetAllMuSig2ParticipantPubkeys();
    


    vicjuma commented at 10:29 PM on September 25, 2026:

    nit: Called on every signing attempt, is it optimal to avoid repeated lookups

    LEAF1=$(./bitcoin-cli -rpcwallet=$LEAF_WALLET getaddressinfo "$LEAF1_ADDR" | jq -r '.pubkey')
    
    LEAF2=$(./bitcoin-cli -rpcwallet=$LEAF_WALLET getaddressinfo "$LEAF2_ADDR" | jq -r '.pubkey')
    
    LEAF3=$(./bitcoin-cli -rpcwallet=$LEAF_WALLET getaddressinfo "$LEAF3_ADDR" | jq -r '.pubkey')
    
    CAROL_DESC="tr(tpubDCwXwcLsjrohrJvaPFgWCwg3CFYjjLPYS37y3nncwkBEByZGA5k2oZ5WMxa25gUWzatAH6MyRPqQRvKtCH9ocNZAN6aVZ3YqwRYV8GtdsRS/0/*,{pk($LEAF1),{pk($LEAF2),pk($LEAF3)}})"
    

    Getting ~4 hits with the above descriptor

    <img width="1624" height="230" alt="Screenshot From 2026-09-26 01-38-41" src="https://github.com/user-attachments/assets/f5b33c80-91e1-4aa0-947e-3b7da15df983" />

     LogInfo("MUSIG2_LOOKUP_HIT\n");
     std::map<CPubKey, std::vector<CPubKey>> agg_keys = provider.GetAllMuSig2ParticipantPubkeys();
    

    Reproduced


    Bicaru20 commented at 6:55 PM on September 27, 2026:

    I agree this could be improved. I can think of doing two things:

    • Create the agg_keys in SignTaproot so we only call GetAllMuSig2ParticipantPubkeys once.
    • 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:
    --- a/src/script/sign.cpp
    +++ b/src/script/sign.cpp
    @@ -291,10 +291,7 @@ static bool SignMuSig2(const BaseSignatureCreator& creator, SignatureData& sigda
             agg_info = misc_pk_it->second.second;
         }
     
    -    std::map<CPubKey, std::vector<CPubKey>> agg_keys = provider.GetAllMuSig2ParticipantPubkeys();
    -    agg_keys.insert(sigdata.musig2_pubkeys.begin(), sigdata.musig2_pubkeys.end());
    -
    -    for (const auto& [agg_pub, part_pks] : agg_keys) {
    +    for (const auto& [agg_pub, part_pks] : sigdata.musig2_pubkeys) {
             if (part_pks.empty()) continue;
     
             // The pubkey in the script may not be the actual aggregate of the participants, but derived from it.
    @@ -321,7 +318,7 @@ static bool SignMuSig2(const BaseSignatureCreator& creator, SignatureData& sigda
             }
     
             // Aggregate is now relevant, add to sigdata
    -        sigdata.musig2_pubkeys.emplace(agg_pub, part_pks);
    +        sigdata.musig2_pubkeys.insert_or_assign(agg_pub, part_pks);
     
             // Fill participant derivation path info
             for (const auto& part_pk : part_pks) {
    @@ -573,6 +570,9 @@ static bool SignTaproot(const SigningProvider& provider, const BaseSignatureCrea
         if (provider.GetTaprootBuilder(output, builder)) {
             sigdata.tr_builder = builder;
         }
    +    if (auto agg_keys = provider.GetAllMuSig2ParticipantPubkeys(); !agg_keys.empty()) {
    +        sigdata.musig2_pubkeys.insert(agg_keys.begin(), agg_keys.end());
    +    }
    

    </detail>

  8. vicjuma commented at 10:52 PM on September 25, 2026: contributor

    ACK bca240a0e0a5b229ffab71760511cb746046212f

  9. Zeegaths commented at 6:21 PM on September 27, 2026: none

    I ran the RPCs on both versions, and this is what I got:

    on master: All leaf hashes are populated <img width="1167" height="696" alt="Screenshot from 2026-09-27 20-53-45" src="https://github.com/user-attachments/assets/bb7a2d70-0ca2-4b14-9611-d0dca0e92af4" />

    on PR: The leaf hashes are empty on 2/3 keys <img width="1172" height="649" alt="image" src="https://github.com/user-attachments/assets/8ea618cf-b4aa-4f98-9410-e2b5de1aafdd" />

    The fourth entry is the taproot aggregate, which is always empty

  10. in test/functional/rpc_psbt.py:344 in bca240a0e0
     339 | +                for scope in ("inputs", "outputs"):
     340 | +                    origins = {
     341 | +                        entry["pubkey"]: entry
     342 | +                        for entry in decoded[scope][0]["taproot_bip32_derivs"]
     343 | +                    }
     344 | +                    if i == 1:
    


    Bicaru20 commented at 7:05 PM on September 27, 2026:

    nit: I think it would make the test easy to follow if the condition check directly the descriptor. Sth like this:

    --- a/test/functional/rpc_psbt.py
    +++ b/test/functional/rpc_psbt.py
    @@ -315,12 +315,10 @@ class PSBTTest(BitcoinTestFramework):
             musig_descriptor = descsum_create(
                 f"tr(musig([11111111]{part_a},[22222222]{part_b}),pk([33333333]{leaf_key}))"
             )
    +        not_musig_descriptor = descsum_create(f"tr([33333333]{leaf_key},pk([33333333]{leaf_key}))")
             # Also retain known origins when the aggregate matches neither the
             # internal key nor the script key of the output being updated.
    -        for i, descriptor in enumerate([
    -            musig_descriptor,
    -            descsum_create(f"tr([33333333]{leaf_key},pk([33333333]{leaf_key}))"),
    -        ]):
    +        for descriptor in [musig_descriptor, not_musig_descriptor]:
                 descriptors = [musig_descriptor, descriptor]
                 address = node.deriveaddresses(descriptor)[0]
                 script = bytes.fromhex(node.validateaddress(address)["scriptPubKey"])
    @@ -341,13 +339,13 @@ class PSBTTest(BitcoinTestFramework):
                             entry["pubkey"]: entry
                             for entry in decoded[scope][0]["taproot_bip32_derivs"]
                         }
    -                    if i == 1:
    +                    if descriptor == not_musig_descriptor:
                             assert "musig_participant_pubkeys" not in decoded[scope][0]
     
                         # Only the script key is involved in the leaf (BIP371/373).
                         assert_equal(len(origins[leaf_key[2:]]["leaf_hashes"]), 1)
     
    -                    if i == 0:
    +                    if descriptor == musig_descriptor:
                             for participant, fingerprint in ((part_a, "11111111"), (part_b, "22222222")):
                                 assert_equal(origins[participant[2:]]["leaf_hashes"], [])
                                 assert_equal(origins[participant[2:]]["master_fingerprint"], fingerprint)
    
  11. Bicaru20 commented at 7:05 PM on September 27, 2026: contributor

    Concept ACK bca240a0e0a5b229ffab71760511cb746046212f. I agree with @vicjuma that in sign.cpp the code could be optimize to avoid repited calls pn GetAllMuSig2ParticipantPubkeys.

    • 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.

    • 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.

    • bca240a0e0a5b229ffab71760511cb746046212f: Test to check that musig2 fields are not accidentally being added


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-09-28 10:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me