bitcoinjs/bitcoinjs-lib

Psbt.signInput Passed but Broadcast Failure with msg `non-mandatory-script-verify-flag (Witness program hash mismatch)`

VictorZhang2014 opened this issue · 5 comments

Hi
My workbench parameters are :

node v22.1.0

"bitcoinjs-lib": "^6.1.6",
"tiny-secp256k1": "^2.2.3",
"ecpair": "^2.1.0"

The full code snippet as shown below

const bitcoin = require('bitcoinjs-lib'); 
const ecc = require('tiny-secp256k1'); 
const ecpair = require('ecpair'); 
bitcoin.initEccLib(ecc);
 
const ECPair = ecpair.ECPairFactory(ecc);  
const toXOnly = (pubKey) => pubKey.length === 32 ? pubKey : pubKey.slice(1, 33);

const LEAF_VERSION_TAPSCRIPT = 0xc0;
const SEQUENCE = 0xfffffffd;

async function etching(from, privateKey, to, serviceFee) {
    const network = bitcoin.networks.bitcoin;

    const utxo = {
        txid: '5de2fd2a35de70ec1699efb0399a6076c1ad60822e1ac2c5af7193b1c72dfbe8',
        vout: 0,
        value: 8000
    } 

    const keyPair = ECPair.fromPrivateKey(Buffer.from(privateKey, 'hex'), { network });  

    const etching_script = Buffer.from("209393706508000ab7b942c8138383d7bfb8641b692068b9eb60fb6a9634e4be3aac00630b0a6bc5b1f67df96acb22b768", "hex"); 
 
    const runestoneScript = Buffer.from("6a5d20020304eb8ac7b5dfafbeb5cbc5dc0503900405bf4106000a904e08a09c011601", "hex");

    const scriptTree = {
        output: etching_script,
    }
    const script_p2tr = bitcoin.payments.p2tr({
        internalPubkey: toXOnly(keyPair.publicKey),
        scriptTree,
        network,
    });
    const etching_redeem = {
        output: etching_script,
        redeemVersion: LEAF_VERSION_TAPSCRIPT
    }
    const etching_p2tr = bitcoin.payments.p2tr({
        internalPubkey: toXOnly(keyPair.publicKey),
        scriptTree,
        redeem: etching_redeem,
        network
    }); 

    const psbt = new bitcoin.Psbt({ network });
    psbt.addInput({
        hash: utxo.txid,
        index: utxo.vout,
        witnessUtxo: { value: utxo.value, script: script_p2tr.output },
        tapLeafScript: [
            {
                leafVersion: etching_redeem.redeemVersion,
                script: etching_redeem.output,
                controlBlock: etching_p2tr.witness[etching_p2tr.witness.length - 1] 
            }
        ],
        sequence: SEQUENCE,
    });

    psbt.addOutput({
        script: runestoneScript,
        value: 0
    })

    const fee = 5500;
    const change = utxo.value - 546 - fee;

    psbt.addOutput({
        address: to,  
        value: 546
    });
    psbt.addOutput({
        address: serviceFee,
        value: change
    });

    psbt.signInput(0, keyPair);
    psbt.finalizeAllInputs();

    let feeRate = psbt.getFeeRate();
    const rawTxHex = psbt.extractTransaction().toHex() 
    console.log({rawTxHex, feeRate}) 
}

const t0 = async () => {
    const fromAddr = "bc1pv4fhy64300wdr7regcanc3q7z5qry0nss8xvvvm4hyey7zxuhwusyq8qx2"
    const fromPubKey = "029393706508000ab7b942c8138383d7bfb8641b692068b9eb60fb6a9634e4be3a" 
    const fromPrivateKey = "PRIVATE KEY"

    const to = "bc1pe5x2f3yahhqr6tcyzy3m0vf96350u33240yzzqt7wmqhguf8m4zs8pdvt5"
    const serviceFee = "bc1pe5x2f3yahhqr6tcyzy3m0vf96350u33240yzzqt7wmqhguf8m4zs8pdvt5"

    await etching(fromAddr, fromPrivateKey, to, serviceFee)
}
t0()

The output is

{
  rawTxHex: '02000000000101e8fb2dc7b19371afc5c21a2e8260adc176609a39b0ef9916ec70de352afde25d0000000000fdffffff030000000000000000236a5d20020304eb8ac7b5dfafbeb5cbc5dc0503900405bf4106000a904e08a09c0116012202000000000000225120cd0ca4c49dbdc03d2f041123b7b125d468fe462aabc821017e76c1747127dd45a207000000000000225120cd0ca4c49dbdc03d2f041123b7b125d468fe462aabc821017e76c1747127dd4503408d5b61b3aa6d23810f3bba992852613773c18a80bda453831b81e45008d18eba3512d3a3193bc35c5cf880c1c6144c06e9e8dfea6c57f226933c072c902e43a531209393706508000ab7b942c8138383d7bfb8641b692068b9eb60fb6a9634e4be3aac00630b0a6bc5b1f67df96acb22b76821c09393706508000ab7b942c8138383d7bfb8641b692068b9eb60fb6a9634e4be3a00000000',
  feeRate: 25
}

When I tried to broadcast the transaction with the RawHex, I always got error with message: sendrawtransaction RPC error: {"code":-26,"message":"non-mandatory-script-verify-flag (Witness program hash mismatch)"}

The broadcast API is https://mempool.space/api/tx.

I have stuck into the issue for weeks, please save me, I really appreciate your assistance and code guidance.

I guessed that the issue is the Tapscript signature, if yes, but why it passed the PSBT sign?

After looking at the code, I think the privateKey may need to be tweaked.
I'm not sure if this is the problem.

Can you provide a runnable demo repo to debug?

Are you trying to key spend or script spend?

Script Spend

