DEV Community

Oghenovo Usiwoma
Oghenovo Usiwoma

Posted on • Updated on

A Guide to creating TapRoot Scripts with bitcoinjs-lib

Taproot and Schnorr are upgrades to the Bitcoin protocol designed to enhance the privacy, efficiency and flexibility of Bitcoin transactions.

Taproot introduces Taptrees, a feature that reduces the size of transaction data and ensures only necessary information is revealed on the blockchain, thereby preserving privacy. With Taproot, multisig transactions are also more private as unused spending conditions are hidden.

Schnorr signatures are 64 bytes long instead of the 72 bytes used by current ECDSA signature scheme. Taproot also use only x-values of public keys for signature creation which saves 1 byte.

With the adoption of Taproot and Schnorr, Bitcoin transactions can be more efficient, flexible, and private.

We'll go over two examples. In our first example, we will create a pay-to-taproot(p2tr) address that will lock funds to a key and create a spend transaction for it. In our second example, we will jump straight into taproot script-spend transactions, we will create a Taptree consisting of two script-spend paths, a hash-lock script spend path and a pay-to-pubkey script spend path. We will create transactions that spend from both paths.

The full code for this article can be found on github at taproot-with-bitcoinjs.

Taproot Key-spend transaction

For illustration purposes, we'll use a random keypair

const keypair = ECPair.makeRandom({ network });
Enter fullscreen mode Exit fullscreen mode

We tweak this keypair with our pubkey.

const tweakedSigner = tweakSigner(keypair, { network });
Enter fullscreen mode Exit fullscreen mode

bitcoinjs-lib provides a p2tr function to generate p2tr outputs.

const p2pktr = payments.p2tr({
  pubkey: toXOnly(tweakedSigner.publicKey),
  network
});
const p2pktr_addr = p2pktr.address ?? "";
console.log(p2pktr_addr);
Enter fullscreen mode Exit fullscreen mode

The toXOnly function extracts the x-value of our public key.

You can use any testnet faucet that supports taproot addresses. I used testnet-faucet.com/btc-testnet while testing.

Creating a spend-transaction for this address with bitcoinjs-lib is straightforward.

const psbt = new Psbt({ network });
psbt.addInput({
        hash: utxos[0].txid,
        index: utxos[0].vout,
        witnessUtxo: { value: utxos[0].value, script: p2pktr.output! },
        tapInternalKey: toXOnly(keypair.publicKey)
});

psbt.addOutput({
        address: "mohjSavDdQYHRYXcS3uS6ttaHP8amyvX78", // faucet address
        value: utxos[0].value - 150
});

psbt.signInput(0, tweakedSigner);
psbt.finalizeAllInputs();
Enter fullscreen mode Exit fullscreen mode

Extract the transaction and broadcast the transaction hex.

const tx = psbt.extractTransaction();
console.log(`Broadcasting Transaction Hex: ${tx.toHex()}`);
const txid = await broadcast(tx.toHex());
console.log(`Success! Txid is ${txid}`);
Enter fullscreen mode Exit fullscreen mode

Taproot Script-spend transaction

We'll create a Tap tree with two spend paths, a hash-lock spend path and a pay-to-pubkey spend path.

The hash-lock script-spend path will require the spender to include a preimage that will produce the hash specified in the script.

Let's make another random keypair for our hash-lock script

const hash_lock_keypair = ECPair.makeRandom({ network });
Enter fullscreen mode Exit fullscreen mode

Now, we will construct our hash-lock script

const secret_bytes = Buffer.from('SECRET');
const hash = crypto.hash160(secret_bytes);
// Construct script to pay to hash_lock_keypair if the correct preimage/secret is provided
const hash_script_asm = `OP_HASH160 ${hash.toString('hex')} OP_EQUALVERIFY ${toXOnly(hash_lock_keypair.publicKey).toString('hex')} OP_CHECKSIG`;
const hash_lock_script = script.fromASM(hash_script_asm);
Enter fullscreen mode Exit fullscreen mode

