Longfellow ZK 0290cb32
Loading...
Searching...
No Matches
zk_common.h
1// Copyright 2025 Google LLC.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#ifndef PRIVACY_PROOFS_ZK_LIB_ZK_ZK_COMMON_H_
16#define PRIVACY_PROOFS_ZK_LIB_ZK_ZK_COMMON_H_
17
18#include <cstddef>
19#include <vector>
20
21#include "arrays/dense.h"
22#include "arrays/eq.h"
23#include "arrays/eqs.h"
24#include "ligero/ligero_param.h"
25#include "random/transcript.h"
26#include "sumcheck/circuit.h"
27#include "sumcheck/quad.h"
28#include "sumcheck/transcript_sumcheck.h"
29#include "util/panic.h"
30
31namespace proofs {
32
33template <class Field>
34// ZkCommon
35//
36// Used by prover and verifier to mimic the checks that the sumcheck verifier
37// applies to the sumcheck transcript. The difference is that the transcript
38// will now be encrypted with a random pad, and the checks will be verified
39// by the Ligero proof system with respect to a hiding commitment scheme.
40class ZkCommon {
41 using index_t = typename Quad<Field>::index_t;
43 using Elt = typename Field::Elt;
44 using CPoly = typename LayerProof<Field>::CPoly;
45 using WPoly = typename LayerProof<Field>::WPoly;
46 using FWPoly = typename LayerProof<Field>::FWPoly;
47
48 public:
49 // pi: witness index for first pad element in a larger commitment
50 static size_t verifier_constraints(
51 const Circuit<Field>& circuit, const Dense<Field>& pub,
52 const Proof<Field>& proof, const ProofAux<Field>* aux,
53 std::vector<Llc>& a, std::vector<typename Field::Elt>& b, Transcript& tsv,
54 size_t pi, const Field& F) {
55 const size_t ninp = circuit.ninputs, npub = circuit.npub_in;
56
57 Challenge<Field> ch(circuit.nl);
58 TranscriptSumcheck<Field> tss(tsv, F);
59
60 tss.begin_circuit(ch.q, ch.g);
61 Claims cla = Claims{
62 .logv = circuit.logv,
63 .claim = {F.zero(), F.zero()},
64 .q = ch.q,
65 .g = {ch.g, ch.g},
66 };
67
68 size_t ci = 0; // Index of the next Ligero constraint.
69
70 const typename FWPoly::dot_interpolation dot_wpoly(F);
71
72 // no copies in this version.
73 check(circuit.logc == 0, "assuming that copies=1");
74
75 // Constraints from the sumcheck verifier.
76 for (size_t ly = 0; ly < circuit.nl; ++ly) {
77 auto clr = &circuit.l.at(ly);
78 auto plr = &proof.l[ly];
79 auto challenge = &ch.l[ly];
80
81 tss.begin_layer(challenge->alpha, challenge->beta, ly);
82
83 // The loop below assumes at least one round.
84 check(clr->logw > 0, "clr->logw > 0");
85
86 PadLayout pl(clr->logw);
87 ConstraintBuilder cb(pl, F); // representing 0
88
89 cb.first(challenge->alpha, cla.claim);
90 // now cb contains claim_{-1} from the previous layer
91
92 for (size_t round = 0; round < clr->logw; ++round) {
93 for (size_t hand = 0; hand < 2; ++hand) {
94 size_t r = 2 * round + hand;
95 const WPoly& hp = plr->hp[hand][round];
96 challenge->hb[hand][round] = tss.round(hp);
97 const FWPoly lag = dot_wpoly.coef(challenge->hb[hand][round], F);
98
99 cb.next(r, &lag[0], hp.t_);
100 // now cb contains a symbolic representation of claim_{r}
101 }
102 }
103
104 // Verify
105 // claim = EQ[Q,C] QUAD[R,L] W[R,C] W[L,C]
106 // by substituting in the symbolic constraint on p(1) from the relation:
107 // claim = <lag, (p(0), p(1), p(2))>.
108 Elt quad = aux == nullptr ? bind_quad(clr, cla, challenge, F)
109 : aux->bound_quad[ly];
110 Elt eqv =
111 Eq<Field>::eval(circuit.logc, circuit.nc, ch.q, challenge->cb, F);
112 Elt eqq = F.mulf(eqv, quad);
113
114 // Add the final constraint from above.
115 cb.finalize(plr->wc, eqq, ci++, ly, pi, a, b);
116
117 tss.write(&plr->wc[0], 1, 2);
118
119 cla = Claims{
120 .logv = clr->logw,
121 .claim = {plr->wc[0], plr->wc[1]},
122 .q = challenge->cb,
123 .g = {challenge->hb[0], challenge->hb[1]},
124 };
125
126 pi += pl.layer_size(); // Update index to poly_pad(0,0) of the
127 // next layer
128 }
129
130 // Constraints induced by the input binding
131 // <eq0 + alpha.eq1, witness> = W_l + alpha.W_r
132 Elt alpha = tsv.elt(F);
133 auto plr = &proof.l[circuit.nl - 1];
134 Elt got = F.addf(plr->wc[0], F.mulf(alpha, plr->wc[1]));
135
136 return input_constraint(cla, pub, npub, ninp, pi, got, alpha, a, b, ci, F);
137 }
138
139 // Returns the size of the proof pad for circuit C.
140 static size_t pad_size(const Circuit<Field>& C) {
141 size_t sz = 0;
142 for (size_t i = 0; i < C.nl; ++i) {
143 PadLayout pl(C.l[i].logw);
144 sz += pl.layer_size();
145 }
146 return sz;
147 }
148
149 // Setup lqc based on proof pad layout.
150 static void setup_lqc(const Circuit<Field>& C,
151 std::vector<LigeroQuadraticConstraint>& lqc,
152 size_t start_pad) {
153 size_t pi = start_pad;
154 for (size_t i = 0; i < C.nl; ++i) {
155 PadLayout pl(C.l[i].logw);
156 lqc[i].x = pi + pl.claim_pad(0);
157 lqc[i].y = pi + pl.claim_pad(1);
158 lqc[i].z = pi + pl.claim_pad(2);
159 pi += pl.layer_size();
160 }
161 }
162
163 // append public parameters to the FS transcript
164 static void initialize_sumcheck_fiat_shamir(Transcript& ts,
165 const Circuit<Field>& circuit,
166 const Dense<Field>& pub,
167 const Field& F) {
168 ts.write(circuit.id, sizeof(circuit.id));
169
170 // Public inputs:
171 for (size_t i = 0; i < circuit.npub_in; ++i) {
172 ts.write(pub.at(i), F);
173 }
174
175 // Outputs pro-forma:
176 ts.write(F.zero(), F);
177
178 // Enough zeroes for correlation intractability, one byte
179 // per term.
180 ts.write0(circuit.nterms());
181 }
182
183 private:
184 // The claims struct mimics the same object in the sumcheck code. This
185 // helps the verifier_constraints method above mimic the same steps as
186 // the sumcheck verifier.
187 struct Claims {
188 size_t logv;
189 Elt claim[2];
190 const Elt* q;
191 const Elt* g[2];
192 };
193
194 class PadLayout {
195 size_t logw_;
196
197 public:
198 explicit PadLayout(size_t logw) : logw_(logw) {}
199
200 // Layout of padding in the expr_.symbolic array.
201 //
202 // A *claim pad* is a triple [dWC[0], dWC[1], dWC[0]*dWC[1]].
203 //
204 // A *poly pad* is a pair [dP(0), dP(2)], where "2" is a generic
205 // name for the third evaluation point of the sumcheck round
206 // polynomial (could be X for binary fields GF(2)[X] / (Q(X))).
207 //
208 // The layout of expr_.symbolic is
209 // [CLAIM_PAD[layer - 1], POLY_PAD[0], POLY_PAD[1], ..
210 // POLY_PAD[LOGW - 1], CLAIM_PAD[layer]]
211 //
212 // The layout of adjacent layers thus overlaps. For layer 0
213 // we still lay out CLAIM_PAD[layer - 1] to keep the representation
214 // uniform, but we don't output the corresponding Ligero terms.
215
216 // Because of different use cases, we have two indexing schemes:
217 //
218 // "with overlap": the first element is CLAIM_PAD[layer - 1][0]
219 // "without overlap": the first element is POLY_PAD[0][0]
220
221 //------------------------------------------------------------
222 // Indexing without overlap.
223 //------------------------------------------------------------
224 size_t poly_pad(size_t r, size_t point) const {
225 check(point == 0 || point == 2, "unknown poly_pad() layout");
226 if (point == 0) {
227 return 2 * r;
228 } else if (point == 2) {
229 return 2 * r + 1;
230 }
231 return 0; // silence noreturn warning
232 }
233 // index of CLAIM_PAD[layer][n]
234 size_t claim_pad(size_t n) const { return poly_pad(2 * logw_, 0) + n; }
235
236 // size of the layer
237 size_t layer_size() const { return claim_pad(3); }
238
239 //------------------------------------------------------------
240 // Indexing with overlap.
241 //------------------------------------------------------------
242 // index of CLAIM_PAD[layer - 1][n]
243 size_t ovp_claim_pad_m1(size_t n) const { return n; }
244 size_t ovp_poly_pad(size_t r, size_t point) const {
245 return 3 + poly_pad(r, point);
246 }
247 size_t ovp_claim_pad(size_t n) const { return 3 + claim_pad(n); }
248 size_t ovp_layer_size() const { return ovp_claim_pad(3); }
249 };
250
251 // Represent symbolic expressions of the form
252 //
253 // KNOWN + SUM_{i} SYMBOLIC[i] * WITNESS[i]
254 //
255 // and support simple linear operations on such quantities
256 class Expression {
257 Elt known_;
258 std::vector<Elt> symbolic_;
259 const Field& f_;
260
261 public:
262 Expression(size_t nvar, const Field& F)
263 : known_(F.zero()), symbolic_(nvar, F.zero()), f_(F) {}
264
265 Elt known() { return known_; }
266 std::vector<Elt> symbolic() { return symbolic_; }
267
268 void scale(const Elt& k) {
269 f_.mul(known_, k);
270 for (auto& e : symbolic_) {
271 f_.mul(e, k);
272 }
273 }
274
275 // We don't need the general case of combining two
276 // Expressions. Instead, we only need the two operations
277 // below.
278
279 // *this += k * (known_value + witness[var]).
280 void axpy(size_t var, const Elt& known_value, const Elt& k) {
281 f_.add(known_, f_.mulf(k, known_value));
282 f_.add(symbolic_[var], k);
283 }
284
285 // *this -= k * (known_value + witness[var])
286 void axmy(size_t var, const Elt& known_value, const Elt& k) {
287 f_.sub(known_, f_.mulf(k, known_value));
288 f_.sub(symbolic_[var], k);
289 }
290 };
291
292 class ConstraintBuilder {
293 Expression expr_;
294 const PadLayout& pl_;
295 const Field& f_;
296
297 public:
298 ConstraintBuilder(const PadLayout& pl, const Field& F)
299 : expr_(pl.ovp_layer_size(), F), pl_(pl), f_(F) {}
300
301 // For given unpadded variable X in the original non-ZK prover,
302 // the transcript contains the padded variable Xhat = X - dX
303 // where dX is the padding of X. Thus the unpadded variable is
304 //
305 // X = Xhat + dX
306 //
307 // The ZK verifier needs to compute linear combinations (and one
308 // quadratic combination) of the X's, but it only has access to
309 // the Xhat's and to a commitment to the dX's. We also want to
310 // discuss the verifier algorithm as if the verifier were
311 // operating on X, in order to keep the discussion simple.
312 //
313 // To this end, the Expression class keeps a symbolic
314 // representation of a variable X as
315 //
316 // X = KNOWN + SUM_{i} SYMBOLIC[i] dX[i]
317 //
318 // which is sufficient to capture any linear combination of
319 // X variables. We do something special for the quadratic
320 // combination in finalize().
321
322 // We store only one quantity EXPR_ that represents either
323 // p(1) at some certain round, or a claim at some round.
324 // Comments make it clear which is which.
325
326 // Initially, compute claim_{-1} = cl0 + alpha*cl1
327 void first(Elt alpha, const Elt claims[]) {
328 // expr_ contains zero
329 expr_.axpy(pl_.ovp_claim_pad_m1(0), claims[0], f_.one());
330 expr_.axpy(pl_.ovp_claim_pad_m1(1), claims[1], alpha);
331 // expr_ contains claim_{-1} = cl0 + alpha*cl1
332 }
333
334 // Given claim_{r-1}, compute claim_{r}
335 void next(size_t r, const Elt lag[], const Elt tr[]) {
336 // expr contains claim_{r-1}
337 expr_.axmy(pl_.ovp_poly_pad(r, 0), tr[0], f_.one());
338 // expr contains p_{r}(1) = claim_{r-1} - p_{r}(0)
339
340 // Compute the dot-product <lag_{r}, p_{r}> in place:
341 // claim_{r} = p_{r}(1) * lag[1], overwriting expr_
342 // claim_{r} += lag[0] * p_{r}(0)
343 // claim_{r} += lag[2] * p_{r}(2)
344 expr_.scale(lag[1]);
345 expr_.axpy(pl_.ovp_poly_pad(r, 0), tr[0], lag[0]);
346 expr_.axpy(pl_.ovp_poly_pad(r, 2), tr[2], lag[2]);
347 // expr_ contains claim_{r} = <lag_{r}, p_{r}>
348 }
349
350 // The finalize method uses the last sumcheck claim to
351 // add a constraint on the dX's (the pad) to the Ligero system.
352 //
353 // Our goal is to verify that
354 //
355 // CLAIM = EQQ * W[R,C] * W[L,C]
356 //
357 // where EQQ = EQ[Q,C] QUAD[R,L] and all variables are unpadded.
358 //
359 // We have a symbolic representation of CLAIM in expr_, the proof
360 // contains W_hat[{R,L},C], the padding witnesses are at index pi,
361 // pi+1, and their product is at index pi+2.
362 //
363 // Let CLAIM = KNOWN + SUM_{i} SYMBOLIC[i] dX[i] from the
364 // Expression class. Then
365 //
366 // KNOWN + SUM_{i} SYMBOLIC[i] dX[i]
367 // = EQQ * (W_hat[R,C] + dW[R,C]) * (W_hat[L,C] + dW[L,C])
368 //
369 // Rearranging in the Ax = b form needed for ligero, we have
370 //
371 // SUM_{i} SYMBOLIC[i] dX[i] - (EQQ * W[R, C]) dW[L, C]
372 // - (EQQ * W[L, C]) dW[R, C] - EQQ * dW[R,C] * dW[L,C]
373 // = EQQ * W[R,C] * W[L,C] - KNOWN
374 void finalize(const Elt wc[], const Elt& eqq, size_t ci, size_t ly,
375 size_t pi, std::vector<Llc>& a, std::vector<Elt>& b) {
376 // break the Expression abstraction and split into constituents.
377
378 // EQQ * W[R,C] * W[L,C] - known
379 Elt rhs = f_.subf(f_.mulf(eqq, f_.mulf(wc[0], wc[1])), expr_.known());
380
381 // symbolic part
382 std::vector<Elt> lhs = expr_.symbolic();
383 f_.sub(lhs[pl_.ovp_claim_pad(0)], f_.mulf(eqq, wc[1]));
384 f_.sub(lhs[pl_.ovp_claim_pad(1)], f_.mulf(eqq, wc[0]));
385 f_.sub(lhs[pl_.ovp_claim_pad(2)], eqq);
386
387 b.push_back(rhs);
388
389 // Layer 0 does not refer to CLAIM_PAD[layer - 1]
390 size_t i0 = (ly == 0) ? pl_.ovp_poly_pad(0, 0) : pl_.ovp_claim_pad_m1(0);
391
392 for (size_t i = i0; i < lhs.size(); ++i) {
393 // "i" is in the "with overlap" reference frame.
394 // "pi" is in the "without overlap" reference frame.
395 //
396 // In theory at least, (pi - pl_.ovp_poly_pad(0, 0))
397 // could overflow, but (pi + i) - pl_.ovp_poly_pad(0, 0) cannot.
398 a.push_back(Llc{ci, (pi + i) - pl_.ovp_poly_pad(0, 0), lhs[i]});
399 }
400 }
401 };
402
403 // binding(inputs, R) = binding(pub_inputs, R_p) + binding(witness, R_w)
404 // This method explicitly computes the public binding, and then adds the
405 // constraints that
406 // binding(witness, R_w) = got - binding(pub_inputs, R_p)
407 static size_t input_constraint(const Claims& cla, const Dense<Field>& pub,
408 size_t pub_inputs, size_t num_inputs,
409 size_t pi, Elt got, Elt alpha,
410 std::vector<Llc>& a, std::vector<Elt>& b,
411 size_t ci, const Field& F) {
412 Eqs<Field> eq0(cla.logv, num_inputs, cla.g[0], F);
413 Eqs<Field> eq1(cla.logv, num_inputs, cla.g[1], F);
414 Elt pub_binding = F.zero();
415 for (index_t i = 0; i < num_inputs; ++i) {
416 Elt b_i = F.addf(eq0.at(i), F.mulf(alpha, eq1.at(i)));
417 if (i < pub_inputs) {
418 F.add(pub_binding, F.mulf(b_i, pub.at(i)));
419 } else {
420 // Use (i - pub_inputs) for the index of private inputs.
421 a.push_back(Llc{ci, i - pub_inputs, b_i});
422 }
423 }
424
425 // We view the input constraints as being at fake layer
426 // one past the last real layer. The alternative of
427 // considering the input as part of the last real layer
428 // yields code that looks even more convoluted.
429 PadLayout pl(/*logw=*/0);
430
431 // This paranoid assertion holds unless the circuit has zero
432 // layers, which is not guaranteed by this function alone.
433 check(pi >= pl.ovp_poly_pad(0, 0), "pi >= pl.ovp_poly_pad(0, 0)");
434
435 size_t claim_pad_m1 = pi - pl.ovp_poly_pad(0, 0);
436 a.push_back(Llc{ci, claim_pad_m1 + 0, F.mone()});
437 a.push_back(Llc{ci, claim_pad_m1 + 1, F.negf(alpha)});
438 b.push_back(F.subf(got, pub_binding));
439 return ++ci;
440 }
441
442 static Elt bind_quad(const Layer<Field>* clr, const Claims& cla,
443 const LayerChallenge<Field>* chal, const Field& F) {
444 return clr->quad->bind_gh_all(
445 // G
446 cla.logv, cla.g[0], cla.g[1], chal->alpha, chal->beta,
447 // H
448 clr->logw, chal->hb[0], chal->hb[1],
449 // Field
450 F);
451 }
452};
453} // namespace proofs
454
455#endif // PRIVACY_PROOFS_ZK_LIB_ZK_ZK_COMMON_H_
Definition dense.h:37
Definition eqs.h:32
Definition transcript.h:70
Definition transcript_sumcheck.h:32
Definition zk_common.h:40
Definition circuit.h:117
Definition circuit.h:45
Definition gf2_128.h:63
Definition circuit.h:104
Definition circuit.h:30
Definition ligero_param.h:360
Definition circuit.h:151
Definition circuit.h:132