Unlocking the Power of P2WSH: A Step-by-Step Guide to Creating and Spending Coins with Bitcoin Scripts using bitcoinjs-lib
Tags: javascript, bitcoinjs, bitcoin
Published: 2023-02-13 11:19:08

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](https://github.com/Eunovo/scripting-with-bitcoinjs)

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
```

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

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

const network = networks.testnet;
```

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
``` 

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

```typescript
const locking_script = script.compile([
  opcodes.OP_ADD,
  script.number.encode(5),
  opcodes.OP_EQUAL
])
```

Now we'll create our P2WSH address:

```
const p2wsh = payments.p2wsh({ redeem: { output: locking_script, network }, network });
console.log(p2wsh.address);
```

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](https://testnet-faucet.com/btc-testnet/).

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:

``` typescript
const psbt = new Psbt({ network });
```

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

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

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:

```typescript
const toAddress = "<address>";
const toAmount = <amount>;
psbt.addOutput({
    address: toAddress,
    value: toAmount
});
```
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
```

We need to add our unlocking script to the input.

``` typescript
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);
```
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:
```typescript
const tx = psbt.extractTransaction();
console.log(tx.toHex());
```

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
```

Our unlocking script will take the form:
```
<signature> <pubkey> <secret>
```

We'll use the `crypto` module from 'bitcoinjs-lib' to hash our secret:
```typescript
import { crypto } from 'bitcoinjs-lib';

const preimage = Buffer.from(secret);
const hash = crypto.hash160(preimage);
```

If you have a bech32 address, you will need to decode the address using the `address` module:
```typescript
import { address } from 'bitcoinjs-lib';

const recipientAddr = address.fromBech32("<bech32 address>"); 
```
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:
```typescript
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);
```

Let's compile the locking script and create our p2wsh address:
```typescript
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);
```

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:
```typescript
psbt.signInput(0, keypair);
```

Next, we'll construct the unlocking script and add it to the input:
```typescript
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);
```

Now, we can extract the transaction hex and broadcast it:
```typescript
const tx = psbt.extractTransaction();
console.log(tx.toHex());
```

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](https://github.com/Eunovo/scripting-with-bitcoinjs)