bitcoinjs / bitcoinjs-lib

A javascript Bitcoin library for node.js and browsers.
MIT License
5.7k stars 2.11k forks source link

taproot example #1871

Closed dannydeezy closed 1 year ago

dannydeezy commented 1 year ago

is there a simpler example than the existing "Taproot Key Spend" example that doesn't involve tweaking? i'm trying to figure out how to spend from an address that was created like this:

 bitcoin.payments.p2tr({ pubkey: somePubkey })

i worry that i messed up because i used pubkey instead of internalPubkey... hoping for some help here :)

i have the corresponding private key for this pubkey, i'm just not sure how to setup the signing

tylerlevine commented 1 year ago

I was able to spend from an address created using pubkey instead of internalPubkey using the code below. @junderw left a bit of a hint here which was really useful.

import * as ecpair from 'ecpair';
import * as ecc from 'tiny-secp256k1';
import * as bitcoin from 'bitcoinjs-lib';
import {serializeTaprootSignature} from "bitcoinjs-lib/src/psbt/bip371.js";

bitcoin.initEccLib(ecc);
const ECPair = ecpair.ECPairFactory(ecc);
const network = bitcoin.networks.testnet;
const faucetReturnAddress = 'mohjSavDdQYHRYXcS3uS6ttaHP8amyvX78';

function toXOnly(key) {
    return key.length === 33 ? key.slice(1, 33) : key;
}

function createTrAddress(wif) {
    const keypair = wif ? ECPair.fromWIF(wif, network) : ECPair.makeRandom({ network });
    const trAddr = bitcoin.payments.p2tr({ pubkey: toXOnly(keypair.publicKey), network });
    return { address: trAddr, keypair };
}

function getUtxoInfo() {
    return {
        hash: 'e628f70763d63b1dcb11c50bb92937fb340db00792bcd22a31e870125f17bf63',
        index: 0,
        value: 5288,
    };
}

async function createSpendingTx(utxoInfo, address, keypair) {
    const psbt = new bitcoin.Psbt({ network });
    const { hash, index, value } = utxoInfo;
    const inputParams = {
        hash,
        index,
        witnessUtxo: { value, script: address.output },
        tapInternalKey: toXOnly(keypair.publicKey),
    };
    psbt.addInput(inputParams);
    psbt.addOutput({
        value: getUtxoInfo().value - 1000,
        address: faucetReturnAddress,
    });

    // don't use the regular signing code - this assumes a tweak needs to be applied but that's not the case here
    // const tweakedSigner = tweakSigner(keypair, { network });
    // await psbt.signInputAsync(0, tweakedSigner);
    psbt.updateInput(0, {
        tapKeySig: serializeTaprootSignature(
            keypair.signSchnorr(
                // there's probably a better way to get at the tx underlying the psbt, but this seems to work ok
                psbt.__CACHE.__TX.hashForWitnessV1(0, [address.output], [value], bitcoin.Transaction.SIGHASH_DEFAULT)
            )
        )
    });
    psbt.finalizeAllInputs();
    return psbt.extractTransaction();
}

async function main() {
    // set to a pre-existing WIF if you have one. If not, a new keypair will be created
    const wif = undefined;
    const { address, keypair } = createTrAddress(wif);

    const tx = await createSpendingTx(getUtxoInfo(), address, keypair);
    console.log('tx:', tx.toBuffer().toString('hex'));
}

main().catch((e) => console.error(e))

Here's the resulting transaction on testnet: https://blockstream.info/testnet/tx/f3fca3e5452897f66764652d086c2b0561324407561823b12cde7cf7f7aff885

dannydeezy commented 1 year ago

legend

junderw commented 1 year ago

Just be careful. Apparently the BIP32 HD wallet spec for simple single-sig spends requires tweaking... so if you don't use tweaking, you might create a wallet that's outside of spec.

Not sure what your actual use case is tho.

https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki#user-content-Address_derivation

The address derivation section on BIP86 goes into more detail. It references BIP341

It is useful to note that the reason why this is recommended is to allow for people to prove that they aren't hiding a script path. I can show you that I have the internal pubkey, then show you that internal pubkey tweaked with it's hash is the output, and you know I can not possibly have a script path in my taproot output.

junderw commented 1 year ago

Note from our DMs:

There is nothing inherently insecure about using raw pubkeys as the taproot output key.

However, later on, if someone says "I want you to prove to me that you don't have any script path on that output" you can not prove it. This is less important for single sig and more important for multisig schnorr signature aggregation... I think this is just being pushed as a default so that later on when wallets start adding these features all wallets generated up to that point can properly prove NUMS.

If you use the tweak method you can prove "nothing up my sleeve" (NUMS)

But if you want to support BIP86 style HD wallets, you must use tweaking. The BIP requires it.

junderw commented 1 year ago

I've hidden the reply with a workaround just to discourage its use and in hopes that people read my comments first.

I think there's value in having it up, but not immediately available without a click and hopefully a read of my replies first.