Zero-Knowledge WHIR I: Why Proximity Testing Leaks, and the Sumcheck That Doesn't

May 30, 2026

What problem are we solving?

If you’ve followed the code-agnostic IOP line — WHIR, Ligerito, the Blaze family — you’ve gotten fast, succinct, post-quantum proximity testing for “constrained interleaved linear codes.” What you have not gotten is privacy.

The proximity test reveals query answers on a codeword ff, and ff encodes the witness. Strip the rest of the protocol away and the leak is right there in the IOPP: WHIR is not zero-knowledge, and neither is anything else in this family. For most deployed SNARKs that doesn’t matter — you wrap your prover in Fiat–Shamir, you don’t care that the verifier sees the witness because there is no verifier. But the moment you build something where there is a verifier — anonymous credentials, identity, age assurance — you need a zero-knowledge proof. The honest-verifier flavor is enough; the malicious-verifier flavor follows from Fiat–Shamir for free.

The state of the art on the zero-knowledge side has been bleak:

  • Concretely-efficient ZK IOPs. Exist, but pay 2×\geq 2\times overhead in prover and verifier time, and are tied to specific polynomial codes — Reed–Solomon, mostly.
  • [BCL22] — a tensor-code-based ZK IOP that works for any zero-knowledge code, but 2×\geq 2\times overhead and leans on heavy PCP machinery, so not concretely efficient.
  • [RW24] — pushes the overhead down to 1+o(1)1 + o(1) relative to witness size, but only targets constant soundness error.

None of the state-of-the-art code-agnostic IOPs provide zero-knowledge. That is the gap.

Chiesa, Fenzi, and Weissenberg (Feb 2026) close it. They present an honest-verifier zero-knowledge IOPP for constrained interleaved linear codes that:

  • Works for any F\mathbb F-additive code with a zero-knowledge encoding — fully code-agnostic.
  • Costs 1+o(1)1 + o(1) in proof length and prover/verifier time relative to the non-private state of the art.
  • Achieves round-by-round knowledge soundness with a straightline extractor and negligible error.
  • Plugs into the WHIR/Ligerito recursion shape directly: Theorem 2 of the paper, together with the building blocks we’re about to develop, literally yields an HVZK variant of WHIR and Ligerito. That is the headline.

This article is Part 1 of two. We build the foundations: what a zero-knowledge encoding is, the “committed sumcheck relation” the protocol actually tests, the right definition of HVZK for interactive oracle reductions (the standard one breaks under composition — we’ll see exactly why), and the first of the two key technical pieces, an HVZK sumcheck reduction that drops a 2k2^k-interleaved code down to its base code without paying a 2×2\times overhead. Part 2 does the zero-knowledge code-switching protocol and assembles everything into the HVZK WHIR/Ligerito corollary.

Prerequisites. You should know sumcheck, multilinear extensions, finite fields, list decoding, and the shape of WHIR-style folding. We lean on the WHIR post for all of that — its folding section in particular. Out-of-domain sampling becomes load-bearing in Part 2; you can skim its WHIR-post treatment when you get there. Everything else (zero-knowledge encodings, the committed relation, HVZK for IORs) we build from scratch.

Tensoring vs. interleaving

Code-agnostic protocols need some structure to hang their tricks on, and there are two canonical ways to manufacture it from a base code: tensoring and interleaving. Sister operations, very different bills. The bill is paid in two numbers, so fix them first.

The base code and its two numbers. Pin CΣ4\mathcal C \subseteq \Sigma^4 — block length n=4n = 4, dimension k=2k = 2 (two message symbols in, four out), minimum distance d=3d = 3. Reed–Solomon over any F\mathbb F with four evaluation points does exactly this. The two numbers a proximity test cares about:

  • Rate ρ=k/n=1/2\rho = k/n = 1/2. How much of the codeword is payload. Lower rate = more redundancy = longer codeword per message symbol = more prover work and a bigger oracle.
  • Relative distance δ=d/n=3/4\delta = d/n = 3/4. How far apart codewords sit. This is what soundness is bought with: a word that isn’t a codeword must disagree with the nearest one in a δ\delta-ish fraction of positions, so random spot checks catch it.

These normally trade off — Singleton says ρ+δ1+1/n\rho + \delta \leq 1 + 1/n, and RS meets it. Now apply both operations and watch what happens to (ρ,δ)(\rho, \delta).

Tensoring: pay on both axes

CC\mathcal C \otimes \mathcal C is the set of 4×44 \times 4 matrices whose every row and every column is a codeword of C\mathcal C:

[v1(1)v1(2)v1(3)v1(4)v2(1)v2(2)v2(3)v2(4)v3(1)v3(2)v3(3)v3(4)v4(1)v4(2)v4(3)v4(4)]\begin{bmatrix} v_1(1) & v_1(2) & v_1(3) & v_1(4) \\ v_2(1) & v_2(2) & v_2(3) & v_2(4) \\ v_3(1) & v_3(2) & v_3(3) & v_3(4) \\ v_4(1) & v_4(2) & v_4(3) & v_4(4) \end{bmatrix}

How you build one. Start with a 2×22 \times 2 block of message symbols. Encode each of its 2 rows with C\mathcal C → a 2×42 \times 4 array. Then encode each of the 4 columns with C\mathcal C → the full 4×44 \times 4. Columns are codewords by construction. Rows still are too: column-encoding applies one fixed linear map down each column, so every output row is a fixed linear combination of the two input rows, and C\mathcal C is linear.

Why rate squares. Read the shapes off that construction: k2=4k^2 = 4 message symbols went in, n2=16n^2 = 16 symbols came out. Rate =k2/n2=ρ2=1/4= k^2/n^2 = \rho^2 = 1/4.

Why distance squares. Take any nonzero MCCM \in \mathcal C \otimes \mathcal C. Some column of it is nonzero — and that column is a nonzero codeword of C\mathcal C, so it has d\geq d nonzero entries. Each of those entries sits in a distinct row, so d\geq d rows of MM are nonzero. Each such row is itself a nonzero codeword, contributing d\geq d nonzero entries. Total d2=9\geq d^2 = 9 nonzero entries out of n2=16n^2 = 16. Relative distance δ2=9/16\delta^2 = 9/16, down from 3/43/4. Tight, too — the outer product vwv \cdot w^\top of two minimum-weight codewords hits exactly d2d^2.

The catch: those are two independent losses, not a trade. ρ+δ\rho + \delta went from 5/45/4 down to 13/16<113/16 < 1 — the tensor code is strictly inside the Singleton bound now. Concretely: at block length 16 and dimension 4, a plain RS code gets δ=13/16\delta = 13/16. The tensor code gets 9/169/16 at the same rate. You paid a longer codeword and handed the adversary more room, for structure.

Interleaving: pay in alphabet

The 33-interleaving C3(Σ3)4\mathcal C^{\equiv 3} \subseteq (\Sigma^3)^4 stacks three codewords v1,v2,v3Cv_1, v_2, v_3 \in \mathcal C and reads them column-wise:

u  =  [(v1(1),v2(1),v3(1)),  (v1(2),v2(2),v3(2)),  (v1(3),v2(3),v3(3)),  (v1(4),v2(4),v3(4))].u \;=\; \big[\,(v_1(1), v_2(1), v_3(1)),\; (v_1(2), v_2(2), v_3(2)),\; (v_1(3), v_2(3), v_3(3)),\; (v_1(4), v_2(4), v_3(4))\,\big].

