I’m trying to create and spend a P2WSH transaction on TestNet with a “complex” script, but I’m running into a bad, purposeless error that I don’t know.
My goal
I want to create a P2WSH output using a script that allows spending under two conditions:
A 4-to-4 multi-sig signature is provided. Alternatively, a time lock (for example, 5 minutes) will pass and a single recovery signature will be provided. The script logic uses OP_IF for the Multisig path and OP_ELSE for the Timelock path.
problem
You can generate wallets, derive P2WSH addresses, and provide funding correctly. Funds trading creates standard P2WSH output (OP_0 <32-byte-hash>).
However, if you try to use this UTXO via a 3-4 multisig path, the transaction is rejected by testMempoolAccept in Bitcoin Core Node with an error.
What I’ve already verified
P2W
- The SH address derived from my script matches the address I funded.
- The debug script checks that my redeemscript is deterministically generated and that sha256 hash matches correctly with the hash of the funded utxo scriptpubkey.
- Funding transactions are correct and standard.
Reproduce the error
There 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 keys and wallet.json.
*Imported as Bitcoin from ‘bitcoinjs-lib’. Import ecpairfactory from “ecpair”. Imported as ECC from ‘tiny-secp256k1’. Import as fs from ‘fs’ *.
//Initialize 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)). const recoverykey = ecpair.makerandom({network}); const recoverypubkey = buffer.from(RecoveryKey.publickey);
// — 2. Time lock definition (5 minutes for tests) — const date = new date(); date.setminutes(date.getMinutes() + 5); const locktime = math.floor(date.getTime() / 1000); const locktimebuffer = bitcoin.script.number.encode(locktime);
// -bitcoin.opcodes.op_else, locktimebuffer, bitcoin.opcodes.op_checktimeverify, bitcoin.opcodes.op_drop, recoveryubkey, bitcoin.opcodes.op_checksig, bitcoin.opcodes.op_endif));
// — 4. Address creation — const p2wsh = bitcoin.payments.p2wsh({redeeem:{output:redeeemscript,network},network,});
// — 5. Data storage — const wallet = {Network: ‘testnet’, locktime: locktime, locktimedate: date.toisostring(), p2wshaddress: p2wsh.address, reeemscriptex: redeemscript.toString(‘hex’), multigkeyswif: multisigkeys.map() RecoveryKeyWif: RecoveryKey.Towif(), };
fs.writefilesync(‘wallet.json’, json.stringify(wallet, null, 2));
console.log (‘wallets generated and saved in wallet.json’); console.log (‘p2wsh deposit address:’, wallet.p2wshaddress);
- Multisig Expense Script (1_SPEND_MULTISIG.TS): This is a script that fails with a bad purpose attitude.
*Imported as Bitcoin from ‘bitcoinjs-lib’. Import ecpairfactory from “ecpair”. Imported 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, const utxo_value_sats = 10000; // atshis 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.redeemscriptex, ‘hex’); const p2wsh = bitcoin.payments.p2wsh({redeem:{output:redeemscript,network},network}); const multisigkeys = wallet.multisigkeyswif.map((wif:string)=> ecpair.fromwif(wif,network));
// —2. buildpsbt — const psbt = new bitcoin.psbt ({network}); psbt.addinput({hash: utxo_txid, index: utxo_index, witherututxo: {script: p2wsh.output!, value: utxo_value_sats}, withinsscript: redeemscript, }); psbt.addoutput({address: destination_address, value: utxo_value_sats -fee_sats});
// — 3. Symbol 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 (0, Createsigner(Multisigkeys(0))). psbt.signinput(0, createsigner(multisigkeys(1))); psbt.signinput(0, createsigner(multisigkeys(2)));// — 4. Finalize transaction — constinginizer = (inputIndex: number, input: any) => {const emptysignature = buffer.from(()); // op_checkmultisig bug const partialsignatures = input.partialsig.map((ps: any)=> ps.signature); const veritionStack = (emptysignature, … partialsignatures, bitcoin.script.number.encode(1), // Standard way to push OP_1 RedeemScript); const videns = withingstack.reduce((acc, item)=> {const push = bitcoin.script.compile((item)); return buffer.concat((acc,push));}, buffer.from((withingstack.length))); return {finalalscriptwitness:withing}; }; psbt.finalizeinput(0, finalizer);
// — 5. Extract and create a validation command —- const tx = psbt.extracttransaction(); const txhex = tx.tohex(); console.log( ‘\n — testmempolaccept command —‘); console.log(
bitcoin-cli -testnet testmempoolaccept '("${txHex}")'
);
- Data to replicate:
wallet.json (testnet key, no value):
{“network”: “testnet”, “locktime”: 1723986942, “locktimedate”: “2025-08-18t13:15:42.339z”, “p2wshaddress”: “TB1QZTQ5RG30LV8Y7KUP7TFTUELPPCY2F9U9YGM8DAQ7GV4LGF0DW3SS3HJ9QW”,
“Redeemscriptex”: “6353210200847c4a13f98cb1e3138bda175ba6f4c7ffd9e03a4c8617878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787878787 85477D88AC77EA00A68F85490D0C663FBA38FCDF582D043F2102C8225556D382A93476D20DE66C87C5E4E499765 4817BFACC69B29F2DC8B6A10210328C2213B0813B4DAC9C063F674B2C61DC50344C6E093DF045C8EE2FE09F67BD85 4AE6704C413A368B1752103C5512E31F8A2555A116146262382BE4BE774FCA326A2EE01D71E0FE33FFE4925AC68″”, “multisigkeyswif”: (“ct5h8lgj2a4v3c4yf5g6h7j8k9l0m1n2p3pp3pp3p3q4r5s6t7u8v9w0xyz”, “,” ct5h8lgj2a4vv3c4yf5g6h7j8k9l0m1n2q4r59ws6t7u8v25t7ws6t7ws6t7v259w0x4r50x4r50x4r50x4r59wt7 “CT5H8LGJ2A4V3C4YF5G6H7J8K9L0M1N2PP3Q4R5S6T7U8V9W0XYZ”, “CT5H8LGJ2A4V3C4YF5G6H7J8K9L0M1N2P3Q4R5S6T7U8V9WWWWF”: “: “CT5H8LGJ2A4V3C4YF5G6H7J8K9L0M1N2PP3Q4R5S6T7U8V9W0XYZ”}
(Note: this is generated using index.ts).
Funding Transactions:
Funding: TB1QZTQ5RG30LG30LV8Y7KUP7TFTUELPPCY2F9U9YGM8DAQ7GV4LGF0DW3SS3HJ9QWFundingTXID: E9E764B3C63740D0EEF68506970E80F819D360BDFC173D0B983F1E3D5411096DFundingVOUT: 1FundingScriptPubkey: OP_0 12C141A22FFB0E4F5B81F2D2BE67E10E08A49785223676F41E432BF425ED7461
Why is my manually constructed witness considered non-standard? thank you.
Discover more from Earlybirds Invest
Subscribe to get the latest posts sent to your email.