§ 08 · Mining
Tickets and target
A diary per tile, a BLAKE3 ticket, and a bar scaled by work
On this page 9 sections
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 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., becomes one ticket.
A 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. 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.
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:
-
Add one slab
Every cell’s running sum grows by r more multiply-accumulates.
-
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.
-
Stir it into the diary
For checkpoint s, update slot (s − 1) mod 16: rotate it left by 13 bits, then XOR in X.
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.
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:
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:
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:
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:
| 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. |
And the check, with every number from the chain:
-
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.
-
Work per ticket
h·w·L = 8 × 16 × 8,192 = 1,048,576 = 2²⁰.
-
Target from bits 0x176af3a6
T = 0x6af3a6 × 2¹⁶⁰ =
0000000000000000006af3a60000000000000000000000000000000000000000 -
The bar
T × 2²⁰ is the target shifted left by 20 bits, five hex digits:
00000000000006af3a6000000000000000000000000000000000000000000000 -
The ticket, read little-endian
The raw bytes are
7540013c2866ec7f…e2b2986036040000 00000000. Reversed into a number:00000000000004366098b2e2ed279f6b2707f945146e076b7fec66283c014075 -
Compare
0x…4366098b…is below0x…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:
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):
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"
);