I have 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,
]);
I am able to unlock the funds with in the OP_IF branch by using the correct secret, and spending from the recipient public key. However, I am encountering an issue when attempting to redeem funds via the OP_ELSE branch of the script using the timelock lockduration
and broadcasting using the sender 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 believe the error stems from how I am encoding my values. I am building a PSBT which I then sign with xverse and convert into a raw transaction using bitcoin-cli finalizepsbt cHNidP8BAFMCAAAAAa/...
Firstly I create the 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();
}
Then I sign it with xverse wallet using a front end and attempt to finalize the PSBT.
// 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) {
if (!input.partialSig || input.partialSig.length === 0) {
throw new Error(`Cannot finalize input #${inputIndex}: Missing partial signatures`);
}
if (!input.witnessUtxo) {
throw new Error(`Cannot finalize input #${inputIndex}: Missing witness UTXO`);
}
if (!script) {
throw new Error(`Cannot finalize input #${inputIndex}: Missing script`);
}
const signature = input.partialSig[0].signature;
const pubkey = input.partialSig[0].pubkey;
const finalScriptWitness = witnessStackToScriptWitness([
signature,
pubkey,
Buffer.from([0]), // Byte array for OP_FALSE or 0, is this correct?
//Buffer.alloc(0), // Or is this correct?
script
]);
return {
finalScriptWitness: finalScriptWitness
};
}
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 gives me a finalized PSBT which I convert into a raw transaction. Here is the raw transaction decoded:
{
"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 I try to broadcast the transaction
bitcoin-cli sendrawtransaction 0200000000010194a1929b44104244a9e6d36539a29c31bc9da1dacc849041fbffb3e0cef969fa0000000000feffffff01581b00000000000017a914836676150f0f5892416d8bd9bc5e923b494677f38704483045022100cf0ca0b7cbdda5e0c6733d508031c160cb93c7f4e9fcc6bafed4e50e7c23dd6e02200bc9a300ed5d6ec1202de946a59b4fdbb07136d5ffc43f8f5fe1e8fae6d291cd01210269bf7a1301185625758c324221cf6614e4a4104f90f83f633a8790f62919aa0a01005963a820ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb8876a914a5102a75c05993aa082ca365b5ba7f49bed517586751b17576a914a5102a75c05993aa082ca365b5ba7f49bed517586888ac01000000
And I am met with the following error:
error code: -26
error message:
non-mandatory-script-verify-flag (OP_IF/NOTIF argument must be minimal)
Could it be this part of the txinwitness
that is not minimal?
"00",
Should it instead be
"0",
Or perhaps even
"",
Or is there some other reason my OP_IF/NOTIF
argument is not minimal?