Longfellow ZK 0290cb32
Loading...
Searching...
No Matches
gf2_128.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_GF2K_GF2_128_H_
16#define PRIVACY_PROOFS_ZK_LIB_GF2K_GF2_128_H_
17
18#include <stdio.h>
19
20#include <array>
21#include <cstddef>
22#include <cstdint>
23#include <optional>
24#include <utility>
25
26#include "gf2k/gf2poly.h"
27#include "gf2k/sysdep.h"
28#include "util/panic.h"
29
30namespace proofs {
31
33
34template <size_t subfield_log_bits = 4>
35class GF2_128 {
36 // avoid writing static_cast<size_t>(1) all the time.
37 static constexpr size_t k1 = 1;
38
39 public:
40 using TypeTag = BinaryFieldTypeTag;
41
42 // Fast representation of the field element via the system-dependent
43 // SIMD type.
44 using N = gf2_128_elt_t;
45
46 // "Slow" representation of the field element as array of
47 // C++ integral types.
48 using N1 = GF2Poly<2>;
49
50 static constexpr size_t kNPolyEvaluationPoints = 6;
51 static constexpr size_t kLogBits = 7;
52 static constexpr size_t kBits = k1 << kLogBits;
53 static constexpr size_t kBytes = kBits / 8u;
54
55 static constexpr size_t kSubFieldLogBits = subfield_log_bits;
56 static constexpr size_t kSubFieldBits = k1 << kSubFieldLogBits;
57 static constexpr size_t kSubFieldBytes = kSubFieldBits / 8u;
58
59 static_assert(kBits == 8u * kBytes);
60 static_assert(kSubFieldBits == 8u * kSubFieldBytes);
61 static constexpr bool kCharacteristicTwo = true;
62
63 struct Elt {
64 N n;
65
66 Elt() : n{} {}
67 explicit Elt(N n_) : n(n_) {}
68
69 // Don't bother using SIMD instructions for comparisons,
70 // otherwise we have to complicate the sysdep API surface.
71 // Unpack into uint64[2] and compute manually.
72 bool operator==(const Elt& y) const { return unpack() == y.unpack(); }
73 bool operator!=(const Elt& y) const { return !operator==(y); }
74
75 // Returns the coefficient of x^i in the polynomial.
76 uint8_t operator[](size_t i) const {
77 auto n1 = uint64x2_of_gf2_128(n);
78 if (i < 64) {
79 return static_cast<uint8_t>((n1[0] >> i) & 0x1);
80 } else if (i < 128) {
81 return static_cast<uint8_t>((n1[1] >> (i - 64)) & 0x1);
82 } else {
83 return 0;
84 }
85 }
86
87 N1 unpack() const { return N1(uint64x2_of_gf2_128(n)); }
88 };
89
90 explicit GF2_128() {
91 kone_ = of_scalar_field(0b1);
92 kx_ = of_scalar_field(0b10);
93
94 // x^{-1} = x^127 + x^6 + x + 1
95 std::array<uint64_t, 2> invx = {
96 (1ull << 6) | (1ull << 1) | (1ull << 0),
97 (1ull << (127 - 64)),
98 };
99 kinvx_ = of_scalar_field(invx);
100
101 Elt g = subfield_generator();
102
103 // basis of the subfield = {1, g, g^2, ...}
104 beta_[0] = one();
105 for (size_t i = 1; i < kSubFieldBits; ++i) {
106 beta_[i] = mulf(beta_[i - 1], g);
107 }
108
109 // Reduce the basis to row-echelon form
110 beta_ref();
111
112 // Evaluation points. We use g^i for these as well
113 poly_evaluation_points_[0] = zero();
114 Elt gi = one();
115 for (size_t i = 1; i < kNPolyEvaluationPoints; ++i) {
116 poly_evaluation_points_[i] = gi;
117 mul(gi, g);
118 }
119
120 for (size_t i = 1; i < kNPolyEvaluationPoints; i++) {
121 for (size_t k = kNPolyEvaluationPoints; k-- > i;) {
122 auto dx =
123 subf(poly_evaluation_points_[k], poly_evaluation_points_[k - i]);
124 check(dx != zero(), "dx != zero()");
125 newton_denominators_[k][i] = invertf(dx);
126 }
127 }
128 }
129
130 GF2_128(const GF2_128&) = delete;
131 GF2_128& operator=(const GF2_128&) = delete;
132
133 // The bits of u are the coordinates with respect to the basis
134 // beta_[] of the subfield.
135 Elt of_scalar(uint64_t u) const {
136 Elt t = zero();
137 for (size_t k = 0; k < kSubFieldBits; ++k, u >>= 1) {
138 if (u & 1) {
139 add(t, beta_[k]);
140 }
141 }
142 check(u == 0, "of_scalar(u), too many bits");
143 return t;
144 }
145
146 Elt of_scalar_field(uint64_t n) const {
147 std::array<uint64_t, 2> u = {n, 0};
148 return of_scalar_field(u);
149 }
150 Elt of_scalar_field(const std::array<uint64_t, 2>& u) const {
151 return Elt(gf2_128_of_uint64x2(u));
152 }
153
154 // The base_only flag is a placeholder that takes meaning when F is an
155 // extension field.
156 std::optional<Elt> of_bytes_field(const uint8_t ab[/* kBytes */],
157 bool base_only = true) const {
158 N1 an = N1::of_bytes(ab);
159 return of_scalar_field(an.u64());
160 }
161
162 void to_bytes_field(uint8_t ab[/* kBytes */], const Elt& x) const {
163 x.unpack().to_bytes(ab);
164 }
165
166 bool in_subfield(Elt e) const {
167 auto eu = solve(e);
168 return eu.first == N1{};
169 }
170
171 std::optional<Elt> of_bytes_subfield(
172 const uint8_t ab[/* kSubFieldBytes */]) const {
173 uint64_t u = 0;
174 for (size_t i = kSubFieldBytes; i-- > 0;) {
175 u <<= 8;
176 u |= ab[i];
177 }
178 return of_scalar(u);
179 }
180
181 void to_bytes_subfield(uint8_t ab[/* kSubFieldBytes */], const Elt& x) const {
182 auto eu = solve(x);
183 check(eu.first == N1{}, "eu.first == N1{}");
184 uint64_t u = eu.second;
185 for (size_t i = 0; i < kSubFieldBytes; ++i) {
186 ab[i] = u & 0xFFu;
187 u >>= 8;
188 }
189 }
190
191 // functional interface
192 Elt addf(const Elt& x, const Elt& y) const {
193 return Elt{gf2_128_add(x.n, y.n)};
194 }
195 Elt subf(const Elt& x, const Elt& y) const {
196 return Elt{gf2_128_add(x.n, y.n)};
197 }
198 Elt mulf(const Elt& x, const Elt& y) const {
199 return Elt{gf2_128_mul(x.n, y.n)};
200 }
201 Elt negf(const Elt& x) const { return x; }
202
203 // two-operands interface
204 void add(Elt& a, const Elt& y) const { a = addf(a, y); }
205 void sub(Elt& a, const Elt& y) const { a = subf(a, y); }
206 void mul(Elt& a, const Elt& y) const { a = mulf(a, y); }
207 void neg(Elt& a) const { /* noop */ }
208 void invert(Elt& a) const { a = invertf(a); }
209
210 Elt zero() const { return Elt{}; }
211 Elt one() const { return kone_; }
212 Elt mone() const { return kone_; }
213 Elt x() const { return kx_; }
214 Elt invx() const { return kinvx_; }
215 Elt beta(size_t i) const {
216 check(i < kSubFieldBits, "i < kSubFieldBits");
217 return beta_[i];
218 }
219
220 Elt poly_evaluation_point(size_t i) const {
221 check(i < kNPolyEvaluationPoints, "i < kNPolyEvaluationPoints");
222 return poly_evaluation_points_[i];
223 }
224
225 // return (X[k] - X[k - i])^{-1}, were X[i] is the
226 // i-th poly evalaluation point.
227 Elt newton_denominator(size_t k, size_t i) const {
228 check(k < kNPolyEvaluationPoints, "k < kNPolyEvaluationPoints");
229 check(i <= k, "i <= k");
230 check(k != (k - i), "k != (k - i)");
231 return newton_denominators_[k][i];
232 }
233
234 Elt invertf(Elt x) const {
235 N1 a = x.unpack();
236 // Let POLY be the generator of GF(2^128) as GF(2)[x]/(POLY(x)).
237 // The Euclid algorithm would initialize B = POLY, but we cannot
238 // store POLY in one N1. Instead, we use the invariant that B is
239 // always "odd" throughout the algorithm, and we represent B =
240 // BM1OX * X + 1, or BM1OX = (B - 1) / X. For B = POLY, BM1OX =
241 // 1/X initially.
242 N1 bm1ox = kinvx_.unpack();
243 Elt u = one();
244 Elt v = zero();
245 while (a != N1(0)) {
246 if (a.bit(0) == 0) {
247 a.shiftr(1);
248 byinvx(u);
249 } else {
250 // Now A is "odd". Write A = AM1OX * X + 1. This operation
251 // be done in-place in the A variable, but we use another
252 // name for clarity.
253 N1 am1ox = a;
254 am1ox.shiftr(1);
255
256 // Normalize to the partial order degree(A) >= degree(B).
257 // We use the stronger total order "<" which is consistent
258 // with the partial order that we care about.
259 if (am1ox < bm1ox) {
260 std::swap(am1ox, bm1ox);
261 std::swap(u, v);
262 }
263 am1ox.sub(bm1ox);
264 sub(u, v);
265 byinvx(u);
266 a = am1ox;
267 }
268 }
269 return v;
270 }
271
272 private:
273 Elt kone_;
274 Elt kx_;
275 Elt kinvx_; // x^{-1}
276 Elt beta_[kSubFieldBits]; // basis of the subfield viewed as a
277 // vector space over GF(2)
278
279 // LU decomposition of beta_, in unpacked format. We store L^{-1}
280 // instead of L, see comments in beta_ref()
281 N1 u_[kSubFieldBits];
282 uint64_t linv_[kSubFieldBits];
283
284 // ldnz_[i] stores the column index of the leading nonzero in u_[i].
285 // This array is in principle redundant, since one can always
286 // reconstruct it from u_, but we cache it for efficiency.
287 size_t ldnz_[kSubFieldBits];
288
289 Elt poly_evaluation_points_[kNPolyEvaluationPoints];
290 Elt newton_denominators_[kNPolyEvaluationPoints][kNPolyEvaluationPoints];
291
292 void byinvx(Elt& u) const { mul(u, kinvx_); }
293
294 Elt subfield_generator() {
295 // Let k = kSubFieldLogBits and n = kLogBits.
296 // Let x be the generator of Field.
297
298 // The generator r of the subfield is then
299 // x^{(2^{2^n}-1)/(2^{2^k}-1)}
300
301 // Compute r via the identity
302 // (2^{2^n}-1)/(2^{2^k}-1) =
303 // (2^{2^k}+1)*(2^{2^(k+1)}+1)*...*(2^{2^(n-1)}+1)
304 Elt r(kx_);
305 for (size_t i = kSubFieldLogBits; i < kLogBits; ++i) {
306 // s <- r^{2^(2^i))
307 Elt s(r);
308 for (size_t j = 0; j < (1u << i); ++j) {
309 mul(s, s);
310 }
311 // r <- r^{2^(2^i)+1)
312 mul(r, s);
313 }
314
315 return r;
316 }
317
318 // beta_ref() is a just a variant of Gaussian elimination, but
319 // because many such variants exist, we now explain the exact
320 // mechanics of the algorithm.
321 //
322 // The problem that we need to solve is the inversion of
323 // of_scalar(): given e = of_scalar(u), solve for u. The constraint
324 // we have is that e and u are arrays of bits, conveniently stored
325 // in uint64_t, and ideally we want to perform parallel bitwise
326 // operations, as opposed to extracting individual bits.
327 //
328 // Consider the following block matrix, or tableau:
329 //
330 // [ B | -I ]
331 // [ ------ ]
332 // [ e | u ]
333 //
334 // Here e and u are reinterpreted as row vectors of GF(2) elements;
335 // I is the identity matrix; B is such that B[i] (the i-th row of b)
336 // is beta(i) (the i-th element of the basis of the subfield), and
337 // beta(i) is interpreted as a row vector of 128 GF(2) elements.
338 // (The minus sign in -I is irrelevant over GF(2), but would be
339 // necessary over other fields.)
340 //
341 // We now postulate that the only allowed operation on the tableau
342 // is "axpy": add one row to another, which we can do efficiently
343 // via bitwise xor.
344 //
345 // of_scalar(u) can be reinterpreted in terms axpy as follows.
346 // Start with a tableau with e=0. Reduce u to 0 via axpy
347 // operations, e.g., for all i such that u[i] = 1, add row i to the
348 // last row. Because this is exactly what of_scalar() does, at the
349 // end of the process we have e = of_scalar(u). Because I is
350 // full-rank, any sequence of axpy's that reduces u to 0 produces
351 // the same e.
352 //
353 // We now want to invert the of_scalar() process. We cannot apply
354 // the axpy operations in of_scalar() in reverse order, because we
355 // don't know u, and thus we don't know which operations
356 // of_scalar(u) would apply. However, because B is a basis, any
357 // sequence of axpy operations that starts with u=0 and reduces e to
358 // 0 reconstructs the same u.
359 //
360 // For lack of a better idea, we choose the following sequence of
361 // axpy operations. First reduce B to row-echelon form via axpy
362 // operations on B, and then reduce e to zero via additional axpy
363 // operations. A matrix U is in row-echelon form if the following
364 // condition holds: i' > i implies that U[i'][ldnz[i]] = 0, where
365 // ldnz[i] is the column index of the leading nonzero in row i.
366 //
367 // Since B is constant, we choose to pre-compute the row-echelon
368 // form of B in beta_ref(), and finish the process in solve() when e
369 // is known, for multiple values of e.
370 //
371 // As it happens, reducing B to row-echelon transforms the -I
372 // in the upper-right block to -L^{-1}, where B=LU is the LU
373 // factorization of B. We don't use this correspondence anywhere
374 // in the code other than in the choice of the name Linv for the block.
375 //
376 void beta_ref() {
377 for (size_t i = 0; i < kSubFieldBits; ++i) {
378 // B in the tableau, becomes U at the end.
379 u_[i] = beta_[i].unpack();
380
381 // -I in the tableau, becomes -L^{-1} at the end.
382 // Ignore the minus sign over GF(2).
383 linv_[i] = (static_cast<uint64_t>(1) << i);
384 }
385
386 // Reduce B to row-echelon form.
387 //
388 // Invariant: B([0,RNK), [0,J)) is already in row-echelon form.
389 // The loop body extends this property to J+1 and possibly RNK+1.
390 size_t rnk = 0;
391 for (size_t j = 0; rnk < kSubFieldBits && j < kBits; ++j) {
392 // find pivot at row >= RNK in column J
393 for (size_t i = rnk; i < kSubFieldBits; ++i) {
394 if (u_[i].bit(j)) {
395 std::swap(u_[rnk], u_[i]);
396 std::swap(linv_[rnk], linv_[i]);
397 goto have_pivot;
398 }
399 }
400 // If we get here there is no pivot. Keep the rank RNK the same
401 // and proceed to the next column ++J
402 continue;
403
404 have_pivot:
405 ldnz_[rnk] = j;
406
407 // Pivot on [rnk][j].
408 for (size_t i1 = rnk + 1; i1 < kSubFieldBits; ++i1) {
409 if (u_[i1].bit(j)) {
410 u_[i1].add(u_[rnk]); // axpy on U
411 linv_[i1] ^= linv_[rnk]; // axpy on Linv
412 }
413 }
414 ++rnk;
415 }
416
417 // the basis is indeed a basis:
418 check(rnk == kSubFieldBits, "rnk == kSubFieldBits");
419 }
420
421 std::pair<N1, uint64_t> solve(const Elt& e) const {
422 uint64_t u = 0;
423 N1 ue = e.unpack();
424 for (size_t rnk = 0; rnk < kSubFieldBits; ++rnk) {
425 size_t j = ldnz_[rnk];
426 if (ue.bit(j)) {
427 ue.add(u_[rnk]);
428 u ^= linv_[rnk];
429 }
430 }
431
432 return std::pair(ue, u);
433 }
434};
435
436} // namespace proofs
437
438#endif // PRIVACY_PROOFS_ZK_LIB_GF2K_GF2_128_H_
Definition gf2poly.h:29
Definition gf2_128.h:32
Definition gf2_128.h:63