Skip to content

Bug: Musig2 signing ignores input.sighashType field, always uses SIGHASH_DEFAULT #136

@OttoAllmendinger

Description

@OttoAllmendinger

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

  1. Create a PSBT with a p2trMusig2 input that has sighashType = SIGHASH_ALL (1) set (e.g., via utxo-core's BIP322 functions)
  2. Sign the input using wasm-utxo's BitGoPsbt.sign() method
  3. 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:

  1. getSignatureValidationArrayPsbt() calls validateTaprootMusig2SignaturesOfInput()
  2. validateTaprootMusig2SignaturesOfInput() extracts sighash type from signatures via getSigHashTypeFromSigs():
    • 33-byte signature → sighash type is last byte
    • 32-byte signature → SIGHASH_DEFAULT (0)
  3. getMusig2SessionKey() calls getTaprootHashForSig(inputIndex, [sigHashType]) with only the extracted sighash type
  4. getTaprootHashForSig() reads input.sighashType and checks if it's in the allowed list
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions