4

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?

1 Answer 1

6

In SegWit (by standardness) and Taproot (by consensus), the arguments to OP_IF and OP_NOTIF must be minimal, i.e. be either the empty vector or the one-byte vector 01. Your transaction uses 00, which is neither.

The code that composes your witness stack should probably be:

const finalScriptWitness = witnessStackToScriptWitness([
    signature,
    pubkey,
    Buffer.from([]),
    script
]);
2
  • Thank you <3, this worked for me Commented Apr 30 at 9:13
  • Yep that did it. :) Thank you for the help
    – Dev Oskii
    Commented Apr 30 at 9:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.