Zero Knowledge
How Hekate's zero-knowledge proofs hide the witness: one-time pads on rounds, claims and bus sums, a hiding hash-based commitment, a zk-Ligero outer argument.
On this page
A Hekate proof made with zero knowledge on, the default, convinces the verifier that every table satisfies the program and reveals nothing else about the rows. The hiding happens in three places: the commitment opens only columns that look random, a one-time pad covers every round, claim and bus sum the proof sends, and the checks the verifier cannot run on padded numbers go into an outer statement, which a zk-Ligero argument proves at the end. This page specifies all three, down to the gadget that keeps packed bit columns working under the pads. PIOP Protocol covers the protocol they hide.
§ What zero knowledge promises
A proof is zero-knowledge when a simulator that never sees the witness produces proofs nobody can tell from real ones. Whatever a verifier learns from a real proof, it could have computed from the statement alone.
In Hekate the statement is public: the program, its public inputs, the Config and the height of every table. The
witness is the trace, every cell the prover filled. Zero knowledge hides the cells and leaves the statement in view, and
a verifier still sees how many rows each table has.
The guarantee is computational. The pads and every other random value are derived from the 32-byte seed you pass to
prove, and telling a simulated proof from a real one takes breaking those generators or the hash.
Why the proof is zero-knowledge gives the argument.
§ What a proof in the clear gives away
A proof made with zero_knowledge: false states the value of every column at , and each value is a
weighted sum of the column's rows with weights anyone can compute from the point.
Take the four-row column of PIOP Protocol, in ordinary integers, at the point (2, 3), where rows 0 to 3 weigh 2, , and 6. Rows 0, 1 and 3 hold the public values 3, 5 and 6, row 2 holds a secret , and the proof claims 16:
In GF(2^128) the solve is the same: one field division. Each claim is one linear equation in the rows of its column. When most rows are unknown, one equation pins down none of them, and when a column holds one secret among public values, the equation has one unknown. Zero knowledge has to hold for every witness, the sparse ones included.
The claims are one channel of several: every sumcheck round polynomial, the row fold and the values at are sums over the rows as well.
§ Where the proof hides the trace
Every value a proof carries is public by design or hidden by one of a few devices, and the table accounts for each.
| The proof carries | Hidden by |
|---|---|
| One Merkle root over all tables, and the helper and outer roots | The hash, over leaves that look random |
| Opened columns of every table | Random support in every encoded row |
| The row fold and the committed values at | One random column in each batch |
| Sumcheck rounds, column claims, bus sums and helper values | A one-time pad |
| Opened columns of the outer argument | random symbols in every row |
| The three responses of the outer argument | A mask over the whole space each test allows |
| The point | Nothing: a hash of the transcript |
| The last claim of each sumcheck | Nothing: the masked rounds fix it |
wire_census, a test in the Hekate repository, walks real proofs, from a single table without buses to
programs with chiplets and packed bits, and assigns every field element they publish to one row of this table. It
destructures the proof format field by field, Brakedown openings aside, and a new field breaks its build until someone
classifies it.
§ The commitment
The commitment hides its openings with random symbols inside every encoded row, and its folds with one random column in each batch.
§ Opened columns
Every encoded row of a committed table mixes in a block of random symbols, and any num_queries opened positions of a
row are uniformly random.
Prover Engine draws the commitment: the tables as one grid, each row
encoded with a Reed-Solomon code, a Merkle tree over the encoded columns. A row's message holds the trace values and a
support of more than num_queries symbols at coordinates of its own, and the support is random.
The encoding reads the message as the coefficients of one polynomial and stores its values at the points of an
evaluation domain, computed with the additive FFT of the binary field. No position of the codeword copies a trace
value.
The support sits in the low coefficients, and its share of any num_queries positions of the codeword can take every
value, which makes the opened positions uniform whatever the trace holds. Under a nonzero security floor, the security
check rejects a grid whose support does not exceed num_queries. The evaluation at reads only the trace part of , and the support,
committed in full with the data, changes the codewords and never the claim. The construction follows the randomizable
support of the Binius64 specification (sec. 2.3).
§ Folds
The opening also sends the row fold and the values of the committed columns at . Both are weighted sums of rows, open to the same solving as the claims, and one column of random values in each batch makes them uniform.
The evaluation argument batches the columns of every table with powers of and opens the batch at , as PIOP Protocol describes. With zero knowledge on, every table commits uniform random columns in the batch of columns committed whole, and a table with packed bit columns adds a random column of 128 bits to the packed batch. Their rows add a uniform vector to and a uniform term to each value at , and what the verifier sees is uniform apart from the one claim the opening has to meet. Random coefficients in every committed row, with a random polynomial batched into the opening, follow Diamond's zero-knowledge commitment for binary fields (Diamond 2025, Construction 4.1, built on BaseFold).
§ One-time pads
Every sumcheck round value, column claim, bus sum and helper value travels as , where is an entry of a pad used once.
Addition in GF(2^128) is XOR, and subtraction is the same operation: . With uniform and used for one number only, is uniform whatever is. This is the one-time pad, on field elements.
§ Which numbers
Each table pads the first three groups of numbers, and the proof pads the last once:
| Numbers | Pad entries |
|---|---|
| ZeroCheck rounds | |
| Column claims at , this row and the next | |
| Claimed bus sums and helper values | |
| Evaluation sumcheck rounds |
The table has rows and sumcheck degree . counts its columns as the constraints read them, one per packed bit. counts the random columns of the previous section, the bits of the packed random column when the table packs bits and 0 otherwise, and the number of bus endpoints on the table; is the larger of the tallest table's and the log of the grid's width. A round sends values, since its value at 0 follows from the claim: in characteristic 2, is the claim plus . The last claim of a sumcheck takes no entry, because the masked rounds fix it.
The verifier computes the count from the program, the Config and the table heights, and hands out one pad index to each masked number it
reads, in the order the prover masked them. The same count sizes the outer statement.
§ Committed before the first challenge
The pad comes from the seed you pass to prove: entry is a hash of a seed derived from it and of . The prover
commits to the pad as the first oracle of the outer argument, and the transcript absorbs its root after the trace root
and before the first challenge. A prover that picked its pad after seeing a challenge could fit a masked number to any
value, and the root in the transcript fixes every entry first.
§ The replay on masked values
Every check inside a sumcheck is affine, and an affine step run on masked numbers gives the true result plus a combination of pad entries with coefficients the verifier knows.
In each round the verifier recovers from the running claim and , and the next claim is : the round's values combined with Lagrange weights at the challenge . On masked values, the running claim after round is the true one plus a mask :
Here pads the round's value at its point , and is the Lagrange weight of that point. ZeroCheck starts from the claimed bus sums combined with powers of , and its combines their pad entries the same way. Every coefficient is public once its challenge is drawn. The verifier replays each sumcheck on the masked rounds, ends on the masked last claim, and hands the form that separates it from the true one to the outer statement.
Two kinds of check do not survive the replay: the check at multiplies column claims, and the bus sums have to cancel in plaintext. Those, and every comparison the verifier would make on a value it now sees masked, become rows of the outer statement.
§ The outer statement
The outer statement collects every plaintext check of a proof as rows over two kinds of unknowns: the pad entries and the wires of each multiplication.
An affine row says that a combination of unknowns with public coefficients equals a public constant. A multiplication row says for three wires. The coefficients and the constants come from the challenges and the masked numbers, and both sides derive the same statement: the verifier from the proof, the prover from its trace. Padding what a verifier reads and proving its checks in an outer argument is a known compiler: Frigo and shelat (2024) state it for Ligero, and the Binius64 specification (sec. 7.1) and VEIL (Dalal et al. 2026) use it.
§ From a constraint to rows
A column claim enters the statement as , the published masked claim plus its pad entry, which is affine in the pad. Sums and scalings of affine forms stay affine. Each multiplication in a constraint gets three wires, left, right and product: two affine rows pin the left and right wires to the forms of its operands, a multiplication row ties the three, and the product wire stands for the multiplication from there up.
The checksum of AIR Constraints has one constraint, xor_step, and one
multiplication:
§ Every row of a proof
Each table contributes the first three, and the proof the last two:
- The consistency row. The ZeroCheck's last claim minus its mask equals the table's sum at , computed from unmasked claims: each constraint through its wires, weighted by , the pinned cells, the random column minus its next row, and the bus terms.
- Fixed-column rows. A fixed column's claim equals the value the verifier computes from the column's shape.
- Bus rows. Each bus endpoint adds a multiplication for with two affine rows that pin its operands, and a row that ties the helper value in the ZeroCheck to the helper's claim in the evaluation argument.
- The evaluation row, once per proof. The evaluation sumcheck's last claim minus its mask equals the value the opening certifies at .
- The squaring chain, once per proof with packed bit columns, from the next section.
One more row per bus id, across the tables, says that the unmasked claimed sums add up to zero. The statement has one multiplication for each multiplication in the constraints, one for each bus endpoint, and 127 when any table has packed columns.
§ Packed bit columns under pads
Ring switching reads the bits of the claims it reduces, and on masked claims that read leaves a correction no affine row can state. Hekate certifies it with a chain of 127 squarings per proof, whatever the number of packed claims, and virtual packing works with zero knowledge on.
§ The bits of a masked claim
A table can declare columns of bits that the prover commits packed: Keccak's 1600 state bits ride in 25 columns of 64 bits, while its constraints, and its claims, are about single bits. Ring switching, a reduction by Diamond and Posen, turns the claims about a packed column's bits into one claim about the packed column. The verifier writes the claimed values in binary, reads that bit matrix along its other axis and folds it with a challenge , as PIOP Protocol shows in the clear.
Under pads the verifier holds for every claim. Reading bits is linear over GF(2): the bits of are the bits of XOR the bits of , and the fold of the masked matrix is the true fold plus a correction that depends on the pad alone:
where is the public weight of claim in the evaluation argument. The outer statement needs to unmask the evaluation argument's starting claim. Its rows are affine over GF(2^128), and is linear over GF(2) only: no row with public coefficients reads the bits of a hidden pad entry. Diamond, who made the large-field commitment zero-knowledge, left the step open: "We leave for follow-up work the task of making ring-switching zero-knowledge" (Diamond 2025, sec. 5).
§ Committing the bits
The direct route commits the bits: 128 unknowns per hidden claim, a multiplication row for each to hold it to 0 or 1, and becomes affine in them. The zero-knowledge wrapper of Binius64 takes it for the one ring-switching reduction in each of its proofs. Hekate makes a claim for every packed bit of every table, on this row and the next: Keccak's 1600 state bits alone make 3200 claims, and at 128 multiplications each, 409,600 multiplication rows for one table.
Packed columns appear across the crates Hekate ships: Keccak, SHA-2, AES, RAM, integer and big-number arithmetic, and the stages of ML-KEM and ML-DSA. The other way out, committing every bit derived from the witness as a column of its own, gives up packing across that list.
§ One linearized polynomial
Every map from GF(2^128) to itself that is linear over GF(2) is a linearized polynomial: a sum of repeated squares of the argument with fixed coefficients (Lidl and Niederreiter, sec. 3.4). The fold of ring switching is one:
where is the trace-dual of Hekate's tower basis, which gives . Both sides compute the once is drawn.
Squaring is a field automorphism in characteristic 2, , and it moves from the hidden pad entries onto the public coefficients. That regroups the correction into 128 affine forms in the pad, one per power, however many claims the tables have:
where undoes squarings. Horner's rule, with squaring in place of multiplication by the variable, evaluates the sum in 127 squarings:
Each squaring is a multiplication row over two wires pinned equal, and each link adds two affine rows. A proof with packed columns adds 127 multiplications, 254 affine rows and 381 wires to the statement, whatever the heights and however many packed claims it has. The verifier still reads the bits of the masked claims as it does in the clear, and the correction is all that moves into the outer statement. Virtual packing stays as it was: the prover commits bits packed, and zero knowledge costs a proof with packed tables 127 multiplications.
Zero-Knowledge Ring-Switching without the Transpose proves that the chain has exactly one satisfying assignment, that it gives , and that the padded reduction is zero-knowledge whenever the outer argument is. It also measures the coordinate-matrix gadget of the Binius64 wrapper at the same degree: 16,384 multiplications between hidden wires for one reduction.
§ The zk-Ligero argument
Ligero proves that a committed vector satisfies affine rows and multiplication rows, with Reed-Solomon codes, Merkle trees and no trusted setup. Hekate runs its zero-knowledge form over GF(2^128) on the outer statement, after the journal version of Ligero by Ames, Hazay, Ishai and Venkitasubramaniam.
§ Two committed oracles
The unknowns go into rows of symbols, in two oracles:
- The pad oracle holds the pad entries. It is committed first, and its root enters the transcript before any challenge.
- The auxiliary oracle holds the wires in three aligned blocks, with the left, right and product wire of a multiplication at the same position of the same row of their blocks, and five mask rows after them. Its root comes after the tables, since the wire values exist only once the tables' challenges are drawn.
Each row is extended with random symbols to symbols, where is the number of columns the verifier opens, and encoded at rate 1/16 into symbols. The row's symbols are its values on one additive subspace of the field, and the codeword holds its values on a disjoint coset: no opened symbol is a message symbol. Each oracle is a Merkle tree over the columns of its encoded rows.
§ Three tests
After the auxiliary root, the verifier draws challenges, and the prover answers each with one polynomial, absorbed before the next challenge:
- Interleaved. A random combination of every row plus the interleaved mask, of degree below . A row far from every codeword makes the combination far from the code, and the test holds every committed row close to a codeword.
- Linear. The affine rows, combined with weights from a tensor of challenges for rows. The weights that fall on each oracle row become a polynomial, and the prover sends the sum of weight polynomials times row polynomials, plus the linear mask, of degree below . Its values on the subspace must add up to the combined constant: every affine row holds.
- Quadratic. Left times right minus product for each aligned triple of rows, combined with one challenge per triple, plus the quadratic mask, of degree below . It must vanish at every message position: every multiplication holds.
The verifier then draws column positions, and the prover opens both oracles there with Merkle multiproofs. At every opened position, each response must equal the same combination of the opened symbols, and a response that does not come from the committed rows disagrees with them at a large share of positions.
§ What the outer proof hides
The responses are combinations of hidden rows, and each one carries a mask that is uniform over the whole space its test allows:
| Test | Mask | Rows |
|---|---|---|
| Interleaved | A random codeword | 1 |
| Linear | A random polynomial of degree below whose values on the subspace sum to zero | 2 |
| Quadratic | A random polynomial of degree below that vanishes on the message positions | 2 |
A polynomial of degree below takes two rows to cover: a low row, and a high row multiplied by , the polynomial that vanishes on the subspace. A low row alone reaches fewer than half the dimensions of either space, and the response would then carry information about the rows.
Row privacy covers the openings. With random symbols in every row, any opened symbols of a row are uniform and independent of its message: Ligero's Lemma 4.15, which asks for . The masks and the random symbols together make the outer argument perfect honest-verifier zero-knowledge. A simulator draws the opened symbols and the responses uniformly from their allowed spaces and solves for the mask rows' symbols at the opened positions.
§ Soundness and size
A cheating prover passes one opened column with probability at most : Lemmas 4.6 and 4.10 of the Ligero
journal version, balanced at distance . At rate 1/16 that is 9/16, about 0.83 bits per column.
outer_queries in the Config sets , and Config::prod() takes the fewest columns that clear its 100-bit floor.
Two terms do not shrink with : Lemma 4.6 adds , which the cap keeps at 104 bits or more,
and the tensor of the linear test adds .
Both sides derive , , and from the number of multiplications and the Config; the pad entries set only
the number of pad rows. The proof carries none of them: a geometry the prover could choose would be a free parameter, and the
query bound would no longer hold. A statement too large for the domain cap is rejected.
§ Why the proof is zero-knowledge
A simulator that holds the statement and the verifier's challenges, and no witness, produces a view distributed like a real proof's, up to breaking the seed's generator or the hash.
- It commits an all-zero trace with real support and real random columns. The roots, the opened columns, the folds and the values at are distributed as in a real proof.
- It picks plaintext numbers that pass every check for its challenges: column claims of its choice, the ZeroCheck's last value from the constraints, each sumcheck round with its free coefficient, bus sums that cancel bus by bus, and each evaluation argument ending on the zero trace's value. Every step is a linear solve with a free coordinate.
- It draws the published numbers uniformly and sets the pad to their difference from the plaintexts.
- It builds the outer argument from the published numbers, the challenges and the public constants alone, as the previous section describes.
In a real proof the published numbers are uniform because the pad is, the outer openings by row privacy, and the responses by their masks. A Hekate proof is non-interactive, and its challenges are hashes of the transcript: the simulator programs the hash as a random oracle, fixing the published values first and answering each transcript prefix with the challenge it chose. The distinguishing advantage is bounded by four terms: the generator of the pad, the generator of the support and the random columns, the hash, and a statistical term of about the number of bus rows divided by , the chance that a LogUp denominator is zero on some row.
The outer part runs as a test. simulated_view_verifies builds both roots, both openings and the three responses
from the challenges and the public constants, and passes every check of the outer verifier at fixed challenges; no
simulator passes the shipped verifier, whose challenges are hashes. The hiding of the commitment follows two published
constructions, the randomizable support of the Binius64 specification and Diamond's, which Hekate cites and does not
re-prove.
§ What stays with you
Zero knowledge covers the bytes of the proof. Three things stay with the code that calls the prover.
- The seed. Draw the 32 bytes you pass to
provefresh from a cryptographic random generator for every proof. Every pad entry and random symbol comes from it, and one seed used for two different witnesses repeats the support and the random columns: opened symbols at a shared position then differ by the encoded difference of the two traces. - The prover variant. The
publicprover runs variable-time arithmetic, and its timing depends on the values it proves. Prove private witnesses withct: zero knowledge hides the trace from the proof, and a timing channel on the proving device is a separate leak. - The shape. The program, the public inputs and every table height are public. Choose heights from public data: a height that follows a secret, such as the length of a hashed message, discloses which power of two it fits in.
Config { zero_knowledge: false, ..Config::default() } proves in the clear, faster and with a smaller proof, and it
reveals what the example above solves for: keep it for data that is public
anyway. Config::dev() turns zero knowledge off as well. Benchmarks lists both modes for each
workload.
§ Next steps
- PIOP Protocol: the claims the pads hide, from sumcheck to the opening.
- Prover Engine: the four steps of the prover, and the seed it takes.
- LogUp Buses: the helper columns and bus sums the outer statement settles.
- Zero-Knowledge Ring-Switching without the Transpose: the squaring chain, with its proofs.