Merge bitcoin/bitcoin#35269: musig: Include pubnonce in session id

2ef6679c2c test: Check that MuSig2 signing does not reuse nonces (Ava Chow)
bb05986c0a musig: Include pubnonce in session id (Ava Chow)

Pull request description:

  It is safe to have multiple musig signing sessions over the same message so long as the nonces used are different. Including the pubnonce in the session id allows for multiple simultaneous signing sessions over the same message, rather than asserting when the user tries to do this.

  The second commit tests this behavior, both ensuring that there is no crash, and verifying that both sessions produce unique nonces and signatures to verify that no reuse is occurring.

  Lastly, the assertion in `SetMuSig2SecNonce` is retained as hitting it now would indicate that a nonce has been reused. We prefer to assert and crash rather than do something that is highly likely to leak a private key.

  Fixes #35250

ACKs for top commit:
  rkrux:
    lgtm ACK 2ef6679c2c
  junbyjun1238:
    utACK 2ef6679c2c
  theStack:
    ACK 2ef6679c2c

Tree-SHA512: 9fb60b68ebe0ea9656408afb65b9ec9f280632e1bb84a4821b074c8d8569847845f7c29da800c757b9ddf3aa31aa890dd9e3646cf119917a714e7daf20be2198
This commit is contained in:
merge-script
2026-06-02 21:33:01 +02:00
5 changed files with 64 additions and 41 deletions

View File

