§ 02 · Cryptography
Tagged hashes
SHA-256 with a label on it, so no hash can be mistaken for another
On this page 7 sections
A tagged hashTagged hash: SHA-256 with a label mixed in first, so a hash made for one purpose (say, signing) can never be confused with one made for another. Pearl uses Bitcoin’s tags unchanged. is SHA-256 with a label mixed in before the message. The label, called a tag, says what the hash is for.
Pearl hashes many different things with SHA-256: signature challenges, signing nonces, Taproot tweaks, script leaves, transaction digests. Tagging each kind with its own name means a hash made for one purpose can never be passed off as a hash made for another. This is called domain separationDomain separation: Mixing a label into a hash so that hashes made for one purpose can never be mistaken for hashes made for another. Tagged hashes and keyed BLAKE3 both do it..
Try it
Pick a tag, type a message and watch the three steps: hash the tag, write it twice, hash the lot.
How is a tagged hash built?
In one line:
-
Hash the tag
SHA-256 of the tag’s text gives 32 bytes, whatever the tag’s length.
-
Write it twice
Two copies make a 64-byte prefix.
-
Add the message and hash
Append the message and take SHA-256 of the whole thing. The result is an ordinary 32-byte hash.
A worked example: the Taproot tweak
Every normal Pearl address hides a Taproot tweak. The wallet takes your x-only public key P and computes TaggedHash(“TapTweak”, P). Here it is for the simplest possible key, private key 1, whose public key is the curve’s generator pointGenerator point: A fixed, publicly known point on an elliptic curve. Your public key is the generator point multiplied by your private key..
| Step | Value |
|---|---|
| P (x-only public key of private key 1) | 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 |
| SHA256(“TapTweak”) | e80fe1639c9ca050e3af1b39c143c63e429cbceb15d940fbb5c5a1f4af57c5e9 |
| SHA256(that ‖ that ‖ P), the tweak t | 3cf5216d476a5e637bf0da674e50ddf55c403270dd36494dfcca438132fa30e7 |
| Plain SHA256(P), for comparison | 132f39a98c31baaddba6525f5d43f2954472097fa15265f45130bfdb70e51def |
The tweak t is then added to the key (Q = P + t·G), and Q goes into the address. For private key 1 that address is prl1pmfr3p9j00pfxjh0zmgp99y8zftmd3s5pmedqhyptwy6lm87hf5sse9xfq3.
The plain SHA-256 row shows why the tag matters: without it, the tweak would be a hash that anyone could also have produced for some unrelated reason.
Why write the tag twice?
Speed. SHA-256 eats its input in 64-byte blocks. Two copies of a 32-byte tag hash fill exactly one block.
So software can feed that first block in once, save SHA-256’s internal state, and start every later hash with that tag from the saved state. The tag then costs nothing per hash. Pearl’s code does a simpler version of this: it keeps a table of each tag’s SHA-256, computed once, so it never hashes the tag text again.
The choice also makes accidents unlikely. BIP-340 points out that a 64-byte prefix made of a hash written twice is not something other protocols naturally produce, so a tagged hash won’t collide with hashes from other systems by accident.
What could go wrong without tags?
Pearl hashes many different 32- to 100-byte inputs with SHA-256: a public key for the Taproot tweak, a point, a key and a message for a signature challenge, a script for a leaf. If two of those jobs used plain SHA-256, a value computed for one job might, with a carefully chosen input, also be accepted in the other.
Nobody has shown such an attack on Bitcoin’s design. Tags simply make the question go away: with a different tag in front, each job uses what is effectively its own hash function, so a hash from one can never be valid in another. It’s cheap insurance, and BIP-340 made it the rule for everything new.
Which tags does Pearl use?
The same seven as Bitcoin, unchanged. The hash of each tag is the same 32 bytes on both chains.
| Tag | Used for | SHA256(tag) starts |
|---|---|---|
BIP0340/challenge |
the challenge e in a Schnorr signature | 7bb52d7a… |
BIP0340/aux |
mixing extra randomness into the signing nonce | f1ef4e5e… |
BIP0340/nonce |
deriving the signing nonce | 07497734… |
TapSighash |
the digest a transaction signature signs | f40a48df… |
TapLeaf |
hashing one script leaf of a Taproot or P2MR tree | aeea8fdc… |
TapBranch |
joining two branches of a script tree | 1941a1f2… |
TapTweak |
tweaking a public key into a Taproot output key | e80fe163… |
How does Pearl’s mining keep its hashes apart?
Mining doesn’t use SHA-256 tags. It uses BLAKE3, which has domain separation built in through its keyed mode: give it a different 32-byte key and it acts as a different hash function.
Pearl’s mining uses that in three ways:
- Keys. The matrix fingerprints are BLAKE3 keyed with the job keyJob key: A BLAKE3 hash of the block template (the header minus its proof commitment) and the mining settings. All of the puzzle's randomness flows from it, tying the work to one block., and the winning ticket is keyed with a noise seed, so nothing computed for one job is valid in another.
- Salts. Since block 99,000, each noise seed is bound to its matrix’s size using a key that is itself a hash of a label: BLAKE3(“pearl/cert-v3/noise-seed/A”) for matrix A, and “…/B” for B.
- Labels. The random numbers that build the noise are hashed with the label “A_tensor” or “B_tensor” in the message, so the two matrices never share noise.
The idea is the same as a tag: label every hash with its job. The details are on the noise seeds page.
Code: TaggedHash in pearld
From chainhash/hash.go (node/chaincfg/chainhash/hash.go:155-180 at v1.5.2, on GitHub), inherited from btcd:
func TaggedHash(tag []byte, msgs ...[]byte) *Hash {
// Check to see if we've already pre-computed the hash of the tag. If
// so then this'll save us an extra sha256 hash.
shaTag, ok := precomputedTags[string(tag)]
if !ok {
shaTag = sha256.Sum256(tag)
}
// h = sha256(sha256(tag) || sha256(tag) || msg)
h := sha256.New()
h.Write(shaTag[:])
h.Write(shaTag[:])
for _, msg := range msgs {
h.Write(msg)
}
// ...
} In JavaScript:
const taggedHash = (tag, msg) => {
const t = sha256(new TextEncoder().encode(tag));
return sha256(new Uint8Array([...t, ...t, ...msg]));
};