Unlocking the Power of P2WSH: A Step-by-Step Guide to Creating and Spending Coins with Bitcoin Scripts using bitcoinjs-lib

eunovo

Oghenovo Usiwoma

Posted on February 13, 2023

Unlocking the Power of P2WSH: A Step-by-Step Guide to Creating and Spending Coins with Bitcoin Scripts using bitcoinjs-lib

In the world of Bitcoin, addresses are used to specify how you want to receive coins. There are several types of addresses, each with its own properties and capabilities.

The simplest type is Pay-to-Pubkey-Hash (P2PKH), which is generated from a public key hash and is secure but lacks advanced features. The Pay-to-Witness-Public-Key-Hash (P2WPKH) address, introduced by the SegWit upgrade, is more efficient and has lower fees. Another type, Pay-to-Script-Hash (P2SH), supports complex scripts and the Pay-to-Witness-Script-Hash (P2WSH) address is a SegWit-upgraded version of P2SH. Both P2WPKH and P2WSH use the bech32 format, an efficient address format with a checksum to detect and correct errors. Bech32 addresses start with "bcrt" on the Regtest network, "tb" on the testnet network, and "bc" on the mainnet network.

The P2WSH (Pay-to-Witness-Script-Hash) format embeds a witness program that includes a hash of a script, instead of just a simple public key hash as in the case of P2PKH (Pay-to-Pubkey-Hash) addresses. The witness program acts as a placeholder for the actual script, which can be more complex and larger in size, but this complexity is hidden from the sender. When a transaction is made to a P2WSH address, the sender only needs to provide the witness program, and the actual script complexity is only processed when a transaction spending from the p2wsh address is validated by the network. This makes sending transactions to P2WSH addresses simple and easy for the sender, while the receiver bears the burden of handling the increased script complexity and transaction fees associated with more complex scripts.

In this article, we'll be focusing on the P2WSH address type and how to create and use them in transactions using the bitcoinjs-lib library. By the end of this article, you'll have a solid understanding of how to create, send, and spend coins locked with a witness script using P2WSH addresses.

This article assumes that you have a solid understanding of how bitcoin and bitcoin-scripting works

The complete code for this article can be found here

Creating a Pay-to-Witness-Script-Hash (P2WSH) address in bitcoin is a bit more complex than a standard Pay-to-Witness-Pubkey-Hash (P2WPKH) address, but it opens up a whole new world of possibilities for specifying complex scripts. In this article, we'll be using the bitcoinjs-lib library, which is a popular JavaScript library for working with Bitcoin transactions. We'll be using TypeScript for our code samples, but the same concepts apply to JavaScript as well.

We'll start by installing bitcoinjs-lib:

npm install bitcoinjs-lib
Enter fullscreen mode Exit fullscreen mode

Next, we'll import the modules we need from the library:

import { networks, script, opcodes, payments, Psbt } from 'bitcoinjs-lib';

const network = networks.testnet;
Enter fullscreen mode Exit fullscreen mode

Now, we'll start with a simple example. In this example, we'll use a very simple locking script. The locking script will POP 2 numbers from the stack and check that they sum up to 5:

OP_ADD 5 OP_EQUAL
Enter fullscreen mode Exit fullscreen mode

We can create a P2WSH address for this script using bitcoinjs-lib.

const locking_script = script.compile([
  opcodes.OP_ADD,
  script.number.encode(5),
  opcodes.OP_EQUAL
])
Enter fullscreen mode Exit fullscreen mode

Now we'll create our P2WSH address:

const p2wsh = payments.p2wsh({ redeem: { output: locking_script, network }, network });
console.log(p2wsh.address);
Enter fullscreen mode Exit fullscreen mode

The P2WSH address will be logged to the console. Sending coins to this address will secure them with a hash of a script, instead of the script itself. The sender does not need to know the details of the script as the coins are locked with a simple scriptPubKey. The coins can only be unlocked by someone with the full redeemScript, which, when hashed, will match the hash value to which the coins are locked. You can now send coins to this address, which will be locked by the script. You can use a testnet faucet.

Let's create a transaction to spend from our Psbt.
We'll use the Psbt (Partially Signed Bitcoin Transaction) class from bitcoinjs-lib. The Psbt class allows us to create, update and finalize partially signed transactions.

We'll create a new instance of the Psbt class:

const psbt = new Psbt({ network });
Enter fullscreen mode Exit fullscreen mode

Now, we'll add the input to the transaction. This is the P2WSH transaction that we want to spend:

psbt.addInput({
    hash: "<txid>",
    index: <vout>,
    witnessUtxo: {
        script: p2wsh.output!,
        value: <amount>
    },
    witnessScript: locking_script
});
Enter fullscreen mode Exit fullscreen mode

Replace <txid>, <vout>, and <amount> with the corresponding values of the P2WSH transaction.

Next, we'll add an output to the transaction. This is where we'll send the coins to:

const toAddress = "<address>";
const toAmount = <amount>;
psbt.addOutput({
    address: toAddress,
    value: toAmount
});
Enter fullscreen mode Exit fullscreen mode

Remember to leave a small amount to pay for transaction fees.

To spend from our address, we need to present any two numbers that add up to 5, so any of the following scripts will work:

1 4
2 3
3 2
4 1
0 5
5 0
Enter fullscreen mode Exit fullscreen mode

We need to add our unlocking script to the input.

const finalizeInput = (_inputIndex: number, input: any) => {
  const redeemPayment = payments.p2wsh({
      redeem: {
        input: script.compile([
          script.number.encode(1),
          script.number.encode(4)
        ]),
        output: input.witnessScript
      }
    });

    const finalScriptWitness = witnessStackToScriptWitness(
      redeemPayment.witness ?? []
    );

    return {
      finalScriptSig: Buffer.from(""),
      finalScriptWitness
    }
}

psbt.finalizeInput(0, finalizeInput);
Enter fullscreen mode Exit fullscreen mode

Notice that we don't need to sign the p2swh input because our locking script doesn't require a signature.

The witnessStackToScriptWitness takes our witness stack(which is an array of our locking script and witness script) and serializes it(coverts it into a single stream of bytes returned as a Buffer).

Now we can extract the transaction using:

const tx = psbt.extractTransaction();
console.log(tx.toHex());
Enter fullscreen mode Exit fullscreen mode

And then broadcast it to the network using any method you prefer.

Let's create a slightly more complex script, where we can demonstrate how to add a signature to the unlocking script.
This time, we'll pay to an address(the script will be a standard p2pkh script) but we will require a secret to be presented along with the signature before the funds can be unlocked.

This is only an example, simple enough that I can use demonstrate adding signatures. In practice, I'm not sure why you would use a script like this without adding a conditional flow so that the coins can be spent in a different way if the secret is not provided

Take a look at our locking script:

OP_HASH160 <HASH160(<secret>)> OP_EQUALVERIFY OP_DUP OP_HASH160 <recipient_address> OP_EQUALVERIFY OP_CHECKSIG
Enter fullscreen mode Exit fullscreen mode

Our unlocking script will take the form:

<signature> <pubkey> <secret>
Enter fullscreen mode Exit fullscreen mode

We'll use the crypto module from 'bitcoinjs-lib' to hash our secret:

import { crypto } from 'bitcoinjs-lib';

const preimage = Buffer.from(secret);
const hash = crypto.hash160(preimage);
Enter fullscreen mode Exit fullscreen mode

If you have a bech32 address, you will need to decode the address using the address module:

import { address } from 'bitcoinjs-lib';

const recipientAddr = address.fromBech32("<bech32 address>"); 
Enter fullscreen mode Exit fullscreen mode

The address module can also decode standard p2pkh addresses or p2sh address encoded in base58check format.
Or you can make a random keypair and get the pubkey and address:

import { ECPairFactory, ECPairAPI, TinySecp256k1Interface } from 'ecpair';

const tinysecp: TinySecp256k1Interface = require('tiny-secp256k1');
const ECPair: ECPairAPI = ECPairFactory(tinysecp);

const keypair = ECPair.makeRandom({ network });
const publicKey = keypair.publicKey;
const recipAddr = crypto.hash160(publicKey);
Enter fullscreen mode Exit fullscreen mode

Let's compile the locking script and create our p2wsh address:

const locking_script = script.compile([
  opcodes.OP_HASH160,
  hash,
  opcodes.OP_EQUALVERIFY,
  opcodes.OP_DUP,
  opcodes.OP_HASH160,
  recipAddr,
  opcodes.OP_EQUALVERIFY,
  opcodes.OP_CHECKSIG,
]);

const p2wsh = payments.p2wsh({ redeem: { output: locking_script, network }, network });
console.log(p2wsh.address);
Enter fullscreen mode Exit fullscreen mode

You can send some coins to this address for testing.

To spend from this address, we'll perform the same steps we performed before but with a little twist; after adding the input and outputs, we'll need to create a signature for the p2wsh input:

psbt.signInput(0, keypair);
Enter fullscreen mode Exit fullscreen mode

Next, we'll construct the unlocking script and add it to the input:

const finalizeInput = (_inputIndex: number, input: any) => {
    const redeemPayment = payments.p2wsh({
      redeem: {
        input: script.compile([
          input.partialSig[0].signature,
          publicKey,
          preimage
        ]),
        output: input.witnessScript
      }
    });

    const finalScriptWitness = witnessStackToScriptWitness(
      redeemPayment.witness ?? []
    );

    return {
      finalScriptSig: Buffer.from(""),
      finalScriptWitness
    }
}

psbt.finalizeInput(0, finalizeInput);
Enter fullscreen mode Exit fullscreen mode

Now, we can extract the transaction hex and broadcast it:

const tx = psbt.extractTransaction();
console.log(tx.toHex());
Enter fullscreen mode Exit fullscreen mode

And that's it! We've successfully created a P2WSH address, sent coins to it, and spent the coins locked with the witness script using bitcoinjs-lib. Keep in mind that this is just a simple example and there are many other ways to use P2WSH and scripts to lock and unlock coins in Bitcoin.

The complete code for this article can be found here

💖 💪 🙅 🚩
eunovo
Oghenovo Usiwoma

Posted on February 13, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related