it('can create (and broadcast via 3PBP) a taproot script-path spend Transaction - OP_CHECKSIG', async () => {
const internalKey = bip32.fromSeed(rng(64), regtest);
const leafKey = bip32.fromSeed(rng(64), regtest);
const leafScriptAsm = `${toXOnly(leafKey.publicKey).toString(
'hex',
)} OP_CHECKSIG`;
const leafScript = bitcoin.script.fromASM(leafScriptAsm);
const scriptTree: Taptree = [
[
{
output: bitcoin.script.fromASM(
'50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG',
),
},
[
{
output: bitcoin.script.fromASM(
'50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac1 OP_CHECKSIG',
),
},
{
output: bitcoin.script.fromASM(
'2258b1c3160be0864a541854eec9164a572f094f7562628281a8073bb89173a7 OP_CHECKSIG',
),
},
],
],
[
{
output: bitcoin.script.fromASM(
'50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac2 OP_CHECKSIG',
),
},
[
{
output: bitcoin.script.fromASM(
'50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac3 OP_CHECKSIG',
),
},
[
{
output: bitcoin.script.fromASM(
'50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac4 OP_CHECKSIG',
),
},
{
output: leafScript,
},
],
],
],
];
const redeem = {
output: leafScript,
redeemVersion: LEAF_VERSION_TAPSCRIPT,
};
const { output, witness } = bitcoin.payments.p2tr({
internalPubkey: toXOnly(internalKey.publicKey),
scriptTree,
redeem,
network: regtest,
});
// amount from faucet
const amount = 42e4;
// amount to send
const sendAmount = amount - 1e4;
// get faucet
const unspent = await regtestUtils.faucetComplex(output!, amount);
const psbt = new bitcoin.Psbt({ network: regtest });
psbt.addInput({
hash: unspent.txId,
index: 0,
witnessUtxo: { value: amount, script: output! },
});
psbt.updateInput(0, {
tapLeafScript: [
{
leafVersion: redeem.redeemVersion,
script: redeem.output,
controlBlock: witness![witness!.length - 1],
},
],
});
const sendInternalKey = bip32.fromSeed(rng(64), regtest);
const sendPubKey = toXOnly(sendInternalKey.publicKey);
const { address: sendAddress } = bitcoin.payments.p2tr({
internalPubkey: sendPubKey,
scriptTree,
network: regtest,
});
psbt.addOutput({
value: sendAmount,
address: sendAddress!,
tapInternalKey: sendPubKey,
tapTree: { leaves: tapTreeToList(scriptTree) },
});
psbt.signInput(0, leafKey);
psbt.finalizeInput(0);
const tx = psbt.extractTransaction();
const rawTx = tx.toBuffer();
const hex = rawTx.toString('hex');
await regtestUtils.broadcast(hex);
await regtestUtils.verify({
txId: tx.getId(),
address: sendAddress!,
vout: 0,
value: sendAmount,
});
});

Key Spend (with unused script tree)

it('can create (and broadcast via 3PBP) a taproot key-path spend Transaction (with unused scriptTree)', async () => {
const internalKey = bip32.fromSeed(rng(64), regtest);
const leafKey = bip32.fromSeed(rng(64), regtest);
const leafScriptAsm = `${toXOnly(leafKey.publicKey).toString(
'hex',
)} OP_CHECKSIG`;
const leafScript = bitcoin.script.fromASM(leafScriptAsm);
const scriptTree = {
output: leafScript,
};
const { output, address, hash } = bitcoin.payments.p2tr({
internalPubkey: toXOnly(internalKey.publicKey),
scriptTree,
network: regtest,
});
// amount from faucet
const amount = 42e4;
// amount to send
const sendAmount = amount - 1e4;
// get faucet
const unspent = await regtestUtils.faucetComplex(output!, amount);
const psbt = new bitcoin.Psbt({ network: regtest });
psbt.addInput({
hash: unspent.txId,
index: 0,
witnessUtxo: { value: amount, script: output! },
tapInternalKey: toXOnly(internalKey.publicKey),
tapMerkleRoot: hash,
});
psbt.addOutput({ value: sendAmount, address: address! });
const tweakedSigner = internalKey.tweak(
bitcoin.crypto.taggedHash(
'TapTweak',
Buffer.concat([toXOnly(internalKey.publicKey), hash!]),
),
);
psbt.signInput(0, tweakedSigner);
psbt.finalizeAllInputs();
const tx = psbt.extractTransaction();
const rawTx = tx.toBuffer();
const hex = rawTx.toString('hex');
await regtestUtils.broadcast(hex);
await regtestUtils.verify({
txId: tx.getId(),
address: address!,
vout: 0,
value: sendAmount,
});
});

For the part of Script Spend in line of 220, which ran as expected, but what the two scripts '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 OP_CHECKSIG' and '2258b1c3160be0864a541854eec9164a572f094f7562628281a8073bb89173a7 OP_CHECKSIG' indicate in the scriptTree data? @junderw

In fact, I'm working around with the Runes etching, it requires two transactions, first is commit and second is reveal separately. As you mentioned above Script Spend or Key Spend should be used in the commit transaction or reveal transaction?

... indicate in the scriptTree data?

These are leaves in the script tree. Any one of these scripts may be selected to spend a UTXO given to this p2tr address. It seems like in your code there is only one leaf. So there's not much of a tree. A large tree is not required, though. One script is fine. Your protocol seems to require the script to be used... so I am guessing your code should use script spend.

I'm working around with the Runes etching

This is a Bitcoin library, if you would like support with other protocols, please ask questions on their forum. You are more likely to get an answer.

I would assume that reveal is "revealing" the witness, so that's probably it, but again I have no clue. Read the protocol documentation (if there is any).

Thank you for the hints. @junderw