Saturday, August 23, 2025
HomeBitcoinjavascript - bitcoinjs-lib: "bad-witness-nonstandard" when spending advanced P2WSH with OP_IF/ELSE

javascript – bitcoinjs-lib: “bad-witness-nonstandard” when spending advanced P2WSH with OP_IF/ELSE

I am making an attempt to create and spend a P2WSH transaction on testnet with a “advanced” script, however I am working right into a bad-witness-nonstandard error that I am unable to determine.

My Objective

I wish to create a P2WSH output with a script that enables spending below two circumstances:

A 3-of-4 multisig signature is offered.
OR
A timelock (e.g., 5 minutes) has handed AND a single restoration signature is offered.
The script logic makes use of OP_IF for the multisig path and OP_ELSE for the timelock path.

The Drawback

I can generate a pockets, derive the P2WSH tackle, and fund it accurately. The funding transaction creates an ordinary P2WSH output (OP_0 <32-byte-hash>).

Nonetheless, when I attempt to spend this UTXO through the 3-of-4 multisig path, my transaction is rejected by my Bitcoin Core node’s testmempoolaccept with the error: mandatory-script-verify-flag-failed (bad-witness-nonstandard)

What I’ve Already Verified

The P2W

  1. SH tackle derived from my scripts matches the tackle I funded.
  2. A debug script confirms that my redeemScript is generated deterministically and its SHA256 hash accurately matches the hash within the funded UTXO’s scriptPubKey.
  3. The funding transaction is right and commonplace.

To Reproduce the Error

Right here is all of the code and knowledge wanted to breed the issue.

  1. bundle.json dependencies:
    {
      "dependencies": {
        "@sorts/node": "^20.12.12",
        "bitcoinjs-lib": "^6.1.5",
        "ecpair": "^2.1.0",
        "ts-node": "^10.9.2",
        "tiny-secp256k1": "^2.2.3",
        "typescript": "^5.4.5"
      }
    }
  1. Pockets Technology Script (index.ts): This script generates the keys and pockets.json.
import * as bitcoin from 'bitcoinjs-lib'; import ECPairFactory from 'ecpair'; 
import * as ecc from 'tiny-secp256k1'; import * as fs from 'fs';

// Initialisation const ECPair = ECPairFactory(ecc); bitcoin.initEccLib(ecc);

const community = bitcoin.networks.testnet;

// --- 1. Key Technology --- 
const multisigKeys = [
    ECPair.makeRandom({ network }),
    ECPair.makeRandom({ network }),
    ECPair.makeRandom({ network }),
    ECPair.makeRandom({ network }), ]; 

const multisigPubkeys = multisigKeys.map(key => Buffer.from(key.publicKey)).kind((a, b) => a.examine(b)); 
const recoveryKey = ECPair.makeRandom({ community }); const recoveryPubkey = Buffer.from(recoveryKey.publicKey);

// --- 2. Timelock Definition (5 minutes for testing) --- 
const date = new Date(); 
date.setMinutes(date.getMinutes() + 5); 
const lockTime = Math.flooring(date.getTime() / 1000); 
const lockTimeBuffer = bitcoin.script.quantity.encode(lockTime);

// --- 3. Redeem Script Building --- 
const redeemScript = bitcoin.script.compile([
    bitcoin.opcodes.OP_IF,
    bitcoin.opcodes.OP_3,
    ...multisigPubkeys,
    bitcoin.opcodes.OP_4,
    bitcoin.opcodes.OP_CHECKMULTISIG,
    bitcoin.opcodes.OP_ELSE,
    lockTimeBuffer,
    bitcoin.opcodes.OP_CHECKLOCKTIMEVERIFY,
    bitcoin.opcodes.OP_DROP,
    recoveryPubkey,
    bitcoin.opcodes.OP_CHECKSIG,
    bitcoin.opcodes.OP_ENDIF, ]);

// --- 4. Handle Creation --- 
const p2wsh = bitcoin.funds.p2wsh({
    redeem: { output: redeemScript, community },
    community, });

// --- 5. Save Information --- 
const pockets = {
    community: 'testnet',
    lockTime: lockTime,
    lockTimeDate: date.toISOString(),
    p2wshAddress: p2wsh.tackle,
    redeemScriptHex: redeemScript.toString('hex'),
    multisigKeysWIF: multisigKeys.map(ok => ok.toWIF()),
    recoveryKeyWIF: recoveryKey.toWIF(), };

fs.writeFileSync('pockets.json', JSON.stringify(pockets, null, 2));

console.log('Pockets generated and saved to pockets.json'); 
console.log('P2WSH Deposit Handle:', pockets.p2wshAddress);
  1. Multisig Spending Script (1_spend_multisig.ts): That is the script that fails with bad-witness-nonstandard.
import * as bitcoin from 'bitcoinjs-lib'; 
import ECPairFactory from 'ecpair'; 
import * as ecc from 'tiny-secp256k1'; 
import * as fs from 'fs';

// --- UTXO Configuration --- 
const UTXO_TXID = 'PASTE_YOUR_FUNDING_TXID_HERE'; 
const UTXO_INDEX = 0; // Or 1, relying on the output 
const UTXO_VALUE_SATS = 10000; // Quantity in satoshis 
const DESTINATION_ADDRESS = 'PASTE_A_TESTNET_ADDRESS_HERE'; 
const FEE_SATS = 2000;