Notice that script still requires a signature to unlock funds.

The pay-to-pubkey spend path is much simpler

const p2pk_script_asm = `${toXOnly(keypair.publicKey).toString('hex')} OP_CHECKSIG`;
const p2pk_script = script.fromASM(p2pk_script_asm);
Enter fullscreen mode Exit fullscreen mode

We can now create our Taptree and p2tr address.

const scriptTree: Taptree = [
        {
            output: hash_lock_script
        },
        {
            output: p2pk_script
        }
];
const script_p2tr = payments.p2tr({
        internalPubkey: toXOnly(keypair.publicKey),
        scriptTree,
        network
});
const script_addr = script_p2tr.address ?? '';
console.log(script_addr);
Enter fullscreen mode Exit fullscreen mode

You can deposit some test btc into the address using a testnet faucet like testnet-faucet.com/btc-testnet.

To spend on any of the leaf scripts, you must present the leafVersion, script and controlBlock for that leaf script. The control block is data required to prove that the leaf script exists in the script tree(merkle proof).

bitcoinjs-lib will generate the control block for us.

const hash_lock_redeem = {
    output: hash_lock_script,
    redeemVersion: 192,
};
const p2pk_redeem = {
    output: p2pk_script,
    redeemVersion: 192
}

const p2pk_p2tr = payments.p2tr({
    internalPubkey: toXOnly(keypair.publicKey),
    scriptTree,
    redeem: p2pk_redeem,
    network
});

const hash_lock_p2tr = payments.p2tr({
    internalPubkey: toXOnly(keypair.publicKey),
    scriptTree,
    redeem: hash_lock_redeem,
    network
});

console.log(`Waiting till UTXO is detected at this Address: ${script_addr}`);
let utxos = await waitUntilUTXO(script_addr)
console.log(`Trying the P2PK path with UTXO ${utxos[0].txid}:${utxos[0].vout}`);

const p2pk_psbt = new Psbt({ network });
p2pk_psbt.addInput({
    hash: utxos[0].txid,
    index: utxos[0].vout,
    witnessUtxo: { value: utxos[0].value, script: p2pk_p2tr.output! },
    tapLeafScript: [
        {
            leafVersion: p2pk_redeem.redeemVersion,
            script: p2pk_redeem.output,
            controlBlock: p2pk_p2tr.witness![p2pk_p2tr.witness!.length - 1] // extract control block from witness data
        }
    ]
});

p2pk_psbt.addOutput({
    address: "mohjSavDdQYHRYXcS3uS6ttaHP8amyvX78", // faucet address
    value: utxos[0].value - 150
});

p2pk_psbt.signInput(0, keypair);
p2pk_psbt.finalizeAllInputs();

let tx = p2pk_psbt.extractTransaction();
console.log(`Broadcasting Transaction Hex: ${tx.toHex()}`);
let txid = await broadcast(tx.toHex());
console.log(`Success! Txid is ${txid}`);
Enter fullscreen mode Exit fullscreen mode

To spend using the hash-lock leaf script, we have to create a custom finalizer function. In our custom finalizer, we will create our witness stack of signature, preimage, original hash-lock script and our control block

const tapLeafScript = {
    leafVersion: hash_lock_redeem.redeemVersion,
    script: hash_lock_redeem.output,
    controlBlock: hash_lock_p2tr.witness![hash_lock_p2tr.witness!.length - 1]
};

const psbt = new Psbt({ network });
psbt.addInput({
    hash: utxos[0].txid,
    index: utxos[0].vout,
    witnessUtxo: { value: utxos[0].value, script: hash_lock_p2tr.output! },
    tapLeafScript: [
        tapLeafScript
    ]
});

psbt.addOutput({
    address: "mohjSavDdQYHRYXcS3uS6ttaHP8amyvX78", // faucet address
    value: utxos[0].value - 150
});

