-
Notifications
You must be signed in to change notification settings - Fork 2
Description
Summary
When signing p2trMusig2 inputs with BitGoPsbt.sign(), wasm-utxo always creates partial signatures using SIGHASH_DEFAULT (0) regardless of the sighashType field set on the PSBT input. This causes signature validation to fail when the input declares a different sighash type (e.g., SIGHASH_ALL = 1).
Steps to Reproduce
- Create a PSBT with a p2trMusig2 input that has
sighashType = SIGHASH_ALL (1)set (e.g., via utxo-core's BIP322 functions) - Sign the input using wasm-utxo's
BitGoPsbt.sign()method - Attempt to validate the signatures using utxo-lib's
getSignatureValidationArrayPsbt()
Expected Behavior
wasm-utxo should respect the input.sighashType field when creating musig2 partial signatures:
- If
sighashType = SIGHASH_ALL (1), append the sighash byte to the 32-byte partial signature (making it 33 bytes) - If
sighashType = SIGHASH_DEFAULT (0)or undefined, create 32-byte signatures (current behavior)
Actual Behavior
wasm-utxo always creates 32-byte musig2 partial signatures without a sighash suffix, which are interpreted as SIGHASH_DEFAULT (0) during validation.
When the input has sighashType = 1 but signatures are SIGHASH_DEFAULT (0), validation fails with:
Error: Sighash type is not allowed. Retry the sign method passing the sighashTypes array of whitelisted types. Sighash type: 1
Technical Analysis
The validation flow in utxo-lib:
getSignatureValidationArrayPsbt()callsvalidateTaprootMusig2SignaturesOfInput()validateTaprootMusig2SignaturesOfInput()extracts sighash type from signatures viagetSigHashTypeFromSigs():- 33-byte signature → sighash type is last byte
- 32-byte signature →
SIGHASH_DEFAULT (0)
getMusig2SessionKey()callsgetTaprootHashForSig(inputIndex, [sigHashType])with only the extracted sighash typegetTaprootHashForSig()readsinput.sighashTypeand checks if it's in the allowed list- Since
input.sighashType (1)is not in[0], validation fails
Test Case
it('should throw sighash type error when input.sighashType mismatches signature sighash type', function () {
const seed = 'p2trMusig2_sighash_test';
const { xprivs } = createTestWalletKeys(seed);
const utxolibRootWalletKeys = new utxolib.bitgo.RootWalletKeys(utxolib.testutil.getKeyTriple(seed));
const chain = utxolib.bitgo.getExternalChainCode('p2trMusig2');
const index = 0;
const messageText = 'BIP322 sighash mismatch test';
// Create BIP322 PSBT using utxo-core (sets sighashType = SIGHASH_ALL)
const psbt = coreBip322.createBaseToSignPsbt(utxolibRootWalletKeys, utxolib.networks.bitcoin);
coreBip322.addBip322InputWithChainAndIndex(psbt, messageText, utxolibRootWalletKeys, { chain, index });
assert.strictEqual(psbt.data.inputs[0].sighashType, 1); // SIGHASH_ALL
// Convert to wasm-utxo PSBT and sign
const wasmPsbt = fixedScriptWallet.BitGoPsbt.fromBytes(psbt.toBuffer(), 'btc');
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);
// Validation fails due to sighash mismatch
const signedPsbt = utxolib.bitgo.createPsbtFromBuffer(
Buffer.from(wasmPsbt.serialize()),
utxolib.networks.bitcoin
);
assert.throws(
() => utxolib.bitgo.getSignatureValidationArrayPsbt(signedPsbt, utxolibRootWalletKeys),
/Sighash type is not allowed.*Sighash type: 1/
);
});Suggested Fix
In the musig2 signing code, check input.sighashType and use it when creating partial signatures:
// Pseudocode
let sighash_type = input.sighash_type.unwrap_or(SIGHASH_DEFAULT);
let partial_sig = create_musig2_partial_sig(hash, nonce, key);
if sighash_type != SIGHASH_DEFAULT {
// Append sighash byte to signature
partial_sig.push(sighash_type as u8);
}Impact
This bug prevents using wasm-utxo for signing BIP322 proofs with p2trMusig2 addresses, as BIP322 requires SIGHASH_ALL for the proof transaction inputs.