§ 08

§ 08 · Mining

Noise seeds

Commit first, then roll the dice

If a miner could choose its own noise, or even just know it in advance, the noise would be useless. It could multiply ahead of time, reuse yesterday’s work, or pick matrices that happen to make the arithmetic easy.

So Pearl makes the noise out of hashes of everything the miner commits to: the block it’s mining, both matrices, and (since block 99,000) their sizes. The rule is simple: commit first, then roll the dice. Only once every input is fixed does the miner learn its noise, and changing any input re-rolls the noise built from it.

This page follows the hashes from the template to the two noise seeds. How a seed grows into a noise matrix is on rank and noise.

Try it

Hash anything with BLAKE3, then change one character. Every seed on this page is built from hashes like these, so one changed input gives a completely different seed.

Hash playground

Change one letter and the whole fingerprint changes

BLAKE3 Mining tickets and matrix commitments

64c73be7ea8de1a1691ea7f2233e2673e195c7ffc51b9789d234d4b2eabcd1c0

Avalanche BLAKE3 · 256 bits

120 of 256 bits changed

compared with “pearl”

Runs in your browser · nothing is sent src/lib/hash.ts · raw output order

What is the job key?

The job key is the first hash in the chain:

job_key=BLAKE3⁡(header76‖config52)\text{job\_key} = \operatorname{BLAKE3}(\text{header}_{76} \,\|\, \text{config}_{52})
  • header₇₆ is the template’s version, previous block, Merkle root, time and bits: the header minus its last field, the proof commitment, which can’t exist yet. These are exactly the header’s first 76 bytes, as stored.
  • config₅₂ is the mining settings: k, the noise rank, the multiply type, the tile’s row and column patterns, and 32 bytes reserved for Mixture-of-Experts jobs. For block 130,825 they were 00200000 8000 0000 070100000000 0001031f0000 and 32 zero bytes: k = 8,192, rank 128, a 2 × 64 tile.

The two make 128 bytes, exactly two BLAKE3 blocks. Because the template includes the previous block and the Merkle root, the job key, and everything after it, is tied to one parent and one set of transactions.

How are the matrices fingerprinted?

With BLAKE3, keyed by the job key:

hashA=BLAKE3job_key⁡(pad1024⁡(A row by row)),hashB=BLAKE3job_key⁡(pad1024⁡(B column by column))\text{hash}_A = \operatorname{BLAKE3}_{\text{job\_key}}\big(\operatorname{pad}_{1024}(A \text{ row by row})\big), \quad \text{hash}_B = \operatorname{BLAKE3}_{\text{job\_key}}\big(\operatorname{pad}_{1024}(B \text{ column by column})\big)

Each entry is one signed byte. The bytes are zero-padded to a multiple of 1,024, then hashed. Three details make this the right tool:

  • It’s a Merkle tree. BLAKE3 is built as a tree over 1 KiB chunks, so its output is a Merkle root. Pearl’s pearl-blake3 library is standard BLAKE3 that exposes that tree, so a miner can later prove “these rows were part of A” with a few sibling hashes. See Merkle trees.
  • B goes column by column, so one column of B is one contiguous run of bytes. A winning tile needs a few rows of A and a few columns of B; with this layout both are cheap to open.
  • It’s keyed. The INT whitepaper’s reason: “to forbid preparing matrices with particularly advantageous hashes”. The key isn’t known until the template is, so no fingerprint can be computed in advance.

At mainnet sizes these are big trees. Block 130,825’s A is 131,072 × 8,192 bytes, exactly 1 GiB, or 1,048,576 chunks; its B is 2 GiB. One row of A is 8,192 bytes, eight chunks.

How do fingerprints become seeds?

Through a short chain of hashes. Since block 99,000 (certificate version 3) it starts by salting each fingerprint with its matrix’s size:

template 76 B‖ settings 52 Bjob keyBLAKE3Arow by rowBcolumn by columnhash_AMerkle roothash_BMerkle rootbound_Asalt ‖ mbound_Bsalt ‖ nseed_Anoise A · ticketsseed_Bnoise Bkeyjob key also feeds seed_Bseed_B = BLAKE3(job key ‖ bound_B) · seed_A = BLAKE3(seed_B ‖ bound_A)
The V3 seed chain. Each matrix is fingerprinted with the job key as the key, bound to its size with a fixed salt, then chained: B's seed first, A's from B's. seed_A also keys every ticket.
boundA=BLAKE3SALTA⁡(hashA‖LE32⁡(m)‖028)boundB=BLAKE3SALTB⁡(hashB‖LE32⁡(n)‖028)seedB=BLAKE3⁡(job_key‖boundB)seedA=BLAKE3⁡(seedB‖boundA)\begin{aligned} \text{bound}_A &= \operatorname{BLAKE3}_{\text{SALT}_A}\big(\text{hash}_A \,\|\, \operatorname{LE32}(m) \,\|\, 0^{28}\big) \\ \text{bound}_B &= \operatorname{BLAKE3}_{\text{SALT}_B}\big(\text{hash}_B \,\|\, \operatorname{LE32}(n) \,\|\, 0^{28}\big) \\ \text{seed}_B &= \operatorname{BLAKE3}(\text{job\_key} \,\|\, \text{bound}_B) \\ \text{seed}_A &= \operatorname{BLAKE3}(\text{seed}_B \,\|\, \text{bound}_A) \end{aligned}
SALT_A and SALT_B are BLAKE3 of the strings 'pearl/cert-v3/noise-seed/A' and '…/B', hard-coded so consensus doesn't depend on hashing strings at run time.

Before block 99,000, the chain was the same without the salting step: hash_B and hash_A went straight in. The certificate still carries the raw fingerprints; a verifier redoes the salting itself.

Every value in the toy example

The toy’s job key and fingerprints (see the puzzle for its inputs), with m = n = 4:

job_key = 55e6f3546f79cb12be7462f5a03703c9115347f7b1f0c95a174a17d4414d242d
hash_A  = 94afc3a464d534521b566c3316fb2ac0b509f3d0f86482297224cee05e90d3ae
hash_B  = f146e7981399e075cc81811357527ee29efb43ad61868d01a701a91a5aaa3425
bound_A = 43ba7eb2c84bdb8873c6d2f42ed677d1002be5d88f2311f5c05ecf172c7946bb
bound_B = a7502e0ac5dcb82b9d1d52a49d32b66c59a2d334f8cd258b4f909027920c99d2
seed_B  = 05899146801117e66a811953e5aa4b480a0a37fb79f9d274f2a51a7e5683f99d
seed_A  = c40e9a24b1de3b558dc081c19b8969be5f6873009faddae1cc35361524ec9745

Under the pre-99,000 chain, the same inputs give seed_B 65bc5b5c…24f2c56f and seed_A dc044396…b1bbd0b5: a completely different puzzle from identical matrices.

The real seeds of block 130,827

Everything the chain needs is public, so anyone can recompute a real block’s seeds, exactly as every node does when it verifies the certificate. For block 130,827: the job key hashes the header’s first 76 bytes and the first 52 bytes of the public data (k = 8,192, rank 128, the 8 × 16 pattern); the roots and sizes come from the rest of the public data (m = n = 131,072). Computed for this site with Pearl’s algorithm:

job_key = 516c20b3a46107dbfda61d04cceeb02c0dbb35f6441df41002c9dc2742068226
hash_A  = 9e1adb4617d7d05111fe2a7e074f81738df4e2181b966ac48f66289b1cab5d4a   (from the public data)
hash_B  = d1b42f82cfe36192a36ed37be30a6f9f75e7bca372449aec615e7d0721dc06ef   (from the public data)
bound_A = d9071a7ce83e040bb1cb69b34ee765f9a14f77b85b57d50ef9e096f9f6461d5e
bound_B = ba172153ac1c7988b637485dcbcea7ea8c27119baa19846218587c81e896fe5a
seed_B  = 595effab3824d2567c3d9a6ab3c5c262cd7e7051515dcb99611c89d226644148
seed_A  = 530a151781ce8cac6332fc92e16591ffa57bd84e766565334e65d8898632c16e

seed_A is also the key of that block’s winning ticket. The miner couldn’t know any of these values until it had fixed its template and both matrices.

Why does B come first?

Look at the chain: seed_B depends only on the job key and B. seed_A depends on seed_B and A, so on everything.

