Timelock Transactions

Absolute Timelock

nLockTime

Part of the original Bitcoin implementation, nLockTime is a field that specifies the earliest time a transaction may be added to a valid block. A later Bitcoin soft fork allowed nLockTime to alternatively specify the lowest block height a transaction may be added to a valid block.

Although every transaction contains the nLockTime field, every wallet up until recently set nLockTime to 0, meaning the transaction was valid in any block. Starting with Bitcoin Core 0.11.0, every normal transaction automatically generated by began including an nLockTime set to a recent block height as a way to make hypothesized fee sniping less profitable; other wallets are recommended to do the same.

OP_CHECKLOCKTIMEVERIFY

In late 2015, the BIP65 soft fork redefined the NOP2 opcode as the CheckLockTimeVerify (CLTV) opcode, allowing transaction outputs (rather than whole transactions) to be encumbered by a timelock. When the CLTV opcode is called, it will cause the script to fail unless the nLockTime on the transaction is equal to or greater than the time parameter provided to the CLTV opcode. Since a transaction may only be included in a valid block if its nLockTime is in the past, this ensures the CLTV-based timelock has expired before the transaction may be included in a valid block.

CLTV is currently used in CLTV-style payment channels.

Relative Timelock

nSequence

In mid-2016, the BIP68/112/113 soft fork gave consensus-enforced meaning to some values in the nSequence field that is a part of every transaction input, creating a "relative locktime". This allowed an input to specify the earliest time it can be added to a block based on how long ago the output being spent by that input was included in a block on the block chain.

OP_CHECKSEQUENCEVERIFY

Also part of the BIP68/112/113 soft fork was the CheckSequenceVerify opcode, which provides for relative locktime the same feature CLTV provides for absolute locktime. When the CSV opcode is called, it will cause the script to fail unless the nSequence on the transaction indicates an equal or greater amount of relative locktime has passed than the parameter provided to the CSV opcode. Since an input may only be included in a valid block if its relative locktime is expired, this ensures the CSV-based timelock has expired before the transaction may be included in a valid block.

CSV is used by Lightning Network transactions.

Script with CHECKLOCKTIMEVERIFY - Legacy P2SH

To follow along this tutorial and enter the commands step-by-step

  • Type node in a terminal after cd into ./code for a Javascript prompt
  • Open the Bitcoin Core GUI console or use bitcoin-cli for the Bitcoin Core commands
  • Use bx aka Libbitcoin-explorer as a handy complement

Let's create a legacy P2SH transaction with a script that contains the OP_CHECKLOCKTIMEVERIFY absolute timelock opcode.

Read more about OP_CHECKLOCKTIMEVERIFY in BIP65

Here is the script. Either Alice can redeem the output of the P2SH after the timelock expiry, or Bob and Alice can redeem the funds at any time. We will set the timelock 6 hours in the past. In real life it should be set in the future, but we don't want to wait for the timelock to expire in order to complete the tutorial.

The generate command, which produce blocks on demand on regtest, will not move forward the mediantime. It sets the mediantime to the current local time of your computer.

We will run both scenarios.

function cltvCheckSigOutput (aQ, bQ, lockTime) {
  return bitcoin.script.compile([
    bitcoin.opcodes.OP_IF,
    bitcoin.script.number.encode(lockTime),
    bitcoin.opcodes.OP_CHECKLOCKTIMEVERIFY,
    bitcoin.opcodes.OP_DROP,

    bitcoin.opcodes.OP_ELSE,
    bQ.publicKey,
    bitcoin.opcodes.OP_CHECKSIGVERIFY,
    bitcoin.opcodes.OP_ENDIF,

    aQ.publicKey,
    bitcoin.opcodes.OP_CHECKSIG
  ])
}

Creating and Funding the P2SH

Import libraries, test wallets and set the network and hashType.

const bitcoin = require('bitcoinjs-lib')
const { alice, bob } = require('./wallets.json')
const network = bitcoin.networks.regtest
const hashType = bitcoin.Transaction.SIGHASH_ALL

We also need an additional library to help us with BIP65 absolute timelock encoding.

const bip65 = require('bip65')

In both scenarios Alice_0 will get back the funds.

const keyPairAlice0 = bitcoin.ECPair.fromWIF(alice[0].wif, network)
const p2wpkhAlice0 = bitcoin.payments.p2wpkh({pubkey: keyPairAlice0.publicKey, network})

Create a key pair for Bob_0.

const keyPairBob0 = bitcoin.ECPair.fromWIF(bob[0].wif, network)

Encode the lockTime value according to BIP65 specification (now - 6 hours).

Method argument is a UNIX timestamp.

const lockTime = bip65.encode({utc: Math.floor(Date.now() / 1000) - (3600 * 6)})
console.log('lockTime  ', lockTime)

Generate the redeemScript with CLTV.

If you do it multiple times you will notice that the hex script is never the same, this is because of the timestamp.

const redeemScript = cltvCheckSigOutput(keyPairAlice0, keyPairBob0, lockTime)
console.log('redeemScript  ', redeemScript.toString('hex'))

You can decode the script in Bitcoin Core CLI with decodescript.

Generate the P2SH.

If you do it multiple times you will notice that the P2SH address is never the same, this is because of redeemScript.

const p2sh = bitcoin.payments.p2sh({redeem: {output: redeemScript, network}, network})
console.log('P2SH address  ', p2sh.address)

Send 1 BTC to this P2SH address.

$ sendtoaddress [p2sh.address] 1

Get the output index so that we have the outpoint (txid / vout).

Find the output index (or vout) under details > vout.

$ gettransaction "txid"

Preparing the spending transaction

Now let's prepare the spending transaction by setting input and output, and the nLockTime value.

Create a BitcoinJS transaction builder object.

const txb = new bitcoin.TransactionBuilder(network)

We need to set the transaction-level locktime in our redeem transaction in order to spend a CLTV. You can use the same value as in the redeemScript.

Because CLTV actually uses nLocktime enforcement consensus rules
the time is checked indirectly by comparing redeem transaction nLocktime with the CLTV value. nLocktime must be <= present time and >= CLTV timelock

txb.setLockTime(lockTime)

Create the input by referencing the outpoint of our P2SH funding transaction.
The input-level nSequence value needs to be change to 0xfffffffe, which means that nSequence is disabled, nLocktime is
enabled and RBF is not signaled.

// txb.addInput(prevTx, input.vout, input.sequence, prevTxScript)
txb.addInput('TX_ID', TX_VOUT, 0xfffffffe)

Alice_0 will redeem the fund to her P2WPKH address, leaving 100 000 satoshis for the mining fees.

txb.addOutput(p2wpkhAlice0.address, 999e5)

Prepare the transaction.

const tx = txb.buildIncomplete()

Creating the unlocking script

Now we can update the transaction with the unlocking script, providing a solution to the locking script.

We generate the hash that will be used to produce the signatures.

const signatureHash = tx.hashForSignature(0, redeemScript, hashType)

There are two ways to redeem the funds, either Alice after the timelock expiry or Alice and Bob at any time.
We control which branch of the script we want to run by ending our unlocking script with a boolean value.

