Bitcoin Taproot Hello World
Ok, it’s been a while, but I’m back. And this time, I’m going all-in on Bitcoin tutorials.
In this tutorial, you will learn how to create, verify, and publish a transaction on the Bitcoin blockchain using bitcoinjs-lib on Node.js.
Development Environment Setup
- Download and install nvm (win, mac)
$ nvm install --lts$ nvm use --lts- Launch VSCode or your favorite IDE and enable JavaScript extensions for code completion.
- Open a terminal and create a directory called
btc-helloworld $ cd btc-helloworld$ npm init$ npm install bitcoinjs-lib@7.0.0-rc.0 tiny-secp256k1 ecpair bip32 bip39
At the time of writing, it installed the following library versions:
bip32 5.0.0-rc.0
bip39 3.1.0
bitcoinjs-lib 7.0.0-rc.0
ecpair 3.0.0
tiny-secp256k1 2.2.3Generate Bitcoin Wallet Addresses
First, let’s create a file called generate_address.js to be able to create bitcoin addresses, don’t use this for production.
Use for development and testing on test nets ONLY:
// A javascript Bitcoin library for node.js and browsers.
const bitcoin = require('bitcoinjs-lib');
// JavaScript implementation of Bitcoin BIP39: Mnemonic code for generating deterministic keys
const bip39 = require('bip39');
// Hierarchical Deterministic Wallets (Bip32) Implementation in JavaScript
const { BIP32Factory } = require('bip32');
// Secp256k1 is the name of the elliptic curve used by Bitcoin to implement its public key cryptography
const ecc = require('tiny-secp256k1');
// Initialize libraries
bitcoin.initEccLib(ecc);
const BIP32 = BIP32Factory(ecc);Insert the lines below to configure for testnet
const network = bitcoin.networks.testnet;
let path = '';
// m / purpose' / coin_type' / account' / change / address_index
if (network == bitcoin.networks.testnet) {
// test net
path = "m/86'/1'/0'/0/0";
} else {
// main net
path = "m/86'/0'/0'/0/0";
}Now the below lines to create a random mnemonic, and get the child to generate the p2tr address
const mnemonic = bip39.generateMnemonic();
const seed = bip39.mnemonicToSeedSync(mnemonic);
const root = BIP32.fromSeed(seed, network);
const child = root.derivePath(path);Get the X-Coordinate alone to create the taproot output script p2tr:
// Removes the first byte (the 0x02 or 0x03 prefix) from the 33-byte compressed public key
// leaving the 32-byte X-coordinate to construct the Taproot output script (P2TR address).
// Schnorr signatures and Taproot scripts operate on the X-coordinate alone, assuming the Y-coordinate has even parity (a convention that simplifies processing and reduces size).
// By dropping the prefix byte, the public key becomes 32 bytes, which is used as the internal public key for Taproot.
internalPubkey = Buffer.from(child.publicKey.slice(1, 33));
const p2tr = bitcoin.payments.p2tr({
internalPubkey: internalPubkey,
network: network
});Last, print the mnemonic and p2tr:
console.log('Mnemonic:', mnemonic);
console.log('Taproot Address:', p2tr.address);Node run the following command from terminal to generate your very first taproot address for testing purposes only:
$ node generate_address.jsOutput, example:
Mnemonic: diagram average divide urban office bench ready dirt popular point robust lawn
Taproot Address: tb1pzt53e6nghqw40zglfzn56cdj82xx68nsd84z3yn5pl6xxj2v3n9qrsheehFund the p2tr address
Go to https://mempool.space/testnet4/faucet to fund your newly created taproot address
Click on the transaction link with the transaction that funds your address and it should look something like this:
From the transaction details page, copy the Transaction ID and output index with your p2tr address. (you can get the index by hovering over the flow image chart like shown below):
Create the Transaction
Create a file called btc_transaction.js, add the following code, very similar to the generate_address file above but I add the ecpair library to be able to sign the transaction for broadcasting it in the bitcoin network and also added the change derived path to send our change bitcoin after our transaction:
// A javascript Bitcoin library for node.js and browsers.
const bitcoin = require('bitcoinjs-lib');
// JavaScript implementation of Bitcoin BIP39: Mnemonic code for generating deterministic keys
const bip39 = require('bip39');
// Hierarchical Deterministic Wallets (Bip32) Implementation in JavaScript
const { BIP32Factory } = require('bip32');
// Secp256k1 is the name of the elliptic curve used by Bitcoin to implement its public key cryptography
const ecc = require('tiny-secp256k1');
// A library for managing SECP256k1 keypairs
const { ECPairFactory } = require('ecpair');
// Initialize libraries
bitcoin.initEccLib(ecc);
const bip32 = BIP32Factory(ecc);
const ECPair = ECPairFactory(ecc);
const network = bitcoin.networks.testnet;
let path = '';
let path_change = '';
// m / purpose' / coin_type' / account' / change / address_index
if (network == bitcoin.networks.testnet) {
// test net
path = "m/86'/1'/0'/0/0";
path_change = "m/86'/1'/0'/1/0";
} else {
// main net
path = "m/86'/0'/0'/0/0";
path_change = "m/86'/0'/0'/1/0";
}Paste your mnemonic:
// Wallet Mnemonic
const mnemonic = 'diagram average divide urban office bench ready dirt popular point robust lawn';Now paste the details from the transaction you copied from the transaction details page above
// Funding Transaction Details
const fundingTxId = '1862e5293c83e138a5768de66a4dc1dc529cbd8189236a88f2923b1249b63601';
const fundingIndex = 1;
const fundingAmount = BigInt(5000);Now enter the new output transaction details:
// New Transaction Output Details
const message = 'Hello World';
const fee = 150n;Generate the seed, keypair and p2tr from the mnemonic
// Generate seed from mnemonic
const seed = bip39.mnemonicToSeedSync(mnemonic);
const root = bip32.fromSeed(seed, network);
const taprootChild = root.derivePath(path);
// Generate the key pair to verify and sign the input we're going to spend from the transaction
const taprootKeyPair = ECPair.fromPrivateKey(taprootChild.privateKey, { network: network });
// Internal public key (x-only, 32 bytes)
const internalPubkey = taprootKeyPair.publicKey.slice(1, 33);
// Generate the p2tr to get the output for the spending input
const p2tr = bitcoin.payments.p2tr({ internalPubkey: internalPubkey, network });Now create the change p2tr so that we receive the change after paying for fees for our transaction, for simplicity I’m using the same mnemonic to generate change addresses:
// Create the change p2tr to get the address where we're going to receive the change
const changeChild = root.derivePath(path_change);
const changeInternalPubkey = changeChild.publicKey.slice(1, 33);
const changep2tr = bitcoin.payments.p2tr({ internalPubkey: changeInternalPubkey, network });Create the transaction using psbt and add the transaction input details we want to spend
// Create PSBT
const psbt = new bitcoin.Psbt({ network });
// Add Taproot input
psbt.addInput({
hash: fundingTxId,
index: fundingIndex,
witnessUtxo: {
script: Uint8Array.from(p2tr.output),
value: inputAmount,
},
tapInternalKey: internalPubkey,
sequence: 0xfffffffd // Enable RBF by setting sequence < 0xffffffff
});hash: funding ID, you copied this from the transaction details page when you funded your addresses using the faucet
index: output index you’re trying to spend, in this case, the one that received the bitcoin from the faucet
witnessUtxo: contains the script and the value, these must match exacly as in the transaction details page
tapInternalKey: the internal x-only Pubkey
sequence: Enables RBF by setting sequence < 0xffffffff
Add the first output, in our case this is where we’re going to print Hello World using OP_RETURN as shown below, we don’t specify a value here because we’re going to add the change to the other output:
// Add OP_RETURN output
const data = Buffer.from(message, 'utf8');
const opReturnScript = bitcoin.script.compile([bitcoin.opcodes.OP_RETURN, data]);
psbt.addOutput({
script: Uint8Array.from(opReturnScript), // Convert Buffer to Uint8Array
value: 0, // OP_RETURN outputs should have a value of 0
});Add the change output, the change value is the amount we’re taking from the input we’re spending minus the fee we want ot pay miners, for testnet I’ve been using 150 as fee and it has been working well:
// Add change output
const changeAmount = fundingAmount - fee;
if (changeAmount < 0n) {
throw new Error("Input amount is less than the fee");
}
psbt.addOutput({
address: changep2tr.address,
value: changeAmount,
});Sign and finalize
// Used for signing, since the output and address are using a tweaked key
// We must tweak the signer in the same way.
const tweakedChildNode = taprootKeyPair.tweak(
Buffer.from(bitcoin.crypto.taggedHash('TapTweak', internalPubkey))
);
psbt.signInput(0, tweakedChildNode);
psbt.finalizeAllInputs();
const tx = psbt.extractTransaction();
console.log('Transaction:', tx.toHex());Run it!
node btc_transaction.jsOutput example:
Transaction: 020000000001010136b649123b92f2886a238981bd9c52dcc14d6ae68d76a538e1833c29e562180100000000fdffffff0200000000000000000d6a0b48656c6c6f20576f726c64f21200000000000022512021833c7c66c2a2a503f5502275926d2b07cdabc5fe5b2a38c3001fe2209abb26014091b3884099860ea37a104359bfa4a06a95bacf8333f6a0377abfb530b92365d48e1908d8089158faf0dda6606e69eb3e4bea1cc46beb7dd430ace846ac0cd03b00000000Test the transaction on https://mempool.space/testnet4/tx/test and it should show a green checkmark if it passes the test as shown below
Broadcast the transaction using https://mempool.space/testnet4/tx/push as shown below; If successful it’s going to show a link to the broadcasted transaction.
Open the transaction details page and you should see the “Hello World” test in one of the output:
Congratulations, you have broadcasted your first bitcoin transaction on testnet4 🚀
Here’s the repository with the full source code:
Ideas:
- Modify the code to use an API to test, and publish the transaction directly from javascript
