§ 08

§ 08 · Mining

Tickets and target

A diary per tile, a BLAKE3 ticket, and a bar scaled by work

A Bitcoin miner buys lottery tickets one hash at a time. A Pearl miner gets its tickets from matrix multiplication: every small patch of the output, a tile, becomes one ticket.

A ticket is a 256-bit number. It is made from a running diary the tile keeps while it’s being computed, and it wins if it’s small enough. “Small enough” depends on the work: a ticket that cost twice as many multiplications gets a bar twice as high. That one rule makes Pearl’s lottery fair per multiply-accumulate, however a miner shapes its matrices.

Try it

Let the toy miner run until a ticket wins. Watch each tile’s diary fill up, slab by slab, and its ticket land above or below the bar.

Toy miner

Bump the time, re-run the job, until a ticket wins

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.

Toy odds per ticket
Speed
Jobs
0
Tickets
0
Multiply-accumulates
0
Blocks
0

Each ticket costs 2 × 2 × 16 = 64 multiply-accumulates and wins with odds of 1 in 64, so a block takes 64 tickets on average, or 16 jobs.

What changed in the last job?

Run two jobs (or press Change one entry of A) to compare them. One small change to an input reshuffles everything that depends on it.

The matrices this miner multiplies

A · 4 × 16

B · 16 × 4, as its columns

The research example: A and B drawn once from −4 to 4, then fixed.

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

What is a tile?

A set of output cells whose sums together make one ticket. The certificate’s settings give a row pattern and a column pattern; a tile is every cell at (t_rows + a row offset, t_cols + a column offset).

  • The reference GPU miner uses rows 0 and 8 and column pairs 0, 1, 8, 9, … 248, 249: a 2 × 64 tile, 128 cells, exactly the sums one GPU thread holds.
  • Consensus allows other shapes: both sides even, and 32 to 256 cells. Block 130,827 used 8 × 16.
  • Valid tile positions cover the output exactly, so every cell belongs to exactly one ticket.

Why that odd pattern, and how a GPU computes it, is on GPUs and tiles.

How does a tile keep its diary?

Each cell of the output is a long sum along the shared dimension k. The diary watches those sums grow. It is 16 words of 32 bits, M[0] to M[15], all zero to start. Then, for every slab of r = 128 steps:

  1. Add one slab

    Every cell’s running sum grows by r more multiply-accumulates.

  2. Fold the tile into one word

    XOR all h·w running sums together, each read as a 32-bit pattern (negative numbers as their two’s complement). Call the result X.

  3. Stir it into the diary

    For checkpoint s, update slot (s − 1) mod 16: rotate it left by 13 bits, then XOR in X.

M[(s−1)mod16]←rotl13⁡(M[(s−1)mod16])⊕︎⨁cellsCtile(s)M[(s-1) \bmod 16] \leftarrow \operatorname{rotl}_{13}\big(M[(s-1) \bmod 16]\big) \oplus \bigoplus_{\text{cells}} C_{\text{tile}}^{(s)}

With k = 8,192 and r = 128 there are 64 checkpoints, so each slot is stirred four times. If k isn’t a multiple of r, the last k mod r steps are still multiplied but not recorded.

slab s128 steps along krunning sumsh·w cells, int32X = XORone u32rotl 13 ⊕ Xslot (s−1) mod 16M0M1M2M3M4M5M6M7M8M9M10M11M12M13M14M15↑ M0: s = 1, 17, 33, 49M15: s = 16, 32, 48, 64 ↑64 bytes, little-endianBLAKE3keyed with seed_Aticket256 bits, read LEwins if ticket ≤ target × h·w·L
One tile's diary. After every 128-step slab, the tile's running sums fold into one 32-bit word that is stirred into the next of 16 slots. At k = 8,192 the 64 checkpoints wrap around the slots four times. The finished 64 bytes are hashed into the ticket.

