script – Why are my P2WSH OP_IF/NOTIF arguments not minimal?
I created an HTLC with the following Bitcoin script using bitcoinlib-js.
const redeemScript = bitcoin.script.compile((
bitcoin.opcodes.OP_IF,
bitcoin.opcodes.OP_SHA256,
secretHashBuffer,
bitcoin.opcodes.OP_EQUALVERIFY,
bitcoin.opcodes.OP_DUP,
bitcoin.opcodes.OP_HASH160,
recipientHash, // Hashed recipient public key
bitcoin.opcodes.OP_ELSE,
bitcoin.script.number.encode(lockduration),
bitcoin.opcodes.OP_CHECKLOCKTIMEVERIFY,
bitcoin.opcodes.OP_DROP,
bitcoin.opcodes.OP_DUP,
bitcoin.opcodes.OP_HASH160,
senderHash, // Hashed sender public key
bitcoin.opcodes.OP_ENDIF,
bitcoin.opcodes.OP_EQUALVERIFY,
bitcoin.opcodes.OP_CHECKSIG,
));
You can unlock funds at the OP_IF branch by using the correct secret and spending from the recipient public key. However, I ran into a problem when trying to redeem funds through the OP_ELSE branch of the script using a time lock. lockduration
Broadcast using the sender’s public key.
The specific error I get is:
error code: -26
error message:
non-mandatory-script-verify-flag (OP_IF/NOTIF argument must be minimal)
I think the error is coming from the way I encode my values. I’m building a PSBT that I sign with xverse and convert to a raw transaction using: bitcoin-cli finalizepsbt cHNidP8BAFMCAAAAAa/...
First, create a PSBT.
import * as bitcoin from 'bitcoinjs-lib';
import crypto from "crypto";
export function createRefundPSBT(
secretHash: string,
lockduration: number,
scriptPubKeyHex: string,
htlcTxId: string,
htlcOutputIndex: number,
refundAmount: number,
recipientPubKey : string,
senderPubKey : string,
recipientAddress : string,
networkType : string
)
const network =
networkType === "testnet"
? bitcoin.networks.testnet
: bitcoin.networks.bitcoin;
const secretHashBuffer = Buffer.from(secretHash, "hex");
// Recreate the HTLC script using the provided secret
const recipientHash = bitcoin.crypto.hash160(
Buffer.from(recipientPubKey, "hex")
);
const senderHash = bitcoin.crypto.hash160(Buffer.from(senderPubKey, "hex"));
const redeemScript = bitcoin.script.compile((
bitcoin.opcodes.OP_IF,
bitcoin.opcodes.OP_SHA256,
secretHashBuffer,
bitcoin.opcodes.OP_EQUALVERIFY,
bitcoin.opcodes.OP_DUP,
bitcoin.opcodes.OP_HASH160,
recipientHash, // Hashed recipient public key
bitcoin.opcodes.OP_ELSE,
bitcoin.script.number.encode(lockduration),
bitcoin.opcodes.OP_CHECKLOCKTIMEVERIFY,
bitcoin.opcodes.OP_DROP,
bitcoin.opcodes.OP_DUP,
bitcoin.opcodes.OP_HASH160,
senderHash, // Hashed sender public key
bitcoin.opcodes.OP_ENDIF,
bitcoin.opcodes.OP_EQUALVERIFY,
bitcoin.opcodes.OP_CHECKSIG,
));
const scriptPubKey = Buffer.from(scriptPubKeyHex, "hex");
console.log("Creating PSBT");
// Create a PSBT
const psbt = new bitcoin.Psbt( network: network )
.addInput(
hash: htlcTxId,
index: htlcOutputIndex,
sequence: 0xfffffffe, // Necessary for OP_CHECKLOCKTIMEVERIFY
witnessUtxo:
script: scriptPubKey,
value: refundAmount,
,
witnessScript: redeemScript,
)
.addOutput(
address: recipientAddress,
value: refundAmount - 3000, // Subtract a nominal fee
)
.setVersion(2)
.setLocktime(lockduration);
console.log("PSBT to be signed:", psbt.toBase64());
return psbt.toBase64();
We then use the frontend to sign with the xverse wallet and attempt PSBT finalization.
// This script is for refund
import * as bitcoin from 'bitcoinjs-lib';
import varuint from 'varuint-bitcoin';
function witnessStackToScriptWitness(witness: any)
let buffer = Buffer.allocUnsafe(0);
function writeSlice(slice: string)
buffer = Buffer.concat((buffer, Buffer.from(slice)));
function writeVarInt(i: number)
const varintLen = varuint.encodingLength(i);
const currentLen = buffer.length;
buffer = Buffer.concat((buffer, Buffer.allocUnsafe(varintLen)));
varuint.encode(i, buffer, currentLen);
function writeVarSlice(slice: string)
writeVarInt(slice.length);
writeSlice(slice);
function writeVector(vector: any)
writeVarInt(vector.length);
vector.forEach(writeVarSlice);
writeVector(witness);
return buffer;
function getFinalScriptsSpecial(inputIndex: number, input: any, script: any, isSegwit: boolean, isP2SH: boolean, isP2WSH: boolean)
export function prepareFinalRefundPSBT(
witnessHexWithdraw: string,
psbtBase64: string,
): string | null
let psbt: any;
try
psbt = bitcoin.Psbt.fromBase64(psbtBase64);
psbt.isSegwit = true;
psbt.isP2SH = false;
psbt.isP2WSH = true;
psbt.finalizeInput(0, getFinalScriptsSpecial);
//adding witness script hex back in from bitcoin-cli decodepsbt <psbtBase64> because psbt.finalizeInput removes it for some reason
psbt.data.inputs(0).witnessScript = Buffer.from(witnessHexWithdraw, 'hex');
catch (e)
console.error(`Error finalizing PSBT: $e`);
process.exit(1);
return psbt.toBase64();
This will give you the final PSBT that is converted into a raw transaction. Here is the decoded raw transaction:
"txid": "d773911f5e0c2bd4590794676e7c95bd2e33d8156c22e64c80e8c5bf4cfcbece",
"hash": "5f1025270b4f2b298d2a90647c2640cc222815d0df3e2d38b72b9e37b04c14ff",
"version": 2,
"size": 285,
"vsize": 134,
"weight": 534,
"locktime": 1,
"vin": (
"txid": "fa69f9cee0b3fffb419084ccdaa19dbc319ca23965d3e6a9444210449b92a194",
"vout": 0,
"scriptSig":
"asm": "",
"hex": ""
,
"txinwitness": (
"3045022100cf0ca0b7cbdda5e0c6733d508031c160cb93c7f4e9fcc6bafed4e50e7c23dd6e02200bc9a300ed5d6ec1202de946a59b4fdbb07136d5ffc43f8f5fe1e8fae6d291cd01",
"0269bf7a1301185625758c324221cf6614e4a4104f90f83f633a8790f62919aa0a",
"00",
"63a820ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb8876a914a5102a75c05993aa082ca365b5ba7f49bed517586751b17576a914a5102a75c05993aa082ca365b5ba7f49bed517586888ac"
),
"sequence": 4294967294
),
"vout": (
"value": 0.00007000,
"n": 0,
"scriptPubKey":
"asm": "OP_HASH160 836676150f0f5892416d8bd9bc5e923b494677f3 OP_EQUAL",
"desc": "addr(2N5E1FYfiQgM5U8aTvU1btZYpiTVPE9ReaK)#00u0x08a",
"hex": "a914836676150f0f5892416d8bd9bc5e923b494677f387",
"address": "2N5E1FYfiQgM5U8aTvU1btZYpiTVPE9ReaK",
"type": "scripthash"
)
Then we want to broadcast the transaction.
bitcoin-cli sendrawtransaction 0200000000010194a1929b44104244a9e6d36539a29c31bc9da1dacc849041fbffb3e0cef969fa0000000000feffffff01581b00000000000017a914836676150f0f5892416d8bd9bc5e923b494677f38704483045022100cf0ca0b7cbdda5e0c6733d508031c160cb93c7f4e9fcc6bafed4e50e7c23dd6e02200bc9a300ed5d6ec1202de946a59b4fdbb07136d5ffc43f8f5fe1e8fae6d291cd01210269bf7a1301185625758c324221cf6614e4a4104f90f83f633a8790f62919aa0a01005963a820ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb8876a914a5102a75c05993aa082ca365b5ba7f49bed517586751b17576a914a5102a75c05993aa082ca365b5ba7f49bed517586888ac01000000
And I got the following error:
error code: -26
error message:
non-mandatory-script-verify-flag (OP_IF/NOTIF argument must be minimal)
Isn’t this the part? txinwitness
That’s not the minimum, is it?
"00",
Rather, should it be so?
"0",
Or maybe
"",
Or is there another reason? OP_IF/NOTIF
Isn’t the debate minimal?