§ 07

§ 07 · Blocks

Proof commitment

The 32 bytes where Bitcoin keeps its nonce

Bitcoin’s header ends with a nonce, a 4-byte number miners change billions of times a second. Pearl’s header has no nonce. Its last 32 bytes are a proof commitment instead.

A commitment is a hash published as a promise: it pins some data down without carrying it. Here the data is the public part of the block’s certificate: which matrices were multiplied, how, and which lottery ticket won. Because the commitment sits inside the header, the block’s ID vouches for all of that.

Try it

Run the toy miner to the end. Its last steps build 164 bytes of public data, hash them into a proof commitment, and append it to the 76-byte template to make a full 108-byte header.

Toy miner

Bump the time, re-run the job, until a ticket wins

Toy sizes Real jobs are thousands of rows wide, with rank-128 noise and odds near 1 in 10¹⁶ per ticket. The steps, hash functions and byte layouts here are mainnet’s.

Toy odds per ticket
Speed
Jobs
0
Tickets
0
Multiply-accumulates
0
Blocks
0

Each ticket costs 2 × 2 × 16 = 64 multiply-accumulates and wins with odds of 1 in 64, so a block takes 64 tickets on average, or 16 jobs.

What changed in the last job?

Run two jobs (or press Change one entry of A) to compare them. One small change to an input reshuffles everything that depends on it.

The matrices this miner multiplies

A · 4 × 16

B · 16 × 4, as its columns

The research example: A and B drawn once from −4 to 4, then fixed.

Runs in your browser · toy parameters m × k × n = 4 × 16 × 4, r = 4 src/lib/miner.ts · runToyJob

What does it look like?

Size
32 bytes
Type
SHA256d hash
Format
Raw hash bytes, as SHA-256 outputs them
Block 130,827 · header 108 bytes
00000020 999c961b04d94b8b21134e862dc31d5fa56cf9d1f027adb2a0cf95c6525c55eb 6389faa36bd9daffdefc800dea88ac3ef5af1198f7d7179ef3b904420c1cc2c6 1a96d76a a6f36a17 4d52a3fd32160cb032e22edf76912f67dfb1b88946a215281f203836c77ae649
Field Bytes Value
Version header.version 4 @0 0x20000000 int32 LE. BIP-9 style 0x20000000 since block 1; no consensus meaning in Pearl.
Previous block header.prevBlock 32 @4 eb555c52c695cfa0b2ad27f0d1f96ca55f1dc32d864e13218b4bd9041b969c99 Hash of the parent block, stored byte-reversed; shown in display order.
Merkle root header.merkleRoot 32 @36 c6c21c0c4204b9f39e17d7f79811aff53eac88ea0d80fcdeffdad96ba3fa8963 Root of the SHA256d Merkle tree of this block’s txids, stored byte-reversed.
Time header.timestamp 4 @68 1792513562 (2026-10-20 16:26:02 UTC) uint32 LE Unix seconds. Must be at least 1 s after the parent and at most 5 min in the future.
Bits header.bits 4 @72 0x176af3a6 Compact target. Difficulty 40157376.49334089. The certificate's jackpot hash (not this header's hash) must meet it.
Proof commitment header.proofCommitment 32 @76 49e67ac73638201f2815a24689b8b1df672f9176df2ee232b00c1632fda3524d SHA256d(LE32(certificate version) ‖ public data). Replaces Bitcoin’s nonce: Pearl headers have no nonce.
Block hash
1df2f2c1bf39b1a444e622c1788c11736da2f91f4e9a875a38dc23063c6d45b9
Height
130,827
Time
2026-10-20 16:26:02 UTC
Difficulty
40,157,376.49

Bytes 76 to 107 of block 130,827:

4d52a3fd32160cb032e22edf76912f67dfb1b88946a215281f203836c77ae649

Like every hash, it’s stored as SHA-256 produced it. Tools that display it the Bitcoin way reverse it to 49e67ac7…fda3524d. Explorers rarely show it at all.

What is it a hash of?

Of exactly two things, joined together:

The input to SHA256d, 168 bytes for a dense V3 certificate
Field Size Type Description
Certificate version 4 uint32 LE 03000000 for blocks since 99,000; 02000000 and 01000000 before.
Public data 164 bytes Mining settings (52) ‖ hash_A (32) ‖ hash_B (32) ‖ hash_jackpot, the ticket (32) ‖ m, n, tile row, tile column (4 each).
proof commitment=SHA256⁡(SHA256⁡(LE32⁡(version)‖public data))\text{proof commitment} = \operatorname{SHA256}\big(\operatorname{SHA256}(\operatorname{LE32}(\text{version}) \,\|\, \text{public data})\big)

Here is the real input for block 130,827, all 168 bytes:

03000000
00200000 8000 0000 070101030000 00010f070000 0000 0000 00000000000000000000000000000000000000000000000000000000
9e1adb4617d7d05111fe2a7e074f81738df4e2181b966ac48f66289b1cab5d4a
d1b42f82cfe36192a36ed37be30a6f9f75e7bca372449aec615e7d0721dc06ef
7540013c2866ec7f6b076e1445f907276b9f27ede2b298603604000000000000
00000200 00000200 84500100 12080100

Line by line: the version (3); the mining settings (k = 8,192, rank 128, an 8 × 16 tile pattern, dense); the fingerprint of matrix A; the fingerprint of matrix B; the winning ticket; and m = 131,072, n = 131,072, tile row 86,148, tile column 67,602. SHA256d of those bytes is 4d52a3fd…c77ae649, exactly the header’s last field. Every field of the public data is explained on certificate.

Block 130,827 · certificate 240 bytes
03000000 b9456d3c0623dc385a879a4e1ff9a26d73118c78c122e644a4b139bfc1f2f21d a4000000 00200000 8000 0000 070101030000 00010f070000 0000 0000 00000000000000000000000000000000000000000000000000000000 9e1adb4617d7d05111fe2a7e074f81738df4e2181b966ac48f66289b1cab5d4a d1b42f82cfe36192a36ed37be30a6f9f75e7bca372449aec615e7d0721dc06ef 7540013c2866ec7f6b076e1445f907276b9f27ede2b298603604000000000000 00000200 00000200 84500100 12080100 a8e80000 1212160103073c375044e471479d284e49400353a81901b092b4245ce3eb7b45
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.
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.

Why include the version?

To keep certificate versions apart. A version 2 and a version 3 certificate have the same byte layout but derive their noise differently, so the same public data means a different puzzle. Hashing the version in gives them different commitments: block 130,827’s public data under version 2 would commit to b1b8847d…0c4de66f, not 4d52a3fd…. Pearl’s code calls this domain separation, and makes version 3 override the version 2 method just to hash a 3.

Why leave the proof out?

Because zero-knowledge proofs are randomized. The same statement has many valid proofs, and proving the same winning ticket twice gives different bytes.

If the header committed to the proof, two copies of one block could have different hashes. Leaving it out means:

  • The block’s ID is fixed early. As soon as a ticket wins, its public data is known, so the commitment, the header and the block hash are final. The slow part, proving (several seconds), happens afterwards and can’t change them.
  • Any valid proof will do. A node accepts the block with whichever valid proof it receives.

The proof still can’t be reused for a different block. It is bound to this header and this public data, as the certificate explains.

Isn’t that circular?

It looks circular: the certificate contains the block hash, the block hash covers the proof commitment, and the proof commitment comes from the certificate. But each step only uses things made before it:

block templatefirst 76 header bytesminingnoise · ticketspublic data164 B, incl. the ticketproof commitmentSHA256d(ver ‖ data)header76 + 32 = 108 Bblock hashSHA256d(header)certificateversion · block hash · public data · proofZK proof (randomized)
Every step uses only earlier ones. The mining puzzle reads just the first 76 header bytes, so the ticket exists before the header is complete. The ZK proof (dashed) is not hashed into the header.

The key is that mining only ever reads the first 76 header bytes, through the job key. The proof commitment is computed after mining, from its result. In Pearl’s mining software, the gateway that builds the certificate writes the commitment into the header at that point.

What does a node check?

Two cheap comparisons first, then the expensive check:

  1. The certificate names this header

    The block hash stored in the certificate must equal SHA256d of the 108-byte header.

  2. The header commits to this certificate

    SHA256d(version ‖ public data), computed from the certificate, must equal the header’s last 32 bytes.

  3. The proof verifies against the first 76 bytes

    The Rust verifier gets the version, previous block, Merkle root, time and bits, but not the proof commitment, which would be circular. From them it rebuilds the job key and checks the ticket and the proof.

Together these tie header, public data and proof into one package: a certificate can’t be moved to another block, and a header can’t be paired with another certificate.

What about the genesis block?

Genesis is hard-coded and never verified, and its proof commitment is 32 zero bytes. Its stand-in certificate (164 zero bytes of public data and a 4-byte “proof”, deadbeef) would commit to 1879296c…30466d22 (shown byte-reversed, the way explorers display hashes), so the two don’t even match. It doesn’t matter: no node checks genesis.

Code: computing and checking the commitment

From certificate_v2.go (node/wire/certificate_v2.go:65-77 at v1.5.2, on GitHub); version 3 reuses it with its own version number (certificate_v3.go (node/wire/certificate_v3.go:21-24 at v1.5.2, on GitHub)):

node/wire/certificate_v2.go Go
// proofCommitment computes SHA256d(CertificateVersion_LE(4) || publicData).
// V3 overrides ProofCommitment to hash version 3 (domain separation from V2).
func proofCommitment(version CertificateVersion, publicData []byte) chainhash.Hash {
	buf := make([]byte, 4+len(publicData))
	binary.LittleEndian.PutUint32(buf[:4], uint32(version))
	copy(buf[4:], publicData)
	return chainhash.DoubleHashH(buf)
}

The node’s two comparisons, from verify.go (node/zkpow/verify.go:114-130 at v1.5.2, on GitHub):

node/zkpow/verify.go Go
certHash := cert.BlockHash()
blockHash := header.BlockHash()
if !certHash.IsEqual(&blockHash) { /* block hash mismatch */ }

proofCommitment := cert.ProofCommitment()
if header.ProofCommitment != proofCommitment { /* proof commitment mismatch */ }

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.