paulmillr / scure-btc-signer

Audited & minimal library for creating, signing & decoding Bitcoin transactions.
https://paulmillr.com/noble/#scure
MIT License
154 stars 40 forks source link

checkScript: non-wrapped ms #114

Open gregdhill opened 5 days ago

gregdhill commented 5 days ago

We're hitting this error when spending from a UTXO whose input(s) come from a multisig. It looks like we can get around this by setting disableScriptCheck to true but I'm not sure why this is flagged as an error since we are not spending from a multisig. Our code is here for reference but to reproduce it should be enough to provide an input to selectUTXO with an array of possibleInputs where at least one input has a UTXO which spends from a multisig to the user's wallet.

paulmillr commented 3 days ago

Can't reproduce, please provide specific input that fails.

Here is an example of this specific case working (attempt at reproduction):

should.only('GH-114: unwrapped multisig', () => {
  const privKey1 = hex.decode('0101010101010101010101010101010101010101010101010101010101010101');
  const P1 = secp256k1.getPublicKey(privKey1, true);
  const wpkh = btc.p2wpkh(P1);
  // multisig utxo
  const compressed = hex.decode(
    '030000000000000000000000000000000000000000000000000000000000000001'
  );
  const compressed2 = hex.decode(
    '030000000000000000000000000000000000000000000000000000000000000002'
  );
  const compressed3 = hex.decode(
    '030000000000000000000000000000000000000000000000000000000000000003'
  );
  const ms = btc.p2ms(2, [compressed, compressed2, compressed3]);
  const oldBrokenTx = new btc.Transaction({
    // NOTE: here we need disableScriptCheck, because we construct raw tx
    // in mentioned use case this tx will be signed and serialized by somebody else.
    disableScriptCheck: true,
    allowLegacyWitnessUtxo: true,
  });
  oldBrokenTx.addInput({
    txid: hex.decode('c061c23190ed3370ad5206769651eaf6fac6d87d85b5db34e30a74e0c4a6da3e'),
    index: 0,
    witnessUtxo: { script: ms.script, amount: 100_000n },
  });
  oldBrokenTx.addOutputAddress(wpkh.address, 90_000n);
  // Add broken signatures
  oldBrokenTx.updateInput(
    0,
    {
      partialSig: [
        [compressed, new Uint8Array(33)],
        [compressed2, new Uint8Array(33)],
      ],
    },
    true
  );
  oldBrokenTx.finalize();
  const brokenCheck = btc.Transaction.fromRaw(hex.decode(oldBrokenTx.hex));
  // Now, lets try to spend output of that transaction
  // we spend wpkh output from this tx, but input inside this tx is broken
  const INPUT = {
    ...wpkh,
    txid: oldBrokenTx.id,
    index: 0,
    nonWitnessUtxo: oldBrokenTx.hex,
  };
  const spendTx = new btc.Transaction();
  spendTx.addInput(INPUT);
  spendTx.addOutputAddress(wpkh.address, 80_000n);
  spendTx.sign(privKey1);
  spendTx.finalize();
  deepStrictEqual(spendTx.id, '63f33dac6b7ed734c1720ed32865601d85fcbdec370df08f8562d40ec9e79c6b');
  deepStrictEqual(
    spendTx.hex,
    '02000000000101ec495ac2d31a09a8511203caec7cade7bdd2f1c9ff9acf106042e1d0e2bcfc8d0000000000ffffffff01803801000000000016001479b000887626b294a914501a4cd226b58b23598302483045022100ca2b3abb71e8b207dd9630cb9c7d5d5b2824183e9a2b79b1a4eff7cf7787f5ab0220464b4b3f688497a7fdb45d2403becd86d4a05d2c211a8a21724b98bdc7742b510121031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f00000000'
  );
  const transaction = btc.selectUTXO([INPUT], [], 'default', {
    changeAddress: wpkh.address, // Refund surplus to the payment address
    feePerByte: BigInt(Math.ceil(1)), // round up to the nearest integer
    bip69: true, // Sort inputs and outputs according to BIP69
    createTx: true, // Create the transaction
    dust: BigInt(546), // TODO: update scure-btc-signer
  });
  //console.log(transaction);
  // no crash here!
});