From 4642a90dd19c0c0a722937ef989ef3274a186cfd Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Thu, 5 Feb 2026 18:19:20 +0100 Subject: [PATCH 1/3] test(abstract-utxo): add test for sighash type mismatch in BIP322 Add test case reproducing an error when PSBT input sighashType doesn't match signature sighash type in p2trMusig2 BIP322 messages. Issue: BTC-2866 Co-authored-by: llm-git --- modules/abstract-utxo/test/unit/bip322.ts | 65 +++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/modules/abstract-utxo/test/unit/bip322.ts b/modules/abstract-utxo/test/unit/bip322.ts index e5b0766512..fd7ed28134 100644 --- a/modules/abstract-utxo/test/unit/bip322.ts +++ b/modules/abstract-utxo/test/unit/bip322.ts @@ -1,6 +1,7 @@ import assert from 'assert'; import * as utxolib from '@bitgo/utxo-lib'; +import { bip322 as coreBip322 } from '@bitgo/utxo-core'; import { bip322 as wasmBip322, fixedScriptWallet, BIP32, type Triple } from '@bitgo/wasm-utxo'; import { @@ -401,4 +402,68 @@ describe('BIP322', function () { ); }); }); + + describe('utxolib verification stack - sighash type mismatch', function () { + // This test reproduces the error: + // "Sighash type is not allowed. Retry the sign method passing the sighashTypes array + // of whitelisted types. Sighash type: 1" + // + // This occurs when: + // 1. A taproot musig2 PSBT input has sighashType set to SIGHASH_ALL (1) + // 2. But the partial signatures were created without an explicit sighash type suffix + // (32 bytes, interpreted as SIGHASH_DEFAULT = 0) + // 3. During validation, getTaprootHashForSig checks if input.sighashType is in the + // allowed list derived from signatures, and throws if not + + it('should throw sighash type error when input.sighashType mismatches signature sighash type', function () { + const seed = 'p2trMusig2_sighash_test'; + const { xprivs } = createTestWalletKeys(seed); + + // Create utxolib RootWalletKeys for utxo-core PSBT construction + const utxolibRootWalletKeys = new utxolib.bitgo.RootWalletKeys(utxolib.testutil.getKeyTriple(seed)); + + // p2trMusig2 external chain code + const chain = utxolib.bitgo.getExternalChainCode('p2trMusig2'); + const index = 0; + const messageText = 'BIP322 sighash mismatch test'; + + // Create BIP322 PSBT using utxo-core + const psbt = coreBip322.createBaseToSignPsbt(utxolibRootWalletKeys, utxolib.networks.bitcoin); + coreBip322.addBip322InputWithChainAndIndex(psbt, messageText, utxolibRootWalletKeys, { chain, index }); + + // Note: utxo-core sets sighashType: Transaction.SIGHASH_ALL (1) for BIP322 inputs + // Verify the sighashType is set to SIGHASH_ALL (1) + const SIGHASH_ALL = 1; + assert.strictEqual(psbt.data.inputs[0].sighashType, SIGHASH_ALL); + + // Convert to wasm-utxo PSBT for cosigning + const wasmPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(psbt.toBuffer(), 'btc'); + + // Generate musig2 nonces and sign with wasm-utxo (uses SIGHASH_DEFAULT by default) + const userKey = BIP32.fromBase58(xprivs[0]); + const bitgoKey = BIP32.fromBase58(xprivs[2]); + + wasmPsbt.generateMusig2Nonces(userKey); + wasmPsbt.generateMusig2Nonces(bitgoKey); + wasmPsbt.sign(0, userKey); + wasmPsbt.sign(0, bitgoKey); + + // Convert back to utxolib PSBT for validation + const signedPsbt = utxolib.bitgo.createPsbtFromBuffer( + Buffer.from(wasmPsbt.serialize()), + utxolib.networks.bitcoin + ); + + // The PSBT now has: + // - input.sighashType = SIGHASH_ALL (1) from utxo-core BIP322 creation + // - musig2 partial signatures without explicit sighash suffix (interpreted as SIGHASH_DEFAULT = 0) + // This mismatch causes the validation error + assert.throws( + () => utxolib.bitgo.getSignatureValidationArrayPsbt(signedPsbt, utxolibRootWalletKeys), + (e: Error) => + e.message === + `Sighash type is not allowed. Retry the sign method passing the sighashTypes array of whitelisted types. Sighash type: ${SIGHASH_ALL}` + ); + }); + }); }); From 30675114fb1380c5d08dd339a4233b04a32a06f7 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 6 Feb 2026 09:10:19 +0100 Subject: [PATCH 2/3] feat(abstract-utxo): bump wasm-utxo to 1.34.0 Update wasm-utxo dependency to version 1.34.0 across several modules. Issue: BTC-2866 Co-authored-by: llm-git --- modules/abstract-utxo/package.json | 2 +- modules/utxo-bin/package.json | 2 +- modules/utxo-core/package.json | 2 +- modules/utxo-ord/package.json | 2 +- modules/utxo-staking/package.json | 2 +- yarn.lock | 16 ++++++++-------- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/modules/abstract-utxo/package.json b/modules/abstract-utxo/package.json index b4265f03af..18d6bd3826 100644 --- a/modules/abstract-utxo/package.json +++ b/modules/abstract-utxo/package.json @@ -67,7 +67,7 @@ "@bitgo/utxo-core": "^1.32.0", "@bitgo/utxo-lib": "^11.20.0", "@bitgo/utxo-ord": "^1.25.0", - "@bitgo/wasm-utxo": "^1.32.0", + "@bitgo/wasm-utxo": "^1.34.0", "@types/lodash": "^4.14.121", "@types/superagent": "4.1.15", "bignumber.js": "^9.0.2", diff --git a/modules/utxo-bin/package.json b/modules/utxo-bin/package.json index 9b5a9b8d78..b89797fb9b 100644 --- a/modules/utxo-bin/package.json +++ b/modules/utxo-bin/package.json @@ -31,7 +31,7 @@ "@bitgo/unspents": "^0.51.0", "@bitgo/utxo-core": "^1.32.0", "@bitgo/utxo-lib": "^11.20.0", - "@bitgo/wasm-utxo": "^1.32.0", + "@bitgo/wasm-utxo": "^1.34.0", "@noble/curves": "1.8.1", "archy": "^1.0.0", "bech32": "^2.0.0", diff --git a/modules/utxo-core/package.json b/modules/utxo-core/package.json index 286d78f63c..f6c2c0215d 100644 --- a/modules/utxo-core/package.json +++ b/modules/utxo-core/package.json @@ -81,7 +81,7 @@ "@bitgo/secp256k1": "^1.10.0", "@bitgo/unspents": "^0.51.0", "@bitgo/utxo-lib": "^11.20.0", - "@bitgo/wasm-utxo": "^1.32.0", + "@bitgo/wasm-utxo": "^1.34.0", "bip174": "npm:@bitgo-forks/bip174@3.1.0-master.4", "fast-sha256": "^1.3.0" }, diff --git a/modules/utxo-ord/package.json b/modules/utxo-ord/package.json index ad2444a77d..bf98243ef9 100644 --- a/modules/utxo-ord/package.json +++ b/modules/utxo-ord/package.json @@ -45,7 +45,7 @@ "directory": "modules/utxo-ord" }, "dependencies": { - "@bitgo/wasm-utxo": "^1.32.0" + "@bitgo/wasm-utxo": "^1.34.0" }, "devDependencies": { "@bitgo/utxo-lib": "^11.20.0" diff --git a/modules/utxo-staking/package.json b/modules/utxo-staking/package.json index 693dae7ce9..8589176fd7 100644 --- a/modules/utxo-staking/package.json +++ b/modules/utxo-staking/package.json @@ -63,7 +63,7 @@ "@bitgo/babylonlabs-io-btc-staking-ts": "^3.4.0", "@bitgo/utxo-core": "^1.32.0", "@bitgo/utxo-lib": "^11.20.0", - "@bitgo/wasm-utxo": "^1.32.0", + "@bitgo/wasm-utxo": "^1.34.0", "bip174": "npm:@bitgo-forks/bip174@3.1.0-master.4", "bip322-js": "^2.0.0", "bitcoinjs-lib": "^6.1.7", diff --git a/yarn.lock b/yarn.lock index ad6e4c5bd2..35b27978cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -996,10 +996,10 @@ monocle-ts "^2.3.13" newtype-ts "^0.3.5" -"@bitgo/wasm-utxo@^1.32.0": - version "1.32.0" - resolved "https://registry.npmjs.org/@bitgo/wasm-utxo/-/wasm-utxo-1.32.0.tgz#fc7e7803eb584ba8ad16aeb0a2805d6905d287d3" - integrity sha512-fqUGh8XOrzbPcTxK3lhS9UjqKxx3UaN6L+eS3vocBeWHbQvl6jm9xPPQ+TDkeiUuZdxaj0+7ca4Algt9vyiXHg== +"@bitgo/wasm-utxo@^1.34.0": + version "1.34.0" + resolved "https://registry.npmjs.org/@bitgo/wasm-utxo/-/wasm-utxo-1.34.0.tgz#318bcc0a20acc3f35b9547548ea36506beaab237" + integrity sha512-aJuLQ8fNAWmI213sIwMt9WJfWvsyVAmmyUWnojUdmK2iHwf6tIo4B9gWHoPuWwNYoqc/UJVbK3dDKxnMIUk/bg== "@brandonblack/musig@^0.0.1-alpha.0": version "0.0.1-alpha.1" @@ -3080,10 +3080,10 @@ resolved "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz" integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ== -"@isaacs/brace-expansion@^5.0.0": - version "5.0.0" - resolved "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz" - integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA== +"@isaacs/brace-expansion@5.0.1", "@isaacs/brace-expansion@^5.0.0": + version "5.0.1" + resolved "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz#0ef5a92d91f2fff2a37646ce54da9e5f599f6eff" + integrity sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ== dependencies: "@isaacs/balanced-match" "^4.0.1" From b7219080087e7f65921f9416f14ea499fc6311cd Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 6 Feb 2026 09:08:40 +0100 Subject: [PATCH 3/3] fix(abstract-utxo): update BIP322 test to verify wasm-utxo respects sighashType Updated test to verify that wasm-utxo correctly respects input.sighashType when creating musig2 partial signatures, instead of testing for the error condition that previously existed. Issue: BTC-2866 Co-authored-by: llm-git --- modules/abstract-utxo/test/unit/bip322.ts | 49 +++++++++++------------ 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/modules/abstract-utxo/test/unit/bip322.ts b/modules/abstract-utxo/test/unit/bip322.ts index fd7ed28134..819ac02877 100644 --- a/modules/abstract-utxo/test/unit/bip322.ts +++ b/modules/abstract-utxo/test/unit/bip322.ts @@ -403,19 +403,17 @@ describe('BIP322', function () { }); }); - describe('utxolib verification stack - sighash type mismatch', function () { - // This test reproduces the error: - // "Sighash type is not allowed. Retry the sign method passing the sighashTypes array - // of whitelisted types. Sighash type: 1" + describe('utxolib verification stack - wasm-utxo respects input.sighashType', function () { + // This test verifies that wasm-utxo correctly respects the input.sighashType field + // when creating musig2 partial signatures. // - // This occurs when: - // 1. A taproot musig2 PSBT input has sighashType set to SIGHASH_ALL (1) - // 2. But the partial signatures were created without an explicit sighash type suffix - // (32 bytes, interpreted as SIGHASH_DEFAULT = 0) - // 3. During validation, getTaprootHashForSig checks if input.sighashType is in the - // allowed list derived from signatures, and throws if not - - it('should throw sighash type error when input.sighashType mismatches signature sighash type', function () { + // Previously (before fix), wasm-utxo would always create signatures with SIGHASH_DEFAULT (0) + // regardless of the input.sighashType field, causing validation to fail. + // + // Now (after fix), wasm-utxo reads input.sighashType and creates signatures with the + // correct sighash type, allowing validation to succeed. + + it('should validate signatures when wasm-utxo respects input.sighashType', function () { const seed = 'p2trMusig2_sighash_test'; const { xprivs } = createTestWalletKeys(seed); @@ -425,21 +423,21 @@ describe('BIP322', function () { // p2trMusig2 external chain code const chain = utxolib.bitgo.getExternalChainCode('p2trMusig2'); const index = 0; - const messageText = 'BIP322 sighash mismatch test'; + const messageText = 'BIP322 sighash test'; // Create BIP322 PSBT using utxo-core const psbt = coreBip322.createBaseToSignPsbt(utxolibRootWalletKeys, utxolib.networks.bitcoin); coreBip322.addBip322InputWithChainAndIndex(psbt, messageText, utxolibRootWalletKeys, { chain, index }); // Note: utxo-core sets sighashType: Transaction.SIGHASH_ALL (1) for BIP322 inputs - // Verify the sighashType is set to SIGHASH_ALL (1) const SIGHASH_ALL = 1; assert.strictEqual(psbt.data.inputs[0].sighashType, SIGHASH_ALL); // Convert to wasm-utxo PSBT for cosigning const wasmPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(psbt.toBuffer(), 'btc'); - // Generate musig2 nonces and sign with wasm-utxo (uses SIGHASH_DEFAULT by default) + // Generate musig2 nonces and sign with wasm-utxo + // wasm-utxo now respects input.sighashType and creates signatures with SIGHASH_ALL const userKey = BIP32.fromBase58(xprivs[0]); const bitgoKey = BIP32.fromBase58(xprivs[2]); @@ -454,16 +452,17 @@ describe('BIP322', function () { utxolib.networks.bitcoin ); - // The PSBT now has: - // - input.sighashType = SIGHASH_ALL (1) from utxo-core BIP322 creation - // - musig2 partial signatures without explicit sighash suffix (interpreted as SIGHASH_DEFAULT = 0) - // This mismatch causes the validation error - assert.throws( - () => utxolib.bitgo.getSignatureValidationArrayPsbt(signedPsbt, utxolibRootWalletKeys), - (e: Error) => - e.message === - `Sighash type is not allowed. Retry the sign method passing the sighashTypes array of whitelisted types. Sighash type: ${SIGHASH_ALL}` - ); + // Validation should succeed because wasm-utxo now creates signatures + // with the correct sighash type (SIGHASH_ALL) matching input.sighashType + const validationResult = utxolib.bitgo.getSignatureValidationArrayPsbt(signedPsbt, utxolibRootWalletKeys); + + // Verify that both user (index 0) and bitgo (index 2) signatures are valid + assert.strictEqual(validationResult.length, 1); + const [inputIndex, sigValidation] = validationResult[0]; + assert.strictEqual(inputIndex, 0); + assert.strictEqual(sigValidation[0], true, 'user signature should be valid'); + assert.strictEqual(sigValidation[1], false, 'backup signature should not be present'); + assert.strictEqual(sigValidation[2], true, 'bitgo signature should be valid'); }); }); });