§ 02

§ 02 · Cryptography

Zero-knowledge proofs

Proving the work was done without showing the work

A zero-knowledge proof lets you convince someone that a statement is true without showing them the secret that makes it true.

The classic picture is a Where’s Waldo? page. To prove you’ve found Waldo without showing where, cover the page with a big sheet of cardboard that has a small Waldo-shaped hole, and line the hole up over him. The verifier sees Waldo, so they know you found him. They learn nothing about where on the page he is.

Every Pearl block carries such a proof, inside its certificate. It proves the miner really did matrix multiplication that produced a winning ticket, without revealing the matrices.

What exactly does the certificate prove?

The winning miner knows a few rows of matrix A and a few columns of matrix B: the ones that made up its winning tile. The proof says, in plain words:

  1. These rows and columns are the committed ones

    Placed at their declared positions, they hash (with keyed BLAKE3, as a Merkle tree) to the public fingerprints of A and B.

  2. They're in range

    Every entry is a whole number between −64 and 64.

  3. They really produce the ticket

    After adding the public noise, multiplying them with the running sums folded every 128 steps gives a 64-byte record, and keyed BLAKE3 of that record is the claimed ticket.

What stays hidden is the rows and columns themselves. What is public is the certificate’s 164-byte public data: the mining settings, the fingerprints of A and B, the ticket, the matrix sizes and the tile’s position.

Some checks don’t need any secrecy, so the node simply does them itself, outside the proof. It recomputes the job key, the noise seeds and the noise for the tile’s rows and columns, and it checks that the ticket is at most the target times the tile’s work. Recomputing the noise is about two thirds of that plain-arithmetic part of verification.

Block 130,825 · certificate 240 bytes
03000000 ae3355eec8af1f407f0e935739615e375f35bedcd556a75e13a093387651c83f a4000000 00200000 8000 0000 070100000000 0001031f0000 0000 0000 00000000000000000000000000000000000000000000000000000000 8e17f731a0c6f8e09a7a29f19732fc29f65932b63886013274b920b057e6930f c2e4cb51cff3f95861c45248b6369f5c2dbd623895a3146be39134d060837c8d b4be6b12fbad2ac915cbc0f80f94a1b3fbe55786f3a2912ded00000000000000 00000200 00000400 00930100 02e10200 a8e80000 121216010307bbbb23999fb93b02b1fd3157fba6a4398fd35f6a45d8cf5284db
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 3fc851763893a0135ea756d5dcbe355f375e613957930e7f401fafc8ee5533ae 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] (h=2) Periodic row offsets of the hash tile, stored as (factor−1, length−1) × 3.
Columns pattern certificate.publicData.colsPattern 6 @54 64 columns from [0,1,8,9,16,17,…] (w=64) 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 8e17f731a0c6f8e09a7a29f19732fc29f65932b63886013274b920b057e6930f Keyed-BLAKE3 Merkle root of matrix A (key = job key).
hash_B certificate.publicData.hashB 32 @124 c2e4cb51cff3f95861c45248b6369f5c2dbd623895a3146be39134d060837c8d Keyed-BLAKE3 Merkle root of Bᵀ (B column-major).
hash_jackpot certificate.publicData.hashJackpot 32 @156 b4be6b12fbad2ac915cbc0f80f94a1b3fbe55786f3a2912ded00000000000000 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 262144 u32 LE; bound into the noise seeds since certificate V3.
Tile row offset certificate.publicData.tRows 4 @196 103168 Smallest row index of the winning hash tile.
Tile column offset certificate.publicData.tCols 4 @200 188674 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.

How is the proof built?

In three layers, each one proving the one before. This is called recursion.

Layer What it is Its job
0 A STARK (Starky), with hand-written circuits for BLAKE3, matrix multiplication and the ticket Proves the whole statement. Fast to make, but large.
1 A Plonky2 proof, using the Poseidon hash Proves “the STARK verifies”. Smaller.
2 A Plonky2 proof, using BLAKE3, with zero-knowledge switched on Proves “layer 1 verifies”, and hides everything. This is what goes on chain.

All three work with numbers in the Goldilocks field, arithmetic modulo the prime 2⁶⁴ − 2³² + 1, which fits neatly in a 64-bit computer word. The system targets 120 bits of security.

How a STARK works, in four sentences

The prover writes the computation out as a big table of numbers, one row per step: a trace. It commits to the table with Merkle trees, so it can’t change it later. Polynomial checks then say “every row follows from the one before” and “the first and last rows are right”. The verifier checks those claims only at a few random spots, chosen by hashing the commitments (the Fiat–Shamir trick), and a technique called FRI makes sure the table really behaves like a low-degree polynomial everywhere else.

The public data is bound into the proof the same way: the proof’s random challenges come from a BLAKE3 hash over the header, the public data and the proof settings. A proof made for one block can’t be reused for another.

How big and how fast is it?

The size is essentially constant, whatever the matrix sizes.

Proof
59,560 bytes
cap 60,000
Certificate
59,768 bytes
cap 65,000
Public data
164 bytes
what the proof is about
Prove (warm)
5.7–17 s
once per block found
Verify
7–21 ms
every node, every block
Security target
120 bits
proof-system setting

A V3 certificate on the wire is 4 + 32 + 4 + 164 + 4 + 59,560 = 59,768 bytes: version, block hash, public-data length, public data, proof length, proof. The research measured the times on an 18-core Apple M5 Max CPU, for a 2 × 64 tile:

Shared dimension k Prove, first run Prove, warm Verify
2,048 10.1 s 5.7 s 7.5–9.1 ms
8,192 15.1 s 17.0 s 10.9–21.0 ms

The proof was 59,560 bytes at both sizes. Nodes ship a precompiled verifier, so a check costs milliseconds.

Make a guess

Who has to wait those 6 to 17 seconds of proving: every miner for every ticket?

Think of your answer first, then check it.

Answer No: only once, after a ticket wins.

Tickets are checked on the GPU as plain numbers. Only a winning share is sent to the pool’s gateway, which turns it into a zero-knowledge certificate. Proving happens once per block found, not once per ticket.

Why isn’t the proof part of the block hash?

Because proofs are randomized: the same statement has many valid proofs. If the block hash covered the proof bytes, two copies of the same block could have different hashes.

So the header commits only to the certificate version and the public data. Its last field, the proof commitment, is SHA256d of the version (as 4 bytes) followed by the public data. The block hash covers the work and the ticket, and is fixed before proving even starts.

What doesn’t the proof prove?

Two things, worth being clear about.

  • That the work was useful. The proof checks arithmetic on any matrices in range. Random numbers pass just as well as an AI model’s layer. Usefulness is up to the miner. See useful output.
  • That there’s no shortcut. The proof shows the ticket came from the claimed multiplication. Whether a clever miner could get tickets more cheaply than by multiplying is a separate question, answered by an argued conjecture, not a theorem. See rank and noise.
Code: where a node checks the certificate

pearld hands each header and certificate to zkpow.VerifyCertificate (node/zkpow/verify.go:41-188 at v1.5.2, on GitHub). It checks that the certificate names this header’s hash and that the header’s proof commitment matches the public data, then calls the Rust verifier in verify.rs (zk-pow/src/api/verify.rs:21-94 at v1.5.2, on GitHub): parse the public data, run sanity checks, test the ticket against the bound, recompute the seeds and noise, and verify the proof.

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.