First branch: {Alice's signature} OP_TRUE

const inputScriptFirstBranch = bitcoin.payments.p2sh({
  redeem: {
    input: bitcoin.script.compile([
      bitcoin.script.signature.encode(keyPairAlice0.sign(signatureHash), hashType),
      bitcoin.opcodes.OP_TRUE,
    ]),
    output: redeemScript
  }
}).input

Second branch: {Alice's signature} {Bob's signature} OP_FALSE

const inputScriptSecondBranch = bitcoin.payments.p2sh({
  redeem: {
    input: bitcoin.script.compile([
      bitcoin.script.signature.encode(keyPairAlice0.sign(signatureHash), hashType),
      bitcoin.script.signature.encode(keyPairBob0.sign(signatureHash), hashType),
      bitcoin.opcodes.OP_FALSE
    ]),
    output: redeemScript
  }
}).input

Update the transaction with the unlocking script.

tx.setInputScript(0, [inputScriptFirstBranch or inputScriptSecondBranch])

Get the raw hex serialization.

No build step here as we have already called buildIncomplete

console.log('tx.toHex  ', tx.toHex())

Inspect the raw transaction with Bitcoin Core CLI, check that everything is correct.

$ decoderawtransaction "hexstring"

Broadcasting the transaction

If you are spending the P2SH as Alice + timelock after expiry, you must have the node's mediantime to be higher than
the timelock value.

mediantime is the median timestamp of the previous 11 blocks. Check out
BIP113 for more information.

Check the current mediantime

$ getblockchaininfo

You need to generate some blocks in order to have the node's mediantime synchronized with your computer local time.

It is not possible to give you an exact number. 20 should be enough.

$ generate 20

It's now time to broadcast the transaction via Bitcoin Core CLI.

$ sendrawtransaction "hexstring"

Inspect the transaction.

$ getrawtransaction "txid" true

Observations

For the first scenario, we note that our scriptSig contains

  • Alice_0 signature
  • 1, which is equivalent to OP_TRUE
  • the redeem script, that we can decode with decodescript

For the second scenario, we note that our scriptSig contains

  • Alice_0 signature
  • Bob_0 signature
  • 0, which is equivalent to OP_FALSE
  • the redeem script, that we can decode with decodescript

Script with CHECKLOCKTIMEVERIFY - Native Segwit P2WSH

To follow along this tutorial and enter the commands step-by-step

  • Type node in a terminal after cd into ./code for a Javascript prompt
  • Open the Bitcoin Core GUI console or use bitcoin-cli for the Bitcoin Core commands
  • Use bx aka Libbitcoin-explorer as a handy complement

Let's create a native Segwit P2WSH transaction with a script that contains the OP_CHECKLOCKTIMEVERIFY absolute timelock opcode.

Read more about OP_CHECKLOCKTIMEVERIFY in BIP65
Read more about P2WSH in BIP141 - Segregated Witness

Here is the script. Either Alice can redeem the output of the P2WSH after the timelock expiry, or Bob and Alice can redeem the funds at any time.
We will set the timelock 6 hours in the past. In real life it should be set in the future, but we don't want to wait for the timelock to expire in order to complete the tutorial.

The generate command, which produce blocks on demand on regtest, will not move forward the mediantime.
It sets the mediantime to the current local time of your computer.

We will run both scenarios.

function cltvCheckSigOutput (aQ, bQ, lockTime) {
  return bitcoin.script.compile([
    bitcoin.opcodes.OP_IF,
    bitcoin.script.number.encode(lockTime),
    bitcoin.opcodes.OP_CHECKLOCKTIMEVERIFY,
    bitcoin.opcodes.OP_DROP,

    bitcoin.opcodes.OP_ELSE,
    bQ.publicKey,
    bitcoin.opcodes.OP_CHECKSIGVERIFY,
    bitcoin.opcodes.OP_ENDIF,

    aQ.publicKey,
    bitcoin.opcodes.OP_CHECKSIG
  ])
}

Creating and Funding the P2WSH

Import libraries, test wallets and set the network and hashType.

const bitcoin = require('bitcoinjs-lib')
const { alice, bob } = require('./wallets.json')
const network = bitcoin.networks.regtest
const hashType = bitcoin.Transaction.SIGHASH_ALL

We also need an additional library to help us with BIP65 absolute timelock encoding.

const bip65 = require('bip65')

In both scenarios Alice_0 will get back the funds.

const keyPairAlice0 = bitcoin.ECPair.fromWIF(alice[0].wif, network)
const p2wpkhAlice0 = bitcoin.payments.p2wpkh({pubkey: keyPairAlice0.publicKey, network})

Create a key pair for Bob_0.

const keyPairBob0 = bitcoin.ECPair.fromWIF(bob[0].wif, network)

Encode the lockTime value according to BIP65 specification (now - 6 hours).

Method argument is a UNIX timestamp.

const lockTime = bip65.encode({utc: Math.floor(Date.now() / 1000) - (3600 * 6)})
console.log('lockTime  ', lockTime)

Generate the witnessScript with CLTV.

In a P2WSH context, a redeem script is called a witness script.
If you do it multiple times you will notice that the hex script is never the same, this is because of the timestamp.

const witnessScript = cltvCheckSigOutput(keyPairAlice0, keyPairBob0, lockTime)
console.log('witnessScript  ', witnessScript.toString('hex'))

You can decode the script in Bitcoin Core CLI with decodescript.

Generate the P2WSH.

If you do it multiple times you will notice that the P2WSH address is never the same, this is because of witnessScript.

const p2wsh = bitcoin.payments.p2wsh({redeem: {output: witnessScript, network}, network})
console.log('P2WSH address  ', p2wsh.address)

Send 1 BTC to this P2WSH address.

$ sendtoaddress [p2wsh.address] 1

Get the output index so that we have the outpoint (txid / vout).

$ getrawtransaction "txid" true

The output of our funding transaction has a locking script composed of <00 version byte> + <32-byte hash witness program>.
SHA256 of the witnessScript must match the 32-byte witness program.

bitcoin.crypto.sha256(witnessScript).toString('hex')

Preparing the spending transaction

Now let's prepare the spending transaction by setting input and output, and the nLockTime value.

Create a BitcoinJS transaction builder object.

const txb = new bitcoin.TransactionBuilder(network)

We need to set the transaction-level locktime in our redeem transaction in order to spend a CLTV. You can use the same value as in the witnessScript.

Because CLTV actually uses nLocktime enforcement consensus rules
the time is checked indirectly by comparing redeem transaction nLocktime with the CLTV value.

nLocktime must be <= present time and >= CLTV timelock

txb.setLockTime(lockTime)

Create the input by referencing the outpoint of our P2WSH funding transaction.
The input-level nSequence value needs to be change to 0xfffffffe, which means that nSequence is disabled, nLocktime is enabled and RBF is not signaled.

// txb.addInput(prevTx, input.vout, input.sequence, prevTxScript)
txb.addInput('TX_ID', TX_VOUT, 0xfffffffe)

Alice_0 will redeem the fund to her P2WPKH address, leaving 100 000 satoshis for the mining fees.

txb.addOutput(p2wpkhAlice0.address, 999e5)

Prepare the transaction.

const tx = txb.buildIncomplete()

Adding the witness stack

Now we can update the transaction with the witness stack (txinwitness field), providing a solution to the locking script.

We generate the hash that will be used to produce the signatures.

Note that we use a special method hashForWitnessV0 for Segwit transactions.

// hashForWitnessV0(inIndex, prevOutScript, value, hashType)
const signatureHash = tx.hashForWitnessV0(0, witnessScript, 1e8, hashType)

There are two ways to redeem the funds, either Alice after the timelock expiry or Alice and Bob at any time. We control which branch of the script we want to run by ending our unlocking script with a boolean value.

First branch: {Alice's signature} OP_TRUE

const witnessStackFirstBranch = bitcoin.payments.p2wsh({
  redeem: {
    input: bitcoin.script.compile([
      bitcoin.script.signature.encode(keyPairAlice0.sign(signatureHash), hashType),
      bitcoin.opcodes.OP_TRUE,
    ]),
    output: witnessScript
  }
}).witness

console.log('First branch witness stack  ', witnessStackFirstBranch.map(x => x.toString('hex')))

Second branch: {Alice's signature} {Bob's signature} OP_FALSE

const witnessStackSecondBranch = bitcoin.payments.p2wsh({
  redeem: {
    input: bitcoin.script.compile([
      bitcoin.script.signature.encode(keyPairAlice0.sign(signatureHash), hashType),
      bitcoin.script.signature.encode(keyPairBob0.sign(signatureHash), hashType),
      bitcoin.opcodes.OP_FALSE
    ]),
    output: witnessScript
  }
}).witness

console.log('First branch witness stack  ', witnessStackSecondBranch.map(x => x.toString('hex')))

We provide the witness stack that BitcoinJS prepared for us.

tx.setWitness(0, [witnessStackFirstBranch OR witnessStackSecondBranch])

Get the raw hex serialization.

No build step here as we have already called buildIncomplete

console.log('tx.toHex  ', tx.toHex())

Inspect the raw transaction with Bitcoin Core CLI, check that everything is correct.

$ decoderawtransaction "hexstring"

Broadcasting the transaction

If you are spending the P2WSH as Alice + timelock after expiry, you must have the node's mediantime to be higher than
the timelock value.

mediantime is the median timestamp of the previous 11 blocks. Check out
BIP113 for more information.

Check the current mediantime

$ getblockchaininfo

You need to generate some blocks in order to have the node's mediantime synchronized with your computer local time.

It is not possible to give you an exact number. 20 should be enough.

$ generate 20

It's now time to broadcast the transaction via Bitcoin Core CLI.

$ sendrawtransaction "hexstring"

Inspect the transaction.

$ getrawtransaction "txid" true

Observations

For both scenarios we note that our scriptSig is empty.

For the first scenario, we note that our witness stack contains

  • Alice_0 signature
  • 1, which is equivalent to OP_TRUE
  • the witness script, that we can decode with decodescript

For the second scenario, we note that our witness stack contains

  • Alice_0 signature
  • Bob_0 signature
  • an empty string, which is equivalent to OP_FALSE
  • the witness script, that we can decode with decodescript

Script with CHECKSEQUENCEVERIFY - Legacy P2SH

To follow along this tutorial and enter the commands step-by-step

  • Type node in a terminal after cd into ./code for a Javascript prompt
  • Open the Bitcoin Core GUI console or use bitcoin-cli for the Bitcoin Core commands
  • Use bx aka Libbitcoin-explorer as a handy complement

Let's create a legacy P2SH transaction with a script that contains the OP_CHECKSEQUENCEVERIFY relative timelock opcode.
The script is almost the same as 9.1: Script with CHECKLOCKTIMEVERIFY - Legacy P2SH but with
a relative timelock of 5 blocks.

To read more about OP_CHECKSEQUENCEVERIFY

Here is the script.
Either Alice can redeem the output of the P2SH after the timelock expiry (after 5 blocks have been mined), or Bob and Alice can redeem the funds at any time.

We will run both scenarios.

function csvCheckSigOutput(aQ, bQ, sequence) {
  return bitcoin.script.compile([
    bitcoin.opcodes.OP_IF,
    bitcoin.script.number.encode(sequence),
    bitcoin.opcodes.OP_CHECKSEQUENCEVERIFY,
    bitcoin.opcodes.OP_DROP,

    bitcoin.opcodes.OP_ELSE,
    bQ.publicKey,
    bitcoin.opcodes.OP_CHECKSIGVERIFY,
    bitcoin.opcodes.OP_ENDIF,

    aQ.publicKey,
    bitcoin.opcodes.OP_CHECKSIG,
  ])
}

Creating and Funding the P2SH

Import libraries, test wallets and set the network and hashType.

const bitcoin = require('bitcoinjs-lib')
const { alice, bob } = require('./wallets.json')
const network = bitcoin.networks.regtest
const hashType = bitcoin.Transaction.SIGHASH_ALL

We also need an additional library to help us with BIP68 relative timelock encoding.

const bip68 = require('bip68')

In both scenarios Alice_0 will get back the funds.

const keyPairAlice0 = bitcoin.ECPair.fromWIF(alice[0].wif, network)
const p2wpkhAlice0 = bitcoin.payments.p2wpkh({pubkey: keyPairAlice0.publicKey, network})

Create a key pair for Bob_0.

const keyPairBob0 = bitcoin.ECPair.fromWIF(bob[0].wif, network)

Encode the sequence value according to BIP68 specification (now + 5 blocks).

const sequence = bip68.encode({blocks: 5})

Generate the redeemScript with CSV 5 blocks from now.

const redeemScript = csvCheckSigOutput(keyPairAlice0, keyPairBob0, sequence)
console.log('redeemScript  ', redeemScript.toString('hex'))

Generate the P2SH.

const p2sh = bitcoin.payments.p2sh({redeem: {output: redeemScript, network}, network})
console.log('p2sh.address  ', p2sh.address)

Send 1 BTC to this P2SH address.

Note that our redeem script doesn't contain any variable data so the P2WSH will always be the same.

$ sendtoaddress 2Mw8mn5xQWk8Pz2KNXLnjSvS6TemKVELLyy 1

Get the output index so that we have the outpoint (txid / vout).

Find the output index (or vout) under details > vout.

$ gettransaction "txid"

Preparing the spending transaction

Now let's prepare the spending transaction by setting input and output, as well as the nSequence value for the first
scenario.

Create a BitcoinJS transaction builder object.

const txb = new bitcoin.TransactionBuilder(network)

Create the input by referencing the outpoint of our P2SH funding transaction.
We add the sequence number only if we want to run the first scenario.

// txb.addInput(prevTx, input.vout, input.sequence, prevTxScript)
txb.addInput('TX_ID', TX_VOUT, [sequence])

Alice_0 will redeem the fund to her P2WPKH address, leaving 100 000 sats for the mining fees.

txb.addOutput(p2wpkhAlice0.address, 999e5)

Prepare the transaction.

const tx = txb.buildIncomplete()

Creating the unlocking script

We generate the hash that will be used to produce the signatures.

const signatureHash = tx.hashForSignature(0, redeemScript, hashType)

There are two ways the redeem the funds, Alice after the timelock expiry or Alice and Bob at any time.
We control which branch of the script we want to run by ending our unlocking script with a boolean value.

First branch: {Alice's signature} OP_TRUE

const inputScriptFirstBranch = bitcoin.payments.p2sh({
  redeem: {
    input: bitcoin.script.compile([
      bitcoin.script.signature.encode(keyPairAlice0.sign(signatureHash), hashType),
      bitcoin.opcodes.OP_TRUE,
    ]),
    output: redeemScript
  },
}).input

Second branch: {Alice's signature} {Bob's signature} OP_FALSE

const inputScriptSecondBranch = bitcoin.payments.p2sh({
  redeem: {
    input: bitcoin.script.compile([
      bitcoin.script.signature.encode(keyPairAlice0.sign(signatureHash), hashType),
      bitcoin.script.signature.encode(keyPairBob0.sign(signatureHash), hashType),
      bitcoin.opcodes.OP_FALSE
    ]),
    output: redeemScript
  }
}).input

Update the transaction with the unlocking script.

tx.setInputScript(0, [inputScriptFirstBranch OR inputScriptSecondBranch])

Get the raw hex serialization.

No build step here as we have already called buildIncomplete.

console.log('tx.toHex  ', tx.toHex())

Inspect the raw transaction with Bitcoin Core CLI, check that everything is correct.

$ decoderawtransaction "hexstring"

Broadcasting the transaction

If we run the first scenario we need 5 blocks to be mined so that the timelock will expire.

$ generate 5

It's time to broadcast the transaction via Bitcoin Core CLI.

$ sendrawtransaction "hexstring"

Inspect the transaction.

$ getrawtransaction "txid" true

Observations

On the first scenario, we note that the input sequence field is 5 and that our scriptSig contains

  • Alice_0 signature
  • 1, which is equivalent to OP_TRUE
  • the redeem script, that we can decode with decodescript

On the second scenario, we note that our scriptSig contains

  • Alice_0 signature
  • Bob_0 signature
  • 0, which is equivalent to OP_FALSE
  • the redeem script, that we can decode with decodescript

Script with CHECKSEQUENCEVERIFY - Native Segwit P2WSH

To follow along this tutorial and enter the commands step-by-step

  • Type node in a terminal after cd into ./code for a Javascript prompt
  • Open the Bitcoin Core GUI console or use bitcoin-cli for the Bitcoin Core commands
  • Use bx aka Libbitcoin-explorer as a handy complement

Let's create a native Segwit P2WSH transaction with a script that contains the OP_CHECKSEQUENCEVERIFY relative timelock opcode.

The script is almost the same as Script with CHECKLOCKTIMEVERIFY - Native Segwit P2WSH
but with a relative timelock of 5 blocks.

To read more about OP_CHECKSEQUENCEVERIFY

Read more about P2WSH in BIP141 - Segregated Witness

Here is the script. Either Alice can redeem the output of the P2WSH after the timelock expiry (after 5 blocks have been mined), or Bob and Alice
can redeem the funds at any time.

We will run both scenarios.

function csvCheckSigOutput(aQ, bQ, sequence) {
  return bitcoin.script.compile([
    bitcoin.opcodes.OP_IF,
    bitcoin.script.number.encode(sequence),
    bitcoin.opcodes.OP_CHECKSEQUENCEVERIFY,
    bitcoin.opcodes.OP_DROP,

    bitcoin.opcodes.OP_ELSE,
    bQ.publicKey,
    bitcoin.opcodes.OP_CHECKSIGVERIFY,
    bitcoin.opcodes.OP_ENDIF,

    aQ.publicKey,
    bitcoin.opcodes.OP_CHECKSIG,
  ])
}

Creating and Funding the P2WSH

Import libraries, test wallets and set the network and hashType.

const bitcoin = require('bitcoinjs-lib')
const { alice, bob } = require('./wallets.json')
const network = bitcoin.networks.regtest
const hashType = bitcoin.Transaction.SIGHASH_ALL

We also need an additional library to help us with BIP68 relative timelock encoding.

const bip68 = require('bip68')

In both scenarios Alice_0 will get back the funds.

const keyPairAlice0 = bitcoin.ECPair.fromWIF(alice[0].wif, network)
const p2wpkhAlice0 = bitcoin.payments.p2wpkh({pubkey: keyPairAlice0.publicKey, network})

Create a key pair for Bob_0.

const keyPairBob0 = bitcoin.ECPair.fromWIF(bob[0].wif, network)

Encode the sequence value according to BIP68 specification (now + 5 blocks).

const sequence = bip68.encode({blocks: 5})

Generate the witnessScript with CSV 5 blocks from now.

In a P2WSH context, a redeem script is called a witness script.

const witnessScript = csvCheckSigOutput(keyPairAlice0, keyPairBob0, sequence)
console.log('witnessScript  ', witnessScript.toString('hex'))

You can decode the script in Bitcoin Core CLI with decodescript.

Generate the P2WSH.

const p2wsh = bitcoin.payments.p2wsh({redeem: {output: witnessScript, network}, network})
console.log('P2WSH address  ', p2wsh.address)

Send 1 BTC to this P2WSH address.

Note that our redeem script doesn't contain any variable data so the P2WSH will always be the same.

$ sendtoaddress bcrt1qjnc0eeslkedv2le9q4t4gak98ygtfx69dlfchlurkyw9rauhuy0qgmazhq 1

Get the output index so that we have the outpoint (txid / vout).

$ getrawtransaction "txid" true

The output of our funding transaction has a locking script composed of  + <32-bytes hash>.
This 32 bytes hash is the SHA256 of our redeem script.

bitcoin.crypto.sha256(witnessScript).toString('hex')

Preparing the spending transaction

Now let's prepare the spending transaction by setting input and output, as well as the nSequence value for the first
scenario.

Create a BitcoinJS transaction builder object.

const txb = new bitcoin.TransactionBuilder(network)

Create the input by referencing the outpoint of our P2WSH funding transaction.
We add the sequence number only if we want to run the first scenario.

// txb.addInput(prevTx, input.vout, input.sequence, prevTxScript)
txb.addInput('TX_ID', TX_VOUT, [sequence])

Alice_0 will redeem the fund to her P2WPKH address, leaving 100 000 satoshis for the mining fees.

txb.addOutput(p2wpkhAlice0.address, 999e5)

Prepare the transaction.

const tx = txb.buildIncomplete()

Adding the witness stack

Now we can update the transaction with the witness stack (txinwitness field), providing a solution to the locking script.

We generate the hash that will be used to produce the signatures.

Note that we use a special method hashForWitnessV0 for Segwit transactions.

// hashForWitnessV0(inIndex, prevOutScript, value, hashType)
const signatureHash = tx.hashForWitnessV0(0, witnessScript, 1e8, hashType)

There are two ways to redeem the funds, either Alice after the timelock expiry or Alice and Bob at any time.
We control which branch of the script we want to run by ending our unlocking script with a boolean value.

First branch: {Alice's signature} OP_TRUE

const witnessStackFirstBranch = bitcoin.payments.p2wsh({
  redeem: {
    input: bitcoin.script.compile([
      bitcoin.script.signature.encode(keyPairAlice0.sign(signatureHash), hashType),
      bitcoin.opcodes.OP_TRUE,
    ]),
    output: witnessScript
  }
}).witness

console.log('First branch witness stack  ', witnessStackFirstBranch.map(x => x.toString('hex')))

Second branch: {Alice's signature} {Bob's signature} OP_FALSE

const witnessStackSecondBranch = bitcoin.payments.p2wsh({
  redeem: {
    input: bitcoin.script.compile([
      bitcoin.script.signature.encode(keyPairAlice0.sign(signatureHash), hashType),
      bitcoin.script.signature.encode(keyPairBob0.sign(signatureHash), hashType),
      bitcoin.opcodes.OP_FALSE
    ]),
    output: witnessScript
  }
}).witness

console.log('First branch witness stack  ', witnessStackSecondBranch.map(x => x.toString('hex')))

We provide the witness stack that BitcoinJS prepared for us.

tx.setWitness(0, [witnessStackFirstBranch OR witnessStackSecondBranch])

Get the raw hex serialization.

No build step here as we have already called buildIncomplete

console.log('tx.toHex  ', tx.toHex())

Inspect the raw transaction with Bitcoin Core CLI, check that everything is correct.

$ decoderawtransaction "hexstring"

Broadcasting the transaction

If we run the first scenario we need 5 blocks to be mined so that the timelock will expire.

$ generate 5

It's time to broadcast the transaction via Bitcoin Core CLI.

$ sendrawtransaction "hexstring"

Inspect the transaction.

$ getrawtransaction "txid" true

Observations

For both scenarios we note that our scriptSig is empty.

For the first scenario, we note that our witness stack contains

  • Alice_0 signature
  • 01, which is equivalent to OP_TRUE
  • the witness script, that we can decode with decodescript

For the second scenario, we note that our witness stack contains

  • Alice_0 signature
  • Bob_0 signature
  • an empty string, which is equivalent to OP_FALSE
  • the witness script, that we can decode with decodescript