psbt.signInput(0, hash_lock_keypair);

// We have to construct our witness script in a custom finalizer

const customFinalizer = (_inputIndex: number, input: any) => {
    const scriptSolution = [
        input.tapScriptSig[0].signature,
        secret_bytes
    ];
    const witness = scriptSolution
        .concat(tapLeafScript.script)
        .concat(tapLeafScript.controlBlock);

    return {
        finalScriptWitness: witnessStackToScriptWitness(witness)
    }
}

psbt.finalizeInput(0, customFinalizer);

tx = psbt.extractTransaction();
console.log(`Broadcasting Transaction Hex: ${tx.toHex()}`);
txid = await broadcast(tx.toHex());
console.log(`Success! Txid is ${txid}`);
Enter fullscreen mode Exit fullscreen mode

Conclusion

By reading this article, you should now have a better understanding of how to use bitcoinjs-lib to create and spend P2TR (Pay to Taproot) payments. With this knowledge, you are one step closer to leveraging the benefits of Taproot in your Bitcoin transactions, such as improved privacy, scalability, and the ability to create more complex smart contracts.
You can find more examples in bitcoinjs-lib's repo. The full code for this article can be found on github at taproot-with-bitcoinjs.
If you need some help understanding taproot, you can check this out More on Taproot.

Top comments (13)

Collapse
 
chironjitd profile image
CD

Hi, i'm trying to use the examples below to replicate an ordinals transfer, paid for by another wallet. May i ask how we would extend these examples for taproot transfer from one wallet to another but payment transfer done from one P2SH wallet to another?

Collapse
 
iyg profile image
y

Hello, do you use bitcoinjs-lib to implement taprot addresses for ord inscribe? I can transfer the inscription id now, but I don't know how to create a new inscription(use bitcoinjs-lib)

Collapse
 
chironjitd profile image
CD

Hey y. I have managed to do a transfer using both bitcoinjs-lib and micro-btc-signer. But I'm in the same boat where i'm not certain yet how to do an actual inscription.

Thread Thread
 
iyg profile image
y

Okay, if you have implemented inscribe, can you share some code examples? I am now using this test inscription: github.com/cmdruid/tapscript/blob/...

Thread Thread
 
chironjitd profile image
CD

Noted, will do. Also, thank you for sharing the example code

Thread Thread
 
cani1see profile image
cani1see

did you find how to do the inscribe now, using tapscript or bitcoinjs-lib?

Thread Thread
 
fboucquez profile image
fboucquez

HI @y, @chironjitd , any luck converting the tapscript ordinal example to bitcoinjs? Thanks in advance!

Collapse
 
eunovo profile image
Oghenovo Usiwoma • Edited

Not sure I understand you correctly. You want to transfer UTXOs from one P2SH address to another?

Collapse
 
eunovo profile image
Oghenovo Usiwoma • Edited

@cd if you are trying to work with P2SH, my article on P2WSH dev.to/eunovo/unlocking-the-power-... might help you

Thread Thread
 
chironjitd profile image
CD

Hi Oghenovo, thank you for your reply. I was trying to do an Ordinals inscription transfer, and needed to figure out how to do so.

I can now do a transfer but i feel that my base understanding of taproot is not quite there. Are you planning to write more articles on deep diving into taproot or have suggestions on reading sources?

Thread Thread
 
eunovo profile image
Oghenovo Usiwoma
Collapse
 
nikicat profile image
Nikolay Bryskin

Thanks!

Collapse
 
mariobtc profile image
Mario

Hello Oghenovo!
Thank you for your helpful article and code samples.

In line 30 of you code on github, you tweak the signer and in line 32 you create the address to where the faucet sends the tBTC.

But in the bitcoinjs-lib example starting from line 19, they use an un-tweaked public key for the address generation (line 46).

However, they use a tweaked key to sign the input (line 55).

How does that work? Or did I miss something?
Thank you.