Script with CHECKLOCKTIMEVERIFY - Native Segwit P2WSH
To follow along this tutorial
|
Let’s create a native Segwit P2WSH transaction with a script that contains the OP_CHECKLOCKTIMEVERIFY
absolute timelock opcode.
Learn more:
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 P2WSH
const bitcoin = require('bitcoinjs-lib')
const { alice, bob } = require('./wallets.json')
const network = bitcoin.networks.regtest
const hashType = bitcoin.Transaction.SIGHASH_ALL
const bip65 = require('bip65')
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)
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. |
const witnessScript = cltvCheckSigOutput(keyPairAlice1, keyPairBob1, lockTime)
console.log('Witness script:')
console.log(witnessScript.toString('hex'))
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 changing timestamp. |
You can decode the script in Bitcoin Core CLI with decodescript
.
const p2wsh = bitcoin.payments.p2wsh({redeem: {output: witnessScript, network}, network})
console.log('P2WSH address:')
console.log(p2wsh.address)
If you do it multiple times you will notice that the P2WSH address is never the same, this is because of the changing witness script. |
sendtoaddress P2WSH_ADDR 1
getrawtransaction TX_ID true
The output script of our funding transaction is a versioned witness program.
It is composed as follow: <00 version byte>
+ <32-byte hash witness program>
.
The SHA256 hash of the witness script (in the witness of the spending tx) must match the 32-byte witness program (in prevTxOut).
console.log(bitcoin.crypto.sha256(witnessScript).toString('hex'))
or
bx sha256 WITNESS_SCRIPT
Preparing the spending transaction
Now let’s prepare the spending transaction by setting input and output, and the nLockTime value.
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)
// txb.addInput(prevTx, prevOut, sequence, prevTxScript)
txb.addInput('TX_ID', TX_VOUT, 0xfffffffe) (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. |
txb.addOutput(p2wpkhAlice1.address, 999e5)
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.
// hashForWitnessV0(inIndex, prevOutScript, value, hashType)
const signatureHash = tx.hashForWitnessV0(0, witnessScript, 1e8, hashType) (1)
1 | Note that we use a special method hashForWitnessV0 for Segwit transactions. |
There are two ways to redeem the funds, either 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.
const witnessStackFirstBranch = bitcoin.payments.p2wsh({
redeem: {
input: bitcoin.script.compile([
bitcoin.script.signature.encode(keyPairAlice1.sign(signatureHash), hashType),
bitcoin.opcodes.OP_TRUE,
]),
output: witnessScript
}
}).witness
console.log('First branch witness stack:')
console.log(witnessStackFirstBranch.map(x => x.toString('hex')))
const witnessStackSecondBranch = bitcoin.payments.p2wsh({
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: witnessScript
}
}).witness
console.log('Second branch witness stack:')
console.log(witnessStackSecondBranch.map(x => x.toString('hex')))
We provide the witness stack that BitcoinJS prepared for us.
tx.setWitness(0, witnessStackFirstBranch)
//tx.setWitness(0, witnessStackSecondBranch)
No build
step here as we have already called buildIncomplete
console.log('Transaction hexadecimal:')
console.log(tx.toHex())
decoderawtransaction TX_HEX
Broadcasting the transaction
If you are spending the P2WSH as alice_1 + 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 both scenarios we note that our scriptSig is empty.
For the first scenario, we note that our witness stack contains:
-
Alice_1 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_1 signature
-
Bob_1 signature
-
An empty string, which is equivalent to OP_FALSE
-
The witness script, that we can decode with
decodescript
The SHA256 hash of the witness script (in the witness of the spending tx) matches the 32-byte witness program (in prevTxOut).