The XOR fold is cheap: one extra operation per cell per 128 multiply-accumulates. On the GPU, each thread folds the 128 sums it already holds in its registers, so no data moves between threads.

Why a diary, and not just the final answer?

Because the final answer can be faked cheaply, and the diary, as far as anyone knows, can’t.

The noise that makes every job fresh is low-rank, which cuts both ways. If you already know A·B, then the noisy product (A + E)(B + F) = A·B + A·F + E·(B + F) costs only a few low-rank corrections. If tickets hashed only the final product, a miner could do one expensive multiplication and then mint endless cheap tickets from it under new noise.

The diary blocks this. It records the running sums after every r-wide slab. To fake those from a known product, a shortcut needs a fresh rank-r correction at every checkpoint, about h·w·r work per tile per checkpoint: the same as just doing the multiplication. Pearl’s INT whitepaper finds no apparent speedup “as long as k ≤ O(r²)”, which is why consensus requires k ≤ 4r².

How is the ticket made?

When the tile is finished, its 16 diary words are written out as 64 little-endian bytes and hashed with BLAKE3, keyed with seed A:

ticket=BLAKE3seedA⁡(M[0]‖M[1]‖…‖M[15])\text{ticket} = \operatorname{BLAKE3}_{\text{seed}_A}(M[0] \,\|\, M[1] \,\|\, \dots \,\|\, M[15])

Keying with seed A ties every ticket to this job’s commitments: the same diary under a different template or different matrices would hash to an unrelated ticket. The certificate calls the winning ticket hash_jackpot.

When does a ticket win?

Read the ticket as a 256-bit little-endian number, and compare it with the target from the header’s bits, scaled by the work the ticket cost:

ticketLE≤T(bits)×h⋅w⋅L,L=k−(kmodr)\text{ticket}_{\text{LE}} \;\le\; T(\text{bits}) \times h \cdot w \cdot L, \qquad L = k - (k \bmod r)
h·w cells per tile, each summed over L recorded steps: the multiply-accumulates one ticket cost. The product saturates at 2²⁵⁶ − 1.

Since block 96,251 there is a second form, the rank penalty: the noise rank must be at least 128, and the ticket must also be at most T × h·w·⌊L/r⌋·128. At rank 128 the two bounds are identical. See rank penalty.

Why scale the target by work?

So that the price of a win is a fixed number of multiply-accumulates, whatever the shape. A ticket costing W = h·w·L multiply-accumulates wins with probability about T·W ÷ 2²⁵⁶. Divide by its cost and W cancels:

Pr⁡[win per multiply-accumulate]≈T2256⟹𝔼[multiply-accumulates per block]≈2256T\Pr[\text{win per multiply-accumulate}] \approx \frac{T}{2^{256}} \quad\Longrightarrow\quad \mathbb{E}[\text{multiply-accumulates per block}] \approx \frac{2^{256}}{T}

Bigger tiles, longer k, more tiles: none of it changes the odds per unit of work. That’s why Pearl’s “hashrate” counts multiply-accumulates (see hashrate). At the 40,157,376 difficulty of 2026-10-20, one 1,048,576-step ticket wins with probability about 9.3 × 10⁻¹⁷.

A real check: block 130,827

Here is block 130,827’s certificate, with the ticket highlighted:

Block 130,827 · certificate 240 bytes
03000000 b9456d3c0623dc385a879a4e1ff9a26d73118c78c122e644a4b139bfc1f2f21d a4000000 00200000 8000 0000 070101030000 00010f070000 0000 0000 00000000000000000000000000000000000000000000000000000000 9e1adb4617d7d05111fe2a7e074f81738df4e2181b966ac48f66289b1cab5d4a d1b42f82cfe36192a36ed37be30a6f9f75e7bca372449aec615e7d0721dc06ef 7540013c2866ec7f6b076e1445f907276b9f27ede2b298603604000000000000 00000200 00000200 84500100 12080100 a8e80000 1212160103073c375044e471479d284e49400353a81901b092b4245ce3eb7b45
Field Bytes Value
Certificate version certificate.version 4 @0 3 u32 LE. V1 before block 71,935, V2 until 98,999, V3 (salted noise seeds) from 99,000. 0 = no certificate.
Block hash certificate.blockHash 32 @4 1df2f2c1bf39b1a444e622c1788c11736da2f91f4e9a875a38dc23063c6d45b9 SHA256d of the 108-byte header this certificate proves (stored byte-reversed); must equal the header’s hash.
Public data length certificate.publicDataLength 4 @36 164 164 for dense jobs; longer means MoE (invalid since 91,630).
Common dimension k certificate.publicData.k 4 @40 8192 u32 LE: the shared dimension of A (m×k) and B (k×n).
Noise rank r certificate.publicData.rank 2 @44 128 u16 LE: rank of the low-rank noise; must be ≥ 128 since block 96,251.
MMA type certificate.publicData.mmaType 2 @46 0 0 = Int7 × Int7 → Int32.
Rows pattern certificate.publicData.rowsPattern 6 @48 [0,8,32,40,64,72,96,104] (h=8) Periodic row offsets of the hash tile, stored as (factor−1, length−1) × 3.
Columns pattern certificate.publicData.colsPattern 6 @54 [0,1,32,33,64,65,96,97,128,129,160,161,192,193,224,225] (w=16) Periodic column offsets of the hash tile.
Experts e certificate.publicData.experts 2 @60 0 Number of MoE experts; 0 = dense (MoE is invalid on mainnet since 91,630).
Top-k certificate.publicData.topK 2 @62 0 0 for dense jobs.
Reserved certificate.publicData.reserved 28 @64 00000000000000000000000000000000000000000000000000000000 Must be zero.
hash_A certificate.publicData.hashA 32 @92 9e1adb4617d7d05111fe2a7e074f81738df4e2181b966ac48f66289b1cab5d4a Keyed-BLAKE3 Merkle root of matrix A (key = job key).
hash_B certificate.publicData.hashB 32 @124 d1b42f82cfe36192a36ed37be30a6f9f75e7bca372449aec615e7d0721dc06ef Keyed-BLAKE3 Merkle root of Bᵀ (B column-major).
hash_jackpot certificate.publicData.hashJackpot 32 @156 7540013c2866ec7f6b076e1445f907276b9f27ede2b298603604000000000000 The winning ticket: BLAKE3 keyed with seed_A over the tile transcript; read as a little-endian number and compared with the bound.
m (rows of A) certificate.publicData.m 4 @188 131072 u32 LE; bound into the noise seeds since certificate V3.
n (columns of B) certificate.publicData.n 4 @192 131072 u32 LE; bound into the noise seeds since certificate V3.
Tile row offset certificate.publicData.tRows 4 @196 86148 Smallest row index of the winning hash tile.
Tile column offset certificate.publicData.tCols 4 @200 67602 Smallest column index of the winning hash tile.
Proof length certificate.proofLength 4 @204 59560 u32 LE; at most 60,000 bytes.
ZK proof (first 32 of 59,560 bytes) certificate.proof 32 @208 59,560 bytes, collapsed pow_bits(3) ‖ rate_bits(3) ‖ zeta(16) ‖ Plonky2 proof: a STARK wrapped in two Plonky2 recursions. Not covered by the block hash.
ProofCommitment check
SHA256d(version ‖ public data) matches the header ✓
Certificate
V3 · 59,768 bytes
Proof
59,560 bytes (99.7%)
The proof itself is 59,560 bytes; only its first 32 are shown.

