I’m trying to create and spend a P2WSH transaction on testnet with a “complex” script, but I’m running into a bad-witness-nonstandard error that I can’t figure out.
My Goal
I want to create a P2WSH output with a script that allows spending under two conditions:
A 3-of-4 multisig signature is provided.
OR
A timelock (e.g., 5 minutes) has passed AND a single recovery signature is provided.
The script logic uses OP_IF for the multisig path and OP_ELSE for the timelock path.
The Problem
I can generate a wallet, derive the P2WSH address, and fund it correctly. The funding transaction creates a standard P2WSH output (OP_0 <32-byte-hash>).
However, when I try to spend this UTXO via 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
- SH address derived from my scripts matches the address I funded.
- A debug script confirms that my redeemScript is generated deterministically and its SHA256 hash correctly matches the hash in the funded UTXO’s scriptPubKey.
- The funding transaction is correct and standard.
To Reproduce the Error
Here is all the code and data needed to reproduce the problem.
- package.json dependencies:
{
"dependencies": {
"@types/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"
}
}
- Wallet Generation Script (index.ts): This script generates the keys and wallet.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 network = bitcoin.networks.testnet;
// --- 1. Key Generation ---
const multisigKeys = [
ECPair.makeRandom({ network }),
ECPair.makeRandom({ network }),
ECPair.makeRandom({ network }),
ECPair.makeRandom({ network }), ];
const multisigPubkeys = multisigKeys.map(key => Buffer.from(key.publicKey)).sort((a, b) => a.compare(b));
const recoveryKey = ECPair.makeRandom({ network }); 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.floor(date.getTime() / 1000);
const lockTimeBuffer = bitcoin.script.number.encode(lockTime);
// --- 3. Redeem Script Construction ---
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. Address Creation ---
const p2wsh = bitcoin.payments.p2wsh({
redeem: { output: redeemScript, network },
network, });
// --- 5. Save Data ---
const wallet = {
network: 'testnet',
lockTime: lockTime,
lockTimeDate: date.toISOString(),
p2wshAddress: p2wsh.address,
redeemScriptHex: redeemScript.toString('hex'),
multisigKeysWIF: multisigKeys.map(k => k.toWIF()),
recoveryKeyWIF: recoveryKey.toWIF(), };
fs.writeFileSync('wallet.json', JSON.stringify(wallet, null, 2));
console.log('Wallet generated and saved to wallet.json');
console.log('P2WSH Deposit Address:', wallet.p2wshAddress);
- Multisig Spending Script (1_spend_multisig.ts): This 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, depending on the output
const UTXO_VALUE_SATS = 10000; // Amount in satoshis
const DESTINATION_ADDRESS = 'PASTE_A_TESTNET_ADDRESS_HERE';
const FEE_SATS = 2000;
// --- Initialization ---
const ECPair = ECPairFactory(ecc); bitcoin.initEccLib(ecc);
const network = bitcoin.networks.testnet;
// --- 1. Load Wallet ---
const wallet = JSON.parse(fs.readFileSync('wallet.json', 'utf-8'));
const redeemScript = Buffer.from(wallet.redeemScriptHex, 'hex');
const p2wsh = bitcoin.payments.p2wsh({ redeem: { output: redeemScript, network }, network });
const multisigKeys = wallet.multisigKeysWIF.map((wif: string) => ECPair.fromWIF(wif, network));
// --- 2. Build PSBT ---
const psbt = new bitcoin.Psbt({ network }); psbt.addInput({
hash: UTXO_TXID,
index: UTXO_INDEX,
witnessUtxo: { script: p2wsh.output!, value: UTXO_VALUE_SATS },
witnessScript: redeemScript, });
psbt.addOutput({ address: DESTINATION_ADDRESS, value: UTXO_VALUE_SATS - FEE_SATS });
// --- 3. Sign Transaction ---
const createSigner = (key: any) => ({ publicKey: Buffer.from(key.publicKey),
sign: (hash: Buffer): Buffer => Buffer.from(key.sign(hash)), }); // Sign 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: number, input: any) => {
const emptySignature = Buffer.from([]); // Placeholder for OP_CHECKMULTISIG bug
const partialSignatures = input.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.reduce((acc, item) => {
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}"]'`);
- Data to reproduce:
wallet.json (TESTNET KEYS, NO VALUE):
{
"network": "testnet",
"lockTime": 1723986942,
"lockTimeDate": "2025-08-18T13:15:42.339Z",
"p2wshAddress": "tb1qztq5rg30lv8y7kup7tftuelppcy2f9u9ygm8daq7gv4lgf0dw3ss3hj9qw",
"redeemScriptHex": "6353210200847c4a13f98cb1e3138bda175ba6f4c7ffd9e03a4c8617878ab03cf4a4a97921024b3e2544b4e311985477d88ac77ea00aa68f85490d0c663fba38fcdf582d043f2102c822f5026d382a93476d20de66c87c5e4e4997654817bfacc69b29f2dc8b6a10210328c2213b0813b4dac9c063f674b2c61dc50344c6e093df045c8ee2fe09f67bd854ae6704c413a368b1752103c5512e31f8a2555a116146262382be4be774fca326a2ee01d71e0fe33ffe4925ac68",
"multisigKeysWIF": [
"cT5h8LgJ2a4V3c4yF5g6H7j8K9L0M1n2p3q4R5s6T7u8V9w0XyZ",
"cT5h8LgJ2a4V3c4yF5g6H7j8K9L0M1n2p3q4R5s6T7u8V9w0XyZ",
"cT5h8LgJ2a4V3c4yF5g6H7j8K9L0M1n2p3q4R5s6T7u8V9w0XyZ",
"cT5h8LgJ2a4V3c4yF5g6H7j8K9L0M1n2p3q4R5s6T7u8V9w0XyZ" ],
"recoveryKeyWIF": "cT5h8LgJ2a4V3c4yF5g6H7j8K9L0M1n2p3q4R5s6T7u8V9w0XyZ" }
(Note: this will be generated using index.ts).
Funding Transaction:
Funded Address: tb1qztq5rg30lv8y7kup7tftuelppcy2f9u9ygm8daq7gv4lgf0dw3ss3hj9qw
Funding TXID: e9e764b3c63740d0eef68506970e80f819d360bdfc173d0b983f1e3d5411096d
Funding VOUT: 1
Funding ScriptPubKey: OP_0 12c141a22ffb0e4f5b81f2d2be67e10e08a49785223676f41e432bf425ed7461
Any idea why my manually constructed witness would be considered non-standard?
Thank you.











