Script with CHECKSEQUENCEVERIFY - 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_CHECKSEQUENCEVERIFY relative timelock opcode. The script is almost the same as Script with CHECKLOCKTIMEVERIFY - Legacy P2SH but with a relative timelock of 5 blocks.

Learn more about OP_CHECKSEQUENCEVERIFY:

Either alice_1 can spend the P2SH UTXO but only when 5 blocks have been mined after the funding transaction is first confirmed, or bob_1 and alice_1 can redeem the funds at any time.

function csvCheckSigOutput(aQ, bQ, lockTime) {
  return bitcoin.script.fromASM(
    `
      OP_IF
          ${bitcoin.script.number.encode(lockTime).toString('hex')}
          OP_CHECKSEQUENCEVERIFY
          OP_DROP
      OP_ELSE
          ${bQ.publicKey.toString('hex')}
          OP_CHECKSIGVERIFY
      OP_ENDIF
      ${aQ.publicKey.toString('hex')}
      OP_CHECKSIG
    `
      .trim()
      .replace(/\s+/g, ' '),
  );
}

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
We also need an additional library to help us with BIP68 relative timelock encoding.
const bip68 = require('bip68')
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)
Set the relative timelock to 5 blocks (to be mined on top of the funding transaction confirmation).
const lockTime = bip68.encode({blocks: 5}) (1)
console.log('Timelock in blocks:')
console.log(lockTime)
1 We encode the timelock value according to BIP68 specification.
Generate the redeem script.
const redeemScript = csvCheckSigOutput(keyPairAlice1, keyPairBob1, timelock)
console.log('Redeem script:')
console.log(redeemScript.toString('hex'))
Generate the P2SH address.
const p2sh = bitcoin.payments.p2sh({redeem: {output: redeemScript, network}, network})
console.log('P2SH address:')
console.log(p2sh.address)
Send 1 BTC to this P2SH address.
sendtoaddress 2Mw8mn5xQWk8Pz2KNXLnjSvS6TemKVELLyy 1
Note that our redeem script doesn’t contain any changing data, so the P2SH address will always be the same.
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, as well as the nSequence value for the first scenario.

Create a BitcoinJS transaction builder object.
const psbt = new bitcoin.Psbt({network})
Create the input by filling TX_ID, TX_OUT and TX_HEX.
psbt.addInput({
  hash: 'TX_ID',
  index: TX_VOUT,
  sequence: lockTime, (1)
  nonWitnessUtxo: Buffer.from('TX_HEX','hex'),
  redeemScript: Buffer.from(redeemScript, 'hex')
})
1 Only in case we want to run the first scenario we have to set the sequence field as the timelock value.
The funds will be redeemed to Alice_1 P2WPKH address, leaving 100 000 satoshis for the mining fees.
psbt.addOutput({
  address: alice[1].p2wpkh,
  value: 999e5,
})

Creating the unlocking script

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

Alice_1 signs the transaction that we just built with her private key.
psbt.signInput(0, keyPairAlice1)
Only in scenario 2 Bob_1 signs the transaction.
psbt.signInput(0, keyPairBob1)
Finalize the PSBT.
const getFinalScripts = (inputIndex, input, script) => {
  // Step 1: Check to make sure the meaningful locking script matches what you expect.
  const decompiled = bitcoin.script.decompile(script)
  if (!decompiled || decompiled[0] !== bitcoin.opcodes.OP_IF) {
    throw new Error(`Can not finalize input #${inputIndex}`)
  }

  // Step 2: Create final scripts
  // Scenario 1
  const paymentFirstBranch = bitcoin.payments.p2sh({
    redeem: {
      input: bitcoin.script.compile([
        input.partialSig[0].signature,
        bitcoin.opcodes.OP_TRUE,
      ]),
      output: redeemScript
    }
  })

  // Scenario 2
  /*
  const paymentSecondBranch = bitcoin.payments.p2sh({
    redeem: {
      input: bitcoin.script.compile([
        input.partialSig[0].signature,
        input.partialSig[1].signature,
        bitcoin.opcodes.OP_FALSE
      ]),
      output: redeemScript
    }
  })
  */

  return {
    finalScriptSig: paymentFirstBranch.input
  }
}

psbt.finalizeInput(0, getFinalScripts)
Extract the transaction and get the raw hex serialization.
console.log('Transaction hexadecimal:')
console.log(psbt.extractTransaction().toHex())
Inspect the raw transaction with Bitcoin Core CLI, check that everything is correct.
decoderawtransaction TX_HEX

Broadcasting the transaction

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

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

Observations

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

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

  • Bob_1 signature

  • 0, which is equivalent to OP_FALSE

  • the redeem script, that we can decode with decodescript