Length-4 word, but each “letter” is now a triple over Σ\Sigma. Nothing was combined — three codewords travel in parallel and a “position” means all three at once.

Why rate is unchanged. 3k=63k = 6 message symbols over Σ\Sigma in, 3n=123n = 12 out. Rate =3k/3n=ρ=1/2= 3k/3n = \rho = 1/2. Interleaving replicates the encoder; replication doesn’t change the payload fraction.

Why relative distance is unchanged. A position of uu is nonzero iff any of the three stacked codewords is nonzero there. If u0u \neq 0 then some vi0v_i \neq 0, and that viv_i alone already forces d=3\geq d = 3 nonzero positions. So d(C3)dd(\mathcal C^{\equiv 3}) \geq d — and taking v1v_1 minimum-weight with v2=v3=0v_2 = v_3 = 0 shows it’s exactly dd. Block length is still 4, so δ=3/4\delta = 3/4, untouched.

The price is the alphabet. ΣΣ3\Sigma \to \Sigma^3, and in general ΣΣK\Sigma \to \Sigma^K. One query to an interleaved codeword returns KK base symbols, whether or not you wanted them — so a qq-query test costs qKqK base-alphabet symbols. That’s what “precludes query-optimal IOPs” means: you cannot get the per-query cost below KK.

Tensor C⊗C as a 4x4 matrix with rows and columns both in C, alongside the 3-interleaving C^≡3 as four length-3 column tuples stacked from three codewords v1, v2, v3 in C, showing the alphabet inflate from Σ to Σ^3 while block length stays 4.

Side by side

C\mathcal C (base)CC\mathcal C \otimes \mathcal CC3\mathcal C^{\equiv 3}
Block length44161644
AlphabetΣ\SigmaΣ\SigmaΣ3\Sigma^3
Rate1/21/21/41/4 (ρ2)(\rho^2)1/21/2 (ρ)(\rho)
Relative distance3/43/49/169/16 (δ2)(\delta^2)3/43/4 (δ)(\delta)
Buys youstructure on two axesparallel claims, one position
Costs yourate and distanceK×K\times alphabet

The two consequences that matter downstream:

  • Tensoring opens up local testability and tensor sumchecks — there is structure on both axes to interrogate. This is the world TensorSwitch lives in. But the base code has to be good enough to survive squaring, and encoding is slower (n+kn + k encoder calls instead of one).
  • Interleaving leaves (ρ,δ)(\rho, \delta) alone, so it works for every code with no parameter slack required. Encoding is KK independent calls — embarrassingly parallel, and streamable in small space since you never hold more than one base codeword.

The WHIR/Ligerito family lives in the interleaving world, and so does this paper. “Works for every code” is exactly the code-agnostic promise we want to keep — and the alphabet blowup, which looks like pure cost here, turns out to be a soundness lever later: the KK symbols a query returns share a position, and that correlation is what keeps the list-decoding union bound from exploding, in §“The HVZK sumcheck reduction”.

Zero-knowledge encodings: the foundational object

Why plain codes leak

A plain linear-code encoding is deterministic: one message in, one codeword out. Look at three coordinates of the codeword, you’ve learned three linear functions of the message. Look at enough coordinates, you can reconstruct the message exactly. That’s the entire story of why WHIR-style proximity testing leaks — the verifier’s spot checks are reading positions of a codeword that’s a fixed function of the witness, and you can’t peek at the witness “a little bit” without peeking at it.

The fix, intuitively

Make the encoding randomized. Now the same message produces many different codewords — one per choice of fresh encoder randomness. The encoder picks one at random and sends it. The promise we want: anyone reading a few coordinates of the sent codeword should learn nothing about the underlying message. Every plausible message had a way to land on those exact coordinates, so the coordinates the verifier sees are consistent with anyone’s witness.

Think of it as a one-time pad with a budget. You can hide up to tt coordinates by spending enough random coins on the encoding. Beyond tt, message structure starts showing through. The number tt is a parameter of the encoding — exactly the number of “free” reads the verifier is allowed without learning anything.

The smallest possible example

The mechanics fall out in the tiniest Reed–Solomon case. One message coefficient mm, one random coefficient rr. The encoder treats (m,r)(m, r) as a polynomial

p(X)  =  m+rXp(X) \;=\; m + r \cdot X

and evaluates pp at fixed nonzero points x1,x2,,xnx_1, x_2, \dots, x_n. The codeword is (p(x1),p(x2),,p(xn))(p(x_1), p(x_2), \dots, p(x_n)). Now look at any single output coordinate:

p(xi)  =  m+rxi.p(x_i) \;=\; m + r \cdot x_i.

Since rr is uniform over F\mathbb F and xix_i is a fixed nonzero scalar, rxir \cdot x_i is uniform, and p(xi)p(x_i) — the value the verifier sees — is uniform too. The message mm contributes a constant shift, but the random coin rr has already smeared everything across F\mathbb F. One coordinate, perfectly hidden.

Two coordinates is more interesting, because rr links them: p(xi)p(xj)=r(xixj)p(x_i) - p(x_j) = r \cdot (x_i - x_j). The verifier can subtract them to recover rr, then back out mm. So t=1t = 1 is the budget with this small encoding.

Scaling up: hiding tt coordinates

Want tt hidden coordinates instead of one? Spend tt coins. Take rtr \geq t fresh random field elements r\boldsymbol r, stack them on top of the message coefficients m\boldsymbol m, and encode the whole thing:

p(X)  =  j=01mjXjpm(X)  +  Xj=0r1rjXjnoise,p(X) \;=\; \underbrace{\sum_{j=0}^{\ell-1} m_j X^j}_{p_{\boldsymbol m}(X)} \;+\; X^{\ell} \cdot \underbrace{\sum_{j=0}^{r-1} r_j X^{j}}_{\text{noise}},

degree +r1\ell + r - 1, evaluated at distinct nonzero x1,,xnx_1, \dots, x_n. The one-coordinate example was this with =r=1\ell = r = 1.

The counting, first. pp has +r\ell + r coefficients, so +r\ell + r evaluations determine it outright — and then m\boldsymbol m is yours. That budget splits cleanly: the rr coins absorb the first rr reads, and the remaining \ell are what pin down the \ell message coefficients. So trt \leq r ought to be free and t>rt > r ought to leak. The rest of this section makes that exact.

What we have to show is that for any query set SS of size tt, the tuple (p(xi))iS(p(x_i))_{i \in S} is uniform over Ft\mathbb F^t jointly — not coordinate-by-coordinate, since two individually-uniform values can still satisfy a relation that pins down m\boldsymbol m, which is how the =r=1\ell = r = 1 encoding died at two queries.

Reading is affine in the coins. Fix m\boldsymbol m and expand a single read, multiplying the XX^{\ell} through:

p(xi)  =  pm(xi)  +  r0xi  +  r1xi+1  +    +  rr1xi+r1.p(x_i) \;=\; p_{\boldsymbol m}(x_i) \;+\; r_0\,x_i^{\ell} \;+\; r_1\,x_i^{\ell+1} \;+\; \cdots \;+\; r_{r-1}\,x_i^{\ell + r - 1}.

Read that as: a constant, plus a fixed row of rr numbers dotted with the coin vector. The row belongs to the query — it’s (xi,xi+1,,xi+r1)(x_i^{\ell}, x_i^{\ell+1}, \dots, x_i^{\ell+r-1}), computable by anyone who knows xix_i. Now write one such row per queried position and stack them. That stacking is all “collect over SS” meant, and it turns the tt reads into a single matrix equation:

(p(xi1)p(xit))what the verifier sees  =  (pm(xi1)pm(xit))depends only on m  +  (xi1xi1+1xi1+r1xitxit+1xit+r1)M,  t×r(r0rr1)the coins\underbrace{\begin{pmatrix} p(x_{i_1}) \\ \vdots \\ p(x_{i_t}) \end{pmatrix}}_{\text{what the verifier sees}} \;=\; \underbrace{\begin{pmatrix} p_{\boldsymbol m}(x_{i_1}) \\ \vdots \\ p_{\boldsymbol m}(x_{i_t}) \end{pmatrix}}_{\text{depends only on } \boldsymbol m} \;+\; \underbrace{\begin{pmatrix} x_{i_1}^{\ell} & x_{i_1}^{\ell+1} & \cdots & x_{i_1}^{\ell+r-1} \\ \vdots & \vdots & & \vdots \\ x_{i_t}^{\ell} & x_{i_t}^{\ell+1} & \cdots & x_{i_t}^{\ell+r-1} \end{pmatrix}}_{M,\ \ t \times r} \underbrace{\begin{pmatrix} r_0 \\ \vdots \\ r_{r-1} \end{pmatrix}}_{\text{the coins}}

One row per query, one column per coin — so MM is t×rt \times r by construction, and entry (i,j)(i,j) says how much coin jj moves read ii. The worked example below is literally this: =1\ell = 1, r=2r = 2, queries at x=1,2x = 1, 2 give rows (11,12)=(1,1)(1^1, 1^2) = (1,1) and (21,22)=(2,4)(2^1, 2^2) = (2,4).

Where rank comes in. As r\boldsymbol r ranges over all of Fr\mathbb F^r, the product MrM\boldsymbol r ranges over exactly the span of MM‘s columns, and the m\boldsymbol m-term only shifts that span bodily. So the reads fill Ft\mathbb F^t precisely when those columns span Ft\mathbb F^t — which is what rank(M)=t\mathrm{rank}(M) = t means. Two ceilings follow immediately, and they’re the whole story:

rank(M)    min(t,r)\mathrm{rank}(M) \;\leq\; \min(t, r)

— at most rr, because rr vectors can’t span more than rr dimensions; at most tt, because the span sits inside Ft\mathbb F^t.

  • t>rt > r: deficient, always. rank(M)r<t\mathrm{rank}(M) \leq r < t, so the image is a proper subspace of Ft\mathbb F^t. You cannot span a tt-dimensional space with r<tr < t vectors, and no choice of code or evaluation points changes that.
  • trt \leq r: the ceiling is tt, and MM reaches it — but only because of which numbers are in it. That is the one place the code earns its keep, and it’s what Vandermonde means.

Vandermonde, in one line. A Vandermonde matrix has geometric progressions for rows, (1,xi,xi2,)(1, x_i, x_i^2, \dots), and with distinct xix_i a square one is invertible — a fact you already know in polynomial clothing, since a nonzero polynomial of degree <t< t cannot vanish at tt distinct points. Our MM is exactly that, row ii scaled by the nonzero xix_i^{\ell}. So for trt \leq r its leftmost t×tt \times t block is an invertible Vandermonde, the rows are independent, and the rank hits tt.

Full rank is the property with teeth: every tt-tuple the verifier might read is produced by some coin vector, for every message. Uniform coins through a surjection stay uniform, and the m\boldsymbol m-shift doesn’t disturb that — so the reads are uniform over Ft\mathbb F^t whatever m\boldsymbol m is, and the simulator just samples tt uniform field elements. Perfect tt-query zero-knowledge, ζ=0\zeta = 0.

Both cases on one example. Take =1\ell = 1, r=2r = 2, so p(X)=m+r0X+r1X2p(X) = m + r_0X + r_1X^2, over any F\mathbb F with charF>3\mathsf{char}\,\mathbb F > 3. Querying x=1,2x = 1, 2 is t=2rt = 2 \leq r, and MM is the 2×22 \times 2 block from above:

(1124)(r0r1)=(y1my2m),det=20.\begin{pmatrix} 1 & 1 \\ 2 & 4 \end{pmatrix}\begin{pmatrix} r_0 \\ r_1 \end{pmatrix} = \begin{pmatrix} y_1 - m \\ y_2 - m \end{pmatrix}, \qquad \det = 2 \neq 0.

Invertible, so whatever target (y1,y2)(y_1, y_2) you name and whatever mm is, exactly one coin pair hits it — the image is all of F2\mathbb F^2, and the message only relabels which coins get you there. Now add x=3x = 3, making t=3>rt = 3 > r, and take c=(3,3,1)\boldsymbol c = (3, -3, 1):

3p(1)3p(2)+p(3)  =  (33+1)m  +  (36+3)r0  +  (312+9)r1  =  m.3\,p(1) - 3\,p(2) + p(3) \;=\; (3-3+1)\,m \;+\; (3-6+3)\,r_0 \;+\; (3-12+9)\,r_1 \;=\; m.

Both coins cancel and the message falls out in the clear. Those weights are the Lagrange coefficients for interpolating back to X=0X = 0, and p(0)=mp(0) = m.

Such a c\boldsymbol c exists whenever rank(M)<t\mathrm{rank}(M) < t — a proper subspace is cut out by a linear relation, so some nonzero c\boldsymbol c kills every column of MM and leaves icip(xi)=icipm(xi)\sum_i c_i\,p(x_i) = \sum_i c_i\,p_{\boldsymbol m}(x_i), computable from what was read. The reachable tuples form a coset of MM‘s column span, the message picks which coset, and c\boldsymbol c reads off which one you’re on.

Two panels contrasting the rank of the map from coin space to read coordinates, under the header 'the reachable tuples form a subspace of dimension rank(M) ≤ min(t, r) — that one inequality decides both cases'. Left panel, t ≤ r: a box labeled coin space F^r has two arrows leaving it, one blue for message m and one red for message m′, both labeled rank = t (Vandermonde), both landing on a fully shaded square labeled F^t — what the verifier reads, annotated 'every point reachable, image = all of F^t'. Caption: both maps are surjective so both views are uniform on F^t and the verifier cannot tell m from m′; the simulator samples t uniform field elements. Right panel, t > r: the same coin space box, arrows labeled rank ≤ r < t (forced by counting), landing inside a mostly empty square labeled F^t — too big to cover, where the image is two thin parallel shaded strips, blue labeled m and red labeled m′. Caption: a nonzero c kills the coin part leaving Σ c_i p(x_i) = Σ c_i p_m(x_i), so the verifier learns which coset it is on — a linear functional of m.

The cost. The codeword now carries +r\ell + r coefficients, so you either add rr evaluation points to hold the rate fixed or keep nn and give up relative distance. Either way you pay rr symbols — but rr only has to cover the verifier’s total query budget, which grows with the security parameter and polylogarithmically in \ell, never linearly. So r/0r/\ell \to 0, and that is one of the places the 1+o(1)1 + o(1) headline comes from.

The abstract object

That picture generalizes to any code. We work over F\mathbb F-additive codes — codes CΣm\mathcal C \subseteq \Sigma^m whose alphabet Σ\Sigma is an F\mathbb F-linear vector space (e.g. Σ=Fι\Sigma = \mathbb F^\iota). Σ=F\Sigma = \mathbb F recovers ordinary linear codes; the interleaved CK\mathcal C^{\equiv K} from the previous section is F\mathbb F-additive whenever C\mathcal C is. The encoding is a randomized map

EncC ⁣:F×FrΣm\mathsf{Enc}_{\mathcal C} \colon \mathbb F^\ell \times \mathbb F^r \to \Sigma^m

taking a message and randomness to a codeword. It is a tt-query zero-knowledge encoding with error ζ\zeta if (i) its image lies in C\mathcal C, and (ii) for any message m\boldsymbol m and any index set S[m]S \subseteq [m] of size t\leq t, the distribution EncC(m,r)[S]\mathsf{Enc}_{\mathcal C}(\boldsymbol m, \boldsymbol r)[S] over fresh rFr\boldsymbol r \leftarrow \mathbb F^r can be simulated up to statistical error ζ\zeta given only SS — not m\boldsymbol m.

Reed–Solomon with appended randomness hits the definition with ζ=0\zeta = 0 (the Vandermonde argument above). [BCL22] gives linear-time-encodable instances. The construction is code-agnostic — we won’t pin down C\mathcal C beyond this property.

Notation throughout. Bold m\boldsymbol m = the message (\ell field elements). Plain ff = its codeword: f=EncC(m,r)f = \mathsf{Enc}_{\mathcal C}(\boldsymbol m, \boldsymbol r). The verifier sees ff as an oracle; the prover knows m\boldsymbol m and r\boldsymbol r.

The committed sumcheck relation

What’s actually being proved

Step back. WHIR’s job — and Ligerito’s, and Blaze’s, and every other code-agnostic IOPP’s — is to convince the verifier of one shape of claim: “the message inside this codeword satisfies some specified linear constraint.” The constraint is the point of the proof; the codeword is the vehicle.

What does “a specified linear constraint” look like? Three examples worth sitting with:

  • “The polynomial whose coefficients are m\boldsymbol m evaluates to μ\mu at point xx.” This is the standard polynomial-commitment opening claim. Same as m,(1,x,x2,,x1)=μ\langle \boldsymbol m, (1, x, x^2, \dots, x^{\ell-1}) \rangle = \mu — a polynomial evaluation is the inner product of coefficients with powers.
  • “The first coefficient of m\boldsymbol m is μ\mu.” Same as m,(1,0,0,,0)=μ\langle \boldsymbol m, (1, 0, 0, \dots, 0) \rangle = \mu.
  • “The sum of m\boldsymbol m‘s coefficients is μ\mu.” Same as m,(1,1,1,,1)=μ\langle \boldsymbol m, (1, 1, 1, \dots, 1) \rangle = \mu.

All three are the same template — pick a vector, take an inner product, claim the result. Abstract it: there’s a query vector vF\boldsymbol v \in \mathbb F^\ell that says which question is being asked, and a claimed answer μF\mu \in \mathbb F. The protocol’s job is to convince the verifier that the secret message m\boldsymbol m inside the codeword satisfies m,v=μ\langle \boldsymbol m, \boldsymbol v \rangle = \mu.

v\boldsymbol v is the question. μ\mu is the answer. That’s all v\boldsymbol v does. Different v\boldsymbol v = different question.

The natural relation

Translate that into an IOR-shaped relation. Verifier sees the question v\boldsymbol v and the claimed answer μ\mu in the clear (explicit instance); has oracle access to a codeword ff (implicit instance); the prover knows the actual message and randomness:

f=EncC(m,r)andm,v=μ.f = \mathsf{Enc}_{\mathcal C}(\boldsymbol m, \boldsymbol r) \quad \text{and} \quad \langle \boldsymbol m, \boldsymbol v \rangle = \mu.

Call this RˉC\bar{\mathcal R}_{\mathcal C}. Fine starting point. Leaks twice.

Leak 1: the question v\boldsymbol v is too big to read. v\boldsymbol v is \ell field elements; a verifier that reads it runs in time Ω()\Omega(\ell). We want polylog. The question itself has to be encoded succinctly.

Leak 2: the answer μ\mu leaks the message. Even if the encoding EncC\mathsf{Enc}_{\mathcal C} is perfectly zero-knowledge — so ff leaks nothing — sending μ=m,v\mu = \langle \boldsymbol m, \boldsymbol v \rangle in the clear leaks one linear projection of m\boldsymbol m. That’s a real witness leak, and the protocol is going to want to talk about μ\mu in the clear.

The paper resolves both with two modifications.

Modification 1 — succinct linear forms

Intuition. The first example of v\boldsymbol v above — the vector of powers (1,x,x2,)(1, x, x^2, \dots) — has \ell entries, but it’s described by a single field element xx. You don’t need to write the vector down; you can generate it from xx as needed. That’s the move: instead of letting v\boldsymbol v live in the explicit instance as a flat \ell-long blob, let the explicit instance be a small description of v\boldsymbol v — call it a state st\mathsf{st} — together with a fixed function sl\mathsf{sl} (a “succinct linear form”) that knows how to unpack the state into the full v\boldsymbol v when needed.

Formally: replace v\boldsymbol v in the relation with v=sl(st)\boldsymbol v = \mathsf{sl}(\mathsf{st}), where st\mathsf{st} is small. The verifier never reads v\boldsymbol v entry-by-entry; it only ever needs to evaluate folds of v\boldsymbol v (sumcheck reductions act on v\boldsymbol v via this fold operation), and a fold of a succinct linear form is itself computable from st\mathsf{st} in polylog time, following the WHIR recipe.

Anchor example. sl(α):=(αi1)i[]\mathsf{sl}(\alpha) := (\alpha^{i-1})_{i \in [\ell]}. State is the single field element α\alpha, v\boldsymbol v is the vector of powers of α\alpha, and the constraint m,v=μ\langle \boldsymbol m, \boldsymbol v \rangle = \mu reads “the polynomial with coefficients m\boldsymbol m evaluates to μ\mu at α\alpha.” This particular form is closely related to the sumcheck-style succinct constraints in WHIR; the two are formally incomparable but both work for our purposes.

Modification 2 — masks in the constraint

Intuition. The answer μ\mu is leaky because μ=m,v\mu = \langle \boldsymbol m, \boldsymbol v \rangle is exactly a linear projection of the message. Stare at μ\mu, you’ve stared at one bit of the witness.

The fix is the standard zero-knowledge move: add noise. Have the prover commit to a fresh random mask and sneak the mask’s contribution into the answer. The verifier now sees μ=m,v+(mask contribution)\mu' = \langle \boldsymbol m, \boldsymbol v \rangle + (\text{mask contribution}); the prover knows both pieces; the verifier sees only the sum and can’t separate them. The mask absorbs the leak.

Where the mask comes from. The prover, before any of this gets sent to the verifier, samples small uniformly random vectors ξ1,,ξnFzk\boldsymbol \xi_1, \dots, \boldsymbol \xi_n \in \mathbb F^{\ell_{\mathsf{zk}}} — one mask per “slot” we want to hide, each just a tiny block of fresh field-element noise. “Small” means zk\ell_{\mathsf{zk}} is way shorter than \ell; these are nothing compared to the witness message m\boldsymbol m.

To make these masks committable — so the verifier can ask questions about them without learning them — encode each ξi\boldsymbol \xi_i under a separate F\mathbb F-additive code Czk\mathcal C_{\mathsf{zk}} dedicated to masks, with its own ZK encoding EncCzk\mathsf{Enc}_{\mathcal C_{\mathsf{zk}}}. The resulting codewords ξi=EncCzk(ξi,ri)\xi_i = \mathsf{Enc}_{\mathcal C_{\mathsf{zk}}}(\boldsymbol \xi_i, \boldsymbol r_i) are sent to the verifier as additional oracles. Each mask comes with its own query vector uiFzk\boldsymbol u_i \in \mathbb F^{\ell_{\mathsf{zk}}} — same role v\boldsymbol v played for m\boldsymbol m, just for the mask side.

The relation now demands a single joint constraint:

m,sl(st)+i[n]ξi,ui  =  μ.\langle \boldsymbol m, \mathsf{sl}(\mathsf{st}) \rangle + \sum_{i \in [n]} \langle \boldsymbol \xi_i, \boldsymbol u_i \rangle \;=\; \mu.

How this actually hides μ\mu. Stare at one mask term ξi,ui\langle \boldsymbol \xi_i, \boldsymbol u_i \rangle. The mask message ξi\boldsymbol \xi_i is uniform random over Fzk\mathbb F^{\ell_{\mathsf{zk}}}; the query vector ui\boldsymbol u_i is fixed. The inner product of a uniform random vector with a fixed nonzero vector is itself a uniform random scalar over F\mathbb F. So each ξi,ui\langle \boldsymbol \xi_i, \boldsymbol u_i \rangle is a fresh uniform field element, and the sum iξi,ui\sum_i \langle \boldsymbol \xi_i, \boldsymbol u_i \rangle is also uniform. Read the equation:

m,sl(st)real projection of m  +  iξi,uifresh uniform scalar  =  μ.\underbrace{\langle \boldsymbol m, \mathsf{sl}(\mathsf{st}) \rangle}_{\text{real projection of } \boldsymbol m} \;+\; \underbrace{\sum_i \langle \boldsymbol \xi_i, \boldsymbol u_i \rangle}_{\text{fresh uniform scalar}} \;=\; \mu.

That’s a one-time pad on μ\mu. From the verifier’s view μ\mu is a uniform field element — it could correspond to any value of the real projection, with the masks absorbing whatever offset is needed. Knowing μ\mu leaks nothing about m,sl(st)\langle \boldsymbol m, \mathsf{sl}(\mathsf{st}) \rangle.

Why the verifier can’t unmask. Reasonable worry: the verifier has oracle access to the mask codewords ξi\xi_i. Could it just query them and reconstruct ξi,ui\langle \boldsymbol \xi_i, \boldsymbol u_i \rangle? No — and this is precisely why Czk\mathcal C_{\mathsf{zk}} is itself a ZK code. The same Vandermonde-style argument from §“Zero-knowledge encodings” applies: any tzk\leq t_{\mathsf{zk}} coordinates of ξi\xi_i can be simulated given only their positions, so the mask message ξi\boldsymbol \xi_i stays hidden behind the encoding the same way m\boldsymbol m stays hidden behind ff. The ZK encoding on the mask side closes the loop.

Putting both modifications together gives the relation we’ll use for the rest of the article. Its data splits three ways — the standard IOR shape, made precise in §“IORs, briefly”:

SlotSymbolWho sees itWhat’s in it here
Explicit instancex\mathsf{x}verifier, in the cleartarget μ\mu, state st\mathsf{st}, mask queries (ui)(\boldsymbol u_i)
Implicit instancey\mathsf{y}verifier, as oraclesmain codeword ff, mask codewords (ξi)(\xi_i)
Witnessw\mathsf{w}prover onlythe messages and coins behind those codewords
RC,Czk,sl  :=  {x=(μ,st,(ui)i[n])y=(f,(ξi)i[n])w=(m,r,(ξi,ri)i[n])  |  f=EncC(m,r)i[n] ⁣:ξi=EncCzk(ξi,ri)m,sl(st)+iξi,ui=μ}.\mathcal R_{\mathcal C, \mathcal C_{\mathsf{zk}}, \mathsf{sl}} \;:=\; \left\{ \begin{aligned} &\mathsf{x} = (\mu, \mathsf{st}, (\boldsymbol u_i)_{i \in [n]}) \\ &\mathsf{y} = (f, (\xi_i)_{i \in [n]}) \\ &\mathsf{w} = (\boldsymbol m, \boldsymbol r, (\boldsymbol \xi_i, \boldsymbol r_i)_{i \in [n]}) \end{aligned} \;\middle|\; \begin{aligned} &f = \mathsf{Enc}_{\mathcal C}(\boldsymbol m, \boldsymbol r) \\ &\forall i \in [n]\colon \xi_i = \mathsf{Enc}_{\mathcal C_{\mathsf{zk}}}(\boldsymbol \xi_i, \boldsymbol r_i) \\ &\langle \boldsymbol m, \mathsf{sl}(\mathsf{st}) \rangle + \sum_{i} \langle \boldsymbol \xi_i, \boldsymbol u_i \rangle = \mu \end{aligned} \right\}.

Data on the left, conditions on the right: ff is the main oracle, the ξi\xi_i are mask oracles, both honest encodings, and the last line is the joint constraint.

That’s the committed sumcheck relation, and the table shows why it needs a protocol: the constraint is about m\boldsymbol m and the ξi\boldsymbol \xi_i, which live in w\mathsf{w}. The verifier has x\mathsf{x} in the clear and y\mathsf{y} only as oracles, so evaluating the constraint directly is exactly what it cannot do.

The “committed” in committed sumcheck relation is doing real work: the implicit instance y=(f,(ξi))\mathsf{y} = (f, (\xi_i)) behaves like a commitment to the message and the masks at the same time. Future protocols will accumulate more claims about this joint object; the masks travel with the protocol as oracles rather than being committed-and-opened once. Hold that thought — it’s the central design decision that makes the sumcheck step work later.

Composable HVZK for IORs (and the counterexample that forces it)

The whole construction is “compose small zero-knowledge pieces.” So we need a definition of zero-knowledge that survives composition. The textbook one doesn’t. Let’s see exactly how it breaks, because the fix is one of the conceptual contributions of the paper.

What HVZK asks for

The view is everything the verifier walks away with from one run — the prover’s messages, its own coins, its oracle query answers — as a distribution, not a fixed string.

A simulator is an efficient algorithm that manufactures a fake view from the public input alone, never the witness. The protocol is zero-knowledge if some simulator’s output lands within ζ\zeta of the real view. The moral: anything you could derive from the transcript, you could have derived alone — so it taught you nothing.

The simulator never runs in the protocol; exhibiting one is just how the privacy claim gets discharged. You’ve already met one — §“Scaling up” closed with “sample tt uniform field elements.”

Honest-verifier restricts the guarantee to verifiers that sample challenges as specified. As noted up top, Fiat–Shamir makes that restriction free.

IORs, briefly

