bitcoinjs/bitcoinjs-lib

A problem in trying scriptless transcation

UIZorrot opened this issue · 14 comments

Hi, I'm trying to using a single aggr pubkey as the pubkey of the p2tr, therefore I expected to using only one signature to unlock the psbt, and in this case, I want to sign input[0].

The function get_agg_* is from musig2 and passed test case, so it should working fine. The get_agg_sign should sign a msg in schnorr manner.

However, this function does not work, it reported mandatory-script-verify-flag-failed (Invalid Schnorr signature) at the last of the function, So I think I may make some mistakes.

I think the problem is coming from the msg that I should sign, the code snippet is here:

    const psbtInput = psbt.data.inputs[0];
  
    const transaction = new Transaction;
    const signatureHash = transaction.hashForSignature(0, p2pktr.redeem!.output!, Transaction.SIGHASH_ALL);
  
    let msg = signatureHash;
    let sign = get_agg_sign(wallets, options, Buff.from(msg));
  
    let tapKeySig = Buffer.from(sign)
    psbt.updateInput(0, { tapKeySig })

My question is, Have I making mistake in let msg = signatureHash ? Or I making mistakes somewhere else? Or It's impossible at the first place?

Below is all of the code:

async function start_musig(keypair: Signer) {
    try {
        // Encode an example string as bytes.
        let wallets = get_agg_keypair(5)
        let options = get_option(wallets)
        let pub = get_agg_pub(wallets, options)

        console.log('Testing schnorr tx.')

        // Generate an address from the tweaked public key
        const p2pktr = payments.p2tr({
            pubkey: Buffer.from(pub),
            network
        });

        // const p2pktr = payments.p2tr({
        //     pubkey: Buffer.from(pub),
        //     network
        // });

        const p2pktr_addr = p2pktr.address ?? "";
        console.log(`Waiting till UTXO is detected at this Address: ${p2pktr_addr}`)

        // push trans but not confirm
        let temp_trans = await pushTrans(p2pktr_addr)
        console.log("the new txid is:", temp_trans)

        // get UTXO
        const utxos = await getUTXOfromTx(temp_trans, p2pktr_addr)
        console.log(`Using UTXO ${utxos.txid}:${utxos.vout}`);

        const psbt = new Psbt({ network });

        psbt.addInput({
            hash: utxos.txid,
            index: utxos.vout,
            witnessUtxo: { value: utxos.value, script: p2pktr.output! },
            tapInternalKey: Buffer.from(pub),
        });

        console.log(psbt.data.inputs[0])

        // utxos.value
        psbt.addOutput({
            address: "bcrt1q5hk8re6mar775fxnwwfwse4ql9vtpn6x558g0w", // main wallet address 
            value: utxos.value - 150
        });

        // Can use validator to get input hash
        let schnorrValidator = (
            pubkey: Buffer,
            msghash: Buffer,
            signature: Buffer,
        ): boolean => {
            return tinysecp.verifySchnorr(msghash, pubkey, signature);
        }

        const psbtInput = psbt.data.inputs[0];

        const transaction = new Transaction;
        const signatureHash = transaction.hashForSignature(0, p2pktr.redeem!.output!, Transaction.SIGHASH_ALL);

        let msg = signatureHash;
        let sign = get_agg_sign(wallets, options, Buff.from(msg));

        let tapKeySig = Buffer.from(sign)
        psbt.updateInput(0, { tapKeySig })
        // psbt.directsign(0, Buffer.from(pub), Buffer.from(sign))
        console.log(psbt.data.inputs)
        psbt.finalizeAllInputs();
        console.log(psbt.data.inputs)

        const isValid2 = schnorr.verify(Buffer.from(sign), Buffer.from(msg), Buffer.from(pub))
        if (isValid2) { console.log('The signature should validate using another library.') }

        const tx = psbt.extractTransaction();
        console.log(`Broadcasting Transaction Hex: ${tx.toHex()}`);
        console.log("Txid is:", tx.getId());

        // Borad cast will failed
        const txHex = await broadcast(tx.toHex());
        console.log(`Success! TxHex is ${txHex}`);

        // generate new block to lookup
        await pushBlock(p2pktr_addr)

    } catch (error) {
        console.error('The error occur in:', error);
    }
}

hashForSignature is for pre-segwit sighashes. Please use segwit v1 (not v0)

Use the hashForWitnessV1 method instead. As taproot commits to all the outputs of the previous transaction, you will need to get an array of all scripts from all outputs of the previous transaction as well as an array of all the values of all the outputs too.

