Script with CHECKSEQUENCEVERIFY - Native Segwit P2WSH
To follow along this tutorial
|
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:
Learn more about P2WSH:
Either alice_1 can spend the P2WSH 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 P2WSH
const bitcoin = require('bitcoinjs-lib')
const { alice, bob } = require('./wallets.json')
const network = bitcoin.networks.regtest
const witnessStackToScriptWitness = require('./tools/witnessStackToScriptWitness')
const bip68 = require('bip68')
const keyPairAlice1 = bitcoin.ECPair.fromWIF(alice[1].wif, network)
const keyPairBob1 = bitcoin.ECPair.fromWIF(bob[1].wif, network)
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. |
const witnessScript = csvCheckSigOutput(keyPairAlice1, keyPairBob1, lockTime)
console.log('Witness script:')
console.log(witnessScript.toString('hex'))
In a P2WSH context, a redeem script is called a witness script. |
We 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)
sendtoaddress bcrt1qjnc0eeslkedv2le9q4t4gak98ygtfx69dlfchlurkyw9rauhuy0qgmazhq 1
Note that our witness script doesn’t contain any changing data, so the P2WSH address will always be the same. |
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).
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, as well as the nSequence value for the first scenario.
const psbt = new bitcoin.Psbt({network})
psbt
.addInput({
hash: 'TX_ID',
index: TX_VOUT,
sequence: lockTime, (1)
witnessUtxo: {
script: Buffer.from('0020' +
bitcoin.crypto.sha256(witnessScript).toString('hex'),
'hex'),
value: 1e8,
},
witnessScript: Buffer.from(witnessScript, 'hex')
})
1 | Only in case we want to run the first scenario we have to set the sequence field as the timelock value. |
psbt.addOutput({
address: alice[1].p2wpkh,
value: 999e5,
})
Adding the witness stack
Now we can update the transaction with the witness stack (txinwitness
field), providing a solution to the locking script.
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.
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.p2wsh({
redeem: {
input: bitcoin.script.compile([
input.partialSig[0].signature,
bitcoin.opcodes.OP_TRUE,
]),
output: witnessScript
}
})
console.log('First branch witness stack:')
console.log(paymentFirstBranch.witness.map(x => x.toString('hex')))
// Scenario 2
/*
const paymentSecondBranch = bitcoin.payments.p2wsh({
redeem: {
input: bitcoin.script.compile([
input.partialSig[0].signature,
input.partialSig[1].signature,
bitcoin.opcodes.OP_FALSE
]),
output: witnessScript
}
})
console.log('Second branch witness stack:')
console.log(paymentSecondBranch.witness.map(x => x.toString('hex')))
*/
return {
finalScriptWitness: witnessStackToScriptWitness(paymentFirstBranch.witness)
}
}
psbt.finalizeInput(0, getFinalScripts)
console.log('Transaction hexadecimal:')
console.log(psbt.extractTransaction().toHex())
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
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
-
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_1 signature
-
Bob_1 signature
-
an empty string, which is equivalent to OP_FALSE
-
the witness script, that we can decode with
decodescript