§ 02 · Cryptography
Zero-knowledge proofs
Proving the work was done without showing the work
On this page 5 sections
A zero-knowledge proofZero-knowledge proof: A proof that convinces you something is true without showing the private data behind it. Pearl's certificate proves a ticket came from real matrix work without revealing the matrices. 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 certificateCertificate: The proof of work that travels in front of every block: about 60 KB, mostly a zero-knowledge proof. Any node can check it in milliseconds without seeing the miner's matrices.. 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 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.. The proof says, in plain words:
-
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.
-
They're in range
Every entry is a whole number between −64 and 64.
-
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 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..
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 keyJob key: A BLAKE3 hash of the block template (the header minus its proof commitment) and the mining settings. All of the puzzle's randomness flows from it, tying the work to one block., the noise seedsNoise seed: One of two random-looking values made from the job key and fingerprints of the two matrices (and, since block 99,000, their sizes). Each seed grows into one noise matrix. 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.
| 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. |
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 STARKSTARK: A kind of proof that needs no secret setup and is built from hash functions. Pearl's certificate starts as a STARK, which is then compressed by Plonky2. (Starky), with hand-written circuits for BLAKE3, matrix multiplication and the ticket | Proves the whole statement. Fast to make, but large. |
| 1 | A Plonky2Plonky2: A proof system that can efficiently check other proofs. Pearl wraps its STARK in two Plonky2 layers to shrink it to about 59.6 KB; the final layer is the zero-knowledge one. 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–ShamirFiat–Shamir: A trick that turns a back-and-forth proof into a single message: the verifier’s random questions are replaced by hashes of everything said so far. 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.