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
- SH handle derived from my scripts matches the handle I funded.
- A debug script confirms that my redeemScript is generated deterministically and its SHA256 hash accurately matches the hash within the funded UTXO’s scriptPubKey.
- 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.
- 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" } }
- 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);
- 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}"]'
);
- 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.