Script with CHECKLOCKTIMEVERIFY - Legacy P2SH

To follow along this tutorial

  • Clone the Github repository

  • cd code

  • npm install or yarn install

  • Execute the transaction code by typing node tx_filename.js

  • Alternatively you can enter the commands step-by-step by cd into ./code then type node in a terminal to open the Node.js REPL

  • 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.

Learn more about OP_CHECKLOCKTIMEVERIFY in BIP65

Either alice_1 can redeem the funds after the timelock has expired, or bob_1 and alice_1 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 generatetoaddress 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.
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 sighash type.
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')
Alice_1 and bob_1 are the signers.
const keyPairAlice1 = bitcoin.ECPair.fromWIF(alice[1].wif, network)
const keyPairBob1 = bitcoin.ECPair.fromWIF(bob[1].wif, network)
In both scenarios alice_1 P2WPKH address will get back the funds.
const p2wpkhAlice1 = bitcoin.payments.p2wpkh({pubkey: keyPairAlice1.publicKey, network})
console.log('P2WPKH address')
console.log(p2wpkhAlice1.address)
Encode the lockTime value according to BIP65 specification (now - 6 hours).
const lockTime = bip65.encode({utc: Math.floor(Date.now() / 1000) - (3600 * 6)}) (1)
console.log('Timelock in UNIX timestamp:')
console.log(lockTime)
1 Method argument is a UNIX timestamp.
Generate the redeemScript with CLTV.
const redeemScript = cltvCheckSigOutput(keyPairAlice1, keyPairBob1, lockTime)
console.log('Redeem script:')
console.log(redeemScript.toString('hex'))
If we do it multiple times you will notice that the hex script is never the same, this is because of the locktime.

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

Generate the P2SH.
const p2sh = bitcoin.payments.p2sh({redeem: {output: redeemScript, network}, network})
console.log('P2SH address:')
console.log(p2sh.address)
If we do it multiple times you will notice that the P2SH address is never the same, this is because of redeemScript.
Send 1 BTC to this P2SH address.
sendtoaddress [p2sh.address] 1
Get the output index so that we have the outpoint (txid / vout).
gettransaction TX_ID
Find the output index (or vout) under details  vout.

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.

txb.setLockTime(lockTime)
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
Create the input by referencing the outpoint of our P2SH funding transaction.
// txb.addInput(prevTx, prevOut, sequence, prevTxScript)
txb.addInput('TX_ID', TX_VOUT, 0xfffffffe, null) (1)
1 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.
Alice_1 will redeem the fund to her P2WPKH address, leaving 100 000 satoshis for the mining fees.
txb.addOutput(p2wpkhAlice1.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(keyPairAlice1.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(keyPairAlice1.sign(signatureHash), hashType),
      bitcoin.script.signature.encode(keyPairBob1.sign(signatureHash), hashType),
      bitcoin.opcodes.OP_FALSE
    ]),
    output: redeemScript
  }
}).input
Update the transaction with the input script you have chosen.
tx.setInputScript(0, inputScriptFirstBranch)
//tx.setInputScript(0, inputScriptSecondBranch)

No build step here as we have already called buildIncomplete

Get the raw hex serialization.
console.log('Transaction hexadecimal')
console.log(tx.toHex())
Inspect the raw transaction with Bitcoin Core CLI, check that everything is correct.
decoderawtransaction TX_HEX

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. Dave_1 is our miner.

generatetoaddress 20 bcrt1qnqud2pjfpkqrnfzxy4kp5g98r8v886wgvs9e7r
It’s now time to broadcast the transaction via Bitcoin Core CLI.
sendrawtransaction TX_HEX
Inspect the transaction.
getrawtransaction TX_ID true

Observations

For the first scenario, we note that our scriptSig contains:

  • Alice_1 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_1 signature

  • Bob_1 signature

  • 0, which is equivalent to OP_FALSE

  • the redeem script, that we can decode with decodescript