That asymmetry is deliberate. In AI inference, B is usually a model’s weights, fixed and known in advance, while A, the activations, changes with every batch. The INT whitepaper’s words: “the matrix B is known in advance … allowing the optimization of pre-noising B once per σ update”, where σ is the template. So a miner can noise its weights once per template and reuse them across many batches, while every batch of activations still gets its own fresh noise for A.

Make a guess

In the toy, one entry of A changes from −3 to −2. What happens to seed_B and seed_A?

Think of your answer first, then check it.

Answer seed_B stays 05899146…; seed_A becomes fb0fa618….

hash_A changes completely (05afdb65…), so bound_A and seed_A do too, and with them A’s noise and every ticket. B’s fingerprint, bound_B and seed_B are untouched, so B’s noise is too.

Why salt the seeds with the sizes?

To close a hole. Under the old chain, the noise seed didn’t depend on the matrix shape a miner claimed. The same committed tree of bytes could be presented under different claimed sizes, and every reading got the same seed. Pearl’s code describes the danger: without pinning the shape, “a miner could open one committed tree under several dimension interpretations”. That’s work amplification: more tickets than the work justified.

The salted noise-seed hard fork at block 99,000 fixed it in two parts:

  • Each reading gets its own noise. m and n are hashed into bound_A and bound_B, so every (data, m, n) combination has its own seeds.
  • The declared shape must match the tree. A share must declare Merkle trees with exactly the number of 1 KiB leaves that m·k and n·k imply. A follow-up fix closed an integer wrap-around in that check that could have reopened the same class of attack.

Because the noise changed, miners had to upgrade at the fork, not just nodes, and certificates switched to version 3. The upgrade guide is in Pearl’s repo. The exact exploit, reported in a private security advisory, has not been published.

Why can’t the miner choose its noise?

Because by the time the noise exists, there’s nothing left to choose:

  • Everything is committed first. The seeds are hashes of the template, the settings, both fingerprints and the sizes. Only after fixing all of them does the miner learn the noise.
  • Any change re-rolls the dice. Change a transaction, the time, the tile pattern or an entry of B and both seeds come out unrelated, by BLAKE3’s avalanche effect. Change an entry of A and seed_A (with A’s noise and every ticket) comes out unrelated, while seed_B stays put. There’s no small adjustment that nudges the noise.
  • No head start. The job key depends on the previous block, which nobody knows until it’s found. Work can’t begin before then, and a product can’t be reused afterwards.

The verifier redoes all of this. From the certificate’s public data and the header, a node recomputes the job key, the salted seeds and the noise for the winning tile’s rows and columns. The noise is never proven inside the zero-knowledge proof; both sides compute it in the open, and it takes about two-thirds of that plain-arithmetic part of verification.

Code: binding and chaining

From seed.rs (zk-pow/src/api/seed.rs:11-59 at v1.5.2, on GitHub):

zk-pow/src/api/seed.rs Rust
/// `root || dim(u32 LE) || 28 zero bytes` — a single 64-byte BLAKE3 block.
fn bind_message(root: &Hash256, dim: u32) -> [u8; 64] {
    let mut msg = [0u8; 64];
    msg[..32].copy_from_slice(root);
    msg[32..36].copy_from_slice(&dim.to_le_bytes());
    msg
}

pub fn bind_root_a(hash_a: &Hash256, m: u32) -> Hash256 {
    blake3_digest(&bind_message(hash_a, m), Some(SEED_SALT_A))
}

And the chain, from proof_utils.rs (zk-pow/src/api/proof_utils.rs:348-372 at v1.5.2, on GitHub) (Pearl’s code calls the seeds “commitment hashes”):

zk-pow/src/api/proof_utils.rs Rust
let (hash_a, hash_b) = self.seed_derivation.bind_roots(&self.hash_a, &self.hash_b, self.m, self.n);
let b_noise_seed = blake3_digest(&[&job_key[..], &hash_b[..]].concat(), None);
let a_noise_seed = blake3_digest(&[&b_noise_seed[..], &hash_activations[..]].concat(), None);

For a dense job hash_activations is just the bound hash_A. Under the legacy derivation, bind_roots returns the raw roots unchanged.

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.