Merge bitcoin/bitcoin#35933: psbt: don't abort on invalid MuSig2 derivations

73a94b4545 psbt: avoid aborting on invalid MuSig2 derivations (Lőrinc)
e3d1e75a51 test: characterize MuSig2 derivation aborts (Lőrinc)

Pull request description:

  **Problem:** A PSBT may contain MuSig2 derivation metadata with a hardened child index or a path that derives to a different key.
  The hardened index aborts during public derivation, while the mismatched key aborts at the result assertion.
  `analyzepsbt`, `finalizepsbt`, and `descriptorprocesspsbt` all reach this code without a wallet.
  Even the read-only `analyzepsbt` can force a co-signer service to restart its node after unexpected input.

  **Fix:** Return failure when a MuSig2 derivation path contains a hardened child index, and skip only the current aggregate when the path derives to a different key so another matching aggregate can still be tried.

  This follows [#35154](https://github.com/bitcoin/bitcoin/pull/35154), with the related contributions credited in the commits.

ACKs for top commit:
  jeanpablojp:
    ACK 73a94b4545
  achow101:
    ACK 73a94b4545
  andrewtoth:
    ACK 73a94b4545

Tree-SHA512: d8e28c5a4184154a4427c644ce62423cbcccdc3d82a6293f36fe99055fa04714598bc92c43b526fbcc7c99b231140669c2d0f1b853999d7dc33f949564c90504
This commit is contained in:
merge-script
2026-08-24 09:19:52 +01:00
2 changed files with 28 additions and 4 deletions

View File

@@ -13,6 +13,7 @@ from test_framework.blocktools import (
MAX_STANDARD_TX_WEIGHT,
)
from test_framework.descriptors import descsum_create
from test_framework.extendedkey import hardened
from test_framework.key import H_POINT
from test_framework.messages import (
COutPoint,
@@ -40,6 +41,8 @@ from test_framework.psbt import (
PSBT_IN_MUSIG2_PUB_NONCE,
PSBT_IN_NON_WITNESS_UTXO,
PSBT_IN_PROPRIETARY,
PSBT_IN_TAP_BIP32_DERIVATION,
PSBT_IN_TAP_INTERNAL_KEY,
PSBT_IN_WITNESS_UTXO,
PSBT_IN_FINAL_SCRIPTWITNESS,
PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS,
@@ -47,8 +50,8 @@ from test_framework.psbt import (
PSBT_OUT_TAP_TREE,
PSBT_OUT_SCRIPT,
)
from test_framework.script import CScript, OP_TRUE, SIGHASH_ALL, SIGHASH_ANYONECANPAY
from test_framework.script_util import MIN_STANDARD_TX_NONWITNESS_SIZE
from test_framework.script import CScript, OP_TRUE, SIGHASH_ALL, SIGHASH_ANYONECANPAY, hash160
from test_framework.script_util import MIN_STANDARD_TX_NONWITNESS_SIZE, output_key_to_p2tr_script
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_not_equal,
@@ -302,6 +305,25 @@ class PSBTTest(BitcoinTestFramework):
assert "participant_pubkeys" in out_participant_pks
assert_equal(out_participant_pks["participant_pubkeys"], [out_pubkey1.hex(), out_pubkey2.hex()])
def test_musig2_untrusted_derivation(self):
self.log.info("Test MuSig2 aggregate derivation from untrusted PSBT fields")
node = self.nodes[0]
script_pubkey = bytes.fromhex(H_POINT)
_, aggregate_pubkey = generate_keypair()
_, participant_pubkey = generate_keypair()
# Both have a matching aggregate fingerprint but cannot derive the script pubkey: 0 derives a different key, hardened(0) cannot be derived at all
for index in [0, hardened(0)]:
psbt = self.create_psbt(inputs={
PSBT_IN_WITNESS_UTXO: CTxOut(nValue=1, scriptPubKey=output_key_to_p2tr_script(script_pubkey)).serialize(),
bytes([PSBT_IN_TAP_BIP32_DERIVATION]) + script_pubkey: ser_compact_size(0) + hash160(aggregate_pubkey)[:4] + index.to_bytes(4, "little"),
PSBT_IN_TAP_INTERNAL_KEY: script_pubkey,
bytes([PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS]) + aggregate_pubkey: [participant_pubkey],
}).to_base64()
assert_equal(node.analyzepsbt(psbt)["inputs"][0]["is_final"], False)
assert_equal(node.finalizepsbt(psbt)["complete"], False)
def test_combinepsbt_preserves_proprietary_fields(self):
self.log.info("Test that combining PSBTs preserves proprietary fields")
@@ -1500,6 +1522,7 @@ class PSBTTest(BitcoinTestFramework):
self.test_psbt_roundtrip()
self.test_psbt_version()
self.test_psbt_with_invalid_signature()
self.test_musig2_untrusted_derivation()
if __name__ == '__main__':
PSBTTest(__file__).main()