And the check, with every number from the chain:

  1. Read the settings

    k = 8,192, r = 128, rows pattern 0, 8, 32, 40, 64, 72, 96, 104 (h = 8), columns 0, 1, 32, 33, … 224, 225 (w = 16). So L = 8,192 − (8,192 mod 128) = 8,192.

  2. Work per ticket

    h·w·L = 8 × 16 × 8,192 = 1,048,576 = 2²⁰.

  3. Target from bits 0x176af3a6

    T = 0x6af3a6 × 2¹⁶⁰ = 0000000000000000006af3a60000000000000000000000000000000000000000

  4. The bar

    T × 2²⁰ is the target shifted left by 20 bits, five hex digits: 00000000000006af3a6000000000000000000000000000000000000000000000

  5. The ticket, read little-endian

    The raw bytes are 7540013c2866ec7f…e2b2986036040000 00000000. Reversed into a number: 00000000000004366098b2e2ed279f6b2707f945146e076b7fec66283c014075

  6. Compare

    0x…4366098b… is below 0x…6af3a600…: the ticket is about 63% of the bar. Valid. With r = 128 the rank-penalty bar, 8 × 16 × (8,192 ÷ 128) × 128, is the same 2²⁰ × T, so it passes that too.

Block 130,825 used the reference 2 × 64 tile instead: also 128 cells, so also a factor of 1,048,576. Its ticket was about 14% of its bar.

Notice the raw ticket bytes end in a run of zeros. Read little-endian, the last bytes are the most significant, and a small number’s high bytes are zero. See byte order.

Make a guess

A miner switches from k = 8,192 to k = 16,384, same tiles. Does it find blocks faster?

Think of your answer first, then check it.

Answer No: same chance per multiply-accumulate.

Each ticket now takes 16,384 steps, twice the work, and L doubles, so the bar doubles: each ticket is twice as likely to win but costs twice as much. Per multiply-accumulate nothing changed. (It does halve the number of tickets per multiplication’s worth of work, and k ≤ 4r² = 65,536 at rank 128 caps how far this goes.)

Who checks tickets?

Three parties, against three bars:

  • The GPU compares every ticket on the chip with an easy share target set by the pool or gateway, already adjusted for the rank penalty. Almost all are thrown away.
  • The pool or gateway recomputes a submitted share from the revealed rows and columns and checks it against that easier target. Pearl’s verifier has a switch for this, nbits_override.
  • Every node checks the certificate’s ticket against the real bar from the header’s bits, before verifying the proof.

More on shares on pools.

Code: the diary and the check

The diary, from compute_jackpot (zk-pow/src/circuit/chip/jackpot/helper.rs:9-35 at v1.5.2, on GitHub), as the prover and the plain-proof verifier compute it:

zk-pow/src/circuit/chip/jackpot/helper.rs Rust
for ll in (r..=k).step_by(r) {
    for u in 0..h {
        for v in 0..w {
            for l in ll - r..ll {
                jackpot[u][v] +=
                    (secret_a[u][l] as i32 + noise.a[u][l] as i32) * (secret_b[v][l] as i32 + noise.b[v][l] as i32);
            }
        }
    }
    let xored_tile = jackpot.iter().flatten().fold(0u32, |a, &x| a ^ x as u32);
    let tid = (ll / r - 1) % JACKPOT_SIZE;
    jackpot_msg[tid] = jackpot_msg[tid].rotate_left(LROT_PER_TILE) ^ xored_tile;
}

The check, from sanity_checks.rs (zk-pow/src/api/sanity_checks.rs:150-159 at v1.5.2, on GitHub); the factor is tile_size × dot_product_length (lines 185–194):

zk-pow/src/api/sanity_checks.rs Rust
let jackpot_hash_bound = extract_difficulty_bound(nbits, &public_params.mining_config);
// hash_jackpot is interpreted as a little-endian 256-bit integer for the difficulty check
ensure!(
    U256::from_little_endian(&public_params.hash_jackpot()) <= jackpot_hash_bound,
    "Jackpot condition not satisfied: hash does not meet difficulty target"
);

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.