§ 03

§ 03 · Matrix math

Rank and noise

Random enough to stop shortcuts, cheap enough to peel off

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 rank, 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.

Low-rank noise

A thin random matrix times a ±1 selector

Rank: columns of E_L

E_L · 8 × 4 · random

× selector S · 4 × 16

= E · 8 × 16 · hover a column

E column 0 = E_L column 3 − E_L column 1

[−3, −1, −4, −3, 3, −1, 3, −1] − [3, −3, −1, 3, 1, −1, −3, 0] = [−6, 2, −3, −6, 2, 0, 6, −1]

The selector column 0 has +1 in row 3 and −1 in row 1, nothing else. No multiplications at all: one subtraction per entry.

Rank of E

3 of 8 possible

r − 1 = 3: every selector column sums to zero, which costs one dimension.

Numbers to write it down

low rank64
full128

Low rank: 8 × 4 random numbers plus 2 small indices per column (32 + 32). Full: 8 × 16.

Real Pearl r = 128 (the minimum since block 96,251; a higher rank makes the winning bar stricter in proportion). The thin factor’s entries run from −32 to 31, so noise runs from −63 to 63. For a 4,096 × 8,192 matrix, low rank needs 540,672 numbers instead of 33,554,432, about 62× fewer, and peeling the noise off the product costs about 2r/k ≈ 3.1% extra.

Toy sizes 8 × 16 here, with factors from −4 to 3. Real jobs are thousands of rows wide.

Runs in your browser · Pearl’s own noise generator src/lib/miner.ts · noiseMatrices

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 product. Every row is a multiple of the same row.

123×20−11=20−111 × row40−222 × row60−333 × row
A rank-1 matrix: 12 numbers, but fully described by 3 + 4 = 7. Each row is 1, 2 or 3 times the row (2, 0, −1, 1).

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:

E(m×k)=EL(m×r)⋅ER(r×k)E \;(m \times k) \;=\; E_L \;(m \times r) \;\cdot\; E_R \;(r \times k)
m·k numbers in the full matrix, but only r·(m + k) needed to describe it. Pearl uses r = 128.

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

E_AL (4 × 4)23−20−2−12−40−1−233−22−3×E_AR (4 × 16): one +1 and one −1 per column+1−1+1+1−1+1−1−1−1+1+1−1+1−1+1−1−1−1+1−1−1+1−1−1+1+1−1−1+1+1+1+1=E_A (4 × 16), the noise added to A4−3−2−532452−5−224−452−4−3−2332−4−3−63−2−6−44−3−6243−1−4−3215−1352−2151−1−64161−4−54−6−51−1−4−5column 0 of E_Acol 0 − col 2of E_AL:(4, −4, 2, 1)
The toy noise from Pearl's worked example (r = 4, k = 16). The selector's column 0 has +1 in row 0 and −1 in row 2, so column 0 of E_A is column 0 minus column 2 of E_AL. Toy numbers, from the research worked example.

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:

A′=A+EA,B′=B+EB,work=A′⋅B′A' = A + E_A, \qquad B' = B + E_B, \qquad \text{work} = A' \cdot B'
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:

A′B′=AB+A⋅EB+EA⋅B′A' B' = A B + A \cdot E_B + E_A \cdot B'

so the real product, the useful answer, is

AB=A′B′−(A⋅EBL)⋅EBR−EAL⋅(EAR⋅B′)A B \;=\; A' B' \;-\; (A \cdot E_{BL}) \cdot E_{BR} \;-\; E_{AL} \cdot (E_{AR} \cdot B')
Denoising
Every correction goes through the thin r-wide matrices, never through a full k-wide product.

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:

zk-pow/src/circuit/pearl_noise.rs Rust
// 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();

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.