§ 04 · Keys & addresses
Taproot tweak
Why the key in your address isn't quite your public key
On this page 5 sections
Your Taproot address doesn’t contain your public key. It contains a slightly shifted version of it, and that small shift is what makes Taproot work.
The shift is called the Taproot tweakTaproot tweak: The step that shifts your public key to the key inside your address, by an amount set by a hash of the key (and of any script tree). Same as Bitcoin’s Taproot (BIP-341, and BIP-86 for key-only addresses).. Your wallet hashes your public key (together with any backup scripts), turns the hash into a number t, and moves the key by t steps along the curve. The result Q is still a perfectly ordinary public key. But it now quietly commits to whatever went into the hash.
Try it
A tagged hash is the first ingredient. Pick the TapTweak tag and hash a key.
Why tweak a key at all?
Because a Taproot output can be spent two ways, and the key has to commit to both.
- The key path: one signature from the key. That’s how Pearl wallets spend almost everything.
- The script path: reveal one of several scripts that were committed in advance, and satisfy it.
The tweak binds the scripts into the key. If there are scripts, the hash includes their Merkle rootMerkle root: One 32-byte hash that sums up every transaction in a block, made by hashing them in pairs up a Merkle tree. It sits in the header, so changing any transaction changes the block., so nobody can invent a new script later. If there are none, the hash includes nothing extra, which proves there’s no hidden script. That second case is BIP-86, and it’s what every default Pearl address uses.
How is the tweak computed?
Here is the whole calculation for private key 1. Its public key is the generator point itself, which keeps the numbers recognizable. (Key 1 is public: never send coins to its address.)
-
Start from the x-only key P
P =79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798. Its y is even, so no adjustment is needed. -
Hash the tag
SHA256(“TapTweak”) =e80fe1639c9ca050e3af1b39c143c63e429cbceb15d940fbb5c5a1f4af57c5e9. -
Tagged hash of P
t = SHA256(tag hash ‖ tag hash ‖ P) =3cf5216d476a5e637bf0da674e50ddf55c403270dd36494dfcca438132fa30e7, read as a 256-bit number. -
Add t·G to P
Q = P + t·G. Its x-coordinate isda4710964f7852695de2da025290e24af6d8c281de5a0b902b7135fd9fd74d21, and its y happens to be odd. -
Write Q as an address
Version 1, program Q, prefix prl:prl1pmfr3p9j00pfxjh0zmgp99y8zftmd3s5pmedqhyptwy6lm87hf5sse9xfq3.
The 96 bytes that go into the final SHA-256 of step 3:
| Field | Bytes | Value |
|---|---|---|
| SHA256("TapTweak") | 32 @0 | the tag hash |
| SHA256("TapTweak") again | 32 @32 | the tag hash, repeated |
| P (x-only) | 32 @64 | internal key; no root follows |
Prefixing the tag’s hash keeps a TapTweak hash from ever colliding with any other kind of hash; writing it twice fills exactly one 64-byte SHA-256 block, which software can precompute. The tagged hashes page explains the trick.
Which private key signs for Q?
The tweaked one. If P = d·G and Q = P + t·G, then Q = (d + t)·G, so d + t is the private key for Q.
One wrinkle: x-only keys always mean the point with even y. If d·G has odd y, the wallet uses n − d instead (same x, even y), then adds t:
For key 1, P has even y, so d’ = 1 + t:
3cf5216d476a5e637bf0da674e50ddf55c403270dd36494dfcca438132fa30e8
That’s t with its last digit bumped from 7 to 8. It’s the key behind every signature that spends from key 1’s address.
What if there are scripts?
Then root is the Merkle root of the script tree, built from TapLeafTapLeaf hash: The tagged hash of one script in a Taproot or P2MR script tree: TaggedHash("TapLeaf", leaf version ‖ length ‖ script). With a single script, it is the Merkle root. hashes. Take the same internal key G and commit to one leaf, <G> OP_CHECKSIG (bytes 20 79be…1798 ac):
| Step | Value |
|---|---|
| Leaf hash = root (one leaf) | 763e9da064b9dc0471fb0f3c8fa2c84b4b84d2ca992497c12d2274386795aa8e |
| t = TaggedHash(“TapTweak”, P ‖ root) | f1028f19b59a9cf0e26b17002188b8c05a2217d12768af23c5539600a089fbff |
| Q (x-only, odd y) | 2d2f7b2a700ab270dd515ef7a02f25c5a344066641fcac563e1b53eca4a456d3 |
| Address | prl1p95hhk2nsp2e8ph23tmm6qte9ck35gpnxg872c437rdf7ef9y2mfsnx7flw |
From outside, this address looks exactly like a key-only one. Only a spender who reveals the leaf, P and the Merkle path (the control blockControl block: The last item of a script-path witness: a few bytes proving that the script being run really belongs to the output's script tree.) shows that a script was there. The node then redoes the tweak and checks it lands on Q. The P2TR page walks through that check.
Pearl’s quantum-safe addresses are tweaks too
When you ask Oyster for getnewaddress … pq=true, it builds one script leaf, 40 <64-byte XMSS public key> de (push 64 bytes, then OP_CHECKXMSSSIG), and tweaks the normal BIP-86 key with that leaf’s hash.
The result is an ordinary prl1p… Taproot address, not a P2MR one. The Schnorr key path still works, and anyone who could break Schnorr could use it too, so the XMSS leaf is an emergency backup, not full protection. It’s opt-in: the desktop wallet never asks for it, and change addresses never include it.
Code: the tweak in JavaScript
With @noble/curves (version 2), the BIP-86 tweak is a few lines:
import { secp256k1 } from '@noble/curves/secp256k1.js';
import { sha256 } from '@noble/hashes/sha2.js';
const hex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
const tag = sha256(new TextEncoder().encode('TapTweak'));
const taggedHash = (msg) => sha256(new Uint8Array([...tag, ...tag, ...msg]));
function bip86OutputKey(xonly /* Uint8Array(32) */) {
const P = secp256k1.Point.fromHex('02' + hex(xonly)); // x-only means the even-y point
const t = BigInt('0x' + hex(taggedHash(xonly)));
const Q = P.add(secp256k1.Point.BASE.multiply(t));
return Q.toBytes(true).slice(1); // x-only Q: the witness program
}
// bip86OutputKey(G.x) → da4710964f7852695de2da025290e24af6d8c281de5a0b902b7135fd9fd74d21 The same function, given an extra 32-byte root appended to xonly inside the hash, gives the script-tree version.