Monday, August 18, 2025
HomeBitcoinjavascript - bitcoinjs-lib: "bad-witness-nonstandard" when spending complicated P2WSH with OP_IF/ELSE

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

I am attempting to create and spend a P2WSH transaction on testnet with a “complicated” script, however I am operating right into a bad-witness-nonstandard error that I can not determine.

My Purpose

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 handle, and fund it accurately. The funding transaction creates a normal P2WSH output (OP_0 <32-byte-hash>).

Nevertheless, 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 handle derived from my scripts matches the handle 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 normal.

To Reproduce the Error

Right here is all of the code and information 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)).type((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. Deal with Creation — const p2wsh = bitcoin.funds.p2wsh({
redeem: { output: redeemScript, community },
community, });

// — 5. Save Knowledge — const pockets = {
community: ‘testnet’,
lockTime: lockTime,
lockTimeDate: date.toISOString(),
p2wshAddress: p2wsh.handle,
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 Deal with:’, 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({ handle: 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.scale 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. Knowledge 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 shall be generated utilizing index.ts).

Funding Transaction:

Funded Deal with:
tb1qztq5rg30lv8y7kup7tftuelppcy2f9u9ygm8daq7gv4lgf0dw3ss3hj9qw Funding
TXID: e9e764b3c63740d0eef68506970e80f819d360bdfc173d0b983f1e3d5411096d
Funding VOUT: 1 Funding ScriptPubKey: OP_0
12c141a22ffb0e4f5b81f2d2be67e10e08a49785223676f41e432bf425ed7461

Any thought 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