hashForWitnessV1(
inIndex: number,
prevOutScripts: Buffer[],
values: number[],
hashType: number,
leafHash?: Buffer,
annex?: Buffer,
): Buffer {
// https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#common-signature-message
typeforce(
types.tuple(
types.UInt32,
typeforce.arrayOf(types.Buffer),
typeforce.arrayOf(types.Satoshi),
types.UInt32,
),
arguments,
);
if (
values.length !== this.ins.length ||
prevOutScripts.length !== this.ins.length
) {
throw new Error('Must supply prevout script and value for all inputs');
}
const outputType =
hashType === Transaction.SIGHASH_DEFAULT
? Transaction.SIGHASH_ALL
: hashType & Transaction.SIGHASH_OUTPUT_MASK;
const inputType = hashType & Transaction.SIGHASH_INPUT_MASK;
const isAnyoneCanPay = inputType === Transaction.SIGHASH_ANYONECANPAY;
const isNone = outputType === Transaction.SIGHASH_NONE;
const isSingle = outputType === Transaction.SIGHASH_SINGLE;
let hashPrevouts = EMPTY_BUFFER;
let hashAmounts = EMPTY_BUFFER;
let hashScriptPubKeys = EMPTY_BUFFER;
let hashSequences = EMPTY_BUFFER;
let hashOutputs = EMPTY_BUFFER;
if (!isAnyoneCanPay) {
let bufferWriter = BufferWriter.withCapacity(36 * this.ins.length);
this.ins.forEach(txIn => {
bufferWriter.writeSlice(txIn.hash);
bufferWriter.writeUInt32(txIn.index);
});
hashPrevouts = bcrypto.sha256(bufferWriter.end());
bufferWriter = BufferWriter.withCapacity(8 * this.ins.length);
values.forEach(value => bufferWriter.writeUInt64(value));
hashAmounts = bcrypto.sha256(bufferWriter.end());
bufferWriter = BufferWriter.withCapacity(
prevOutScripts.map(varSliceSize).reduce((a, b) => a + b),
);
prevOutScripts.forEach(prevOutScript =>
bufferWriter.writeVarSlice(prevOutScript),
);
hashScriptPubKeys = bcrypto.sha256(bufferWriter.end());
bufferWriter = BufferWriter.withCapacity(4 * this.ins.length);
this.ins.forEach(txIn => bufferWriter.writeUInt32(txIn.sequence));
hashSequences = bcrypto.sha256(bufferWriter.end());
}
if (!(isNone || isSingle)) {
const txOutsSize = this.outs
.map(output => 8 + varSliceSize(output.script))
.reduce((a, b) => a + b);
const bufferWriter = BufferWriter.withCapacity(txOutsSize);
this.outs.forEach(out => {
bufferWriter.writeUInt64(out.value);
bufferWriter.writeVarSlice(out.script);
});
hashOutputs = bcrypto.sha256(bufferWriter.end());
} else if (isSingle && inIndex < this.outs.length) {
const output = this.outs[inIndex];
const bufferWriter = BufferWriter.withCapacity(
8 + varSliceSize(output.script),
);
bufferWriter.writeUInt64(output.value);
bufferWriter.writeVarSlice(output.script);
hashOutputs = bcrypto.sha256(bufferWriter.end());
}
const spendType = (leafHash ? 2 : 0) + (annex ? 1 : 0);
// Length calculation from:
// https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-14
// With extension from:
// https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki#signature-validation
const sigMsgSize =
174 -
(isAnyoneCanPay ? 49 : 0) -
(isNone ? 32 : 0) +
(annex ? 32 : 0) +
(leafHash ? 37 : 0);
const sigMsgWriter = BufferWriter.withCapacity(sigMsgSize);
sigMsgWriter.writeUInt8(hashType);
// Transaction
sigMsgWriter.writeInt32(this.version);
sigMsgWriter.writeUInt32(this.locktime);
sigMsgWriter.writeSlice(hashPrevouts);
sigMsgWriter.writeSlice(hashAmounts);
sigMsgWriter.writeSlice(hashScriptPubKeys);
sigMsgWriter.writeSlice(hashSequences);
if (!(isNone || isSingle)) {
sigMsgWriter.writeSlice(hashOutputs);
}
// Input
sigMsgWriter.writeUInt8(spendType);
if (isAnyoneCanPay) {
const input = this.ins[inIndex];
sigMsgWriter.writeSlice(input.hash);
sigMsgWriter.writeUInt32(input.index);
sigMsgWriter.writeUInt64(values[inIndex]);
sigMsgWriter.writeVarSlice(prevOutScripts[inIndex]);
sigMsgWriter.writeUInt32(input.sequence);
} else {
sigMsgWriter.writeUInt32(inIndex);
}
if (annex) {
const bufferWriter = BufferWriter.withCapacity(varSliceSize(annex));
bufferWriter.writeVarSlice(annex);
sigMsgWriter.writeSlice(bcrypto.sha256(bufferWriter.end()));
}
// Output
if (isSingle) {
sigMsgWriter.writeSlice(hashOutputs);
}
// BIP342 extension
if (leafHash) {
sigMsgWriter.writeSlice(leafHash);
sigMsgWriter.writeUInt8(0);
sigMsgWriter.writeUInt32(0xffffffff);
}
// Extra zero byte because:
// https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-19
return bcrypto.taggedHash(
'TapSighash',
Buffer.concat([Buffer.from([0x00]), sigMsgWriter.end()]),
);
}

I'm so sorry, that I may need to ask here again (sincerely, I just don't know anywhere else to ask), the code is still output same error Invaild schnorr sigature. And I have changed the segwit to V1. Here is the alternative code:

        let wallets = get_agg_keypair(5)
        let options = get_option(wallets)
        let pub = get_agg_pub(wallets, options)

        console.log('Testing schnorr tx.')

        // Generate an address from the tweaked public key
        const p2pktr = payments.p2tr({
            internalPubkey: Buffer.from(pub),
            network
        });

        const p2pktr_addr = p2pktr.address ?? "";
        console.log(`Waiting till UTXO is detected at this Address: ${p2pktr_addr}`)

        // push trans but not confirm
        let temp_trans = await pushTrans(p2pktr_addr)
        console.log("the new txid is:", temp_trans)

        // get UTXO
        const utxos = await getUTXOfromTx(temp_trans, p2pktr_addr)
        console.log(`Using UTXO ${utxos.txid}:${utxos.vout}`);

        const psbt = new Psbt({ network });

        psbt.addInput({
            hash: utxos.txid,
            index: utxos.vout,
            witnessUtxo: { value: utxos.value, script: p2pktr.output! },
            tapInternalKey: Buffer.from(pub),
        });

        // utxos.value
        psbt.addOutput({
            address: "bcrt1q5hk8re6mar775fxnwwfwse4ql9vtpn6x558g0w", // main wallet address 
            value: utxos.value - 150
        });

        let transaction = new Transaction;
        transaction.addInput(Buffer.from(utxos.txid, 'hex').reverse(), utxos.vout)
        transaction.addOutput(p2pktr.output!, utxos.value - 150)
        let signatureHash = transaction.hashForWitnessV1(0, [p2pktr.output!], [utxos.value - 150], Transaction.SIGHASH_ALL);

        let msg = signatureHash
        let sign = get_agg_sign(wallets, options, Buff.from(msg));

        let tapKeySig = Buffer.from(sign)
        psbt.updateInput(0, { tapKeySig })
        console.log(psbt.data.inputs)
        psbt.finalizeAllInputs();
        console.log(psbt.data.inputs)

        // Here is perfectly passed, when using a third-party schnorr verification
        // const isValid2 = schnorr.verify(Buffer.from(sign), Buffer.from(msg), Buffer.from(pub))
        // if (isValid2) { console.log('The signature should validate using another library.') }

        const tx = psbt.extractTransaction();
        console.log(`Broadcasting Transaction Hex: ${tx.toHex()}`);
        console.log("Txid is:", tx.getId());

        // Borad cast will failed
        const txHex = await broadcast(tx.toHex());
        console.log(`Success! TxHex is ${txHex}`);

        // generate new block to lookup
        await pushBlock(p2pktr_addr)

So according to your code, the full transaction with the txid of utxos.txid contains only 1 output.
That output's script is equal to p2pktr.output! and the value of that same output is equal to utxos.value - 150.

This doesn't make sense, because your input (which points to the transaction at utxos.txid) says { value: utxos.value, script: p2pktr.output! } which contradicts what your hashForWitnessV1 call is saying.

You need:

  • all of the scripts
  • all of the values
  • from all of the outputs

of the transaction with the id utxos.txid

I'll trying, thank you again!

Hi, sorry to bored you again, It will be the last time I come here to ask, in case its heavily disturbing you
I think I have done everything right, but it still not work.
By the way, It passed two different schnorr verification, so I just can't figure out where is the problem

