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