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