async function start_musig_txbuilder() {
    let wallets = get_agg_keypair(5);
    let options = get_option(wallets);
    let pub = get_agg_pub(wallets, options);

    console.log('Testing schnorr tx.');

    // Generate an address from the tweaked public key
    const p2pktr = payments.p2tr({
        pubkey: Buffer.from(pub),
        network
    });

    const p2pktr_addr = p2pktr.address ?? "";
    console.log(`Waiting till UTXO is detected at this Address: ${p2pktr_addr}`);

    // Push transaction but not confirm
    let temp_trans = await pushTrans(p2pktr_addr);
    console.log("the new txid is:", temp_trans);

    // Get UTXO
    const utxos = await getUTXOfromTx(temp_trans, p2pktr_addr);
    console.log(`Using UTXO ${utxos.txid}:${utxos.vout}`);

    // Building a new transaction
    let transaction = new Transaction(); // Assuming you have defined network
    transaction.addInput(Buffer.from(utxos.txid, 'hex').reverse(), utxos.vout);

    const scriptPubKey = bitcoin.address.toOutputScript("bcrt1q5hk8re6mar775fxnwwfwse4ql9vtpn6x558g0w", network);

    transaction.addOutput(scriptPubKey, utxos.value - 200);

    const pubKey = Buffer.from(pub);
    const prevOutScript = bitcoin.script.compile([
        bitcoin.opcodes.OP_1,
        pubKey,
    ]);

    let signatureHash = transaction.hashForWitnessV1(0, [prevOutScript], [utxos.value], Transaction.SIGHASH_ALL);
    let sign: Buff = get_agg_sign(wallets, options, signatureHash);
    let tapKeySig = Buffer.from(sign); // Ensure the signature is in the correct format

    transaction.ins[0].witness = [tapKeySig];

    // Check if the signature is valid.
    const isValid2 = schnorr.verify(sign, signatureHash, pub)
    if (isValid2) { console.log('The signature should validate using another library.') }
    // Both of this two passed.
    const isValid1 = tinysecp.verifySchnorr(signatureHash, pub, sign);
    if (isValid1) { console.log('The test demo should produce a valid signature.') }

    transaction.version = 1
    console.log(transaction)

    // Broadcasting the transaction
    const txHex = transaction.toHex();
    console.log(`Broadcasting Transaction Hex: ${txHex}`);
    const broadcastResult = await broadcastraw(txHex);
    console.log(`Success! Broadcast result: ${broadcastResult}`);

    // Generate new block to confirm
    await pushBlock(p2pktr_addr);
}

It gets:

error: {
  code: -26,
  message: 'mandatory-script-verify-flag-failed (Invalid Schnorr signature)'
},

As for how I make musig and aggr key, I was using https://github.com/cmdruid/musig2

  • Are you absolutely sure that the transaction at utxos.txid ONLY has one output. It has NO OTHER OUTPUTS? Like no change outputs AT ALL?
  • You can not change the transaction version AFTER getting the signatureHash so you must remove transaction.version = 1 (although the default is 1, so this should not matter)
  • You should use SIGHASH_DEFAULT instead of SIGHASH_ALL and make sure that your schnorr signature is 64 bytes.

Please read the related BIP (341) to learn about what SIGHASH_DEFAULT is and how it affects the shape of the signature Buffer.

Are you absolutely sure that the transaction at utxos.txid ONLY has one output. It has NO OTHER OUTPUTS? Like no change outputs AT ALL?

There is two uxto from the pervious tx

image

however, If I include all two uxto in the prevOutScript and Value like:

    let prevOutScript: any[] = []
    let prevValue: any[] = []
    for (var i = 0; i < all_utxos.length; i++) {
        prevOutScript.push(bitcoin.address.toOutputScript(all_utxos[i].address, network));
        prevValue.push(all_utxos[i].value)
    }

    // const prevOutScript = ;
    // const prevOutScript2 = bitcoin.address.toOutputScript(p2pktr_addr, network);

    let signatureHash = transaction.hashForWitnessV1(0, prevOutScript, prevValue, Transaction.SIGHASH_DEFAULT);

It will return an error, named Error: Must supply prevout script and value for all inputs

I think it should only need one of the UTXO, cause the other change address is back to the faucet itself, like

faucet ---> tapaddress -- my new tx is here --> faucet
············· ---> change it back

and I check it, the sign is 64 bytes

image

You should use SIGHASH_DEFAULT instead of SIGHASH_ALL and make sure that your schnorr signature is 64 bytes.

I tried both, none of them are working

You can not change the transaction version AFTER getting the signatureHash so you must remove transaction.version = 1 (although the default is 1, so this should not matter)

Yeah, I deleted, it doesn't work

Hi, I success, and I found this problem is really tricky, it maybe a good lesson for other guys.

It was because

    // Wrong
    // Generate an address from the tweaked public key
    const p2pktr = payments.p2tr({
        pubkey: Buffer.from(pub),
        network
    });
    // Right
    // Generate an address from the tweaked public key
    const p2pktr = payments.p2tr({
        pubkey: Buffer.from(pub, 'hex'),
        network
    });

Yeah, there's no way I could have known that because you had so many black box functions...

Glad you figured it out.

It will return an error, named Error: Must supply prevout script and value for all inputs

I was mistaken. It isn't an array of all outputs of utxos.txid, but it is the outputs of the utxos that every input of THIS transaction points to. So if THIS transaction only has 1 input you only have 1 item, if this tx has 5 inputs, then every call of hashForWitnessV1 (regardless of the index) must have the same two arrays with 5 items each.