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
- SH tackle derived from my scripts matches the tackle 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 commonplace.
To Reproduce the Error
Right here is all of the code and knowledge 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)).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);
- 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}"]'`);
- 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.