§ 08

§ 08 · Mining

The puzzle

Mining one Pearl block, step by step, with every intermediate value

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.

Toy miner

Pearl’s real mining steps, one at a time, on tiny matrices

Toy sizes Real jobs are thousands of rows wide, with rank-128 noise and odds near 1 in 10¹⁶ per ticket. The steps, hash functions and byte layouts here are mainnet’s.

time1,760,000,000 job key55e6f354 hash_A94afc3a4 hash_Bf146e798 seed_B05899146 seed_Ac40e9a24 tickets 94f9a7 cc31a8 325a4b d5cd77

Step 1 of 11

Start from a block template

A miner starts from a block template: the first 76 bytes of the header. Version, the previous block’s hash, the Merkle root of the transactions, the time and the difficulty bits. There’s no nonce. The last 32 bytes, the proof commitment, can only be filled in once a ticket wins.

Change the time and watch every fingerprint above change.

Real Pearl The template comes from pearld (getblocktemplate) with real hashes; the toy uses aa…aa and bb…bb. Same 76-byte layout, with both hashes byte-reversed.

Runs in your browser · toy parameters m × k × n = 4 × 16 × 4, r = 4 src/lib/miner.ts · runToyJob

What are the steps?

1 template76 B + settings 52 B2 matricesA m×k · B k×n3 job key+ fingerprintshash_A · hash_B4 seedssalted with m, nseed_B → seed_A5 noiserank 128added to A and B6 multiply A′·B′diary per tile,every 128 steps7 ticketsBLAKE3_seedA(diary)8 ticket ≤target × h·w·L ?9 certificatecommitment →header → block hash10 denoiseA·B, the outputalmost all lose:next tile, next job
The pipeline. Mining reads the template's first 76 bytes; the matrices are committed before any noise exists; each tile of the noisy product becomes a ticket; the winner becomes the certificate, and the noise is removed to recover A·B.
  1. Start from a block template

    The miner prepares a block template: 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.

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

  3. Fingerprint everything

    BLAKE3 hashes the template and the mining settings into a job key. 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.

  4. Roll the dice for the noise

    The fingerprints, salted with the matrix sizes since block 99,000, are hashed into two noise seeds. 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.

  5. Add structured noise

    Each seed grows into a noise matrix the size of A or B: a thin random matrix times a ±1 selector, rank 128. Added on top, A′ = A + E_A and B′ = B + E_B still fit in a byte.

  6. Multiply, keeping a diary

    The GPU multiplies A′·B′. For each tile 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 transcript.

  7. Turn every tile into a ticket

    When a tile is done, BLAKE3 keyed with seed A hashes its diary into a 256-bit ticket. One big multiplication gives hundreds of millions of them.

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

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

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

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

job_key=BLAKE3⁡(header76‖config52)\text{job\_key} = \operatorname{BLAKE3}(\text{header}_{76} \,\|\, \text{config}_{52})

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.
EA=EAL⋅EAR,EB=EBL⋅EBR,A′=A+EA,B′=B+EBE_A = E_{AL} \cdot E_{AR}, \qquad E_B = E_{BL} \cdot E_{BR}, \qquad A' = A + E_A, \quad B' = B + E_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:

A⋅B=A′⋅B′−(A⋅EBL)⋅EBR−EAL⋅(EAR⋅B′)A \cdot B = A' \cdot B' - (A \cdot E_{BL}) \cdot E_{BR} - E_{AL} \cdot (E_{AR} \cdot B')

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:

zk-pow/src/ffi/mine.rs Rust
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.

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.