wallet: Be able to receive and spend inputs involving MuSig2 aggregate keys #29675

pull achow101 wants to merge 17 commits into bitcoin:master from achow101:musig2 changing 18 files +976 −47
  1. achow101 commented at 10:32 pm on March 18, 2024: member

    This PR implements MuSig2 signing so that the wallet can receive and spend from imported musig(0 descriptors.

    The libsecp musig module is enabled so that it can be used for all of the MuSig2 cryptography.

    Secnonces are handled in a separate class which holds the libsecp secnonce object in a secure_unique_ptr. Since secnonces must not be used, this class has no serialization and will only live in memory. A restart of the software will require a restart of the MuSig2 signing process.

  2. DrahtBot commented at 10:32 pm on March 18, 2024: contributor

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

    Code Coverage & Benchmarks

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

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK rkrux, theStack, fjahr
    Concept ACK jonatack

    If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #32876 (refactor: use options struct for signing and PSBT operations by Sjors)
    • #21283 (Implement BIP 370 PSBTv2 by achow101)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    LLM Linter (✨ experimental)

    Possible typos and grammar issues:

    • It is the SHA256 of aggregate xonly key, + participant pubkey + sighash. -> It is the SHA256 of the aggregate xonly key + participant pubkey + sighash. [The stray comma before “+” makes the phrase awkward/confusing; adding “the” and removing the comma clarifies the list of items being hashed.]

    drahtbot_id_5_m

  3. DrahtBot added the label Wallet on Mar 18, 2024
  4. DrahtBot added the label CI failed on Mar 19, 2024
  5. DrahtBot commented at 2:45 am on March 19, 2024: contributor

    🚧 At least one of the CI tasks failed. Make sure to run all tests locally, according to the documentation.

    Possibly this is due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    Leave a comment here, if you need help tracking down a confusing failure.

    Debug: https://github.com/bitcoin/bitcoin/runs/22808312237

  6. in test/functional/wallet_musig.py:158 in a1e4c323db outdated
    144+        for deriv_path in dec_psbt["inputs"][0]["taproot_bip32_derivs"]:
    145+            if deriv_path["pubkey"] in part_pks:
    146+                part_pks.remove(deriv_path["pubkey"])
    147+        assert_equal(len(part_pks), 0)
    148+
    149+        nonce_psbts = []
    


    Sjors commented at 10:49 am on March 19, 2024:
    a1e4c323dbff9fa5095cf216d7cd528f10a1feeb: I assume this where the first nonce collection round starts, maybe say so in a comment?
  7. in test/functional/wallet_musig.py:184 in a1e4c323db outdated
    155+        comb_nonce_psbt = self.nodes[0].combinepsbt(nonce_psbts)
    156+
    157+        dec_psbt = self.nodes[0].decodepsbt(comb_nonce_psbt)
    158+        assert_equal(len(dec_psbt["inputs"][0]["musig2_pubnonces"]), exp_key_leaf)
    159+
    160+        psig_psbts = []
    


    Sjors commented at 10:51 am on March 19, 2024:
    a1e4c323dbff9fa5095cf216d7cd528f10a1feeb: and that this is where round 2 happens (maybe link to the BIP at the top of the test and briefly summarise the steps)
  8. in test/functional/wallet_musig.py:192 in a1e4c323db outdated
    161+        for wallet in wallets:
    162+            proc = wallet.walletprocesspsbt(comb_nonce_psbt)
    163+            assert_equal(proc["complete"], False)
    164+            psig_psbts.append(proc["psbt"])
    165+
    166+        comb_psig_psbt = self.nodes[0].combinepsbt(psig_psbts)
    


    Sjors commented at 10:54 am on March 19, 2024:
    a1e4c323dbff9fa5095cf216d7cd528f10a1feeb: because all wallets live on the same node, it’s useful to point out here that anyone, including non-participants can combine the partial signatures. Which is why the non-wallet combinepsbt and finalizepsbt RPC’s are used.
  9. Sjors commented at 2:22 pm on March 19, 2024: member

    Very cool stuff! Will review more later.

    This pulls in (an older version of) the musig module in libsecp

    What do you mean by “older”? Just that the PR to libsecp needs another rebase?

    An open question is whether the approach for handling the secnonces is ideal and safe. Since nonces must not be reused, this PR holds them exclusively in memory, so a restart of the software will require a restart of the MuSig2 signing process.

    It sounds safe, but not ideal, which might make it unsafe. Every Bitcoin Core instance involved would need to keep running, with the wallet loaded (and decrypted?) throughout the two rounds. For an airgapped setup with keys in multiple locations, the node in each location would have to be left running unattended (assuming one person running between them).

    My understanding is that Ledger (cc @bigspider) creates a nonce, stores it, and then deletes it from storage as soon as it’s loaded (before signing). We could similarly store the nonce in our wallet and then delete the field at the start of the new round. For safety we could disable backups and dump RPC’s while a round is in progress (e.g. with a NO_BACKUP flag).

    That only prevents accidental replay, not a replay attack, but it seems that anyone who is able to replay a node, already has access to its private keys (from the time a wallet was decrypted), so can’t do additional harm?


    Implementation questions.

    I tried making a 2 party tr(musig(A,B)) in a blank wallet. Initially I obtained two private keys and their public keys from another legacy wallet. I gave the new Alice wallet her private key and Bob’s public key, i.e. tr(musig(a,B)/0/*) but this failed with Ranged musig() requires all participants to be xpubs. Why though? Given that bip-musig2-derivation defines a virtual root xpub, and providers a fake chaincode, this restriction seems unneeded? (though it’s not blocker either, with descriptor wallets it’s easy to get an xpub - after #29130 anyway)

    Once I had two wallets, I could see they generated the same receive address, nice! I then imported the same xpub/xpriv pair for the change address 1/*. I sent some (signet) coins to it, which arrived and confirmed.

    Sadly after the GUI rugged me :-) Trying to send any amount elsewhere resulted in “Signing transaction failed” followed by “Transaction creation failed!”. Whereas I was hoping to get a PSBT this way.

    Using the send RPC I do get a PSBT (from Alice). I had the musig2_participant_pubkeys set, but no musig2_pubnonces. That required calling walletprocesspsbt which seems an unnecessary extra step (but such fine tuning can wait). On Bob’s side the GUI complained with “Could not sign any more inputs”, but it did add a nonce.

    At this point all the nonces were commited, so Bob could have added his partial signature. But at the stage the GUI crashes when trying to sign: [libsecp256k1] illegal argument: secp256k1_memcmp_var(&nonce->data[0], secp256k1_musig_pubnonce_magic, 4) == 0.

    After a restart Bob’s walletprocesspsbt command didn’t fail. Which seems wrong: at this point the nonce should be gone, which he should complain about.

    Starting with a fresh transaction, sing only the RPC I got the same crash, i.e.:

    1. Alice: send
    2. Alice: processpsbt
    3. Bob: processpsbt
    4. Bob: processpsbt: crash

    Perhaps relevant: Bob’s wallet is encrypted, though it was unlocked throughout steps 3 and 4.


    0 % test/functional/wallet_musig.py 
    12024-03-19T14:23:33.113000Z TestFramework (INFO): PRNG seed is: 6470719924404054174
    22024-03-19T14:23:33.115000Z TestFramework (INFO): Initializing test directory /var/folders/h6/qrb4j9vn6530kp7j4ymj934h0000gn/T/bitcoin_func_test_66knao3l
    32024-03-19T14:23:35.070000Z TestFramework (INFO): Testing rawtr(musig(keys/*))
    42024-03-19T14:23:35.192000Z TestFramework (ERROR): Unexpected exception caught during testing
    

    (didn’t check if it’s the same crash)

  10. bigspider commented at 2:36 pm on March 19, 2024: contributor

    My understanding is that Ledger (cc @bigspider) creates a nonce, stores it, and then deletes it from storage as soon as it’s loaded (before signing). We could similarly store the nonce in our wallet and then delete the field at the start of the new round. For safety we could disable backups and dump RPC’s while a round is in progress (e.g. with a NO_BACKUP flag).

    Not yet implemented, but that’s the plan: store nonces in flash memory (persistent memory) after generation; remove them from flash memory before signing starts (therefore, they’re gone even if there is a later failure, and signing must restart from nonce generation).

    Note that there is no backup possibility for the persistent memory.

  11. achow101 commented at 3:52 pm on March 19, 2024: member

    What do you mean by “older”? Just that the PR to libsecp needs another rebase?

    I pulled in a commit that is probably outdated at this point. There may have been API changes since.

    We could similarly store the nonce in our wallet and then delete the field at the start of the new round. For safety we could disable backups and dump RPC’s while a round is in progress (e.g. with a NO_BACKUP flag).

    Disabling backups with a flag would not help as an oft suggested method for backing up a wallet is by copying the wallet file. There’s nothing that we can do about that, so to be safe, I don’t think we can store the nonces in the wallet file.

    I tried making a 2 party tr(musig(A,B)) in a blank wallet. Initially I obtained two private keys and their public keys from another legacy wallet. I gave the new Alice wallet her private key and Bob’s public key, i.e. tr(musig(a,B)/0/*) but this failed with Ranged musig() requires all participants to be xpubs. Why though? Given that bip-musig2-derivation defines a virtual root xpub, and providers a fake chaincode, this restriction seems unneeded? (though it’s not blocker either, with descriptor wallets it’s easy to get an xpub - after #29130 anyway)

    It’s specified in bip-musig2-descriptors that the musig must only contain xpubs if the aggregate will be derived from. I believe the rationale for this is that xpubs are intended to have derivation done on them whereas normal keys are not, and so there may be particular handling of such keys to deal with possibilities of derivation doing something unexpected, and so if we do anything with derivation, we should only use keys that are intended for derivation to avoid any confusion. I think @sipa was the one who made this suggestion.

    Sadly after the GUI rugged me :-) Trying to send any amount elsewhere resulted in “Signing transaction failed” followed by “Transaction creation failed!”. Whereas I was hoping to get a PSBT this way.

    The GUI may be expecting that at least one signature is produced, but we can’t do that with musig without at least one round with the cosigners. I have it implemented such that ProduceSignature does not report the tx as being signed until there is actually a signature, so even the partial sigs generation will not return “signed”.

    After a restart Bob’s walletprocesspsbt command didn’t fail. Which seems wrong: at this point the nonce should be gone, which he should complain about.

    Currently it just ignores if there is already a nonce for a participant’s key. It doesn’t replace the nonce, but it also doesn’t validate whether that key belongs to the wallet or whether the nonce exists in the wallet.

    At this point all the nonces were commited, so Bob could have added his partial signature. But at the stage the GUI crashes when trying to sign: [libsecp256k1] illegal argument: secp256k1_memcmp_var(&nonce->data[0], secp256k1_musig_pubnonce_magic, 4) == 0. … Starting with a fresh transaction, sing only the RPC I got the same crash, i.e.:

    1. Alice: `send`
    
    2. Alice: `processpsbt`
    
    3. Bob: `processpsbt`
    
    4. Bob: `processpsbt`: crash
    

    Perhaps relevant: Bob’s wallet is encrypted, though it was unlocked throughout steps 3 and 4.

    0 % test/functional/wallet_musig.py 
    12024-03-19T14:23:33.113000Z TestFramework (INFO): PRNG seed is: 6470719924404054174
    22024-03-19T14:23:33.115000Z TestFramework (INFO): Initializing test directory /var/folders/h6/qrb4j9vn6530kp7j4ymj934h0000gn/T/bitcoin_func_test_66knao3l
    32024-03-19T14:23:35.070000Z TestFramework (INFO): Testing rawtr(musig(keys/*))
    42024-03-19T14:23:35.192000Z TestFramework (ERROR): Unexpected exception caught during testing
    

    (didn’t check if it’s the same crash)

    Huh, works fine for me.

  12. Sjors commented at 4:37 pm on March 19, 2024: member

    Huh, works fine for me.

    This was on Intel macOS 14.4 with a clean checkout and ./configure --disable-bench --disable-tests --enable-wallet --disable-fuzz-binary --disable-zmq --with-gui.

    On Ubuntu 23.10 with gcc 13.2.0 the test do pass, odd.

    (if this still happens after CI passes, I’ll dig a bit deeper, for now I’ll just test on Ubuntu)

    I don’t think we can store the nonces in the wallet file.

    Storing them in some other file might be fine too. As long as we delete it upon read, don’t sign anything if deletion fails and maybe also commit to some unique property of the PSBT.

    Currently it just ignores if there is already a nonce for a participant’s key.

    I guess we need to distinguish here between a nonce for our own key and one for other participants. We have no idea if some other node crashed. But it does seem reasonable to fail if we see a nonce for ourselves. Whether we previously crashed or if someone is trying a replay attack doesn’t really matter. Though it’s unusual for processpsbt to fail when called twice normally, here it seems justifiable.


    Update: successfully completed the MuSig2 signing on Ubuntu!

  13. achow101 force-pushed on Mar 19, 2024
  14. achow101 force-pushed on Mar 20, 2024
  15. achow101 force-pushed on Mar 25, 2024
  16. achow101 force-pushed on Mar 25, 2024
  17. achow101 force-pushed on Mar 25, 2024
  18. achow101 force-pushed on Mar 26, 2024
  19. DrahtBot added the label Needs rebase on Mar 29, 2024
  20. achow101 force-pushed on Apr 1, 2024
  21. DrahtBot removed the label Needs rebase on Apr 1, 2024
  22. Sjors commented at 10:36 am on April 2, 2024: member
    Only 3 red CI machines to go :-)
  23. achow101 force-pushed on Apr 2, 2024
  24. achow101 commented at 5:10 pm on April 2, 2024: member

    Only 3 red CI machines to go :-)

    Only the tidy job is an actual failure from this PR. MSan is an issue with libsecp that needs to be fixed in https://github.com/bitcoin-core/secp256k1/pull/1479. The ASan failure affects all PRs currently, see #29788

  25. Sjors commented at 8:05 am on April 3, 2024: member
    The test passes for me now on macOS.
  26. DrahtBot added the label Needs rebase on Apr 6, 2024
  27. achow101 force-pushed on Apr 16, 2024
  28. bitcoin deleted a comment on May 23, 2024
  29. bigspider commented at 11:43 am on June 6, 2024: contributor

    Hi all, an early alpha of the Ledger Bitcoin Testnet app with MuSig2 support is available for testing. (NB: the app is called Bitcoin Test Musig and not Bitcoin Test). It should be compatible with the latest draft of the specs.

    Instructions and an easy end-2-end script for MuSig signing to play with it is available here for anyone interested in trying it out:

    https://github.com/bigspider/moosig

    It works for both keypath and script path spending (but it was only tested on very simple policies, so far).

    ADDENDUM: MuSig2 support will not be available on Nano S. Sorry, not enough RAM to make it fit.

  30. achow101 force-pushed on Oct 12, 2024
  31. achow101 commented at 0:18 am on October 12, 2024: member

    Rebased and updated the libsecp subtree to its master

    Still need to work on the location of musig specific functions as currently it requires linking secp256k1 directly for a bunch of targets.

  32. DrahtBot removed the label Needs rebase on Oct 12, 2024
  33. achow101 force-pushed on Oct 24, 2024
  34. achow101 force-pushed on Oct 25, 2024
  35. achow101 force-pushed on Oct 25, 2024
  36. achow101 force-pushed on Oct 25, 2024
  37. achow101 force-pushed on Oct 25, 2024
  38. achow101 force-pushed on Oct 29, 2024
  39. DrahtBot removed the label CI failed on Oct 29, 2024
  40. achow101 force-pushed on Nov 1, 2024
  41. DrahtBot added the label CI failed on Nov 1, 2024
  42. achow101 force-pushed on Nov 4, 2024
  43. DrahtBot removed the label CI failed on Nov 4, 2024
  44. in src/psbt.h:55 in 6d8213dda3 outdated
    49@@ -50,6 +50,9 @@ static constexpr uint8_t PSBT_IN_TAP_LEAF_SCRIPT = 0x15;
    50 static constexpr uint8_t PSBT_IN_TAP_BIP32_DERIVATION = 0x16;
    51 static constexpr uint8_t PSBT_IN_TAP_INTERNAL_KEY = 0x17;
    52 static constexpr uint8_t PSBT_IN_TAP_MERKLE_ROOT = 0x18;
    53+static constexpr uint8_t PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS = 0x19;
    54+static constexpr uint8_t PSBT_IN_MUSIG2_PUB_NONCE = 0x1a;
    55+static constexpr uint8_t PSBT_IN_MUSIG2_PARTIAL_SIG = 0x1b;
    


    bigspider commented at 6:09 pm on November 5, 2024:
    These three constants are 0x1a , 0x1b, 0x1c in the final version of BIP-373.

    achow101 commented at 8:11 pm on November 5, 2024:
    Good catch! Fixed.
  45. achow101 force-pushed on Nov 5, 2024
  46. fanquake referenced this in commit 80cb630bd9 on Nov 6, 2024
  47. bigspider commented at 11:37 am on November 6, 2024: contributor

    EDIT: this is now resolved.

    The current implementation seems to be using the aggregate pubkey (before the tweaks) inside the key of the PSBT_IN_MUSIG2_PUB_NONCE (and I’d assume PSBT_IN_MUSIG2_PARTIAL_SIGNATURE, but I didn’t reach there, yet); instead, BIP-373 says that it must be the key found in the script and not the aggregate public key that it was derived from, if it was derived from an aggregate key. Therefore, I interpreted it as the taproot pubkey for a keypath spend, and the exact key that appears in the tapleaf for a scriptspend.

    Using the aggregate key pre-tweaks could be problematic if the same aggregate key appears multiple times, for example in something like: tr(NUMS/<0;1>/*,or_d(pk(musig(A,B)/<0;1>/*),pk(musig(A,B)/<2;3>/*))) Here, the two musigs have the same participants, and same aggregate key pre-tweaks (and they are in the same leaf, so even the tapleaf_hash won’t help); only the tweaks allow the disambiguation.


    Here’s what I pulled from the test I’m working on:

    Descriptor: tr(musig([f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT,tpubD6NzVbkrYhZ4YAPXpMw61GrdqXJJEiYhHo6wxVkfwZgUged5qXm6Df4NLf8ZTFXxW1UhxDKGeKdAVxZtmodC8KfR7SqmW6LGQfDGfnFLmQ6)/<0;1>/*)

    (where core has the private key for the second tpub).

    Descriptors (with tprivs) imported in core:

    0[{'desc': "tr(musig([f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT,tprv8ZgxMBicQKsPehMjviGVbsCXGVnN5PMniVWAfyiNXHt5rANKD8wW3ASWAVnoD3FPLGH3v7RGJ4FffNHhZQbUGN1cRDKQ1CosyX2MQtFsGht)/0/*)#j27a8m7j", 'active': True, 'internal': False, 'timestamp': 'now'}, {'desc': "tr(musig([f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT,tprv8ZgxMBicQKsPehMjviGVbsCXGVnN5PMniVWAfyiNXHt5rANKD8wW3ASWAVnoD3FPLGH3v7RGJ4FffNHhZQbUGN1cRDKQ1CosyX2MQtFsGht)/1/*)#r7mu6ww2", 'active': True, 'internal': True, 'timestamp': 'now'}]
    

    Unsigned psbt:

    0cHNidP8BAH0CAAAAAdL2NNlBE2JSvqSfT7rD55qtlQo0vpBj+7qay5jlRK8aAQAAAAD9////AkBCDwAAAAAAFgAU2bY05Ye8D/o31ppWq8ycsL5td1WxU4kAAAAAACJRIG8BY+C8txooPxDbrDXmFNCHPn5OWgStjAvWJPmkG/CSAAAAAAABASuAlpgAAAAAACJRIMlJh9KeiTnXKN2Ieci1qmwhtFxhowdR45TK++8OWX8UIRaZg4sXm+qcswzNRehImrkH8m8OkH3oq9iqbJO4E9721AUAG4RQpCEW29VNEFgkvwKv+7zMiMVCtBJrM43iZkRMWEATVlTy31YNACbnYhIAAAAAAwAAACEW6Ex/S3Zi+u2fXrLYEtm3vPDgvyozsXrEXji4RZZpwyERAPWswv0sAACAAQAAgAAAAIABFyDb1U0QWCS/Aq/7vMyIxUK0EmszjeJmRExYQBNWVPLfViIaA0gHk0hckD8RuKzi2BqJ9MDsOxlEr5i2BIQm01cD+YmKQgOZg4sXm+qcswzNRehImrkH8m8OkH3oq9iqbJO4E9721APoTH9LdmL67Z9estgS2be88OC/KjOxesReOLhFlmnDIQAAAQUg9zq5DQINlksdoy1F6n8H4aGsIgVYVEHwMprZjOBWEKUhB5mDixeb6pyzDM1F6EiauQfybw6Qfeir2Kpsk7gT3vbUBQAbhFCkIQfoTH9LdmL67Z9estgS2be88OC/KjOxesReOLhFlmnDIREA9azC/SwAAIABAACAAAAAgCEH9zq5DQINlksdoy1F6n8H4aGsIgVYVEHwMprZjOBWEKUNACbnYhIBAAAAAAAAACIIA0gHk0hckD8RuKzi2BqJ9MDsOxlEr5i2BIQm01cD+YmKQgOZg4sXm+qcswzNRehImrkH8m8OkH3oq9iqbJO4E9721APoTH9LdmL67Z9estgS2be88OC/KjOxesReOLhFlmnDIQA=
    

    PSBT processed by core:

    0cHNidP8BAH0CAAAAAdL2NNlBE2JSvqSfT7rD55qtlQo0vpBj+7qay5jlRK8aAQAAAAD9////AkBCDwAAAAAAFgAU2bY05Ye8D/o31ppWq8ycsL5td1WxU4kAAAAAACJRIG8BY+C8txooPxDbrDXmFNCHPn5OWgStjAvWJPmkG/CSAAAAAAABASuAlpgAAAAAACJRIMlJh9KeiTnXKN2Ieci1qmwhtFxhowdR45TK++8OWX8UIRaZg4sXm+qcswzNRehImrkH8m8OkH3oq9iqbJO4E9721AUAG4RQpCEW29VNEFgkvwKv+7zMiMVCtBJrM43iZkRMWEATVlTy31YNACbnYhIAAAAAAwAAACEW6Ex/S3Zi+u2fXrLYEtm3vPDgvyozsXrEXji4RZZpwyERAPWswv0sAACAAQAAgAAAAIABFyDb1U0QWCS/Aq/7vMyIxUK0EmszjeJmRExYQBNWVPLfViIaA0gHk0hckD8RuKzi2BqJ9MDsOxlEr5i2BIQm01cD+YmKQgOZg4sXm+qcswzNRehImrkH8m8OkH3oq9iqbJO4E9721APoTH9LdmL67Z9estgS2be88OC/KjOxesReOLhFlmnDIUMbA5mDixeb6pyzDM1F6EiauQfybw6Qfeir2Kpsk7gT3vbUA0gHk0hckD8RuKzi2BqJ9MDsOxlEr5i2BIQm01cD+YmKQgPQuidM6rVFptyqaKqAOFl7PD7UfSkbp1rMhATpFUiEXwN8MICPo5paODBnrSm6350HF7EjM5LWgYTPXWGmFhFW1QAAAQUg9zq5DQINlksdoy1F6n8H4aGsIgVYVEHwMprZjOBWEKUhB5mDixeb6pyzDM1F6EiauQfybw6Qfeir2Kpsk7gT3vbUBQAbhFCkIQfoTH9LdmL67Z9estgS2be88OC/KjOxesReOLhFlmnDIREA9azC/SwAAIABAACAAAAAgCEH9zq5DQINlksdoy1F6n8H4aGsIgVYVEHwMprZjOBWEKUNACbnYhIBAAAAAAAAACIIA0gHk0hckD8RuKzi2BqJ9MDsOxlEr5i2BIQm01cD+YmKQgOZg4sXm+qcswzNRehImrkH8m8OkH3oq9iqbJO4E9721APoTH9LdmL67Z9estgS2be88OC/KjOxesReOLhFlmnDIQA=
    

    PSBT processed by core, decoded:

      0{
      1  "tx": {
      2    "txid": "71ac326e339863accc5ccb85d071ac41a7162fc1406c322c23648c0fe5839b92",
      3    "hash": "71ac326e339863accc5ccb85d071ac41a7162fc1406c322c23648c0fe5839b92",
      4    "version": 2,
      5    "size": 125,
      6    "vsize": 125,
      7    "weight": 500,
      8    "locktime": 0,
      9    "vin": [
     10      {
     11        "txid": "1aaf44e598cb9abafb6390be340a95ad9ae7c3ba4f9fa4be52621341d934f6d2",
     12        "vout": 1,
     13        "scriptSig": {
     14          "asm": "",
     15          "hex": ""
     16        },
     17        "sequence": 4294967293
     18      }
     19    ],
     20    "vout": [
     21      {
     22        "value": 0.01000000,
     23        "n": 0,
     24        "scriptPubKey": {
     25          "asm": "0 d9b634e587bc0ffa37d69a56abcc9cb0be6d7755",
     26          "desc": "addr(bcrt1qmxmrfev8hs8l5d7knft2hnyukzlx6a64ystah7)#4sr0u9wg",
     27          "hex": "0014d9b634e587bc0ffa37d69a56abcc9cb0be6d7755",
     28          "address": "bcrt1qmxmrfev8hs8l5d7knft2hnyukzlx6a64ystah7",
     29          "type": "witness_v0_keyhash"
     30        }
     31      },
     32      {
     33        "value": 0.08999857,
     34        "n": 1,
     35        "scriptPubKey": {
     36          "asm": "1 6f0163e0bcb71a283f10dbac35e614d0873e7e4e5a04ad8c0bd624f9a41bf092",
     37          "desc": "rawtr(6f0163e0bcb71a283f10dbac35e614d0873e7e4e5a04ad8c0bd624f9a41bf092)#f0mm5uaz",
     38          "hex": "51206f0163e0bcb71a283f10dbac35e614d0873e7e4e5a04ad8c0bd624f9a41bf092",
     39          "address": "bcrt1pduqk8c9ukudzs0csmwkrtes56zrnuljwtgz2mrqt6cj0nfqm7zfqgd563z",
     40          "type": "witness_v1_taproot"
     41        }
     42      }
     43    ]
     44  },
     45  "global_xpubs": [
     46  ],
     47  "psbt_version": 0,
     48  "proprietary": [
     49  ],
     50  "unknown": {
     51  },
     52  "inputs": [
     53    {
     54      "witness_utxo": {
     55        "amount": 0.10000000,
     56        "scriptPubKey": {
     57          "asm": "1 c94987d29e8939d728dd8879c8b5aa6c21b45c61a30751e394cafbef0e597f14",
     58          "desc": "rawtr(c94987d29e8939d728dd8879c8b5aa6c21b45c61a30751e394cafbef0e597f14)#qt9mflrv",
     59          "hex": "5120c94987d29e8939d728dd8879c8b5aa6c21b45c61a30751e394cafbef0e597f14",
     60          "address": "bcrt1pe9yc05573yuaw2xa3puu3dd2dssmghrp5vr4rcu5eta77rje0u2qamzrjq",
     61          "type": "witness_v1_taproot"
     62        }
     63      },
     64      "taproot_bip32_derivs": [
     65        {
     66          "pubkey": "99838b179bea9cb30ccd45e8489ab907f26f0e907de8abd8aa6c93b813def6d4",
     67          "master_fingerprint": "1b8450a4",
     68          "path": "m",
     69          "leaf_hashes": [
     70          ]
     71        },
     72        {
     73          "pubkey": "dbd54d105824bf02affbbccc88c542b4126b338de266444c5840135654f2df56",
     74          "master_fingerprint": "26e76212",
     75          "path": "m/0/3",
     76          "leaf_hashes": [
     77          ]
     78        },
     79        {
     80          "pubkey": "e84c7f4b7662faed9f5eb2d812d9b7bcf0e0bf2a33b17ac45e38b8459669c321",
     81          "master_fingerprint": "f5acc2fd",
     82          "path": "m/44h/1h/0h",
     83          "leaf_hashes": [
     84          ]
     85        }
     86      ],
     87      "taproot_internal_key": "dbd54d105824bf02affbbccc88c542b4126b338de266444c5840135654f2df56",
     88      "musig2_participant_pubkeys": [
     89        {
     90          "aggregate_pubkey": "03480793485c903f11b8ace2d81a89f4c0ec3b1944af98b6048426d35703f9898a",
     91          "participant_pubkeys": [
     92            "0399838b179bea9cb30ccd45e8489ab907f26f0e907de8abd8aa6c93b813def6d4",
     93            "03e84c7f4b7662faed9f5eb2d812d9b7bcf0e0bf2a33b17ac45e38b8459669c321"
     94          ]
     95        }
     96      ],
     97      "musig2_pubnonces": [
     98        {
     99          "participant_pubkey": "0399838b179bea9cb30ccd45e8489ab907f26f0e907de8abd8aa6c93b813def6d4",
    100          "aggregate_pubkey": "03480793485c903f11b8ace2d81a89f4c0ec3b1944af98b6048426d35703f9898a",
    101          "pubnonce": "03d0ba274ceab545a6dcaa68aa8038597b3c3ed47d291ba75acc8404e91548845f037c30808fa39a5a383067ad29badf9d0717b1233392d68184cf5d61a6161156d5"
    102        }
    103      ]
    104    }
    105  ],
    106  "outputs": [
    107    {
    108    },
    109    {
    110      "taproot_internal_key": "f73ab90d020d964b1da32d45ea7f07e1a1ac2205585441f0329ad98ce05610a5",
    111      "taproot_bip32_derivs": [
    112        {
    113          "pubkey": "99838b179bea9cb30ccd45e8489ab907f26f0e907de8abd8aa6c93b813def6d4",
    114          "master_fingerprint": "1b8450a4",
    115          "path": "m",
    116          "leaf_hashes": [
    117          ]
    118        },
    119        {
    120          "pubkey": "e84c7f4b7662faed9f5eb2d812d9b7bcf0e0bf2a33b17ac45e38b8459669c321",
    121          "master_fingerprint": "f5acc2fd",
    122          "path": "m/44h/1h/0h",
    123          "leaf_hashes": [
    124          ]
    125        },
    126        {
    127          "pubkey": "f73ab90d020d964b1da32d45ea7f07e1a1ac2205585441f0329ad98ce05610a5",
    128          "master_fingerprint": "26e76212",
    129          "path": "m/1/0",
    130          "leaf_hashes": [
    131          ]
    132        }
    133      ],
    134      "musig2_participant_pubkeys": [
    135        {
    136          "aggregate_pubkey": "03480793485c903f11b8ace2d81a89f4c0ec3b1944af98b6048426d35703f9898a",
    137          "participant_pubkeys": [
    138            "0399838b179bea9cb30ccd45e8489ab907f26f0e907de8abd8aa6c93b813def6d4",
    139            "03e84c7f4b7662faed9f5eb2d812d9b7bcf0e0bf2a33b17ac45e38b8459669c321"
    140          ]
    141        }
    142      ]
    143    }
    144  ],
    145  "fee": 0.00000143
    146}
    

    The aggregate_pubkey added by core is 03480793485c903f11b8ace2d81a89f4c0ec3b1944af98b6048426d35703f9898a, but I think it should be 02c94987d29e8939d728dd8879c8b5aa6c21b45c61a30751e394cafbef0e597f14 (after the BIP32-tweaks + the taptweak), matching the pubkey in the Script, if my understanding of BIP-373 is correct.

    As a consequence, the musig2_pubnonces should probably use the name aggregate_pubkey_tweaked or something else that avoids confusion with the untweaked aggregate_pubkey that appears in musig2_participant_pubkeys.

  48. achow101 force-pushed on Nov 6, 2024
  49. achow101 commented at 6:01 pm on November 6, 2024: member

    The current implementation seems to be using the aggregate pubkey (before the tweaks) inside the key of the PSBT_IN_MUSIG2_PUB_NONCE

    Indeed, fixed.

    Therefore, I interpreted it as the taproot pubkey for a keypath spend

    I’ve interpreted (and implemented) it as also allowing the taproot internal key, not just the output key. I think that is actually what I meant when writing the BIP, but it’s been a while.

  50. achow101 marked this as ready for review on Nov 6, 2024
  51. achow101 force-pushed on Nov 6, 2024
  52. achow101 force-pushed on Nov 6, 2024
  53. DrahtBot added the label CI failed on Nov 6, 2024
  54. DrahtBot commented at 8:09 pm on November 6, 2024: contributor

    🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/32617937935

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  55. DrahtBot removed the label CI failed on Nov 6, 2024
  56. bigspider commented at 9:53 pm on November 6, 2024: contributor

    I’ve interpreted (and implemented) it as also allowing the taproot internal key, not just the output key. I think that is actually what I meant when writing the BIP, but it’s been a while.

    I think it’s important that the (aggregate) plain key used in the key of PSBT_IN_MUSIG2_PUB_NONCE/PSBT_IN_MUSIG2_PARTIAL_SIGNATURE is unambiguous and clearly specified, or implementations will essentially have to try both in order to find in the PSBT all the pubnonces/musig_partial_signatures for a certain key.

    Using the same public key that can be used to verify the final signature (therefore, the tweaked taproot pubkey for keypath spends, or the pubkey as it appears in tapleaves for script spends) seems the most natural choice to me.

    I don’t have an opinion on PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS - I’m not using it, since it’s redundant in the context of signing based on BIP-388 wallet policies.

  57. achow101 force-pushed on Nov 6, 2024
  58. achow101 commented at 10:46 pm on November 6, 2024: member

    Using the same public key that can be used to verify the final signature (therefore, the tweaked taproot pubkey for keypath spends, or the pubkey as it appears in tapleaves for script spends) seems the most natural choice to me.

    That’s a good point, I don’t feel too strongly about this, it was just a bit more annoying to figure out how. I’ve updated the PR to do that.

    Also opened https://github.com/bitcoin/bips/pull/1695 to clarify in the BIP.

  59. bigspider commented at 1:34 pm on November 7, 2024: contributor

    Looking good!

    I can confirm that I was able to complete two e2e tests on regtest (commit 09a6091711eef299de7cfbfd340a112706422c81), using Ledger’s MuSig2 implementation for a cosigner and bitcoin-core for the other one.

    The descriptors had the form:

    tr(musig(ledger_key, bitcoin_core_key)/<0;1>/*)

    and:

    tr(nums_key/<0;1>/*, musig(ledger_key, bitcoin_core_key)/<0;1>/*)

    In both cases, the Ledger device was just the MuSig2 cosigner, while bitcoin-core was both the other musig cosigner and the combiner/finalizer/extractor.

    I will report more once I have more extensive tests.

  60. bigspider commented at 4:44 pm on November 7, 2024: contributor

    Attempting some fancier setups, I’m trying to do a “decaying MuSig” that starts as a 3-of-3 in the keypath, with 3 timelocked 2-of-2 in the scriptpaths. Not managing to work with the descriptor in core:

    0$ ./bitcoin-cli -regtest getdescriptorinfo "tr(musig(tpubDD863BuWFdsaCg6f1SGdwLxp9mDcm3YRm3HxxbppBrizxvU1MqhQ1WpMwhz4vrZHNT7NFbXQ35CquVG9xaLsWaUWfSMZamDESisvtKZ7veF,tpubD6NzVbkrYhZ4YAPXpMw61GrdqXJJEiYhHo6wxVkfwZgUged5qXm6Df4NLf8ZTFXxW1UhxDKGeKdAVxZtmodC8KfR7SqmW6LGQfDGfnFLmQ6,tpubD6NzVbkrYhZ4WSLhjy9cMaGTEW7No4ALTRikqWo5xFsiTTkRfxA8eRyj2GTkFbkKU6HeW5z8LqrcgHbcVoVg2QZ8JECSHv4PpQ5vUdKJbkR)/1/*,{and_v(v:pk(musig(tpubDD863BuWFdsaCg6f1SGdwLxp9mDcm3YRm3HxxbppBrizxvU1MqhQ1WpMwhz4vrZHNT7NFbXQ35CquVG9xaLsWaUWfSMZamDESisvtKZ7veF,tpubD6NzVbkrYhZ4YAPXpMw61GrdqXJJEiYhHo6wxVkfwZgUged5qXm6Df4NLf8ZTFXxW1UhxDKGeKdAVxZtmodC8KfR7SqmW6LGQfDGfnFLmQ6)/1/*),older(12960)),{and_v(v:pk(musig(tpubDD863BuWFdsaCg6f1SGdwLxp9mDcm3YRm3HxxbppBrizxvU1MqhQ1WpMwhz4vrZHNT7NFbXQ35CquVG9xaLsWaUWfSMZamDESisvtKZ7veF,tpubD6NzVbkrYhZ4WSLhjy9cMaGTEW7No4ALTRikqWo5xFsiTTkRfxA8eRyj2GTkFbkKU6HeW5z8LqrcgHbcVoVg2QZ8JECSHv4PpQ5vUdKJbkR)/1/*),older(12960)),and_v(v:pk(musig(tpubD6NzVbkrYhZ4YAPXpMw61GrdqXJJEiYhHo6wxVkfwZgUged5qXm6Df4NLf8ZTFXxW1UhxDKGeKdAVxZtmodC8KfR7SqmW6LGQfDGfnFLmQ6,tpubD6NzVbkrYhZ4WSLhjy9cMaGTEW7No4ALTRikqWo5xFsiTTkRfxA8eRyj2GTkFbkKU6HeW5z8LqrcgHbcVoVg2QZ8JECSHv4PpQ5vUdKJbkR)/1/*),older(12960))}})"
    1error code: -5
    2error message:
    3'and_v(v:pk(musig(tpubDD863BuWFdsaCg6f1SGdwLxp9mDcm3YRm3HxxbppBrizxvU1MqhQ1WpMwhz4vrZHNT7NFbXQ35CquVG9xaLsWaUWfSMZamDESisvtKZ7veF,tpubD6NzVbkrYhZ4YAPXpMw61GrdqXJJEiYhHo6wxVkfwZgUged5qXm6Df4NLf8ZTFXxW1UhxDKGeKdAVxZtmodC8KfR7SqmW6LGQfDGfnFLmQ6)/1/*),older(12960))' is not a valid descriptor function
    

    Same descriptor formatted:

     0tr(
     1  musig(tpubDD863BuWFdsaCg6f1SGdwLxp9mDcm3YRm3HxxbppBrizxvU1MqhQ1WpMwhz4vrZHNT7NFbXQ35CquVG9xaLsWaUWfSMZamDESisvtKZ7veF,tpubD6NzVbkrYhZ4YAPXpMw61GrdqXJJEiYhHo6wxVkfwZgUged5qXm6Df4NLf8ZTFXxW1UhxDKGeKdAVxZtmodC8KfR7SqmW6LGQfDGfnFLmQ6,tpubD6NzVbkrYhZ4WSLhjy9cMaGTEW7No4ALTRikqWo5xFsiTTkRfxA8eRyj2GTkFbkKU6HeW5z8LqrcgHbcVoVg2QZ8JECSHv4PpQ5vUdKJbkR)/1/*,
     2  {
     3    and_v(
     4      v:pk(musig(tpubDD863BuWFdsaCg6f1SGdwLxp9mDcm3YRm3HxxbppBrizxvU1MqhQ1WpMwhz4vrZHNT7NFbXQ35CquVG9xaLsWaUWfSMZamDESisvtKZ7veF,tpubD6NzVbkrYhZ4YAPXpMw61GrdqXJJEiYhHo6wxVkfwZgUged5qXm6Df4NLf8ZTFXxW1UhxDKGeKdAVxZtmodC8KfR7SqmW6LGQfDGfnFLmQ6)/1/*),
     5      older(12960)
     6    ),
     7    {
     8      and_v(
     9        v:pk(musig(tpubDD863BuWFdsaCg6f1SGdwLxp9mDcm3YRm3HxxbppBrizxvU1MqhQ1WpMwhz4vrZHNT7NFbXQ35CquVG9xaLsWaUWfSMZamDESisvtKZ7veF,tpubD6NzVbkrYhZ4WSLhjy9cMaGTEW7No4ALTRikqWo5xFsiTTkRfxA8eRyj2GTkFbkKU6HeW5z8LqrcgHbcVoVg2QZ8JECSHv4PpQ5vUdKJbkR)/1/*),
    10        older(12960)
    11      ),
    12      and_v(
    13        v:pk(musig(tpubD6NzVbkrYhZ4YAPXpMw61GrdqXJJEiYhHo6wxVkfwZgUged5qXm6Df4NLf8ZTFXxW1UhxDKGeKdAVxZtmodC8KfR7SqmW6LGQfDGfnFLmQ6,tpubD6NzVbkrYhZ4WSLhjy9cMaGTEW7No4ALTRikqWo5xFsiTTkRfxA8eRyj2GTkFbkKU6HeW5z8LqrcgHbcVoVg2QZ8JECSHv4PpQ5vUdKJbkR)/1/*),
    14        older(12960)
    15      )
    16    }
    17  }
    18)
    

    Been staring at it for a while, it seems valid to me. Am I missing something? Sorry if it’s obvious

  61. achow101 commented at 4:51 pm on November 7, 2024: member
    musig() is not being parsed in Miniscript expressions yet.
  62. bigspider commented at 4:58 pm on November 7, 2024: contributor

    musig() is not being parsed in Miniscript expressions yet.

    Ah, ok, I’ll keep an eye for updates. Thanks!

  63. achow101 force-pushed on Nov 7, 2024
  64. achow101 marked this as a draft on Nov 7, 2024
  65. achow101 commented at 6:23 pm on November 7, 2024: member
    Several earlier commits have been split out into separate PRs. See the tracking issue #31246 for the breakdown.
  66. achow101 force-pushed on Nov 7, 2024
  67. achow101 force-pushed on Nov 7, 2024
  68. NicolasDorier commented at 4:12 am on November 8, 2024: contributor
    Given BIP373 doesn’t have test vectors, it would be very useful that either this PR or the BIP include some hard coded PSBT examples to ensure every implementations are on the same page.
  69. jonatack commented at 12:54 pm on November 26, 2024: member

    Concept ACK

    Given BIP373 doesn’t have test vectors, it would be very useful that either this PR or the BIP include some hard coded PSBT examples to ensure every implementations are on the same page.

    Good point (the BIP373 test vector section currently states “TBD” and seems worth completing even if the implementation here also has tests).

  70. Sjors commented at 3:24 pm on November 26, 2024: member

    It might be useful if someone expands doc/multisig-tutorial.md to add a MuSig2 section. That doesn’t have to go in this PR, but it will make testing and review easier. The functional test added in this PR can be used for inspiration.

    It can take advantage of the new <0;1> syntax and the new gethdkeys RPC.

    I was able to generate a simple 2-of-2 tr(musig(A,B)/<0;1>/*) watch-only wallet on testnet4 and receive to it. Keys A and B were extracted from regular wallets by taking the pkh() account level xpub, including its origin.

    However when trying to send walletprocesspsbt (with the wallet that has private keys) does not add any fields.

  71. Sjors commented at 9:49 am on December 3, 2024: member

    I also tried @bigspider’s MooSig demo which worked. I then crafted a multisig between A and the device: tr(musig(A,L)/<0;1>/*). I managed to register the policy (after several mistakes, it’s very tedious to do this manually).

    I wanted to try using HWI to display the address, but I would have to modify it to work with the test app. I just yolo funded it.

    I then created a withdrawal PSBT and pasted it in the Moosig script, modifying it to only add its public nonce. I also hardcoded the registered hmac. The device recognized the account being spent from. I can see that musig2_pubnonces was added to the PSBT.

    I then ran it through walletprocesspsbt in wallet A. The resulting PSBT was longer, but it didn’t add its nonce to musig2_pubnonces.

  72. Sjors commented at 10:02 am on December 3, 2024: member

    Looking at the functional tests in 3649c2eb2053a0c166c68beb310c9c64ddc5b273 it seems the way this is designed to work is by swapping out the active tr([m/86'/1'/0']xpriv/<0;1>)/* descriptor for tr(musig([m/86'/1'/0']xpriv,other,other)/<0;1>)/*.

    I guess that’s fine for the purpose of getting MuSig2 functionality in for experimental use, but it seems a bit unsafe and confusing for general use.

    The test could make the intention a bit more clear by starting with blank wallets, only adding a tr() descriptor and mentioning in a comment that we just want its keys.


    So instead of creating a watch-only wallet, I created a blank wallet. I imported the same tr(musig(A,L)/<0;1>/*) descriptor, but using the xpriv instead of xpub for A. It found the deposited coin. This time I started the withdrawal from the Core, so the Moosig could do its two calls to device in quick succession.

    Even though I had “enable PSBT controls” selected, the GUI did not give me a chance to create a PSBT and immediately complained “Signing transaction failed” after I clicked “Send”.

    The send RPC did work though it didn’t add a nonce. I had to use walletprocesspsbt for that.

    I fed the result to Moosig generate_public_nonces and generate_partial_signatures. I fed the result to walletprocesspsbt et voila! Money back.

  73. bigspider commented at 4:03 pm on December 3, 2024: contributor

    Thanks @Sjors for testing the app!

    I just updated it with a new version (Bitcoin Test Musig v2.4.0-rc):

    • Some bug fixes for more complex policies. Testing is still not extensive, but it should work for all combinations of musig and miniscript (with the due limits to the policy size; currently, at most 5 keys for MuSig2).
    • If a psbt is sent where only MuSig2 round 1 is executed (no signatures returned), the app will return pubnonces with no user interaction. Note that if there are other internal keys in the wallet policy for whom the PSBT_IN_TAP_BIP32_DERIVATION is present, then the device will sign for those, and in that case confirmation is of course still required.

    Also a quick note that I made a PR to add support for musig in BIP-388 wallet policies - comments welcome.

  74. DrahtBot added the label CI failed on Dec 17, 2024
  75. hugohn commented at 12:42 pm on December 19, 2024: contributor

    Hey guys, we’ve successfully integrated this into Nunchuk, so you should be able to test this out with actual UI/UX very soon.

    MuSig2 key path spend: https://mempool.space/tx/69c75aa798e03dbe782c9a11eed316440fa2a4cb9c4645af2f5d8d566c04207b?mode=details

    MuSig2 script path spend: https://mempool.space/tx/73f63684994477924105966e646427f7fc802352d9ba9d1baebf05b1f3dc3fab?mode=details

    Sample descriptors: tr(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/**,[7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/**),{{{pk(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/**,[07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/**)),pk(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/**,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/**))},{pk(musig([7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/**,[07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/**)),pk(musig([7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/**,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/**))}},pk(musig([07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/**,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/**))})

  76. DrahtBot removed the label CI failed on Dec 22, 2024
  77. fjahr commented at 1:00 pm on December 30, 2024: contributor
    Concept ACK
  78. starius commented at 2:18 pm on January 4, 2025: contributor

    @hugohn Great work!

    Does /** in descriptors mean a combination of /0/* and /1/*? I.e. a receive and a change descriptor.

  79. hugohn commented at 4:35 pm on January 4, 2025: contributor
    @starius Kind of. We build on top of the descriptor template defined in BIP 129 (BSMS). The above snippet is part of a larger BSMS wallet configuration file, which includes derivation path restrictions.
  80. starius commented at 10:41 pm on January 4, 2025: contributor

    @hugohn I built bitcoin core using this PR rebased on master. I tried the descriptor from your message, replacing /**/ with /0/* and /1/*. It works!

     0getdescriptorinfo "tr(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/0/*,[7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/0/*),{{{pk(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/0/*,[07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/0/*)),pk(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/0/*,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/0/*))},{pk(musig([7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/0/*,[07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/0/*)),pk(musig([7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/0/*,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/0/*))}},pk(musig([07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/0/*,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/0/*))})"
     1{
     2  "descriptor": "tr(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/0/*,[7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/0/*),{{{pk(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/0/*,[07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/0/*)),pk(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/0/*,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/0/*))},{pk(musig([7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/0/*,[07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/0/*)),pk(musig([7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/0/*,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/0/*))}},pk(musig([07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/0/*,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/0/*))})#eywenfaf",
     3  "checksum": "eywenfaf",
     4  "isrange": true,
     5  "issolvable": true,
     6  "hasprivatekeys": false
     7}
     8
     9deriveaddresses "tr(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/0/*,[7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/0/*),{{{pk(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/0/*,[07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/0/*)),pk(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/0/*,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/0/*))},{pk(musig([7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/0/*,[07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/0/*)),pk(musig([7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/0/*,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/0/*))}},pk(musig([07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/0/*,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/0/*))})#eywenfaf" 0
    10[
    11  "bc1psdmetx4vudkwte82duvxdn6np9z64njp9d9qd55ffeh5x3jey0gqmr25nu"
    12]
    13
    14
    15
    16getdescriptorinfo "tr(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/1/*,[7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/1/*),{{{pk(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/1/*,[07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/1/*)),pk(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/1/*,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/1/*))},{pk(musig([7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/1/*,[07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/1/*)),pk(musig([7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/1/*,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/1/*))}},pk(musig([07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/1/*,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/1/*))})"
    17{
    18  "descriptor": "tr(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/1/*,[7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/1/*),{{{pk(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/1/*,[07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/1/*)),pk(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/1/*,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/1/*))},{pk(musig([7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/1/*,[07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/1/*)),pk(musig([7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/1/*,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/1/*))}},pk(musig([07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/1/*,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/1/*))})#7q32hxqs",
    19  "checksum": "7q32hxqs",
    20  "isrange": true,
    21  "issolvable": true,
    22  "hasprivatekeys": false
    23}
    24
    25
    26deriveaddresses "tr(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/1/*,[7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/1/*),{{{pk(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/1/*,[07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/1/*)),pk(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/1/*,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/1/*))},{pk(musig([7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/1/*,[07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/1/*)),pk(musig([7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/1/*,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/1/*))}},pk(musig([07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/1/*,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/1/*))})#7q32hxqs" 0
    27[
    28  "bc1pfx72zmfwx34tcay26l3g4sm4unypjnqqmrnxt9hdjg5t4xy03ufshc6e8k"
    29]
    

    This is how I generated addressed bc1psdmetx4vudkwte82duvxdn6np9z64njp9d9qd55ffeh5x3jey0gqmr25nu and bc1pfx72zmfwx34tcay26l3g4sm4unypjnqqmrnxt9hdjg5t4xy03ufshc6e8k used in the transactions that you posted.

    That is a really cool stuff! Many thanks!

  81. bigspider commented at 10:50 pm on January 4, 2025: contributor

    @starius Kind of. We build on top of the descriptor template defined in BIP 129 (BSMS). The above snippet is part of a larger BSMS wallet configuration file, which includes derivation path restrictions. @hugohn: FYI BIP-388 generalizes descriptor templates to arbitrary wallets, including with miniscript and musig; it should be entirely compatible with the special cases defined in BIP-129 for multisig.

  82. starius commented at 4:28 am on January 5, 2025: contributor

    I attempted to test this on Signet with a 2-of-2 MuSig2 Taproot address (without script leaves).

    Succeeded using walletprocesspsbt, but failed when using GUI “Load PSBT from keyboard” option.

    Setup:

    • Node 1: Watch-only, connected to the network.
    • Node 2: Offline, holds the first private key.
    • Node 3: Offline, holds the second private key.

    Steps to Reproduce:

    1. Imported descriptors for each node:
     0node 2 (first private key):
     1importdescriptors '[{
     2  "desc": "tr(musig(tprv8ZgxMBicQKsPdRg438LnQj6Fpx1vR6uSwJ3Nda2cZ3oqLqQwT2eae4DcnPNWLc6n8WbXceFuUPGL2QuPCb1DRp9UBsmxbAk8BqDuUZZWWLw/86h/1h/0h/0/*,[370c1c18/86h/1h/0h]tpubDCgnZAFGxVZ5AVLcjCGkLX7sNb85itosYgy25KSPHpM7hCDCwBBn3b9tdHvD9x9DnCrGedGu7gBkRjiAaUFuZJpGUu6s3YerC5KvSCA9NqB/0/*))#l68aglxt",
     3  "active": true,
     4  "internal": false,
     5  "range": 1000,
     6  "timestamp": "now"
     7}, {
     8  "desc": "tr(musig(tprv8ZgxMBicQKsPdRg438LnQj6Fpx1vR6uSwJ3Nda2cZ3oqLqQwT2eae4DcnPNWLc6n8WbXceFuUPGL2QuPCb1DRp9UBsmxbAk8BqDuUZZWWLw/86h/1h/0h/1/*,[370c1c18/86h/1h/0h]tpubDCgnZAFGxVZ5AVLcjCGkLX7sNb85itosYgy25KSPHpM7hCDCwBBn3b9tdHvD9x9DnCrGedGu7gBkRjiAaUFuZJpGUu6s3YerC5KvSCA9NqB/1/*))#umlr3wjn",
     9  "active": true,
    10  "internal": true,
    11  "range": 1000,
    12  "timestamp": "now"
    13}]'
    14
    15node 3 (second private key):
    16importdescriptors '[{
    17  "desc": "tr(musig([a59b4dab/86h/1h/0h]tpubDCwHkWFDhGWHMRaR7U9awzmVYnD4PsmM5kn8E5qMQFptGLLddvmZnhEgqMJ4NP1Bg8UwVcRy6M1rHVCpjiAp7WT2NYBVz8fppCfot7aBtwC/0/*,tprv8ZgxMBicQKsPet5Q2PMpAdv2NeA1siXwywqxRMFvQcuYQCMf39uc9BAvBMNpNxyNqDLPcWv1NrxGsFwvhUN7FStVZj5u78j3x4wGMsNFfgf/86h/1h/0h/0/*))#lhmv3sth",
    18  "active": true,
    19  "internal": false,
    20  "range": 1000,
    21  "timestamp": "now"
    22}, {
    23  "desc": "tr(musig([a59b4dab/86h/1h/0h]tpubDCwHkWFDhGWHMRaR7U9awzmVYnD4PsmM5kn8E5qMQFptGLLddvmZnhEgqMJ4NP1Bg8UwVcRy6M1rHVCpjiAp7WT2NYBVz8fppCfot7aBtwC/1/*,tprv8ZgxMBicQKsPet5Q2PMpAdv2NeA1siXwywqxRMFvQcuYQCMf39uc9BAvBMNpNxyNqDLPcWv1NrxGsFwvhUN7FStVZj5u78j3x4wGMsNFfgf/86h/1h/0h/1/*))#rmh783wt",
    24  "active": true,
    25  "internal": true,
    26  "range": 1000,
    27  "timestamp": "now"
    28}]'
    29
    30node 1 (watch only):
    31importdescriptors '[{
    32  "desc": "tr(musig([a59b4dab/86h/1h/0h]tpubDCwHkWFDhGWHMRaR7U9awzmVYnD4PsmM5kn8E5qMQFptGLLddvmZnhEgqMJ4NP1Bg8UwVcRy6M1rHVCpjiAp7WT2NYBVz8fppCfot7aBtwC/0/*,[370c1c18/86h/1h/0h]tpubDCgnZAFGxVZ5AVLcjCGkLX7sNb85itosYgy25KSPHpM7hCDCwBBn3b9tdHvD9x9DnCrGedGu7gBkRjiAaUFuZJpGUu6s3YerC5KvSCA9NqB/0/*))#fmwctz5x",
    33  "active": true,
    34  "internal": false,
    35  "range": 1000,
    36  "timestamp": "now"
    37}, {
    38  "desc": "tr(musig([a59b4dab/86h/1h/0h]tpubDCwHkWFDhGWHMRaR7U9awzmVYnD4PsmM5kn8E5qMQFptGLLddvmZnhEgqMJ4NP1Bg8UwVcRy6M1rHVCpjiAp7WT2NYBVz8fppCfot7aBtwC/1/*,[370c1c18/86h/1h/0h]tpubDCgnZAFGxVZ5AVLcjCGkLX7sNb85itosYgy25KSPHpM7hCDCwBBn3b9tdHvD9x9DnCrGedGu7gBkRjiAaUFuZJpGUu6s3YerC5KvSCA9NqB/1/*))#qnx2rsgm",
    39  "active": true,
    40  "internal": true,
    41  "range": 1000,
    42  "timestamp": "now"
    43}]'
    
    1. Generated the same address across all nodes:
    0getnewaddress first bech32m
    1tb1pjtz0kg2xz263m6gg6tdgemratx8yucec475jrmq986ae9x8c20dqwux9lm
    
    1. I funded the address with 10k signet sats.

    2. Then I tried to spend the funds. I created an unsigned transaction on node 1 using Send GUI.

    0cHNidP8BAFMCAAAAAbCurhHBbTaWjUb3AJm5tiL+UuZEBKL57olqD4NhTes9AQAAAAD9////ARwlAAAAAAAAF6kULPYzwL1RJx2z4JbkdKLDC7WTsXWHTYADAAABASsQJwAAAAAAACJRIJLE+yFGErUd6QjS2ozsfVmOTmM4r6kh7AU+u5KY+FPaIRbIQKO0UExrrtpBwA4TAlMFYcEXqcWaXTJrp+ZvBuNbrxkANwwcGFYAAIABAACAAAAAgAAAAAAAAAAAIRbP5CTNgLooVS8JstkMC+dZTu09WKpyDXreYc7rJbloExkApZtNq1YAAIABAACAAAAAgAAAAAAAAAAAIRbdYILLif9F1ydNUxjrADeL+Zq3omPmImN4D35ckFJkBQUA7CwezwEXIN1ggsuJ/0XXJ01TGOsAN4v5mreiY+YiY3gPflyQUmQFIhoC3WCCy4n/RdcnTVMY6wA3i/mat6Jj5iJjeA9+XJBSZAVCAshAo7RQTGuu2kHADhMCUwVhwRepxZpdMmun5m8G41uvA8/kJM2AuihVLwmy2QwL51lO7T1YqnINet5hzusluWgTAAA=
    

    I copied it to node 2, loaded PSBT there, signed, then copied to node 3, loaded PSBT there, signed, then copied to node 2, loaded PSBT there, signed, then copied to node 3, loaded PSBT there, signed.

    1. Resulting PSBT:
    0cHNidP8BAFMCAAAAAbCurhHBbTaWjUb3AJm5tiL+UuZEBKL57olqD4NhTes9AQAAAAD9////ARwlAAAAAAAAF6kULPYzwL1RJx2z4JbkdKLDC7WTsXWHTYADAAABASsQJwAAAAAAACJRIJLE+yFGErUd6QjS2ozsfVmOTmM4r6kh7AU+u5KY+FPaARNAfM5w41qiYZnRTghSR1I5mkMl3Em7oqR8NJZjOofQDPV1LOcK3bs8Ibfs+W+pKyLIq2CqiC1uaQYHfC8xgQ1CWyEWyECjtFBMa67aQcAOEwJTBWHBF6nFml0ya6fmbwbjW68ZADcMHBhWAACAAQAAgAAAAIAAAAAAAAAAACEWz+QkzYC6KFUvCbLZDAvnWU7tPViqcg163mHO6yW5aBMZAKWbTatWAACAAQAAgAAAAIAAAAAAAAAAACEW3WCCy4n/RdcnTVMY6wA3i/mat6Jj5iJjeA9+XJBSZAUFAOwsHs8BFyDdYILLif9F1ydNUxjrADeL+Zq3omPmImN4D35ckFJkBSIaAt1ggsuJ/0XXJ01TGOsAN4v5mreiY+YiY3gPflyQUmQFQgLIQKO0UExrrtpBwA4TAlMFYcEXqcWaXTJrp+ZvBuNbrwPP5CTNgLooVS8JstkMC+dZTu09WKpyDXreYc7rJbloE0MbAshAo7RQTGuu2kHADhMCUwVhwRepxZpdMmun5m8G41uvApLE+yFGErUd6QjS2ozsfVmOTmM4r6kh7AU+u5KY+FPaQgNDOrym8PtabR9c5PgAgqhpNiY8OkwD7sX7qVZh8Tg/UAP0lrxYDDIFzWAkcHcUiH++b96c4MMrEpuI/ng9E0qEYkMbA8/kJM2AuihVLwmy2QwL51lO7T1YqnINet5hzusluWgTApLE+yFGErUd6QjS2ozsfVmOTmM4r6kh7AU+u5KY+FPaQgIOALVOKGFjNXOJc6BswdpoDlwdEjuB0MsUost132owmAJs9dBiyOnSBpsWZpuqBd0GROS/aqh3MBGFqQUa/LrM20McAshAo7RQTGuu2kHADhMCUwVhwRepxZpdMmun5m8G41uvApLE+yFGErUd6QjS2ozsfVmOTmM4r6kh7AU+u5KY+FPaIGsO4eRmAJAXw2HtTCZcgNtugDuJS8Xkk+2WncNVRyZAQxwDz+QkzYC6KFUvCbLZDAvnWU7tPViqcg163mHO6yW5aBMCksT7IUYStR3pCNLajOx9WY5OYzivqSHsBT67kpj4U9ogTcmLQbWqMoPInXc7RgmBw5TdMyaZ4Cq9Euidd8LGVsEAAA==
    

    Decoded version:

      0{
      1  "tx": {
      2    "txid": "e7b6cc769c03ff4c4ade13aa8d912e0a5eaa5aa5a7c73df5a297f2acc6c5aeba",
      3    "hash": "e7b6cc769c03ff4c4ade13aa8d912e0a5eaa5aa5a7c73df5a297f2acc6c5aeba",
      4    "version": 2,
      5    "size": 83,
      6    "vsize": 83,
      7    "weight": 332,
      8    "locktime": 229453,
      9    "vin": [
     10      {
     11        "txid": "3deb4d61830f6a89eef9a20444e652fe22b6b99900f7468d96366dc111aeaeb0",
     12        "vout": 1,
     13        "scriptSig": {
     14          "asm": "",
     15          "hex": ""
     16        },
     17        "sequence": 4294967293
     18      }
     19    ],
     20    "vout": [
     21      {
     22        "value": 0.00009500,
     23        "n": 0,
     24        "scriptPubKey": {
     25          "asm": "OP_HASH160 2cf633c0bd51271db3e096e474a2c30bb593b175 OP_EQUAL",
     26          "desc": "addr(2MwLxf9gM6RHdkyhm5hqJ4zwBj66YkkqkVU)#96wqgez4",
     27          "hex": "a9142cf633c0bd51271db3e096e474a2c30bb593b17587",
     28          "address": "2MwLxf9gM6RHdkyhm5hqJ4zwBj66YkkqkVU",
     29          "type": "scripthash"
     30        }
     31      }
     32    ]
     33  },
     34  "global_xpubs": [
     35  ],
     36  "psbt_version": 0,
     37  "proprietary": [
     38  ],
     39  "unknown": {
     40  },
     41  "inputs": [
     42    {
     43      "witness_utxo": {
     44        "amount": 0.00010000,
     45        "scriptPubKey": {
     46          "asm": "1 92c4fb214612b51de908d2da8cec7d598e4e6338afa921ec053ebb9298f853da",
     47          "desc": "rawtr(92c4fb214612b51de908d2da8cec7d598e4e6338afa921ec053ebb9298f853da)#3uy4ake4",
     48          "hex": "512092c4fb214612b51de908d2da8cec7d598e4e6338afa921ec053ebb9298f853da",
     49          "address": "tb1pjtz0kg2xz263m6gg6tdgemratx8yucec475jrmq986ae9x8c20dqwux9lm",
     50          "type": "witness_v1_taproot"
     51        }
     52      },
     53      "taproot_key_path_sig": "7cce70e35aa26199d14e08524752399a4325dc49bba2a47c3496633a87d00cf5752ce70addbb3c21b7ecf96fa92b22c8ab60aa882d6e6906077c2f31810d425b",
     54      "taproot_bip32_derivs": [
     55        {
     56          "pubkey": "c840a3b4504c6baeda41c00e1302530561c117a9c59a5d326ba7e66f06e35baf",
     57          "master_fingerprint": "370c1c18",
     58          "path": "m/86h/1h/0h/0/0",
     59          "leaf_hashes": [
     60          ]
     61        },
     62        {
     63          "pubkey": "cfe424cd80ba28552f09b2d90c0be7594eed3d58aa720d7ade61ceeb25b96813",
     64          "master_fingerprint": "a59b4dab",
     65          "path": "m/86h/1h/0h/0/0",
     66          "leaf_hashes": [
     67          ]
     68        },
     69        {
     70          "pubkey": "dd6082cb89ff45d7274d5318eb00378bf99ab7a263e62263780f7e5c90526405",
     71          "master_fingerprint": "ec2c1ecf",
     72          "path": "m",
     73          "leaf_hashes": [
     74          ]
     75        }
     76      ],
     77      "taproot_internal_key": "dd6082cb89ff45d7274d5318eb00378bf99ab7a263e62263780f7e5c90526405",
     78      "musig2_participant_pubkeys": [
     79        {
     80          "aggregate_pubkey": "02dd6082cb89ff45d7274d5318eb00378bf99ab7a263e62263780f7e5c90526405",
     81          "participant_pubkeys": [
     82            "02c840a3b4504c6baeda41c00e1302530561c117a9c59a5d326ba7e66f06e35baf",
     83            "03cfe424cd80ba28552f09b2d90c0be7594eed3d58aa720d7ade61ceeb25b96813"
     84          ]
     85        }
     86      ],
     87      "musig2_pubnonces": [
     88        {
     89          "participant_pubkey": "02c840a3b4504c6baeda41c00e1302530561c117a9c59a5d326ba7e66f06e35baf",
     90          "aggregate_pubkey": "0292c4fb214612b51de908d2da8cec7d598e4e6338afa921ec053ebb9298f853da",
     91          "pubnonce": "03433abca6f0fb5a6d1f5ce4f80082a86936263c3a4c03eec5fba95661f1383f5003f496bc580c3205cd6024707714887fbe6fde9ce0c32b129b88fe783d134a8462"
     92        },
     93        {
     94          "participant_pubkey": "03cfe424cd80ba28552f09b2d90c0be7594eed3d58aa720d7ade61ceeb25b96813",
     95          "aggregate_pubkey": "0292c4fb214612b51de908d2da8cec7d598e4e6338afa921ec053ebb9298f853da",
     96          "pubnonce": "020e00b54e28616335738973a06cc1da680e5c1d123b81d0cb14a2cb75df6a3098026cf5d062c8e9d2069b16669baa05dd0644e4bf6aa877301185a9051afcbaccdb"
     97        }
     98      ],
     99      "musig2_partial_sigs": [
    100        {
    101          "participant_pubkey": "02c840a3b4504c6baeda41c00e1302530561c117a9c59a5d326ba7e66f06e35baf",
    102          "aggregate_pubkey": "0292c4fb214612b51de908d2da8cec7d598e4e6338afa921ec053ebb9298f853da",
    103          "partial_sig": "6b0ee1e466009017c361ed4c265c80db6e803b894bc5e493ed969dc355472640"
    104        },
    105        {
    106          "participant_pubkey": "03cfe424cd80ba28552f09b2d90c0be7594eed3d58aa720d7ade61ceeb25b96813",
    107          "aggregate_pubkey": "0292c4fb214612b51de908d2da8cec7d598e4e6338afa921ec053ebb9298f853da",
    108          "partial_sig": "4dc98b41b5aa3283c89d773b460981c394dd332699e02abd12e89d77c2c656c1"
    109        }
    110      ]
    111    }
    112  ],
    113  "outputs": [
    114    {
    115    }
    116  ],
    117  "fee": 0.00000500
    118}
    

    Issue:
    Despite including a taproot_key_path_sig, the transaction remains incomplete. Why?

    When I use walletprocesspsbt instead of GUI for nonce exchange and signing (4 times), the process completes and I get a hex encoded final transaction.

    Any insights or clarifications on this would be appreciated!

  83. bigspider commented at 6:44 pm on January 5, 2025: contributor

    Sample descriptors: tr(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/**,[7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/**),{{{pk(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/**,[07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/**)),pk(musig([15d62cdf/87'/0'/0']xpub6CpM1svHYyNMTVdmDh5syFXCJHKctJNajbyLEdA8pAgAeg1jotmg9G1aVkND5Rzf37uhwhs8o2Lvq22iRpWwcbNGCrYxAozQfYQYi1eduES/**,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/**))},{pk(musig([7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/**,[07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/**)),pk(musig([7f15646b/87'/0'/0']xpub6ChFTmSdBrhN3D16Rna7hJVQe4w56Gx83U4uNhT3oJaEXiPv7LKnY2gXi3FbbusCb145c3SMEUsSLMRdkxa82MNKqkatnK5b77BXPc3aK8h/**,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/**))}},pk(musig([07895d1c/87'/0'/0']xpub6DF4oz8Ws6Qcd87qKeKFJCMMvcY3X8vQkS5uQE6P5GxCjNE6XfCeak8xc7VUWjUnH4W1N9rmyjVrUHS5S5odkipXkH8G3VGqVoqoRJzL3UZ/**,[17f48baa/87'/0'/0']xpub6DQGEWSeUwmDE9HHzV3Biwj6VWxJj3VkGjefC7zqRJWM1xTU1s5dozA7DNty3ZniaejgLZBPVhsmrR88cpAeW8E3yieJHhfkPVDmAtuhkym/**))}) @hugohn: Update: I notice now that you are using the derive-then-aggregate pattern (musig(xpub1/**,xpub2/**,...)), instead of the aggregate-then-derive one: (musig(xpub1,xpub2,...)/**); therefore, contrarily to what I claimed above, it is not compatible with BIP-388, which only supports the latter.

    Descriptors, as currently specified, support both, which I think it’s unfortunate.

    I recommend using the aggregate-then-derive pattern as it is going to be a lot more efficient in hardware signing devices - and the only one compatible with BIP-388.

  84. hugohn commented at 2:22 am on January 6, 2025: contributor

    Thanks @bigspider , we’ll take a look.

    Our main concern would be compatibility with other wallets. Do you know what wallets support BIP-388 right now (besides Ledger)? Does Sparrow support it?

  85. bigspider commented at 8:22 am on January 6, 2025: contributor

    Thanks @bigspider , we’ll take a look.

    Our main concern would be compatibility with other wallets. Do you know what wallets support BIP-388 right now (besides Ledger)? Does Sparrow support it?

    BIP-388 is the base for the miniscript implementation used currently in Ledger, BitBox and Jade. Sparrow only supports the standard multisig types, and I suppose it works with most devices.

    The musig part of BIP-388 is currently only implemented in Ledger in a test application (details here), and should reach production this quarter.

  86. Sjors commented at 9:25 am on January 6, 2025: member
    Maybe we should have a wallet or importdescriptors flag that restricts imported descriptors to BIP-388? Whether to make that the default would be a separate debate.
  87. achow101 force-pushed on Jan 6, 2025
  88. achow101 commented at 9:19 pm on January 6, 2025: member

    Succeeded using walletprocesspsbt, but failed when using GUI “Load PSBT from keyboard” option.

    Thanks for testing! This revealed an issue with sighash type handling in the aggregation code. I’ve pushed a fix for it, as well as a functional test which could replicate the issue with walletprocesspsbt.

    This fix does require changing how we handle non-default sighash types - namely we now will add PSBT_IN_SIGHASH to an input if we are trying to sign it with something other than SIGHASH_DEFAULT (note that SIGHASH_DEFAULT == SIGHASH_ALL for non-taproot, so the normal non-taproot won’t have this field added, unless SIGHASH_ALL was explicitly specified on the command line).

  89. starius commented at 3:08 am on January 7, 2025: contributor

    @achow101 I tried the updated version (2da3f0e659d3e89da0cdf525f8ce370bb35365a1). I tested GUI flow - it works the same (doesn’t produce a transaction). I also re-tested walletprocesspsbt flow - now it is also broken:

    0walletprocesspsbt "cHNidP8BAH4CAAAAAfkoG4WU8+OG7ihR9ax1V+NQK6C9ZIEbsNH8qfB/A90YAAAAAAD9////AlcDAAAAAAAAF6kUp6q1daWOXVcRwue0FRtYgEAxvTCHKCMAAAAAAAAiUSBug0N9qhtsEJizov7RUbZtdDokLCvlo5+zNl+ocwJn636BAwAAAQErECcAAAAAAAAiUSBZP7q1V48G5XegaVSU+plRyc0hddLNxEwKjgaxGnEVXwEDBAEAAAAhFkNN5PloHAjb0E0osBGFERJSFPdYILJBOGBJ2JaPanvHBQBF3NXJIRZv7RyVBRBNtuBaMSje0M/TrzPCwWIAwsFJNQSMJyagTBkANwwcGFYAAIABAACAAAAAgAAAAAACAAAAIRbk+7os0tinqFncH2A0ptli9Ee0Bworam/Red00sAzNTxkApZtNq1YAAIABAACAAAAAgAAAAAACAAAAARcgQ03k+WgcCNvQTSiwEYURElIU91ggskE4YEnYlo9qe8ciGgNDTeT5aBwI29BNKLARhRESUhT3WCCyQThgSdiWj2p7x0IC5Pu6LNLYp6hZ3B9gNKbZYvRHtAcKK2pv0XndNLAMzU8Db+0clQUQTbbgWjEo3tDP068zwsFiAMLBSTUEjCcmoEwAAAEFIM2w2fouNow30T2TaXy1RedtUv3HPUlj94AmquLBD4vjIQciy89SfFIJjEsrmkBUGjRAF5uKUVaoO7YceX7/+NFBjRkANwwcGFYAAIABAACAAAAAgAEAAAACAAAAIQe3pe9vP18BEUSz/NdpLmQETB90YgqM4GKWJYx6VawM7hkApZtNq1YAAIABAACAAAAAgAEAAAACAAAAIQfNsNn6LjaMN9E9k2l8tUXnbVL9xz1JY/eAJqriwQ+L4wUAqsEXDyIIA82w2fouNow30T2TaXy1RedtUv3HPUlj94AmquLBD4vjQgIiy89SfFIJjEsrmkBUGjRAF5uKUVaoO7YceX7/+NFBjQK3pe9vP18BEUSz/NdpLmQETB90YgqM4GKWJYx6VawM7gA=" true
    1
    2Specified sighash value does not match value stored in PSBT (code -22)
    
  90. achow101 commented at 4:52 am on January 7, 2025: member

    @achow101 I tried the updated version (2da3f0e). I tested GUI flow - it works the same (doesn’t produce a transaction). I also re-tested walletprocesspsbt flow - now it is also broken:

    Ah, the sighash stuff needs a bit more work, and it also has impacts outside of MuSig support.

    For the GUI workflow, if you apply https://github.com/bitcoin-core/gui/pull/850 on top of 3649c2e, it should “work”. However that is not a complete fix as it doesn’t work with other sighash types

  91. achow101 commented at 2:10 am on January 9, 2025: member
    I’ve pushed several commits to fix the sighash issues. These are also opened in their own PR #31622
  92. Sjors commented at 10:52 am on January 16, 2025: member
    Can you update the PR description to have a list of pre-requisite PRs?
  93. fanquake referenced this in commit f9032a4abb on Jan 16, 2025
  94. achow101 commented at 8:16 pm on January 16, 2025: member

    Can you update the PR description to have a list of pre-requisite PRs?

    There’s a tracking issue with everything listed #31246

  95. achow101 force-pushed on Jan 22, 2025
  96. DrahtBot added the label Needs rebase on Feb 4, 2025
  97. achow101 force-pushed on Feb 10, 2025
  98. DrahtBot removed the label Needs rebase on Feb 12, 2025
  99. DrahtBot added the label Needs rebase on Mar 20, 2025
  100. achow101 force-pushed on Apr 10, 2025
  101. DrahtBot removed the label Needs rebase on Apr 11, 2025
  102. DrahtBot added the label CI failed on Apr 11, 2025
  103. DrahtBot commented at 1:23 am on April 11, 2025: contributor

    🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/40360903127

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  104. achow101 force-pushed on Apr 14, 2025
  105. DrahtBot removed the label CI failed on Apr 14, 2025
  106. glozow referenced this in commit c7b592fbd7 on Apr 18, 2025
  107. DrahtBot added the label Needs rebase on Apr 18, 2025
  108. achow101 force-pushed on Apr 18, 2025
  109. DrahtBot removed the label Needs rebase on Apr 19, 2025
  110. glozow referenced this in commit 3e78ac6811 on Apr 21, 2025
  111. DrahtBot added the label Needs rebase on Apr 21, 2025
  112. achow101 force-pushed on Apr 21, 2025
  113. DrahtBot removed the label Needs rebase on Apr 21, 2025
  114. DrahtBot added the label CI failed on May 1, 2025
  115. achow101 force-pushed on May 1, 2025
  116. DrahtBot removed the label CI failed on May 1, 2025
  117. DrahtBot added the label Needs rebase on May 7, 2025
  118. achow101 force-pushed on May 7, 2025
  119. DrahtBot removed the label Needs rebase on May 7, 2025
  120. DrahtBot added the label Needs rebase on May 14, 2025
  121. achow101 force-pushed on May 14, 2025
  122. DrahtBot removed the label Needs rebase on May 14, 2025
  123. achow101 force-pushed on May 21, 2025
  124. Sjors commented at 1:47 pm on June 2, 2025: member
    It seems that if a (miniscript, not yet spendable) script path is present in the descriptor, then walletprocesspsbt won’t provide a pubnonce. (nvm, mistake in my testing flow) @bigspider what version of the Ledger test app is required? And can it just sign (or provide the nonce) the PSBT (via HWI) and register the policy on first use? Or do you have to first explicitly register the policy like Moosig does?
  125. bigspider commented at 5:54 pm on June 2, 2025: contributor

    @bigspider what version of the Ledger test app is required? And can it just sign (or provide the nonce) the PSBT (via HWI) and register the policy on first use? Or do you have to first explicitly register the policy like Moosig does? @Sjors, musig2 is supported from version 2.4.0 of the Bitcoin app, which at this time is the latest version. Note that the firmware OS must be up to date, or you would only find older versions of the app.

    Registration is necessary (and not implemented in HWI).

  126. Sjors commented at 6:01 pm on June 2, 2025: member

    @bigspider Ledger Live says my LedgerX device is up to date. The mainnet Bitcoin app version is 2.4.0, but the testnet is at 2.3.0.

    Or do I need the Bitcoin Test Legacy app? That one has a version 2.4.7, but it throws “ClaNotSupported” if I try it with async-hwi device list command.

  127. bigspider commented at 7:48 pm on June 2, 2025: contributor

    @bigspider Ledger Live says my LedgerX device is up to date. The mainnet Bitcoin app version is 2.4.0, but the testnet is at 2.3.0.

    Or do I need the Bitcoin Test Legacy app? That one has a version 2.4.7, but it throws “ClaNotSupported” if I try it with async-hwi device list command.

    No, “Bitcoin Test” version 2.4.0 is the correct app; maybe something went wrong and its deployment is missing - I’ll check tomorrow. What is the firmware version on your LNX?

  128. Sjors commented at 5:06 am on June 3, 2025: member
    @bigspider OS (firmware) version 2.4.2. I tried removing and reinstalling Bitcoin Test, but that didn’t bump the version. Will await your update. So you’re testing on mainnet then? :-)
  129. bigspider commented at 7:48 am on June 3, 2025: contributor

    @bigspider OS (firmware) version 2.4.2. I tried removing and reinstalling Bitcoin Test, but that didn’t bump the version. Will await your update. So you’re testing on mainnet then? :-) @Sjors thanks for pointing that out, you should now be able to find Bicoin Test version 2.4.0 in the store!

  130. Sjors commented at 8:18 am on June 3, 2025: member

    @bigspider got it! Registration seems to work and device recognized which of the keys is “ours”. Though after approval async-hwi threw Error: Device("ClientError(\n \"Failed to parse descriptor\",\n)") and did not return an HMAC.

    IMG_9612 groot IMG_9613 groot

  131. bigspider commented at 8:30 am on June 3, 2025: contributor

    @Sjors async-hwi probably doesn’t support musig() in descriptors. Rust libraries are generally waiting for upstream support in rust-bitcoin.

    The python package ledger-bitcoin is currently the only client library that is expected to work with musig2 (but it doesn’t have a CLI).

  132. Sjors commented at 9:01 am on June 3, 2025: member

    I see. So instead I registered the policy by tweaking Moosig. That worked and returned an HMAC.

    I then hardcode that HMAC into moosig.py along with the PSBT generated by Bitcoin Core and try to make it request a pubnonce. For this I removed the HotMusig2Cosigner.

    The device recognizes the policy, destination address, amount and fees which I then approve. But then it fails again:

     0🐮 Requesting pubnonces (Round 1)
     1Traceback (most recent call last):
     2  File "/Users/sjors/dev/moosig/moosig.py", line 104, in <module>
     3    main(client)
     4  File "/Users/sjors/dev/moosig/moosig.py", line 74, in main
     5    signer.generate_public_nonces(psbt)
     6  File "/Users/sjors/dev/moosig/utils/musig2.py", line 812, in generate_public_nonces
     7    res = self.client.sign_psbt(psbt, self.wallet_policy, self.wallet_hmac)
     8  File "/Users/sjors/dev/moosig/venv/lib/python3.10/site-packages/ledger_bitcoin/client.py", line 330, in sign_psbt
     9    raise DeviceException(error_code=sw, ins=BitcoinInsType.SIGN_PSBT)
    10ledger_bitcoin.exception.errors.IncorrectDataError: ('0x6a80', 'Error in <BitcoinInsType.SIGN_PSBT: 4> command', '')
    

    I tried both a PSBT generated with send, as well as one that went through walletprocesspsbt which added the public nonce from Bitcoin Core’s side.

    Unfortunately that error is a bit cryptic.

  133. bigspider commented at 9:24 am on June 3, 2025: contributor

    If you’re only requesting pubnonces to the device, this should happen silently (without on-screen interaction). So if you’re validating the transaction in what is supposed to be round 1, it ain’t round 1 :)

    If you have multiple spending paths, it might be doing round 1 for the musig2 key, and signing normally for the other spending path(s).

    For reference, here’s the code in our e2e tests with core, if that helps.

    Also note that as MuSig2 is a stateful process, if you try round 2 and it fails for any reason, you’ll need to start over from round 1.

  134. Sjors commented at 10:20 am on June 3, 2025: member

    It fails at this step, in the generate_public_nonces function:

    0  print("\n🐮 Requesting pubnonce (Round 1)")
    1    signer_1.generate_public_nonces(psbt)
    

    Just in case, I restarted the signing session by having Bitcoin Core make a fresh musig2_pubnonces for its key.

    Are your e2e tests running against the latest version of this branch?

    and signing normally for the other spending path(s).

    The other paths don’t have enough confirmations yet, but I guess that doesn’t mattter?

    Update: I get the same error with the following policy: tr(musig(@0,@1)/**) if I pass it a PSBT that already has the Bitcoin Core public nonce. It also prompts to confirm the send, so it’s clearly doing round 2 if the public nonce is present.

    But when I pass a PSBT without the Bitcoin Core public nonce, it goes ahead and does round 1 without prompt. I then take the PSBT to Bitcoin Core, has it add its nonce and sign (two calls to walletprocesspsbt) and then give that back to the Ledger. Then it happily signs and the transaction can be broadcast.


    Do PRINTF( statements get propagated to the caller? In that case it would aid in debugging to add these to more failure modes. Otherwise it’s hard to figure out which of the 37 SW_INCORRECT_DATA checks triggered the error.

    https://github.com/LedgerHQ/app-bitcoin-new/blob/2.4.0/src/handler/sign_psbt.c

    I guess it doesn’t, because if I deliberately pass an invalid HMAC, I don’t see the message generated by PRINTF("Incorrect hmac\n");. I guess I need to run an emulator for that?

    I’m able to run the emulator using the Visual Studio extension, but I have no idea how to connect to it from the host machine.


    So I think there’s two things the device doesn’t like:

    1. If there’s a script path it can spend
    2. If it’s not the first to provide a public nonce

    Though it’s possible I’m still doing something wrong on my end.

  135. bigspider commented at 2:31 pm on June 3, 2025: contributor

    Are your e2e tests running against the latest version of this branch?

    The container is rebuilt every time a PR is merged, and the musig2 test never failed over the last 6+ months since they were added.

    I rebuilt the container today and confirmed it works on the tagged commit for 2.4.0 release (this is the CI run).

    But when I pass a PSBT without the Bitcoin Core public nonce, it goes ahead and does round 1 without prompt. I then take the PSBT to Bitcoin Core, has it add its nonce and sign (two calls to walletprocesspsbt) and then give that back to the Ledger. Then it happily signs and the transaction can be broadcast.

    Yes, the app expects that there are either no nonces, or all nonces.

    I guess it doesn’t, because if I deliberately pass an invalid HMAC, I don’t see the message generated by PRINTF("Incorrect hmac\n");. I guess I need to run an emulator for that?

    PRINTFs are only present in the debug builds for the emulator.

    I’m able to run the emulator using the Visual Studio extension, but I have no idea how to connect to it from the host machine.

    I recommend to only use the VSCode extension to build it, but then install and run Speculos locally, so you don’t have to deal with containers. Other people work entirely inside the containers, but I personally don’t like Docker that much.

    So I think there’s two things the device doesn’t like:

    1. If there’s a script path it can spend

    The expected behavior is that the app will sign (or run the first round) for all the internal key placeholders (where a key placeholder is internal if it’s a key controlled by the signing device, or a MuSig with a key controlled by the device), as long as the BIP-32 derivations are present for that internal key. That’s because the API is sign(psbt, wallet_policy), so there’s otherwise no way of determining which key you want the device to sign for. Before MuSig2, signing for unnecessary paths was only a waste of time, but now that’s necessary in order to be able to keep the first round silent.

    Prior to version 2.4.0 the app might have signed for some spending paths even with incomplete or BIP32_DERIVATION.

    1. If it’s not the first to provide a public nonce

    The workflow I’m expecting is that the coordinator would give the same PSBT in parallel to all the signers, which execute round 1, then all nonces are added to the PSBT, and eah signer is invoked again and produces the partial musig signatures. If only some nonces are present, then the device assumes Round 1 already happened, but it will fail to run Round 2.

  136. Sjors commented at 2:52 pm on June 3, 2025: member

    If only some nonces are present, then the device assumes Round 1 already happened, but it will fail to run Round 2.

    Ok, that explains issue (2).

    But not why it failed with (1), because the policy variant with older fallback triggers the error without a public nonce from Bitcoin Core for the key path.

    The workflow I’m expecting is that the coordinator would give the same PSBT in parallel to all the signers, which execute round 1, then all nonces are added to the PSBT, and eah signer is invoked again and produces the partial musig signatures.

    That’s suboptimal though. In a two-of-two, if you need to physically go to a second location, you’ll want to:

    1. site A: get nonce 1
    2. site B: get nonce 2
    3. site B: get partial signature 2
    4. site A: get partial signature 2 and broadcast

    Where ideally step (2) and (3) are a single action. Though you can still achieve this by running combinepsbt in site B after step (2).

    Are your e2e tests running against the latest version of this branch?

    The container is rebuilt every time a PR is merged

    Right, but that just tests Bitcoin Core master, not this PR?

    I guess if I want to debug this further, I’d have to modify the e2e test to use this branch and recreate the multisig setup I described in the test. Then if a test fails it will log the specific error in the console (if I add a few more PRINTFs).

    but then install and run Speculos locally, so you don’t have to deal with containers

    I couldn’t figure out how to do that on an M4 mac, but it might be easier on my Ubuntu machine. I’m not a Docker fan either.


    On a different note, thinking about MuSig2 session management a bit more, the BIP says:

    To avoid accidental reuse of secnonce, an implementation may securely erase the secnonce argument by overwriting it with 64 zero bytes after it has been read by Sign. A secnonce consisting of only zero bytes is invalid for Sign and will cause it to fail.

    So if the user makes multiple calls to walletprocesspsbt, and the psbt contains our public nonce, then the first call would return a PSBT with a partial signature. If the call is repeated, i.e. without the partial signature that we produced earlier, it should fail (not silently).

    Currently it just quietly removes musig2_partial_sigs, which may have contributed to my confusion.

     0diff --git a/test/functional/wallet_musig.py b/test/functional/wallet_musig.py
     1index b7f3cc9d96..9c8e0a8fd3 100755
     2--- a/test/functional/wallet_musig.py
     3+++ b/test/functional/wallet_musig.py
     4@@ -8,7 +8,7 @@ import re
     5 from test_framework.descriptors import descsum_create
     6 from test_framework.key import H_POINT
     7 from test_framework.test_framework import BitcoinTestFramework
     8-from test_framework.util import assert_equal
     9+from test_framework.util import assert_equal, assert_raises_rpc_error
    10
    11 PRIVKEY_RE = re.compile(r"^tr\((.+?)/.+\)#.{8}$")
    12 PUBKEY_RE = re.compile(r"^tr\((\[.+?\].+?)/.+\)#.{8}$")
    13@@ -150,6 +150,9 @@ class WalletMuSigTest(BitcoinTestFramework):
    14         for wallet in wallets:
    15             proc = wallet.walletprocesspsbt(psbt=comb_nonce_psbt, sighashtype=sighash_type)
    16             assert_equal(proc["complete"], False)
    17+            # Never sign twice:
    18+            assert_raises_rpc_error(-1, "sec nonce lost in tragic boating accident", wallet.walletprocesspsbt, psbt=comb_nonce_psbt, sighashtype=sighash_type)
    19+
    20             psig_psbts.append(proc["psbt"])
    
  137. bigspider commented at 3:20 pm on June 3, 2025: contributor

    But not why it failed with (1), because the policy variant with older fallback triggers the error without a public nonce from Bitcoin Core for the key path.

    Please feel free to open an issue and I’ll investigate.

    The workflow I’m expecting is that the coordinator would give the same PSBT in parallel to all the signers, which execute round 1, then all nonces are added to the PSBT, and eah signer is invoked again and produces the partial musig signatures.

    That’s suboptimal though. In a two-of-two, if you need to physically go to a second location, you’ll want to:

    1. site A: get nonce 1
    2. site B: get nonce 2
    3. site B: get partial signature 2
    4. site A: get partial signature 2 and broadcast

    Where ideally step (2) and (3) are a single action. Though you can still achieve this by running combinepsbt in site B after step (2).

    Sure, but while the Ledger app is using PSBT in its protocol, it is not designed to be a full implementation of the PSBT standard, as it’s running in a very constrained setting where it doesn’t even have full access to the PSBT without communicating with the client. Verifying if ‘my nonce is present’ is a substantial amount of work, while checking if ‘some nonces’ are present is trivial by iterating once through the PSBT keys.

    It is much easier to handle this on the client side (‘if only some but not all nonces are present, delete them before calling the device"), rather than implementing such complex logic on the device.

    Right, but that just tests Bitcoin Core master, not this PR?

    I’m currently using Bitcoin Core compiled from this PR, since MuSig2 is part of the CI; I’ll switch back to master once this PR is merged.

  138. Sjors commented at 3:31 pm on June 3, 2025: member

    But not why it failed with (1), because the policy variant with older fallback triggers the error without a public nonce from Bitcoin Core for the key path.

    Please feel free to open an issue and I’ll investigate.

    I’ll test it once more and will open an issue if needed.

    Verifying if ‘my nonce is present’ is a substantial amount of work, while checking if ‘some nonces’ are present is trivial by iterating once through the PSBT keys.

    I see. It would be good to document these caveats somewhere. They’re easy to work around, but hard to debug if you run into them.

  139. in src/musig.h:46 in 5e90a3c747 outdated
    39@@ -40,8 +40,7 @@ std::optional<CPubKey> MuSig2AggregatePubkeys(const std::vector<CPubKey>& pubkey
    40 class MuSig2SecNonce
    41 {
    42 private:
    43-    //! The actual secnonce itself
    44-    secure_unique_ptr<secp256k1_musig_secnonce> m_nonce;
    45+    std::unique_ptr<MuSig2SecNonceImpl> m_impl;
    


    theStack commented at 1:17 pm on June 5, 2025:

    in commit 5e90a3c747ac2d44f9f52a3eeffbbe391f837bd8: seems like this change should be part of the earlier commit 01f639835783aa10d4c4b63da477c0300bc8495f, which currently doesn’t compile

    0...
    1In file included from /home/thestack/bitcoin_prrev/pr29675/src/musig.cpp:5:
    2/home/thestack/bitcoin_prrev/pr29675/src/musig.h:44:5: error: no template named 'secure_unique_ptr'
    3    secure_unique_ptr<secp256k1_musig_secnonce> m_nonce;
    4
    5/home/thestack/bitcoin_prrev/pr29675/src/musig.cpp:74:36: error: member initializer 'm_impl' does not name a non-static data member or base class
    6MuSig2SecNonce::MuSig2SecNonce() : m_impl{std::make_unique<MuSig2SecNonceImpl>()} {}
    7...
    

    (not sure why the test-each-commit CI job doesn’t detect this though?)


    Sjors commented at 1:36 pm on June 5, 2025:
    @theStack it only runs the most recent N (6?) commits.

    achow101 commented at 8:55 pm on June 5, 2025:
    Fixed
  140. theStack commented at 1:18 pm on June 5, 2025: contributor
    Concept ACK
  141. achow101 force-pushed on Jun 5, 2025
  142. achow101 force-pushed on Jun 11, 2025
  143. DrahtBot added the label CI failed on Jun 16, 2025
  144. achow101 force-pushed on Jun 17, 2025
  145. achow101 force-pushed on Jun 17, 2025
  146. DrahtBot removed the label CI failed on Jun 17, 2025
  147. achow101 force-pushed on Jun 18, 2025
  148. achow101 force-pushed on Jun 19, 2025
  149. DrahtBot added the label CI failed on Jun 19, 2025
  150. DrahtBot commented at 0:16 am on June 19, 2025: contributor

    🚧 At least one of the CI tasks failed. Task tidy: https://github.com/bitcoin/bitcoin/runs/44377387322 LLM reason (✨ experimental): The CI failure is caused by a clang-tidy error due to the use of a variable ‘match’ that is copy-constructed from a const reference but not used, which is treated as an error.

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  151. DrahtBot removed the label CI failed on Jun 19, 2025
  152. Sjors commented at 2:16 pm on July 2, 2025: member
    Meanwhile https://github.com/LedgerHQ/app-bitcoin-new/issues/329 has been figured out and I was able to do a keypath spend on the more complicated policy. That’s a good sign for interoperability.
  153. achow101 force-pushed on Jul 30, 2025
  154. achow101 commented at 11:37 pm on July 30, 2025: member
    Added a few commits to address followups from #31244
  155. in src/pubkey.h:227 in 5ff19c37af outdated
    223@@ -224,7 +224,7 @@ class CPubKey
    224     bool Decompress();
    225 
    226     //! Derive BIP32 child pubkey.
    227-    [[nodiscard]] bool Derive(CPubKey& pubkeyChild, ChainCode &ccChild, unsigned int nChild, const ChainCode& cc) const;
    228+    [[nodiscard]] bool Derive(CPubKey& pubkeyChild, ChainCode &ccChild, unsigned int nChild, const ChainCode& cc, uint256& tweak_out) const;
    


    Sjors commented at 6:48 am on July 31, 2025:

    In 5ff19c37af53f6ebcd69a9973ededcbadd69d3bd pubkey: Return tweaks from BIP32 derivation: in BIP32 this is IL which isn’t given a name. In libsecp it’s called tweak, so I guess that’s fine.

    Although maybe bip32_tweak is better, so as not to confuse it with the tweaks in taproot. https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#public-parent-key--public-child-key


    achow101 commented at 6:31 pm on August 14, 2025:
    Done
  156. in src/script/sign.cpp:575 in 1d4fdeb637 outdated
    368+
    369+        KeyOriginInfo output_key_info;
    370+        if (provider.GetKeyOriginByXOnly(output, output_key_info)) {
    371+            auto it = sigdata.taproot_misc_pubkeys.find(output);
    372+            if (it == sigdata.taproot_misc_pubkeys.end()) {
    373+                sigdata.taproot_misc_pubkeys.emplace(output, std::make_pair(std::set<uint256>(), output_key_info));
    


    Sjors commented at 7:07 am on July 31, 2025:
    In 1d4fdeb63792988b4fb93cf8e84142a3a5b34a39 sign: Include taproot output key’s KeyOriginInfo in sigdata: can you update the description of taproot_misc_pubkeys to mention that this can now also include the output key?

    achow101 commented at 6:31 pm on August 14, 2025:
    Done
  157. in src/script/signingprovider.cpp:129 in f4b4c704e4 outdated
    114+    if (!musig2_secnonces) return;
    115+    musig2_secnonces->emplace(session_id, std::move(nonce));
    116+}
    117+
    118+std::optional<std::reference_wrapper<MuSig2SecNonce>> FlatSigningProvider::GetMuSig2SecNonce(const uint256& session_id) const
    119+{
    


    Sjors commented at 7:39 am on July 31, 2025:

    In f4b4c704e40cc11663dbdaa156b1775be668a15f signingprovider: Add musig2 secnonces: needs guard?

    0if (!musig2_secnonces) return std::nullopt;
    

    Ditto for DeleteMuSig2Session


    theStack commented at 2:04 am on August 10, 2025:
    Agree that all the methods deferencing musig2_secnonces should have guards, either by returning nullopt or adding an Assume. (// EDIT: that was a reply to #29675 (review))

    achow101 commented at 6:31 pm on August 14, 2025:
    Done
  158. in src/script/signingprovider.cpp:124 in f4b4c704e4 outdated
    108@@ -94,6 +109,24 @@ std::vector<CPubKey> FlatSigningProvider::GetMuSig2ParticipantPubkeys(const CPub
    109     return participant_pubkeys;
    110 }
    111 
    112+void FlatSigningProvider::SetMuSig2SecNonce(const uint256& session_id, MuSig2SecNonce&& nonce) const
    113+{
    114+    if (!musig2_secnonces) return;
    


    Sjors commented at 7:40 am on July 31, 2025:
    In f4b4c704e40cc11663dbdaa156b1775be668a15f signingprovider: Add musig2 secnonces: maybe add an Assume here?

    achow101 commented at 6:31 pm on August 14, 2025:
    Done
  159. in src/script/signingprovider.cpp:151 in f4b4c704e4 outdated
    134@@ -102,6 +135,7 @@ FlatSigningProvider& FlatSigningProvider::Merge(FlatSigningProvider&& b)
    135     origins.merge(b.origins);
    136     tr_trees.merge(b.tr_trees);
    137     aggregate_pubkeys.merge(b.aggregate_pubkeys);
    138+    if (!musig2_secnonces) musig2_secnonces = b.musig2_secnonces;
    


    Sjors commented at 7:42 am on July 31, 2025:
    In f4b4c704e40cc11663dbdaa156b1775be668a15f signingprovider: Add musig2 secnonces: I guess there’s no realistic scenario where two different sessions actually need to be merged, but maybe add a comment to point this out.

    achow101 commented at 6:31 pm on August 14, 2025:
    Done
  160. in src/script/sign.cpp:189 in ae6889565a outdated
    140+    CKey key;
    141+    if (!provider.GetKey(part_pubkey.GetID(), key)) return {};
    142+
    143+    // Retrieve participant pubkeys
    144+    std::vector<CPubKey> pubkeys = provider.GetMuSig2ParticipantPubkeys(aggregate_pubkey);
    145+    if (!pubkeys.size()) return {};
    


    Sjors commented at 7:49 am on July 31, 2025:
    In ae6889565af3afed567e08976b4bc0f0381d0bf8 sign: Add CreateMuSig2Nonce: maybe sanity check that part_pubkey is in pubkeys?

    achow101 commented at 6:31 pm on August 14, 2025:
    Done
  161. in src/script/sign.cpp:202 in ae6889565a outdated
    152+    std::vector<uint8_t> out = key.CreateMuSig2Nonce(secnonce, *sighash, aggregate_pubkey, pubkeys);
    153+    if (out.empty()) return {};
    154+
    155+    // Store the secnonce in the SigningProvider
    156+    HashWriter hasher;
    157+    hasher << script_pubkey << part_pubkey << *sighash;
    


    Sjors commented at 8:02 am on July 31, 2025:

    In ae6889565af3afed567e08976b4bc0f0381d0bf8 sign: Add CreateMuSig2Nonce: it’s not clear to me how careful we need to be regarding parallel sessions.

    If it’s not a big deal then I guess the current choice is fine. Since the same key can be used in multiple tap leaves, we need a unique nonce per leaf, which sighash takes care of. We might have multiple participant keys, which part_pubkey takes care of.

    script_pubkey isn’t enough to cover address reuse, but for now ComputeSchnorrSignatureHash is limited to SIGHASH_DEFAULT, which commits to the prevout. Maybe for good measure add an Assume for SIGHASH_DEFAULT?

    If it is a big deal, then I think libsecp should give us a function to generate it.


    achow101 commented at 6:29 pm on August 14, 2025:

    If it’s not a big deal then I guess the current choice is fine. Since the same key can be used in multiple tap leaves, we need a unique nonce per leaf, which sighash takes care of. We might have multiple participant keys, which part_pubkey takes care of.

    script_pubkey isn’t enough to cover address reuse,

    Data unique to the input is always included in the sighash, so address reuse is not an issue either.

    but for now ComputeSchnorrSignatureHash is limited to SIGHASH_DEFAULT, which commits to the prevout. Maybe for good measure add an Assume for SIGHASH_DEFAULT?

    Other sighash types are allowed, they’re a member of MutableTransactionSignatureCreator.

  162. Sjors commented at 8:14 am on July 31, 2025: member

    Reviewed the non-base commits up to ae6889565af3afed567e08976b4bc0f0381d0bf8 sign: Add CreateMuSig2Nonce, but I skipped over 25c4fd4e50a6d230c959fb0072df04ef7093aa0a sign: Add CreateMuSig2AggregateSig.

    It’s a bit confusing that 25c4fd4e50a6d230c959fb0072df04ef7093aa0a sign: Add CreateMuSig2AggregateSig checks for and uses public nonces, but those are not available until later commits like ae6889565af3afed567e08976b4bc0f0381d0bf8 sign: Add CreateMuSig2Nonce.

    Note that PR is currently isn’t based on the latest #31244 but the tests pass if I do rebase it, so that seems a minor difference.

    The only thing I haven’t been able to test against a Ledger is script path spending, waiting for the v2.4.1 release. I did test script path spending using just Bitcoin Core keys.

  163. glozow referenced this in commit 5ee4e79669 on Jul 31, 2025
  164. descriptors: Fix meaning of any_key_parsed
    Invert any_key_parsed so that the name matches the behavior.
    2320184d0e
  165. descriptors: Add a doxygen comment for has_hardened output_parameter 39a63bf2e7
  166. tests: Clarify why musig derivation adds a pubkey and xpub a4cfddda64
  167. sign: Refactor Schnorr sighash computation out of CreateSchnorrSig
    There will be other functions within MutableTransactionSignatureCreator
    that need to compute the same sighash, so make it a separate member
    function.
    fb8720f1e0
  168. achow101 force-pushed on Jul 31, 2025
  169. DrahtBot added the label CI failed on Aug 1, 2025
  170. achow101 marked this as ready for review on Aug 1, 2025
  171. achow101 commented at 3:45 am on August 1, 2025: member
    All prerequisites have been merged, ready for review.
  172. DrahtBot removed the label CI failed on Aug 1, 2025
  173. in src/pubkey.cpp:341 in c78d25b890 outdated
    337@@ -338,13 +338,14 @@ bool CPubKey::Decompress() {
    338     return true;
    339 }
    340 
    341-bool CPubKey::Derive(CPubKey& pubkeyChild, ChainCode &ccChild, unsigned int nChild, const ChainCode& cc) const {
    342+bool CPubKey::Derive(CPubKey& pubkeyChild, ChainCode &ccChild, unsigned int nChild, const ChainCode& cc, uint256& tweak_out) const {
    


    theStack commented at 4:35 pm on August 5, 2025:
    in commit c78d25b89074325ea37185ca87bb00385ad4ab20: alternatively, could pass tweak_out as pointer (set to nullptr by default, in which case no memcpy would happen) here and for CExtPubkey::Derive to avoid the method overloading for the latter

    achow101 commented at 8:48 pm on August 5, 2025:
    Done as suggested
  174. in src/musig.h:34 in 751b1884b1 outdated
    29+ * MuSig2SecNonce encapsulates a secret nonce in use in a MuSig2 signing session.
    30+ * Since this nonce persists outside of libsecp256k1 signing code, we must handle
    31+ * its construction and destruction ourselves.
    32+ * The secret nonce must be kept a secret, otherwise the private key may be leaked.
    33+ * As such, it needs to be treated in the same way that CKeys are treated.
    34+ * So this class handles the secure allocation of the secp25k1_musig_secnonce object
    


    theStack commented at 4:40 pm on August 5, 2025:
    0 * So this class handles the secure allocation of the secp256k1_musig_secnonce object
    

    achow101 commented at 8:49 pm on August 5, 2025:
    Done
  175. in src/musig.cpp:105 in 0c42077224 outdated
    100+    // Get the keyagg cache and aggregate pubkey
    101+    secp256k1_musig_keyagg_cache keyagg_cache;
    102+    if (!GetMuSig2KeyAggCache(participants, keyagg_cache)) return std::nullopt;
    103+    std::optional<CPubKey> agg_key = GetCPubKeyFromMuSig2KeyAggCache(keyagg_cache);
    104+    if (!agg_key.has_value()) return std::nullopt;
    105+    if (aggregate_pubkey != *agg_key) return std::nullopt;
    


    theStack commented at 5:42 pm on August 5, 2025:
    in 0c420772247ff51b510d44061a2e94e3f5195899: this pattern of creating the keyagg cache and verifying it against a given aggregate pubkey appears more often in later commits (in CKey::CreateMuSig2Nonce and CKey::CreateMuSig2PartialSig), so could refactor it into an own helper (can be done in a follow-up though)

    achow101 commented at 8:49 pm on August 5, 2025:
    Done
  176. in src/script/sign.cpp:326 in 1165b7aab9 outdated
    322+                    // Get the BIP32 derivation tweaks
    323+                    CExtPubKey extpub;
    324+                    extpub.nDepth = 0;
    325+                    std::memset(extpub.vchFingerprint, 0, 4);
    326+                    extpub.nChild = 0;
    327+                    extpub.chaincode = uint256::FromHex("6589e367712c6200e367717145cb322d76576bc3248959c474f9a602ca878086").value();
    


    theStack commented at 5:44 pm on August 5, 2025:
    could use the MUSIG_CHAINCODE constant, here and in SignTaproot below

    achow101 commented at 8:49 pm on August 5, 2025:
    Done
  177. in src/musig.cpp:200 in 0c42077224 outdated
    172+        }
    173+    }
    174+
    175+    // Aggregate partial sigs
    176+    std::vector<uint8_t> sig;
    177+    sig.resize(64);
    


    theStack commented at 5:50 pm on August 5, 2025:
    in 0c420772247ff51b510d44061a2e94e3f5195899: nit: since the size is known at compile-time, could alternatively return a std::array (OTOH, at the call-site a std::vector is still needed due to potential adding of the sighash byte)

    achow101 commented at 8:49 pm on August 5, 2025:
    Leaving as-is since we need to pass around the signature in a std::vector elsewhere in signing.
  178. achow101 force-pushed on Aug 5, 2025
  179. in src/key.cpp:353 in 6991ecb30b outdated
    349@@ -349,6 +350,39 @@ KeyPair CKey::ComputeKeyPair(const uint256* merkle_root) const
    350     return KeyPair(*this, merkle_root);
    351 }
    352 
    353+std::vector<uint8_t> CKey::CreateMuSig2Nonce(MuSig2SecNonce& secnonce, const uint256& hash, const CPubKey& aggregate_pubkey, const std::vector<CPubKey>& pubkeys)
    


    theStack commented at 1:41 am on August 10, 2025:

    module organization/refactoring nit: I think conceptually this functionality would be better fit in the musig.cpp module instead of being a method of the CKey class. Even though a secret key is also passed in for generating the musig nonce here, it merely serves as (optional) additional data to derive the nonce for increasing misuse-resistance, rather than being a central part that would justify an own method. The cleanest approach would be in general to only include and use the secp256k1 musig module in musig.cpp (also for partial signing), IMHO.

    Can be dealt in a follow-up though, as the secp256k1_context_sign object would have to be made non-static and shared with other modules (presumably that was the main reason why nonce generation and partial signing was decided to be in key.cpp), which might lead to more general discussions that are probably best kept in a separate PR to not block this one.


    achow101 commented at 6:30 pm on August 14, 2025:
    It’s mainly here to use secp256k1_context_sign.

    theStack commented at 2:25 pm on September 9, 2025:
    Note that the only musig API function that requires to use this context is secp256k1_musig_nonce_gen, for all the others you can simply use the static one (secp256k1_context_static).

    rkrux commented at 8:59 am on September 15, 2025:

    In https://github.com/bitcoin/bitcoin/commit/6b01d7379b9e29760abd7b549a04db43937a7b8d “sign: Add CreateMuSig2Nonce”

    For the pubnonce that we know is of a fixed size of 66 bytes, can’t we use a fixed size array instead everywhere, which I find to be more expressive for this use case? I suppose there is no case when we need to use the vector specific properties for a pubnonce.

    0std::vector<uint8_t> 
    

    achow101 commented at 9:14 pm on September 16, 2025:
    I’m going to leave this as-is for now since changing the type causes a ton of other things to be changed, particularly in PSBT and SignatureData. We also use the fact that it’s a vector to indicate when a pubnonce was not successfully created, and turning it into an array requires changing all of those returns as well.
  180. achow101 force-pushed on Aug 14, 2025
  181. achow101 force-pushed on Aug 14, 2025
  182. in src/musig.cpp:104 in d65c8df972 outdated
     99@@ -100,3 +100,89 @@ bool MuSig2SecNonce::IsValid()
    100 {
    101     return m_impl->IsValid();
    102 }
    103+
    104+std::optional<std::vector<uint8_t>> CreateMuSig2AggregateSig(const std::vector<CPubKey>& participants, const CPubKey& aggregate_pubkey, const std::vector<std::pair<uint256, bool>>& tweaks, const uint256& sighash, const std::map<CPubKey, std::vector<uint8_t>>& pubnonces, const std::map<CPubKey, uint256>& partial_sigs)
    


    theStack commented at 2:20 pm on September 3, 2025:
    in commit d65c8df972de55ade06557cabb1b8972d4169fb1: naming nit: participants doesn’t say much imho, could rename to participant_pubkeys (or just part_pubkeys / pubkeys, if that’s too long) to be more specific

    achow101 commented at 6:52 pm on September 3, 2025:
    Changed to part_pubkeys.
  183. in src/script/sign.cpp:159 in a7901710a6 outdated
    154+    if (out.empty()) return {};
    155+
    156+    // Store the secnonce in the SigningProvider
    157+    HashWriter hasher;
    158+    hasher << script_pubkey << part_pubkey << *sighash;
    159+    uint256 id = hasher.GetSHA256();
    


    theStack commented at 2:25 pm on September 3, 2025:
    in commit a7901710a6b222cbdeba474bbe8b78788841e58a: it’s only a few lines of code, but to deduplicate with the next commit (introducing CreateMuSig2PartialSig), could introduce a helper function like MuSig2SessionId

    achow101 commented at 6:53 pm on September 3, 2025:
    Done
  184. in src/key.cpp:398 in 38d74136c6 outdated
    393+    if (!MuSig2AggregatePubkeys(pubkeys, keyagg_cache, aggregate_pubkey)) return std::nullopt;
    394+
    395+    // Parse the pubnonces
    396+    std::vector<std::pair<secp256k1_pubkey, secp256k1_musig_pubnonce>> signers_data;
    397+    std::vector<const secp256k1_musig_pubnonce*> pubnonce_ptrs;
    398+    size_t ours = 0;
    


    theStack commented at 2:29 pm on September 3, 2025:
    in commit 38d74136c6552ae746bec6e05b8ec3069cd581ce: nit, could rename to something like our_pubkey_index to be more expressive

    achow101 commented at 6:53 pm on September 3, 2025:
    Done
  185. in src/key.cpp:410 in 38d74136c6 outdated
    402+        if (pn_it == pubnonces.end()) return std::nullopt;
    403+        const std::vector<uint8_t> pubnonce = pn_it->second;
    404+        if (pubnonce.size() != 66) return std::nullopt;
    405+        if (part_pk == our_pubkey) {
    406+            ours = signers_data.size();
    407+        }
    


    theStack commented at 2:30 pm on September 3, 2025:
    in 38d74136c6552ae746bec6e05b8ec3069cd581ce: if our_pubkey is not contained in the pubkeys list, I suppose we want to return early with std::nullopt, rather than accessing at index 0?

    achow101 commented at 6:53 pm on September 3, 2025:
    Done
  186. theStack commented at 2:32 pm on September 3, 2025: contributor

    Reviewed up to 38d74136c6552ae746bec6e05b8ec3069cd581ce, left a few non-blocking suggestions below.

    I think the order of commits d65c8df972de55ade06557cabb1b8972d4169fb138d74136c6552ae746bec6e05b8ec3069cd581ce is currently slightly confusing for reviewers, as it doesn’t reflect the protocol flow, i.e. the signature aggregation function CreateMuSig2AggregateSig should ideally be introduced after the partial signature creation function CreateMuSig2PartialSig.

  187. rkrux commented at 2:54 pm on September 3, 2025: contributor

    Concept ACK ae645ef858702ff85dab4b1ac6296d53b573c81b, started reviewing.

    Maybe the PR description could be updated to remove the following references as their detailed implementations were done in prior PRs.

    This PR implements MuSig2 descriptors (BIP 390), derivation (BIP 328), and PSBT fields (BIP 373)

  188. achow101 force-pushed on Sep 3, 2025
  189. achow101 force-pushed on Sep 3, 2025
  190. Sjors commented at 4:36 pm on September 5, 2025: member

    Meanwhile I managed to test a script path spend with Ledger as well (though I haven’t tried MuSig2 inside a script path), using https://github.com/bitcoin-core/HWI/pull/794. With that I’m pretty happy with interoperability.

    Will do another code review round soon(tm).

  191. in test/functional/wallet_musig.py:16 in d23116987d outdated
    11+from test_framework.util import assert_equal
    12+
    13+PRIVKEY_RE = re.compile(r"^tr\((.+?)/.+\)#.{8}$")
    14+PUBKEY_RE = re.compile(r"^tr\((\[.+?\].+?)/.+\)#.{8}$")
    15+ORIGIN_PATH_RE = re.compile(r"^\[\w{8}(/.*)\].*$")
    16+MULTIPATH_RE = re.compile(r"(.*?)<(\d+);(\d+)>")
    


    rkrux commented at 11:27 am on September 8, 2025:

    In d23116987d19587746d392895c06f5c426c1d0d2 “test: Test MuSig2 in the wallet”

    MULTIPATH_RE is unused.


    achow101 commented at 7:08 pm on September 9, 2025:
    Used now for has_internal.
  192. in test/functional/wallet_musig.py:105 in d23116987d outdated
    100+        def_wallet.sendtoaddress(addr, 10)
    101+        self.generate(self.nodes[0], 1)
    102+
    103+        # Spend that UTXO
    104+        utxo = wallets[0].listunspent()[0]
    105+        psbt = wallets[0].send(outputs=[{def_wallet.getnewaddress(): 5}], inputs=[utxo], change_type="bech32m")["psbt"]
    


    rkrux commented at 11:54 am on September 8, 2025:

    In https://github.com/bitcoin/bitcoin/commit/d23116987d19587746d392895c06f5c426c1d0d2 “test: Test MuSig2 in the wallet”

    Because the imported musig descriptor was an active one, the change address that is created here is also musig that leads to a outputs property in the decoded PSBT that is untested at the moment unlike the inputs section that is asserted on down below. IMO we should check for the musig properties in the output section too.


    achow101 commented at 7:08 pm on September 9, 2025:
    Done
  193. in test/functional/wallet_musig.py:114 in d23116987d outdated
    89+            if addr is None:
    90+                addr = wallet.getnewaddress(address_type="bech32m")
    91+            else:
    92+                assert_equal(addr, wallet.getnewaddress(address_type="bech32m"))
    93+            if has_int:
    94+                if change_addr is None:
    


    rkrux commented at 11:59 am on September 8, 2025:

    In https://github.com/bitcoin/bitcoin/commit/d23116987d19587746d392895c06f5c426c1d0d2 “test: Test MuSig2 in the wallet”

    Can consider using this change_addr below like addr is used to have fewer moving parts.

    0- psbt = wallets[0].send(outputs=[{def_wallet.getnewaddress(): 5}], inputs=[utxo], change_type="bech32m")["psbt"]
    1+ psbt = wallets[0].send(outputs=[{def_wallet.getnewaddress(): 5}], inputs=[utxo], change_address= change_addr)["psbt"]
    

    achow101 commented at 7:08 pm on September 9, 2025:
    I think it is useful to to exercise the change address of send.
  194. in test/functional/wallet_musig.py:102 in d23116987d outdated
    79+            })
    80+
    81+            res = wallet.importdescriptors(import_descs)
    82+            for r in res:
    83+                assert_equal(r["success"], True)
    84+
    


    rkrux commented at 12:22 pm on September 8, 2025:

    In https://github.com/bitcoin/bitcoin/commit/d23116987d19587746d392895c06f5c426c1d0d2 “test: Test MuSig2 in the wallet”

    For the last 4 patterns, I see the following error:

    0test_framework.authproxy.JSONRPCException: Can't get descriptor string. (-4)
    

    if the private descs are listed immediately after importing musig descriptor:

    0wallets[0].listdescriptors(True)["descriptors"]
    

    The first 5 patterns work fine - I am guessing because of this default from the previous PR: #31244 (review).

    Not debugged yet but I guess the fix from #32471 PR should handle the failing patterns as well?


    achow101 commented at 7:09 pm on September 9, 2025:
    Fixing that is orthogonal to this PR

    rkrux commented at 9:26 am on September 10, 2025:
    Didn’t intend to suggest fixing it in this PR, instead highlighting an issue that could probably be fixed by the other PR if/when it’s merged.
  195. in test/functional/wallet_musig.py:122 in d23116987d outdated
     97+                    assert_equal(change_addr, wallet.getrawchangeaddress(address_type="bech32m"))
     98+
     99+        # Fund that address
    100+        def_wallet.sendtoaddress(addr, 10)
    101+        self.generate(self.nodes[0], 1)
    102+
    


    rkrux commented at 12:35 pm on September 8, 2025:

    In https://github.com/bitcoin/bitcoin/commit/d23116987d19587746d392895c06f5c426c1d0d2 “test: Test MuSig2 in the wallet”

    Before spending, would be good to check that wallets agree on the received UTXO.

    0        # Check that the wallets agree on the received UTXO
    1        utxo = None
    2        for wallet in wallets:
    3            if utxo is None:
    4                utxo = wallet.listunspent()[0]
    5            else:
    6                assert_equal(utxo, wallet.listunspent()[0])
    

    achow101 commented at 7:08 pm on September 9, 2025:
    Done
  196. in test/functional/wallet_musig.py:30 in d23116987d outdated
    25+        self.skip_if_no_wallet()
    26+
    27+    def do_test(self, comment, pattern, sighash_type=None):
    28+        self.log.info(f"Testing {comment}")
    29+        def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
    30+        has_int = "<" in pattern and ">" in pattern
    


    rkrux commented at 12:42 pm on September 8, 2025:

    In https://github.com/bitcoin/bitcoin/commit/d23116987d19587746d392895c06f5c426c1d0d2 “test: Test MuSig2 in the wallet”

    0- has_int = "<" in pattern and ">" in pattern
    1+ has_internal = "<" in pattern and ">" in pattern
    

    While this check suffices due to the test cases, but it doesn’t differentiate between a 2 and 3 indexed multipath descriptor. A test like the below one fails:

    0self.do_test("tr(H,{pk(musig/*), pk(same keys different musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1;2>/*),pk(musig($1,$2)/0/*)})")
    

    achow101 commented at 7:09 pm on September 9, 2025:
    Updated the name and changed this to use a regex that matches only on a 2 index multipath.
  197. in test/functional/wallet_musig.py:29 in d23116987d outdated
    24+    def skip_test_if_missing_module(self):
    25+        self.skip_if_no_wallet()
    26+
    27+    def do_test(self, comment, pattern, sighash_type=None):
    28+        self.log.info(f"Testing {comment}")
    29+        def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
    


    rkrux commented at 12:46 pm on September 8, 2025:

    In https://github.com/bitcoin/bitcoin/commit/d23116987d19587746d392895c06f5c426c1d0d2 “test: Test MuSig2 in the wallet”

    Nit - a candidate for the class param instead of retrieving in every case:

    0self.def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
    

    achow101 commented at 7:09 pm on September 9, 2025:
    Done
  198. in test/functional/wallet_musig.py:38 in d23116987d outdated
    33+        keys = []
    34+
    35+        pat = pattern.replace("$H", H_POINT)
    36+
    37+        # Figure out how many wallets are needed and create them
    38+        exp_key_leaf = 0
    


    rkrux commented at 12:48 pm on September 8, 2025:

    In https://github.com/bitcoin/bitcoin/commit/d23116987d19587746d392895c06f5c426c1d0d2 “test: Test MuSig2 in the wallet”

    Nit for clarity:

    0- exp_key_leaf = 0
    1+ expected_key_leaves_count = 0
    

    achow101 commented at 7:09 pm on September 9, 2025:
    Updated to expected_key_leaves
  199. in test/functional/wallet_musig.py:65 in d23116987d outdated
    60+                    # to extract and insert the origin path from the pubkey as well.
    61+                    privkey += ORIGIN_PATH_RE.search(pubkey).group(1)
    62+                    break
    63+                keys.append((privkey, pubkey))
    64+
    65+        # Construct and import each wallet's musig descriptor
    


    rkrux commented at 12:50 pm on September 8, 2025:

    In https://github.com/bitcoin/bitcoin/commit/d23116987d19587746d392895c06f5c426c1d0d2 “test: Test MuSig2 in the wallet”

    0- # Construct and import each wallet's musig descriptor
    1+ # Construct and import each wallet's musig descriptor that
    2+ # contains private key of that wallet and public keys of others
    

    achow101 commented at 7:09 pm on September 9, 2025:
    Done
  200. in test/functional/wallet_musig.py:101 in d23116987d outdated
    78+                "timestamp": "now",
    79+            })
    80+
    81+            res = wallet.importdescriptors(import_descs)
    82+            for r in res:
    83+                assert_equal(r["success"], True)
    


    rkrux commented at 1:05 pm on September 8, 2025:

    In https://github.com/bitcoin/bitcoin/commit/d23116987d19587746d392895c06f5c426c1d0d2 “test: Test MuSig2 in the wallet”

    Not sure if we have ever asserted on warnings before but the expectation here is that (n-1) warnings of type “not all private …” would be returned for (n) keys in the musig descriptor - though the following asserts using any only.

    0-            for r in res:
    1-                assert_equal(r["success"], True)
    2+            assert_equal(len(res), 1)
    3+            assert_equal(res[0]["success"], True)
    4+            assert_greater_than(len(res[0]["warnings"]), 0)
    5+            assert_equal(any("Not all private keys provided" in warning for warning in res[0]["warnings"]), True)
    

    achow101 commented at 7:10 pm on September 9, 2025:
    It’s not necessary to check for the warning.
  201. in test/functional/wallet_musig.py:27 in d23116987d outdated
    22+        self.num_nodes = 1
    23+
    24+    def skip_test_if_missing_module(self):
    25+        self.skip_if_no_wallet()
    26+
    27+    def do_test(self, comment, pattern, sighash_type=None):
    


    rkrux commented at 1:48 pm on September 8, 2025:

    In https://github.com/bitcoin/bitcoin/commit/d23116987d19587746d392895c06f5c426c1d0d2 “test: Test MuSig2 in the wallet”

    This is a pretty packed function and slightly difficult to follow - can consider splitting this function into 2 because it can be noticed that first portion is preparing the wallets for spending and the second portion is spending from the musig address while asserting on the relevant data.

     0diff --git a/test/functional/wallet_musig.py b/test/functional/wallet_musig.py
     1index b7f3cc9d96..554c09b31d 100755
     2--- a/test/functional/wallet_musig.py
     3+++ b/test/functional/wallet_musig.py
     4@@ -24,21 +24,18 @@ class WalletMuSigTest(BitcoinTestFramework):
     5     def skip_test_if_missing_module(self):
     6         self.skip_if_no_wallet()
     7 
     8-    def do_test(self, comment, pattern, sighash_type=None):
     9-        self.log.info(f"Testing {comment}")
    10-        def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
    11-        has_int = "<" in pattern and ">" in pattern
    12-
    13+    # Figure out how many wallets are needed and create them
    14+    def prepare_musig_wallets_for_spending(self, pattern):
    15         wallets = []
    16         keys = []
    17+        exp_key_leaf = 0
    18 
    19-        pat = pattern.replace("$H", H_POINT)
    20+        pattern = pattern.replace("$H", H_POINT)
    21+        has_int = "<" in pattern and ">" in pattern
    22 
    23-        # Figure out how many wallets are needed and create them
    24-        exp_key_leaf = 0
    25         for i in range(10):
    26-            if f"${i}" in pat:
    27-                exp_key_leaf += pat.count(f"${i}")
    28+            if f"${i}" in pattern:
    29+                exp_key_leaf += pattern.count(f"${i}")
    30                 wallet_name = f"musig_{self.WALLET_NUM}"
    31                 self.WALLET_NUM += 1
    32                 self.nodes[0].createwallet(wallet_name)
    33@@ -64,7 +61,7 @@ class WalletMuSigTest(BitcoinTestFramework):
    34 
    35         # Construct and import each wallet's musig descriptor
    36         for i, wallet in enumerate(wallets):
    37-            desc = pat
    38+            desc = pattern
    39             import_descs = []
    40             for j, (priv, pub) in enumerate(keys):
    41                 if j == i:
    42@@ -97,13 +94,18 @@ class WalletMuSigTest(BitcoinTestFramework):
    43                     assert_equal(change_addr, wallet.getrawchangeaddress(address_type="bech32m"))
    44 
    45         # Fund that address
    46-        def_wallet.sendtoaddress(addr, 10)
    47+        self.def_wallet.sendtoaddress(addr, 10)
    48         self.generate(self.nodes[0], 1)
    49 
    50         # Spend that UTXO
    51         utxo = wallets[0].listunspent()[0]
    52-        psbt = wallets[0].send(outputs=[{def_wallet.getnewaddress(): 5}], inputs=[utxo], change_type="bech32m")["psbt"]
    53 
    54+        return (wallets, keys, exp_key_leaf, utxo)
    55+
    56+    def spend_from_musig_address(self, pattern, sighash_type, musig_spending_data):
    57+        wallets, keys, exp_key_leaf, utxo = musig_spending_data
    58+
    59+        psbt = wallets[0].send(outputs=[{self.def_wallet.getnewaddress(): 5}], inputs=[utxo], change_type="bech32m")["psbt"]
    60         dec_psbt = self.nodes[0].decodepsbt(psbt)
    61         assert_equal(len(dec_psbt["inputs"]), 1)
    62         assert_equal(len(dec_psbt["inputs"][0]["musig2_participant_pubkeys"]), pattern.count("musig("))
    63@@ -175,7 +177,13 @@ class WalletMuSigTest(BitcoinTestFramework):
    64         assert "hex" in finalized
    65         self.nodes[0].sendrawtransaction(finalized["hex"])
    66 
    67+    def do_test(self, comment, pattern, sighash_type=None):
    68+        self.log.info(f"Testing {comment}")
    69+        musig_spending_data = self.prepare_musig_wallets_for_spending(pattern)
    70+        self.spend_from_musig_address(pattern, sighash_type, musig_spending_data)
    71+
    72     def run_test(self):
    73+        self.def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
    74         self.do_test("rawtr(musig(keys/*))", "rawtr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))")
    75         self.do_test("rawtr(musig(keys/*)) with ALL|ANYONECANPAY", "rawtr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))", "ALL|ANYONECANPAY")
    76         self.do_test("tr(musig(keys/*))", "tr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))")
    

    achow101 commented at 7:10 pm on September 9, 2025:
    I don’t think refactoring like this is helpful as the resulting functions won’t be called by anything else anyways.

    rkrux commented at 2:41 pm on September 10, 2025:

    It was mostly to put the related code in a block, not for reusability; calling the inline sub-functions is an alternate imho.

    0def do_test(self, ...):
    1 self.log.info(f"Testing {comment}")
    2 def prepare_musig_wallets_for_spending(...):
    3  ...
    4
    5 def spend_from_musig_address(...):
    6  ...
    7 
    8 musig_spending_data = prepare_musig_wallets_for_spending()
    9 spend_from_musig_address(musig_spending_data)
    
  202. in test/functional/wallet_musig.py:36 in d23116987d outdated
    28+        self.log.info(f"Testing {comment}")
    29+        def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
    30+        has_int = "<" in pattern and ">" in pattern
    31+
    32+        wallets = []
    33+        keys = []
    


    rkrux commented at 1:52 pm on September 8, 2025:

    In https://github.com/bitcoin/bitcoin/commit/d23116987d19587746d392895c06f5c426c1d0d2 “test: Test MuSig2 in the wallet”

    Nit for verbosity

    0- keys = []
    1+ musig_participant_key_pairs = [] 
    

    achow101 commented at 7:10 pm on September 9, 2025:
    I don’t think this needs to be more verbose.
  203. in test/functional/wallet_musig.py:115 in d23116987d outdated
    110+
    111+        # Retrieve all participant pubkeys
    112+        part_pks = set()
    113+        for agg in dec_psbt["inputs"][0]["musig2_participant_pubkeys"]:
    114+            for part_pub in agg["participant_pubkeys"]:
    115+                part_pks.add(part_pub[2:])
    


    rkrux commented at 2:51 pm on September 8, 2025:

    In https://github.com/bitcoin/bitcoin/commit/d23116987d19587746d392895c06f5c426c1d0d2 “test: Test MuSig2 in the wallet”

    Is this extracting the x-only version of the key before the set insertion? Can consider creating a small util function in key.py in the framework. It’s used thrice in this file already.


    achow101 commented at 7:11 pm on September 9, 2025:
    The behavior is simple enough that I don’t think a separate function will make this better.
  204. in test/functional/wallet_musig.py:39 in d23116987d outdated
    34+
    35+        pat = pattern.replace("$H", H_POINT)
    36+
    37+        # Figure out how many wallets are needed and create them
    38+        exp_key_leaf = 0
    39+        for i in range(10):
    


    rkrux commented at 2:57 pm on September 8, 2025:

    In https://github.com/bitcoin/bitcoin/commit/d23116987d19587746d392895c06f5c426c1d0d2 “test: Test MuSig2 in the wallet”

    This 10 is arbitrary, can put it inside a named constant.

    0- for i in range(10):
    1+ for i in range(MAX_UNIQUE_PARTICIPANT_KEYS_IN_TEST):
    

    achow101 commented at 7:11 pm on September 9, 2025:
    Changed to figure out how many wallets are needed automatically.
  205. rkrux commented at 2:57 pm on September 8, 2025: contributor

    Code review 1 - d23116987d19587746d392895c06f5c426c1d0d2

    Reviewed the functional test.

  206. rkrux commented at 3:31 pm on September 8, 2025: contributor

    In 8849f574104d920c1629e756c5495e1d2c523844 “sign: Create MuSig2 signatures for known MuSig2 aggregate keys”

    In commit message:

    Lastly, if the partial signatures could be created, add our own public nonces for the private keys that we know, if they do not yet exist.

    Is is supposed to say “… partial signatures could not be created …” instead? Based on the tone in the message used.

  207. in test/functional/wallet_musig.py:239 in d23116987d outdated
    183+        self.do_test("tr(musig/*)", "tr(musig($0,$1,$2)/<0;1>/*)")
    184+        self.do_test("tr(H, pk(musig(keys/*)))", "tr($H,pk(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*)))")
    185+        self.do_test("tr(H,pk(musig/*))", "tr($H,pk(musig($0,$1,$2)/<0;1>/*))")
    186+        self.do_test("tr(H,{pk(musig/*), pk(musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($3,$4,$5)/0/*)})")
    187+        self.do_test("tr(H,{pk(musig/*), pk(same keys different musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($1,$2)/0/*)})")
    188+
    


    rkrux commented at 12:54 pm on September 9, 2025:

    In d23116987d19587746d392895c06f5c426c1d0d2 “test: Test MuSig2 in the wallet”

    Can also add the below test case that works right away where the musig is in both the key path spend and the script path spend - KP has all 3 keys in the musig, SP scripts have 2 partial keys in their musig each.

    0self.do_test("tr(musig/*,{pk(partial keys diff musig-1/*),pk(partial keys diff musig-2/*)})}", "tr(musig($0,$1,$2)/<3;4>/*,{pk(musig($0,$1)/<5;6>/*),pk(musig($1,$2)/7/*)})")
    

    achow101 commented at 7:11 pm on September 9, 2025:
    Done
  208. in test/functional/wallet_musig.py:173 in d23116987d outdated
    168+                    assert False, "Aggregate pubkey for partial sig not seen as output key, internal key, or in any scripts"
    169+            else:
    170+                assert False, "Aggregate pubkey for partial sig not seen as output key or internal key"
    171+
    172+        # Non-participant aggregates partial sigs and send
    173+        finalized = self.nodes[0].finalizepsbt(comb_psig_psbt)
    


    rkrux commented at 1:50 pm on September 9, 2025:

    In https://github.com/bitcoin/bitcoin/commit/d23116987d19587746d392895c06f5c426c1d0d2 “test: Test MuSig2 in the wallet”

    With the addition of the case where musig is both in keypath and spendpath, I think it would be nice to assert which spending path is triggered - even though the current test setup ensures that the PSBT signing goes through all the signers in which case keypath spending would be preferred always.

     0diff --git a/test/functional/wallet_musig.py b/test/functional/wallet_musig.py
     1index b7f3cc9d96..63276b1eb7 100755
     2--- a/test/functional/wallet_musig.py
     3+++ b/test/functional/wallet_musig.py
     4@@ -8,7 +8,7 @@ import re
     5 from test_framework.descriptors import descsum_create
     6 from test_framework.key import H_POINT
     7 from test_framework.test_framework import BitcoinTestFramework
     8-from test_framework.util import assert_equal
     9+from test_framework.util import assert_equal, assert_greater_than
    10 
    11 PRIVKEY_RE = re.compile(r"^tr\((.+?)/.+\)#.{8}$")
    12 PUBKEY_RE = re.compile(r"^tr\((\[.+?\].+?)/.+\)#.{8}$")
    13@@ -24,7 +24,7 @@ class WalletMuSigTest(BitcoinTestFramework):
    14     def skip_test_if_missing_module(self):
    15         self.skip_if_no_wallet()
    16 
    17-    def do_test(self, comment, pattern, sighash_type=None):
    18+    def do_test(self, comment, pattern, sighash_type=None, spending_path="keypath"):
    19         self.log.info(f"Testing {comment}")
    20         def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
    21         has_int = "<" in pattern and ">" in pattern
    22@@ -170,7 +170,14 @@ class WalletMuSigTest(BitcoinTestFramework):
    23                 assert False, "Aggregate pubkey for partial sig not seen as output key or internal key"
    24 
    25         # Non-participant aggregates partial sigs and send
    26-        finalized = self.nodes[0].finalizepsbt(comb_psig_psbt)
    27+        finalized = self.nodes[0].finalizepsbt(comb_psig_psbt, False)
    28+        assert_equal(finalized["complete"], True)
    29+        final_input_witness = self.nodes[0].decodepsbt(finalized["psbt"])["inputs"][0]["final_scriptwitness"]
    30+        if spending_path == "scriptpath":
    31+            assert_greater_than(len(final_input_witness), 1)
    32+        else:
    33+            assert_equal(len(final_input_witness), 1)
    34+        finalized = self.nodes[0].finalizepsbt(finalized["psbt"])
    35         assert_equal(finalized["complete"], True)
    36         assert "hex" in finalized
    37         self.nodes[0].sendrawtransaction(finalized["hex"])
    38@@ -181,10 +188,11 @@ class WalletMuSigTest(BitcoinTestFramework):
    39         self.do_test("tr(musig(keys/*))", "tr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))")
    40         self.do_test("rawtr(musig/*)", "rawtr(musig($0,$1,$2)/<0;1>/*)")
    41         self.do_test("tr(musig/*)", "tr(musig($0,$1,$2)/<0;1>/*)")
    42-        self.do_test("tr(H, pk(musig(keys/*)))", "tr($H,pk(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*)))")
    43-        self.do_test("tr(H,pk(musig/*))", "tr($H,pk(musig($0,$1,$2)/<0;1>/*))")
    44-        self.do_test("tr(H,{pk(musig/*), pk(musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($3,$4,$5)/0/*)})")
    45-        self.do_test("tr(H,{pk(musig/*), pk(same keys different musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($1,$2)/0/*)})")
    46+        self.do_test("tr(H, pk(musig(keys/*)))", "tr($H,pk(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*)))", None, "scriptpath")
    47+        self.do_test("tr(H,pk(musig/*))", "tr($H,pk(musig($0,$1,$2)/<0;1>/*))", None, "scriptpath")
    48+        self.do_test("tr(H,{pk(musig/*), pk(musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($3,$4,$5)/0/*)})", None, "scriptpath")
    49+        self.do_test("tr(H,{pk(musig/*), pk(same keys different musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($1,$2)/0/*)})", None, "scriptpath")
    50+        self.do_test("tr(musig/*,{pk(partial keys diff musig-1/*),pk(partial keys diff musig-2/*)})}", "tr(musig($0,$1,$2)/<3;4>/*,{pk(musig($0,$1)/<5;6>/*),pk(musig($1,$2)/7/*)})", None, "keypath")
    51 
    52 
    53 if __name__ == '__main__':
    

    achow101 commented at 7:11 pm on September 9, 2025:
    Done
  209. achow101 force-pushed on Sep 9, 2025
  210. achow101 force-pushed on Sep 9, 2025
  211. achow101 commented at 7:12 pm on September 9, 2025: member

    Is is supposed to say “… partial signatures could not be created …” instead? Based on the tone in the message used.

    Yes, done.

  212. Sjors commented at 6:40 am on September 10, 2025: member
    The “Unknown named parameter” ci failure might be a case of #33230 / #32821?
  213. in src/script/sign.cpp:110 in 6b01d7379b outdated
    101@@ -100,6 +102,33 @@ bool MutableTransactionSignatureCreator::CreateSchnorrSig(const SigningProvider&
    102     return true;
    103 }
    104 
    105+std::vector<uint8_t> MutableTransactionSignatureCreator::CreateMuSig2Nonce(const SigningProvider& provider, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion, const SignatureData& sigdata) const
    106+{
    107+    assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT);
    108+
    109+    // Retrieve the private key
    110+    CKey key;
    


    rkrux commented at 1:39 pm on September 10, 2025:

    In 6b01d7379b9e29760abd7b549a04db43937a7b8d “sign: Add CreateMuSig2Nonce”

    Nit: Reordering to defer getting the private key.

     0diff --git a/src/script/sign.cpp b/src/script/sign.cpp
     1index b183be8939..d208f44626 100644
     2--- a/src/script/sign.cpp
     3+++ b/src/script/sign.cpp
     4@@ -106,10 +106,6 @@ std::vector<uint8_t> MutableTransactionSignatureCreator::CreateMuSig2Nonce(const
     5 {
     6     assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT);
     7 
     8-    // Retrieve the private key
     9-    CKey key;
    10-    if (!provider.GetKey(part_pubkey.GetID(), key)) return {};
    11-
    12     // Retrieve participant pubkeys
    13     std::vector<CPubKey> pubkeys = provider.GetMuSig2ParticipantPubkeys(aggregate_pubkey);
    14     if (!pubkeys.size()) return {};
    15@@ -119,6 +115,10 @@ std::vector<uint8_t> MutableTransactionSignatureCreator::CreateMuSig2Nonce(const
    16     std::optional<uint256> sighash = ComputeSchnorrSignatureHash(leaf_hash, sigversion);
    17     if (!sighash.has_value()) return {};
    18 
    19+    // Retrieve the private key
    20+    CKey key;
    21+    if (!provider.GetKey(part_pubkey.GetID(), key)) return {};
    22+
    23     MuSig2SecNonce secnonce;
    24     std::vector<uint8_t> out = key.CreateMuSig2Nonce(secnonce, *sighash, aggregate_pubkey, pubkeys);
    25     if (out.empty()) return {};
    

    achow101 commented at 9:10 pm on September 11, 2025:
    We want to get the private key early so that we know whether we will be able to even do anything here.
  214. in src/script/sign.h:105 in 10b7148efa outdated
     96@@ -95,6 +97,8 @@ struct SignatureData {
     97     std::map<std::vector<uint8_t>, std::vector<uint8_t>> hash160_preimages; ///< Mapping from a HASH160 hash to its preimage provided to solve a Script
     98     //! Mapping from pair of MuSig2 aggregate pubkey, and tapleaf hash to map of MuSig2 participant pubkeys to MuSig2 public nonce
     99     std::map<std::pair<CPubKey, uint256>, std::map<CPubKey, std::vector<uint8_t>>> musig2_pubnonces;
    100+    //! Mapping from pair of MuSig2 aggregate pubkey, and tapleaf hash to map of MuSig2 participant pubkeys to MuSig2 partial signature
    101+    std::map<std::pair<CPubKey, uint256>, std::map<CPubKey, uint256>> musig2_partial_sigs;
    


    rkrux commented at 2:01 pm on September 10, 2025:

    In 6b01d7379b9e29760abd7b549a04db43937a7b8d “sign: Add CreateMuSig2Nonce” In 10b7148efa754169cc625e93c98fa54f7b375e5d “sign: Add CreateMuSig2PartialSig”

    To avoid reading these very long types multiple times, also to avoid duplication. An alternative is to have these 3 objects in a struct that could be used in psbt.h and sign.h, but that’d increase the diff.

     0diff --git a/src/musig.h b/src/musig.h
     1index 95f495a40a..020fdc09c3 100644
     2--- a/src/musig.h
     3+++ b/src/musig.h
     4@@ -14,6 +14,13 @@ struct secp256k1_musig_keyagg_cache;
     5 class MuSig2SecNonceImpl;
     6 struct secp256k1_musig_secnonce;
     7 
     8+//! Map MuSig2 aggregate pubkeys to its participants
     9+using MuSig2Participants = std::map<CPubKey, std::vector<CPubKey>>;
    10+//! Mapping from pair of MuSig2 aggregate pubkey, and tapleaf hash to map of MuSig2 participant pubkeys to MuSig2 public nonce
    11+using MuSig2Pubnonces = std::map<std::pair<CPubKey, uint256>, std::map<CPubKey, std::vector<uint8_t>>>;
    12+//! Mapping from pair of MuSig2 aggregate pubkey, and tapleaf hash to map of MuSig2 participant pubkeys to MuSig2 partial signature
    13+using MuSig2PartialSigs = std::map<std::pair<CPubKey, uint256>, std::map<CPubKey, uint256>>;
    14+
    15 //! MuSig2 chaincode as defined by BIP 328
    16 using namespace util::hex_literals;
    17 constexpr uint256 MUSIG_CHAINCODE{"868087ca02a6f974c4598924c36b57762d32cb45717167e300622c7167e38965"_hex_u8};
    18diff --git a/src/psbt.h b/src/psbt.h
    19index f8098b0450..5851caf5a3 100644
    20--- a/src/psbt.h
    21+++ b/src/psbt.h
    22@@ -15,6 +15,7 @@
    23 #include <script/signingprovider.h>
    24 #include <span.h>
    25 #include <streams.h>
    26+#include <musig.h>
    27 
    28 #include <optional>
    29 
    30@@ -267,12 +268,9 @@ struct PSBTInput
    31     XOnlyPubKey m_tap_internal_key;
    32     uint256 m_tap_merkle_root;
    33 
    34-    // MuSig2 fields
    35-    std::map<CPubKey, std::vector<CPubKey>> m_musig2_participants;
    36-    // Key is the aggregate pubkey and the script leaf hash, value is a map of participant pubkey to pubnonce
    37-    std::map<std::pair<CPubKey, uint256>, std::map<CPubKey, std::vector<uint8_t>>> m_musig2_pubnonces;
    38-    // Key is the aggregate pubkey and the script leaf hash, value is a map of participant pubkey to partial_sig
    39-    std::map<std::pair<CPubKey, uint256>, std::map<CPubKey, uint256>> m_musig2_partial_sigs;
    40+    MuSig2Participants m_musig2_participants;
    41+    MuSig2Pubnonces m_musig2_pubnonces;
    42+    MuSig2PartialSigs m_musig2_partial_sigs;
    43 
    44     std::map<std::vector<unsigned char>, std::vector<unsigned char>> unknown;
    45     std::set<PSBTProprietary> m_proprietary;
    46diff --git a/src/script/sign.h b/src/script/sign.h
    47index dd86a8066a..a9d09fa190 100644
    48--- a/src/script/sign.h
    49+++ b/src/script/sign.h
    50@@ -14,6 +14,7 @@
    51 #include <script/keyorigin.h>
    52 #include <script/signingprovider.h>
    53 #include <uint256.h>
    54+#include <musig.h>
    55 
    56 class CKey;
    57 class CKeyID;
    58@@ -97,12 +98,9 @@ struct SignatureData {
    59     std::map<std::vector<uint8_t>, std::vector<uint8_t>> hash256_preimages; ///< Mapping from a HASH256 hash to its preimage provided to solve a Script
    60     std::map<std::vector<uint8_t>, std::vector<uint8_t>> ripemd160_preimages; ///< Mapping from a RIPEMD160 hash to its preimage provided to solve a Script
    61     std::map<std::vector<uint8_t>, std::vector<uint8_t>> hash160_preimages; ///< Mapping from a HASH160 hash to its preimage provided to solve a Script
    62-    //! Map MuSig2 aggregate pubkeys to its participants
    63-    std::map<CPubKey, std::vector<CPubKey>> musig2_pubkeys;
    64-    //! Mapping from pair of MuSig2 aggregate pubkey, and tapleaf hash to map of MuSig2 participant pubkeys to MuSig2 public nonce
    65-    std::map<std::pair<CPubKey, uint256>, std::map<CPubKey, std::vector<uint8_t>>> musig2_pubnonces;
    66-    //! Mapping from pair of MuSig2 aggregate pubkey, and tapleaf hash to map of MuSig2 participant pubkeys to MuSig2 partial signature
    67-    std::map<std::pair<CPubKey, uint256>, std::map<CPubKey, uint256>> musig2_partial_sigs;
    68+    MuSig2Participants musig2_pubkeys;
    69+    MuSig2Pubnonces musig2_pubnonces;
    70+    MuSig2PartialSigs musig2_partial_sigs;
    71 
    72     SignatureData() = default;
    73     explicit SignatureData(const CScript& script) : scriptSig(script) {}
    

    achow101 commented at 9:22 pm on September 11, 2025:
    I actually prefer verbosity in type names.
  215. in src/script/sign.cpp:603 in 7c085554dc outdated
    601+                            CExtPubKey extpub;
    602+                            extpub.nDepth = 0;
    603+                            std::memset(extpub.vchFingerprint, 0, 4);
    604+                            extpub.nChild = 0;
    605+                            extpub.chaincode = MUSIG_CHAINCODE;
    606+                            extpub.pubkey = agg_pub;
    


    rkrux commented at 2:27 pm on September 10, 2025:

    In 7c085554dce336eb1597ab2fc482163876a49270 “sign: Create MuSig2 signatures for known MuSig2 aggregate keys”

    Easy candidate to deduplicate and improve readability.

     0diff --git a/src/musig.cpp b/src/musig.cpp
     1index 28de6dc819..d5da34feb7 100644
     2--- a/src/musig.cpp
     3+++ b/src/musig.cpp
     4@@ -61,6 +61,16 @@ std::optional<CPubKey> MuSig2AggregatePubkeys(const std::vector<CPubKey>& pubkey
     5     return MuSig2AggregatePubkeys(pubkeys, keyagg_cache, std::nullopt);
     6 }
     7 
     8+CExtPubKey CreateMuSig2SyntheticXpub(CPubKey agg_pub) {
     9+    CExtPubKey extpub;
    10+    extpub.nDepth = 0;
    11+    std::memset(extpub.vchFingerprint, 0, 4);
    12+    extpub.nChild = 0;
    13+    extpub.chaincode = MUSIG_CHAINCODE;
    14+    extpub.pubkey = agg_pub;
    15+    return extpub;
    16+}
    17+
    18 class MuSig2SecNonceImpl
    19 {
    20 private:
    21diff --git a/src/musig.h b/src/musig.h
    22index 95f495a40a..d01865a97f 100644
    23--- a/src/musig.h
    24+++ b/src/musig.h
    25@@ -59,4 +66,5 @@ uint256 MuSig2SessionID(const CPubKey& script_pubkey, const CPubKey& part_pubkey
    26 
    27 std::optional<std::vector<uint8_t>> CreateMuSig2AggregateSig(const std::vector<CPubKey>& participants, const CPubKey& aggregate_pubkey, const std::vector<std::pair<uint256, bool>>& tweaks, const uint256& sighash, const std::map<CPubKey, std::vector<uint8_t>>& pubnonces, const std::map<CPubKey, uint256>& partial_sigs);
    28 
    29+CExtPubKey CreateMuSig2SyntheticXpub(CPubKey);
    30 #endif // BITCOIN_MUSIG_H
    31diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp
    32index 3a40270217..5da24f61eb 100644
    33--- a/src/script/descriptor.cpp
    34+++ b/src/script/descriptor.cpp
    35@@ -640,14 +640,7 @@ public:
    36 
    37             // Make our pubkey provider
    38             if (IsRangedDerivation() || !m_path.empty()) {
    39-                // Make the synthetic xpub and construct the BIP32PubkeyProvider
    40-                CExtPubKey extpub;
    41-                extpub.nDepth = 0;
    42-                std::memset(extpub.vchFingerprint, 0, 4);
    43-                extpub.nChild = 0;
    44-                extpub.chaincode = MUSIG_CHAINCODE;
    45-                extpub.pubkey = m_aggregate_pubkey.value();
    46-
    47+                CExtPubKey extpub = CreateMuSig2SyntheticXpub(m_aggregate_pubkey.value());
    48                 m_aggregate_provider = std::make_unique<BIP32PubkeyProvider>(m_expr_index, extpub, m_path, m_derive, /*apostrophe=*/false);
    49             } else {
    50                 m_aggregate_provider = std::make_unique<ConstPubkeyProvider>(m_expr_index, m_aggregate_pubkey.value(), /*xonly=*/false);
    51diff --git a/src/script/sign.cpp b/src/script/sign.cpp
    52index b183be8939..0b04c19b4f 100644
    53--- a/src/script/sign.cpp
    54+++ b/src/script/sign.cpp
    55@@ -316,12 +316,7 @@ static bool CreateTaprootScriptSig(const BaseSignatureCreator& creator, Signatur
    56                         continue;
    57                     }
    58                     // Get the BIP32 derivation tweaks
    59-                    CExtPubKey extpub;
    60-                    extpub.nDepth = 0;
    61-                    std::memset(extpub.vchFingerprint, 0, 4);
    62-                    extpub.nChild = 0;
    63-                    extpub.chaincode = MUSIG_CHAINCODE;
    64-                    extpub.pubkey = agg_pub;
    65+                    CExtPubKey extpub = CreateMuSig2SyntheticXpub(agg_pub);
    66                     for (const int i : info.path) {
    67                         auto& [tweak, xonly] = tweaks.emplace_back();
    68                         xonly = false;
    69@@ -595,12 +590,7 @@ static bool SignTaproot(const SigningProvider& provider, const BaseSignatureCrea
    70                                 continue;
    71                             }
    72                             // Get the BIP32 derivation tweaks
    73-                            CExtPubKey extpub;
    74-                            extpub.nDepth = 0;
    75-                            std::memset(extpub.vchFingerprint, 0, 4);
    76-                            extpub.nChild = 0;
    77-                            extpub.chaincode = MUSIG_CHAINCODE;
    78-                            extpub.pubkey = agg_pub;
    79+                            CExtPubKey extpub = CreateMuSig2SyntheticXpub(agg_pub);
    80                             for (const int i : info.path) {
    81                                 auto& [t, xonly] = tweaks.emplace_back();
    82                                 xonly = false;
    

    achow101 commented at 9:26 pm on September 11, 2025:
    For a followup or if I need to retouch.

    achow101 commented at 11:19 pm on September 16, 2025:
    Done
  216. rkrux commented at 2:33 pm on September 10, 2025: contributor

    Partial Code review 2 - a06017dfce7ce72afbebe6f68d9a29cf72d26593

    wallet_musig.py & descriptor_tests.cpp tests pass with these suggestions.

  217. rkrux commented at 3:19 pm on September 10, 2025: contributor

    In bff8e053f4e6de3ce39e87a4faea161b1b84d850 “pubkey: Return tweaks from BIP32 derivation”

    It would be helpful to mention why this is done in the commit message - specifically to mention around its usage in the “sign: Create MuSig2 signatures for known MuSig2 aggregate keys” commit pertaining to BIP 328/327.

  218. achow101 commented at 9:27 pm on September 11, 2025: member

    It would be helpful to mention why this is done in the commit message

    If I need to retouch.

  219. in src/key.cpp:459 in 10b7148efa outdated
    450+    // Create partial signature
    451+    secp256k1_musig_partial_sig psig;
    452+    if (!secp256k1_musig_partial_sign(secp256k1_context_sign, &psig, secnonce.Get(), &keypair, &keyagg_cache, &session)) {
    453+        return std::nullopt;
    454+    }
    455+    secnonce.Invalidate();
    


    rkrux commented at 8:19 am on September 15, 2025:

    In 10b7148efa754169cc625e93c98fa54f7b375e5d “sign: Add CreateMuSig2PartialSig”

     0diff --git a/src/key.cpp b/src/key.cpp
     1index 005a913236..c312f0713f 100644
     2--- a/src/key.cpp
     3+++ b/src/key.cpp
     4@@ -373,7 +373,7 @@ std::vector<uint8_t> CKey::CreateMuSig2Nonce(MuSig2SecNonce& secnonce, const uin
     5         return {};
     6     }
     7 
     8-    // Serialize nonce
     9+    // Serialize pubnonce
    10     std::vector<uint8_t> out;
    11     out.resize(66);
    12     if (!secp256k1_musig_pubnonce_serialize(secp256k1_context_sign, out.data(), &pubnonce)) {
    13@@ -452,6 +452,7 @@ std::optional<uint256> CKey::CreateMuSig2PartialSig(const uint256& hash, const C
    14     if (!secp256k1_musig_partial_sign(secp256k1_context_sign, &psig, secnonce.Get(), &keypair, &keyagg_cache, &session)) {
    15         return std::nullopt;
    16     }
    17+    // Mandatorily delete the secnonce after signing to prohibit nonce reuse!
    18     secnonce.Invalidate();
    19 
    20     // Verify partial signature
    

    achow101 commented at 11:20 pm on September 16, 2025:
    Done
  220. in src/script/sign.cpp:172 in 10b7148efa outdated
    164+    std::optional<uint256> sig = key.CreateMuSig2PartialSig(*sighash, aggregate_pubkey, pubkeys, pubnonces, *secnonce, tweaks);
    165+    if (!sig) return false;
    166+    partial_sig = std::move(*sig);
    167+
    168+    // Delete the secnonce now that we're done with it
    169+    provider.DeleteMuSig2Session(session_id);
    


    rkrux commented at 8:41 am on September 15, 2025:

    In https://github.com/bitcoin/bitcoin/commit/10b7148efa754169cc625e93c98fa54f7b375e5d “sign: Add CreateMuSig2PartialSig”

    We can prefer to fail the process if the secnonce was not deleted for some reason.

    0@@ -166,6 +166,7 @@ bool MutableTransactionSignatureCreator::CreateMuSig2PartialSig(const SigningPro
    1     partial_sig = std::move(*sig);
    2 
    3     // Delete the secnonce now that we're done with it
    4+    assert(!secnonce->get().IsValid());
    5     provider.DeleteMuSig2Session(session_id);
    6 
    7     return true;
    

    achow101 commented at 11:20 pm on September 16, 2025:
    Done
  221. in src/key.cpp:378 in 6b01d7379b outdated
    373+        return {};
    374+    }
    375+
    376+    // Serialize nonce
    377+    std::vector<uint8_t> out;
    378+    out.resize(66);
    


    rkrux commented at 8:55 am on September 15, 2025:

    In 6b01d7379b9e29760abd7b549a04db43937a7b8d “sign: Add CreateMuSig2Nonce”

    Can consider creating the constant now in the musig.h dedicated file, even though there is no pubnonce object yet - ref #31247 (review)

    0-    // Serialize nonce
    1+    // Serialize pubnonce
    2     std::vector<uint8_t> out;
    3-    out.resize(66);
    4+    out.resize(MUSIG2_PUBNONCE_SIZE);
    

    achow101 commented at 11:20 pm on September 16, 2025:
    Done
  222. in src/key.cpp:404 in 10b7148efa outdated
    399+    CPubKey our_pubkey = GetPubKey();
    400+    for (const CPubKey& part_pk : pubkeys) {
    401+        const auto& pn_it = pubnonces.find(part_pk);
    402+        if (pn_it == pubnonces.end()) return std::nullopt;
    403+        const std::vector<uint8_t> pubnonce = pn_it->second;
    404+        if (pubnonce.size() != 66) return std::nullopt;
    


    rkrux commented at 9:04 am on September 15, 2025:

    In 10b7148efa754169cc625e93c98fa54f7b375e5d “sign: Add CreateMuSig2PartialSig”

    I believe by using a fixed size array for pubnonce as mentioned in an earlier comment can avoid the need for this kind of check.

  223. in src/musig.h:46 in 645fcaa831 outdated
    37+ * copyable to avoid nonce reuse.
    38+*/
    39+class MuSig2SecNonce
    40+{
    41+private:
    42+    std::unique_ptr<MuSig2SecNonceImpl> m_impl;
    


    rkrux commented at 9:14 am on September 15, 2025:

    In 645fcaa83108e6a0faed2c49c72ef710f0231407 “Add MuSig2SecNonce class for secure allocation of musig nonces”

    While not opposed to the idea of encapsulating MuSig2SecNonceImpl inside MuSig2SecNonce, I don’t fully understand the need for it. Both the classes have copy constructors deleted and the Get, Invalidate, IsValid functions directly call the corresponding functions of the MuSig2SecNonceImpl impl class without any other code in between.

    What would be the downside of having only one MuSig2SecNonce class with the private secp256k1_musig_secnonce inside it?


    achow101 commented at 9:18 pm on September 16, 2025:
    It’s to avoid having to include and link the libsecp module in a bunch of irrelevant places.

    rkrux commented at 1:42 pm on September 17, 2025:
    Thanks, this reason can be mentioned explicitly in the function doc above.
  224. in src/key.cpp:400 in 10b7148efa outdated
    392+    secp256k1_musig_keyagg_cache keyagg_cache;
    393+    if (!MuSig2AggregatePubkeys(pubkeys, keyagg_cache, aggregate_pubkey)) return std::nullopt;
    394+
    395+    // Parse the pubnonces
    396+    std::vector<std::pair<secp256k1_pubkey, secp256k1_musig_pubnonce>> signers_data;
    397+    std::vector<const secp256k1_musig_pubnonce*> pubnonce_ptrs;
    


    rkrux commented at 9:50 am on September 15, 2025:

    In https://github.com/bitcoin/bitcoin/commit/10b7148efa754169cc625e93c98fa54f7b375e5d “sign: Add CreateMuSig2PartialSig”

    Might be opinionated: I think having a struct for the pubnonce can add some structure in the overall MuSig signing code. The 66 size checks spread across CreateMuSig2PartialSig & CreateMuSig2AggregateSig functions seem distracting and IMO can go inside the constructor of the struct that internally can use secp256k1_musig_pubnonce. At the moment, treatment of pubnonce seems quite barebones compared to the corresponding SecNonce.


    achow101 commented at 9:17 pm on September 16, 2025:
    We want to avoid libsecp includes and linking being everywhere throughout the codebase that a pubnonce might appear, so we prefer to use something that contains the serialized nonce, and deserializing it into secp256k1_musig_pubnonce when needed.
  225. in test/functional/wallet_musig.py:239 in a06017dfce outdated
    209+        self.do_test("tr(H, pk(musig(keys/*)))", "tr($H,pk(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*)))", scriptpath=True)
    210+        self.do_test("tr(H,pk(musig/*))", "tr($H,pk(musig($0,$1,$2)/<0;1>/*))", scriptpath=True)
    211+        self.do_test("tr(H,{pk(musig/*), pk(musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($3,$4,$5)/0/*)})", scriptpath=True)
    212+        self.do_test("tr(H,{pk(musig/*), pk(same keys different musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($1,$2)/0/*)})", scriptpath=True)
    213+        self.do_test("tr(musig/*,{pk(partial keys diff musig-1/*),pk(partial keys diff musig-2/*)})}", "tr(musig($0,$1,$2)/<3;4>/*,{pk(musig($0,$1)/<5;6>/*),pk(musig($1,$2)/7/*)})")
    214+
    


    rkrux commented at 1:14 pm on September 15, 2025:

    In a06017dfce7ce72afbebe6f68d9a29cf72d26593 “test: Test MuSig2 in the wallet”

    In the SignTaproot function in src/script/sign.cpp, key path spending is tried first and then script path spending that makes the key path spending the de-facto spending path in these tests.

    Consider updating these tests to allow testing for script path spending in case of a valid key path as well.

      0diff --git a/test/functional/wallet_musig.py b/test/functional/wallet_musig.py
      1index 283ba604eb..c45fc6cff3 100755
      2--- a/test/functional/wallet_musig.py
      3+++ b/test/functional/wallet_musig.py
      4@@ -17,7 +17,7 @@ PRIVKEY_RE = re.compile(r"^tr\((.+?)/.+\)#.{8}$")
      5 PUBKEY_RE = re.compile(r"^tr\((\[.+?\].+?)/.+\)#.{8}$")
      6 ORIGIN_PATH_RE = re.compile(r"^\[\w{8}(/.*)\].*$")
      7 MULTIPATH_TWO_RE = re.compile(r"<(\d+);(\d+)>")
      8-
      9+MUSIG_RE = re.compile(r"musig\(.+?\)")
     10 
     11 class WalletMuSigTest(BitcoinTestFramework):
     12     WALLET_NUM = 0
     13@@ -27,7 +27,7 @@ class WalletMuSigTest(BitcoinTestFramework):
     14     def skip_test_if_missing_module(self):
     15         self.skip_if_no_wallet()
     16 
     17-    def do_test(self, comment, pattern, sighash_type=None, scriptpath=False):
     18+    def do_test(self, comment, pattern, sighash_type=None, scriptpath=False, nosign_wallets=None):
     19         self.log.info(f"Testing {comment}")
     20         has_internal = MULTIPATH_TWO_RE.search(pattern) is not None
     21 
     22@@ -36,6 +36,8 @@ class WalletMuSigTest(BitcoinTestFramework):
     23 
     24         pat = pattern.replace("$H", H_POINT)
     25         wallets_needed = pat.count("$")
     26+        musig_patterns = pat.count("musig(")
     27+        all_musig_patterns = set(MUSIG_RE.findall(pat))
     28 
     29         # Figure out how many wallets are needed and create them
     30         expected_key_leaves = 0
     31@@ -115,9 +117,9 @@ class WalletMuSigTest(BitcoinTestFramework):
     32 
     33         dec_psbt = self.nodes[0].decodepsbt(psbt)
     34         assert_equal(len(dec_psbt["inputs"]), 1)
     35-        assert_equal(len(dec_psbt["inputs"][0]["musig2_participant_pubkeys"]), pattern.count("musig("))
     36+        assert_equal(len(dec_psbt["inputs"][0]["musig2_participant_pubkeys"]), musig_patterns)
     37         if has_internal:
     38-            assert_equal(len(dec_psbt["outputs"][1]["musig2_participant_pubkeys"]), pattern.count("musig("))
     39+            assert_equal(len(dec_psbt["outputs"][1]["musig2_participant_pubkeys"]), musig_patterns)
     40 
     41         # Check all participant pubkeys in the input and change output
     42         psbt_maps = [dec_psbt["inputs"][0]]
     43@@ -136,9 +138,31 @@ class WalletMuSigTest(BitcoinTestFramework):
     44                     part_pks.remove(deriv_path["pubkey"])
     45             assert_equal(len(part_pks), 0)
     46 
     47+        signing_wallets = []
     48+        to_sign_key_leaves = 0
     49+        signed_key_leaves = 0
     50+        if nosign_wallets:
     51+            nosign_musig_patterns = set()
     52+            for i, wallet in enumerate(wallets):
     53+                if i in nosign_wallets:
     54+                    for musig_pattern in all_musig_patterns:
     55+                        if f"${i}" in musig_pattern:
     56+                            nosign_musig_patterns.add(musig_pattern)
     57+                else:
     58+                    signing_wallets.append(wallet)
     59+                    to_sign_key_leaves += pat.count(f"${i}")
     60+
     61+            to_sign_musig_patterns = all_musig_patterns - nosign_musig_patterns
     62+            for p in to_sign_musig_patterns:
     63+                signed_key_leaves += p.count("$")
     64+        else:
     65+            signing_wallets = wallets
     66+            to_sign_key_leaves = expected_key_leaves
     67+            signed_key_leaves = expected_key_leaves
     68+
     69         # Add pubnonces
     70         nonce_psbts = []
     71-        for wallet in wallets:
     72+        for wallet in signing_wallets:
     73             proc = wallet.walletprocesspsbt(psbt=psbt, sighashtype=sighash_type)
     74             assert_equal(proc["complete"], False)
     75             nonce_psbts.append(proc["psbt"])
     76@@ -146,7 +170,7 @@ class WalletMuSigTest(BitcoinTestFramework):
     77         comb_nonce_psbt = self.nodes[0].combinepsbt(nonce_psbts)
     78 
     79         dec_psbt = self.nodes[0].decodepsbt(comb_nonce_psbt)
     80-        assert_equal(len(dec_psbt["inputs"][0]["musig2_pubnonces"]), expected_key_leaves)
     81+        assert_equal(len(dec_psbt["inputs"][0]["musig2_pubnonces"]), to_sign_key_leaves)
     82         for pn in dec_psbt["inputs"][0]["musig2_pubnonces"]:
     83             pubkey = pn["aggregate_pubkey"][2:]
     84             if pubkey in dec_psbt["inputs"][0]["witness_utxo"]["scriptPubKey"]["hex"]:
     85@@ -162,7 +186,7 @@ class WalletMuSigTest(BitcoinTestFramework):
     86 
     87         # Add partial sigs
     88         psig_psbts = []
     89-        for wallet in wallets:
     90+        for wallet in signing_wallets:
     91             proc = wallet.walletprocesspsbt(psbt=comb_nonce_psbt, sighashtype=sighash_type)
     92             assert_equal(proc["complete"], False)
     93             psig_psbts.append(proc["psbt"])
     94@@ -170,7 +194,7 @@ class WalletMuSigTest(BitcoinTestFramework):
     95         comb_psig_psbt = self.nodes[0].combinepsbt(psig_psbts)
     96 
     97         dec_psbt = self.nodes[0].decodepsbt(comb_psig_psbt)
     98-        assert_equal(len(dec_psbt["inputs"][0]["musig2_partial_sigs"]), expected_key_leaves)
     99+        assert_equal(len(dec_psbt["inputs"][0]["musig2_partial_sigs"]), signed_key_leaves)
    100         for ps in dec_psbt["inputs"][0]["musig2_partial_sigs"]:
    101             pubkey = ps["aggregate_pubkey"][2:]
    102             if pubkey in dec_psbt["inputs"][0]["witness_utxo"]["scriptPubKey"]["hex"]:
    103@@ -211,6 +235,7 @@ class WalletMuSigTest(BitcoinTestFramework):
    104         self.do_test("tr(H,{pk(musig/*), pk(musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($3,$4,$5)/0/*)})", scriptpath=True)
    105         self.do_test("tr(H,{pk(musig/*), pk(same keys different musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($1,$2)/0/*)})", scriptpath=True)
    106         self.do_test("tr(musig/*,{pk(partial keys diff musig-1/*),pk(partial keys diff musig-2/*)})}", "tr(musig($0,$1,$2)/<3;4>/*,{pk(musig($0,$1)/<5;6>/*),pk(musig($1,$2)/7/*)})")
    107+        self.do_test("tr(musig/*,{pk(partial keys diff musig-1/*),pk(partial keys diff musig-2/*)})} script-path", "tr(musig($0,$1,$2)/<3;4>/*,{pk(musig($0,$1)/<5;6>/*),pk(musig($1,$2)/7/*)})", scriptpath=True, nosign_wallets=[0])
    108 
    109 
    110 if __name__ == '__main__':
    

    achow101 commented at 11:20 pm on September 16, 2025:
    Done with a different approach
  226. in src/key.h:225 in 6b01d7379b outdated
    220@@ -220,6 +221,8 @@ class CKey
    221      *                               Merkle root of the script tree).
    222      */
    223     KeyPair ComputeKeyPair(const uint256* merkle_root) const;
    224+
    225+    std::vector<uint8_t> CreateMuSig2Nonce(MuSig2SecNonce& secnonce, const uint256& hash, const CPubKey& aggregate_pubkey, const std::vector<CPubKey>& pubkeys);
    


    rkrux commented at 1:26 pm on September 15, 2025:

    In 6b01d7379b9e29760abd7b549a04db43937a7b8d “sign: Add CreateMuSig2Nonce”

    The only use of this function is to create the nonce for the sighash passed; I did end up wondering twice what this hash corresponds while navigating the code earlier.

    s/hash/sighash

     0diff --git a/src/key.cpp b/src/key.cpp
     1index 005a913236..e2476096ce 100644
     2--- a/src/key.cpp
     3+++ b/src/key.cpp
     4@@ -350,7 +350,7 @@ KeyPair CKey::ComputeKeyPair(const uint256* merkle_root) const
     5     return KeyPair(*this, merkle_root);
     6 }
     7 
     8-std::vector<uint8_t> CKey::CreateMuSig2Nonce(MuSig2SecNonce& secnonce, const uint256& hash, const CPubKey& aggregate_pubkey, const std::vector<CPubKey>& pubkeys)
     9+std::vector<uint8_t> CKey::CreateMuSig2Nonce(MuSig2SecNonce& secnonce, const uint256& sighash, const CPubKey& aggregate_pubkey, const std::vector<CPubKey>& pubkeys)
    10 {
    11     // Get the keyagg cache and aggregate pubkey
    12     secp256k1_musig_keyagg_cache keyagg_cache;
    13@@ -369,11 +369,11 @@ std::vector<uint8_t> CKey::CreateMuSig2Nonce(MuSig2SecNonce& secnonce, const uin
    14 
    15     // Generate nonce
    16     secp256k1_musig_pubnonce pubnonce;
    17-    if (!secp256k1_musig_nonce_gen(secp256k1_context_sign, secnonce.Get(), &pubnonce, rand.data(), UCharCast(begin()), &pubkey, hash.data(), &keyagg_cache, nullptr)) {
    18+    if (!secp256k1_musig_nonce_gen(secp256k1_context_sign, secnonce.Get(), &pubnonce, rand.data(), UCharCast(begin()), &pubkey, sighash.data(), &keyagg_cache, nullptr)) {
    19         return {};
    20     }
    21 
    22diff --git a/src/key.h b/src/key.h
    23index 83c154c421..f35f3cc015 100644
    24--- a/src/key.h
    25+++ b/src/key.h
    26@@ -222,7 +222,7 @@ public:
    27      */
    28     KeyPair ComputeKeyPair(const uint256* merkle_root) const;
    29 
    30-    std::vector<uint8_t> CreateMuSig2Nonce(MuSig2SecNonce& secnonce, const uint256& hash, const CPubKey& aggregate_pubkey, const std::vector<CPubKey>& pubkeys);
    31+    std::vector<uint8_t> CreateMuSig2Nonce(MuSig2SecNonce& secnonce, const uint256& sighash, const CPubKey& aggregate_pubkey, const std::vector<CPubKey>& pubkeys);
    32     std::optional<uint256> CreateMuSig2PartialSig(const uint256& hash, const CPubKey& aggregate_pubkey, const std::vector<CPubKey>& pubkeys, const std::map<CPubKey, std::vector<uint8_t>>& pubnonces, MuSig2SecNonce& secnonce, const std::vector<std::pair<uint256, bool>>& tweaks);
    33 };
    34 
    

    achow101 commented at 11:23 pm on September 16, 2025:
    Done
  227. in src/key.cpp:386 in 10b7148efa outdated
    382@@ -383,6 +383,91 @@ std::vector<uint8_t> CKey::CreateMuSig2Nonce(MuSig2SecNonce& secnonce, const uin
    383     return out;
    384 }
    385 
    386+std::optional<uint256> CKey::CreateMuSig2PartialSig(const uint256& hash, const CPubKey& aggregate_pubkey, const std::vector<CPubKey>& pubkeys, const std::map<CPubKey, std::vector<uint8_t>>& pubnonces, MuSig2SecNonce& secnonce, const std::vector<std::pair<uint256, bool>>& tweaks)
    


    rkrux commented at 1:27 pm on September 15, 2025:

    In 10b7148efa754169cc625e93c98fa54f7b375e5d “sign: Add CreateMuSig2PartialSig”

    Same s/hash/sighash suggestion in both key.cpp and key.h.


    achow101 commented at 11:23 pm on September 16, 2025:
    Done
  228. in src/musig.cpp:115 in 6b01d7379b outdated
     99@@ -100,3 +100,10 @@ bool MuSig2SecNonce::IsValid()
    100 {
    101     return m_impl->IsValid();
    102 }
    103+
    104+uint256 MuSig2SessionID(const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256& sighash)
    


    rkrux commented at 1:52 pm on September 15, 2025:

    In 6b01d7379b9e29760abd7b549a04db43937a7b8d “sign: Add CreateMuSig2Nonce”

    Any particular reason to name the first argument script_pubkey when plain_pubkey is passed down in both the callers from the top?


    achow101 commented at 9:43 pm on September 16, 2025:
    The callers all pass a script_pubkey. It is also more descriptive because it is actually the pubkey that appears in the script, which is not necessarily the same as the aggregate.
  229. in src/wallet/scriptpubkeyman.h:308 in b938496037 outdated
    303+     * reusing a nonce, this map is held only in memory and must not be written to disk.
    304+     * The side effect is that signing sessions cannot persist across restarts, but this
    305+     * must be done in order to prevent nonce reuse.
    306+     *
    307+     * The session id is an arbitrary value set by the signer in order for the signing logic
    308+     * to find ongoing signing sessions. It is the SHA256 of aggregate xonly key, + participant pubkey + sighash.
    


    rkrux commented at 1:58 pm on September 15, 2025:

    In b9384960374db881285118670beb39228844f142 “wallet: Keep secnonces in DescriptorScriptPubKeyMan”

    It is the SHA256 of aggregate xonly key, + participant pubkey + sighash.

    The need for noting this down here can be avoided if we use a dedicated type for MuSig2SessionID here and everywhere else.

    For example starting from here:

    0diff --git a/src/musig.h b/src/musig.h
    1index 95f495a40a..0cde2ffe1c 100644
    2--- a/src/musig.h
    3+++ b/src/musig.h
    4-uint256 MuSig2SessionID(const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256& sighash);
    5+using MuSig2SessionID = uint256;
    6+MuSig2SessionID createMuSig2SessionID(const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256& sighash);
    

    achow101 commented at 8:26 pm on September 16, 2025:
    I think it would still be necessary to note what the session id is composed of.
  230. in src/wallet/scriptpubkeyman.h:310 in b938496037 outdated
    306+     *
    307+     * The session id is an arbitrary value set by the signer in order for the signing logic
    308+     * to find ongoing signing sessions. It is the SHA256 of aggregate xonly key, + participant pubkey + sighash.
    309+     */
    310+    // TODO: Check thread safety of this
    311+    mutable std::map<uint256, MuSig2SecNonce> m_musig2_secnonces;
    


    rkrux commented at 2:27 pm on September 15, 2025:

    In https://github.com/bitcoin/bitcoin/commit/b9384960374db881285118670beb39228844f142 “wallet: Keep secnonces in DescriptorScriptPubKeyMan”

    Note: Storing the secnonce in SPKM would also end up reusing-before-signing the nonce if a new PSBT session is started for the same transaction among the same signers as the components of the SessionID remain unchanged. I believe this would not be an issue because it’s stored only before the partial signature is created post which it’s securely deleted.

    For example: Between 2 signers A and B, A creates a PSBT and starts the signing session by adding its sig nonce. For some reason, instead of B adding its signature nonce decides to come up with a new PSBT altogether, and send it across to A for signing after adding its signature nonce. In this case, A would start with signing the new PSBT with this previously stored secnonce.

    All this happened without A’s node/wallet being restarted.


    achow101 commented at 8:33 pm on September 16, 2025:

    A cannot add their sig before B has added a pubnonce. Furthermore, if A added their sig, then the secnonce would be already be deleted.

    If B instead provides a “new” PSBT which is really just the original PSBT with their pubnonce, and without A’s pubnonce, then it’s fine to “reuse” the nonce since that is equivalent to combining both PSBTs.


    theStack commented at 4:58 pm on September 29, 2025:
    Is this TODO still relevant? I’m planning to look more in-depth at c14a4bcbb1f84fb4776f43cbb918e7771164029b, but at least on a first rough glance I haven’t discovered a scenario where it would be possible to run any of the three methods accessing this map (FlatSigningProvider::{Set,Get}MuSig2SecNonce and FlatSigningProvider::DeleteMuSig2Session) concurrently for the same DescriptorScriptPubKeyMan.

    achow101 commented at 6:20 pm on September 30, 2025:
    It’s no longer relevant, removed.
  231. rkrux commented at 2:27 pm on September 15, 2025: contributor
    Halfway through the code review - a06017dfce7ce72afbebe6f68d9a29cf72d26593
  232. in src/script/sign.cpp:1 in 7c085554dc outdated


    rkrux commented at 1:56 pm on September 16, 2025:

    In https://github.com/bitcoin/bitcoin/commit/7c085554dce336eb1597ab2fc482163876a49270 “sign: Create MuSig2 signatures for known MuSig2 aggregate keys”

    I have noticed there is some duplication in MuSig2 signing flows in the key path and script path spending. A common SignMuSig2 function can help in removing this duplication and in removing risks of introducing discrepancies in the two spending paths - I checked that the tests pass. Also, should make the review easier by reducing diff.

      0diff --git a/src/script/sign.cpp b/src/script/sign.cpp
      1index b183be8939..ed21ab88f8 100644
      2--- a/src/script/sign.cpp
      3+++ b/src/script/sign.cpp
      4@@ -262,6 +263,100 @@ static bool CreateSig(const BaseSignatureCreator& creator, SignatureData& sigdat
      5     return false;
      6 }
      7 
      8+static bool SignMuSig2(SigVersion sigversion, const SigningProvider& provider, const BaseSignatureCreator& creator, SignatureData& sigdata, const XOnlyPubKey& pk, KeyOriginInfo pk_info, const uint256* merkle_root, const uint256* leaf_hash, std::pair<XOnlyPubKey, uint256>* lookup_key, std::vector<unsigned char>& sig)
      9+{
     10+    for (const auto& [agg_pub, part_pks] : sigdata.musig2_pubkeys) {
     11+        if (part_pks.empty()) continue;
     12+
     13+        // Fill participant derivation path info
     14+        for (const auto& part_pk : part_pks) {
     15+            KeyOriginInfo part_info;
     16+            if (provider.GetKeyOrigin(part_pk.GetID(), part_info)) {
     17+                XOnlyPubKey xonly_part(part_pk);
     18+                auto it = sigdata.taproot_misc_pubkeys.find(xonly_part);
     19+                if (sigversion == SigVersion::TAPSCRIPT) {
     20+                    if (it == sigdata.taproot_misc_pubkeys.end()) {
     21+                        sigdata.taproot_misc_pubkeys.emplace(xonly_part, std::make_pair(std::set<uint256>({*leaf_hash}), part_info));
     22+                    } else {
     23+                        it->second.first.insert(*leaf_hash);
     24+                    }
     25+                } else {
     26+                    if (it == sigdata.taproot_misc_pubkeys.end()) {
     27+                        sigdata.taproot_misc_pubkeys.emplace(xonly_part, std::make_pair(std::set<uint256>(), part_info));
     28+                    }
     29+                }
     30+            }
     31+        }
     32+
     33+        std::vector<std::pair<uint256, bool>> tweaks;
     34+        CPubKey plain_pub = agg_pub;
     35+        if (XOnlyPubKey(agg_pub) != pk) {
     36+            if (pk_info.path.size() > 0) {
     37+                // Compute and compare fingerprint
     38+                CKeyID keyid = agg_pub.GetID();
     39+                if (!std::equal(pk_info.fingerprint, pk_info.fingerprint + sizeof(pk_info.fingerprint), keyid.data())) {
     40+                    continue;
     41+                }
     42+                // Get the BIP32 derivation tweaks
     43+                CExtPubKey extpub = CreateMuSig2SyntheticXpub(agg_pub);
     44+                for (const int i : pk_info.path) {
     45+                    auto& [t, xonly] = tweaks.emplace_back();
     46+                    xonly = false;
     47+                    if (!extpub.Derive(extpub, i, &t)) {
     48+                        return false;
     49+                    }
     50+                }
     51+                Assert(XOnlyPubKey(extpub.pubkey) == pk);
     52+                plain_pub = extpub.pubkey;
     53+            } else {
     54+                continue;
     55+            }
     56+        }
     57+
     58+        // Add the merkle root tweak
     59+        if (sigversion == SigVersion::TAPROOT && merkle_root) {
     60+            tweaks.emplace_back(pk.ComputeTapTweakHash(merkle_root->IsNull() ? nullptr : merkle_root), true);
     61+            std::optional<std::pair<XOnlyPubKey, bool>> tweaked = pk.CreateTapTweak(merkle_root->IsNull() ? nullptr : merkle_root);
     62+            if (!Assume(tweaked)) return false;
     63+            plain_pub = tweaked->first.GetCPubKeys().at(tweaked->second ? 1 : 0);
     64+        }
     65+
     66+        // First try to aggregate
     67+        if (creator.CreateMuSig2AggregateSig(part_pks, sig, agg_pub, plain_pub, leaf_hash, tweaks, sigversion, sigdata)) {
     68+            if (sigversion == SigVersion::TAPSCRIPT) {
     69+                sigdata.taproot_script_sigs[*lookup_key] = sig;
     70+            } else {
     71+                sigdata.taproot_key_path_sig = sig;
     72+            }
     73+            return true;
     74+        }
     75+        // Cannot aggregate, try making partial sigs for every participant
     76+        std::pair<CPubKey, uint256> pub_key_leaf_hash = (sigversion == SigVersion::TAPSCRIPT) ? std::make_pair(plain_pub, *leaf_hash) : std::make_pair(plain_pub, uint256());
     77+        for (const CPubKey& part_pk : part_pks) {
     78+            uint256 partial_sig;
     79+            if (creator.CreateMuSig2PartialSig(provider, partial_sig, agg_pub, plain_pub, part_pk, leaf_hash, tweaks, sigversion, sigdata) && Assume(!partial_sig.IsNull())) {
     80+                sigdata.musig2_partial_sigs[pub_key_leaf_hash].emplace(part_pk, partial_sig);
     81+            }
     82+        }
     83+        // If there are any partial signatures, exit early
     84+        auto partial_sigs_it = sigdata.musig2_partial_sigs.find(pub_key_leaf_hash);
     85+        if (partial_sigs_it != sigdata.musig2_partial_sigs.end() && !partial_sigs_it->second.empty()) {
     86+            return false;
     87+        }
     88+        // No partial sigs, try to make pubnonces
     89+        std::map<CPubKey, std::vector<uint8_t>>& pubnonces = sigdata.musig2_pubnonces[pub_key_leaf_hash];
     90+        for (const CPubKey& part_pk : part_pks) {
     91+            if (pubnonces.contains(part_pk)) continue;
     92+            std::vector<uint8_t> pubnonce = creator.CreateMuSig2Nonce(provider, agg_pub, plain_pub, part_pk, leaf_hash, merkle_root, sigversion, sigdata);
     93+            if (pubnonce.empty()) continue;
     94+            pubnonces[part_pk] = std::move(pubnonce);
     95+        }
     96+
     97+        if (sigversion == SigVersion::TAPROOT) break;
     98+    }
     99+    return false;
    100+}
    101+
    102 static bool CreateTaprootScriptSig(const BaseSignatureCreator& creator, SignatureData& sigdata, const SigningProvider& provider, std::vector<unsigned char>& sig_out, const XOnlyPubKey& pubkey, const uint256& leaf_hash, SigVersion sigversion)
    103 {
    104     KeyOriginInfo info;
    105@@ -289,81 +384,7 @@ static bool CreateTaprootScriptSig(const BaseSignatureCreator& creator, Signatur
    106             info = misc_pk_it->second.second;
    107         }
    108 
    109-        for (const auto& [agg_pub, part_pks] : sigdata.musig2_pubkeys) {
    110-            if (part_pks.empty()) continue;
    111-
    112-            // Fill participant derivation path info
    113-            for (const auto& part_pk : part_pks) {
    114-                KeyOriginInfo part_info;
    115-                if (provider.GetKeyOrigin(part_pk.GetID(), part_info)) {
    116-                    XOnlyPubKey xonly_part(part_pk);
    117-                    auto it = sigdata.taproot_misc_pubkeys.find(xonly_part);
    118-                    if (it == sigdata.taproot_misc_pubkeys.end()) {
    119-                        sigdata.taproot_misc_pubkeys.emplace(xonly_part, std::make_pair(std::set<uint256>({leaf_hash}), part_info));
    120-                    } else {
    121-                        it->second.first.insert(leaf_hash);
    122-                    }
    123-                }
    124-            }
    125-
    126-            std::vector<std::pair<uint256, bool>> tweaks;
    127-            CPubKey plain_pub = agg_pub;
    128-            if (XOnlyPubKey(agg_pub) != pubkey) {
    129-                if (info.path.size() > 0) {
    130-                    // Compute and compare fingerprint
    131-                    CKeyID keyid = agg_pub.GetID();
    132-                    if (std::memcmp(keyid.data(), info.fingerprint, sizeof(info.fingerprint)) != 0) {
    133-                        continue;
    134-                    }
    135-                    // Get the BIP32 derivation tweaks
    136-                    CExtPubKey extpub;
    137-                    extpub.nDepth = 0;
    138-                    std::memset(extpub.vchFingerprint, 0, 4);
    139-                    extpub.nChild = 0;
    140-                    extpub.chaincode = MUSIG_CHAINCODE;
    141-                    extpub.pubkey = agg_pub;
    142-                    for (const int i : info.path) {
    143-                        auto& [tweak, xonly] = tweaks.emplace_back();
    144-                        xonly = false;
    145-                        if (!extpub.Derive(extpub, i, &tweak)) {
    146-                            return false;
    147-                        }
    148-                    }
    149-                    Assert(XOnlyPubKey(extpub.pubkey) == pubkey);
    150-                    plain_pub = extpub.pubkey;
    151-                } else {
    152-                    continue;
    153-                }
    154-            }
    155-
    156-            // We know this is musig, try musig signing
    157-            // First try to aggregate
    158-            if (creator.CreateMuSig2AggregateSig(part_pks, sig_out, agg_pub, plain_pub, &leaf_hash, tweaks, sigversion, sigdata)) {
    159-                sigdata.taproot_script_sigs[lookup_key] = sig_out;
    160-                return true;
    161-            }
    162-            // Cannot aggregate, try making partial sigs for every participant
    163-            auto pub_key_leaf_hash = std::make_pair(plain_pub, leaf_hash);
    164-            for (const CPubKey& part_pk : part_pks) {
    165-                uint256 partial_sig;
    166-                if (creator.CreateMuSig2PartialSig(provider, partial_sig, agg_pub, plain_pub, part_pk, &leaf_hash, tweaks, sigversion, sigdata) && Assume(!partial_sig.IsNull())) {
    167-                    sigdata.musig2_partial_sigs[pub_key_leaf_hash].emplace(part_pk, partial_sig);
    168-                }
    169-            }
    170-            // If there are any partial signatures, exit early
    171-            auto partial_sigs_it = sigdata.musig2_partial_sigs.find(pub_key_leaf_hash);
    172-            if (partial_sigs_it != sigdata.musig2_partial_sigs.end() && !partial_sigs_it->second.empty()) {
    173-                return false;
    174-            }
    175-            // No partial sigs, try to make pubnonces
    176-            std::map<CPubKey, std::vector<uint8_t>>& pubnonces = sigdata.musig2_pubnonces[pub_key_leaf_hash];
    177-            for (const CPubKey& part_pk : part_pks) {
    178-                if (pubnonces.contains(part_pk)) continue;
    179-                std::vector<uint8_t> pubnonce = creator.CreateMuSig2Nonce(provider, agg_pub, plain_pub, part_pk, &leaf_hash, nullptr, sigversion, sigdata);
    180-                if (pubnonce.empty()) continue;
    181-                pubnonces[part_pk] = std::move(pubnonce);
    182-            }
    183-        }
    184+        return SignMuSig2(sigversion, provider, creator, sigdata, pubkey, info, /*merkle_root*/nullptr, &leaf_hash, &lookup_key, sig_out);
    185     }
    186 
    187     return sigdata.taproot_script_sigs.contains(lookup_key);
    188@@ -570,88 +591,7 @@ static bool SignTaproot(const SigningProvider& provider, const BaseSignatureCrea
    189                     info = misc_pk_it->second.second;
    190                 }
    191 
    192-                for (const auto& [agg_pub, part_pks] : sigdata.musig2_pubkeys) {
    193-                    if (part_pks.empty()) continue;
    194-
    195-                    // Fill participant derivation path info
    196-                    for (const auto& part_pk : part_pks) {
    197-                        KeyOriginInfo info;
    198-                        if (provider.GetKeyOrigin(part_pk.GetID(), info)) {
    199-                            XOnlyPubKey xonly_part(part_pk);
    200-                            auto it = sigdata.taproot_misc_pubkeys.find(xonly_part);
    201-                            if (it == sigdata.taproot_misc_pubkeys.end()) {
    202-                                sigdata.taproot_misc_pubkeys.emplace(xonly_part, std::make_pair(std::set<uint256>(), info));
    203-                            }
    204-                        }
    205-                    }
    206-
    207-                    std::vector<std::pair<uint256, bool>> tweaks;
    208-                    CPubKey plain_pub = agg_pub;
    209-                    if (XOnlyPubKey(agg_pub) != pk) {
    210-                        if (info.path.size() > 0) {
    211-                            // Compute and compare fingerprint
    212-                            CKeyID keyid = agg_pub.GetID();
    213-                            if (!std::equal(info.fingerprint, info.fingerprint + sizeof(info.fingerprint), keyid.data())) {
    214-                                continue;
    215-                            }
    216-                            // Get the BIP32 derivation tweaks
    217-                            CExtPubKey extpub;
    218-                            extpub.nDepth = 0;
    219-                            std::memset(extpub.vchFingerprint, 0, 4);
    220-                            extpub.nChild = 0;
    221-                            extpub.chaincode = MUSIG_CHAINCODE;
    222-                            extpub.pubkey = agg_pub;
    223-                            for (const int i : info.path) {
    224-                                auto& [t, xonly] = tweaks.emplace_back();
    225-                                xonly = false;
    226-                                if (!extpub.Derive(extpub, i, &t)) {
    227-                                    return;
    228-                                }
    229-                            }
    230-                            Assert(XOnlyPubKey(extpub.pubkey) == pk);
    231-                            plain_pub = extpub.pubkey;
    232-                        } else {
    233-                            continue;
    234-                        }
    235-                    }
    236-
    237-                    // Add the merkle root tweak
    238-                    if (merkle_root) {
    239-                        tweaks.emplace_back(pk.ComputeTapTweakHash(merkle_root->IsNull() ? nullptr : merkle_root), true);
    240-                        std::optional<std::pair<XOnlyPubKey, bool>> tweaked = pk.CreateTapTweak(merkle_root->IsNull() ? nullptr : merkle_root);
    241-                        if (!Assume(tweaked)) return;
    242-                        plain_pub = tweaked->first.GetCPubKeys().at(tweaked->second ? 1 : 0);
    243-                    }
    244-
    245-                    // We know this is musig, try musig signing
    246-                    // First try to aggregate
    247-                    if (creator.CreateMuSig2AggregateSig(part_pks, sig, agg_pub, plain_pub, nullptr, tweaks, SigVersion::TAPROOT, sigdata)) {
    248-                        sigdata.taproot_key_path_sig = sig;
    249-                        return;
    250-                    }
    251-                    // Cannot aggregate, try making partial sigs for every participant
    252-                    auto pub_key_leaf_hash = std::make_pair(plain_pub, uint256());
    253-                    for (const CPubKey& part_pk : part_pks) {
    254-                        uint256 partial_sig;
    255-                        if (creator.CreateMuSig2PartialSig(provider, partial_sig, agg_pub, plain_pub, part_pk, nullptr, tweaks, SigVersion::TAPROOT, sigdata) && Assume(!partial_sig.IsNull())) {
    256-                            sigdata.musig2_partial_sigs[pub_key_leaf_hash].emplace(part_pk, partial_sig);
    257-                        }
    258-                    }
    259-                    // If there are any partial signatures, exit early
    260-                    auto partial_sigs_it = sigdata.musig2_partial_sigs.find(pub_key_leaf_hash);
    261-                    if (partial_sigs_it != sigdata.musig2_partial_sigs.end() && !partial_sigs_it->second.empty()) {
    262-                        return;
    263-                    }
    264-                    // No partial sigs, try to make pubnonces
    265-                    std::map<CPubKey, std::vector<uint8_t>>& pubnonces = sigdata.musig2_pubnonces[pub_key_leaf_hash];
    266-                    for (const CPubKey& part_pk : part_pks) {
    267-                        if (pubnonces.contains(part_pk)) continue;
    268-                        std::vector<uint8_t> pubnonce = creator.CreateMuSig2Nonce(provider, agg_pub, plain_pub, part_pk, nullptr, merkle_root, SigVersion::TAPROOT, sigdata);
    269-                        if (pubnonce.empty()) continue;
    270-                        pubnonces[part_pk] = std::move(pubnonce);
    271-                    }
    272-                    break;
    273-                }
    274+                SignMuSig2(SigVersion::TAPROOT, provider, creator, sigdata, pk, info, merkle_root, /*leaf_hash*/nullptr, /*lookup_key*/nullptr, sig);
    275             }
    276         };
    277 
    

    rkrux commented at 1:58 pm on September 16, 2025:
    This^ diff also uses the function from this earlier suggestion #29675 (review).

    achow101 commented at 11:21 pm on September 16, 2025:
    Done

    rkrux commented at 1:17 pm on September 29, 2025:

    In 2806f8152c751641257f3f1ab0d886d9c8ece1e1 “pubkey: Return tweaks from BIP32 derivation”

    Reconsider updating the commit description retouched, I can imagine future readers would want to know the intent of this commit, following up on #29675 (comment)


    achow101 commented at 6:19 pm on September 30, 2025:
    Done
  233. rkrux commented at 1:56 pm on September 16, 2025: contributor
    Reviewed commit 7c085554dce336eb1597ab2fc482163876a49270 with the aim of deduplicating MuSig2 signing flow.
  234. musig: Move synthetic xpub construction to its own function f14876213a
  235. achow101 force-pushed on Sep 16, 2025
  236. achow101 force-pushed on Sep 16, 2025
  237. achow101 force-pushed on Sep 16, 2025
  238. in src/script/sign.cpp:280 in 1614a2da5e outdated
    275+            KeyOriginInfo part_info;
    276+            if (provider.GetKeyOrigin(part_pk.GetID(), part_info)) {
    277+                XOnlyPubKey xonly_part(part_pk);
    278+                auto it = sigdata.taproot_misc_pubkeys.find(xonly_part);
    279+                if (it == sigdata.taproot_misc_pubkeys.end()) {
    280+                    it = sigdata.taproot_misc_pubkeys.emplace(xonly_part, std::make_pair(std::set<uint256>(), agg_info)).first;
    


    rkrux commented at 10:48 am on September 17, 2025:

    In 1614a2da5ea32b3efeecf6c756036539c567b656 “sign: Create MuSig2 signatures for known MuSig2 aggregate keys”

    Otherwise part_info acts as a dummy object.

     0diff --git a/src/script/sign.cpp b/src/script/sign.cpp
     1index b60d6dab55..f377a32750 100644
     2--- a/src/script/sign.cpp
     3+++ b/src/script/sign.cpp
     4@@ -277,7 +277,7 @@ static bool SignMuSig2(const BaseSignatureCreator& creator, SignatureData& sigda
     5                 XOnlyPubKey xonly_part(part_pk);
     6                 auto it = sigdata.taproot_misc_pubkeys.find(xonly_part);
     7                 if (it == sigdata.taproot_misc_pubkeys.end()) {
     8-                    it = sigdata.taproot_misc_pubkeys.emplace(xonly_part, std::make_pair(std::set<uint256>(), agg_info)).first;
     9+                    it = sigdata.taproot_misc_pubkeys.emplace(xonly_part, std::make_pair(std::set<uint256>(), part_info)).first;
    10                 }
    11                 if (leaf_hash) it->second.first.insert(*leaf_hash);
    12             }
    

    achow101 commented at 5:12 pm on September 17, 2025:
    Fixed
  239. in src/key.cpp:404 in d53a70d769 outdated
    399+    CPubKey our_pubkey = GetPubKey();
    400+    for (const CPubKey& part_pk : pubkeys) {
    401+        const auto& pn_it = pubnonces.find(part_pk);
    402+        if (pn_it == pubnonces.end()) return std::nullopt;
    403+        const std::vector<uint8_t> pubnonce = pn_it->second;
    404+        if (pubnonce.size() != 66) return std::nullopt;
    


    rkrux commented at 10:54 am on September 17, 2025:

    In d53a70d769505b28c76d0468ea9503f7e00c1e04 “sign: Add CreateMuSig2PartialSig”

     0diff --git a/src/key.cpp b/src/key.cpp
     1index 29427689bb..a952acb260 100644
     2--- a/src/key.cpp
     3+++ b/src/key.cpp
     4@@ -401,7 +401,7 @@ std::optional<uint256> CKey::CreateMuSig2PartialSig(const uint256& sighash, cons
     5         const auto& pn_it = pubnonces.find(part_pk);
     6         if (pn_it == pubnonces.end()) return std::nullopt;
     7         const std::vector<uint8_t> pubnonce = pn_it->second;
     8-        if (pubnonce.size() != 66) return std::nullopt;
     9+        if (pubnonce.size() != MUSIG2_PUBNONCE_SIZE) return std::nullopt;
    10         if (part_pk == our_pubkey) {
    11             our_pubkey_idx = signers_data.size();
    12         }
    

    achow101 commented at 5:12 pm on September 17, 2025:
    Done
  240. in src/musig.cpp:142 in 09105c55e7 outdated
    137+    std::vector<const secp256k1_musig_partial_sig*> partial_sig_ptrs;
    138+    for (const CPubKey& part_pk : part_pubkeys) {
    139+        const auto& pn_it = pubnonces.find(part_pk);
    140+        if (pn_it == pubnonces.end()) return std::nullopt;
    141+        const std::vector<uint8_t> pubnonce = pn_it->second;
    142+        if (pubnonce.size() != 66) return std::nullopt;
    


    rkrux commented at 10:54 am on September 17, 2025:

    In 09105c55e71d0614508c9111ed448ea1a370b38b “sign: Add CreateMuSig2AggregateSig”

     0diff --git a/src/musig.cpp b/src/musig.cpp
     1index c8dfe70ef9..686ec5e869 100644
     2--- a/src/musig.cpp
     3+++ b/src/musig.cpp
     4@@ -139,7 +139,7 @@ std::optional<std::vector<uint8_t>> CreateMuSig2AggregateSig(const std::vector<C
     5         const auto& pn_it = pubnonces.find(part_pk);
     6         if (pn_it == pubnonces.end()) return std::nullopt;
     7         const std::vector<uint8_t> pubnonce = pn_it->second;
     8-        if (pubnonce.size() != 66) return std::nullopt;
     9+        if (pubnonce.size() != MUSIG2_PUBNONCE_SIZE) return std::nullopt;
    10         const auto& it = partial_sigs.find(part_pk);
    11         if (it == partial_sigs.end()) return std::nullopt;
    12         const uint256& partial_sig = it->second;
    

    achow101 commented at 5:12 pm on September 17, 2025:
    Done
  241. in src/script/sign.cpp:319 in 1614a2da5e outdated
    314+            std::optional<std::pair<XOnlyPubKey, bool>> tweaked = script_pubkey.CreateTapTweak(merkle_root->IsNull() ? nullptr : merkle_root);
    315+            if (!Assume(tweaked)) return false;
    316+            plain_pub = tweaked->first.GetCPubKeys().at(tweaked->second ? 1 : 0);
    317+        }
    318+
    319+        // We know this is musig, try musig signing
    


    rkrux commented at 10:57 am on September 17, 2025:

    In https://github.com/bitcoin/bitcoin/commit/1614a2da5ea32b3efeecf6c756036539c567b656 “sign: Create MuSig2 signatures for known MuSig2 aggregate keys”

    The name of the function makes this comment not required.

     0diff --git a/src/script/sign.cpp b/src/script/sign.cpp
     1index b60d6dab55..a83cf82f19 100644
     2--- a/src/script/sign.cpp
     3+++ b/src/script/sign.cpp
     4@@ -316,7 +316,6 @@ static bool SignMuSig2(const BaseSignatureCreator& creator, SignatureData& sigda
     5             plain_pub = tweaked->first.GetCPubKeys().at(tweaked->second ? 1 : 0);
     6         }
     7 
     8-        // We know this is musig, try musig signing
     9         // First try to aggregate
    10         if (creator.CreateMuSig2AggregateSig(part_pks, sig_out, agg_pub, plain_pub, leaf_hash, tweaks, sigversion, sigdata)) {
    11             if (sigversion == SigVersion::TAPROOT) {
    

    achow101 commented at 5:12 pm on September 17, 2025:
    Done
  242. in src/script/sign.cpp:282 in 1614a2da5e outdated
    268+    Assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT);
    269+
    270+    for (const auto& [agg_pub, part_pks] : sigdata.musig2_pubkeys) {
    271+        if (part_pks.empty()) continue;
    272+
    273+        // Fill participant derivation path info
    


    rkrux commented at 1:10 pm on September 17, 2025:

    In 1614a2da5ea32b3efeecf6c756036539c567b656 “sign: Create MuSig2 signatures for known MuSig2 aggregate keys”

     0diff --git a/src/script/sign.cpp b/src/script/sign.cpp
     1index b60d6dab55..eea6cd0a9f 100644
     2--- a/src/script/sign.cpp
     3+++ b/src/script/sign.cpp
     4@@ -270,19 +270,6 @@ static bool SignMuSig2(const BaseSignatureCreator& creator, SignatureData& sigda
     5     for (const auto& [agg_pub, part_pks] : sigdata.musig2_pubkeys) {
     6         if (part_pks.empty()) continue;
     7 
     8-        // Fill participant derivation path info
     9-        for (const auto& part_pk : part_pks) {
    10-            KeyOriginInfo part_info;
    11-            if (provider.GetKeyOrigin(part_pk.GetID(), part_info)) {
    12-                XOnlyPubKey xonly_part(part_pk);
    13-                auto it = sigdata.taproot_misc_pubkeys.find(xonly_part);
    14-                if (it == sigdata.taproot_misc_pubkeys.end()) {
    15-                    it = sigdata.taproot_misc_pubkeys.emplace(xonly_part, std::make_pair(std::set<uint256>(), agg_info)).first;
    16-                }
    17-                if (leaf_hash) it->second.first.insert(*leaf_hash);
    18-            }
    19-        }
    20-
    21         std::vector<std::pair<uint256, bool>> tweaks;
    22         CPubKey plain_pub = agg_pub;
    23         if (XOnlyPubKey(agg_pub) != script_pubkey) {
    24@@ -308,6 +295,19 @@ static bool SignMuSig2(const BaseSignatureCreator& creator, SignatureData& sigda
    25             }
    26         }
    27 
    28+        // Fill participant derivation path info
    29+        for (const auto& part_pk : part_pks) {
    30+            KeyOriginInfo part_info;
    31+            if (provider.GetKeyOrigin(part_pk.GetID(), part_info)) {
    32+                XOnlyPubKey xonly_part(part_pk);
    33+                auto it = sigdata.taproot_misc_pubkeys.find(xonly_part);
    34+                if (it == sigdata.taproot_misc_pubkeys.end()) {
    35+                    it = sigdata.taproot_misc_pubkeys.emplace(xonly_part, std::make_pair(std::set<uint256>(), part_info)).first;
    36+                }
    37+                if (leaf_hash) it->second.first.insert(*leaf_hash);
    38+            }
    39+        }
    40+
    41         // Add the merkle root tweak
    42         if (sigversion == SigVersion::TAPROOT && merkle_root) {
    43             tweaks.emplace_back(script_pubkey.ComputeTapTweakHash(merkle_root->IsNull() ? nullptr : merkle_root), true);
    

    achow101 commented at 5:08 pm on September 17, 2025:
    The order is intentional, we want to always fill in data that we know about first.
  243. in src/script/sign.cpp:296 in 1614a2da5e outdated
    303+                }
    304+                Assert(XOnlyPubKey(extpub.pubkey) == script_pubkey);
    305+                plain_pub = extpub.pubkey;
    306+            } else {
    307+                continue;
    308+            }
    


    rkrux commented at 1:19 pm on September 17, 2025:

    In https://github.com/bitcoin/bitcoin/commit/1614a2da5ea32b3efeecf6c756036539c567b656 “sign: Create MuSig2 signatures for known MuSig2 aggregate keys”

    Usually avoid suggesting this kind of change but in this case it seems quite helpful for readability.

     0diff --git a/src/script/sign.cpp b/src/script/sign.cpp
     1index b60d6dab55..de17219bd5 100644
     2--- a/src/script/sign.cpp
     3+++ b/src/script/sign.cpp
     4@@ -286,26 +286,26 @@ static bool SignMuSig2(const BaseSignatureCreator& creator, SignatureData& sigda
     5         std::vector<std::pair<uint256, bool>> tweaks;
     6         CPubKey plain_pub = agg_pub;
     7         if (XOnlyPubKey(agg_pub) != script_pubkey) {
     8-            if (agg_info.path.size() > 0) {
     9-                // Compute and compare fingerprint
    10-                CKeyID keyid = agg_pub.GetID();
    11-                if (!std::equal(agg_info.fingerprint, agg_info.fingerprint + sizeof(agg_info.fingerprint), keyid.data())) {
    12-                    continue;
    13-                }
    14-                // Get the BIP32 derivation tweaks
    15-                CExtPubKey extpub = CreateMuSig2SyntheticXpub(agg_pub);
    16-                for (const int i : agg_info.path) {
    17-                    auto& [t, xonly] = tweaks.emplace_back();
    18-                    xonly = false;
    19-                    if (!extpub.Derive(extpub, i, &t)) {
    20-                        return false;
    21-                    }
    22-                }
    23-                Assert(XOnlyPubKey(extpub.pubkey) == script_pubkey);
    24-                plain_pub = extpub.pubkey;
    25-            } else {
    26+            if (agg_info.path.size() == 0) {
    27+                continue;
    28+            }
    29+
    30+            // Compute and compare fingerprint
    31+            CKeyID keyid = agg_pub.GetID();
    32+            if (!std::equal(agg_info.fingerprint, agg_info.fingerprint + sizeof(agg_info.fingerprint), keyid.data())) {
    33                 continue;
    34             }
    35+            // Get the BIP32 derivation tweaks
    36+            CExtPubKey extpub = CreateMuSig2SyntheticXpub(agg_pub);
    37+            for (const int i : agg_info.path) {
    38+                auto& [t, xonly] = tweaks.emplace_back();
    39+                xonly = false;
    40+                if (!extpub.Derive(extpub, i, &t)) {
    41+                    return false;
    42+                }
    43+            }
    44+            Assert(XOnlyPubKey(extpub.pubkey) == script_pubkey);
    45+            plain_pub = extpub.pubkey;
    46         }
    47 
    48         // Add the merkle root tweak
    

    achow101 commented at 5:12 pm on September 17, 2025:
    Done
  244. in src/script/sign.cpp:299 in 1614a2da5e outdated
    283+            }
    284+        }
    285+
    286+        std::vector<std::pair<uint256, bool>> tweaks;
    287+        CPubKey plain_pub = agg_pub;
    288+        if (XOnlyPubKey(agg_pub) != script_pubkey) {
    


    rkrux commented at 1:30 pm on September 17, 2025:

    In https://github.com/bitcoin/bitcoin/commit/1614a2da5ea32b3efeecf6c756036539c567b656 “sign: Create MuSig2 signatures for known MuSig2 aggregate keys”

    Suggesting because I ended up spending some time to understand the need for this check.

    0@@ -285,6 +286,7 @@ static bool SignMuSig2(const BaseSignatureCreator& creator, SignatureData& sigda
    1
    2         std::vector<std::pair<uint256, bool>> tweaks;
    3         CPubKey plain_pub = agg_pub;
    4+        // Check if we can derive the script pubkey from this aggregate pubkey
    5         if (XOnlyPubKey(agg_pub) != script_pubkey) {
    6             if (agg_info.path.size() > 0) {
    7                 // Compute and compare fingerprint
    

    achow101 commented at 5:13 pm on September 17, 2025:
    Done
  245. in src/script/sign.cpp:323 in 1614a2da5e outdated
    310+
    311+        // Add the merkle root tweak
    312+        if (sigversion == SigVersion::TAPROOT && merkle_root) {
    313+            tweaks.emplace_back(script_pubkey.ComputeTapTweakHash(merkle_root->IsNull() ? nullptr : merkle_root), true);
    314+            std::optional<std::pair<XOnlyPubKey, bool>> tweaked = script_pubkey.CreateTapTweak(merkle_root->IsNull() ? nullptr : merkle_root);
    315+            if (!Assume(tweaked)) return false;
    


    rkrux commented at 1:38 pm on September 17, 2025:

    In https://github.com/bitcoin/bitcoin/commit/1614a2da5ea32b3efeecf6c756036539c567b656 “sign: Create MuSig2 signatures for known MuSig2 aggregate keys”

    There are 2 instances of returning false from this function (other on line 301) in which case it is expected that the signing process would throw an error at a later stage. Do you think adding an error log here would be helpful for debugging later?


    achow101 commented at 5:11 pm on September 17, 2025:
    No, signing does not log.
  246. rkrux commented at 1:46 pm on September 17, 2025: contributor
    More than three quarters of code review complete at 6400f7b82f4d2c22fb05f7122ce3a750938526f7
  247. achow101 force-pushed on Sep 17, 2025
  248. in src/wallet/scriptpubkeyman.cpp:1261 in 821d78bddd outdated
    1255@@ -1256,6 +1256,10 @@ std::unique_ptr<FlatSigningProvider> DescriptorScriptPubKeyMan::GetSigningProvid
    1256         FlatSigningProvider master_provider;
    1257         master_provider.keys = GetKeys();
    1258         m_wallet_descriptor.descriptor->ExpandPrivate(index, master_provider, *out_keys);
    1259+
    1260+        // Always include musig_secnonces as this descriptor may have a participant private key
    1261+        // but not a musig() descriptor
    


    rkrux commented at 9:31 am on September 20, 2025:

    In 821d78bddd0ecb04d36d1f860ef7e129b248ccb8 “wallet: Keep secnonces in DescriptorScriptPubKeyMan”

    I believe this comment should be updated as I spent some time trying to verify it. While the scenario mentioned is a good catch which warrants a test case on its own, the following line seems required in the current tests as well, which fail if this line is commented out.

     0diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp
     1index ff18265d70..1e2bef739a 100644
     2--- a/src/wallet/scriptpubkeyman.cpp
     3+++ b/src/wallet/scriptpubkeyman.cpp
     4@@ -1257,8 +1257,8 @@ std::unique_ptr<FlatSigningProvider> DescriptorScriptPubKeyMan::GetSigningProvid
     5         master_provider.keys = GetKeys();
     6         m_wallet_descriptor.descriptor->ExpandPrivate(index, master_provider, *out_keys);
     7
     8-        // Always include musig_secnonces as this descriptor may have a participant private key
     9-        // but not a musig() descriptor
    10+        // Always include the generated musig_secnonces so that the
    11+        // partial signature can be created later in the PSBT signing flow
    12         out_keys->musig2_secnonces = &m_musig2_secnonces;
    13     }
    

    achow101 commented at 0:24 am on September 23, 2025:
    The line it is commenting is always required, including in normal operation when a musig() is present, and when a musig() is now known but the keys are. The comment is there to inform future readers wondering why the secnonces are unconditionally added. I don’t think there is anything to update.
  249. in test/functional/wallet_musig.py:181 in 93b1d56221 outdated
    170+                    if pubkey in leaf_scripts["script"]:
    171+                        break
    172+                else:
    173+                    assert False, "Aggregate pubkey for pubnonce not seen as output key, internal key, or in any scripts"
    174+            else:
    175+                assert False, "Aggregate pubkey for pubnonce not seen as output key or internal key"
    


    rkrux commented at 11:18 am on September 22, 2025:

    In 93b1d5622194165444ea9e919ccf319a5e990de0 “test: Test MuSig2 in the wallet”

    internal key

    The internal key doesn’t seem to be checked for here in the above two checks. Can we remove it from the assert message? I checked that both for rawtr(musig(...)) and tr(musig(..)), the aggregate_pubkey[2:] is in the scriptPubKey in the witness_utxo section. I infer that its presence in only the output key is checked.


    achow101 commented at 11:12 pm on September 23, 2025:
    Changed the message.
  250. in src/wallet/scriptpubkeyman.cpp:1260 in 821d78bddd outdated
    1255@@ -1256,6 +1256,10 @@ std::unique_ptr<FlatSigningProvider> DescriptorScriptPubKeyMan::GetSigningProvid
    1256         FlatSigningProvider master_provider;
    1257         master_provider.keys = GetKeys();
    1258         m_wallet_descriptor.descriptor->ExpandPrivate(index, master_provider, *out_keys);
    1259+
    1260+        // Always include musig_secnonces as this descriptor may have a participant private key
    


    rkrux commented at 2:34 pm on September 22, 2025:

    In https://github.com/bitcoin/bitcoin/commit/821d78bddd0ecb04d36d1f860ef7e129b248ccb8 “wallet: Keep secnonces in DescriptorScriptPubKeyMan”

    this descriptor may have a participant private key but not a musig() descriptor

    I tried to add test for this scenario but the pubnonces are not added for signers that don’t have the musig descriptor imported. For the following test, only the first signer that has the musig descriptor is able to add the nonce. Is there a bug or have I misunderstood the intent of this comment?

     0diff --git a/test/functional/wallet_musig.py b/test/functional/wallet_musig.py
     1index de7c08b9ef..0b6fcdb656 100755
     2--- a/test/functional/wallet_musig.py
     3+++ b/test/functional/wallet_musig.py
     4@@ -28,7 +28,7 @@ class WalletMuSigTest(BitcoinTestFramework):
     5     def skip_test_if_missing_module(self):
     6         self.skip_if_no_wallet()
     7 
     8-    def do_test(self, comment, pattern, sighash_type=None, scriptpath=False, nosign_wallets=None):
     9+    def do_test(self, comment, pattern, sighash_type=None, scriptpath=False, nosign_wallets=None, nomusig_wallets=None):
    10         self.log.info(f"Testing {comment}")
    11         has_internal = MULTIPATH_TWO_RE.search(pattern) is not None
    12 
    13@@ -80,6 +80,9 @@ class WalletMuSigTest(BitcoinTestFramework):
    14         # Construct and import each wallet's musig descriptor that
    15         # contains the private key from that wallet and pubkeys of the others
    16         for i, wallet in enumerate(wallets):
    17+            if nomusig_wallets and i in nomusig_wallets:
    18+                continue
    19+
    20             desc = pat
    21             import_descs = []
    22             for j, (priv, pub) in enumerate(keys):
    23@@ -101,15 +104,15 @@ class WalletMuSigTest(BitcoinTestFramework):
    24         # Check that the wallets agree on the same musig address
    25         addr = None
    26         change_addr = None
    27-        for wallet in wallets:
    28+        for i, wallet in enumerate(wallets):
    29             if addr is None:
    30                 addr = wallet.getnewaddress(address_type="bech32m")
    31-            else:
    32+            elif not nomusig_wallets or i not in nomusig_wallets:
    33                 assert_equal(addr, wallet.getnewaddress(address_type="bech32m"))
    34             if has_internal:
    35                 if change_addr is None:
    36                     change_addr = wallet.getrawchangeaddress(address_type="bech32m")
    37-                else:
    38+                elif not nomusig_wallets or i not in nomusig_wallets:
    39                     assert_equal(change_addr, wallet.getrawchangeaddress(address_type="bech32m"))
    40 
    41         # Fund that address
    42@@ -118,10 +121,10 @@ class WalletMuSigTest(BitcoinTestFramework):
    43 
    44         # Spend that UTXO
    45         utxo = None
    46-        for wallet in wallets:
    47+        for i, wallet in enumerate(wallets):
    48             if utxo is None:
    49                 utxo = wallet.listunspent()[0]
    50-            else:
    51+            elif not nomusig_wallets or i not in nomusig_wallets:
    52                 assert_equal(utxo, wallet.listunspent()[0])
    53         psbt = wallets[0].send(outputs=[{self.def_wallet.getnewaddress(): 5}], inputs=[utxo], change_type="bech32m", change_position=1)["psbt"]
    54 
    55@@ -228,7 +231,7 @@ class WalletMuSigTest(BitcoinTestFramework):
    56         self.do_test("tr(H,{pk(musig/*), pk(same keys different musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($1,$2)/0/*)})", scriptpath=True)
    57         self.do_test("tr(musig/*,{pk(partial keys diff musig-1/*),pk(partial keys diff musig-2/*)})}", "tr(musig($0,$1,$2)/<3;4>/*,{pk(musig($0,$1)/<5;6>/*),pk(musig($1,$2)/7/*)})")
    58         self.do_test("tr(musig/*,{pk(partial keys diff musig-1/*),pk(partial keys diff musig-2/*)})} script-path", "tr(musig($0,$1,$2)/<3;4>/*,{pk(musig($0,$1)/<5;6>/*),pk(musig($1,$2)/7/*)})", scriptpath=True, nosign_wallets=[0])
    59-
    60+        self.do_test("tr(H,pk(musig/*)) nomusig", "tr($H,pk(musig($0,$1,$2)/<0;1>/*))", scriptpath=True, nomusig_wallets=[1, 2])
    61 
    62 if __name__ == '__main__':
    63     WalletMuSigTest(__file__).main()
    

    The private key is unable to be found here in MutableTransactionSignatureCreator::CreateMuSig2Nonce: https://github.com/bitcoin/bitcoin/pull/29675/commits/f7ccce9bf63954f672e4f0a3a71eba89886906d9#diff-8a974828ccf5a554c068f5e859e62d1ab1e5010c66baaa6d6b83f42b26b219adR111


    achow101 commented at 11:13 pm on September 23, 2025:
    It is supposed to sign when any descriptor has a private key for a participant. Added a test and fixed.
  251. in test/functional/wallet_musig.py:171 in 93b1d56221 outdated
    160+        comb_nonce_psbt = self.nodes[0].combinepsbt(nonce_psbts)
    161+
    162+        dec_psbt = self.nodes[0].decodepsbt(comb_nonce_psbt)
    163+        assert_equal(len(dec_psbt["inputs"][0]["musig2_pubnonces"]), expected_pubnonces)
    164+        for pn in dec_psbt["inputs"][0]["musig2_pubnonces"]:
    165+            pubkey = pn["aggregate_pubkey"][2:]
    


    rkrux commented at 2:50 pm on September 22, 2025:

    In https://github.com/bitcoin/bitcoin/commit/93b1d5622194165444ea9e919ccf319a5e990de0 “test: Test MuSig2 in the wallet”

    Why does the compressed form of the aggregate_pubkey in the nonces and sigs section start with a 03? Given they are valid x-only keys, shouldn’t this representation have a prefix of 02 because of even Y?

    From the BIP:

    The plain public key must be the key found in the script and not the aggregate public key that it was derived from, if it was derived from an aggregate key.

     0'musig2_participant_pubkeys': [{'aggregate_pubkey': '020a6c24967f768b6b53d2b14833e739c8f60b42da76f7c67a2bab3f1c0d299f4c',
     1                                 'participant_pubkeys': ['027753a063d3ea73942d07f01e38e9fd0db6f26302666a2591e81a291d40669461',
     2                                                         '02a92fa1461c78ec6cca81486ee031dc6fcf452d8ef4d2cfa46b21c9c85dd6cc76',
     3                                                         '02dd225c78173d2d7039247ab1b4c10d942429dee62193fa75f5dea3b470ed76ab']}],
     4 'musig2_pubnonces': [{'participant_pubkey': '027753a063d3ea73942d07f01e38e9fd0db6f26302666a2591e81a291d40669461',
     5                       'aggregate_pubkey': '036e1fb4cdd685976482e1edd85bd81d5763ee848c45efcb7c978f74a2d3e9f8d1',
     6                       'leaf_hash': '25e6e9a3fa4db641ba15c80eccbf0487d3641fde0d39dfce6c85b08e4a622497',
     7                       'pubnonce': '03bc111dead1b589f57a989cc48ffa1ae33054780b42cdee2edae6d11e7a73f8560303fd927859b469798ed51dad44ae845281f64629c1f6b5009e58748abffa5e00'},
     8                      {'participant_pubkey': '02a92fa1461c78ec6cca81486ee031dc6fcf452d8ef4d2cfa46b21c9c85dd6cc76',
     9                       'aggregate_pubkey': '036e1fb4cdd685976482e1edd85bd81d5763ee848c45efcb7c978f74a2d3e9f8d1',
    10                       'leaf_hash': '25e6e9a3fa4db641ba15c80eccbf0487d3641fde0d39dfce6c85b08e4a622497',
    11                       'pubnonce': '03907da11bc957fa678c86f5e6b139beaab08378265371760c714ebb7e636fb3bf02c60b83d1e5ed4beb8a03e76f5111eba592186bd717808f2561f52ca7172e0cd7'},
    12                      {'participant_pubkey': '02dd225c78173d2d7039247ab1b4c10d942429dee62193fa75f5dea3b470ed76ab',
    13                       'aggregate_pubkey': '036e1fb4cdd685976482e1edd85bd81d5763ee848c45efcb7c978f74a2d3e9f8d1',
    14                       'leaf_hash': '25e6e9a3fa4db641ba15c80eccbf0487d3641fde0d39dfce6c85b08e4a622497',
    15                       'pubnonce': '0275c6e5279746e9623f705970c782d62e14031ddd415d937a31d8310bd41821d30204a59593fa7dd6d38b253e986c6e846c54f93e0f7b873e5ab0388f35c5fdcc98'}],
    16 'musig2_partial_sigs': [{'participant_pubkey': '027753a063d3ea73942d07f01e38e9fd0db6f26302666a2591e81a291d40669461',
    17                          'aggregate_pubkey': '036e1fb4cdd685976482e1edd85bd81d5763ee848c45efcb7c978f74a2d3e9f8d1',
    18                          'leaf_hash': '25e6e9a3fa4db641ba15c80eccbf0487d3641fde0d39dfce6c85b08e4a622497',
    19                          'partial_sig': '92f5270f279da66123076488d653fa14919fb4e5b8b3f778e83ed9cee19b7f94'},
    20                         {'participant_pubkey': '02a92fa1461c78ec6cca81486ee031dc6fcf452d8ef4d2cfa46b21c9c85dd6cc76',
    21                          'aggregate_pubkey': '036e1fb4cdd685976482e1edd85bd81d5763ee848c45efcb7c978f74a2d3e9f8d1',
    22                          'leaf_hash': '25e6e9a3fa4db641ba15c80eccbf0487d3641fde0d39dfce6c85b08e4a622497',
    23                          'partial_sig': 'db9c4b05ebb60f6432a1a9a35d4c17d1959c0eb277d4c49742ce24dab2fd19f3'},
    24                         {'participant_pubkey': '02dd225c78173d2d7039247ab1b4c10d942429dee62193fa75f5dea3b470ed76ab',
    25                          'aggregate_pubkey': '036e1fb4cdd685976482e1edd85bd81d5763ee848c45efcb7c978f74a2d3e9f8d1',
    26                          'leaf_hash': '25e6e9a3fa4db641ba15c80eccbf0487d3641fde0d39dfce6c85b08e4a622497',
    27                          'partial_sig': 'a8c02ea58ac0c9351dbd6dfe187b5a87acdfb7f690e7aa1dc120952dc3c37db9'}]}
    

    achow101 commented at 0:37 am on September 23, 2025:
    It must still have the correct parity bit. While it is the key must be found in the script, the script is not where the key is sourced from.
  252. rkrux commented at 3:04 pm on September 22, 2025: contributor
    Asked couple questions for clarity at 93b1d5622194165444ea9e919ccf319a5e990de0
  253. achow101 force-pushed on Sep 23, 2025
  254. achow101 force-pushed on Sep 23, 2025
  255. achow101 force-pushed on Sep 23, 2025
  256. in test/functional/wallet_musig.py:210 in 4078bbd5c7
    205+                    assert False, "Aggregate pubkey for partial sig not seen as output key or in any scripts"
    206+            else:
    207+                assert False, "Aggregate pubkey for partial sig not seen as output key"
    208+
    209+        # Non-participant aggregates partial sigs and send
    210+        finalized = self.nodes[0].finalizepsbt(comb_psig_psbt, extract=False)
    


    fjahr commented at 4:47 pm on September 25, 2025:

    This line fails in the no IPC, i686, DEBUG CI.

     0Traceback (most recent call last):
     1  File "/home/admin/actions-runner/_work/_temp/test/functional/test_framework/test_framework.py", line 199, in main
     2    self.run_test()
     3  File "/home/admin/actions-runner/_work/_temp/build/test/functional/wallet_musig.py", line 224, in run_test
     4    self.do_test("rawtr(musig(keys/*))", "rawtr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))")
     5  File "/home/admin/actions-runner/_work/_temp/build/test/functional/wallet_musig.py", line 210, in do_test
     6    finalized = self.nodes[0].finalizepsbt(comb_psig_psbt, extract=False)
     7                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     8  File "/home/admin/actions-runner/_work/_temp/test/functional/test_framework/test_node.py", line 903, in __call__
     9    return self.cli.send_cli(self.command, *args, **kwargs)
    10           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    11  File "/home/admin/actions-runner/_work/_temp/test/functional/test_framework/test_node.py", line 982, in send_cli
    12    raise JSONRPCException(dict(code=int(code), message=message))
    13test_framework.authproxy.JSONRPCException: Unknown named parameter cHNidP8BAH0CAAAAAbUnKJncaIWRKF8Z4VNQM8iITMGbABHruhKsmLanYlfxAAAAAAD9////AgBlzR0AAAAAFgAUrHinD6MdahWYjKA/oGghegf7plLUWc0dAAAAACJRIKKi33wOsfScQFUWvH/TNkyj9fsJRKfO5MIQ7twdkfDiAAAAAAABASsAypo7AAAAACJRIL7BwZwV7IU+2BvqGva8s4J9g5X+xZp+kjHUIyBGdDOzIRYH3BLspWfr7htFp3fGnuos0Vdi+6AmcbBcGRh57LDgrBkALMS05VYAAIABAACAAAAAgAIAAAAAAAAAIRaEIegPT/3eb5mTOV9Xtu+jPgB4ykDfNVDiy9Q0KuWxPBkAtrVsNlYAAIABAACAAAAAgAEAAAAAAAAAIRabohOJtNOV2FZPDTBFX+UlztSDmyX//flz2CwSWzSyVhkAEcFSbVYAAIABAACAAAAAgAAAAAAAAAAAIRa+wcGcFeyFPtgb6hr2vLOCfYOV/sWafpIx1CMgRnQzswUAA/wk7iIaAr7BwZwV7IU+2BvqGva8s4J9g5X+xZp+kjHUIyBGdDOzYwKEIegPT/3eb5mTOV9Xtu+jPgB4ykDfNVDiy9Q0KuWxPAMH3BLspWfr7htFp3fGnuos0Vdi+6AmcbBcGRh57LDgrAObohOJtNOV2FZPDTBFX+UlztSDmyX//flz2CwSWzSyVkMbAoQh6A9P/d5vmZM5X1e276M+AHjKQN81UOLL1DQq5bE8Ar7BwZwV7IU+2BvqGva8s4J9g5X+xZp+kjHUIyBGdDOzQgO3vhMvgNsuPAjeUieVp4fpSmnjOZdCbgDrGjnqL7rcOgLqKeqGlbp0gTK2U4gKSWVNQK53ALKrgBCd9tumABM8rEMbAwfcEuylZ+vuG0Wnd8ae6izRV2L7oCZxsFwZGHnssOCsAr7BwZwV7IU+2BvqGva8s4J9g5X+xZp+kjHUIyBGdDOzQgLzM72yrAXYdd045/g97aTF+NSb0uK5hotPG1Z/bFQz3wMgKAn39ddoW+B35bK7aJtFpfZqNEqtq5l9JccWhz5lu0MbA5uiE4m005XYVk8NMEVf5SXO1IObJf/9+XPYLBJbNLJWAr7BwZwV7IU+2BvqGva8s4J9g5X+xZp+kjHUIyBGdDOzQgJqMhE3c2kNklVJHE1CZ3vm0fHj2C1v1x3jNmBkfm29FwI83A9lw3pvXqk3ksVpNLXZ13ASUbuGuLicJ5fHJtD8SEMcAoQh6A9P/d5vmZM5X1e276M+AHjKQN81UOLL1DQq5bE8Ar7BwZwV7IU+2BvqGva8s4J9g5X+xZp+kjHUIyBGdDOzIMQWDp0Dxotzx8sJ+hzX8QJBdA1RJ0jTkx5szuqp1KJBQxwDB9wS7KVn6+4bRad3xp7qLNFXYvugJnGwXBkYeeyw4KwCvsHBnBXshT7YG+oa9ryzgn2Dlf7Fmn6SMdQjIEZ0M7MgJmxjzlp1tCxGxApmFk/6z+gHHLDVmCerAI9y/ivCBF1DHAObohOJtNOV2FZPDTBFX+UlztSDmyX//flz2CwSWzSyVgK+wcGcFeyFPtgb6hr2vLOCfYOV/sWafpIx1CMgRnQzsyCwKwahQMmKKCzH3rToG9MDcV6ehCTCdeRLPtIC2ok37wAAIQdXu4dqFR2Z+m5wKpJjnz+Jkj5BX70dj5I8RTzgbubc2BkAEcFSbVYAAIABAACAAAAAgAEAAAABAAAAIQeJGewgzFALKO9ZqxcXHICNEeKhwnwfvlOmwkF79hodrBkALMS05VYAAIABAACAAAAAgAMAAAABAAAAIQeiot98DrH0nEBVFrx/0zZMo/X7CUSnzuTCEO7cHZHw4gUATAK7PiEHtBQOfhCJBAeqVVveN+wxM6vVSd7pfhYoSB6RQSR52dgZALa1bDZWAACAAQAAgAAAAIACAAAAAQAAACIIAqKi33wOsfScQFUWvH/TNkyj9fsJRKfO5MIQ7twdkfDiYwKJGewgzFALKO9ZqxcXHICNEeKhwnwfvlOmwkF79hodrAK0FA5+EIkEB6pVW9437DEzq9VJ3ul+FihIHpFBJHnZ2ANXu4dqFR2Z+m5wKpJjnz+Jkj5BX70dj5I8RTzgbubc2AA (-8)
    

    I assume that happens because of the mix of named and positional arguments and the following would fix this (untested)

    0        finalized = self.nodes[0].finalizepsbt(psbt=comb_psig_psbt, extract=False)
    

    achow101 commented at 6:38 pm on September 25, 2025:
    Done
  257. in src/psbt.cpp:154 in 2a49082e49 outdated
    148@@ -149,6 +149,9 @@ void PSBTInput::FillSignatureData(SignatureData& sigdata) const
    149     for (const auto& [hash, preimage] : hash256_preimages) {
    150         sigdata.hash256_preimages.emplace(std::vector<unsigned char>(hash.begin(), hash.end()), preimage);
    151     }
    152+    sigdata.musig2_pubkeys.insert(m_musig2_participants.begin(), m_musig2_participants.end());
    153+    sigdata.musig2_pubnonces.insert(m_musig2_pubnonces.begin(), m_musig2_pubnonces.end());
    154+    sigdata.musig2_partial_sigs.insert(m_musig2_partial_sigs.begin(), m_musig2_partial_sigs.end());
    


    theStack commented at 5:20 pm on September 25, 2025:
    Compiling at commit 2a49082e491810fab48485e0a63e1fadfb09b1b7 currently fails, as the musig2_… fields haven’t been added to the SignatureData struct yet.

    achow101 commented at 6:38 pm on September 25, 2025:
    Fixed
  258. achow101 force-pushed on Sep 25, 2025
  259. in src/psbt.cpp:212 in fd52cd05d2 outdated
    203+    for (const auto& [agg_key_lh, pubnonces] : sigdata.musig2_pubnonces) {
    204+        m_musig2_pubnonces[agg_key_lh].insert(pubnonces.begin(), pubnonces.end());
    205+    }
    206+    for (const auto& [agg_key_lh, psigs] : sigdata.musig2_partial_sigs) {
    207+        m_musig2_partial_sigs[agg_key_lh].insert(psigs.begin(), psigs.end());
    208+    }
    


    theStack commented at 12:40 pm on September 26, 2025:

    in commit fd52cd05d2bf11e059472e74fe6a771aa139b136: I noticed that the creation/extension of the PSBTInput.m_musig2_{pubnonces,partial_sigs} maps is slightly more complex here (involving a loop), in contrast to the counter-part in the other direction in FillSignatureData, where a single-line .insert is used. Is that intentional, or can they be unified in either way (i.e. also only use bare .insert here, or introduce loops in FillSignatureData as well if it’s needed)?

    Right now, FillSignatureData is only called on new empty SignatureData objects, but not sure if that is an assumption in the code. If yes, then even just a plain assignment would do it, e.g.:

     0diff --git a/src/psbt.cpp b/src/psbt.cpp
     1index d144787571..4b4b988b58 100644
     2--- a/src/psbt.cpp
     3+++ b/src/psbt.cpp
     4@@ -149,9 +149,9 @@ void PSBTInput::FillSignatureData(SignatureData& sigdata) const
     5     for (const auto& [hash, preimage] : hash256_preimages) {
     6         sigdata.hash256_preimages.emplace(std::vector<unsigned char>(hash.begin(), hash.end()), preimage);
     7     }
     8-    sigdata.musig2_pubkeys.insert(m_musig2_participants.begin(), m_musig2_participants.end());
     9-    sigdata.musig2_pubnonces.insert(m_musig2_pubnonces.begin(), m_musig2_pubnonces.end());
    10-    sigdata.musig2_partial_sigs.insert(m_musig2_partial_sigs.begin(), m_musig2_partial_sigs.end());
    11+    sigdata.musig2_pubkeys = m_musig2_participants;
    12+    sigdata.musig2_pubnonces = m_musig2_pubnonces;
    13+    sigdata.musig2_partial_sigs = m_musig2_partial_sigs;
    14 }
    

    and the SignatureData could be returned directly instead of passing by reference (that’s follow-up material though, if it applies).


    achow101 commented at 9:07 pm on September 26, 2025:

    Unified.

    Right now, FillSignatureData is only called on new empty SignatureData objects, but not sure if that is an assumption in the code.

    That is not an assumption.

  260. in src/key.cpp:394 in e54d27d0f8 outdated
    389+    if (!secp256k1_keypair_create(secp256k1_context_sign, &keypair, UCharCast(begin()))) return std::nullopt;
    390+
    391+    // Get the keyagg cache and aggregate pubkey
    392+    secp256k1_musig_keyagg_cache keyagg_cache;
    393+    if (!MuSig2AggregatePubkeys(pubkeys, keyagg_cache, aggregate_pubkey)) return std::nullopt;
    394+
    


    theStack commented at 1:59 pm on September 26, 2025:

    in commit e54d27d0f81c7a9c8991516f1ed06e86d52d6c79: consistency nit: same as in CreateMuSig2AggregateSig, could also check that the pubnonces and pubkey count match here, i.e.

    0    // Check if enough pubnonces
    1    if (pubnonces.size() != pubkeys.size()) return std::nullopt;
    

    achow101 commented at 9:07 pm on September 26, 2025:
    Done
  261. in src/key.cpp:362 in 0829833bf4 outdated
    357+    if (!MuSig2AggregatePubkeys(pubkeys, keyagg_cache, aggregate_pubkey)) return {};
    358+
    359+    // Parse participant pubkey
    360+    CPubKey our_pubkey = GetPubKey();
    361+    secp256k1_pubkey pubkey;
    362+    if (!secp256k1_ec_pubkey_parse(secp256k1_context_sign, &pubkey, our_pubkey.data(), our_pubkey.size())) {
    


    theStack commented at 2:06 pm on September 26, 2025:

    in commits 0829833bf418d3fae35ceac57cae6137c1e9067d, e54d27d0f81c7a9c8991516f1ed06e86d52d6c79 and b6b66426125e46deb331927a5942e157578e712c: nit, in spirit of #33399: could use secp256k1_context_static where sufficient. If I didn’t miss anything, I think the only two secp256k1 calls introduced in this PR that need the secp256k1_context_sign context are:

    • secp256k1_musig_nonce_gen and
    • secp256k1_keypair_create

    achow101 commented at 9:08 pm on September 26, 2025:
    Done
  262. in src/script/sign.cpp:268 in 1181568816 outdated
    264@@ -265,6 +265,93 @@ static bool CreateSig(const BaseSignatureCreator& creator, SignatureData& sigdat
    265     return false;
    266 }
    267 
    268+static bool SignMuSig2(const BaseSignatureCreator& creator, SignatureData& sigdata, const SigningProvider& provider, std::vector<unsigned char>& sig_out, const KeyOriginInfo& agg_info, const XOnlyPubKey& script_pubkey, const uint256* merkle_root, const uint256* leaf_hash, SigVersion sigversion)
    


    rkrux commented at 2:56 pm on September 26, 2025:

    In 1181568816e32f378f3f9a50a0658d96541771de “sign: Create MuSig2 signatures for known MuSig2 aggregate keys” (but also in the 3 previous commits)

    The original impetus of this suggestion was for the reader to avoid reading redundant code, but having the sighash calculated only once during MuSig2 signing flow for a script_pubkey seems better from performance POV as well, even though this redundant calculation is inside wallet signing and not in the more latency sensitive operations of the node. This also made the participant derivation info filling in a separate loop before the sighash calculation.

    The nonce and partial sig calculation is done for every participant pubkey that internally calculates the same sighash every time. Consider this redundancy removal before the first MuSig2 flow is checked in.

      0diff --git a/src/script/sign.cpp b/src/script/sign.cpp
      1index e4e5859de0..8aa9aea12c 100644
      2--- a/src/script/sign.cpp
      3+++ b/src/script/sign.cpp
      4@@ -102,7 +102,7 @@ bool MutableTransactionSignatureCreator::CreateSchnorrSig(const SigningProvider&
      5     return true;
      6 }
      7 
      8-std::vector<uint8_t> MutableTransactionSignatureCreator::CreateMuSig2Nonce(const SigningProvider& provider, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion, const SignatureData& sigdata) const
      9+std::vector<uint8_t> MutableTransactionSignatureCreator::CreateMuSig2Nonce(const SigningProvider& provider, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion, const SignatureData& sigdata, const uint256& sighash) const
     10 {
     11     assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT);
     12 
     13@@ -116,21 +116,17 @@ std::vector<uint8_t> MutableTransactionSignatureCreator::CreateMuSig2Nonce(const
     14     const std::vector<CPubKey>& pubkeys = it->second;
     15     if (std::find(pubkeys.begin(), pubkeys.end(), part_pubkey) == pubkeys.end()) return {};
     16 
     17-    // Compute sighash
     18-    std::optional<uint256> sighash = ComputeSchnorrSignatureHash(leaf_hash, sigversion);
     19-    if (!sighash.has_value()) return {};
     20-
     21     MuSig2SecNonce secnonce;
     22-    std::vector<uint8_t> out = key.CreateMuSig2Nonce(secnonce, *sighash, aggregate_pubkey, pubkeys);
     23+    std::vector<uint8_t> out = key.CreateMuSig2Nonce(secnonce, sighash, aggregate_pubkey, pubkeys);
     24     if (out.empty()) return {};
     25 
     26     // Store the secnonce in the SigningProvider
     27-    provider.SetMuSig2SecNonce(MuSig2SessionID(script_pubkey, part_pubkey, *sighash), std::move(secnonce));
     28+    provider.SetMuSig2SecNonce(MuSig2SessionID(script_pubkey, part_pubkey, sighash), std::move(secnonce));
     29 
     30     return out;
     31 }
     32 
     33-bool MutableTransactionSignatureCreator::CreateMuSig2PartialSig(const SigningProvider& provider, uint256& partial_sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const
     34+bool MutableTransactionSignatureCreator::CreateMuSig2PartialSig(const SigningProvider& provider, uint256& partial_sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata, const uint256& sighash) const
     35 {
     36     assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT);
     37 
     38@@ -153,17 +149,13 @@ bool MutableTransactionSignatureCreator::CreateMuSig2PartialSig(const SigningPro
     39     // Check if enough pubnonces
     40     if (pubnonces.size() != pubkeys.size()) return false;
     41 
     42-    // Compute sighash
     43-    std::optional<uint256> sighash = ComputeSchnorrSignatureHash(leaf_hash, sigversion);
     44-    if (!sighash.has_value()) return false;
     45-
     46     // Retrieve the secnonce
     47-    uint256 session_id = MuSig2SessionID(script_pubkey, part_pubkey, *sighash);
     48+    uint256 session_id = MuSig2SessionID(script_pubkey, part_pubkey, sighash);
     49     std::optional<std::reference_wrapper<MuSig2SecNonce>> secnonce = provider.GetMuSig2SecNonce(session_id);
     50     if (!secnonce || !secnonce->get().IsValid()) return false;
     51 
     52     // Compute the sig
     53-    std::optional<uint256> sig = key.CreateMuSig2PartialSig(*sighash, aggregate_pubkey, pubkeys, pubnonces, *secnonce, tweaks);
     54+    std::optional<uint256> sig = key.CreateMuSig2PartialSig(sighash, aggregate_pubkey, pubkeys, pubnonces, *secnonce, tweaks);
     55     if (!sig) return false;
     56     partial_sig = std::move(*sig);
     57 
     58@@ -174,7 +166,7 @@ bool MutableTransactionSignatureCreator::CreateMuSig2PartialSig(const SigningPro
     59     return true;
     60 }
     61 
     62-bool MutableTransactionSignatureCreator::CreateMuSig2AggregateSig(const std::vector<CPubKey>& participants, std::vector<uint8_t>& sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const
     63+bool MutableTransactionSignatureCreator::CreateMuSig2AggregateSig(const std::vector<CPubKey>& participants, std::vector<uint8_t>& sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata, const uint256& sighash) const
     64 {
     65     assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT);
     66     if (!participants.size()) return false;
     67@@ -192,11 +184,7 @@ bool MutableTransactionSignatureCreator::CreateMuSig2AggregateSig(const std::vec
     68     if (pubnonces.size() != participants.size()) return false;
     69     if (partial_sigs.size() != participants.size()) return false;
     70 
     71-    // Compute sighash
     72-    std::optional<uint256> sighash = ComputeSchnorrSignatureHash(leaf_hash, sigversion);
     73-    if (!sighash.has_value()) return false;
     74-
     75-    std::optional<std::vector<uint8_t>> res = ::CreateMuSig2AggregateSig(participants, aggregate_pubkey, tweaks, *sighash, pubnonces, partial_sigs);
     76+    std::optional<std::vector<uint8_t>> res = ::CreateMuSig2AggregateSig(participants, aggregate_pubkey, tweaks, sighash, pubnonces, partial_sigs);
     77     if (!res) return false;
     78     sig = res.value();
     79     if (nHashType) sig.push_back(nHashType);
     80@@ -269,10 +257,10 @@ static bool SignMuSig2(const BaseSignatureCreator& creator, SignatureData& sigda
     81 {
     82     Assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT);
     83 
     84+    // Fill participant derivation path info
     85     for (const auto& [agg_pub, part_pks] : sigdata.musig2_pubkeys) {
     86         if (part_pks.empty()) continue;
     87 
     88-        // Fill participant derivation path info
     89         for (const auto& part_pk : part_pks) {
     90             KeyOriginInfo part_info;
     91             if (provider.GetKeyOrigin(part_pk.GetID(), part_info)) {
     92@@ -284,6 +272,15 @@ static bool SignMuSig2(const BaseSignatureCreator& creator, SignatureData& sigda
     93                 if (leaf_hash) it->second.first.insert(*leaf_hash);
     94             }
     95         }
     96+    }
     97+
     98+    // Compute sighash
     99+    std::optional<uint256> sighash = creator.ComputeSchnorrSignatureHash(leaf_hash, sigversion);
    100+    if (!sighash.has_value()) return false;
    101+
    102+    // Execute signing flow
    103+    for (const auto& [agg_pub, part_pks] : sigdata.musig2_pubkeys) {
    104+        if (part_pks.empty()) continue;
    105 
    106         // The pubkey in the script may not be the actual aggregate of the participants, but derived from it.
    107         // Check the derivation, and compute the BIP 32 derivation tweaks
    108@@ -318,7 +315,7 @@ static bool SignMuSig2(const BaseSignatureCreator& creator, SignatureData& sigda
    109         }
    110 
    111         // First try to aggregate
    112-        if (creator.CreateMuSig2AggregateSig(part_pks, sig_out, agg_pub, plain_pub, leaf_hash, tweaks, sigversion, sigdata)) {
    113+        if (creator.CreateMuSig2AggregateSig(part_pks, sig_out, agg_pub, plain_pub, leaf_hash, tweaks, sigversion, sigdata, *sighash)) {
    114             if (sigversion == SigVersion::TAPROOT) {
    115                 sigdata.taproot_key_path_sig = sig_out;
    116             } else {
    117@@ -331,7 +328,7 @@ static bool SignMuSig2(const BaseSignatureCreator& creator, SignatureData& sigda
    118         auto pub_key_leaf_hash = std::make_pair(plain_pub, leaf_hash ? *leaf_hash : uint256());
    119         for (const CPubKey& part_pk : part_pks) {
    120             uint256 partial_sig;
    121-            if (creator.CreateMuSig2PartialSig(provider, partial_sig, agg_pub, plain_pub, part_pk, leaf_hash, tweaks, sigversion, sigdata) && Assume(!partial_sig.IsNull())) {
    122+            if (creator.CreateMuSig2PartialSig(provider, partial_sig, agg_pub, plain_pub, part_pk, leaf_hash, tweaks, sigversion, sigdata, *sighash) && Assume(!partial_sig.IsNull())) {
    123                 sigdata.musig2_partial_sigs[pub_key_leaf_hash].emplace(part_pk, partial_sig);
    124             }
    125         }
    126@@ -344,7 +341,7 @@ static bool SignMuSig2(const BaseSignatureCreator& creator, SignatureData& sigda
    127         std::map<CPubKey, std::vector<uint8_t>>& pubnonces = sigdata.musig2_pubnonces[pub_key_leaf_hash];
    128         for (const CPubKey& part_pk : part_pks) {
    129             if (pubnonces.contains(part_pk)) continue;
    130-            std::vector<uint8_t> pubnonce = creator.CreateMuSig2Nonce(provider, agg_pub, plain_pub, part_pk, leaf_hash, merkle_root, sigversion, sigdata);
    131+            std::vector<uint8_t> pubnonce = creator.CreateMuSig2Nonce(provider, agg_pub, plain_pub, part_pk, leaf_hash, merkle_root, sigversion, sigdata, *sighash);
    132             if (pubnonce.empty()) continue;
    133             pubnonces[part_pk] = std::move(pubnonce);
    134         }
    135@@ -969,18 +966,22 @@ public:
    136         sig.assign(64, '\000');
    137         return true;
    138     }
    139-    std::vector<uint8_t> CreateMuSig2Nonce(const SigningProvider& provider, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion, const SignatureData& sigdata) const override
    140+    std::optional<uint256> ComputeSchnorrSignatureHash(const uint256* leaf_hash, SigVersion sigversion) const override
    141+    {
    142+        return uint256::ONE;
    143+    }
    144+    std::vector<uint8_t> CreateMuSig2Nonce(const SigningProvider& provider, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion, const SignatureData& sigdata, const uint256& sighash) const override
    145     {
    146         std::vector<uint8_t> out;
    147         out.assign(MUSIG2_PUBNONCE_SIZE, '\000');
    148         return out;
    149     }
    150-    bool CreateMuSig2PartialSig(const SigningProvider& provider, uint256& partial_sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const override
    151+    bool CreateMuSig2PartialSig(const SigningProvider& provider, uint256& partial_sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata, const uint256& sighash) const override
    152     {
    153         partial_sig = uint256::ONE;
    154         return true;
    155     }
    156-    bool CreateMuSig2AggregateSig(const std::vector<CPubKey>& participants, std::vector<uint8_t>& sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const override
    157+    bool CreateMuSig2AggregateSig(const std::vector<CPubKey>& participants, std::vector<uint8_t>& sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata, const uint256& sighash) const override
    158     {
    159         sig.assign(64, '\000');
    160         return true;
    161diff --git a/src/script/sign.h b/src/script/sign.h
    162index dd86a8066a..009d70b9ac 100644
    163--- a/src/script/sign.h
    164+++ b/src/script/sign.h
    165@@ -34,9 +34,10 @@ public:
    166     /** Create a singular (non-script) signature. */
    167     virtual bool CreateSig(const SigningProvider& provider, std::vector<unsigned char>& vchSig, const CKeyID& keyid, const CScript& scriptCode, SigVersion sigversion) const =0;
    168     virtual bool CreateSchnorrSig(const SigningProvider& provider, std::vector<unsigned char>& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion) const =0;
    169-    virtual std::vector<uint8_t> CreateMuSig2Nonce(const SigningProvider& provider, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion, const SignatureData& sigdata) const =0;
    170-    virtual bool CreateMuSig2PartialSig(const SigningProvider& provider, uint256& partial_sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const =0;
    171-    virtual bool CreateMuSig2AggregateSig(const std::vector<CPubKey>& participants, std::vector<uint8_t>& sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const =0;
    172+    virtual std::optional<uint256> ComputeSchnorrSignatureHash(const uint256* leaf_hash, SigVersion sigversion) const =0;
    173+    virtual std::vector<uint8_t> CreateMuSig2Nonce(const SigningProvider& provider, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion, const SignatureData& sigdata, const uint256& sighash) const =0;
    174+    virtual bool CreateMuSig2PartialSig(const SigningProvider& provider, uint256& partial_sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata, const uint256& sighash) const =0;
    175+    virtual bool CreateMuSig2AggregateSig(const std::vector<CPubKey>& participants, std::vector<uint8_t>& sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata, const uint256& sighash) const =0;
    176 };
    177 
    178 /** A signature creator for transactions. */
    179@@ -49,17 +50,16 @@ class MutableTransactionSignatureCreator : public BaseSignatureCreator
    180     const MutableTransactionSignatureChecker checker;
    181     const PrecomputedTransactionData* m_txdata;
    182 
    183-    std::optional<uint256> ComputeSchnorrSignatureHash(const uint256* leaf_hash, SigVersion sigversion) const;
    184-
    185 public:
    186     MutableTransactionSignatureCreator(const CMutableTransaction& tx LIFETIMEBOUND, unsigned int input_idx, const CAmount& amount, int hash_type);
    187     MutableTransactionSignatureCreator(const CMutableTransaction& tx LIFETIMEBOUND, unsigned int input_idx, const CAmount& amount, const PrecomputedTransactionData* txdata, int hash_type);
    188     const BaseSignatureChecker& Checker() const override { return checker; }
    189     bool CreateSig(const SigningProvider& provider, std::vector<unsigned char>& vchSig, const CKeyID& keyid, const CScript& scriptCode, SigVersion sigversion) const override;
    190     bool CreateSchnorrSig(const SigningProvider& provider, std::vector<unsigned char>& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion) const override;
    191-    std::vector<uint8_t> CreateMuSig2Nonce(const SigningProvider& provider, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion, const SignatureData& sigdata) const override;
    192-    bool CreateMuSig2PartialSig(const SigningProvider& provider, uint256& partial_sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const override;
    193-    bool CreateMuSig2AggregateSig(const std::vector<CPubKey>& participants, std::vector<uint8_t>& sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata) const override;
    194+    std::optional<uint256> ComputeSchnorrSignatureHash(const uint256* leaf_hash, SigVersion sigversion) const override;
    195+    std::vector<uint8_t> CreateMuSig2Nonce(const SigningProvider& provider, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion, const SignatureData& sigdata, const uint256& sighash) const override;
    196+    bool CreateMuSig2PartialSig(const SigningProvider& provider, uint256& partial_sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const CPubKey& part_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata, const uint256& sighash) const override;
    197+    bool CreateMuSig2AggregateSig(const std::vector<CPubKey>& participants, std::vector<uint8_t>& sig, const CPubKey& aggregate_pubkey, const CPubKey& script_pubkey, const uint256* leaf_hash, const std::vector<std::pair<uint256, bool>>& tweaks, SigVersion sigversion, const SignatureData& sigdata, const uint256& sighash) const override;
    198 };
    199 
    200 /** A signature checker that accepts all signatures */
    

    achow101 commented at 6:27 pm on September 26, 2025:
    The sighash is calculated multiple times during signing outside of musig signing too. I will leave refactoring of this to a followup.
  263. in test/functional/wallet_musig.py:132 in 02994c2cbe outdated
    127+                continue
    128+            if utxo is None:
    129+                utxo = wallet.listunspent()[0]
    130+            else:
    131+                assert_equal(utxo, wallet.listunspent()[0])
    132+        psbt = wallets[0].walletcreatefundedpsbt(outputs=[{self.def_wallet.getnewaddress(): 5}], inputs=[utxo], change_type="bech32m", changePosition=1)["psbt"]
    


    rkrux commented at 3:04 pm on September 26, 2025:

    In 02994c2cbe2f051b868f49e65fac042feace2edf “test: Test MuSig2 in the wallet”

    In continuation to the previous suggestion of handling the cases where the musig descriptor is not known to the wallet and instead only few keys are that could sign - https://github.com/bitcoin/bitcoin/pull/29675/#discussion_r2368738533 (thanks for adding the fix btw).

    When I was debugging this case earlier, 2 tests failed because of incorrect number of nonces and partial sigs added - earlier send RPC was used instead of walletcreatefundedpsbt that also tried to sign initially.

    IMO, using walletcreatefundedpsbt seems better for the MuSig2 flow because it is expected (and likely) that the first round (so to speak) will not have the transaction fully signed.

    But should this be mentioned somewhere either in the RPC doc or in the docs directory that send RPC is not to be used for MuSig2 signing?


    achow101 commented at 6:26 pm on September 26, 2025:
    The test failure with send was because of sighash types, not that send doesn’t work for musig. It was failing on the test that changes the sighash type.
  264. rkrux commented at 3:23 pm on September 26, 2025: contributor

    Code review 02994c2cbe2f051b868f49e65fac042feace2edf

    Thanks for addressing the previous comments.

    I’m about to wrap up my review by taking a look at the libsecp specific functions used in the CreateMuSig2* functions.

  265. glozow referenced this in commit 7e08445449 on Sep 26, 2025
  266. achow101 force-pushed on Sep 26, 2025
  267. in src/key.cpp:375 in 793d727791 outdated
    370+    // Generate nonce
    371+    secp256k1_musig_pubnonce pubnonce;
    372+    if (!secp256k1_musig_nonce_gen(secp256k1_context_sign, secnonce.Get(), &pubnonce, rand.data(), UCharCast(begin()), &pubkey, sighash.data(), &keyagg_cache, nullptr)) {
    373+        return {};
    374+    }
    375+
    


    rkrux commented at 1:06 pm on September 29, 2025:

    In 793d727791fe042bbb4a9ff5df3db012a77a40fe “sign: Add CreateMuSig2Nonce”

    Wouldn’t hurt to add this assert imo. I’m inclining to fail early the whole process in case of any such scenarios.

     0diff --git a/src/key.cpp b/src/key.cpp
     1index a952acb260..f7e065c8da 100644
     2--- a/src/key.cpp
     3+++ b/src/key.cpp
     4@@ -372,6 +372,8 @@ std::vector<uint8_t> CKey::CreateMuSig2Nonce(MuSig2SecNonce& secnonce, const uin
     5     if (!secp256k1_musig_nonce_gen(secp256k1_context_sign, secnonce.Get(), &pubnonce, rand.data(), UCharCast(begin()), &pubkey, sighash.data(), &keyagg_cache, nullptr)) {
     6         return {};
     7     }
     8+    // `rand` should have been set to 0 after the nonces are generated successfully
     9+    assert(rand.IsNull());
    10
    11     // Serialize pubnonce
    12     std::vector<uint8_t> out;
    

    achow101 commented at 5:45 pm on September 30, 2025:
    I don’t think it is necessary to check this, we can assume that libsecp is working correctly.
  268. in test/functional/wallet_musig.py:123 in 36f83554a2 outdated
    118+
    119+        # Fund that address
    120+        self.def_wallet.sendtoaddress(addr, 10)
    121+        self.generate(self.nodes[0], 1)
    122+
    123+        # Spend that UTXO
    


    rkrux commented at 1:15 pm on September 29, 2025:

    In 36f83554a2dc5b397ee3b4495d8bf2aff36cedc6 “test: Test MuSig2 in the wallet”

    I feel it’d be prudent to be comprehensive in testing here. A more practical scenario would be where the MuSig2 unspents are combined with non-MuSig2/Taproot unspents, and/or there could be multiple MuSig2 unspents in the PSBT. The following diff tests for:

    1. 2 MuSig2 inputs in the PSBT along with 1 SegWit unspent. A secondary reason is to avoid seeing dec_psbt["inputs"][0] in several places.
    2. Iterates all the test cases over ALL|ANYONECANPAY sighash type.
      0diff --git a/test/functional/wallet_musig.py b/test/functional/wallet_musig.py
      1index 277ca9276e..0b52296e16 100755
      2--- a/test/functional/wallet_musig.py
      3+++ b/test/functional/wallet_musig.py
      4@@ -20,16 +20,29 @@ MULTIPATH_TWO_RE = re.compile(r"<(\d+);(\d+)>")
      5 MUSIG_RE = re.compile(r"musig\((.*?)\)")
      6 PLACEHOLDER_RE = re.compile(r"\$\d")
      7 
      8+def filter_musig2_unspents(unspents):
      9+    return [unspent for unspent in unspents if "musig(" in unspent["parent_descs"][0]]
     10+
     11+def filter_musig2_inputs(psbt_inputs):
     12+    return [psbt_input for psbt_input in psbt_inputs if "musig2_participant_pubkeys" in psbt_input]
     13+
     14+def filter_musig2_finalized_inputs(psbt_inputs):
     15+    # approximate heuristic to filter the MuSig2 input given this test setup
     16+    return [psbt_input for psbt_input in psbt_inputs if psbt_input["witness_utxo"]["scriptPubKey"]["type"] == "witness_v1_taproot"]
     17+
     18 class WalletMuSigTest(BitcoinTestFramework):
     19     WALLET_NUM = 0
     20+    MUSIG2_INPUTS_NUM = 2
     21+
     22     def set_test_params(self):
     23         self.num_nodes = 1
     24+        self.extra_args = [[f"-keypool={self.MUSIG2_INPUTS_NUM}"]]
     25 
     26     def skip_test_if_missing_module(self):
     27         self.skip_if_no_wallet()
     28 
     29     def do_test(self, comment, pattern, sighash_type=None, scriptpath=False, nosign_wallets=None, only_one_musig_wallet=False):
     30-        self.log.info(f"Testing {comment}")
     31+        self.log.info(f"Testing {comment} with sighash_type {sighash_type}")
     32         has_internal = MULTIPATH_TWO_RE.search(pattern) is not None
     33 
     34         wallets = []
     35@@ -80,6 +93,7 @@ class WalletMuSigTest(BitcoinTestFramework):
     36         # Construct and import each wallet's musig descriptor that
     37         # contains the private key from that wallet and pubkeys of the others
     38         for i, wallet in enumerate(wallets):
     39+            # The first wallet is assumed to be a MuSig2 wallet always
     40             if only_one_musig_wallet and i > 0:
     41                 continue
     42             desc = pat
     43@@ -100,47 +114,56 @@ class WalletMuSigTest(BitcoinTestFramework):
     44             for r in res:
     45                 assert_equal(r["success"], True)
     46 
     47-        # Check that the wallets agree on the same musig address
     48-        addr = None
     49-        change_addr = None
     50-        for i, wallet in enumerate(wallets):
     51-            if only_one_musig_wallet and i > 0:
     52-                continue
     53-            if addr is None:
     54-                addr = wallet.getnewaddress(address_type="bech32m")
     55-            else:
     56-                assert_equal(addr, wallet.getnewaddress(address_type="bech32m"))
     57-            if has_internal:
     58-                if change_addr is None:
     59-                    change_addr = wallet.getrawchangeaddress(address_type="bech32m")
     60+        # Check that the wallets agree on the same MuSig2 addresses
     61+        for _ in range(0, self.MUSIG2_INPUTS_NUM):
     62+            addr = None
     63+            change_addr = None
     64+            for i, wallet in enumerate(wallets):
     65+                # The first wallet is assumed to be a MuSig2 wallet always
     66+                if only_one_musig_wallet and i > 0:
     67+                    continue
     68+                if addr is None:
     69+                    addr = wallet.getnewaddress(address_type="bech32m")
     70                 else:
     71-                    assert_equal(change_addr, wallet.getrawchangeaddress(address_type="bech32m"))
     72+                    assert_equal(addr, wallet.getnewaddress(address_type="bech32m"))
     73+                if has_internal:
     74+                    if change_addr is None:
     75+                        change_addr = wallet.getrawchangeaddress(address_type="bech32m")
     76+                    else:
     77+                        assert_equal(change_addr, wallet.getrawchangeaddress(address_type="bech32m"))
     78+            # Fund that address
     79+            self.def_wallet.sendtoaddress(addr, 10)
     80 
     81-        # Fund that address
     82-        self.def_wallet.sendtoaddress(addr, 10)
     83+        # Send some funds to a non-MuSig2 address in the first wallet to simulate real-world scenario
     84+        self.def_wallet.sendtoaddress(wallets[0].getnewaddress(address_type="bech32"), 5)
     85         self.generate(self.nodes[0], 1)
     86 
     87-        # Spend that UTXO
     88-        utxo = None
     89+        # Spend the MuSig2 UTXOs
     90+        utxos = None
     91         for i, wallet in enumerate(wallets):
     92             if only_one_musig_wallet and i > 0:
     93                 continue
     94-            if utxo is None:
     95-                utxo = wallet.listunspent()[0]
     96+            if utxos is None:
     97+                utxos = filter_musig2_unspents(wallet.listunspent())
     98             else:
     99-                assert_equal(utxo, wallet.listunspent()[0])
    100-        psbt = wallets[0].walletcreatefundedpsbt(outputs=[{self.def_wallet.getnewaddress(): 5}], inputs=[utxo], change_type="bech32m", changePosition=1)["psbt"]
    101+                assert_equal(utxos, wallet.listunspent())
    102+
    103+        change_index = 1
    104+        # Spend more than what the MuSig2 unspents have so that non-MuSig2 inputs are also selected
    105+        psbt = wallets[0].walletcreatefundedpsbt(outputs=[{self.def_wallet.getnewaddress(): 21}], inputs=utxos, change_type="bech32m", changePosition=change_index, add_inputs=True)["psbt"]
    106 
    107         dec_psbt = self.nodes[0].decodepsbt(psbt)
    108-        assert_equal(len(dec_psbt["inputs"]), 1)
    109-        assert_equal(len(dec_psbt["inputs"][0]["musig2_participant_pubkeys"]), pattern.count("musig("))
    110+        musig2_psbt_inputs = filter_musig2_inputs(dec_psbt["inputs"])
    111+        assert_equal(len(musig2_psbt_inputs), self.MUSIG2_INPUTS_NUM)
    112+        for musig2_psbt_input in musig2_psbt_inputs:
    113+            assert_equal(len(musig2_psbt_input["musig2_participant_pubkeys"]), pattern.count("musig("))
    114         if has_internal:
    115-            assert_equal(len(dec_psbt["outputs"][1]["musig2_participant_pubkeys"]), pattern.count("musig("))
    116+            assert_equal(len(dec_psbt["outputs"][change_index]["musig2_participant_pubkeys"]), pattern.count("musig("))
    117 
    118         # Check all participant pubkeys in the input and change output
    119-        psbt_maps = [dec_psbt["inputs"][0]]
    120+        psbt_maps = musig2_psbt_inputs
    121         if has_internal:
    122-            psbt_maps.append(dec_psbt["outputs"][1])
    123+            psbt_maps.append(dec_psbt["outputs"][change_index])
    124         for psbt_map in psbt_maps:
    125             part_pks = set()
    126             for agg in psbt_map["musig2_participant_pubkeys"]:
    127@@ -166,19 +189,21 @@ class WalletMuSigTest(BitcoinTestFramework):
    128         comb_nonce_psbt = self.nodes[0].combinepsbt(nonce_psbts)
    129 
    130         dec_psbt = self.nodes[0].decodepsbt(comb_nonce_psbt)
    131-        assert_equal(len(dec_psbt["inputs"][0]["musig2_pubnonces"]), expected_pubnonces)
    132-        for pn in dec_psbt["inputs"][0]["musig2_pubnonces"]:
    133-            pubkey = pn["aggregate_pubkey"][2:]
    134-            if pubkey in dec_psbt["inputs"][0]["witness_utxo"]["scriptPubKey"]["hex"]:
    135-                continue
    136-            elif "taproot_scripts" in dec_psbt["inputs"][0]:
    137-                for leaf_scripts in dec_psbt["inputs"][0]["taproot_scripts"]:
    138-                    if pubkey in leaf_scripts["script"]:
    139-                        break
    140+        musig2_psbt_inputs = filter_musig2_inputs(dec_psbt["inputs"])
    141+        for musig2_psbt_input in musig2_psbt_inputs:
    142+            assert_equal(len(musig2_psbt_input["musig2_pubnonces"]), expected_pubnonces)
    143+            for pn in musig2_psbt_input["musig2_pubnonces"]:
    144+                pubkey = pn["aggregate_pubkey"][2:]
    145+                if pubkey in musig2_psbt_input["witness_utxo"]["scriptPubKey"]["hex"]:
    146+                    continue
    147+                elif "taproot_scripts" in musig2_psbt_input:
    148+                    for leaf_scripts in musig2_psbt_input["taproot_scripts"]:
    149+                        if pubkey in leaf_scripts["script"]:
    150+                            break
    151+                    else:
    152+                        assert False, "Aggregate pubkey for pubnonce not seen as output key, or in any scripts"
    153                 else:
    154-                    assert False, "Aggregate pubkey for pubnonce not seen as output key, or in any scripts"
    155-            else:
    156-                assert False, "Aggregate pubkey for pubnonce not seen as output key or internal key"
    157+                    assert False, "Aggregate pubkey for pubnonce not seen as output key or internal key"
    158 
    159         # Add partial sigs
    160         psig_psbts = []
    161@@ -192,28 +217,32 @@ class WalletMuSigTest(BitcoinTestFramework):
    162         comb_psig_psbt = self.nodes[0].combinepsbt(psig_psbts)
    163 
    164         dec_psbt = self.nodes[0].decodepsbt(comb_psig_psbt)
    165-        assert_equal(len(dec_psbt["inputs"][0]["musig2_partial_sigs"]), expected_partial_sigs)
    166-        for ps in dec_psbt["inputs"][0]["musig2_partial_sigs"]:
    167-            pubkey = ps["aggregate_pubkey"][2:]
    168-            if pubkey in dec_psbt["inputs"][0]["witness_utxo"]["scriptPubKey"]["hex"]:
    169-                continue
    170-            elif "taproot_scripts" in dec_psbt["inputs"][0]:
    171-                for leaf_scripts in dec_psbt["inputs"][0]["taproot_scripts"]:
    172-                    if pubkey in leaf_scripts["script"]:
    173-                        break
    174+        musig2_psbt_inputs = filter_musig2_inputs(dec_psbt["inputs"])
    175+        for musig2_psbt_input in musig2_psbt_inputs:
    176+            assert_equal(len(musig2_psbt_input["musig2_partial_sigs"]), expected_partial_sigs)
    177+            for ps in musig2_psbt_input["musig2_partial_sigs"]:
    178+                pubkey = ps["aggregate_pubkey"][2:]
    179+                if pubkey in musig2_psbt_input["witness_utxo"]["scriptPubKey"]["hex"]:
    180+                    continue
    181+                elif "taproot_scripts" in musig2_psbt_input:
    182+                    for leaf_scripts in musig2_psbt_input["taproot_scripts"]:
    183+                        if pubkey in leaf_scripts["script"]:
    184+                            break
    185+                    else:
    186+                        assert False, "Aggregate pubkey for partial sig not seen as output key or in any scripts"
    187                 else:
    188-                    assert False, "Aggregate pubkey for partial sig not seen as output key or in any scripts"
    189-            else:
    190-                assert False, "Aggregate pubkey for partial sig not seen as output key"
    191+                    assert False, "Aggregate pubkey for partial sig not seen as output key"
    192 
    193         # Non-participant aggregates partial sigs and send
    194         finalized = self.nodes[0].finalizepsbt(psbt=comb_psig_psbt, extract=False)
    195-        assert_equal(finalized["complete"], True)
    196-        witness = self.nodes[0].decodepsbt(finalized["psbt"])["inputs"][0]["final_scriptwitness"]
    197-        if scriptpath:
    198-            assert_greater_than(len(witness), 1)
    199-        else:
    200-            assert_equal(len(witness), 1)
    201+        musig2_psbt_inputs = filter_musig2_finalized_inputs(self.nodes[0].decodepsbt(finalized["psbt"])["inputs"])
    202+        for musig2_psbt_input in musig2_psbt_inputs:
    203+            witness = musig2_psbt_input["final_scriptwitness"]
    204+            if scriptpath:
    205+                assert_greater_than(len(witness), 1)
    206+            else:
    207+                assert_equal(len(witness), 1)
    208+
    209         finalized = self.nodes[0].finalizepsbt(comb_psig_psbt)
    210         assert "hex" in finalized
    211         self.nodes[0].sendrawtransaction(finalized["hex"])
    212@@ -221,22 +250,21 @@ class WalletMuSigTest(BitcoinTestFramework):
    213     def run_test(self):
    214         self.def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
    215 
    216-        self.do_test("rawtr(musig(keys/*))", "rawtr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))")
    217-        self.do_test("rawtr(musig(keys/*)) with ALL|ANYONECANPAY", "rawtr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))", "ALL|ANYONECANPAY")
    218-        self.do_test("tr(musig(keys/*)) no multipath", "tr(musig($0/0/*,$1/1/*,$2/2/*))")
    219-        self.do_test("tr(musig(keys/*)) 2 index multipath", "tr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))")
    220-        self.do_test("tr(musig(keys/*)) 3 index multipath", "tr(musig($0/<0;1;2>/*,$1/<1;2;3>/*,$2/<2;3;4>/*))")
    221-        self.do_test("rawtr(musig/*)", "rawtr(musig($0,$1,$2)/<0;1>/*)")
    222-        self.do_test("tr(musig/*)", "tr(musig($0,$1,$2)/<0;1>/*)")
    223-        self.do_test("rawtr(musig(keys/*)) without all wallets importing", "rawtr(musig($0/<0;1>/*,$1/<0;1>/*,$2/<0;1>/*))", only_one_musig_wallet=True)
    224-        self.do_test("tr(musig(keys/*)) without all wallets importing", "tr(musig($0/<0;1>/*,$1/<0;1>/*,$2/<0;1>/*))", only_one_musig_wallet=True)
    225-        self.do_test("tr(H, pk(musig(keys/*)))", "tr($H,pk(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*)))", scriptpath=True)
    226-        self.do_test("tr(H,pk(musig/*))", "tr($H,pk(musig($0,$1,$2)/<0;1>/*))", scriptpath=True)
    227-        self.do_test("tr(H,{pk(musig/*), pk(musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($3,$4,$5)/0/*)})", scriptpath=True)
    228-        self.do_test("tr(H,{pk(musig/*), pk(same keys different musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($1,$2)/0/*)})", scriptpath=True)
    229-        self.do_test("tr(musig/*,{pk(partial keys diff musig-1/*),pk(partial keys diff musig-2/*)})}", "tr(musig($0,$1,$2)/<3;4>/*,{pk(musig($0,$1)/<5;6>/*),pk(musig($1,$2)/7/*)})")
    230-        self.do_test("tr(musig/*,{pk(partial keys diff musig-1/*),pk(partial keys diff musig-2/*)})} script-path", "tr(musig($0,$1,$2)/<3;4>/*,{pk(musig($0,$1)/<5;6>/*),pk(musig($1,$2)/7/*)})", scriptpath=True, nosign_wallets=[0])
    231-
    232+        for sighash_type in [None, "ALL|ANYONECANPAY"]:
    233+            self.do_test("rawtr(musig(keys/*))", "rawtr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))", sighash_type)
    234+            self.do_test("tr(musig(keys/*)) no multipath", "tr(musig($0/0/*,$1/1/*,$2/2/*))", sighash_type)
    235+            self.do_test("tr(musig(keys/*)) 2 index multipath", "tr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))", sighash_type)
    236+            self.do_test("tr(musig(keys/*)) 3 index multipath", "tr(musig($0/<0;1;2>/*,$1/<1;2;3>/*,$2/<2;3;4>/*))", sighash_type)
    237+            self.do_test("rawtr(musig/*)", "rawtr(musig($0,$1,$2)/<0;1>/*)", sighash_type)
    238+            self.do_test("tr(musig/*)", "tr(musig($0,$1,$2)/<0;1>/*)", sighash_type)
    239+            self.do_test("rawtr(musig(keys/*)) without all wallets importing", "rawtr(musig($0/<0;1>/*,$1/<0;1>/*,$2/<0;1>/*))", sighash_type, only_one_musig_wallet=True)
    240+            self.do_test("tr(musig(keys/*)) without all wallets importing", "tr(musig($0/<0;1>/*,$1/<0;1>/*,$2/<0;1>/*))", sighash_type, only_one_musig_wallet=True)
    241+            self.do_test("tr(H, pk(musig(keys/*)))", "tr($H,pk(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*)))", sighash_type, scriptpath=True)
    242+            self.do_test("tr(H,pk(musig/*))", "tr($H,pk(musig($0,$1,$2)/<0;1>/*))", sighash_type, scriptpath=True)
    243+            self.do_test("tr(H,{pk(musig/*), pk(musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($3,$4,$5)/0/*)})", sighash_type, scriptpath=True)
    244+            self.do_test("tr(H,{pk(musig/*), pk(same keys different musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($1,$2)/0/*)})", sighash_type, scriptpath=True)
    245+            self.do_test("tr(musig/*,{pk(partial keys diff musig-1/*),pk(partial keys diff musig-2/*)})}", "tr(musig($0,$1,$2)/<3;4>/*,{pk(musig($0,$1)/<5;6>/*),pk(musig($1,$2)/7/*)})", sighash_type)
    246+            self.do_test("tr(musig/*,{pk(partial keys diff musig-1/*),pk(partial keys diff musig-2/*)})} script-path", "tr(musig($0,$1,$2)/<3;4>/*,{pk(musig($0,$1)/<5;6>/*),pk(musig($1,$2)/7/*)})", sighash_type, scriptpath=True, nosign_wallets=[2])
    247 
    248 if __name__ == '__main__':
    249     WalletMuSigTest(__file__).main()
    

    achow101 commented at 6:00 pm on September 30, 2025:

    A more practical scenario would be where the MuSig2 unspents are combined with non-MuSig2/Taproot unspents

    I don’t think multisig inputs tend to be mixed with non-multisig inputs.

    A secondary reason is to avoid seeing dec_psbt["inputs"][0] in several places.

    I prefer to have these tests check the input that we know is musig rather than searching for it dynamically.

    Iterates all the test cases over ALL|ANYONECANPAY sighash type.

    There’s nothing special about ALL|ANYONECANPAY that it needs to be tested in all cases. We just need one test to use the non-default sighash type.


    rkrux commented at 9:20 am on October 1, 2025:

    I don’t think multisig inputs tend to be mixed with non-multisig inputs.

    Ah, I should have rephrased the sentence to highlight spending multiple MuSig2 unspents in the PSBT instead of highlighting the other one.

  269. rkrux commented at 1:24 pm on September 29, 2025: contributor

    Code review complete at 36f83554a2dc5b397ee3b4495d8bf2aff36cedc6.

    I will share my final thoughts in a subsequent comment.

  270. in src/script/sign.cpp:88 in fb8720f1e0 outdated
    90+    return hash;
    91+}
    92+
    93+bool MutableTransactionSignatureCreator::CreateSchnorrSig(const SigningProvider& provider, std::vector<unsigned char>& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion) const
    94+{
    95+    assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT);
    


    fjahr commented at 1:43 pm on September 30, 2025:
    nit: Not sure if assert is needed in both places now, seems like keeping it in ComputeSchnorrSignatureHash might be enough.

    achow101 commented at 0:18 am on October 4, 2025:
    If I need to retouch.
  271. in src/script/sign.cpp:587 in c14a4bcbb1 outdated
    585+                KeyOriginInfo info;
    586+                auto misc_pk_it = sigdata.taproot_misc_pubkeys.find(pk);
    587+                if (misc_pk_it != sigdata.taproot_misc_pubkeys.end()) {
    588+                    info = misc_pk_it->second.second;
    589+                }
    590+                SignMuSig2(creator, sigdata, provider, sig, info, pk, merkle_root, /*leaf_hash=*/nullptr, SigVersion::TAPROOT);
    


    theStack commented at 3:56 pm on September 30, 2025:
    potential refactoring idea: the derivation path lookup prior to calling SignMuSig2 currently happens for both key- and script-path spending. Could it be moved inside of SignMuSig2 in order to deduplicate (and also reduce the interface by one parameter, as the KeyOriginInfo object would be created inside)? Tried this here, and the tests still pass: https://github.com/theStack/bitcoin/commit/fc20504660aed9674ae27482fad885b5b9fe399f

    achow101 commented at 6:20 pm on September 30, 2025:
    Done
  272. pubkey: Return tweaks from BIP32 derivation
    MuSig2 needs the BIP32 derivation tweaks in order to sign with a key
    derived from the aggregate pubkey.
    4b24bfeab9
  273. sign: Include taproot output key's KeyOriginInfo in sigdata 9baff05e49
  274. Add MuSig2SecNonce class for secure allocation of musig nonces c06a1dc86f
  275. signingprovider: Add musig2 secnonces
    Adds GetMuSig2SecNonces which returns secp256k1_musig_secnonce*, and
    DeleteMuSig2Session which removes the MuSig2 secnonce from wherever it
    was retrieved. FlatSigningProvider stores it as a pointer to a map of
    session id to secnonce so that deletion will actually delete from the
    object that actually owns the secnonces.
    
    The session id is just a unique identifier for the caller to determine
    what secnonces have been created.
    4d8b4f5336
  276. psbt: MuSig2 data in Fill/FromSignatureData d99a081679
  277. musig: Add MuSig2AggregatePubkeys variant that validates the aggregate
    A common pattern that MuSig2 functions will use is to aggregate the
    pubkeys to get the keyagg_cache and then validate the aggregated pubkey
    against a provided aggregate pubkey. A variant of MuSig2AggregatePubkeys
    is added which does that.
    
    The functionality of GetMuSig2KeyAggCache and GetCPubKeyFromMuSig2KeyAggCache
    are included in MuSig2AggregatePubkeys (and used internally) so there is
    no expectation that callers will need these so they are made static.
    82ea67c607
  278. sign: Add CreateMuSig2Nonce 512b17fc56
  279. sign: Add CreateMuSig2PartialSig bf69442b3f
  280. sign: Add CreateMuSig2AggregateSig 258db93889
  281. sign: Create MuSig2 signatures for known MuSig2 aggregate keys
    When creating Taproot signatures, if the key being signed for is known
    to be a MuSig2 aggregate key, do the MuSig2 signing algorithms.
    
    First try to create the aggregate signature. This will fail if there are
    not enough partial signatures or public nonces. If it does fail, try to
    create a partial signature with all participant keys. This will fail for
    those keys that we do not have the private keys for, and if there are
    not enough public nonces. Lastly, if the partial signatures could not be
    created, add our own public nonces for the private keys that we know, if
    they do not yet exist.
    4a273edda0
  282. wallet: Keep secnonces in DescriptorScriptPubKeyMan 68ef954c4c
  283. test: Test MuSig2 in the wallet ac599c4a9c
  284. achow101 force-pushed on Sep 30, 2025
  285. in src/script/signingprovider.cpp:115 in 4d8b4f5336 outdated
    108@@ -94,6 +109,26 @@ std::vector<CPubKey> FlatSigningProvider::GetMuSig2ParticipantPubkeys(const CPub
    109     return participant_pubkeys;
    110 }
    111 
    112+void FlatSigningProvider::SetMuSig2SecNonce(const uint256& session_id, MuSig2SecNonce&& nonce) const
    113+{
    114+    if (!Assume(musig2_secnonces)) return;
    115+    musig2_secnonces->emplace(session_id, std::move(nonce));
    


    fjahr commented at 9:37 pm on September 30, 2025:
    Should this first check that no value exists already for this key? Otherwise, if the key wasn’t deleted properly and session id is reused somehow, there will be no effect and this might lead to danger reusing the nonce? It’s a bit far fetched but might be good as belt and suspenders.

    achow101 commented at 0:22 am on October 4, 2025:
    I think it’s fine as is but can change if I need to retouch.
  286. rkrux approved
  287. rkrux commented at 9:38 am on October 1, 2025: contributor

    receive and spend from imported musig(0 descriptors.

    Is this a typo in the PR description?


    This was a big PR that required me to split the review into multiple parts in which I tried to think of scenarios that could cause the flow to break or to be unsafe.

    Few points from my understanding of this PR:

    1. Appropriate steps are taken to secure the MuSig2SecNonce.

      • It is stored only in memory - never backed up on disk, or serialised. Okay with the tradeoff of having to restart the MuSig2 signing session when the node is restarted.
      • Uses secure_unique_ptr that memory_cleanses while destructing the object. The same one that’s used for CKey in the wallet, and the same one that’s recommended in the libsecp example - helps in having the compiler not optimising away the memset that clears the SecNonce before destruction.
      • Also used via secure_unique_ptr is LockedPoolManager that ensures non-swappable memory.
      • The secp256k1_context is destroyed in the MuSig2 example in libsecp; in the wallet though, it appears it is created (along with being randomised) and is destroyed on node start & shutdown.
    2. There is some redundancy in sighash calculation wherein it’s calculated multiple times in the same flow; it can be addressed in a follow-up: #29675 (review)

    3. Inclining to agree that the MuSig2 functions can be moved from CKey to musig.cpp later where it seems more suitable; in only 2 places secp256k1_context_sign is used: #29675 (review)

    4. Thorough functional testing gives more confidence because it covers various cases such as: a. both single & multiple musig portions in the descriptor, b. both rawtr and tr descriptors, c. different sighash types, d. same descriptor spending through key and script path in case of missing signers, e. only 1 wallet with the Musig descriptor with rest being just plain signers having individual keys.

    Thanks for addressing all the comments previously in the partial reviews.

    Range diff from last review:

    0git range-diff 36f8355...ac599c4
    

    I might take another look after few days if I feel I missed something.

    lgtm tACK ac599c4a9cb3b2d424932d3fd91f9eed17426827

  288. DrahtBot requested review from fjahr on Oct 1, 2025
  289. DrahtBot requested review from jonatack on Oct 1, 2025
  290. DrahtBot requested review from theStack on Oct 1, 2025
  291. in src/script/sign.cpp:345 in 4a273edda0 outdated
    340+            uint256 partial_sig;
    341+            if (creator.CreateMuSig2PartialSig(provider, partial_sig, agg_pub, plain_pub, part_pk, leaf_hash, tweaks, sigversion, sigdata) && Assume(!partial_sig.IsNull())) {
    342+                sigdata.musig2_partial_sigs[pub_key_leaf_hash].emplace(part_pk, partial_sig);
    343+            }
    344+        }
    345+        // If there are any partial signatures, exit early
    


    fjahr commented at 3:36 pm on October 1, 2025:
    nit: I wouldn’t call a continue exit early, I would expect to exit the whole function here then.

    achow101 commented at 0:23 am on October 4, 2025:
    If I need to retouch.
  292. in src/script/sign.cpp:348 in 4a273edda0 outdated
    343+            }
    344+        }
    345+        // If there are any partial signatures, exit early
    346+        auto partial_sigs_it = sigdata.musig2_partial_sigs.find(pub_key_leaf_hash);
    347+        if (partial_sigs_it != sigdata.musig2_partial_sigs.end() && !partial_sigs_it->second.empty()) {
    348+            continue;
    


    fjahr commented at 3:40 pm on October 1, 2025:
    Doesn’t this mean that we can never recover from a situation where we have some partial sig but not all pubnonces? I guess this situation is prevented by the calling code but still, I would have expected here to rather check if any new partial sigs were added in the code above just now because that would imply the necessary pubnonce for that particular partial sig was available.

    achow101 commented at 11:15 pm on October 1, 2025:
    Having some partial sigs and not all pubnonces should be a contradiction. It is not possible to create a valid partial sig without all of the pubnonces. I think in that situation it is safer to do nothing rather than try to continue by adding a new pubnonce.
  293. in src/musig.cpp:61 in f14876213a outdated
    56+{
    57+    CExtPubKey extpub;
    58+    extpub.nDepth = 0;
    59+    std::memset(extpub.vchFingerprint, 0, 4);
    60+    extpub.nChild = 0;
    61+    extpub.chaincode = MUSIG_CHAINCODE;
    


    theStack commented at 5:07 pm on October 1, 2025:
    nit: could move the MUSIG_CHAINCODE constant from the header to musig.cpp, as its only used there now

    achow101 commented at 0:18 am on October 4, 2025:
    If I need to retouch.
  294. in test/functional/wallet_musig.py:56 in ac599c4a9c
    51+                if musig_partial_sigs is not None:
    52+                    musig_partial_sigs += 1
    53+                if wallet_index < len(wallets):
    54+                    continue
    55+                wallet_name = f"musig_{self.WALLET_NUM}"
    56+                self.WALLET_NUM += 1
    


    theStack commented at 5:49 pm on October 1, 2025:
    nit: seems that this shouldn’t be in upper-case, if it’s not really a constant

    achow101 commented at 0:18 am on October 4, 2025:
    If I need to retouch.
  295. theStack approved
  296. theStack commented at 5:55 pm on October 1, 2025: contributor

    Code-review ACK ac599c4a9cb3b2d424932d3fd91f9eed17426827 :old_key:

    Happy to review potential follow-ups.

  297. fjahr commented at 10:22 pm on October 1, 2025: contributor

    Looks very good to me, I am just curious about the two questions I have from my last pass, but I am happy to ACK once these are addressed (with code or comment).

    FWIW, I didn’t like that the functional test only checked the happy path, so I drafted some tests for failure scenarios here: https://github.com/fjahr/bitcoin/commit/889af13325a0f5fa16c2da6d71808f5fa6f15a1d I will open this as a follow-up after merge.

  298. fjahr commented at 2:28 pm on October 4, 2025: contributor

    tACK ac599c4a9cb3b2d424932d3fd91f9eed17426827

    I might just address some of the left-over comments in a follow-up together with my additional tests, if this PR doesn’t get retouched anymore.


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: 2025-10-10 18:13 UTC

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