§ 04

§ 04 · Keys & addresses

Taproot tweak

Why the key in your address isn't quite your public key

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 tweak. 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.

Tagged hash

SHA-256 with a label baked in, so hashes for different jobs never collide

Tag

TapTweak: Taproot tweak: the message is the internal key (and a script root, if any)

  1. Hash the tag once

    t = SHA256(“TapTweak”)

    e80fe1639c9ca050e3af1b39c143c63e429cbceb15d940fbb5c5a1f4af57c5e9

  2. Put it in front of the message, twice

    t ‖ t ‖ message = 96 bytes

    e80fe1639c9ca050e3af1b39c143c63e429cbceb15d940fbb5c5a1f4af57c5e9e80fe1639c9ca050e3af1b39c143c63e429cbceb15d940fbb5c5a1f4af57c5e979be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798

    The two copies of t fill exactly one 64-byte SHA-256 block, so software can precompute it once per tag.

  3. Hash all of it once more

    tagged = SHA256(t ‖ t ‖ message)

    Tagged hash ✓ matches taggedHash()

    3cf5216d476a5e637bf0da674e50ddf55c403270dd36494dfcca438132fa30e7

Same message, other tags

The same 32 bytes give an unrelated hash under each tag, and under plain SHA-256. That is the point: a hash made for one job can never be passed off as another.

plain SHA-256
132f39a9…70e51def
TapLeaf
94333e8b…760a3135
TapBranch
a4481a6e…b12f3de0
TapSighash
87c2b87e…ca89657a
BIP0340/challenge
d75ca81e…781add6f
Runs in your browser · nothing is sent src/lib/hash.ts · BIP-340 tags, unchanged in Pearl

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 root, 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.

internal keyP+tweak pointt·G=output keyQ → prl1p…t = TaggedHash(“TapTweak”, P ‖ root)root = empty for a normal wallet address
The tweak moves the internal key P to the output key Q. Both are points on the same curve; only Q appears in the address.

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.)

  1. Start from the x-only key P

    P = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798. Its y is even, so no adjustment is needed.
  2. Hash the tag

    SHA256(“TapTweak”) = e80fe1639c9ca050e3af1b39c143c63e429cbceb15d940fbb5c5a1f4af57c5e9.
  3. Tagged hash of P

    t = SHA256(tag hash ‖ tag hash ‖ P) = 3cf5216d476a5e637bf0da674e50ddf55c403270dd36494dfcca438132fa30e7, read as a 256-bit number.
  4. Add t·G to P

    Q = P + t·G. Its x-coordinate is da4710964f7852695de2da025290e24af6d8c281de5a0b902b7135fd9fd74d21, and its y happens to be odd.
  5. 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:

TapTweak preimage, key 1 96 bytes
e80fe1639c9ca050e3af1b39c143c63e429cbceb15d940fbb5c5a1f4af57c5e9 e80fe1639c9ca050e3af1b39c143c63e429cbceb15d940fbb5c5a1f4af57c5e9 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
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
Computed with this site's library from private key 1; it matches the research vectors made with Pearl's own code.

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:

d′={d+tif d⋅G has even yn−d+totherwise(modn)d' = \begin{cases} d + t & \text{if } d \cdot G \text{ has even } y \\ n - d + t & \text{otherwise} \end{cases} \pmod n

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 TapLeaf 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 block) 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:

JavaScript
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.

The codex

Suggested

Up and down arrows move through the results, Enter opens one, Escape closes. Anywhere on the site, slash or Command K opens this search, and the square brackets [ and ] go to the previous and next page.