bitcoinjs / bitcoinjs-lib

A javascript Bitcoin library for node.js and browsers.
MIT License
5.69k stars 2.11k forks source link

[Help needed] How to use p2wsh with custom script #2151

Closed danielwpz closed 2 months ago

danielwpz commented 2 months ago

Hi, I wanna to convert my custom p2sh transaction into a p2wsh one, but failed to make it work. Could anybody kindly help me with that?

btw I have scanned other issues but there is no working p2wsh with custom script exists. There are someone who asked the similar question but when they figured out they didn't post the working code eventually.

The following is my p2sh script: (actual scripts are replaced with some dummy codes but the idea stands)


  const p2sh = bitcoin.payments.p2sh({
    redeem: {
      output: lockScript(),
    },
    network,
  });

  /// -- 1. Fund
  const fundUnspent = await regtestUtils.faucet(p2sh.address!, 3e5);

  /// -- 2. Spend
  const spendTx = new bitcoin.Transaction();
  spendTx.version = 2;
  spendTx.addInput(idToHash(fundUnspent.txId), fundUnspent.vout);
  // withdraw to user's address
  spendTx.addOutput(toOutputScript(regtestUtils.RANDOM_ADDRESS), 1e5);

  const redeemScriptSig = bitcoin.payments.p2sh({
    network,
    redeem: {
      network,
      output: p2sh.redeem!.output!,
      input: unlockScript(),
    },
  }).input!;
  spendTx.setInputScript(0, redeemScriptSig);

  await regtestUtils.mine(1);
  await regtestUtils.broadcast(spendTx.toHex());
  await regtestUtils.verify({
    txId: spendTx.getId(),
    address: regtestUtils.RANDOM_ADDRESS,
    vout: 0,
    value: 1e5,
  });
danielwpz commented 2 months ago

which method should I call to set the witness? anything else I need to do before broadcasting the transaction? 😵‍💫

junderw commented 2 months ago
  1. replace both bitcoin.payments.p2sh with bitcoin.payments.p2wsh
  2. replace }).input!; with }).witness!;
  3. replace setInputScript with setWitness
  4. if you do any signing inside unlockScript() make sure to use hashForWitnessV0 instead of hashForSignature. (Note: this new signature hash requires the satoshi value of the output that you're spending.)
  5. (Optional) change other variable names to make more sense

Fixing these 4 things should work fine.

danielwpz commented 2 months ago

will try, thanks!

danielwpz commented 2 months ago

hey @junderw , as for the signing, what I did is

  const p2wsh = bitcoin.payments.p2wsh({
    redeem: {
      output: lockScript(),
    },
    network,
  });

  const sigHash = spendTx.hashForWitnessV0(
    0,
    p2wsh.redeem!.output!,
    1e5,
    bitcoin.Transaction.SIGHASH_ALL,
  );

but seems the CHECKSIG failed

junderw commented 2 months ago

Use 3e5 not 1e5

You are signing "This input is spending this much satoshis"

danielwpz commented 2 months ago

@junderw thanks for the help! it works eventually. I'm posting the entire working script for future reference

import * as bitcoin from "bitcoinjs-lib";
import ECPairFactory, { ECPairInterface } from "ecpair";
import { RegtestUtils } from "regtest-client";
import * as ecc from "tiny-secp256k1";

const regtestUtils = new RegtestUtils({ APIURL: "http://localhost:8080/1" });
const network = regtestUtils.network;
const ECPair = ECPairFactory(ecc);

function idToHash(txid: string): Buffer {
  return Buffer.from(txid, 'hex').reverse();
}

function toOutputScript(address: string): Buffer {
  return bitcoin.address.toOutputScript(address, network);
}

function lockScript(user: ECPairInterface) {
  return bitcoin.script.fromASM(
    `
      OP_DROP
      ${user.publicKey.toString("hex")}
      OP_CHECKSIG
    `
      .trim()
      .replace(/\s+/g, ' '),
  )
}

function unlockScript(user: ECPairInterface, sigHash: Buffer) {
  return bitcoin.script.compile([
    bitcoin.script.signature.encode(
      user.sign(sigHash),
      bitcoin.Transaction.SIGHASH_ALL,
    ),
    bitcoin.opcodes.OP_0,
  ]);
}

async function test() {
  const user = ECPair.makeRandom();
  const p2wsh = bitcoin.payments.p2wsh({
    redeem: {
      output: lockScript(user),
    },
    network,
  });

  /// -- 1. Fund
  const fundUnspent = await regtestUtils.faucet(p2wsh.address!, 3e5);

  /// -- 2. Spend
  const spendTx = new bitcoin.Transaction();
  spendTx.version = 2;
  spendTx.addInput(idToHash(fundUnspent.txId), fundUnspent.vout);
  // withdraw to user's address
  spendTx.addOutput(toOutputScript(regtestUtils.RANDOM_ADDRESS), 1e5);

  const sigHash = spendTx.hashForWitnessV0(
    0,
    p2wsh.redeem!.output!,
    3e5,
    bitcoin.Transaction.SIGHASH_ALL,
  );
  const redeemScriptWitness = bitcoin.payments.p2wsh({
    network,
    redeem: {
      network,
      output: p2wsh.redeem!.output!,
      input: unlockScript(user, sigHash),
    },
  }).witness!;
  spendTx.setWitness(0, redeemScriptWitness);

  await regtestUtils.mine(1);
  await regtestUtils.broadcast(spendTx.toHex());
  await regtestUtils.verify({
    txId: spendTx.getId(),
    address: regtestUtils.RANDOM_ADDRESS,
    vout: 0,
    value: 1e5,
  });
}

test();