§ 02

§ 02 · Cryptography

Tagged hashes

SHA-256 with a label on it, so no hash can be mistaken for another

A tagged hash 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 separation.

Try it

Pick a tag, type a message and watch the three steps: hash the tag, write it twice, hash the lot.

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

How is a tagged hash built?

In one line:

TaggedHash(𝑡𝑎𝑔,m)=SHA2(SHA2(𝑡𝑎𝑔)‖SHA2(𝑡𝑎𝑔)‖m)\mathrm{TaggedHash}(\mathit{tag}, m) = \mathrm{SHA256}\big(\mathrm{SHA256}(\mathit{tag}) \,\|\, \mathrm{SHA256}(\mathit{tag}) \,\|\, m\big)
BIP-340
‖ means “followed by”. The tag is plain ASCII text, such as TapTweak.
  1. Hash the tag

    SHA-256 of the tag’s text gives 32 bytes, whatever the tag’s length.

  2. Write it twice

    Two copies make a 64-byte prefix.

  3. 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 point.

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 key, 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:

node/chaincfg/chainhash/hash.go Go
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:

JavaScript
const taggedHash = (tag, msg) => {
  const t = sha256(new TextEncoder().encode(tag));
  return sha256(new Uint8Array([...t, ...t, ...msg]));
};

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.