Script with CHECKLOCKTIMEVERIFY - Legacy P2SH
To follow along this tutorial
|
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 on her P2WPKH address 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.fromASM(
`
OP_IF
${bitcoin.script.number.encode(lockTime).toString('hex')}
OP_CHECKLOCKTIMEVERIFY
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
const bitcoin = require('bitcoinjs-lib')
const { alice, bob } = require('./wallets.json')
const network = bitcoin.networks.regtest
const bip65 = require('bip65')
const keyPairAlice1 = bitcoin.ECPair.fromWIF(alice[1].wif, network)
const keyPairBob1 = bitcoin.ECPair.fromWIF(bob[1].wif, network)
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. |
Make sure to use the same lockTime throughout the tutorial. You can run the code a first time to get a lockTime and hardcode that value everywhere it’s needed. |
const redeemScript = cltvCheckSigOutput(keyPairAlice1, keyPairBob1, lockTime)
console.log('Redeem script:')
console.log(redeemScript.toString('hex'))
We can decode the script in Bitcoin Core CLI with decodescript
.
const p2sh = bitcoin.payments.p2sh({redeem: {output: redeemScript, network}, network})
console.log('P2SH address:')
console.log(p2sh.address)
The P2SH address depends on the redeemScript which depends on the lockTime, make sure to hardcode the lockTime. |
sendtoaddress P2SH_ADDR 1
gettransaction TX_ID
Find the output index (or vout) under | .
Preparing the spending transaction
Now let’s prepare the spending transaction by setting input and output, and the nLockTime value.
const psbt = new bitcoin.Psbt({network})
We need to set the transaction-level locktime in our redeem transaction in order to spend a CLTV. This is only required when executing the first scenario (Alice_1 + CLTV). Use the same value that you used in the redeemScript.
psbt.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 |
psbt.addInput({
hash: 'TX_ID',
index: TX_VOUT,
sequence: 0xfffffffe, (1)
nonWitnessUtxo: Buffer.from('TX_HEX','hex'),
redeemScript: Buffer.from(redeemScript, 'hex')
})
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. |
psbt.addOutput({
address: alice[1].p2wpkh,
value: 999e5,
})
Creating the unlocking script
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.
psbt.signInput(0, keyPairAlice1)
psbt.signInput(0, keyPairBob1)
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)
console.log('Transaction hexadecimal:')
console.log(psbt.extractTransaction().toHex())
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.
|
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
sendrawtransaction TX_HEX
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