@@ -12,6 +12,7 @@ from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
assert_greater_than,
assert_not_equal,
)
PRIVKEY_RE = re.compile(r"^tr\((.+?)/.+\)#.{8}$")
@@ -111,6 +112,31 @@ class WalletMuSigTest(BitcoinTestFramework):
return wallets, psbt
def assert_musig_signer_data(self, first, second, different_key):
assert_equal(first["participant_pubkey"], second["participant_pubkey"])
assert_equal(first["aggregate_pubkey"], second["aggregate_pubkey"])
if "leaf_hash" in first:
assert_equal(first["leaf_hash"], second["leaf_hash"])
else:
assert "leaf_hash" not in second
assert_not_equal(first[different_key], second[different_key])
def assert_musig_aggregate_in_script(self, signer_data, pattern, psbtin):
pubkey = signer_data["aggregate_pubkey"][2:]
if "pkh" in pattern or "pk_h" in pattern:
pubkey = hash160(bytes.fromhex(pubkey)).hex()
if pubkey in psbtin["witness_utxo"]["scriptPubKey"]["hex"]:
return
elif "taproot_scripts" in psbtin:
for leaf_scripts in psbtin["taproot_scripts"]:
if pubkey in leaf_scripts["script"]:
break
else:
assert False, "Aggregate pubkey not seen as output key, or in any scripts"
else:
assert False, "Aggregate pubkey not seen as output key or internal key"
def test_failure_case_1(self, comment, pat):
self.log.info(f"Testing {comment}")
wallets, psbt = self.setup_musig_scenario(pat)
@@ -247,66 +273,57 @@ class WalletMuSigTest(BitcoinTestFramework):
part_pks.remove(deriv_path["pubkey"])
assert_equal(len(part_pks), 0)
# Run 2 signing sessions simultaneously to verify no nonce reuse
# Add pubnonces
nonce_psbts = []
nonce_psbts2 = []
for i, wallet in enumerate(wallets):
if nosign_wallets and i in nosign_wallets:
continue
proc = wallet.walletprocesspsbt(psbt=psbt, sighashtype=sighash_type)
assert_equal(proc["complete"], False)
nonce_psbts.append(proc["psbt"])
for psbt_list in [nonce_psbts, nonce_psbts2]:
proc = wallet.walletprocesspsbt(psbt=psbt, sighashtype=sighash_type)
assert_equal(proc["complete"], False)
psbt_list.append(proc["psbt"])
comb_nonce_psbt = self.nodes[0].combinepsbt(nonce_psbts)
comb_nonce_psbt2 = self.nodes[0].combinepsbt(nonce_psbts2)
dec_psbt = self.nodes[0].decodepsbt(comb_nonce_psbt)
dec_psbt2 = self.nodes[0].decodepsbt(comb_nonce_psbt2)
assert_equal(len(dec_psbt["inputs"][0]["musig2_pubnonces"]), expected_pubnonces)
for pn in dec_psbt["inputs"][0]["musig2_pubnonces"]:
pubkey = pn["aggregate_pubkey"][2:]
if "pkh" in pattern or "pk_h" in pattern:
pubkey = hash160(bytes.fromhex(pubkey)).hex()
if pubkey in dec_psbt["inputs"][0]["witness_utxo"]["scriptPubKey"]["hex"]:
continue
elif "taproot_scripts" in dec_psbt["inputs"][0]:
for leaf_scripts in dec_psbt["inputs"][0]["taproot_scripts"]:
if pubkey in leaf_scripts["script"]:
break
else:
assert False, "Aggregate pubkey for pubnonce not seen as output key, or in any scripts"
else:
assert False, "Aggregate pubkey for pubnonce not seen as output key or internal key"
assert_equal(len(dec_psbt2["inputs"][0]["musig2_pubnonces"]), expected_pubnonces)
for pn, pn2 in zip(dec_psbt["inputs"][0]["musig2_pubnonces"], dec_psbt2["inputs"][0]["musig2_pubnonces"]):
self.assert_musig_signer_data(pn, pn2, "pubnonce")
self.assert_musig_aggregate_in_script(pn, pattern, dec_psbt["inputs"][0])
# Add partial sigs
psig_psbts = []
psig_psbts2 = []
for i, wallet in enumerate(wallets):
if nosign_wallets and i in nosign_wallets:
continue
proc = wallet.walletprocesspsbt(psbt=comb_nonce_psbt, sighashtype=sighash_type)
assert_equal(proc["complete"], False)
psig_psbts.append(proc["psbt"])
for psbt, psbt_list in [(comb_nonce_psbt, psig_psbts), (comb_nonce_psbt2, psig_psbts2)]:
proc = wallet.walletprocesspsbt(psbt=psbt, sighashtype=sighash_type)
assert_equal(proc["complete"], False)
psbt_list.append(proc["psbt"])
comb_psig_psbt = self.nodes[0].combinepsbt(psig_psbts)
comb_psig_psbt2 = self.nodes[0].combinepsbt(psig_psbts2)
dec_psbt = self.nodes[0].decodepsbt(comb_psig_psbt)
dec_psbt2 = self.nodes[0].decodepsbt(comb_psig_psbt2)
assert_equal(len(dec_psbt["inputs"][0]["musig2_partial_sigs"]), expected_partial_sigs)
for ps in dec_psbt["inputs"][0]["musig2_partial_sigs"]:
pubkey = ps["aggregate_pubkey"][2:]
if "pkh" in pattern or "pk_h" in pattern:
pubkey = hash160(bytes.fromhex(pubkey)).hex()
if pubkey in dec_psbt["inputs"][0]["witness_utxo"]["scriptPubKey"]["hex"]:
continue
elif "taproot_scripts" in dec_psbt["inputs"][0]:
for leaf_scripts in dec_psbt["inputs"][0]["taproot_scripts"]:
if pubkey in leaf_scripts["script"]:
break
else:
assert False, "Aggregate pubkey for partial sig not seen as output key or in any scripts"
else:
assert False, "Aggregate pubkey for partial sig not seen as output key"
assert_equal(len(dec_psbt2["inputs"][0]["musig2_partial_sigs"]), expected_partial_sigs)
for ps, ps2 in zip(dec_psbt["inputs"][0]["musig2_partial_sigs"], dec_psbt2["inputs"][0]["musig2_partial_sigs"]):
self.assert_musig_signer_data(ps, ps2, "partial_sig")
self.assert_musig_aggregate_in_script(ps, pattern, dec_psbt["inputs"][0])
# Non-participant aggregates partial sigs and send
finalized = self.nodes[0].finalizepsbt(psbt=comb_psig_psbt, extract=False)
assert_equal(finalized["complete"], True)
finalized2 = self.nodes[0].finalizepsbt(psbt=comb_psig_psbt2, extract=False)
assert_equal(finalized["complete"], finalized2["complete"], True)
witness = self.nodes[0].decodepsbt(finalized["psbt"])["inputs"][0]["final_scriptwitness"]
assert_not_equal(witness, self.nodes[0].decodepsbt(finalized2["psbt"])["inputs"][0]["final_scriptwitness"])
if scriptpath:
assert_greater_than(len(witness), 1)
else: