The secret tweak needed to unlock Taproot

Posted on 10/22/24 by Arnaud, Founding Engineer at Turnkey (follow on X)

We’ve recently leveled up support for Bitcoin on Turnkey to help customers who are building in this ecosystem. As outlined in our documentation, Turnkey has “always” supported Bitcoin because our signer enclave has supported bare Secp256k1 signing operations since day one (what we call “tier 1” or “curve-level” support1).

We now offer address derivation (“tier 2” or “address-level” support) for different types of addresses on Bitcoin. At first glance this might seem “easy”: aren’t Bitcoin addresses simply derived from Secp256k1 public keys? Turns out supporting Taproot required an entirely new signature scheme and support for cryptographic tweaks. We didn’t see this coming until a mysterious “invalid signature” error surfaced during our testing.

We’ll see why Bitcoin addresses aren’t “simply” derived from public keys: they’re data containers which contain script or public key digests, used to create locking scripts of different types. With this in mind we’ll detail the types of Bitcoin addresses supported on Turnkey: P2PKH, P2SH, P2WPKH, P2WSH, and P2TR. And we’ll take a close look at Taproot locking scripts and the tweaks they require.

Bitcoin addresses are data containers

Bitcoin is an unusual ledger. Instead of tracking balances for each account on the network, Bitcoin keeps track of individual Unspent Transaction Outputs (or “UTXOs”).

UTXOs are locked so that they can only be spent under certain conditions. This is what determines their “owner”. Bitcoin has a scripting language to define these conditions: Bitcoin Script. A UTXO’s “locking script” refers to the conditions under which it can be spent. Scripts are arbitrary, but because some spending conditions are so commonly used, script “patterns” have emerged and been standardized:

  • P2PK (for “Pay-to-Public-Key”): requires a signature from a given public key
  • P2PKH (for “Pay-to-Public-Key-Hash”): requires a public key matching a given hash, plus a valid signature from that key
  • P2SH (for “Pay-to-Script-Hash”): requires another script which matches a given hash, and a successful execution of that script
  • And more2

Note that we have not yet talked about addresses! How are Bitcoin addresses used then? In short, they’re a convenient way for a recipient to communicate enough information to a sender so that they can lock UTXOs correctly (which is to say: send them coins!). Taking the 3 examples from above:

  • Constructing a P2PK locking script requires the recipient’s bare Secp256k1 public key. This doesn’t require a Bitcoin address! Your public key is your address.
  • Constructing a P2PKH locking script requires the recipient’s public key hash. The hash function used by Bitcoin is HASH1603.
  • Constructing a P2SH locking script requires a hash of the Bitcoin script. HASH160 is also used to hash scripts.

A recipient could hash their public key or script, and send the raw hash to the sender. That would be sufficient for the sender to lock UTXOs for the recipient. Bitcoin addresses standardize information exchange between recipients and senders, with a few bonuses:

  • Bitcoin addresses are checksummed to make simple typos detectable.
  • Bitcoin addresses are encoded using a friendly set of characters to avoid confusion between similar-looking glyphs. For example: zero (“0”) and capital o (“O”) look alike in most fonts, so they’re both excluded from the character set.
  • Bitcoin addresses are prefixed to avoid confusion about the network and type of data they contain (otherwise, if I send you a hash with no other information, how can you tell if it’s supposed to be a script hash or a public key hash?)

Below is a sample P2PKH Bitcoin address:

Bitcoin address bytes, visualized

Each square represents a single byte. The prefix is a single byte indicating the network and address type (see a list of prefixes here). The data is a public key hash, and the checksum is added as the last 4 bytes. You can play with this page to see how addresses get generated and encoded in more detail4.

Restating the important ideas:

  • Bitcoin addresses are typed, checksummed, data containers. Addresses contain public key hashes or script hashes.
  • Bitcoin addresses are used by recipients to communicate to senders how they’d like their Bitcoins locked.
  • Senders use Bitcoin addresses to construct locking scripts and lock UTXOs: addresses dictate the type of locking script, as well as their contents.

Address types supported on Turnkey

Turnkey operates secure enclaves to generate key pairs and sign with them. What does it mean for Turnkey to provide “address-level” support for Bitcoin?

  • Turnkey must generate Bitcoin addresses from Secp256k1 key pairs. This enables senders to lock UTXOs using the data contained in these addresses.
  • Turnkey must be able to unlock the UTXOs by signing with these key pairs! Otherwise, users won’t be able to move the coins sent to them.

Let’s break down address types available on Turnkey5 and the data they hold. All of these addresses are meant to generate locking scripts which can be unlocked by a single key pair: the one that’s held in Turnkey’s secure enclaves and controlled by customers’ authenticators.

BITCOIN_MAINNET_P2PKH

Starts with 1…
Example 147RGvTvjoE39fRfXfNYXCaHPuCzBrv3H5
Address data A public key hash.
Locking script Locks the UTXO to the public key hash contained in the address.
How to unlock Present a public key which hashes to the public key hash, along with a valid signature.

BITCOIN_MAINNET_P2SH

Starts with 3…
Example 3NR5vHeGkz5AjNYn2YRkJZiVt9fsakXfUT
Address data A script hash.
Locking script Locks the UTXO to the script hash contained in the address.
This script hash is actually a witness script hash instead of a normal Bitcoin script hash. The witness script used in Turnkey's P2SH addresses contains the hash of the public key. This is known as "wrapped SegWit6" or "P2SH-P2WPKH" because the witness script is wrapped inside of a standard P2SH locking script.
How to unlock Present the witness script which matches the hash, and execute it successfully by providing a valid signature in witness data. The public key needs to match the public key in the wrapped P2WPKH witness script.
6

BITCOIN_MAINNET_P2WPKH

Starts with bc1…
Example bc1qyg0j9mm2t7ltlnadqj906rpyr6rrc7dwf8cgdh
(note the bech32 encoding7)
Address data A public key hash.
Locking script Witness script referencing the public key hash contained in the address.
How to unlock Present the public key which matches the hash, and a valid signature in witness data.
7

BITCOIN_MAINNET_P2WSH

Starts with bc1…
Example bc1qfq886cd5z9z5sucxm0dqaqklpe4yeyjl89g6mpjm3muny8gn9z0q8akpnf
Address data A script hash.
Digests are 32 bytes instead of 20 bytes8, hence the difference in length between P2WSH and P2WPKH addresses.
Locking script Witness script referencing the script hash from the address data. The digest's pre-image, for Turnkey's P2WSH addresses, is the same witness script that is used in P2WPKH addresses. We simply reference its digest instead of revealing it directly in the locking script, which is good for privacy.
How to unlock Present the witness script (P2WPKH), the associated public key, and a valid signature from that public key.
8

BITCOIN_MAINNET_P2TR

Starts with bc1…
Example bc1pn9fkfwztdq7wxhct5ac0r44dcumtv2064lfufgfmvlwn7d5t73mqtvxsut
(note: these addresses use bech32m9 for encoding)
Address data A witness script hash.
Note: all P2TR addresses contain witness script hashes. No longer do we have to distinguish between addresses containing script or public key digests: they're all the same!
Locking script Witness script referencing the digest from the address data. The pre-image is a v1 witness script which contains a hash of the tweaked public key. This is similar to a P2WPKH script, except for the fact that the public key is tweaked before being hashed (more on this in the following section!)
How to unlock Present the witness script (P2WPKH), the tweaked public key, and a valid signature from this public key. This signature has to be a Schnorr signature.
9

Taproot tweaks: a click deeper

Now that we’ve detailed every address type supported on Turnkey, let’s dive into P2TR in particular. What makes Taproot special? Tweaks!

A cryptographic tweak is a straightforward modification to a public key. For a point $P$ and a scalar10 $t$, the “tweaked” point is $Q = P + t*G$, where $G$ is the group generator11 and $t$ is the “tweak”.

In Taproot, tweaked points are used to “commit” to scripts. Taproot scripts are arranged in Merkle trees, which means committing to the root node of the script tree is sufficient (BIP341 explains this in detail if you’re curious). In order to commit to a Merkle root with hash $m$, the tweaked public key will be:

$$ Q = P + hash(xbytes(P)\ ||\ m)*G $$

(where $||$ is byte concatenation and $xbytes(P)$ is the byte representation of $p$’s x coordinate)

Turnkey addresses do not use complex Taproot scripts, and thus do not need to commit to a Merkle root. When there is no Merkle root hash $m$ the tweaked public key simplifies to:

$$ Q = P + hash(xbytes(P))*G $$

This is how Turnkey handles Taproot (P2TR) outputs. Remember from the previous section:

  • Q (not P!) will be used inside of a witness script, and this witness script is hashed and referenced in P2TR locking scripts.
  • To unlock P2TR UTXOs we need to present this witness script, the public key Q, and a signature made with Q.

Do you see the issue? If we produce a signature with our original (un-tweaked) key P, the signature is not valid and we cannot unlock P2TR UTXOs. This might come as a surprise and frankly, is a bit of an abstraction violation: locking scripts derived from a key P require a signature by a different key $Q$ to be unlocked! The secret key for $Q$ can be computed from $P$’s secret key with a trivial addition: $q = p + t$ 12.

However: this addition isn’t possible if the key is stored on a device or environment which doesn’t support extracting and manipulating secret keys arbitrarily!

To implementers out there: proceed very carefully. If you derive P2TR addresses for your users without supporting cryptographic tweaks, they will lock coins irreversibly, because there is no way to unlock P2TR outputs with the original un-tweaked key.

To Turnkey users: we got your back! Turnkey’s signer enclave adds the right tweak to the secret key when signing for a Bitcoin P2TR address13. Everything will just work.

Conclusion

We’ve learned a lot while implementing Bitcoin address derivation at Turnkey, and we didn’t expect to! Classic rabbit-hole story. The most important takeaways are:

  • Bitcoin addresses are not merely derived from public keys. Bitcoin addresses are typed, checksummed data containers. They can contain public key digests or script digests.
  • Bitcoin addresses are meant to be used by senders to generate locking scripts. It’s the receiver’s responsibility to unlock them (and spend them as they see fit!)
  • Turnkey supports all of the most common Bitcoin address types: P2PKH, P2SH, P2WPKH, P2WSH and P2TR addresses.
  • Supporting P2TR addresses forced us to modify our core signer enclave: we implemented Schnorr signatures, and had to support tweaking secret keys to unlock P2TR outputs. We didn’t see this coming until a mysterious “invalid signature” error happened during our testnet trials.
  • Offering address generation is a promise that cryptocurrency can be received at these addresses and also spent from them. If you’re an implementer: don’t forget to test both sides. Otherwise you put your users’ funds at risk of being stuck or lost.

Additional Resources

  • LearnMeABitcoin is a fantastic resource for technical education on Bitcoin. The detailed pages about different types of locking scripts were a really handy reference while writing this blog post. For example: P2PKH locking scripts.
  • Bitcoin Improvement Proposals (“BIPs”) are also very well written. They are technical and dense, but do a good job of explaining high-level motivations and outlining the main ideas in the first few sections. For example I’d highly recommend reading the “Design” section of BIP341 to understand Taproot. It’s only a few paragraphs long, well worth your time.

Acknowledgements

Thanks to Dr. Adam Everspaugh, Raheel Ahmed, Zane Kharitonov, Michael Avrukin, Sarah Lu, and Hannah Arnold for reading drafts of this post and providing valuable comments, feedback, and suggestions!


  1. See our documentation for more information on our approach to asset support and definition of the 4 tiers we currently use: https://docs.turnkey.com/documentation/ecosystem-integrations/ ↩︎

  2. There are more locking script patterns than these 3 (like P2MS for multisig), but I’m omitting these for brevity. ↩︎

  3. HASH160 isn’t a new hash function: it’s the composition of RIPEMD160 and SHA256. HASH160(publicKey) = RIPEMD160(SHA256(publicKey)) ↩︎

  4. If you want to obtain the exact same address, you can input the private key fae29d3daa860b9a0ba5dac8dbd2be53fd4d5b67e5d975a2666fe6687f3d9dcb↩︎

  5. For brevity I’m only listing mainnet types. We also support Testnet, Regnet and Signet variants. See our documentation for more information. ↩︎

  6. The SegWit upgrade brought a major change to how Bitcoin transactions are constructed: signatures and unlocking scripts are not part of the transaction hash anymore. This solves malleability issues and makes transactions cheaper, among other things. ↩︎

  7. Bech32 works with 32 characters: numbers (0-9 except “1”) and 22 letters (a-z except “b”, “i”, and “o”). This encoding is defined in BIP173↩︎

  8. The difference in length is because different hash functions are used. For all other address data, HASH160 digests (20 bytes) are used. But for witness script hashes, SHA256 digests (32 bytes) are used. ↩︎

  9. Bech32m is identical to bech32 except for an improved checksum (see BIP350 for details) ↩︎

  10. “Scalar” is a fancy mathematical word for “integer value”. Think “1”, “2”, or “99389234590”. ↩︎

  11. The generator for Secp256k1 is defined as the point 0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 (in compressed form) ↩︎

  12. $q$ is the secret key for $Q$ ($Q = q*G$), $p$ is the secret key for $P$ ($P = p*G$), and $t$ is our tweak scalar. ↩︎

  13. This is something that might change in the future. Right now we’re assuming that the tweak scalar is always hash(xbytes(P)). If we wanted to support arbitrary tweaks to let customers create addresses which commit to arbitrary Merkle roots we would have to add metadata to addresses, or support passing tweak values at signature time. ↩︎