// --- Initialization --- 
const ECPair = ECPairFactory(ecc); bitcoin.initEccLib(ecc); 
const community = bitcoin.networks.testnet;

// --- 1. Load Pockets --- 
const pockets = JSON.parse(fs.readFileSync('pockets.json', 'utf-8')); 
const redeemScript = Buffer.from(pockets.redeemScriptHex, 'hex'); 
const p2wsh = bitcoin.funds.p2wsh({ redeem: { output: redeemScript, community }, community }); 
const multisigKeys = pockets.multisigKeysWIF.map((wif: string) => ECPair.fromWIF(wif, community));

// --- 2. Construct PSBT --- 
const psbt = new bitcoin.Psbt({ community }); psbt.addInput({
    hash: UTXO_TXID,
    index: UTXO_INDEX,
    witnessUtxo: { script: p2wsh.output!, worth: UTXO_VALUE_SATS },
    witnessScript: redeemScript, }); 
psbt.addOutput({ tackle: DESTINATION_ADDRESS, worth: UTXO_VALUE_SATS - FEE_SATS });

// --- 3. Signal Transaction --- 
const createSigner = (key: any) => ({   publicKey: Buffer.from(key.publicKey),   
    signal: (hash: Buffer): Buffer => Buffer.from(key.signal(hash)), }); // Signal with 3 of the 4 keys 
psbt.signInput(0, createSigner(multisigKeys[0])); 
psbt.signInput(0, createSigner(multisigKeys[1])); 
psbt.signInput(0, createSigner(multisigKeys[2]));

// --- 4. Finalize Transaction --- 
const finalizer = (inputIndex: quantity, enter: any) => {
    const emptySignature = Buffer.from([]); // Placeholder for OP_CHECKMULTISIG bug
    const partialSignatures = enter.partialSig.map((ps: any) => ps.signature);
    const witnessStack = [
        emptySignature,
        ...partialSignatures,
        bitcoin.script.number.encode(1), // Standard way to push OP_1
        redeemScript,
    ];
    const witness = witnessStack.cut back((acc, merchandise) => {
        const push = bitcoin.script.compile([item]);
        return Buffer.concat([acc, push]);
    }, Buffer.from([witnessStack.length]));
    return { finalScriptWitness: witness }; }; psbt.finalizeInput(0, finalizer);

// --- 5. Extract and create validation command --- 
const tx = psbt.extractTransaction(); 
const txHex = tx.toHex(); 
console.log('n--- testmempoolaccept command ---'); 
console.log(`bitcoin-cli -testnet testmempoolaccept '["${txHex}"]'`);
  1. Information to breed:

pockets.json (TESTNET KEYS, NO VALUE):

{   
    "community": "testnet",   
    "lockTime": 1723986942,   
    "lockTimeDate": "2025-08-18T13:15:42.339Z",   
    "p2wshAddress": "tb1qztq5rg30lv8y7kup7tftuelppcy2f9u9ygm8daq7gv4lgf0dw3ss3hj9qw",   
    "redeemScriptHex": "6353210200847c4a13f98cb1e3138bda175ba6f4c7ffd9e03a4c8617878ab03cf4a4a97921024b3e2544b4e311985477d88ac77ea00aa68f85490d0c663fba38fcdf582d043f2102c822f5026d382a93476d20de66c87c5e4e4997654817bfacc69b29f2dc8b6a10210328c2213b0813b4dac9c063f674b2c61dc50344c6e093df045c8ee2fe09f67bd854ae6704c413a368b1752103c5512e31f8a2555a116146262382be4be774fca326a2ee01d71e0fe33ffe4925ac68", 
    "multisigKeysWIF": [
        "cT5h8LgJ2a4V3c4yF5g6H7j8K9L0M1n2p3q4R5s6T7u8V9w0XyZ",
        "cT5h8LgJ2a4V3c4yF5g6H7j8K9L0M1n2p3q4R5s6T7u8V9w0XyZ",
        "cT5h8LgJ2a4V3c4yF5g6H7j8K9L0M1n2p3q4R5s6T7u8V9w0XyZ",
        "cT5h8LgJ2a4V3c4yF5g6H7j8K9L0M1n2p3q4R5s6T7u8V9w0XyZ"   ],   
"recoveryKeyWIF": "cT5h8LgJ2a4V3c4yF5g6H7j8K9L0M1n2p3q4R5s6T7u8V9w0XyZ" }

(Observe: this can be generated utilizing index.ts).

Funding Transaction:
 
Funded Handle: tb1qztq5rg30lv8y7kup7tftuelppcy2f9u9ygm8daq7gv4lgf0dw3ss3hj9qw   
Funding TXID: e9e764b3c63740d0eef68506970e80f819d360bdfc173d0b983f1e3d5411096d  
Funding VOUT: 1   
Funding ScriptPubKey: OP_0 12c141a22ffb0e4f5b81f2d2be67e10e08a49785223676f41e432bf425ed7461  

Any concept why my manually constructed witness could be thought-about non-standard?
Thanks.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments