§ 08 · Mining
The puzzle
Mining one Pearl block, step by step, with every intermediate value
On this page 4 sections
Here is the whole Pearl mining puzzle in one paragraph. A miner takes a block template, multiplies two matrices of small whole numbers, and turns every little patch of the result into a lottery ticket. Before multiplying, it must add noise that nobody could predict until the block and the matrices were fixed. While multiplying, it keeps a diary of each patch’s running totals, and the ticket is a hash of that diary. A ticket wins if it’s small enough, scaled by how much work it cost. The winner is proven with a zero-knowledge certificate, and the noise is peeled off so the real product isn’t wasted.
This page walks through it step by step, first in plain words, then exactly, with every intermediate value from a small worked example you can run below.
Try it
The toy miner runs Pearl’s real steps, with the real hash functions and byte formats, on 4 × 16 and 16 × 4 matrices. Step through it in order.
What are the steps?
-
Start from a block template
The miner prepares a block templateBlock template: The block a miner is trying to win: transactions chosen, header filled in except its last field. On Pearl the template fixes the first 76 header bytes, and all the puzzle’s randomness flows from them.: version, previous block, Merkle root, time and bits, the first 76 bytes of the header. There is no nonce, and the last 32 bytes (the proof commitment) can’t be filled in yet.
-
Pick two matrices
A is m × k and B is k × n, whole numbers from −64 to 64. In the official miner they are an AI layer: the activations of at least 1,024 tokens, and the layer’s weights. The rules don’t care what they mean.
-
Fingerprint everything
BLAKE3 hashes the template and the mining settings into a 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.. Keyed with it, BLAKE3 fingerprints A (row by row) and B (column by column). Each fingerprint is the root of a Merkle tree over 1 KiB chunks, so single rows can be proven later.
-
Roll the dice for the noise
The fingerprints, salted with the matrix sizes since block 99,000, are hashed into two noise seedsNoise seed: One of two random-looking values made from the job key and fingerprints of the two matrices (and, since block 99,000, their sizes). Each seed grows into one noise matrix.. Change one byte of the template, or one entry of B, and both seeds change completely. Change one entry of A and A’s seed does, while B’s stays put.
-
Add structured noise
Each seed grows into a noise matrix the size of A or B: a thin random matrix times a ±1 selector, rankRank: A measure of how much independent information a matrix holds. A low-rank matrix can be built from two thin ones, which makes it cheap to create and to undo. 128. Added on top, A′ = A + E_A and B′ = B + E_B still fit in a byte.
-
Multiply, keeping a diary
The GPU multiplies A′·B′. For each tileTile: A small patch of cells in the output matrix: 2 × 64 cells in the official GPU miner, though miners may choose other shapes (block 130,827 used 8 × 16). Each tile of a multiplication becomes one lottery ticket. of output cells (2 × 64 in the reference miner), every 128 steps along k, it XORs the tile’s running sums into one 32-bit word and stirs it into a 64-byte diary, the transcriptTranscript: A tile's running diary: every 128 steps along the long sums, the tile's running totals are folded into a 64-byte record. The ticket is a hash of it, so it depends on the work along the way, not just the final answer..
-
Turn every tile into a ticket
When a tile is done, BLAKE3 keyed with seed A hashes its diary into a 256-bit ticketTicket: A 256-bit BLAKE3 hash of a tile's transcript. It wins a block if, read as a number, it is at most the target times the work that tile cost.. One big multiplication gives hundreds of millions of them.
-
Check the ticket
Read as a little-endian number, a ticket wins if it is at most the target times the work it cost, h·w·L multiply-accumulates. Since block 96,251 a higher noise rank also makes the bar stricter.
-
Prove it without revealing it
The winning tile’s rows and columns, with their Merkle paths, go to the pool or gateway, which turns them into a ~60 KB zero-knowledge certificate. The header’s last field commits to its public data, and the block hash is SHA256d of the finished header.
-
Peel off the noise
In the same pass, the GPU subtracts two cheap low-rank corrections and gets back A·B, the layer’s real output. (In integers the identity is exact; whether the GPU’s floating-point path is bit-for-bit exact is an open question.)
-
Why can't anyone cheat?
The noise is unknowable until everything is committed, so no product can be prepared or reused. And because the ticket depends on every intermediate running sum, the known shortcuts cost as much as the multiplication. That last part is an argued conjecture, not a proof.
What does each step compute, exactly?
The plain story has exact arithmetic behind it. Each expander below gives the rule as Pearl’s code implements it, with the toy example’s real values.
Template, settings and job key
Steps 1–3a: the 76-byte template, the 52-byte settings and the job key
The incomplete header is the header’s first 76 bytes, exactly as stored: version (u32 LE) ‖ previous block (32) ‖ Merkle root (32) ‖ time (u32 LE) ‖ bits (u32 LE). The mining configuration is 52 bytes: k (u32) ‖ rank r (u16) ‖ MMA type (u16, 0 = 7-bit integers into 32-bit sums) ‖ rows pattern (6) ‖ columns pattern (6) ‖ 32 bytes of Mixture-of-Experts trailer (zeros for dense jobs). Together they are exactly 128 bytes, two BLAKE3 blocks.
Toy values (version 1, previous block aa…aa, Merkle root bb…bb, time 1,760,000,000, bits 0x20010000; k = 16,
r = 4, a 2 × 2 tile):
header76 = 01000000 aa…aa bb…bb 0078e768 00000120
config52 = 10000000 0400 0000 000100000000 000100000000 00…00
job_key = 55e6f3546f79cb12be7462f5a03703c9115347f7b1f0c95a174a17d4414d242d On mainnet, block 130,825’s settings were k = 8,192, r = 128 and the reference 2 × 64 tile.
Fingerprints and seeds
Steps 3b–4: keyed fingerprints, then the salted seed chain
Each matrix is flattened to signed bytes, padded with zeros to a multiple of 1,024, and hashed with BLAKE3 keyed by the job key. B is flattened column by column, so a column is one contiguous run of bytes.
hash_A = BLAKE3_keyed(job_key, pad1024(A row by row)) = 94afc3a464d534521b566c3316fb2ac0b509f3d0f86482297224cee05e90d3ae
hash_B = BLAKE3_keyed(job_key, pad1024(B column by column)) = f146e7981399e075cc81811357527ee29efb43ad61868d01a701a91a5aaa3425 Since block 99,000 (certificate version 3), each fingerprint is first bound to its matrix’s size with a fixed salt, then the two seeds are chained, B’s first:
bound_A = BLAKE3_keyed(BLAKE3("pearl/cert-v3/noise-seed/A"), hash_A ‖ LE32(m) ‖ 28 zero bytes) = 43ba7eb2…2c7946bb
bound_B = BLAKE3_keyed(BLAKE3("pearl/cert-v3/noise-seed/B"), hash_B ‖ LE32(n) ‖ 28 zero bytes) = a7502e0a…920c99d2
seed_B = BLAKE3(job_key ‖ bound_B) = 05899146801117e66a811953e5aa4b480a0a37fb79f9d274f2a51a7e5683f99d
seed_A = BLAKE3(seed_B ‖ bound_A) = c40e9a24b1de3b558dc081c19b8969be5f6873009faddae1cc35361524ec9745 Why B first, and why salt? See noise seeds.
Noise and noisy matrices
Step 5: from two seeds to rank-r noise
A pseudorandom generator expands each seed: R(i) = BLAKE3_keyed(seed, 64-byte message) where the message holds the
counter 1 + i and a label, A_tensor or B_tensor. From it come four factors:
- E_AL (m × r): uniform entries
(byte & 63) − 32, in [−32, 31] (the toy uses(byte & 7) − 4). - E_AR (r × k): a selector. Each column l has a +1 in one row f and a −1 in another row s.
- E_BL (k × r) and E_BR (r × n): the same for B, from seed_B.
Each column of E_A is just one column of E_AL minus another. In the toy, E_AL’s rows are (2, 3, −2, 0), (−2, −1, 2, −4), (0, −1, −2, 3), (3, −2, 2, −3), and column 0 of the selector is (f, s) = (0, 2), so column 0 of E_A is (2 − (−2), −2 − 2, 0 − (−2), 3 − 2) = (4, −4, 2, 1). Row 0 of A, (−3, 1, 4, 4, …), becomes row 0 of A′, (1, −2, 2, −1, …).
On mainnet, noise entries fall in [−63, 63], so A′ and B′ stay within [−127, 127]: one signed byte each. The details of building and removing the noise are in rank and noise.
The diary and the ticket
Steps 6–7: XOR fold, rotate by 13, keyed BLAKE3
For one tile (rows R, columns C) the diary M is 16 words of 32 bits, all zero to start. For each slab s = 1 … ⌊k/r⌋ of r steps along k:
C_tile += A′[R, slab s] · B′[slab s, C] # r more multiply-accumulates per cell
X = XOR of all h·w running sums, as u32
M[(s−1) mod 16] = rotl(M[(s−1) mod 16], 13) XOR X then ticket = BLAKE3_keyed(seed_A, M as 64 little-endian bytes). The toy’s first tile, rows 0–1 × columns 0–1, with
r = 4 and k = 16, has four checkpoints:
| s | Steps | Running sums | X | Diary word |
|---|---|---|---|---|
| 1 | 0–3 | [[6, 1], [−66, 10]] | ffffffb3 |
M[0] = ffffffb3 |
| 2 | 4–7 | [[−37, 23], [−17, 14]] | 0000002d |
M[1] = 0000002d |
| 3 | 8–11 | [[5, 39], [−69, 5]] | ffffff9c |
M[2] = ffffff9c |
| 4 | 12–15 | [[80, 49], [−81, 37]] | ffffffeb |
M[3] = ffffffeb |
The last row is the finished top-left corner of A′·B′. The ticket is
94f9a7eec9e3ddfd2a0e76c6d05abda230c7a0c5f92ba2b030bf7d5a80b9870e. On mainnet, k = 8,192 and r = 128 give 64
checkpoints, so every diary word is stirred four times. The full story is on
tickets and target.
The check, the certificate and the header
Steps 8–9: the bound, then public data → proof commitment → block hash
The bound is the target times the tile’s work, h·w·L with L = k − (k mod r). The toy’s bits 0x20010000 mean a target
of 2²⁴⁸, and 2 × 2 × 16 = 64 = 2⁶, so the bound is 2²⁵⁴: one ticket in four wins. Read little-endian, the first ticket
is 0x0e87b980…eea7f994, below 0x4000…00. It wins. The other three tiles’ tickets (0xab22c993…,
0xd5fd4e86…, 0xd55654ba…) are above it and lose.
The winner fills the 164-byte public data: settings ‖ hash_A ‖ hash_B ‖ ticket ‖ m ‖ n ‖ tile row ‖ tile column. Then:
proof_commitment = SHA256d(03000000 ‖ public_data) = a5791938f80d02ef6eea1f050036bb0a1f68323861ae65e658a0bc5d40c9baf7
header108 = header76 ‖ proof_commitment
block hash = reverse(SHA256d(header108)) = 095c5529d6536a9c396a6392be9baf707de78920002f1ccb536c122c6a08c403 In the toy the “proof” is simply revealing A and B. On mainnet the pool or gateway turns the winning rows and columns into a 59,560-byte zero-knowledge proof. See the certificate.
Getting the real answer back
Step 10: the denoising identity
Because the noise is low-rank, its effect can be subtracted exactly:
In the toy, A′·B′ starts with the row (80, 49, 42, 56); subtracting the two corrections leaves (17, 56, 78, 62), exactly row 0 of A·B computed directly. On a GPU the corrections cost about 2r/k of the main multiplication, around 3% at k = 8,192. See useful output.
How big is the real thing?
The toy and mainnet side by side. The mainnet column is block 130,825’s certificate.
| Toy | Mainnet (block 130,825) | |
|---|---|---|
| A, B | 4 × 16, 16 × 4 | 131,072 × 8,192, 8,192 × 262,144 |
| Entries | [−4, 4] | [−64, 64] |
| Noise rank | 4 | 128 |
| Tile | 2 × 2 = 4 cells | 2 × 64 = 128 cells |
| Checkpoints per ticket | 4 | 8,192 ÷ 128 = 64 |
| Tickets per multiplication | 4 | 268,435,456 |
| Work per ticket | 64 | 1,048,576 multiply-accumulates |
| Chance a ticket wins | 1 in 4 | about 9.3 × 10⁻¹⁷ at difficulty 40,157,376 |
| Proof | reveal everything | 59,560-byte zero-knowledge proof |
At that difficulty a block takes about 1.1 × 10²² multiply-accumulates on average: roughly 40 million multiplications the size of block 130,825’s, across the whole network, every few minutes.
Make a guess
The miner changes a single entry of A, from −3 to −2. Which of these change: hash_A, seed_B, seed_A, the tickets?
Think of your answer first, then check it.
Answer hash_A, seed_A and every ticket. seed_B does not.
In the toy, hash_A becomes 05afdb65…, seed_A becomes fb0fa618…, and all four tickets change (three of the four now
win, by chance). seed_B stays 05899146…, because B’s seed is derived from the job key and B’s fingerprint only. That
asymmetry is deliberate: an AI model’s weights (B) are known in advance, so B’s noise can be prepared once per
template.
Code: the reference CPU miner's inner loop
Pearl’s own CPU miner, “naive … for completeness and testing”, runs every step in one function,
try_mine_one (zk-pow/src/ffi/mine.rs:19-128 at v1.5.2, on GitHub). It draws A and B uniformly from [−64, 64]: random
matrices, not an AI model. Its tile loop:
for ll in (rank..=k).step_by(rank) {
for (u, &a_idx) in a_rows.iter().enumerate() {
for (v, &b_idx) in b_cols.iter().enumerate() {
for l in ll - rank..ll {
jackpot_tile[u][v] += a_noised[a_idx][l] * b_noised_t[b_idx][l];
}
}
}
let xored_tile = jackpot_tile.iter().flatten().fold(0u32, |a, &x| a ^ x as u32);
let tid = (ll / rank - 1) % JACKPOT_SIZE;
jackpot[tid] = jackpot[tid].rotate_left(LROT_PER_TILE) ^ xored_tile;
}
let jackpot_hash = compute_jackpot_hash(&jackpot, a_noise_seed);
let jackpot_bound = extract_difficulty_bound(header.nbits, &config); JACKPOT_SIZE is 16 and LROT_PER_TILE is 13. The GPU miner does the same inside a fused CUDA kernel.