OP_1
OP_PUSHBYTES_32
562529047f476b9a833a5a780a75845ec32980330d76d1ac9f351dc76bce5d72
Taproot
Technical explanation
Taproot was a major upgrade to Bitcoin activated in 2021.
The primary upgrade was the introduction of the Pay To Taproot (P2TR) locking script, which allows you to unlock an output using a choice of custom scripts.
There were also a couple of extra changes:
- Schnorr Signatures — The new P2TR locking script uses the more efficient Schnorr signature scheme instead of ECDSA.
- Tapscript — The custom locking scripts inside P2TR use a slightly modified version of Script called Tapscript.
So the taproot upgrade just introduced a new and useful locking mechanism for outputs, which use Schnorr Signatures and Tapscript. This was a fairly large technical upgrade overall, which makes it the biggest major upgrade to Bitcoin since Segregated Witness (2017).
This page focuses on how to construct and spend the new P2TR locking script.
Basics
How does Taproot work?
A P2TR locking script can be unlocked in one of two ways:
- Key Path Spend - This is where you provide a signature for the public key used to create the locking script. This works in a similar way to a simple P2WPKH.
- Script Path Spend - This is where you use one of multiple possible custom scripts to unlock the output. This is like using P2WSH, but you have multiple custom scripts to choose from.
The interesting part of P2TR is that all of the custom scripts are organized in to a tree. This script tree is used to create a merkle root, which is a fingerprint for all the different ways the output can be unlocked. This fingerprint (tweak) is then combined with the public key to create a tweaked public key.
This tweaked public key is what gets placed inside the locking script.
You can then unlock this locking script in one of two ways:
- Via the public key by tweaking its private key to create a signature for the tweaked public key. This is a key path spend.
- Using one of the custom scripts in the tree by providing a merkle path to prove that your chosen script was used to create the merkle root that was used to create the tweaked public key. This is a script path spend.
In short, both of these spending options are embedded in to a single tweaked public key (called the taproot).
The beauty of the P2TR locking mechanism is that the tweaked public key can be unlocked with a single signature. This signature is created by first tweaking the original private key by the same value as the tweak used on the original public key. This works thanks to the mathematics of elliptic curves.
The official name for the tweaked public key is the taproot. But to make the upcoming explanations clearer I usually just refer to it as the tweaked public key from here on out.
Benefits
What are the benefits of Taproot?
The design of the Pay To Taproot (P2TR) locking script provides a few benefits.
In short, it provides the flexibility to place a variety of different locking conditions on an output in a way that is both private and efficient.
1. Flexibility
The P2TR locking scripts allows you to place multiple different spending conditions (custom scripts) on a single output.
You can think of the key path spend as the "default" method, and the script path spend as alternatives in the event the default method is not an option:
2. Privacy
Thanks to the merkle tree structure used for the custom scripts, only a single script is actually revealed when you unlock a P2TR using the script path spend.
So you could construct a script tree with hundreds of different custom locking scripts, but only the custom script you use will be revealed when you unlock it (the rest will only appear as hashes of the original scripts).
This means all the other possible spending conditions of a P2TR are kept private.
If you spend a P2TR using the key path spend, none of the custom scripts in the tree are revealed. So anyone looking at a P2TR spent in this way wouldn't know if there were any additional spending conditions.
3. Efficiency
You'd think that including multiple different spending conditions on an output would create a large and expensive locking script.
However, thanks again to the merkle tree structure used for all the different custom scripts, all of the spending conditions are reduced down to a single 32-byte fingerprint (merkle root). You then only need to reveal a small portion of this tree (merkle path) when you come to unlock the output using your chosen script.
Furthermore, thanks to the mathematics of elliptic curve cryptography (i.e. point addition), we can merge this merkle root with a public key to create a tweaked public key. This tweaked public key allows the output to be spent using either a signature, or by using one of custom scripts that made up the merkle root.
As a result, all of the possible spending conditions can be embedded within a single 32-byte "tweaked public key" (aka the taproot), which is more efficient than placing multiple raw custom scripts directly inside the locking script.
A P2TR locking script is only 34 bytes in length, which is exactly the same size as a P2WSH (which contains one possible spending condition only).
If you use a large number of custom scripts in your script tree, the amount of data required for unlocking a P2TR will be larger due to the need to provide a longer merkle path to prove that your script was in the tree. But this tree structure means the amount of data you need to provide is relatively small compared to the size of the tree.
4. Upgradability
The P2TR locking mechanism in Bitcoin has been designed to allow for easy upgrades in the future.
This is why you'll see a few seemingly redundant fields (e.g. leaf version, annex, extension flag, sighash epoch) included as part of spending a P2TR. These additional fields may make the spending of a P2TR seem more complicated at first, but they're all there for a reason.
Construction
How do you create a P2TR?
Constructing a P2TR output is fairly straightforward. You just need to create a ScriptPubKey with the following pattern:
OP_1
OP_PUSHBYTES_32
<32-byte tweaked public key>The tweaked public key is created by combining a public key with the merkle root from a tree of custom scripts.
The most complex part is organizing all the custom scripts in to a tree structure and calculating the merkle root from it. However, this script tree can be as simple or as complex as you want to make it (or you could just not use a script tree at all).
Below is an overview of the steps involved in creating a P2TR locking script.
See the examples for a technical step-by-step guide with actual data.
1. Public Key
aka Internal Public Key, Taproot Internal Key
The first step is to generate a public key.
This public key is used for the key path spend, which will be the "default" method for unlocking the P2TR output. It's generated in the same way as you'd generate any private key/public key pair used in Bitcoin.
However, P2TR uses 32-byte public keys as opposed to 33-byte compressed public keys. This is because P2TR locking scripts use Schnorr signatures, and public keys in this signature scheme contain the 32-byte x-coordinate only.
Example
public key = a2fc329a085d8cfc4fa28795993d7b666cee024e94c40115141b8e9be4a29fa4
You can convert a standard 33-byte compressed public key to a 32-byte public key for P2TR by simply removing the first byte.
A public key is required to construct a P2TR locking script.
2. Script Tree
The script tree contains all the custom scripts you'd like to be able to use for unlocking the P2TR output (in addition to the public key spend option above).
You can use as many custom scripts as you want*, and you can organize the tree in any way you like.
*You're actually limited to 2128 different scripts. This is due to the maximum size of the merkle path in the control block, but this is a huge number.
The script tree and its merkle root is constructed in 3 steps:
A script tree is not required to construct a P2TR locking script.
1. Leaves
The leaves of the tree are made from the custom scripts you'd like to be able to use to unlock the P2TR output.
Each leaf contains two pieces of data:
-
Version — This indicates the version of the upcoming custom script. This is used to allow for changes to the script language in future upgrades.
192 (0xc0) = tapscript
(current default)
- Script — This is your custom locking script (i.e. tapscript). It can contain any kind of script you like (the same as you might use in a P2WSH for example).
Example
leaf version = c0
leaf script = 5187
- You do not have to construct any leaves for your script tree. In this situation you will have no script tree at all, and the P2TR output will be a key path spend only.
- Leaf scripts currently use version number
192
(0xc0
), which indicates the upcoming custom script is using tapscript (which is a minor variation of standard Script).
Why is the leaf version 192 (0xc0
)?
0xc0
)?The leaf version is encoded within the first byte of the control block, so it's ideal if this byte does not conflict with the first byte of other elements in the witness field for a script path spend.
There are a few restrictions on the choice of possible leaf versions:
- It cannot be odd. This is because the leaf version is encoded in the control byte, and the lowest bit of the control byte is used to store the parity of the tweaked public key. So seeing as the lowest bit is reserved, the leaf version can only be an even number.
- It cannot be 80 (0x50). This is because 0x50 is used as the first byte of the annex to distinguish it from other elements in the witness. So if you had a leaf version of 0x50, you could end up confusing the control block with the annex.
- It should avoid being confused as the first byte of a valid script. To help distinguish the control block element from the script element in the witness, it's ideal if the resulting control byte is a value that corresponds to an unused or invalid opcode.
This leaves the following bytes for use as distinctive leaf versions:
0xc0 to 0xfe
0x66
,0x7e
,0x80
,0x84
,0x96
,0x98
,0xba
,0xbc
,0xbe
And seeing as 0xc0
is at the start of a range of non-conflicting bytes that we can choose from, the leaf version has been chosen to start at 0xc0
(192).
So the leaf version could have started at 1, which would be logical, but starting at 192 helps to prevent the control block from being confused with other elements in the witness for P2TR spends (whilst also allowing for successive leaf version numbers).
2. Organization
The next step is to organize your leaves in to a tree.
If you have a small number of leaves (1 or 2), the tree structure will be straightforward. However, if you have lots of leaves (3+) you have more flexibility in how you organize the leaves of the tree.
The only consideration in the organization of your leaves is that you want the script that is most likely to be executed closest to the root of the tree. This is because you will save on fees when you come to unlock this script later on, as the merkle path you need to provide will be shorter.
Don't worry too much about the organization of your leaves if you're unsure. It just gives you the opportunity to save a small amount in fees if you know that some leaf scripts are more likely to be executed than others.
3. Merkle Root
The final step is to hash all of the leaves in the script tree to produce a single merkle root.
This merkle root is calculated in 3 steps:
1. Leaf Hash
Start by creating a tagged hash for each of the leaves.
- Tag = "TapLeaf"
- Data =
leaf version
+size(leaf script)
+leaf script
The leaf version
and leaf script
must be in bytes. So convert the leaf script from ASM to hexadecimal bytes first.
Also, don't forget to include the size(leaf script)
, which is a compact size field indicating the size of the leaf script (in bytes).
Example
leaf version = c0
size(leaf script) = 02
leaf script = 5187
data = leaf version + size(leaf script) + leaf script
data = c0025187
leaf hash = tagged_hash("TapLeaf", data)
leaf hash = 6b13becdaf0eee497e2f304adcfa1c0c9e84561c9989b7f2b5fc39f5f90a60f6
The size(script) + script
format is referred to as a "serialized script". This format (i.e. prepending the script with a compact size field) is also used when hashing scriptpubkeys within the signature algorithm. This is easy to forget and is likely to trip you up the first time around.
2. Branch Hash
After calculating each leaf hash, you then calculate the subsequent branches for your tree.
Each branch hash is calculating by creating a tagged hash of previous leaf/branch hash pairs:
- Tag = "TapBranch"
- Data =
lower leaf/branch hash
+higher leaf/branch hash
Depending on where you are at in the tree, the upcoming branch will be calculated using previous leaf hashes or branch hashes (or a combination of both).
It's important to note that the lower hash goes first. This is so the merkle root can be recalculated later on without having to provide information about the order of the hashes.
Example
┌────────┐ ┌────────┐
│ leaf 1 │ │ leaf 2 │
└───┬────┘ └───┬────┘
└────┬─────┘
┌───┴────┐ ┌────────┐
│branch 1│ │ leaf 3 │
└───┬────┘ └─────┬──┘
└─────┬──────┘
┌───┴────┐ ┌────────┐
│branch 2│ │ leaf 4 │
└───┬────┘ └────┬───┘
└────┬──────┘
┌───┴────┐ ┌────────┐
│branch 3│ │ leaf 5 │
└───┬────┘ └────┬───┘
└─────┬─────┘
┌───┴────┐
│branch 4│
└────────┘
leaf 1 version = c0
size(leaf 1 script) = 02
leaf 1 script = 5187
leaf 1 hash = 6b13becdaf0eee497e2f304adcfa1c0c9e84561c9989b7f2b5fc39f5f90a60f6
leaf 2 version = c0
size(leaf 2 script) = 02
leaf 2 script = 5287
leaf 2 hash = ed5af8352e2a54cce8d3ea326beb7907efa850bdfe3711cef9060c7bb5bcf59e
leaf 3 version = c0
size(leaf 3 script) = 02
leaf 3 script = 5387
leaf 3 hash = 160bd30406f8d5333be044e6d2d14624470495da8a3f91242ce338599b233931
leaf 4 version = c0
size(leaf 4 script) = 02
leaf 4 script = 5487
leaf 4 hash = bf2c4bf1ca72f7b8538e9df9bdfd3ba4c305ad11587f12bbfafa00d58ad6051d
leaf 5 version = c0
size(leaf 5 script) = 02
leaf 5 script = 5587
leaf 5 hash = 54962df196af2827a86f4bde3cf7d7c1a9dcb6e17f660badefbc892309bb145f
branch 1 (leaf 1 hash + leaf 2 hash) = 1324300a84045033ec539f60c70d582c48b9acf04150da091694d83171b44ec9
branch 2 (branch 1 + leaf 3 hash) = beec0122bddd26f642140bcd922e0264ce1e2be5808a41ae58d82e829bc913d7
branch 3 (branch 2 + leaf 4 hash) = a4e0d9cc12ce2f32069e98247581d5eb9ca0a4cf175771a8df2c53a93dcb0ebd
branch 4 (leaf 5 hash + branch 3) = b5b72eea07b3e338962944a752a98772bbe1f1b6550e6fb6ab8c6e6adb152e7c
NOTE: For branch 4, the leaf 5 hash goes first because it's lower than the branch 3 hash.
3. Root Hash
Keep hashing of the branches recursively until you end up with a single root hash.
This is the merkle root for your script tree.
Example
┌────────┐ ┌────────┐
│ leaf 1 │ │ leaf 2 │
└───┬────┘ └───┬────┘
└────┬─────┘
┌───┴────┐ ┌────────┐
│branch 1│ │ leaf 3 │
└───┬────┘ └─────┬──┘
└─────┬──────┘
┌───┴────┐ ┌────────┐
│branch 2│ │ leaf 4 │
└───┬────┘ └────┬───┘
└────┬──────┘
┌───┴────┐ ┌────────┐
│branch 3│ │ leaf 5 │
└───┬────┘ └────┬───┘
└─────┬─────┘
┌───┴────┐
│branch 4│ <- root hash (aka merkle root)
└────────┘
branch 1 (leaf 1 + leaf 2) = 1324300a84045033ec539f60c70d582c48b9acf04150da091694d83171b44ec9
branch 2 (branch 1 + leaf 3) = beec0122bddd26f642140bcd922e0264ce1e2be5808a41ae58d82e829bc913d7
branch 3 (branch 2 + leaf 4) = a4e0d9cc12ce2f32069e98247581d5eb9ca0a4cf175771a8df2c53a93dcb0ebd
branch 4 (leaf 5 + branch 3) = b5b72eea07b3e338962944a752a98772bbe1f1b6550e6fb6ab8c6e6adb152e7c
merkle root = branch 4
merkle root = b5b72eea07b3e338962944a752a98772bbe1f1b6550e6fb6ab8c6e6adb152e7c
- If you only have one leaf in your script tree, the merkle root will be that leaf hash.
- If you do not use a script tree, your merkle will be empty (zero bytes).
3. Taproot
aka tweaked public key, Taproot Output Key
After you've selected your public key and calculated the merkle root for your script tree, you now combine them to create a new tweaked public key (taproot) to place in the final P2TR locking script.
This tweaked public key is calculated in 3 steps:
1. Tweak
The first step is to calculate the tweak that will be used to tweak the public key.
This tweak is a tagged hash of the public key and the merkle root (from the previous step).
- Tag = "TapTweak"
- Data =
public key
+merkle root
Example
public key = a2fc329a085d8cfc4fa28795993d7b666cee024e94c40115141b8e9be4a29fa4
merkle root = b5b72eea07b3e338962944a752a98772bbe1f1b6550e6fb6ab8c6e6adb152e7c
tweak = tagged_hash("TapTweak", public key + merkle root)
tweak = bf0094eae70ba67e2f9fc3c4b81f078c90931855a8d24c959619174c92060cde
If you are not using a script tree, the data will just be the public key
.
2. Tweaked Public Key
Next, we use this tweak to calculate a tweaked public key.
This is calculated by adding the public key point to a new point calculated from the tweak:
- Multiply the generator point by the tweak to create a new "tweak point".
- Add this "tweak point" to the original public key point to create the tweaked public key.
Example
tweak = 0xbf0094eae70ba67e2f9fc3c4b81f078c90931855a8d24c959619174c92060cde (hexadecimal)
tweak = 0d86392781870822657352762415054053103831554399693054914473067050015239558270174 (decimal)
tweak point = generator point * tweak
tweak point = {
x: 95269675158353859168464540318226133424394895005665805192400764661565929057235
y: 29242344601443126469758111771751638455084106410674355391051939918836006591115
}
public key = a2fc329a085d8cfc4fa28795993d7b666cee024e94c40115141b8e9be4a29fa4
public key point = {
x: 73720276170843416906336055756797472465980506800013345098392580296822094995364
y: 11058485031460388084262061562798396269554476902102637685035851565088085936250
}
tweaked public key = tweak point + public key
tweaked public key = {
x: 38964561412401756362079620665362329679043962000991028886256303611203090799986
y: 63284987156862771696287631704887998797781193745849716996216065406275168637129
}
tweaked public key = 562529047f476b9a833a5a780a75845ec32980330d76d1ac9f351dc76bce5d72 (x coordinate in hexadecimal)
This tweaked public key will be a completely different point on the elliptic curve compared to the original public key point.
Thanks to the mathematics of elliptic curves, if we tweak the private key used to create the public key by the same value, it will actually correspond to this tweaked public key. This means we can use the tweaked private key to create a signature for the tweaked public key.
4. ScriptPubKey
Finally, we place this tweaked public key in to a locking script to create a P2TR output.
A P2TR ScriptPubKey has the following pattern:
OP_1
OP_PUSHBYTES_32
<32-byte tweaked public key>So a P2TR locking script is just a data push of the tweaked public key, prepended with an OP_1
opcode to indicate that it's a P2TR.
Example
5120562529047f476b9a833a5a780a75845ec32980330d76d1ac9f351dc76bce5d72
Transaction: 0c27dceb3c5e8aeffaad5c01229eac985b4a3d56388fcef07daecb1b5482495e (Output 0) (Used for key path spend)
Transaction: 88047644c7e42421861b5d15551aa29151f86d81409fd9a3831f43a541505720 (Output 1) (Used for script path spend)
- A P2TR ScriptPubKey is always 34 bytes in length.
- P2TR uses the
OP_1
version opcode, whereas P2WPKH and P2WSH use theOP_0
version opcode. - This ScriptPubKey pattern is similar to P2WPKH and P2WSH locking scripts introduced in the Segregated Witness upgrade.
Address
A P2TR ScriptPubKey can be converted to an address by converting it to Bech32m.
Example
scriptpubkey (hex) = 5120562529047f476b9a833a5a780a75845ec32980330d76d1ac9f351dc76bce5d72
address = bc1p2cjjjprlga4e4qe6tfuq5avytmpjnqpnp4mdrtylx5wuw67wt4eqg9jscq
A P2TR address is always 62 characters long and starts with bc1p (mainnet). This is due to the fact the ScriptPubKey is always 34 bytes in length and starts with the byte 0x51
(OP_1
).
Spending
How do you spend a P2TR?
There are two methods for spending a P2TR output:
- Key Path Spend — This is where you spend via the public key.
- Script Path Spend — This is where you spend using one of the leaf scripts in the script tree.
The unlocking code for both of these spending methods is placed inside the witness. The structure of the witness field varies depending on whether you use the key path spend or script path spend.
1. Key Path Spend
A P2TR key path spend is where you unlock the output via the default public key.
This spend is fairly straightforward, as you only need to provide a signature that corresponds to the tweaked public key in the ScriptPubKey.
A key path spend is performed in 3 steps:
1. Tweaked Private Key
To be able to create a signature that corresponds to this tweaked public key, you first need to tweak the original private key that was used to create the original public key.
The tweaked private key is calculated by simply adding the tweak to the original private key.
Example
private key = ce1fc7baa9db31c4ef9c6564f70d551f41fc479bb23fa844d50848220edaaf91
NOTE: If the private key produces a public key with an odd y coordinate, it needs to be negated so that it produces the same public key but with an even y coordinate
n = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
private key negated = n - private key
private key negated = 31e038455624ce3b10639a9b08f2aadf78b2954afd08f7f6eaca166ac15b91b0
tweak = bf0094eae70ba67e2f9fc3c4b81f078c90931855a8d24c959619174c92060cde
tweaked private key = (private key negated + tweak) % n
tweaked private key = f0e0cd303d3074b940035e5fc111b26c0945ada0a5db448c80e32db753619e8e
- If the original private key produces a public key with an odd y-coordinate, you need to negate the private key first so that it produces the equivalent public key but with an even y-coordinate instead. This is done by subtracting the private key from the number of points on the curve (n). The reason for this is that the public keys in the Schnorr signature scheme in Bitcoin use even y-coordinates only.
- The addition of the private key and the tweak is performed modulo the number of points on the curve (n). This ensures that the resulting tweaked private key remains within the range of valid private keys.
Thanks to the mathematics of elliptic curves, this tweaked private key will actually correspond to the tweaked public key.
Don't lose the script tree side of the P2TR. If you've included a script tree when constructing the P2TR lock, you will need the original script tree (or at least the merkle root) to be able to calculate the tweaked private key.
2. Signature Hash
To create a signature from this tweaked private key, you first need to construct a message to be signed.
This message is called the signature hash, which is a hash of the transaction data constructed in a specific way.
See the taproot signature algorithm for details.
Example
transaction (unsigned) = 020000000001015e4982541bcbae7df0ce8f38563d4a5b98ac9e22015cadfaef8a5e3cebdc270c0000000000ffffffff014c1d0000000000001600142cfec11b4e6d86607fc072804f0c7cc5dad213d00000000000
hash type byte = 01
annex present = 0
extension flag = 0
version = 02000000
locktime = 00000000
hash prevouts = fcb2a500a85bcc659833d70ab67aca05e96403a334455c7c17a9f8e88fc56c43
hash amounts = 8e965763e6a4bbc1088a94bf6c9cb3cbbdb4955f88355c807362a0fd43de4e3a
hash scriptpubkeys = 84fbd6ff94eb947a0cb9de10adb3485927dcfff68fce92bbc955327a27fbf166
hash sequences = ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e
hash outputs = c8db30b8a8c45860a0670e3036487888d35c21230f2fc37b0243d11ecf8d934f
spend type = 00
input index = 00000000
signature message = hash type byte + version + locktime + hash prevouts + hash amounts + hash scriptpubkeys + hash sequences + hash outputs + spend type + input index
signature message = 010200000000000000fcb2a500a85bcc659833d70ab67aca05e96403a334455c7c17a9f8e88fc56c438e965763e6a4bbc1088a94bf6c9cb3cbbdb4955f88355c807362a0fd43de4e3a84fbd6ff94eb947a0cb9de10adb3485927dcfff68fce92bbc955327a27fbf166ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0ec8db30b8a8c45860a0670e3036487888d35c21230f2fc37b0243d11ecf8d934f0000000000
signature hash = be2cbfeb3f4e9966451179c13658ef059268f58ddda6c3708bdd51719a004d2c
The taproot signature algorithm is similar the segwit signature algorithm, but with a few minor differences.
For key path spends: extension flag = 0
. This is used during the signature algorithm step.
3. Signature
The signature for a key path spend is created by signing the signature hash from the previous step using the tweaked private key.
See Schnorr signatures for details.
Example
tweaked private key = f0e0cd303d3074b940035e5fc111b26c0945ada0a5db448c80e32db753619e8e
aux rand = 0000000000000000000000000000000000000000000000000000000000000000
message = be2cbfeb3f4e9966451179c13658ef059268f58ddda6c3708bdd51719a004d2c
signature = schnorr_sign(tweaked private key, message, aux rand)
signature = 095282ddced815fc7130a4b78b8000b14b0205614f92958d89529afeedef26712dd452731d1721599b4e902b98017c970e51827ed287f6171234e054a61275a4
signature + hash type byte = 095282ddced815fc7130a4b78b8000b14b0205614f92958d89529afeedef26712dd452731d1721599b4e902b98017c970e51827ed287f6171234e054a61275a401
All P2TR outputs use the Schnorr signature algorithm.
Witness
Finally, after creating the signature you just need to construct a witness field to unlock the P2TR output.
For a key path spend, the witness contains a single Schnorr signature:
Example
095282ddced815fc7130a4b78b8000b14b0205614f92958d89529afeedef26712dd452731d1721599b4e902b98017c970e51827ed287f6171234e054a61275a401
{ "stackitems": "01", "0": { "size": "41", "item": "095282ddced815fc7130a4b78b8000b14b0205614f92958d89529afeedef26712dd452731d1721599b4e902b98017c970e51827ed287f6171234e054a61275a401" }, }}
0141095282ddced815fc7130a4b78b8000b14b0205614f92958d89529afeedef26712dd452731d1721599b4e902b98017c970e51827ed287f6171234e054a61275a401
Transaction: 6f43140e371f071dafe11a73fcda9ae455f1dd9be93f2b7cc4cde21737c759a5 (Input 0)
- The fact that the witness contains 1 element is what identifies this as a key path spend.
- Signatures in P2TR can be either 64 or 65 bytes in length. This is because the trailing hash type byte is optional (the default is
SIGHASH_ALL
if no hash type byte is present).
Annex
The annex is an optional data element that can be included at the end of the witness. It is reserved for future upgrades.
It is identified by a leading 0x50
byte, and is always the last element in the witness for both a key path spend or script path spend. It gets removed from the witness before script validation.
The annex is not currently used. It should not be included when spending a P2TR output.
Set annex present = 1
during the signature algorithm step if you're including an annex in your witness.
2. Script Path Spend
This is the fun part.
A script path spend requires 3 things:
- A leaf script from the script tree.
- The inputs required to unlock that leaf script.
- A merkle path proving that leaf script was part of the script tree.
Each of these 3 components are then placed in to the witness. Here's how you construct each component:
1. Script Inputs
These are the stack items required to unlock your chosen leaf script.
These stack items are equivalent to the data elements you'd use to unlock any custom P2WSH locking script. These script inputs can be a variable number of elements; you just need to provide the relevant data required to unlock the leaf script.
Example
script inputs = 03
NOTE: This will unlock the simple "OP_EQUAL OP_3" upcoming leaf script
Script Path Spend Signature Algorithm
If your script inputs include a signature, the signature algorithm will be slightly different compared to a key path spend.
The 2 key differences are:
- You need to use
extension flag = 1
. - You need to construct an extension to include some extra data at the end of the common signature message.
It's not a huge difference, but it's something that will probably catch you out the first time you try to make a script path spend that requires a signature.
2. Leaf Script
The next element in the witness is the complete leaf script.
This leaf script will be run in conjunction with the previous script inputs to unlock the P2TR output during script validation.
Example
leaf script = 5387 (leaf 3)
NOTE: This is the hexadecimal representation of the following script: OP_EQUAL OP_3
CAUTION: This is an insecure script and should not be used. Your coins can easily be stolen if you use it.
The script inputs + leaf script
part of the witness for a P2TR script path spend is the same structure as the witness for a P2WSH.
3. Control Block
The last element in the witness is the control block.
This control block contains the merkle path that proves the previous leaf script was part of the original script tree. It also contains the original public key, which is required to verify that the public key and merkle root can be combined to match the tweaked public key in the ScriptPubKey.
Example
control byte = c1
public key = a2fc329a085d8cfc4fa28795993d7b666cee024e94c40115141b8e9be4a29fa4
merkle path = 1324300a84045033ec539f60c70d582c48b9acf04150da091694d83171b44ec9bf2c4bf1ca72f7b8538e9df9bdfd3ba4c305ad11587f12bbfafa00d58ad6051d54962df196af2827a86f4bde3cf7d7c1a9dcb6e17f660badefbc892309bb145f
control block = <control byte> <public key> <merkle path>
control block = c1a2fc329a085d8cfc4fa28795993d7b666cee024e94c40115141b8e9be4a29fa41324300a84045033ec539f60c70d582c48b9acf04150da091694d83171b44ec9bf2c4bf1ca72f7b8538e9df9bdfd3ba4c305ad11587f12bbfafa00d58ad6051d54962df196af2827a86f4bde3cf7d7c1a9dcb6e17f660badefbc892309bb145f
The complete control block is split into 3 sections:
1. Control Byte
This first byte of the control block is the control byte. This encodes two pieces of data:
- The leaf version of your chosen leaf script.
- A bit indicating the parity of the y-coordinate of the full tweaked public key point.
even = 0
odd = 1
To create the control byte, you just add the leaf version and parity bit.
Example
leaf version = c0 (leaf version of the leaf script we are using i.e. leaf 3)
parity bit = 1 (y coordinate of tweaked public key is odd)
control byte = leaf version + parity bit
control byte = c1
- The parity of the tweaked public key is required to allow batch verification.
- The last bit of control byte is reserved for the parity bit, which means that all leaf versions are restricted to being even numbers.
2. Public Key
The next 32 bytes of the control block contains the original public key.
This needs to be revealed so that the tweaked public key can be calculated from it.
Example
public key = a2fc329a085d8cfc4fa28795993d7b666cee024e94c40115141b8e9be4a29fa4
3. Merkle Path
The remaining data in the control block is the merkle path.
This is a series of the 32-byte leaf/branch hashes required to construct the merkle root. The relevant leaf/branch hashes must be placed in order, so that anyone else can construct the same merkle root.
This merkle path proves that your leaf script was part of the original script tree, as the combination of the previous public key and the resulting merkle root will match the tweaked public key placed in the ScriptPubKey.
Example
┌────────┐ ┌────────┐
│ leaf 1 │ │ leaf 2 │
└───┬────┘ └───┬────┘
└────┬─────┘
X───┴────┐ ┌────────┐
│branch 1│ │ leaf 3 │ <- spending
└───┬────┘ └─────┬──┘
└─────┬──────┘
┌───┴────┐ X────────┐
│branch 2│ │ leaf 4 │
└───┬────┘ └────┬───┘
└────┬──────┘
┌───┴────┐ X────────┐
│branch 3│ │ leaf 5 │
└───┬────┘ └────┬───┘
└─────┬─────┘
┌───┴────┐
│branch 4│
└────────┘ Note: X = hashes required to construct the merkle path
leaf 1 hash = 6b13becdaf0eee497e2f304adcfa1c0c9e84561c9989b7f2b5fc39f5f90a60f6
leaf 2 hash = ed5af8352e2a54cce8d3ea326beb7907efa850bdfe3711cef9060c7bb5bcf59e
leaf 3 hash = 160bd30406f8d5333be044e6d2d14624470495da8a3f91242ce338599b233931
leaf 4 hash = bf2c4bf1ca72f7b8538e9df9bdfd3ba4c305ad11587f12bbfafa00d58ad6051d
leaf 5 hash = 54962df196af2827a86f4bde3cf7d7c1a9dcb6e17f660badefbc892309bb145f
branch 1 = 1324300a84045033ec539f60c70d582c48b9acf04150da091694d83171b44ec9
branch 2 = beec0122bddd26f642140bcd922e0264ce1e2be5808a41ae58d82e829bc913d7
branch 3 = a4e0d9cc12ce2f32069e98247581d5eb9ca0a4cf175771a8df2c53a93dcb0ebd
branch 4 = b5b72eea07b3e338962944a752a98772bbe1f1b6550e6fb6ab8c6e6adb152e7c
merkle path = branch 1 + leaf 4 hash + leaf 5 hash
merkle path = 1324300a84045033ec539f60c70d582c48b9acf04150da091694d83171b44ec9bf2c4bf1ca72f7b8538e9df9bdfd3ba4c305ad11587f12bbfafa00d58ad6051d54962df196af2827a86f4bde3cf7d7c1a9dcb6e17f660badefbc892309bb145f
The merkle path contains the minimum number of leaf/branch hashes required to reconstruct the merkle root. The closer your leaf script is to the merkle root, the shorter your merkle path will be.
The merkle path is limited to 128 leaf/branch hashes.
Witness
The witness for a script path spend contains the script inputs, leaf script, and control block
Example
03 5387 c1a2fc329a085d8cfc4fa28795993d7b666cee024e94c40115141b8e9be4a29fa41324300a84045033ec539f60c70d582c48b9acf04150da091694d83171b44ec9bf2c4bf1ca72f7b8538e9df9bdfd3ba4c305ad11587f12bbfafa00d58ad6051d54962df196af2827a86f4bde3cf7d7c1a9dcb6e17f660badefbc892309bb145f
{ "stackitems": "03", "0": { "size": "01", "item": "03" }, "1": { "size": "02", "item": "5387" }, "2": { "size": "81", "item": "c1a2fc329a085d8cfc4fa28795993d7b666cee024e94c40115141b8e9be4a29fa41324300a84045033ec539f60c70d582c48b9acf04150da091694d83171b44ec9bf2c4bf1ca72f7b8538e9df9bdfd3ba4c305ad11587f12bbfafa00d58ad6051d54962df196af2827a86f4bde3cf7d7c1a9dcb6e17f660badefbc892309bb145f" } }
03010302538781c1a2fc329a085d8cfc4fa28795993d7b666cee024e94c40115141b8e9be4a29fa41324300a84045033ec539f60c70d582c48b9acf04150da091694d83171b44ec9bf2c4bf1ca72f7b8538e9df9bdfd3ba4c305ad11587f12bbfafa00d58ad6051d54962df196af2827a86f4bde3cf7d7c1a9dcb6e17f660badefbc892309bb145f
Transaction: fa7eb13f6d854ed32ef284983c620f74050dd6d119dc9e91ad09c083b0267f8f (Input 1)
The size of the witness for a script path spend will vary depending on:
- The size of the leaf script.
- How many script inputs are required (i.e. the complexity of the leaf script).
- How far up the leaf script was up the script tree (i.e. how many leaf/branch hashes need to be provided in the control block).
The fact that the witness contains more than 1 element is what identifies it as a script path spend.
Signature Algorithm
How do you create a signature for a P2TR?
P2TR outputs use a special signature algorithm for both key path spends and script path spends.
Instead of using the entire raw transaction data as it is (as you do in the legacy algorithm), you collect specific parts in to a common signature message structure first.
The benefit of this is parts of the common signature message can be re-used when creating further signatures for the same transaction. This makes it more efficient when creating multiple signatures to unlock multiple inputs in the same transaction.
This signature algorithm is similar to segwit algorithm. It just has some small adjustments and includes some extra taproot-specific data for signing.
See the key path spend example and script path spend (signature) example for actual code examples.
Common Signature Message
The common signature message holds the bulk of the transaction data.
The parts of the transaction data included in this structure varies depending on how much of the transaction data you're signing (as specified by the hash type).
Field | Size (bytes) | Description |
---|---|---|
Hash Type | 1 | A byte indicating the amount of the transaction data you want to sign. |
Version | 4 | The transaction version field. |
Locktime | 4 | The transaction locktime field. |
Hash Prevouts | 32 | The SHA-256 hash of the txid+vout outpoints for all the inputs included in the transaction. |
Hash Amounts | 32 | The SHA-256 hash of all the output amount fields for all the inputs included in the transaction. |
Hash ScriptPubKeys | 32 | The SHA-256 hash all the output scriptpubkeys for all the inputs included in the transaction.Note: The data for each scriptpubkey is scriptpubkeysize+scriptpubkey .
|
Hash Sequences | 32 | The SHA-256 hash of all the sequence fields for all the inputs included in the transaction. |
Hash Outputs | 32 | The SHA-256 hash of all the outputs in the transaction. Note: The data for each output is amount+scriptpubkeysize+scriptpubkey .
|
Spend Type | 1 | A single byte that encodes the extension flag and annex present values. |
Input Index | 4 | The vin of the input being signed for. |
Input Outpoint | 36 | The txid+vout outpoint of the input being signed for. |
Input Amount | 8 | The amount field of the input being signed for. |
Input ScriptPubKey | variable | The scriptpubkey of the input being signed for. |
Input Sequence | 4 | The sequence field of the input being signed for. |
Hash Annex | 32 | The SHA-256 of the optional annex included at the end of the witness field. Note: The annex must have a compact size field prefixed before hashing. |
Hash Single Output | 32 | The SHA-256 of the output opposite the input currently being signed for. Note: The data for the output is amount+scriptpubkeysize+scriptpubkey
|
Key:
- NOT SIGHASH_ANYONECANPAY
- NOT SIGHASH_NONE or SIGHASH_SINGLE
- SIGHASH_ANYONECANPAY
- SIGHASH_SINGLE
Hash Type
The hash type byte indicates how much of the transaction you want to sign.
The amount of the transaction you sign determines whether other people can add or remove inputs and outputs from your signed transaction.
Byte | Type | Description |
---|---|---|
0x01 |
SIGHASH_ALL |
Sign all inputs and outputs |
0x02 |
SIGHASH_NONE |
Sign all inputs only |
0x03 |
SIGHASH_SINGLE |
Sign all inputs and one corresponding output |
0x81 |
SIGHASH_ANYONECANPAY | SIGHASH_ALL |
Sign one input and all outputs |
0x82 |
SIGHASH_ANYONECANPAY | SIGHASH_NONE |
Sign one input only |
0x83 |
SIGHASH_ANYONECANPAY | SIGHASH_SINGLE |
Sign one inputs and one corresponding output |
See signature hash types for more details.
Default Hash Type
Taproot introduces a new default SIGHASH byte for use in P2TR:
0x00 = SIGHASH_DEFAULT
This default reverts to SIGHASH_ALL
, which is the most common hash type as it signs the entire transaction data (preventing anyone from making any changes to the signed transaction).
SIGHASH_DEFAULT
is also used if the no hash type byte is appended to the final signature. So if you're signing a transaction using SIGHASH_DEFAULT
(i.e. SIGHASH_ALL
), the hash type byte at the end of the signature is optional.
Spend Type
The spend type encodes the extension flag and annex present values in to a single byte.
spend type = (extension flag * 2) + annex present
Extension Flag
The extension flag indicates whether an extension has been added to the end of the common signature message.
An extension is only used if you're creating a signature for a script path spend.
0
= key path spend1
= script path spend (tapscript)
All leaf scripts currently use tapscript, so the extension flag for all script path spends should be 1
.
Annex Present
The annex present value indicates whether the optional annex element has been included at the end of the witness field.
0
= annex is present1
= annex is not present
The annex is not currently used, so the annex present value should be 0
.
Common Signature Message Extension
Script path spends require an extension to the common signature message.
This extension contains some extra information about the chosen leaf script:
Field | Size (bytes) | Description |
---|---|---|
Leaf Hash | 32 | The leaf hash for the chosen script you're using from the script tree. |
Public Key Version | 1 | The type of public key used in the leaf script. Used indicate different types of public keys in future upgrades.default = 0x00
|
Codeseparator Position | 4 | The opcode position of the last OP_CODESEPARATOR in the leaf script (if there is one).none = 0xffffffff
|
Unless you have a reason to change them, for most leaf scripts the public key version = 0x00
and codeseparator position = 0xffffffff
.
Signature Hash
The signature hash is the actual "message" that we sign when creating the signature.
It's a tagged hash of the common signature message, along with a sighash epoch prefix and the optional extension:
- Tag = "TapSigHash"
- Data =
sighash epoch
+common signature message
+extension
Sighash Epoch
0x00 = default
The sighash epoch byte is prefixed to the common signature message when creating the signature hash.
It's an extra byte that allows for upgrades to the signature algorithm in the future. Using this prefix means that the "TapSigHash" can be reused without having to use different tags to indicate different algorithm changes in the future.
Just use 0x00
and don't worry about it for now.
Signature
The signature for P2TR spends is created using the Schnorr signature scheme. This involves signing the signature hash from above using a private key.
signature = schorr_sign(private key, message, aux rand)
- private key — For key paths spends, this is the tweaked private key. For script path spends, this is the corresponding private key for the public key in the leaf script.
- message — The signature hash calculated in the previous step.
- aux rand — An additional 32-bytes of randomly generated data.
This returns a signature that can be used inside the witness to unlock the P2TR output.
The actual signature that gets placed inside the witness has the hash type byte appended to the end. If you do not append a hash type byte to the signature, SIGHASH_DEFAULT is used.
Tapscript
Tapscript is the locking mechanism used in leaf scripts.
It's basically the same as Script.
The only difference is that some opcodes have been modified to use Schnorr signature validation instead of ECDSA, and a handful of other opcodes have been modified or disabled. But for the most part, it works the same as traditional Script.
The version of tapscript is indicated by the leaf version, which allows for new variations of tapscript in the future.
Opcodes
Tapscript changes the operation of the following opcodes:
Byte | Opcode | Description |
---|---|---|
0xac | OP_CHECKSIG |
Now uses Schnorr signature verification with the taproot signature algorithm. |
0xad | OP_CHECKSIGVERIFY |
Now uses Schnorr signature verification with the taproot signature algorithm. |
0xae | OP_CHECKMULTISIG |
Disabled. |
0xaf | OP_CHECKMULTISIGVERIFY |
Disabled. |
0xba | OP_CHECKSIGADD |
New. Replaces OP_CHECKMULTISIG and OP_CHECKMULTISIGVERIFY . Allows for more efficient multisignature scripts using batch verification. |
OP_NOPX |
Renamed to OP_SUCCESSX .
These are reserved for upgrades requiring new opcode functionality. These opcodes were previously disabled, but in tapscript (version 192) the presence of these opcodes mark the script as valid (no matter where they appear). OP_NOPX refers to the following opcodes: 0x50, 0x62, 0x7e-0x81, 0x83-0x86, 0x89-0x8a, 0x8d-0x8e, 0x95-0x99, 0xbb-0xfe.
|
Limits
Tapscript has increased limits compared to Script. There are two significant changes:
-
Total Script Size:
- Script: max 10,000 bytes
- Tapscript: no limit
-
Opcodes:
- Script: max 201 non-push opcodes
- Tapscript: no limit
This means that the size of leaf scripts are now only limited by the size of a block.
Sigops Budget
Tapscript uses a "budget" to limit the number of signature operations (i.e. OP_CHECKSIG
, OP_CHECKSIGVERIFY
) in a single script.
Budget = 50 + witness size
Signature Operation = -50
This budget means that every tapscript can include at least 1 signature operation, but can include more depending on the size of leaf script and the complexity of the script tree (as this will influence the size of the control block).
Legacy Sigops Limit
This sigops budget means that the sigops limit for tapscript is now per-script as opposed to per-block. This makes it easier for miners to select transactions for inclusion in their blocks.
The legacy sigops limit acts as a secondary constraint in addition to the block size limit, so miners have to be extra cautious about which transactions they include in their block.
Examples
Here are some actual examples showing the construction and spending of P2TR outputs. They show raw data and code for the most common ways to create and spend P2TR locking scripts.
These examples are basic, but they're ideal if you're coding your own implementation and want to check that the results you're getting along the way are correct.
Common Code
This is a file of common helper functions used in all the code examples below. You can save it as examples-taproot-common.rb
in the same directory as the code examples below, or copy and paste the code at the top of each one.
# -------------------------
# Elliptic Curve Parameters
# -------------------------
# these are the parameters for secp256k1, which is the same curve used in ECDSA
# note: setting these as $global variables so they're accessible from with the functions below (without having to pass them as arguments)
# y² = x³ + ax + b
$a = 0
$b = 7
# prime field
$p = 115792089237316195423570985008687907853269984665640564039457584007908834671663 #=> 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
# number of points on the curve we can hit ("order")
$n = 115792089237316195423570985008687907852837564279074904382605163141518161494337 #=> 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
# generator point (the starting point on the curve used for all calculations)
$G = {
x: 55066263022277343669578718895168534326250603453777594175500187360389116729240, #=> 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
y: 32670510020758816978083085130507043184471273380659243275938904335757337482424, #=> 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
}
# --------------------------
# Elliptic Curve Mathematics
# --------------------------
# Modular Inverse: Ruby doesn't have a built-in modinv function
def inverse(a, m = $p)
m_orig = m # store original modulus
a = a % m if a < 0 # make sure a is positive
y_prev = 0
y = 1
while a > 1
q = m / a
y_before = y # store current value of y
y = y_prev - q * y # calculate new value of y
y_prev = y_before # set previous y value to the old y value
a_before = a # store current value of a
a = m % a # calculate new value of a
m = a_before # set m to the old a value
end
return y % m_orig
end
# Double: add a point to itself
def double(point)
# check for point at infinity (greatest common divisor between 2y and p isn't 1)
if (((2 * point[:y]) % $p).gcd($p) != 1) # taken from BitcoinECDSA.php
raise "Point at infinity."
end
# slope = (3x₁² + a) / 2y₁
slope = ((3 * point[:x] ** 2 + $a) * inverse((2 * point[:y]), $p)) % $p # using inverse to help with division
# x = slope² - 2x₁
x = (slope ** 2 - (2 * point[:x])) % $p
# y = slope * (x₁ - x) - y₁
y = (slope * (point[:x] - x) - point[:y]) % $p
# Return the new point¢ªº
return { x: x, y: y }
end
# Add: add two points together
def add(point1, point2)
# double if both points are the same
if point1 == point2
return double(point1)
end
# check for point at infinity (greatest common divisor between x1-x2 and p isn't 1)
if ((point1[:x] - point2[:x]).gcd($p) != 1) # taken from BitcoinECDSA.php
raise "Point at infinity."
end
# slope = (y₁ - y₂) / (x₁ - x₂)
slope = ((point1[:y] - point2[:y]) * inverse(point1[:x] - point2[:x], $p)) % $p
# x = slope² - x₁ - x₂
x = (slope ** 2 - point1[:x] - point2[:x]) % $p
# y = slope * (x₁ - x) - y₁
y = ((slope * (point1[:x] - x)) - point1[:y]) % $p
# Return the new point
return { x: x, y: y }
end
# Multiply: use double and add operations to quickly multiply a point by an integer value (i.e. a private key)
def multiply(k, point = $G)
# create a copy the initial starting point (for use in addition later on)
current = point
# convert integer to binary representation
binary = k.to_s(2)
# double and add algorithm for fast multiplication
binary.split("").drop(1).each do |char| # from left to right, ignoring first binary character
# 0 = double
current = double(current)
# 1 = double and add
current = add(current, point) if char == "1"
end
# return the final point
current
end
# ----------------
# BIP340 Functions (Schnorr Signatures)
# ----------------
# convert hexadecimal string of bytes to integer
def int(bytes)
return bytes.to_i(16)
end
# convert integer to hexadecimal string of bytes
def bytes(int)
return int.to_s(16).rjust(64, "0") # convert to hex and pad with zeros to make it 32 bytes (64 characters)
end
# hash some data using SHA256 with a tag prefix
def tagged_hash(tag, message)
# create a hash of the tag first
tag_hash = Digest::SHA256.hexdigest(tag) # hash the string directly
# prefix the message with the tag hash (the tag_hash is prefixed twice so that the prefix is 64 bytes in total)
preimage = [tag_hash + tag_hash + message].pack("H*") # also convert to byte sequence before hashing
# SHA256(tag_hash || tag_hash || message)
result = Digest::SHA256.hexdigest(preimage);
return result
end
# convert public key (x coordinate only) in to a point - lift_x() in BIP 340
def lift_x(public_key)
x = int(public_key) # convert from x coordinate from hex to an integer
y_sq = (x**3 + 7) % $p # use the elliptic curve equation (y² = x³ + ax + b) to work out the value of y from x
y = y_sq.pow(($p+1)/4, $p) # secp256k1 is chosen in a special way so that the square root of y is y^((p+1)/4)
# check that x coordinate is less than the field size
if x >= $p
raise "x value in public key is not a valid coordinate because it is not less than the elliptic curve field size"
end
# verify that the computed y value is the square root of y_sq (otherwise the public key was not a valid x coordinate on the curve)
if (y**2) % $p != y_sq
raise "public key is not a valid x coordinate on the curve"
end
# if the calculated y value is odd, negate it to get the even y value instead (for this x-coordinate)
if y % 2 != 0
y = $p - y
end
# public key point
public_key_point = {x: x, y: y}
return public_key_point
end
# ----------------
# BIP341 Functions (Taproot)
# ----------------
# calculate control byte from leaf version and parity of tweaked public key
def calculate_control_byte(leaf_version, tweaked_pubkey_point)
# set parity bit based on whether y is even or odd
if (tweaked_pubkey_point[:y] % 2 == 0)
parity_bit = 0 # y is even
else
parity_bit = 1 # y is odd
end
# Why is it necessary to reveal a bit in a script path spend and check that it matches the parity of the Y coordinate of Q?
# The parity of the Y coordinate is necessary to lift the X coordinate q to a unique point. While this is not strictly necessary for verifying the taproot commitment as described above, it is necessary to allow batch verification. Alternatively, Q could be forced to have an even Y coordinate, but that would require retrying with different internal public keys (or different messages) until Q has that property. There is no downside to adding the parity bit because otherwise the control block bit would be unused.
# calculate control byte
control_byte = field(dechex(leaf_version + parity_bit), 1)
return control_byte
end
# -----------------
# General Functions
# -----------------
# convert decimal number to hexadecimal
def dechex(dec)
return dec.to_i.to_s(16)
end
# add padding to create a fixed-size field (e.g. 4 => 00000004)
def field(field, size=4)
return field.to_s.rjust(size*2, '0')
end
# swap endianness
def reversebytes(hex)
return hex.to_s.scan(/../).reverse.join
end
# get length of bytes in compact size structure
def compact_size(i)
if (i <= 252)
result = field(dechex(i), 1)
elsif (i > 252 && i <= 65535)
result = 'fd' + field(dechex(i), 2)
elsif (i > 65535 && i <= 4294967295)
result = 'fe' + field(dechex(i), 4)
elsif (i > 4294967295 && i <= 18446744073709551615)
result = 'ff' + field(dechex(i), 8)
end
return result
end
# add compact size field to start of scriptpubkey
def serialize_script(script)
# get length of script as number of bytes
length = script.length / 2
# return script with compact size prepended
return compact_size(length) + script
end
1. Key Path Spend
This is an example of a key path spend.
This is the "default" method for spending a P2TR output.
Construct
public key = 924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329 merkle root = tweak = 8dc8b9030225e044083511759b58328b46dffcc78b920b4b97169f9d7b43d3b5 tweaked public key = 0f0c8db753acbd17343a39c2f3f4e35e4be6da749f9e35137ab220e7b238a667 scriptpubkey = 51200f0c8db753acbd17343a39c2f3f4e35e4be6da749f9e35137ab220e7b238a667
TXID: a7115c7267dbb4aab62b37818d431b784fe731f4d2f9fa0939a9980d581690ec (Output 0)
This example doesn't use a script tree, which is why the merkle root is empty.
Spend
private key = 55d7c5a9ce3d2b15a62434d01205f3e59077d51316f5c20628b3a4b8b2a76f4c public key = 924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329 merkle root = tweak = 8dc8b9030225e044083511759b58328b46dffcc78b920b4b97169f9d7b43d3b5 tweaked private key = 37f0f35933e8b52e6210dca589523ea5b66827b4749c49456e62fae4c89c6469 sighash = a7b390196945d71549a2454f0185ece1b47c56873cf41789d78926852c355132 aux rand = 0000000000000000000000000000000000000000000000000000000000000000 signature = b693a0797b24bae12ed0516a2f5ba765618dca89b75e498ba5b745b71644362298a45ca39230d10a02ee6290a91cebf9839600f7e35158a447ea182ea0e022ae witness = b693a0797b24bae12ed0516a2f5ba765618dca89b75e498ba5b745b71644362298a45ca39230d10a02ee6290a91cebf9839600f7e35158a447ea182ea0e022ae01
TXID: 091d2aaadc409298fd8353a4cd94c319481a0b4623fb00872fe240448e93fcbe (Input 0)
This example data does not show the construction of the sighash from the original unsigned transaction data. See the code below for the complete steps and data.
Code
This code requires examples-taproot-common.rb
require_relative 'examples-taproot-common.rb' # common functions
require "digest" # library for SHA256 hash function
# =========
# Construct
# =========
# ----------
# Public Key
# ----------
internal_pubkey = '924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329'
# -----------
# Merkle Tree
# -----------
# merkle root
merkle_root = ""
# ----------------
# Tweak Public Key
# ----------------
# create a hash from the public key and merkle root - this will be used to tweak the public key
tweak = tagged_hash("TapTweak", internal_pubkey + merkle_root)
#=> 8dc8b9030225e044083511759b58328b46dffcc78b920b4b97169f9d7b43d3b5
# tweak the public key
t = int(tweak) # convert to integer (so it's like a private key) (check this is less than curve order)
internal_pubkey_point = lift_x(internal_pubkey)
tweaked_pubkey_point = add(internal_pubkey_point, multiply(t, $G)) # (check y is even)
tweaked_pubkey = bytes(tweaked_pubkey_point[:x])
#=> 0f0c8db753acbd17343a39c2f3f4e35e4be6da749f9e35137ab220e7b238a667
# convert to scriptpubkey
scriptpubkey = '51' + '20' + tweaked_pubkey
#=> 51200f0c8db753acbd17343a39c2f3f4e35e4be6da749f9e35137ab220e7b238a667
# =====
# Spend
# =====
# -------------------
# Signature Algorithm
# -------------------
hash_type = 1 # SIGHASH_ALL
hash_type_byte = field(dechex(hash_type), 1) # specific hash type being used for this input (1 = SIGHASH_ALL)
ext_flag = 0
annex_present = 0 # 1 = true, 0 = false
spend_type = field(dechex((ext_flag * 2) + annex_present), 1)
# unsigned raw tx:
#
# 02000000000101ec9016580d98a93909faf9d2f431e74f781b438d81372bb6aab4db67725c11a70000000000ffffffff0110270000000000001600144e44ca792ce545acba99d41304460dd1f53be3840000000000
version = '02000000'
locktime = '00000000'
prevouts = 'ec9016580d98a93909faf9d2f431e74f781b438d81372bb6aab4db67725c11a7'+'00000000' # txid and amount are in little-endian
amounts = reversebytes(field(dechex('20000'), 8))
sequences = 'ffffffff'
scriptpubkeys = serialize_script('51200f0c8db753acbd17343a39c2f3f4e35e4be6da749f9e35137ab220e7b238a667')
sha_prevouts = Digest::SHA256.hexdigest([prevouts].pack("H*")); #=> eaff979f4771d11a857e48550a28c4d3503cf2a966182c94010fd21d5b700700
sha_amounts = Digest::SHA256.hexdigest([amounts].pack("H*")); #=> ae9475d31b535bec000c9bfc7abc79b6a07db9eea2dd0e5066adddfb349bb53b
sha_sequences = Digest::SHA256.hexdigest([sequences].pack("H*")); #=> ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e
sha_scriptpubkeys = Digest::SHA256.hexdigest([scriptpubkeys].pack("H*")); #=> 4cd686f794463476c6fc24b4a43e0abc7b58a0ea78a998d2be39cdb73f8d9cc2
outputs = '1027000000000000' + serialize_script('00144e44ca792ce545acba99d41304460dd1f53be384') # amount + scriptpubkey
sha_outputs = Digest::SHA256.hexdigest([outputs].pack("H*")); #=> c3a3f98ac2310126a614269e5715b0cabf38ce62232dd9ed8a878bdc0addea75
input_index = '00000000' # 4-byte vin of the input you're spending
sha_annex = ''
sha_single_output = ''
# signature message
sigmsg = ''
sigmsg += hash_type_byte + version + locktime
# NOT SIGHASH_ANYONECANPAY (0x80)
if (hash_type & 0x80 == 0)
sigmsg += sha_prevouts + sha_amounts + sha_scriptpubkeys + sha_sequences
end
# NOT SIGHASH_NONE (0x02/0x82) OR SIGHASH_SINGLE (0x03/0x83)
if (hash_type & 3 < 2)
sigmsg += sha_outputs
end
sigmsg += spend_type
# NOT SIGHASH_ANYONECANPAY (0x80)
if (hash_type & 0x80 == 0)
sigmsg += input_index
# SIGHASH_ANYONECANPAY (0x80)
else
sigmsg += input_outpoint + input_amount + input_scriptpubkey + input_sequence
end
sigmsg += sha_annex
# SIGHASH_SINGLE
if (hash_type & 3 == 3)
sigmsg += sha_single_output
end
#=> 010200000000000000eaff979f4771d11a857e48550a28c4d3503cf2a966182c94010fd21d5b700700ae9475d31b535bec000c9bfc7abc79b6a07db9eea2dd0e5066adddfb349bb53b4cd686f794463476c6fc24b4a43e0abc7b58a0ea78a998d2be39cdb73f8d9cc2ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0ec3a3f98ac2310126a614269e5715b0cabf38ce62232dd9ed8a878bdc0addea750000000000
# sighash epoch
epoch = '00'
# sighash
sighash = tagged_hash("TapSighash", epoch + sigmsg) # don't forget the 0x00 prefix to the sigmsg
#=> a7b390196945d71549a2454f0185ece1b47c56873cf41789d78926852c355132
# -----------------
# Tweak Private Key
# -----------------
# original data from before
internal_pubkey = '924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329'
tweak = '8dc8b9030225e044083511759b58328b46dffcc78b920b4b97169f9d7b43d3b5'
private_key = '55d7c5a9ce3d2b15a62434d01205f3e59077d51316f5c20628b3a4b8b2a76f4c'
# convert private key to the public key point so we can check if it creates an even y value
private_key_int = int(private_key) #=> 38827828470485795394567956987183954348973858899545806359243020977513867734860
private_key_to_public_key = multiply(private_key_int)
# {:x=>66172109705071441823295681989107852967180089637640153745774876919271983297321, :y=>48613218598235331436749946747294004934275959149063298181052452063566809595043}
# x = 924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329 (same as internal public key)
# negate the private key if y is odd
if private_key_to_public_key[:y] % 2 == 1
private_key_int_negated = $n - private_key_int #=> 76964260766830400029003028021503953503863705379529098023362142164004293759477
else
private_key_int_negated = private_key_int
end
tweaked_privkey_int = (private_key_int_negated + int(tweak)) % $n
tweaked_privkey = bytes(tweaked_privkey_int)
#=> 37f0f35933e8b52e6210dca589523ea5b66827b4749c49456e62fae4c89c6469
# ----
# Sign
# ----
# signing data
private_key = tweaked_privkey
message = sighash
aux_rand = '0000000000000000000000000000000000000000000000000000000000000000'
# convert private key to an integer
d0 = int(private_key)
# make sure private key is in valid range (greater than 0 and less than the number of points on the curve)
unless (1..$n-1).include?(d0)
raise "private key must be in the range 1..n-1"
end
# calculate the public key point from the private key
public_key_point = multiply(d0) # multiply() checks for point at infinity
# negate the private key if the public key it creates doesn't have an even y value, else keep the private key the same
# note: due to the way the elliptic curve works, negate the private key will produce a public key with the same x coordinate, but the opposite y value
if public_key_point[:y] % 2 != 0
d = $n - d0
else
d = d0
end
# create a tagged hash of the auxiliary bytes
aux_rand_hash = tagged_hash("BIP0340/aux", aux_rand)
# first step toward creating the nonce is to XOR the private key with the hash of the auxiliary bytes
t = d ^ int(aux_rand_hash)
# create the nonce by hashing t (from the previous step) along with the public_key and message
k0 = int(tagged_hash("BIP0340/nonce", bytes(t) + bytes(public_key_point[:x]) + message)) % $n # public key is included in hash for "key-prefixed" schnorr signatures
# check that the nonce isn't zero
if k0 == 0
raise "nonce must not be zero (this is almost impossible, but checking anyway)"
end
# use this nonce to get a point on the curve
random_point = multiply(k0) # multiply() checks for point at infinity
# negate the nonce used to create the random point if the public key it creates doesn't have an even y value
if random_point[:y] % 2 != 0
k = $n - k0
# note: due to the way the elliptic curve works, the inverse private key will produce an even y value
else
k = k0
end
# create the challenge e value by hashing the random point with the public key and message
e = int(tagged_hash("BIP0340/challenge", bytes(random_point[:x]) + bytes(public_key_point[:x]) + message)) % $n
# r value is the x-coordinate of point R
r = random_point[:x]
# s value: (k + e*d) mod n
s = (k + e * d) % $n # this is linear (whereas s in ECDSA is non-linear)
# signature is the r and s values converted to 32-byte hexadecimal string and concatenated
sig = bytes(r) + bytes(s)
#=> b693a0797b24bae12ed0516a2f5ba765618dca89b75e498ba5b745b71644362298a45ca39230d10a02ee6290a91cebf9839600f7e35158a447ea182ea0e022ae
# -------
# Witness
# -------
# witness (signature + hash_type byte)
witness = sig + hash_type_byte
puts witness
#=> b693a0797b24bae12ed0516a2f5ba765618dca89b75e498ba5b745b71644362298a45ca39230d10a02ee6290a91cebf9839600f7e35158a447ea182ea0e022ae01
2. Script Path Spend (Simple)
This is an example of a basic script path spend.
The script tree contains one leaf. The leaf script is a basic OP_8
OP_EQUAL
script that does not require a signature.
It's easier to get to grips with script path spends the first time around by using a leaf script that does not require a signature.
This script is not secure. I'm just using this basic leaf script to highlight the mechanics of a script path spend. If you use this kind of script in the real world your coins are likely to be stolen when you broadcast the spending transaction (which is actually what happened to me when I spent this output).
Construct
public key = 924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329 leaf version = c0 leaf script = 5887 leaf hash = e4b47d76d2f78791323a035811c350bb7875568006cb60f0e171efb70c11bda4 merkle root = e4b47d76d2f78791323a035811c350bb7875568006cb60f0e171efb70c11bda4 tweak = fe9007dc134942f8aabcc1af9e1c201c454d333501efccfbb8abf05bdfcb09a5 tweaked public key = 1baeaaf9047cc42055a37a3ac981bdf7f5ab96fad0d2d07c54608e8a181b9477 scriptpubkey = 51201baeaaf9047cc42055a37a3ac981bdf7f5ab96fad0d2d07c54608e8a181b9477
TXID: 8bc4f8facaaf7c4bdf6d77fac90aea208c2099a091d4b09658d002739daaad87 (Output 1)
This script tree only contains one leaf, so the resulting merkle root is equal to that leaf hash.
Spend
script inputs = 08 leaf script = 5887 control byte = c1 public key = 924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329 merkle path = control block = c1924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329 witness = 03010802588721c1924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329
TXID: 5ff05f74d385bd39e344329330461f74b390c1b5ead87c4f51b40c555b75719d (Input 1)
The script input is 08
, which satisfies the original leaf script of OP_8
OP_EQUAL
.
No merkle path is required to spend using this leaf script, because the script tree only contained one leaf and merkle root can be reconstructed from the single leaf script alone.
The resulting witness is the witness encoding of the following items: script inputs
+ leaf script
+ control block
.
The control byte is c1
instead of c0
, as the parity of the tweaked public key is odd.
Code
This code requires examples-taproot-common.rb
require_relative 'examples-taproot-common.rb' # common functions
require "digest" # library for SHA256 hash function
# =========
# Construct
# =========
# ----------
# Public Key
# ----------
internal_pubkey = '924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329'
# -----------
# Merkle Tree
# -----------
# leaf
leaf_version = 192 # 0xc0
script = '5887' # OP_8 OP_EQUAL
leaf_hash = tagged_hash("TapLeaf", field(dechex(leaf_version), 1) + serialize_script(script))
#=> e4b47d76d2f78791323a035811c350bb7875568006cb60f0e171efb70c11bda4
# merkle root
merkle_root = leaf_hash
# ----------------
# Tweak Public Key
# ----------------
# create a hash from the public key and merkle root - this will be used to tweak the public key
tweak = tagged_hash("TapTweak", internal_pubkey + merkle_root)
#=> fe9007dc134942f8aabcc1af9e1c201c454d333501efccfbb8abf05bdfcb09a5
# tweak the public key
t = int(tweak) # convert to integer (so it's like a private key) (check this is less than curve order)
internal_pubkey_point = lift_x(internal_pubkey)
tweaked_pubkey_point = add(internal_pubkey_point, multiply(t, $G)) # (check y is even)
tweaked_pubkey = bytes(tweaked_pubkey_point[:x])
#=> 1baeaaf9047cc42055a37a3ac981bdf7f5ab96fad0d2d07c54608e8a181b9477
# convert to scriptpubkey
scriptpubkey = '51' + '20' + tweaked_pubkey
#=> 51201baeaaf9047cc42055a37a3ac981bdf7f5ab96fad0d2d07c54608e8a181b9477
# =====
# Spend
# =====
# -------------
# Control Block
# -------------
# no merkle path required, as the leaf hash is the merkle root
merkle_path = ''
# note: all leaf_versions are 192
control_byte = calculate_control_byte(192, tweaked_pubkey_point)
# control block (no branches or leaves are needed to build the merkle root - the hash of the script is the merkle root)
control_block = control_byte + internal_pubkey
puts control_block
#=> c1924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329
# -------
# Witness
# -------
# <script inputs> <script> <control block>
script_inputs = '08' # CAUTION: The witness only contains data pushes, unlike the scriptSig.
script = '5887' # OP_8 OP_EQUAL
control_block = control_block
# NOTE: The witness only contains data pushes, unlike the scriptSig.
# If you try to set 58 (OP_8) as the script input, the Script interpreter compares 0x58 and 8 and pushes 0 on the stack.
# You will then get this error:
# mandatory-script-verify-flag-failed (Script evaluated without error but finished with a false/empty top stack element)
witness =
compact_size(3) +
compact_size(script_inputs.length / 2) + script_inputs +
compact_size(script.length / 2) + script +
compact_size(control_block.length / 2) + control_block
puts witness
#=> 03015802588721c1924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329
3. Script Path Spend (Signature)
This is another example of a script path spend, but this time the leaf script requires a signature to unlock it.
This means you need to use the signature algorithm for script path spends, which is slightly different to the algorithm used in key path spends.
Construct
public key = 924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329 leaf version = c0 leaf script = 206d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0ac leaf hash = 858dfe26a3dd48a2c1fcee1d631f0aadf6a61135fc51f75758e945bca534ef16 merkle root = 858dfe26a3dd48a2c1fcee1d631f0aadf6a61135fc51f75758e945bca534ef16 tweak = 479785dd89a6441dbe00c7661865a0cc68672e8021f4547ac7f89ac26ac049f2 tweaked public key = f3778defe5173a9bf7169575116224f961c03c725c0e98b8da8f15df29194b80 scriptpubkey = 5120f3778defe5173a9bf7169575116224f961c03c725c0e98b8da8f15df29194b80
TXID: d1c40446c65456a9b11a9dddede31ee34b8d3df83788d98f690225d2958bfe3c (Output 0)
Spend
script private key = 9b8de5d7f20a8ebb026a82babac3aa47a008debbfde5348962b2c46520bd5189 sighash = 752453d473e511a0da2097d664d69fe5eb89d8d9d00eab924b42fc0801a980c9 aux rand = 0000000000000000000000000000000000000000000000000000000000000000 signature = 01769105cbcbdcaaee5e58cd201ba3152477fda31410df8b91b4aee2c4864c7700615efb425e002f146a39ca0a4f2924566762d9213bd33f825fad83977fba7f01 leaf script = 206d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0ac control byte = c0 public key = 924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329 merkle path = control block = c0924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329 witness = 034101769105cbcbdcaaee5e58cd201ba3152477fda31410df8b91b4aee2c4864c7700615efb425e002f146a39ca0a4f2924566762d9213bd33f825fad83977fba7f0122206d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0ac21c0924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329
TXID: 797505b104b5fb840931c115ea35d445eb1f64c9279bf23aa5bb4c3d779da0c2 (Input 0)
To create a signature to unlock this leaf script, you need to use the private key for the public key in the leaf script (and not the one for the public key used in a key path spend).
The resulting witness is the witness encoding of the following items: signature
+ leaf script
+ control block
.
This example data does not show the construction of the sighash from the original unsigned transaction data. See the code below for the complete steps and data.
Code
This code requires examples-taproot-common.rb
require_relative 'examples-taproot-common.rb' # common functions
require "digest" # library for SHA256 hash function
# =========
# Construct
# =========
# ----------
# Public Key
# ----------
internal_pubkey = '924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329'
# -----------
# Merkle Tree
# -----------
# hash leaf
leaf_version = 192 # 0xc0
script = '206d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0ac' # OP_PUSHBYTES_32 6d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0 OP_CHECKSIG
leaf_hash = tagged_hash("TapLeaf", field(dechex(leaf_version), 1) + serialize_script(script))
#=> 858dfe26a3dd48a2c1fcee1d631f0aadf6a61135fc51f75758e945bca534ef16
# merkle root
merkle_root = leaf_hash
# ----------------
# Tweak Public Key
# ----------------
# create a hash from the public key and merkle root - this will be used to tweak the public key
tweak = tagged_hash("TapTweak", internal_pubkey + merkle_root)
#=> 479785dd89a6441dbe00c7661865a0cc68672e8021f4547ac7f89ac26ac049f2
# tweak the public key
t = int(tweak) # convert to integer (so it's like a private key) (check this is less than curve order)
internal_pubkey_point = lift_x(internal_pubkey)
tweaked_pubkey_point = add(internal_pubkey_point, multiply(t, $G)) # (check y is even)
tweaked_pubkey = bytes(tweaked_pubkey_point[:x])
#=> f3778defe5173a9bf7169575116224f961c03c725c0e98b8da8f15df29194b80
# convert to scriptpubkey
scriptpubkey = '51' + '20' + tweaked_pubkey
#=> 5120f3778defe5173a9bf7169575116224f961c03c725c0e98b8da8f15df29194b80
# =====
# Spend
# =====
# -------------
# Control Block
# -------------
# <control byte with leaf version and parity bit> <internal pubkey> <leaves/branches required to calculate merkle root>
# note: all leaf_versions are 192
control_byte = calculate_control_byte(192, tweaked_pubkey_point)
# control block (no branches or leaves are needed to build the merkle root - the hash of the script is the merkle root)
control_block = control_byte + internal_pubkey
#=> c0924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329
# -------------------
# Signature Algorithm
# -------------------
# unsigned raw tx:
#
# 020000000001013cfe8b95d22502698fd98837f83d8d4be31ee3eddd9d1ab1a95654c64604c4d10000000000ffffffff01983a0000000000001600140de745dc58d8e62e6f47bde30cd5804a82016f9e0000000000
hash_type = 1 # SIGHASH_ALL
hash_type_byte = field(dechex(hash_type), 1)
ext_flag = 1 # Use ext_flag = 1 to indicate we are spending using tapscript (i.e. a script path spend of a taproot output)
annex_present = 0 # 1 = true, 0 = false
spend_type = field(dechex((ext_flag * 2) + annex_present), 1)
version = '02000000'
locktime = '00000000'
# hash_type & 0x80 != SIGHASH_ANYONECANPAY
prevouts = '3cfe8b95d22502698fd98837f83d8d4be31ee3eddd9d1ab1a95654c64604c4d1'+'00000000'
amounts = reversebytes(field(dechex('20000'), 8))
sequences = 'ffffffff'
scriptpubkeys = serialize_script('5120f3778defe5173a9bf7169575116224f961c03c725c0e98b8da8f15df29194b80')
sha_prevouts = Digest::SHA256.hexdigest([prevouts].pack("H*")); #=> fd9703aeae8f25e8734366f3be9b5e7ac2a56772d577c598cfa6e869c698f7eb
sha_amounts = Digest::SHA256.hexdigest([amounts].pack("H*")); #=> ae9475d31b535bec000c9bfc7abc79b6a07db9eea2dd0e5066adddfb349bb53b
sha_sequences = Digest::SHA256.hexdigest([sequences].pack("H*")); #=> ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e
sha_scriptpubkeys = Digest::SHA256.hexdigest([scriptpubkeys].pack("H*")); #=> ef0ffac40687f5fc2a58c5348cd6f17469c333d0f2782769e0c3f05e97062182
outputs = '983a000000000000' + serialize_script('00140de745dc58d8e62e6f47bde30cd5804a82016f9e')
sha_outputs = Digest::SHA256.hexdigest([outputs].pack("H*")); #=> 41c602530c3cfa80923771e080db8674c730480f9764290bc5b372ae28cf8dbc
input_outpoint = '3cfe8b95d22502698fd98837f83d8d4be31ee3eddd9d1ab1a95654c64604c4d1'+'00000000'
input_amount = reversebytes(field(dechex('20000'), 8))
input_scriptpubkey = serialize_script('5120f3778defe5173a9bf7169575116224f961c03c725c0e98b8da8f15df29194b80') # always 32 bytes
input_sequence = 'ffffffff'
input_index = '00000000' # 4-byte vin of the input you're spending
sha_annex = '' # SHA256 of (compact_size(size of annex) || annex), where annex includes the mandatory 0x50 prefix
sha_single_output = ''
# common signature message
sigmsg = ''
sigmsg += hash_type_byte + version + locktime
# NOT SIGHASH_ANYONECANPAY (0x80)
if (hash_type & 0x80 == 0)
sigmsg += sha_prevouts + sha_amounts + sha_scriptpubkeys + sha_sequences
end
# NOT SIGHASH_NONE (0x02/0x82) OR SIGHASH_SINGLE (0x03/0x83)
if (hash_type & 3 < 2)
sigmsg += sha_outputs
end
sigmsg += spend_type
# NOT SIGHASH_ANYONECANPAY (0x80)
if (hash_type & 0x80 == 0)
sigmsg += input_index
# SIGHASH_ANYONECANPAY (0x80)
else
sigmsg += input_outpoint + input_amount + input_scriptpubkey + input_sequence
end
if (annex_present == 1)
annex = ''
sha_annex = Digest::SHA256.hexdigest([annex].pack("H*"))
sigmsg += sha_annex
end
# SIGHASH_SINGLE
if (hash_type & 3 == 3)
sigmsg += sha_single_output
end
# SigMsg extension (BIP 342)
if (ext_flag == 1)
# <extension> = <tap leaf hash> <key version = 0x00> <codesep position = 0xffffffff>
# <tap leaf hash> = the leaf hash of the script you're spending
# <key version> = public key version (currently 0x00)
# <codesep position> = the opcode position of the last OP_CODESEPARATOR (0xffffffff if none)
extension = leaf_hash + '00' + 'ffffffff'
sigmsg += extension
#=> 010200000000000000fd9703aeae8f25e8734366f3be9b5e7ac2a56772d577c598cfa6e869c698f7ebae9475d31b535bec000c9bfc7abc79b6a07db9eea2dd0e5066adddfb349bb53bef0ffac40687f5fc2a58c5348cd6f17469c333d0f2782769e0c3f05e97062182ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e41c602530c3cfa80923771e080db8674c730480f9764290bc5b372ae28cf8dbc0200000000858dfe26a3dd48a2c1fcee1d631f0aadf6a61135fc51f75758e945bca534ef1600ffffffff
end
# sighash
sighash = tagged_hash("TapSighash", '00' + sigmsg) # don't forget the 0x00 prefix to the sigmsg
#=> 752453d473e511a0da2097d664d69fe5eb89d8d9d00eab924b42fc0801a980c9
# ---------
# Signature
# ---------
# signing data
private_key = '9b8de5d7f20a8ebb026a82babac3aa47a008debbfde5348962b2c46520bd5189'
message = sighash
aux_rand = '0000000000000000000000000000000000000000000000000000000000000000' # all signatures are created with an all-zero (0x0000...0000) BIP340 auxiliary randomness array.
# ----
# Sign
# ----
# convert private key to an integer
d0 = int(private_key)
# make sure private key is in valid range (greater than 0 and less than the number of points on the curve)
unless (1..$n-1).include?(d0)
raise "private key must be in the range 1..n-1"
end
# calculate the public key point from the private key
public_key_point = multiply(d0) # multiply() checks for point at infinity
# negate the private key if the public key it creates doesn't have an even y value, else keep the private key the same
# note: due to the way the elliptic curve works, negate the private key will produce a public key with the same x coordinate, but the opposite y value
if public_key_point[:y] % 2 != 0
d = $n - d0
else
d = d0
end
# create a tagged hash of the auxiliary bytes
aux_rand_hash = tagged_hash("BIP0340/aux", aux_rand)
# first step toward creating the nonce is to XOR the private key with the hash of the auxiliary bytes
t = d ^ int(aux_rand_hash)
# create the nonce by hashing t (from the previous step) along with the public_key and message
k0 = int(tagged_hash("BIP0340/nonce", bytes(t) + bytes(public_key_point[:x]) + message)) % $n # public key is included in hash for "key-prefixed" schnorr signatures
# check that the nonce isn't zero
if k0 == 0
raise "nonce must not be zero (this is almost impossible, but checking anyway)"
end
# use this nonce to get a point on the curve
random_point = multiply(k0) # multiply() checks for point at infinity
# negate the nonce used to create the random point if the public key it creates doesn't have an even y value
if random_point[:y] % 2 != 0
k = $n - k0
# note: due to the way the elliptic curve works, the inverse private key will produce an even y value
else
k = k0
end
# create the challenge e value by hashing the random point with the public key and message
e = int(tagged_hash("BIP0340/challenge", bytes(random_point[:x]) + bytes(public_key_point[:x]) + message)) % $n
# r value is the x-coordinate of point R
r = random_point[:x]
# s value: (k + e*d) mod n
s = (k + e * d) % $n # this is linear (whereas s in ECDSA is non-linear)
# signature is the r and s values converted to 32-byte hexadecimal string and concatenated
sig = bytes(r) + bytes(s)
#=> 01769105cbcbdcaaee5e58cd201ba3152477fda31410df8b91b4aee2c4864c7700615efb425e002f146a39ca0a4f2924566762d9213bd33f825fad83977fba7f
# witness (signature + hash_type byte)
signature = sig + hash_type_byte
#=> 01769105cbcbdcaaee5e58cd201ba3152477fda31410df8b91b4aee2c4864c7700615efb425e002f146a39ca0a4f2924566762d9213bd33f825fad83977fba7f01
# NOTE: If the hash_type_byte is not given (i.e. the signature is 64 instead of 64 bytes), the hash_type is assumed to be 0x00
# Why permit two signature lengths? By making the most common type of hash_type implicit, a byte can often be saved.
# -------
# Witness
# -------
# <script inputs> <script> <control block>
script_inputs = signature # CAUTION: The witness only contains data pushes, unlike the scriptSig.
script = '206d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0ac' # OP_PUSHBYTES_32 6d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0 OP_CHECKSIG
control_block = control_block
witness =
compact_size(3) +
compact_size(script_inputs.length / 2) + script_inputs +
compact_size(script.length / 2) + script +
compact_size(control_block.length / 2) + control_block
puts witness
#=> 034101769105cbcbdcaaee5e58cd201ba3152477fda31410df8b91b4aee2c4864c7700615efb425e002f146a39ca0a4f2924566762d9213bd33f825fad83977fba7f0122206d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0ac21c0924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329
4. Script Path Spend (Tree)
This is another example of a script path spend, but this time using a complex script tree.
This example highlights how to construct a complex script tree, as well as the resulting merkle path required when spending to prove your chosen leaf script was part of the original tree.
For the purposes of this example, the leaf scripts are extremely basic and do not require a signature (e.g. OP_N
OP_EQUAL
).
These leaf scripts are not secure. If you use these kind of scripts in the real world your coins are likely to be stolen when you broadcast the spending transaction (which is actually what happened to me when I spent this output).
Construct
public key = 924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329 ┌────────┐ ┌────────┐ │ leaf 1 │ │ leaf 2 │ └───┬────┘ └───┬────┘ └────┬─────┘ ┌───┴────┐ ┌────────┐ │branch 1│ │ leaf 3 │ └───┬────┘ └─────┬──┘ └─────┬──────┘ ┌───┴────┐ ┌────────┐ │branch 2│ │ leaf 4 │ └───┬────┘ └────┬───┘ └────┬──────┘ ┌───┴────┐ ┌────────┐ │branch 3│ │ leaf 5 │ └───┬────┘ └────┬───┘ └─────┬─────┘ ┌───┴────┐ │branch 4│ └────────┘ leaf 1 version = c0 size(leaf 1 script) = 02 leaf 1 script = 5187 leaf 1 hash = 6b13becdaf0eee497e2f304adcfa1c0c9e84561c9989b7f2b5fc39f5f90a60f6 leaf 2 version = c0 size(leaf 2 script) = 02 leaf 2 script = 5287 leaf 2 hash = ed5af8352e2a54cce8d3ea326beb7907efa850bdfe3711cef9060c7bb5bcf59e leaf 3 version = c0 size(leaf 3 script) = 02 leaf 3 script = 5387 leaf 3 hash = 160bd30406f8d5333be044e6d2d14624470495da8a3f91242ce338599b233931 leaf 4 version = c0 size(leaf 4 script) = 02 leaf 4 script = 5487 leaf 4 hash = bf2c4bf1ca72f7b8538e9df9bdfd3ba4c305ad11587f12bbfafa00d58ad6051d leaf 5 version = c0 size(leaf 5 script) = 02 leaf 5 script = 5587 leaf 5 hash = 54962df196af2827a86f4bde3cf7d7c1a9dcb6e17f660badefbc892309bb145f branch 1 (leaf 1 + leaf 2) = 1324300a84045033ec539f60c70d582c48b9acf04150da091694d83171b44ec9 branch 2 (branch 1 + leaf 3) = beec0122bddd26f642140bcd922e0264ce1e2be5808a41ae58d82e829bc913d7 branch 3 (branch 2 + leaf 4) = a4e0d9cc12ce2f32069e98247581d5eb9ca0a4cf175771a8df2c53a93dcb0ebd branch 4 (leaf 5 + branch 3) = b5b72eea07b3e338962944a752a98772bbe1f1b6550e6fb6ab8c6e6adb152e7c merkle root = b5b72eea07b3e338962944a752a98772bbe1f1b6550e6fb6ab8c6e6adb152e7c tweak = 28dcaf275e25b339c2b8362dd0db3347fc7336602b2b52d95ffae0149038776c tweaked public key = 979cff99636da1b0e49f8711514c642f640d1f64340c3784942296368fadd0a5 scriptpubkey = 5120979cff99636da1b0e49f8711514c642f640d1f64340c3784942296368fadd0a5
TXID: ec7b0fdfeb2c115b5a4b172a3a1cf406acc2425229c540d40ec752d893aac0d7 (Output 0)
This example uses an unbalanced tree. You'd use this kind of structure when leaf 5 is more likely to be executed than the other leaves. In this example I'll actually be spending this output using leaf 3.
Don't forget that the lower leaf/branch hash goes first when combining previous leaf/branch hashes to construct the next branch hash. This applies to branch hash 4 in this example (i.e. leaf hash 5 goes before branch hash 3).
The merkle root is the final hash in the tree, which is branch 4.
Spend
script inputs = 03 script = 5387 control byte = c0 public key = 924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329 ┌────────┐ ┌────────┐ │ leaf 1 │ │ leaf 2 │ └───┬────┘ └───┬────┘ └────┬─────┘ X───┴────┐ ┌────────┐ │branch 1│ │ leaf 3 │ <- spending └───┬────┘ └─────┬──┘ └─────┬──────┘ ┌───┴────┐ X────────┐ │branch 2│ │ leaf 4 │ └───┬────┘ └────┬───┘ └────┬──────┘ ┌───┴────┐ X────────┐ │branch 3│ │ leaf 5 │ └───┬────┘ └────┬───┘ └─────┬─────┘ ┌───┴────┐ │branch 4│ └────────┘ Note: X = hashes required to construct the merkle path merkle path = 1324300a84045033ec539f60c70d582c48b9acf04150da091694d83171b44ec9bf2c4bf1ca72f7b8538e9df9bdfd3ba4c305ad11587f12bbfafa00d58ad6051d54962df196af2827a86f4bde3cf7d7c1a9dcb6e17f660badefbc892309bb145f control block = c0924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a33291324300a84045033ec539f60c70d582c48b9acf04150da091694d83171b44ec9bf2c4bf1ca72f7b8538e9df9bdfd3ba4c305ad11587f12bbfafa00d58ad6051d54962df196af2827a86f4bde3cf7d7c1a9dcb6e17f660badefbc892309bb145f witness = 03010302538781c0924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a33291324300a84045033ec539f60c70d582c48b9acf04150da091694d83171b44ec9bf2c4bf1ca72f7b8538e9df9bdfd3ba4c305ad11587f12bbfafa00d58ad6051d54962df196af2827a86f4bde3cf7d7c1a9dcb6e17f660badefbc892309bb145f
TXID: 992af7eb67f37a4dfaa64ea6f03a70c35b6063ba5ee3fe41734c3460b4006463 (Input 0)
This P2TR is being unlocked using leaf 3.
The script input is 03
, which satisfies the original leaf script of OP_3
OP_EQUAL
.
The merkle path is branch hash 1 + leaf hash 4 + leaf hash 5
. These are the hashes required (in order) to be able to reconstruct the merkle root during validation (which proves that leaf 3 was part of the original script tree). As you can see, none of the raw scripts are actually revealed inside the control block.
The resulting witness is the witness encoding of the following items: script inputs
+ leaf script
+ control block
.
Code
This code requires examples-taproot-common.rb
require_relative 'examples-taproot-common.rb' # common functions
require "digest" # library for SHA256 hash function
# =========
# Construct
# =========
# ----------
# Public Key
# ----------
internal_pubkey = '924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329'
# -----------
# Merkle Tree
# -----------
#
# ┌────────┐ ┌────────┐
# │ leaf 1 │ │ leaf 2 │
# └───┬────┘ └───┬────┘
# └────┬─────┘
# X───┴────┐ ┌────────┐
# │branch 1│ │ leaf 3 │ <- spending
# └───┬────┘ └─────┬──┘
# └─────┬──────┘
# ┌───┴────┐ X────────┐
# │branch 2│ │ leaf 4 │
# └───┬────┘ └────┬───┘
# └────┬──────┘
# ┌───┴────┐ X────────┐
# │branch 3│ │ leaf 5 │
# └───┬────┘ └────┬───┘
# └─────┬─────┘
# ┌───┴────┐
# │branch 4│
# └────────┘
#
# X = hashes required to construct the merkle path
# leaf 1
leaf_1_version = 192
leaf_1_script = '5187' # OP_1 OP_EQUAL
leaf_1_hash = tagged_hash("TapLeaf", field(dechex(leaf_1_version), 1) + serialize_script(leaf_1_script))
#=> 6b13becdaf0eee497e2f304adcfa1c0c9e84561c9989b7f2b5fc39f5f90a60f6
# leaf 2
leaf_2_version = 192
leaf_2_script = '5287' # OP_2 OP_EQUAL
leaf_2_hash = tagged_hash("TapLeaf", field(dechex(leaf_2_version), 1) + serialize_script(leaf_2_script))
#=> ed5af8352e2a54cce8d3ea326beb7907efa850bdfe3711cef9060c7bb5bcf59e
# leaf 3
leaf_3_version = 192
leaf_3_script = '5387' # OP_3 OP_EQUAL
leaf_3_hash = tagged_hash("TapLeaf", field(dechex(leaf_3_version), 1) + serialize_script(leaf_3_script))
#=> 160bd30406f8d5333be044e6d2d14624470495da8a3f91242ce338599b233931
# leaf 4
leaf_4_version = 192
leaf_4_script = '5487' # OP_4 OP_EQUAL
leaf_4_hash = tagged_hash("TapLeaf", field(dechex(leaf_4_version), 1) + serialize_script(leaf_4_script))
#=> bf2c4bf1ca72f7b8538e9df9bdfd3ba4c305ad11587f12bbfafa00d58ad6051d
# leaf 5
leaf_5_version = 192
leaf_5_script = '5587' # OP_4 OP_EQUAL
leaf_5_hash = tagged_hash("TapLeaf", field(dechex(leaf_5_version), 1) + serialize_script(leaf_5_script))
#=> 54962df196af2827a86f4bde3cf7d7c1a9dcb6e17f660badefbc892309bb145f
# branch 1: (leaf 1 + leaf 2)
branch_1_hash = tagged_hash("TapBranch", leaf_1_hash + leaf_2_hash)
#=> 1324300a84045033ec539f60c70d582c48b9acf04150da091694d83171b44ec9
# branch 2: (branch 1 + leaf 3)
branch_2_hash = tagged_hash("TapBranch", branch_1_hash + leaf_3_hash)
#=> beec0122bddd26f642140bcd922e0264ce1e2be5808a41ae58d82e829bc913d7
# branch 3: (branch 2 + leaf 4)
branch_3_hash = tagged_hash("TapBranch", branch_2_hash + leaf_4_hash)
#=> a4e0d9cc12ce2f32069e98247581d5eb9ca0a4cf175771a8df2c53a93dcb0ebd
# branch 4: (branch 3 + leaf 5)
branch_4_hash = tagged_hash("TapBranch", leaf_5_hash + branch_3_hash) # lower hash comes first
#=> b5b72eea07b3e338962944a752a98772bbe1f1b6550e6fb6ab8c6e6adb152e7c
# merkle root
merkle_root = branch_4_hash # this branch is the merkle root
# ----------------
# Tweak Public Key
# ----------------
# create a hash from the public key and merkle root - this will be used to tweak the public key
tweak = tagged_hash("TapTweak", internal_pubkey + merkle_root)
#=> 28dcaf275e25b339c2b8362dd0db3347fc7336602b2b52d95ffae0149038776c
# tweak the public key
t = int(tweak) # convert to integer (so it's like a private key) (check this is less than curve order)
internal_pubkey_point = lift_x(internal_pubkey)
tweaked_pubkey_point = add(internal_pubkey_point, multiply(t, $G)) # (check y is even)
tweaked_pubkey = bytes(tweaked_pubkey_point[:x])
#=> 979cff99636da1b0e49f8711514c642f640d1f64340c3784942296368fadd0a5
# convert to scriptpubkey
scriptpubkey = '51' + '20' + tweaked_pubkey
#=> 5120979cff99636da1b0e49f8711514c642f640d1f64340c3784942296368fadd0a5
# =====
# Spend
# =====
# unsigned raw tx:
#
# 02000000000101d7c0aa93d852c70ed440c5295242c2ac06f41c3a2a174b5a5b112cebdf0f7bec0000000000ffffffff014c1d0000000000001600140de745dc58d8e62e6f47bde30cd5804a82016f9e0000000000
# -------------
# Control Block
# -------------
# <control byte with leaf version and parity bit> <internal pubkey> <leaves/branches required to calculate merkle root>
# note: all leaf_versions are 192
control_byte = calculate_control_byte(192, tweaked_pubkey_point)
# merkle path - spending leaf 3 script (OP_3 OP_EQUAL), so need to provide the merkle path to be able to calculate the merkle root
merkle_path = branch_1_hash + leaf_4_hash + leaf_5_hash
#=> 1324300a84045033ec539f60c70d582c48b9acf04150da091694d83171b44ec9bf2c4bf1ca72f7b8538e9df9bdfd3ba4c305ad11587f12bbfafa00d58ad6051d54962df196af2827a86f4bde3cf7d7c1a9dcb6e17f660badefbc892309bb145f
# control block
control_block = control_byte + internal_pubkey + merkle_path
#=> c0924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a33291324300a84045033ec539f60c70d582c48b9acf04150da091694d83171b44ec9bf2c4bf1ca72f7b8538e9df9bdfd3ba4c305ad11587f12bbfafa00d58ad6051d54962df196af2827a86f4bde3cf7d7c1a9dcb6e17f660badefbc892309bb145f
# -------
# Witness
# -------
# <script inputs> <script> <control block>
script_inputs = '03' # CAUTION: The witness only contains data pushes, unlike the scriptSig.
script = leaf_3_script # 5387 (OP_3 OP_EQUAL)
control_block = control_block # c0924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a33291324300a84045033ec539f60c70d582c48b9acf04150da091694d83171b44ec9bf2c4bf1ca72f7b8538e9df9bdfd3ba4c305ad11587f12bbfafa00d58ad6051d54962df196af2827a86f4bde3cf7d7c1a9dcb6e17f660badefbc892309bb145f
witness =
compact_size(3) +
compact_size(script_inputs.length / 2) + script_inputs +
compact_size(script.length / 2) + script +
compact_size(control_block.length / 2) + control_block
puts witness
#=> 03010302538761c1924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329bf2c4bf1ca72f7b8538e9df9bdfd3ba4c305ad11587f12bbfafa00d58ad6051d1324300a84045033ec539f60c70d582c48b9acf04150da091694d83171b44ec9
History
When did the Taproot upgrade take place?
The idea behind Taproot was first proposed by Gregory Maxwell in 2018.
The BIPs outlining the specific technical changes to Bitcoin were organized by Pieter Wuille and released in 2020:
- BIP 340: Schnorr Signatures for secp256k1
- BIP 341: Taproot: SegWit version 1 spending rules
- BIP 342: Validation of Taproot Scripts
The Taproot upgrade was eventually activated on Bitcoin on .
Deployment
The deployment of the Taproot upgrade used version bits signalling.
- Version Bit =
2
For the upgrade to be accepted, 90% of miners had to signal for the upgrade over a 2016-block target adjustment period during the following window:
Start: | |
---|---|
End: |
If 90% of miners signalled readiness for the Taproot upgrade during any of the target adjustment periods, the upgrade would be activated at the minimum activation height of 709,632.
Signalling Results
Start Time | Target Period | Signalling Blocks | Percentage | Note |
---|---|---|---|---|
679,392 to 681,407 | 0/2016 | 0.00% | ||
681,408 to 683,423 | 823/2016 | 40.82% | First period in signalling window. | |
683,424 to 685,439 | 1690/2016 | 83.83% | ||
685,440 to 687,455 | 1983/2016 | 98.36% | Successful signalling period | |
687,456 to 689,471 | 2011/2016 | 99.75% | ||
689,472 to 691,487 | 2010/2016 | 99.70% | ||
691,488 to 693,503 | 2013/2016 | 99.85% | Last period in signalling window | |
693,504 to 695,519 | 1990/2016 | 98.71% | ||
695,520 to 697,535 | 1957/2016 | 97.07% |
This 90% threshold was exceeded for the first time on , with 98.36% (1983/2016) of blocks signalling for the Taproot upgrade.
The Taproot upgrade was then activated at block height 709,632 on .
Etymology
Why is it called "Taproot"?
The name "Taproot" was coined by Gregory Maxwell:
The idea is that the primary spending condition (key path spend) is the taproot, and all of the other possible spending conditions (script path spends) are the smaller roots branching off from the main root.
The name also stems from the fact that all the additional spending conditions converge in to a single merkle root.
Summary
The Taproot upgrade basically introduced the "ultimate" locking mechanism for bitcoins.
This new P2TR locking script provides all of the functionality of all previous locking scripts in one single locking script. In other words; if you could only have one locking script in Bitcoin, it would be P2TR.
From a technical point of view, P2TR simply combines a standard public key lock with the option of including multiple custom spending conditions alongside it. It's like a P2WPKH and P2WSH rolled in to one, with Schnorr signatures and tapscript thrown in for good measure.
It's very useful, and pretty clever.
Understanding how P2TR works is much easier if you're already familiar with merkle trees and elliptic curve cryptography. But even if you're not, implementing it in to existing code isn't too difficult; the trickiest part is not forgetting all the annoying additional bytes that need to be included along the way (e.g. the pesky sighash epoch).
But once you've got it working, P2TR is an extremely handy locking script to have in your toolbox.