Longfellow ZK 0290cb32

Loading...
Searching...
No Matches
ligero_param.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_LIGERO_LIGERO_PARAM_H_
16#define PRIVACY_PROOFS_ZK_LIB_LIGERO_LIGERO_PARAM_H_
17
18#include <stddef.h>
19
20#include <algorithm>
21#include <array>
22#include <cstdint>
23#include <cstring>
24#include <vector>
25
26#include "algebra/blas.h"
27#include "merkle/merkle_commitment.h"
28#include "merkle/merkle_tree.h"
29#include "util/ceildiv.h"
30#include "util/crypto.h"
31#include "util/panic.h"
32
33/*
34
35 This is an implementation of the Ligero protocol described in
36
37 Ligero: Lightweight Sublinear Arguments
38 Without a Trusted Setup,
39
40 Scott Ames and Carmit Hazay and Yuval Ishai and
41 Muthuramakrishnan Venkitasubramaniam,
42 https://eprint.iacr.org/2022/1608
43 doi = {10.1145/3133956},
44
45 The main data structure in the prover is a 2D array which we call a
46 tableau organized as follows.
47
48 Fix a block size BLOCK and let DBLOCK = 2 * BLOCK - 1. Fix another
49 quantity BLOCK_EXT >= 0.
50
51 Each row in the tableau has the form [X XD XEXT], where X is a row
52 of BLOCK elements, XD is a row of BLOCK - 1 elements, and XEXT is a
53 row of BLOCK_EXT elements. We call the X part the "block" and the
54 XEXT part the "extension".
55
56 Let BLOCK_ENC = 2 * BLOCK - 1 + BLOCK_EXT = DBLOCK + BLOCK_EXT be
57 the total size of the row.
58
59 A "witness block" has the form [RANDOM[R], WITNESS[W]], where R + W
60 = BLOCK. The randomess (of size R) is used for zero-knowledge
61 blinding. Although not strictly required by Ligero, we require W >=
62 R to avoid wasting too much space, so that a witness block is at
63 least half full.
64
65 A block is interpreted as evaluations of some polynomial at point
66 INJ(j) for 0 <= j < BLOCK, where INJ(.) is some field-specific
67 injection that injects small natural numbers into distinct field
68 elements. With the condition that the degree of the polynomial be
69 less than BLOCK, the polynomial is uniquely determined, and the rest
70 [XD XEXT] of the row is then computed as the evaluations of that
71 polynomial for BLOCK <= j < BLOCK_ENC.
72
73 To the extent that Ligero is based on Reed-Solomon codes, X is the
74 "message" and XEXT is the "codeword". The "rate" is thus BLOCK /
75 BLOCK_EXT.
76
77 However, Ligero also needs products of two polynomials of degree
78 less than BLOCK, so that the product has degree less than 2 * BLOCK
79 - 1 = DBLOCK. XD exists in the tableau to facilitate the
80 computation of these products. For zero knowledge, the indices of
81 XD must be distinct from the indices of BLOCK_EXT.
82
83 We now discuss the row structure of the tableau. The first three
84 rows are special and used for zero-knowledge blinding purposes.
85
86 The first row, row ILDT for ILDT = 0, used for the low-degree test,
87 consists of BLOCK random field elements, extended to BLOCK_ENC.
88
89 The second row, row IDOT for IDOT = 1, used in the linear test,
90 consists of DBLOCK random field elements, with the additional
91 constraint that the double block sum to 0. As usual, the row is
92 extended to BLOCK_ENC by interpolation.
93
94 The third row, row IQUAD for IQUAD = 2, used in the quadratic test,
95 consists of DBLOCK random field elements, with the additional
96 constraint that the WITNESS portion of the block be zero. Thus, the
97 structure is really [RANDOM[R] ZERO[W] RANDOM[BLOCK-1]], extended to
98 BLOCK_ENC by interpolation.
99
100 The next group of "witness rows" IW <= I < IQ for IW = 3, stores
101 witnesses. Each row is a witness block extended to BLOCK_ENC.
102
103 The next group of "quadratic" rows IQ <= I < NROW, has the same
104 syntactic structure as the "witness" rows, but they are used in the
105 quadratic check in addition to the linear check. In Ligero, a
106 quadratic constraint induces three entries in three quadratic rows.
107 Thus, for NQ total quadratic constraints and W useful entries per
108 row, we have a total of 3 * (NQ / W) quadratic rows. To enforce
109 this structure, the code stores NQTRIPLES = (NQ / W) instead of the
110 number 3 * NQTRIPLES of rows.
111
112 */
113
114 namespace proofs {
115
116 template <class Field>
117 struct LigeroParam {
118 using Elt = typename Field::Elt;
119
120 // parameters passed by the user
121 size_t nw; // total number of witnesses
122 size_t nq; // total number of quadratic constraints
123 size_t rateinv; // inverse rate of the error-correcting code
124 size_t nreq; // number of opened columns
125
126 // computed parameters
127 size_t block_enc; // total number of elts per row
128 size_t block; // number of elts per block
129 size_t dblock; // 2 * BLOCK - 1
130 size_t block_ext; // BLOCK_ENC - DBLOCK (number of leaves in the
131 // Merkle tree).
132 size_t r; // number of random elts in a witness block
133 size_t w; // number of witnesses in a witness block
134 size_t nwrow; // number of witness rows
135 size_t nqtriples; // number of triples of quadratic-check rows
136 size_t nwqrow; // nwqrow + nqtriples
137 size_t nrow; // total number of rows (nwqrow + three blinding rows)
138 size_t mc_pathlen; // length of a Merkle-tree proof
139 // with BLOCK_ENC-BLOCK leaves
140
141 // layout of rows
142 size_t ildt; // blinding for the low-degree test
143 size_t idot; // blinding row for the dot-product check
144 size_t iquad; // blinding row for the quadratic check
145 size_t iw; // first witness row
146 size_t iq; // first quadratic row
147
148 // Deprecated version of the constructor attempts to optimize the block_enc
149 // parameter. This optimization can now be performed offline and stored as
150 // a parameter in ZkSpecStruct.
151 // TODO(shelat): Remove this constructor once version 3 is deprecated.
152 LigeroParam(size_t nw, size_t nq, size_t rateinv, size_t nreq)
153 : nw(nw), nq(nq), rateinv(rateinv), nreq(nreq) {
154 r = nreq;
155
156 size_t min_proof_size = SIZE_MAX;
157 size_t best_block_enc = 1;
158 for (size_t e = 1; e <= (1 << 28); e *= 2) {
159 size_t proof_size = layout(e);
160 if (proof_size < min_proof_size) {
161 min_proof_size = proof_size;
162 best_block_enc = e;
163 }
164 }
165
166 // recompute parameters
167 layout(best_block_enc);
168 sanity();
169 }
170
171 // Constructor that accepts a pre-computed block_enc.
172 LigeroParam(size_t nw, size_t nq, size_t rateinv, size_t nreq,
173 size_t be)
174 : nw(nw), nq(nq), rateinv(rateinv), nreq(nreq), block_enc(be) {
175 r = nreq;
176 check(layout(block_enc) < SIZE_MAX, "block_enc too large");
177 sanity();
178 }
179
180 // Return an estimate of the proof size.
181 //
182 // This function is kind of a hack in that it breaks abstraction
183 // boundaries, e.g. it knows about the size and layout of the Merkle
184 // commitment. Punt on this wart until we have a better theory.
185 size_t layout(size_t e) {
186 // Maximum size we are prepared to handle. All dimensions will be
187 // required to be < MAX_SIZE. In principle we could handle all
188 // size_t, but we want 64-bit code to fail if it would fail on a
189 // 32-bit machine, and for maximum paranoia we restrict to 28
190 // bits, since one cannot malloc 2^{28} Elts on a 32-bit machine
191 // anyway.
192 constexpr size_t max_lg_size = 28;
193 constexpr size_t max_size = static_cast<size_t>(1) << max_lg_size;
194 block_enc = e;
195
196 // block_enc must fit in the subfield
197 size_t subfield_bits = 8 * Field::kSubFieldBytes;
198 if (subfield_bits <= max_lg_size) {
199 if (block_enc >= (static_cast<size_t>(1) << subfield_bits)) {
200 return SIZE_MAX;
201 }
202 }
203
204 // limit block_enc to avoid overflow in the computation
205 // of the proof size
206 if (block_enc > max_size || rateinv > max_size ||
207 (block_enc + 1) < (2 + rateinv)) {
208 return SIZE_MAX;
209 }
210
211 block = (block_enc + 1) / (2 + rateinv);
212 // now 1 <= BLOCK < MAX_SIZE / 2
213
214 // Ensure BLOCK = R + W (syntactic property)
215 if (block < r) {
216 return SIZE_MAX;
217 }
218 w = block - r;
219
220 // now r <= BLOCK < MAX_SIZE / 2
221 // 0 <= W < MAX_SIZE / 2
222 // 0 <= W <= BLOCK
223 // 0 <= R <= BLOCK
224 // W + R == BLOCK
225
226 // Ensure W >= R (needed for reasonable space utilization).
227 if (w < r) {
228 return SIZE_MAX;
229 }
230 // now R <= W < MAX_SIZE
231
232 // Finish the layout of a row
233 dblock = 2 * block - 1;
234 // now DBLOCK < MAX_SIZE
235
236 // Ensure BLOCK_ENC >= 0 (syntactic property). Should be true
237 // for any reasonable rateinv, but check anyway.
238 if (block_enc < dblock) {
239 return SIZE_MAX;
240 }
241 // now DBLOCK <= BLOCK_ENC
242
243 block_ext = block_enc - dblock;
244 // now 0 <= BLOCK_EXT < MAX_SIZE
245
246 nwrow = ceildiv(nw, w);
247 nqtriples = ceildiv(nq, w);
248
249 nwqrow = nwrow + 3 * nqtriples;
250 nrow = nwqrow + /*blinding rows=*/3;
251
252 // The total number of elements (NROW * BLOCK_ENC) in the tableau
253 // must fit in MAX_SIZE.
254 if (nrow >= max_size / block_enc) {
255 return SIZE_MAX;
256 }
257
258 mc_pathlen = merkle_commitment_len(block_ext);
259
260 /* proof+commitment size. */
261 // Compute the size in uint64_t instead of size_t since
262 // I am too lazy to worry about overflow.
263 uint64_t sz = 0;
264
265 // commitment
266 sz += sizeof(Digest);
267
268 // Merkle openings, approximated because the exact # of leaves depends
269 // on the random coins.
270 sz += static_cast<uint64_t>(mc_pathlen) / 2 * static_cast<uint64_t>(nreq) *
271 static_cast<uint64_t>(Digest::kLength);
272
273 // y_ldt
274 sz += static_cast<uint64_t>(block) * static_cast<uint64_t>(Field::kBytes);
275
276 // y_dot
277 sz += static_cast<uint64_t>(dblock) * static_cast<uint64_t>(Field::kBytes);
278
279 // y_quad
280 // The quadratic-test response has size DBLOCK, but W elements
281 // are expected to be zero and not serialized.
282 sz += static_cast<uint64_t>(dblock - w) *
283 static_cast<uint64_t>(Field::kBytes);
284
285 // nonces
286 sz += static_cast<uint64_t>(nreq) *
287 static_cast<uint64_t>(MerkleNonce::kLength);
288
289 // req. Assume optimistically that all elements are in the subfield.
290 sz += static_cast<uint64_t>(nrow) * static_cast<uint64_t>(nreq) *
291 static_cast<uint64_t>(Field::kSubFieldBytes);
292
293 sz = std::min<uint64_t>(sz, SIZE_MAX);
294 return static_cast<size_t>(sz);
295 }
296
297 private:
298 void sanity() {
299 proofs::check(block_enc > block, "block_enc > block");
300 ildt = 0;
301 idot = 1;
302 iquad = 2;
303 iw = 3;
304 iq = iw + nwrow;
305 proofs::check(nrow == iq + 3 * nqtriples, "nrow == iq + 3 * nqtriples");
306 }
307};
308
309 template <class Field>
310 struct LigeroCommitment {
311 Digest root;
312};
313
314 template <class Field>
315 struct LigeroProof {
316 using Elt = typename Field::Elt;
317 explicit LigeroProof(const LigeroParam<Field> *p)
318 : block(p->block),
319 dblock(p->dblock),
320 r(p->r),
321 block_enc(p->block_enc),
322 nrow(p->nrow),
323 nreq(p->nreq),
324 mc_pathlen(p->mc_pathlen),
325 y_ldt(p->block),
326 y_dot(p->dblock),
327 y_quad_0(p->r),
328 y_quad_2(p->dblock - p->block),
329 req(p->nrow * p->nreq),
330 merkle(p->nreq) {}
331
332 // The proof stores a copy of all parameters relevant to the proof.
333 size_t block;
334 size_t dblock;
335 size_t r;
336 size_t block_enc;
337 size_t nrow;
338 size_t nreq;
339 size_t mc_pathlen;
340
341 std::vector<Elt> y_ldt; // [block]
342 std::vector<Elt> y_dot; // [dblock]
343 std::vector<Elt> y_quad_0; // [r] first part of y_quad.
344 // The middle part [w] of y_quad is zero and not transmitted.
345 std::vector<Elt> y_quad_2; // [dblock - block] last part of y_quad
346 std::vector<Elt> req; // [nrow, nreq]
347 MerkleProof merkle;
348
349 Elt &req_at(size_t i, size_t j) { return req[i * nreq + j]; }
350 const Elt &req_at(size_t i, size_t j) const { return req[i * nreq + j]; }
351};
352
353// a nonzero entry in the matrix A that defines
354// the linear constraints A w = b. The term
355// states that A[c, w] = k, where the "row"
356// c is interpreted as the constraint index, and
357// the "column" w is interpreted as the witness
358// index
359 template <class Field>
360 struct LigeroLinearConstraint {
361 using Elt = typename Field::Elt;
362 size_t c;
363 size_t w;
364 Elt k;
365};
366
367// encode W[X] * W[Y] - W[Z] = 0
368 struct LigeroQuadraticConstraint {
369 size_t x;
370 size_t y;
371 size_t z;
372};
373
374 template <class Field>
375 class LigeroCommon {
376 using Elt = typename Field::Elt;
377
378 public:
379 // create a grand dot product by A given the user-provided
380 // linear-constraint terms LLTERM, the quadratic constraints LQC,
381 // and their random challenges ALPHAL, ALPHAQ.
382 static void inner_product_vector(
383 Elt A[/*nwqrow, w*/], const LigeroParam<Field> &p, size_t nl,
384 size_t nllterm, const LigeroLinearConstraint<Field> llterm[/*nllterm*/],
385 const Elt alphal[/*nl*/], const LigeroQuadraticConstraint lqc[/*nq*/],
386 const std::array<Elt, 3> alphaq[/*nq*/], const Field &F) {
387 // clear A and overwrite it later.
388 Blas<Field>::clear(p.nwqrow * p.w, A, 1, F);
389
390 // random linear combinations of the linear constraints
391 for (size_t l = 0; l < nllterm; ++l) {
392 const auto &term = llterm[l];
393 proofs::check(term.w < p.nw, "term.w < p.nw");
394 proofs::check(term.c < nl, "term.c < nl");
395 F.add(A[term.w], F.mulf(term.k, alphal[term.c]));
396 }
397
398 // routing terms for quadratic constraints
399 Elt *Ax = &A[p.nwrow * p.w];
400 Elt *Ay = Ax + (p.nqtriples * p.w);
401 Elt *Az = Ay + (p.nqtriples * p.w);
402
403 for (size_t i = 0; i < p.nqtriples; ++i) {
404 for (size_t j = 0; j < p.w && j + i * p.w < p.nq; ++j) {
405 // index into [_ , W] arrays
406 size_t iw = j + i * p.w;
407 const auto *l = &lqc[iw];
408 F.add(Ax[iw], alphaq[iw][0]);
409 F.sub(A[l->x], alphaq[iw][0]);
410
411 F.add(Ay[iw], alphaq[iw][1]);
412 F.sub(A[l->y], alphaq[iw][1]);
413
414 F.add(Az[iw], alphaq[iw][2]);
415 F.sub(A[l->z], alphaq[iw][2]);
416 }
417 }
418 }
419
420 // layout a witness block where the "witness" is public, and
421 // thus the randomess is zero.
422 static void layout_Aext(Elt Aext[/*>=block*/], const LigeroParam<Field> &p,
423 size_t i, const Elt A[/*nwqrow, nw*/],
424 const Field &F) {
425 Blas<Field>::clear(p.r, &Aext[0], 1, F);
426 Blas<Field>::copy(p.w, &Aext[p.r], 1, &A[i * p.w], 1);
427 }
428
429 static void column_hash(size_t n, const Elt x[/*n:incx*/], size_t incx,
430 SHA256 &sha, const Field &F) {
431 for (size_t i = 0; i < n; ++i) {
432 uint8_t buf[Field::kBytes];
433 F.to_bytes_field(buf, x[i * incx]);
434 sha.Update(buf, sizeof(buf));
435 }
436 }
437};
438
439// A struct representing the hash of llterms. It is really the
440// same as Digest, but in theory Ligero should exist independently
441// of the Merkle tree.
442 struct LigeroHash {
443 static constexpr size_t kLength = kSHA256DigestSize;
444 uint8_t bytes[kLength];
445};
446
447} // namespace proofs
448
449#endif // PRIVACY_PROOFS_ZK_LIB_LIGERO_LIGERO_PARAM_H_
proofs::LigeroCommon
Definition ligero_param.h:375
proofs::SHA256
Definition crypto.h:40
proofs::Digest
Definition merkle_tree.h:43
proofs::GF2_128::Elt
Definition gf2_128.h:63
proofs::LigeroCommitment
Definition ligero_param.h:310
proofs::LigeroHash
Definition ligero_param.h:442
proofs::LigeroLinearConstraint
Definition ligero_param.h:360
proofs::LigeroParam
Definition ligero_param.h:117
proofs::LigeroQuadraticConstraint
Definition ligero_param.h:368
proofs::MerkleProof
Definition merkle_commitment.h:36
proofs::term
Definition node.h:30