DEV Community

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

Posted on

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

Top comments (6)

Collapse
 
apongpoh profile image
Apongpoh

Great! well explained. Was looking for something like this over the passed view months.

Collapse
 
radicleart profile image
Mike Cohen

This is really well written and helpful. Thank you fro putting it together.

Collapse
 
eunovo profile image
Oghenovo Usiwoma

Just glad you found it helpful.

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
geremy735 profile image
Geremy735 • Edited

Great post. I enjoy reading articles on this subject. But don't you think it increases the chances that such transactions can then be easily traced. Bitcoin is already an unprotected currency and after such transactions even more so. Of course, if you have one bitcoin, it is unlikely to arouse the interest of not quite good citizens. But let's say you have 10 bitcoins in your account, and they are of interest to hackers who make money on it all the time. By the way, do you know how to increase the anonymity of various transactions? I know my friend uses a bitcoin mixer httрs://yomіx.io/ for this. I think I will study your article. I'm new at this. So I apologize if I said something wrong.

Collapse
 
arimed4000 profile image
arimed4000

Great article! helped me ramp up quick on this topic! Thank you.