Longfellow ZK 0290cb32
Loading...
Searching...
No Matches
circuit.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_PROTO_CIRCUIT_H_
16#define PRIVACY_PROOFS_ZK_LIB_PROTO_CIRCUIT_H_
17
18#include <sys/types.h>
19
20#include <cstddef>
21#include <cstdint>
22#include <cstdio>
23#include <cstring>
24#include <memory>
25#include <optional>
26#include <unordered_map>
27#include <vector>
28
29#include "algebra/hash.h"
30#include "sumcheck/circuit.h"
31#include "sumcheck/circuit_id.h"
32#include "sumcheck/quad.h"
33#include "util/ceildiv.h"
34#include "util/panic.h"
35#include "util/readbuffer.h"
36
37namespace proofs {
38
39// CircuitRep class handles custom circuit serialization.
40//
41// We expect circuits to be created and stored locally by the prover and
42// verifier respectively. The byte representations are thus assumed to be
43// trusted. As a result, the methods below perform only basic sanity checking.
44//
45// An earlier experiment implemented the IO methods using protobuf parsing.
46// Despite applying techniques like arena allocation, those methods required
47// several seconds to deserialize the circuit. In contrast, these methods take
48// 100s of ms.
49//
50// This class implements an optimization by which internal indices for
51// wire and gate labels and circuit size statistics are stored in a configurable
52// number of bytes (kBytesWritten) which we set to 4 instead of 8 to save
53// space. If this value is set to >4, there is a possibility of failure on
54// 32b platforms, which currently stops execution. Thus, all circuits must be
55// tested on 32b platforms to ensure they are small enough to work.
56enum FieldID {
57 NONE = 0,
58 P256_ID = 1,
59 P384_ID = 2,
60 P521_ID = 3,
61 GF2_128_ID = 4,
62 GF2_16_ID = 5,
63 FP128_ID = 6,
64 FP64_ID = 7,
65 GOLDI_ID = 8,
66 FP64_2_ID = 9,
67 SECP_ID = 10,
68};
69
70template <class Field>
71class CircuitRep {
72 using Elt = typename Field::Elt;
73 using QuadCorner = typename Quad<Field>::quad_corner_t;
74 constexpr static size_t kMaxLayers = 10000; /* deep circuits are errors */
75
76 public:
77 // Serialize kBytesWritten bytes of a size or index used in the circuit to
78 // save space.
79 static constexpr size_t kBytesWritten = 3;
80
81 explicit CircuitRep(const Field& f, FieldID field_id)
82 : f_(f), field_id_(field_id) {}
83
84 void to_bytes(const Circuit<Field>& sc_c, std::vector<uint8_t>& bytes) {
85 EltHash eh(f_);
86 bytes.push_back(0x1); // version
87 serialize_field_id(bytes, field_id_);
88 serialize_size(bytes, sc_c.nv);
89 serialize_size(bytes, sc_c.nc);
90 serialize_size(bytes, sc_c.npub_in);
91 serialize_size(bytes, sc_c.subfield_boundary);
92 serialize_size(bytes, sc_c.ninputs);
93 serialize_size(bytes, sc_c.l.size());
94
95 // Scan the circuit to generate the constant table. To keep one
96 // scan, write the quad to a separate byte vector and later copy it.
97 std::vector<uint8_t> quadb;
98 quadb.reserve(1 << 24);
99 for (const auto& layer : sc_c.l) {
100 serialize_size(quadb, layer.logw);
101 serialize_size(quadb, layer.nw);
102 serialize_size(quadb, layer.quad->n_);
103
104 QuadCorner prevg(0), prevh0(0), prevh1(0);
105 for (size_t i = 0; i < layer.quad->n_; ++i) {
106 serialize_index(quadb, layer.quad->c_[i].g, prevg);
107 prevg = layer.quad->c_[i].g;
108 serialize_index(quadb, layer.quad->c_[i].h[0], prevh0);
109 prevh0 = layer.quad->c_[i].h[0];
110 serialize_index(quadb, layer.quad->c_[i].h[1], prevh1);
111 prevh1 = layer.quad->c_[i].h[1];
112 serialize_num(quadb, eh.kstore(layer.quad->c_[i].v));
113 }
114 }
115
116 serialize_size(bytes, eh.constants_.size());
117 for (const auto& v : eh.constants_) {
118 uint8_t buf[Field::kBytes];
119 f_.to_bytes_field(buf, v);
120 bytes.insert(bytes.end(), buf, buf + Field::kBytes);
121 }
122
123 bytes.insert(bytes.end(), quadb.begin(), quadb.end());
124 bytes.insert(bytes.end(), sc_c.id, sc_c.id + 32);
125 }
126
127 // Returns a unique_ptr<Circuit> or nullptr if there is an error in
128 // deserializing the circuit.
129 //
130 // If ENFORCE_CIRCUIT_ID is TRUE, check that the circuit id in
131 // the serialization matches the id stored in the circuit.
132 std::unique_ptr<Circuit<Field>> from_bytes(ReadBuffer& buf,
133 bool enforce_circuit_id) {
134 if (!buf.have(8 * kBytesWritten + 1)) {
135 return nullptr;
136 }
137
138 uint8_t version = *buf.next(1);
139 if (version != 1) {
140 return nullptr;
141 }
142
143 size_t fid_as_size_t = read_field_id(buf);
144 size_t nv = read_size(buf);
145 size_t nc = read_size(buf);
146 size_t npub_in = read_size(buf);
147 size_t subfield_boundary = read_size(buf);
148 size_t ninputs = read_size(buf);
149 size_t nl = read_size(buf);
150 size_t numconst = read_size(buf);
151
152 // Basic sanity checks.
153 if (fid_as_size_t != static_cast<size_t>(field_id_) || npub_in > ninputs ||
154 subfield_boundary > ninputs || nl > kMaxLayers) {
155 return nullptr;
156 }
157
158 // Ensure there are enough input bytes for the quad constants.
159 auto need = checked_mul(numconst, Field::kBytes);
160 if (!need || !buf.have(need.value())) {
161 return nullptr;
162 }
163
164 std::vector<Elt> constants(numconst);
165 for (size_t i = 0; i < numconst; ++i) {
166 // Fail if Elt cannot be parsed.
167 auto vv = f_.of_bytes_field(buf.next(Field::kBytes));
168 if (!vv.has_value()) {
169 return nullptr;
170 }
171 constants[i] = vv.value();
172 }
173
174 auto c = std::make_unique<Circuit<Field>>();
175 *c = Circuit<Field>{
176 .nv = nv,
177 .logv = lg(nv),
178 .nc = nc,
179 .logc = lg(nc),
180 .nl = nl,
181 .ninputs = ninputs,
182 .npub_in = npub_in,
183 .subfield_boundary = subfield_boundary,
184 };
185 c->l.reserve(nl);
186
187 size_t max_g = nv; // a starting bound on quad number
188
189 for (size_t ly = 0; ly < nl; ++ly) {
190 // Ensure there are enough input bytes for the layer, 3 values.
191 if (!buf.have(3 * kBytesWritten)) {
192 return nullptr;
193 }
194
195 size_t lw = read_size(buf);
196 size_t nw = read_size(buf);
197 size_t nq = read_size(buf);
198
199 // Each quad takes 4 values, check for overflow.
200 need = checked_mul(4 * kBytesWritten, nq);
201 if (!need || !buf.have(need.value())) {
202 return nullptr;
203 }
204
205 auto qq = std::make_unique<Quad<Field>>(nq);
206 size_t prevg = 0, prevhl = 0, prevhr = 0;
207 for (size_t i = 0; i < nq; ++i) {
208 size_t g = read_index(buf, prevg);
209 if (g > max_g) { // index of quad must be < wires in the layer
210 return nullptr;
211 }
212 prevg = g;
213 size_t hl = read_index(buf, prevhl);
214 size_t hr = read_index(buf, prevhr);
215 if (hl > nw || hr > nw) {
216 return nullptr;
217 }
218 prevhl = hl;
219 prevhr = hr;
220 size_t vi = read_num(buf);
221 if (vi >= numconst) {
222 return nullptr;
223 }
224
225 qq->c_[i] = typename Quad<Field>::corner{
226 QuadCorner(g), {QuadCorner(hl), QuadCorner(hr)}, constants[vi]};
227 }
228 c->l.push_back(Layer<Field>{
229 .nw = nw,
230 .logw = lw,
231 .quad = std::unique_ptr<const Quad<Field>>(std::move(qq))});
232 max_g = nw;
233 }
234 // Read the circuit name from the serialization.
235 if (!buf.have(32)) {
236 return nullptr;
237 }
238 buf.next(32, c->id);
239
240 if (enforce_circuit_id) {
241 uint8_t idtmp[32];
242 circuit_id(idtmp, *c, f_);
243 if (memcmp(idtmp, c->id, 32) != 0) {
244 return nullptr;
245 }
246 }
247 return c;
248 }
249
250 private:
251 static constexpr uint64_t kMaxValue = (1ULL << (kBytesWritten * 8)) - 1;
252
253 // Multiplies arguments and checks for overflow.
254 template <typename T>
255 std::optional<T> checked_mul(T a, T b) {
256 T ab = a * b;
257 if (a == 0 || ab / a == b) return ab;
258 return std::nullopt;
259 }
260
261 static void serialize_field_id(std::vector<uint8_t>& bytes, FieldID id) {
262 serialize_num(bytes, static_cast<size_t>(id));
263 }
264
265 static void serialize_size(std::vector<uint8_t>& bytes, size_t sz) {
266 serialize_num(bytes, sz);
267 }
268
269 // We write indices as differences from the previous index. This
270 // encoding appears to produce byte streams that compress better
271 // under both gzip and xz compression. For example, xz compresses
272 // a 35MB test circuit to 2MB without delta encoding, and to 100KB
273 // with delta encoding. We have no real theory to explain this
274 // phenomenon, but at least part of the reason is that the deltas
275 // are usually smaller than the indices.
276 //
277 static void serialize_index(std::vector<uint8_t>& bytes, QuadCorner ind0,
278 QuadCorner prev_ind0) {
279 size_t ind = static_cast<size_t>(ind0);
280 size_t prev_ind = static_cast<size_t>(prev_ind0);
281
282 // Encode the delta IND - PREV_IND. Since the delta can be
283 // negative, and the rest of the code is unsigned only,
284 // use the LSB as sign bit.
285 if (ind >= prev_ind) {
286 serialize_num(bytes, 2u * (ind - prev_ind));
287 } else {
288 serialize_num(bytes, 2u * (prev_ind - ind) + 1u);
289 }
290 }
291
292 static void serialize_num(std::vector<uint8_t>& bytes, size_t g) {
293 check(g < kMaxValue, "Violating small wire-label assumption");
294 uint8_t tmp[kBytesWritten];
295 for (size_t i = 0; i < kBytesWritten; ++i) {
296 tmp[i] = static_cast<uint8_t>(g & 0xff);
297 g >>= 8;
298 }
299 bytes.insert(bytes.end(), tmp, tmp + kBytesWritten);
300 }
301
302 // These routine reads bytes written by serialize_* methods, and thus
303 // only needs to handle values expressed in kBytesWritten.
304 // On 32b platforms, some large circuits may fail; this method
305 // causes a failure in that case.
306
307 // Do not cast to FieldID, since the input is untrusted and the
308 // cast may fail.
309 static size_t read_field_id(ReadBuffer& buf) { return read_num(buf); }
310
311 static size_t read_size(ReadBuffer& buf) { return read_num(buf); }
312
313 static size_t read_index(ReadBuffer& buf, size_t prev_ind) {
314 size_t delta = read_num(buf);
315 if (delta & 1) {
316 return prev_ind - (delta >> 1);
317 } else {
318 return prev_ind + (delta >> 1);
319 }
320 }
321
322 static size_t read_num(ReadBuffer& buf) {
323 uint64_t r = 0;
324 const uint8_t* p = buf.next(kBytesWritten);
325 for (size_t i = 0; i < kBytesWritten; ++i) {
326 r |= (p[i] << (i * 8));
327 }
328
329 // SIZE_MAX is system defined as max value for size_t.
330 // This check fails if a large circuit is loaded on a 32b machine.
331 check(r < SIZE_MAX, "Violating small wire-label assumption");
332 return static_cast<size_t>(r);
333 }
334
335 // Class that defines the hash function for Elt.
336 class EHash {
337 public:
338 const Field& f_;
339 explicit EHash(const Field& f) : f_(f) {}
340 size_t operator()(const Elt& k) const { return elt_hash(k, f_); }
341 };
342
343 // This structure encapsulates the hash used by the compiler.
344 class EltHash {
345 public:
346 std::vector<Elt> constants_;
347
348 explicit EltHash(const Field& f) : f_(f), table_(1000, EHash(f)) {}
349
350 size_t kstore(const Elt& k) {
351 if (auto search = table_.find(k); search != table_.end()) {
352 return search->second;
353 }
354
355 size_t ki = constants_.size();
356 constants_.push_back(k);
357 table_[k] = ki;
358 return ki;
359 }
360
361 private:
362 const Field& f_;
363 std::unordered_map<Elt, size_t, EHash> table_;
364 };
365
366 const Field& f_;
367 FieldID field_id_;
368};
369} // namespace proofs
370
371#endif // PRIVACY_PROOFS_ZK_LIB_PROTO_CIRCUIT_H_
Definition quad.h:37
Definition readbuffer.h:26
Definition circuit.h:45
Definition gf2_128.h:63
Definition circuit.h:30
Definition quad.h:51