Skip to content
Oumuamua Labs
Hekate ZK Engine menu

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.

Updated zero-knowledge.md
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 rfinalr_{\text{final}}, 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, −4-4, −3-3 and 6. Rows 0, 1 and 3 hold the public values 3, 5 and 6, row 2 holds a secret ss, and the proof claims 16:

16=2⋅3−4⋅5−3s+6⋅6=22−3s,s=2. 16 = 2 \cdot 3 - 4 \cdot 5 - 3s + 6 \cdot 6 = 22 - 3s, \qquad s = 2.

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 qq and the values at r′r' 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 carriesHidden by
One Merkle root over all tables, and the helper and outer rootsThe hash, over leaves that look random
Opened columns of every tableRandom support in every encoded row
The row fold qq and the committed values at r′r'One random column in each batch
Sumcheck rounds, column claims, bus sums and helper valuesA one-time pad
Opened columns of the outer argumentt+1t + 1 random symbols in every row
The three responses of the outer argumentA mask over the whole space each test allows
The point rfinalr_{\text{final}}Nothing: a hash of the transcript
The last claim of each sumcheckNothing: 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.

One row of a committed table: a support of three random symbols, outlined, and the trace values 3, 5, 2, 6 and 1, read as the coefficients of one polynomial and evaluated at 16 points. Every position of the encoded row mixes in the support, and two opened positions are outlined. Any num_queries opened positions are uniform, and the evaluation at r′ reads the trace values alone.
Random support in every row. Sizes are illustrative.

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 r′r' reads only the trace part of qq, 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 qq and the values of the committed columns at r′r'. 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 η\eta and opens the batch at r′r', 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 qq and a uniform term to each value at r′r', 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 v′=v+pv' = v + p, where pp is an entry of a pad used once.

Addition in GF(2^128) is XOR, and subtraction is the same operation: v=v′+pv = v' + p. With pp uniform and used for one number only, v′v' is uniform whatever vv 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:

NumbersPad entries
ZeroCheck roundsn⋅dn \cdot d
Column claims at rfinalr_{\text{final}}, this row and the next2(V+K+R+B)2\,(V + K + R + B)
Claimed bus sums and helper values2B2B
Evaluation sumcheck rounds2ν2\nu

The table has 2n2^n rows and sumcheck degree dd. VV counts its columns as the constraints read them, one per packed bit. KK counts the random columns of the previous section, R=128R = 128 the bits of the packed random column when the table packs bits and 0 otherwise, and BB the number of bus endpoints on the table; ν\nu is the larger of the tallest table's nn and the log of the grid's width. A round sends dd values, since its value at 0 follows from the claim: in characteristic 2, g(0)g(0) is the claim plus g(1)g(1). 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 ii is a hash of a seed derived from it and of ii. 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 g(0)g(0) from the running claim and g(1)g(1), and the next claim is g(r)g(r): the round's values combined with Lagrange weights at the challenge rr. On masked values, the running claim after round ii is the true one plus a mask mim_i:

mi=L0(ri)(mi−1+pi,1)+∑t=1dLt(ri)pi,t m_i = L_0(r_i)\,\big(m_{i-1} + p_{i,1}\big) + \sum_{t=1}^{d} L_t(r_i)\, p_{i,t}

Here pi,tp_{i,t} pads the round's value at its point tt, and LtL_t is the Lagrange weight of that point. ZeroCheck starts from the claimed bus sums combined with powers of α\alpha, and its m0m_0 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 mnm_n that separates it from the true one to the outer statement.

Two kinds of check do not survive the replay: the check at rfinalr_{\text{final}} 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 x⋅y=zx \cdot y = z 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 c′+pc' + p, 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:

The constraint col(active) times the sum of next(acc), col(acc) and col(word), drawn as a tree: a multiplication over the leaf a and a sum of the leaves n, c and w. Each leaf is its published masked claim plus a pad entry, from a′ plus p1 to w′ plus p4. The multiplication gets three wires, L, R and P, and four outer rows follow: L equals a′ plus p1, R equals n′ plus p2 plus c′ plus p3 plus w′ plus p4, L times R equals P, and P joins the table's sum at r_final.
xor_step in the outer statement: one multiplication, three wires, two affine rows.

§ 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 rfinalr_{\text{final}}, computed from unmasked claims: each constraint through its wires, weighted by αkeq(τ,rfinal)\alpha^k \, \mathrm{eq}(\tau, r_{\text{final}}), 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 h⋅𝑘𝑒𝑦h \cdot \mathit{key} 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 r′r'.
  • 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 r′′r'', as PIOP Protocol shows in the clear.

Under pads the verifier holds c′=c+pc' = c + p for every claim. Reading bits is linear over GF(2): the bits of c′c' are the bits of cc XOR the bits of pp, and the fold of the masked matrix is the true fold plus a correction that depends on the pad alone:

Δ=∑cacφ(pc),φ(x)=∑u=0127eq(r′′,u)bitu(x) \Delta = \sum_c a_c \, \varphi(p_c), \qquad \varphi(x) = \sum_{u=0}^{127} \mathrm{eq}(r'', u) \, \mathrm{bit}_u(x)

where aca_c is the public weight of claim cc in the evaluation argument. The outer statement needs Δ\Delta to unmask the evaluation argument's starting claim. Its rows are affine over GF(2^128), and φ\varphi 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 b⋅b=bb \cdot b = b for each to hold it to 0 or 1, and φ\varphi 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:

φ(x)=∑j=0127µjx2j,µj=∑u=0127eq(r′′,u)γu2j \varphi(x) = \sum_{j=0}^{127} \mu_j \, x^{2^j}, \qquad \mu_j = \sum_{u=0}^{127} \mathrm{eq}(r'', u) \, \gamma_u^{2^j}

where γ0,…,γ127\gamma_0, \dots, \gamma_{127} is the trace-dual of Hekate's tower basis, which gives bitu(x)=Tr(γux)\mathrm{bit}_u(x) = \mathrm{Tr}(\gamma_u x). Both sides compute the µj\mu_j once r′′r'' is drawn.

Squaring is a field automorphism in characteristic 2, (x+y)2=x2+y2(x + y)^2 = x^2 + y^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:

Tj=∑cσ−j(µjac)pc,Δ=∑j=0127Tj2j T_j = \sum_c \sigma^{-j}(\mu_j a_c) \, p_c, \qquad \Delta = \sum_{j=0}^{127} T_j^{\,2^j}

where σ−j\sigma^{-j} undoes jj squarings. Horner's rule, with squaring in place of multiplication by the variable, evaluates the sum in 127 squarings:

X127=T127,Xj=Tj+Xj+12,Δ=X0. X_{127} = T_{127}, \qquad X_j = T_j + X_{j+1}^2, \qquad \Delta = X_0.

Two ways to certify the correction. Committing the bits: 128 bit unknowns, each with b times b equals b, repeated for every packed claim. The Frobenius-Horner chain, once per proof: the pad entries of all packed claims give 128 affine forms T0 to T127; X127 equals T127, each next X squares the one before and adds the next T, through 125 more links to X1, and a last squaring plus T0 gives Delta, which is X0. 127 squarings, one multiplication row each.
The correction for every packed claim of a proof, 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 Δ\Delta, 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 ll 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 t+1t + 1 random symbols to k=l+t+1k = l + t + 1 symbols, where tt is the number of columns the verifier opens, and encoded at rate 1/16 into n=16kn = 16k symbols. The row's kk 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.

The two oracles of the outer argument, each row l symbols followed by t plus 1 random ones. The pad oracle holds the pad entries, and its root comes first. The auxiliary oracle holds blocks of left, right and product wires and five mask rows, one interleaved, two linear and two quadratic, the first quadratic one with a zero message, and its root comes last. One position is outlined in all three wire blocks: one multiplication, L times R equals P. Each row of k symbols encodes into n equals 16k, and both trees open the same t columns.
The pad and auxiliary oracles of the outer argument. Sizes are illustrative.

§ Three tests

After the auxiliary root, the verifier draws challenges, and the prover answers each with one polynomial, absorbed before the next challenge:

  1. Interleaved. A random combination of every row plus the interleaved mask, of degree below kk. A row far from every codeword makes the combination far from the code, and the test holds every committed row close to a codeword.
  2. Linear. The affine rows, combined with weights from a tensor of ⌈log2⁡M⌉\lceil \log_2 M \rceil challenges for MM 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 2k2k. Its values on the subspace must add up to the combined constant: every affine row holds.
  3. 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 2k2k. It must vanish at every message position: every multiplication holds.

The verifier then draws tt 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:

TestMaskRows
InterleavedA random codeword1
LinearA random polynomial of degree below 2k2k whose values on the subspace sum to zero2
QuadraticA random polynomial of degree below 2k2k that vanishes on the message positions2

A polynomial of degree below 2k2k takes two rows to cover: a low row, and a high row multiplied by ZkZ_k, 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 t+1t + 1 random symbols in every row, any tt opened symbols of a row are uniform and independent of its message: Ligero's Lemma 4.15, which asks for k>l+tk > l + t. 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 (n+2k)/(2n)(n + 2k)/(2n): Lemmas 4.6 and 4.10 of the Ligero journal version, balanced at distance e=(n−2k)/2e = (n - 2k)/2. At rate 1/16 that is 9/16, about 0.83 bits per column. outer_queries in the Config sets tt, and Config::prod() takes the fewest columns that clear its 100-bit floor. Two terms do not shrink with tt: Lemma 4.6 adds n/2128n / 2^{128}, which the cap n≤224n \le 2^{24} keeps at 104 bits or more, and the tensor of the linear test adds ⌈log2⁡M⌉/2128\lceil \log_2 M \rceil / 2^{128}.

Both sides derive ll, kk, nn and tt 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.

  1. It commits an all-zero trace with real support and real random columns. The roots, the opened columns, the folds and the values at r′r' are distributed as in a real proof.
  2. 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.
  3. It draws the published numbers uniformly and sets the pad to their difference from the plaintexts.
  4. 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 21282^{128}, the chance that a LogUp denominator γ+𝑘𝑒𝑦\gamma + \mathit{key} 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 prove fresh 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 public prover runs variable-time arithmetic, and its timing depends on the values it proves. Prove private witnesses with ct: 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