We are working with interactive oracle reductions (IORs), not IOPs. The difference: an IOR verifier doesn’t output accept/reject — it outputs a new instance for some target relation R\mathcal R'. The instance is split: an explicit part the next verifier receives as input, and an implicit part the next verifier receives as an oracle. So an IOR from R\mathcal R to R\mathcal R' takes (x,y,w)R(\mathsf{x}, \mathsf{y}, \mathsf{w}) \in \mathcal R and produces (x,y,w)R(\mathsf{x}', \mathsf{y}', \mathsf{w}') \in \mathcal R' via an interaction. Composition stitches IORs end-to-end: IOR1IOR2\mathsf{IOR}_1 \circ \mathsf{IOR}_2 runs the first reduction, then runs the second on its output.

This is the right abstraction for our purposes because the WHIR/Ligerito construction is a tower of reductions: sumcheck reduces interleaving to base, code-switching reduces big code to small code, repeat, finally land in a tiny instance you can check directly.

Where the standard definition looks — and where it doesn’t

For a protocol ending in accept/reject, §“What HVZK asks for” is the whole story. An IOR doesn’t end: its verifier outputs a new instance, part of which is an oracle handed to whatever runs next.

The standard definition audits the queries this verifier made, and says nothing about the oracle passed on. That leaves somewhere to hide a leak — an oracle nobody has queried yet.

The counterexample

Let R\mathcal R be any relation and R\mathcal R' be “check membership in R\mathcal R directly” — same instances, empty witness slot. Two reductions:

  • IOR1\mathsf{IOR}_1. Prover sends the witness w\mathsf{w} as an oracle. Verifier queries nothing, then outputs (x,y):=(x,w)(\mathsf{x}', \mathsf{y}') := (\mathsf{x}, \mathsf{w}). Complete and sound, and its view is empty — so a simulator that outputs nothing matches it perfectly. HVZK.
  • IOR2\mathsf{IOR}_2. Prover sends nothing. Verifier reads all of y\mathsf{y}' and checks (x,y)R(\mathsf{x}', \mathsf{y}') \in \mathcal R. Also HVZK: the standard definition hands the simulator the same oracle access the verifier has, so it reproduces that view exactly.

Each clears the bar. Now compose: IOR1\mathsf{IOR}_1‘s output oracle is w\mathsf{w}, and IOR2\mathsf{IOR}_2 reads it in full. The composed verifier learns the witness.

Two-box pipeline showing the composition leak: IOR_1 (left box) takes (x, w) and outputs implicit instance y' = w with zero verifier queries — marked HVZK. IOR_2 (right box) takes y' as an oracle and queries it in full — also marked HVZK. The seam between the boxes carries an arrow labeled 'w as oracle'; a dashed red arrow from IOR_2's queries reaches back through the seam and exfiltrates w to the verifier. Caption: each piece passes the standard definition; composition reveals the witness.

Neither piece cheats. The leak lives in the seam: IOR1\mathsf{IOR}_1 is graded on queries it never makes, and IOR2\mathsf{IOR}_2 is graded on an oracle it is handed and told is fine.

The fix — put the future queries in the definition

If the danger is what happens to the output oracle later, then later has to be part of the definition. Model “whatever will query my output oracle” as an algorithm D\mathcal D — a distinguisher, so called because it is the thing trying to tell a real view from a simulated one.

The simulator must now produce the view and answer D\mathcal D‘s queries into the output oracle, still without the witness:

Sy,D(x)  ζ  View(P,V,D,x,y,w).\mathsf{S}^{\mathsf{y}, \mathcal D}(\mathsf{x}) \;\approx_\zeta\; \mathsf{View}(\mathbf P, \mathbf V, \mathcal D, \mathsf{x}, \mathsf{y}, \mathsf{w}).

S\mathsf{S} may run D\mathcal D as a black box — execute it on the output instance to see which positions it wants, then invent consistent answers.

Does it kill the counterexample? Take D\mathcal D that reads the whole output oracle, exactly what IOR2\mathsf{IOR}_2 does. Now IOR1\mathsf{IOR}_1‘s simulator has to produce every position of y=w\mathsf{y}' = \mathsf{w} knowing only x\mathsf{x} — that is the witness, so it cannot. IOR1\mathsf{IOR}_1 now fails, which is the verdict we wanted. And when D\mathcal D queries nothing, it collapses back to the standard definition.

In practice D\mathcal D is pinned to a class: Dt\mathfrak D^{\leq t}, distinguishers making at most tt queries to the implicit instance.

Composition — which D\mathcal D do you pick?

Every IOR is now HVZK only relative to some D\mathcal D. Which one? The one that actually shows up next.

Composition theorem (Lemma 2.6 of the paper). If IOR2\mathsf{IOR}_2 is HVZK with respect to D2\mathcal D_2 with simulator S2\mathsf{S}_2, and IOR1\mathsf{IOR}_1 is HVZK with respect to D1:=S2\mathcal D_1 := \mathsf{S}_2 (running with its own distinguisher D2\mathcal D_2), then IOR1IOR2\mathsf{IOR}_1 \circ \mathsf{IOR}_2 is HVZK with respect to D2\mathcal D_2, with errors adding: ζ1+ζ2\zeta_1 + \zeta_2.

Read D1:=S2\mathcal D_1 := \mathsf{S}_2 slowly, because it is the whole idea: the next protocol’s simulator becomes the previous protocol’s distinguisher. In the simulated world, S2\mathsf{S}_2 is the only thing that ever touches IOR1\mathsf{IOR}_1‘s output oracle — so those are the only future queries IOR1\mathsf{IOR}_1 has to survive. The statistical errors chain along the seam and add.

Each reduction we build is then proved HVZK against a bounded query class matching what the next stage will ask — the framework’s contribution being that “what happens to this oracle later” gets written down instead of assumed.

The HVZK sumcheck reduction

Now the first of the two key technical contributions: an HVZK reduction from testing the interleaving of a code to testing the base code itself. It is what kicks the verifier’s job down from a 2k2^k-blown-up alphabet to the original. It is also where this paper buys 1+o(1)1 + o(1) overhead instead of 2×\geq 2\times.

The setup: interleaving via multilinear decomposition

We are going to interleave by a power-of-two factor K=2kK = 2^k. The trick is to define the 2k2^k-interleaving directly in terms of a zero-knowledge encoding, by splitting the message into 2k2^k pieces using multilinear-polynomial structure.

Let eq(X1,,Xk,Y1,,Yk):=i=1k(XiYi+(1Xi)(1Yi))\mathsf{eq}(\mathsf X_1, \dots, \mathsf X_k, \mathsf Y_1, \dots, \mathsf Y_k) := \prod_{i=1}^k (\mathsf X_i \mathsf Y_i + (1 - \mathsf X_i)(1 - \mathsf Y_i)) — the familiar equality polynomial in kk pairs of variables.

Given a message mF2k\boldsymbol m \in \mathbb F^{2^k \ell} and randomness rF2kr\boldsymbol r \in \mathbb F^{2^k r}, view them as 2k2^k pieces (mb)b{0,1}k(\boldsymbol m_{\boldsymbol b})_{\boldsymbol b \in \{0,1\}^k} and (rb)b{0,1}k(\boldsymbol r_{\boldsymbol b})_{\boldsymbol b \in \{0,1\}^k}, indexed by the boolean hypercube of side kk. The interleaved encoding EncC2k(m,r)\mathsf{Enc}_{\mathcal C^{\equiv 2^k}}(\boldsymbol m, \boldsymbol r) separately encodes each piece under EncC\mathsf{Enc}_{\mathcal C} and stacks the results:

z[m] ⁣:EncC2k(m,r)(z)  :=  (EncC(mb,rb)(z))b{0,1}k    Σ2k.\forall z \in [m]\colon\quad \mathsf{Enc}_{\mathcal C^{\equiv 2^k}}(\boldsymbol m, \boldsymbol r)(z) \;:=\; \big(\mathsf{Enc}_{\mathcal C}(\boldsymbol m_{\boldsymbol b}, \boldsymbol r_{\boldsymbol b})(z)\big)_{\boldsymbol b \in \{0,1\}^k} \;\in\; \Sigma^{2^k}.

The multilinear interpretation: if you view m\boldsymbol m and r\boldsymbol r as multilinear polynomials m^,r^\hat{\boldsymbol m}, \hat{\boldsymbol r} in k+logk + \log \ell and k+logrk + \log r variables respectively, the first kk variables index which “piece” you’re in, and the remaining variables index inside the piece.

Interleaved folding

The other half of the trick: how to fold an interleaved codeword. The interleaved fold of a function f ⁣:[m]Σ2kf \colon [m] \to \Sigma^{2^k} at a point γFk\boldsymbol \gamma \in \mathbb F^k outputs a function [m]Σ[m] \to \Sigma defined by

Fold(f,γ)(z)  :=  b{0,1}keq(b,γ)fb(z),\mathsf{Fold}(f, \boldsymbol \gamma)(z) \;:=\; \sum_{\boldsymbol b \in \{0,1\}^k} \mathsf{eq}(\boldsymbol b, \boldsymbol \gamma) \cdot f_{\boldsymbol b}(z),

where fb(z)Σf_{\boldsymbol b}(z) \in \Sigma is the b\boldsymbol b-th coordinate of the Σ2k\Sigma^{2^k}-vector f(z)f(z). Concretely, the verifier folds an interleaved codeword by taking the eq\mathsf{eq}-weighted combination of its 2k2^k coordinates at every position.

The construction is set up so the natural identity holds:

Claim (encode-then-fold = fold-then-encode). For every message m\boldsymbol m, randomness r\boldsymbol r, and folding point γFk\boldsymbol \gamma \in \mathbb F^k, Fold(EncC2k(m,r),γ)  =  EncC(Fold(m,γ),Fold(r,γ)).\mathsf{Fold}\big(\mathsf{Enc}_{\mathcal C^{\equiv 2^k}}(\boldsymbol m, \boldsymbol r),\, \boldsymbol \gamma\big) \;=\; \mathsf{Enc}_{\mathcal C}\big(\mathsf{Fold}(\boldsymbol m, \boldsymbol \gamma),\, \mathsf{Fold}(\boldsymbol r, \boldsymbol \gamma)\big).

In English: folding the interleaved codeword gives you a valid base-code codeword whose message is the fold of the original messages. The prover folds messages, the verifier folds codewords, and the two stay in sync. This is a direct extension to zero-knowledge codes of the folding identities used in WHIR, STIR, and Ligerito.

Commuting square: top-left is the message-randomness pair (m, r) over the 2^k hypercube; arrows go right via Enc_{C^{≡2^k}} to the interleaved codeword f, and down via Fold(·, γ) to the folded message-randomness pair. Both paths converge at bottom-right on the same base-code codeword Enc_C(Fold(m, γ), Fold(r, γ)). Square commutes.

The reduction, operationally

We have an instance of RC2k,Czk,sl\mathcal R_{\mathcal C^{\equiv 2^k}, \mathcal C_{\mathsf{zk}}, \mathsf{sl}}: a 2k2^k-interleaved main oracle ff, nn small mask oracles (ξi)(\xi_i), and a joint constraint with target μ\mu. The reduction outputs an instance of RC,Czk,sl\mathcal R_{\mathcal C, \mathcal C_{\mathsf{zk}}, \mathsf{sl}'} — same shape, but over the base code C\mathcal C, with a new succinct linear form sl\mathsf{sl}' that encodes the fold.

Operationally:

The reduction, step by step
Prover — knows m,r\boldsymbol m, \boldsymbol r and every (ξi,ri)(\boldsymbol \xi_i, \boldsymbol r_i)
Verifier — holds μ,st,(ui)\mu, \mathsf{st}, (\boldsymbol u_i), and reaches f,(ξi)f, (\xi_i) only by query

Step 1  ·  Prover
Sample kk small masks s1,,skFzk\boldsymbol s_1, \dots, \boldsymbol s_k \in \mathbb F^{\ell_{\mathsf{zk}}} and encode each under Czk\mathcal C_{\mathsf{zk}}. Read each as a univariate s^j\hat s_j, and set the masked target so the mask half sums correctly on its own:

μ~  :=  b{0,1}k(s^1(b1)++s^k(bk))\tilde\mu \;:=\; \sum_{\boldsymbol b \in \{0,1\}^k} \big(\hat s_1(b_1) + \dots + \hat s_k(b_k)\big)
mask oracles s1,,sks_1, \dots, s_k, and the masked target μ~\tilde\mu

Step 2  ·  Verifier
Draw one scalar to fuse the mask claim and the real claim into a single sumcheck. Because ε\varepsilon is random, the fused claim can hold only if both halves do, bar probability 1/F1/|\mathbb F|.
batching scalar εF\varepsilon \leftarrow \mathbb F

Step 3  ·  Both, for kk rounds
Both sides now hold one claim, and sumcheck it:

b{0,1}k(s^1(b1)++s^k(bk)  +  εG^(b))  =  μ~+εμc0\sum_{\boldsymbol b \in \{0,1\}^k} \big(\hat s_1(b_1) + \dots + \hat s_k(b_k) \;+\; \varepsilon \cdot \hat G(\boldsymbol b)\big) \;=\; \underbrace{\tilde \mu + \varepsilon \cdot \mu}_{c_0}
where G^(b):=(am^(b,a)Fold(sl(st),b)[a])+iξi,ui\hat G(\boldsymbol b) := \big(\sum_{\boldsymbol a} \hat{\boldsymbol m}(\boldsymbol b, \boldsymbol a) \cdot \mathsf{Fold}(\mathsf{sl}(\mathsf{st}), \boldsymbol b)[\boldsymbol a]\big) + \sum_i \langle \boldsymbol \xi_i, \boldsymbol u_i \rangle — the message against the folded coefficients, plus the old mask terms.
each round jj, carrying a running claim cj1c_{j-1}
round polynomial gjg_j, the sum with Xj\mathsf X_j left free
verifier checks gj(0)+gj(1)=cj1g_j(0) + g_j(1) = c_{j-1}
challenge γjF\gamma_j \leftarrow \mathbb F, then cj:=gj(γj)c_j := g_j(\gamma_j)
After kk rounds the verifier holds γ=(γ1,,γk)\boldsymbol \gamma = (\gamma_1, \dots, \gamma_k) and a final claim ckc_k about a single point.

Step 4  ·  Verifier
Output the new instance — same shape, now over the base code C\mathcal C:

target μ:=ck=gk(γk)\mu' := c_k = g_k(\gamma_k) — the running claim after the last round
oracles Fold(f,γ)\mathsf{Fold}(f, \boldsymbol \gamma), the old (ξi)(\xi_i), the new (sj)(s_j)
constraint ckc_k regrouped into a fresh joint constraint

After this single reduction, the problem size has dropped from 2km2^k \cdot m alphabet symbols to mm — the size of one base codeword.

The point — small masks, not a full mask

This is where 1+o(1)1 + o(1) overhead is won. Prior zero-knowledge sumchecks ([CFS17], [BCL22], the binary-field variant [Dia25]) add a full random polynomial of the same size as the message, then prove the original claim relative to it via a random linear combination. That doubles prover work — every operation has to be done on a 2×2\times-sized object. It’s a clean construction; it’s also too expensive for our setting.

This paper does it differently: one small mask per sumcheck round, à la [XZZPS19], instead of a single full mask up front. Each mask’s message is only zk\ell_{\mathsf{zk}} field elements (much smaller than \ell, the actual message size), and its encoding a correspondingly short codeword. With kk rounds and zk\ell_{\mathsf{zk}} \ll \ell, the total masking overhead is sublinear — exactly the o(1)o(1) we want.

That’s the high-level move. The non-trivial part is making it composable with the WHIR/Ligerito recursion. Three details earn their place:

  • Masks travel as oracles, not opened evaluations. [XZZPS19]‘s zero-knowledge sumcheck reveals an evaluation of the mask polynomial at the end — and inside our framework that would reveal an evaluation of m^\hat{\boldsymbol m}, breaking ZK. We cannot afford that. Instead, the reduction accumulates a claim about the masks into the next relation’s joint constraint and carries the mask oracles forward. This is exactly the work the “committed” part of RC,Czk,sl\mathcal R_{\mathcal C, \mathcal C_{\mathsf{zk}}, \mathsf{sl}} was set up to do. Without the joint constraint in the relation, this sumcheck would not compose.
  • char(F)2\mathsf{char}(\mathbb F) \neq 2. The mask sjs_j‘s contribution to round jj‘s polynomial enters scaled by 2kj2^{k-j} — the number of hypercube points it’s summed over. In characteristic 2 that factor vanishes for every round but the last, and the masks stop masking. A real instantiation constraint, but it doesn’t bite for the fields the WHIR/Ligerito family cares about (Goldilocks, BabyBear, large primes).
  • Correlating queries over the larger alphabet. Round-by-round knowledge soundness wants a union bound over codewords at distance δ\delta from the sent oracles. Naively, you’d “open up” the interleaved word ff into its 2k2^k base-code parts and union-bound each — leading to a union over Λ(C,δ)2k|\Lambda(\mathcal C, \delta)|^{2^k} events. That blows up the required field size to absurdity. The fix: analyze codewords relative to the interleaved codes C2k\mathcal C^{\equiv 2^k} and Czkn+k\mathcal C_{\mathsf{zk}}^{\equiv n+k} directly. Interleaving raises a code’s list size by only a constant factor [GGR11], so the union bound stays modest. Operationally, the verifier’s queries are correlated across the 2k2^k stacked base-codewords — they all share the same position — and this correlation is the soundness lever. It also shrinks compiled argument size as a bonus, because correlated queries become a single Merkle path over the bigger alphabet rather than 2k2^k paths.

What this buys us

After running the reduction once, we have:

  • Reduced testing the 2k2^k-interleaving of C\mathcal C to testing C\mathcal C itself.
  • Reduced problem size by a factor of 2k2^k.
  • Spent O~(kλ)\widetilde O(k \cdot \lambda) extra symbols on masks — sublinear in mm.
  • Preserved HVZK with respect to a bounded distinguisher class, by carrying mask oracles forward and accumulating their constraints.

The natural next move is to recursively shrink the codeword, which is the WHIR/Ligerito move. Each round takes a code, 2k2^k-interleaves it, runs this sumcheck reduction to drop the interleaving, then switches to a new, smaller code and does it again. Recurse O(log/loglog)O(\log \ell / \log\log \ell) times and the verifier ends up looking at something tiny enough to handle directly.

But the “switch to a new code” step is the second non-trivial piece, and naive code-switching is incompatible with zero-knowledge in two distinct ways. That’s Part 2.

Why we’re not done yet

We have, in zero-knowledge, the ability to reduce testing the interleaving of a code to testing the base code itself, at 1+o(1)1 + o(1) overhead, with composable HVZK guarantees that survive being plugged into a larger pipeline. That is half the construction.

The other half: at some point we need to stop testing C\mathcal C and start testing something smaller — a code with shorter message length, so that recursion actually terminates in something cheap. That’s code-switching: an IOR from a test for code C\mathcal C to a test for code C\mathcal C'. WHIR uses it, Ligerito uses it, Blaze uses it. The non-ZK version is well understood; this paper had to find the ZK version.

Two things break when you try to make the standard code-switching template zero-knowledge:

  • The randomness in the encoding gets in the way. The consistency check between old and new codewords assumes the codeword is a function of the message alone. With a zero-knowledge encoding it isn’t — it’s a function of the message and the randomness — and the obvious fix (encoding both into the new code) costs another 2×2\times.
  • Out-of-domain sampling is incompatible with privacy. WHIR’s OOD trick is what lets the protocol work in the list-decoding regime. The OOD vector forces the prover to commit to a single codeword among the possibilities — which is exactly what zero-knowledge forbids.

The resolution to both: a new gadget called a private zero-evader, and a joint OOD sample that operates on the message and fresh randomness. That, plus assembling everything into the headline corollary — an HVZK variant of WHIR/Ligerito at 1+o(1)1 + o(1) overhead — is Part 2.

References

  • This paper — Chiesa, A., Fenzi, G., & Weissenberg, G. (Feb 2026). Zero-Knowledge IOPPs for Constrained Interleaved Codes.
  • WHIR — Arnon, G., Chiesa, A., Fenzi, G., & Yogev, E. (2025). WHIR: Reed–Solomon Proximity Testing with Super-Fast Verification. EUROCRYPT ‘25. eprint
  • Ligerito — Novakovic, A., & Angeris, G. (2025). Ligerito: A Small and Concretely Fast Polynomial Commitment Scheme. eprint 2025/1187
  • STIR — Arnon, G., Chiesa, A., Fenzi, G., & Yogev, E. (2024). STIR: Reed–Solomon Proximity Testing with Fewer Queries. CRYPTO ‘24.
  • Ligero — Ames, S., Hazay, C., Ishai, Y., & Venkitasubramaniam, M. (2017). Ligero: Lightweight Sublinear Arguments Without a Trusted Setup. CCS ‘17.
  • BCL22 — Bootle, J., Chiesa, A., & Liu, S. (2022). Zero-Knowledge IOPs with Linear-Time Prover and Polylogarithmic-Time Verifier. EUROCRYPT ‘22.
  • RW24 — Ron-Zewi, N., & Weiss, M. (2024). Zero-knowledge IOPs Approaching Witness Length. CRYPTO ‘24.
  • XZZPS19 (Libra) — Xie, T., Zhang, J., Zhang, Y., Papamanthou, C., & Song, D. (2019). Libra: Succinct Zero-Knowledge Proofs with Optimal Prover Computation. CRYPTO ‘19.
  • CFS17 — Chiesa, A., Forbes, M.A., & Spooner, N. (2017). A Zero Knowledge Sumcheck and its Applications. arXiv:1704.02086
  • GGR11 — Gopalan, P., Guruswami, V., & Raghavendra, P. (2011). List Decoding Tensor Products and Interleaved Codes. SICOMP 40(5).
  • Dia25 — Diamond, B. (2025). Zero-Knowledge Polynomial Commitment in Binary Fields. eprint 2025/1015
  • TensorSwitch (companion blog)TensorSwitch I.
  • WHIR (companion blog)WHIR.
  • Spartan + SPARK (companion blogs)Spartan, SPARK.