§ 03 · Matrix math
Rank and noise
Random enough to stop shortcuts, cheap enough to peel off
On this page 7 sections
If Pearl’s puzzle were simply “multiply A by B”, a miner could do one multiplication and replay the answer forever. So before multiplying, the miner must add noise: random numbers that nobody can predict until the block and the matrices are fixed.
The noise has a peculiar shape. It’s random-looking, but it has low 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., which makes it cheap to create and, after multiplying, cheap to remove, so the useful answer survives. This page explains rank, the noise’s exact recipe, and the catch that low rank creates.
Try it
Build a noise matrix from a thin random matrix and a ±1 selector. See how few numbers describe the whole thing.
What is rank?
The rank of a matrix is how many truly independent rows it has. If some rows are just combinations of others, they add no new information.
The simplest case is rank 1: a column times a row, an outer productOuter product: A column of numbers times a row of numbers, giving a whole matrix where every row is a multiple of the same row. It is the simplest kind of matrix: rank 1.. Every row is a multiple of the same row.
A rank-r matrix is a sum of r such outer products. Equivalently, it’s a thin m × r matrix times a wide r × k matrix:
How does Pearl build its noise?
Two seeds, made by hashing the block template and the miner’s fingerprints of A and B (see noise seeds), drive a BLAKE3 random number generator. From seed A it builds:
- E_AL, a thin m × r matrix of random numbers in [−32, 31] (6-bit).
- E_AR, an r × k selector: each column has exactly one +1 and one −1, in two different rows f and s, and zeros elsewhere.
Their product is the noise added to A: E_A = E_AL · E_AR. Because each selector column picks one column and subtracts another, every column of the noise is a difference of two columns of E_AL. That’s why its entries land in [−63, 63].
The noise for B is built the same way from seed B, mirrored: E_B = E_BL · E_BR, with a k × r selector on the left (one +1 and one −1 per row) and a random r × n matrix on the right. Then the miner multiplies the noisy matrices:
Two details: the effective rank, and the cost of making noise
Effective rank. Every column of a selector has one +1 and one −1, so its entries sum to zero. That makes the selector’s rows linearly dependent, and the noise’s rank is at most r − 1: at most 127 on mainnet. (The research inferred this from the construction; the toy noise, with r = 4, has rank 3.)
Cost. E_A has m·k entries but is described by m·r random numbers plus 2k selector positions. For m = 1,024, k = 8,192 and r = 128 that’s 147,456 numbers instead of 8,388,608. Generating and adding the noise takes time proportional to (m + n)·k, a small fraction of the m·n·k multiplication.
Why does noise stop reuse?
Because nobody knows it in advance. The seeds depend on the previous block, the transaction root, the time, the mining settings, the fingerprints of A and B and, since block 99,000, the declared sizes m and n. Change any of these and the noise changes completely.
There’s a second, subtler job. Pearl’s security argument leans on a result from the paper behind the design: every r × r block of a random rank-r matrix looks perfectly uniform. So A′ = A + E_A looks random block by block, even if A is all zeros. Multiplying A′ by B′ should then be as hard as multiplying random matrices: no matrix the miner chooses can make the work easier.
How is the noise removed?
Expand the noisy product. Since A′ = A + E_A and B′ = B + E_B:
so the real product, the useful answer, is
The bracketed products with the ±1 selectors (A·E_BL and E_AR·B′) are only additions and subtractions. The two remaining products have an inner size of r instead of k, costing about m·n·r each. So the extra work is about 2r/k of the main multiplication:
| Shared size k | Denoising overhead ≈ 2 × 128 / k |
|---|---|
| 4,096 | 6.25% |
| 8,192 | 3.1% |
| 28,672 | 0.9% |
The integer identity is exact. On the GPU, Pearl’s kernel applies the corrections in 16-bit floating point inside the same pass, then rescales and writes the layer’s output, so whether the GPU result is bit-for-bit exact is an open question (it doesn’t matter much for an AI model). More on the useful output page.
What’s the catch with low rank?
Cheap to remove means cheap to add. If a miner already knew A·B, it could compute the noisy product A′B′ = AB + A·E_B + E_A·B′ with only the cheap thin products, about m·n·r work instead of m·n·k. If the ticket depended only on the final product, one expensive multiplication could be recycled into endless cheap tickets.
So Pearl’s ticket doesn’t hash the final product. It records each tile’s running sums after every slab of r = 128 steps along k (see matrix multiplication). To fake those, a shortcut would need a fresh low-rank correction at every checkpoint, which costs as much as just doing the multiplication. The whitepaper sees no apparent speedup as long as k ≤ O(r²), which is why consensus requires k ≤ 4r².
Why does rank need a penalty?
A ticket’s winning bar is scaled by the work it costs, and the rank of the noise divides that work: with a bigger r there are fewer checkpoints per tile. Pearl’s developers found that mining at a larger rank bought a proportionally easier bar for the same effort.
Since block 96,251, the rank must be at least 128, and the bar is scaled by 128/r:
| Noise rank r | Below 128 | 128 | 256 | 512 | 1,024 |
|---|---|---|---|---|---|
| Winning bar | rejected | normal | ½ | ¼ | ⅛ |
The rank penalty page has the full story.
Code: building the noise in Pearl's prover
From pearl_noise.rs (zk-pow/src/circuit/pearl_noise.rs:140-153 at v1.5.2, on GitHub). Only the rows and columns a
tile needs are computed; matvec_sparse_perm applies the ±1 selector without any multiplications:
// Generate the 4 matrices
let e_al = generate_uniform_random_matrix(&seed_a, &a_noise_seed, a_rows_indices, noise_rank);
let e_ar_transposed = generate_permutation_matrix(&seed_a, &a_noise_seed, k, noise_rank);
let e_bl = generate_permutation_matrix(&seed_b, &b_noise_seed, k, noise_rank);
let e_br_transposed = generate_uniform_random_matrix(&seed_b, &b_noise_seed, b_cols_indices, noise_rank);
// Compute only the needed rows of NOISE_A = E_AL * E_AR
let noise_a: Vec<Vec<i8>> = e_al.par_iter().map(|row| matvec_sparse_perm(&e_ar_transposed, row)).collect();