Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
element_impl.hpp
Go to the documentation of this file.
1// === AUDIT STATUS ===
2// internal: { status: Planned, auditors: [], commit: }
3// external_1: { status: not started, auditors: [], commit: }
4// external_2: { status: not started, auditors: [], commit: }
5// =====================
6
7#pragma once
13#include "element.hpp"
14#include <cstdint>
15
16// NOLINTBEGIN(readability-implicit-bool-conversion, cppcoreguidelines-avoid-c-arrays)
17namespace bb::group_elements {
18template <class Fq, class Fr, class T>
19constexpr element<Fq, Fr, T>::element(const Fq& a, const Fq& b, const Fq& c) noexcept
20 : x(a)
21 , y(b)
22 , z(c)
23{}
24
25template <class Fq, class Fr, class T>
27 : x(other.x)
28 , y(other.y)
29 , z(other.z)
30{}
31
32template <class Fq, class Fr, class T>
34 : x(other.x)
35 , y(other.y)
36 , z(other.z)
37{}
38
39template <class Fq, class Fr, class T>
41 : x(other.x)
42 , y(other.y)
43 , z(Fq::one())
44{}
45
46template <class Fq, class Fr, class T>
48{
49 if (this == &other) {
50 return *this;
51 }
52 x = other.x;
53 y = other.y;
54 z = other.z;
55 return *this;
56}
57
58template <class Fq, class Fr, class T>
60{
61 x = other.x;
62 y = other.y;
63 z = other.z;
64 return *this;
65}
66
67// Warning: variable-time — calls `z.invert()` (Bernstein-Yang safegcd). Do not
68// use on points derived from secret material (signing nonces, private keys, DH
69// shared secrets). For those, call `to_affine_const_time()` explicitly; the
70// implicit conversion does NOT pick up the const-time path.
71template <class Fq, class Fr, class T> constexpr element<Fq, Fr, T>::operator affine_element<Fq, Fr, T>() const noexcept
72{
73 if (is_point_at_infinity()) {
75 result.x = Fq(0);
76 result.y = Fq(0);
77 result.self_set_infinity();
78 return result;
79 }
80 Fq z_inv = z.invert();
81 Fq zz_inv = z_inv.sqr();
82 Fq zzz_inv = zz_inv * z_inv;
83 affine_element<Fq, Fr, T> result(x * zz_inv, y * zzz_inv);
84 return result;
85}
86
87template <class Fq, class Fr, class T>
89{
90 if (is_point_at_infinity()) {
92 result.x = Fq(0);
93 result.y = Fq(0);
94 result.self_set_infinity();
95 return result;
96 }
97 Fq z_inv = z.invert_const_time();
98 Fq zz_inv = z_inv.sqr();
99 Fq zzz_inv = zz_inv * z_inv;
100 affine_element<Fq, Fr, T> result(x * zz_inv, y * zzz_inv);
101 return result;
102}
103
104template <class Fq, class Fr, class T> constexpr void element<Fq, Fr, T>::self_dbl() noexcept
105{
106 if constexpr (Fq::modulus.data[3] >= MODULUS_TOP_LIMB_LARGE_THRESHOLD) {
107 if (is_point_at_infinity()) {
108 return;
109 }
110 } else {
111 if (x.is_msb_set_word()) {
112 return;
113 }
114 }
115
116 // T0 = x*x
117 Fq T0 = x.sqr();
118
119 // T1 = y*y
120 Fq T1 = y.sqr();
121
122 // T2 = T1*T1 = y*y*y*y
123 Fq T2 = T1.sqr();
124
125 // T1 = T1 + x = x + y*y
126 T1 += x;
127
128 // T1 = T1 * T1
129 T1.self_sqr();
130
131 // T3 = T0 + T2 = xx + y*y*y*y
132 Fq T3 = T0 + T2;
133
134 // T1 = T1 - T3 = x*x + y*y*y*y + 2*x*x*y*y*y*y - x*x - y*y*y*y = 2*x*x*y*y*y*y = 2*S
135 T1 -= T3;
136
137 // T1 = 2T1 = 4*S
138 T1 += T1;
139
140 // T3 = 3T0
141 T3 = T0 + T0;
142 T3 += T0;
143 if constexpr (T::has_a) {
144 T3 += (T::a * z.sqr().sqr());
145 }
146
147 // z2 = 2*y*z
148 z += z;
149 z *= y;
150
151 // T0 = 2T1
152 T0 = T1 + T1;
153
154 // x2 = T3*T3
155 x = T3.sqr();
156
157 // x2 = x2 - 2T1
158 x -= T0;
159
160 // T2 = 8T2
161 T2 += T2;
162 T2 += T2;
163 T2 += T2;
164
165 // y2 = T1 - x2
166 y = T1 - x;
167
168 // y2 = y2 * T3 - T2
169 y *= T3;
170 y -= T2;
171}
172
173template <class Fq, class Fr, class T> constexpr element<Fq, Fr, T> element<Fq, Fr, T>::dbl() const noexcept
174{
175 element result(*this);
176 result.self_dbl();
177 return result;
178}
179
180template <class Fq, class Fr, class T>
182{
183 if constexpr (Fq::modulus.data[3] >= MODULUS_TOP_LIMB_LARGE_THRESHOLD) {
184 // If either point is infinity, return the other point
185 if (other.is_point_at_infinity()) {
186 return *this;
187 }
188 if (is_point_at_infinity()) {
189 *this = { other.x, other.y, Fq::one() };
190 return *this;
191 }
192 } else {
193 const bool edge_case_trigger = x.is_msb_set() || other.x.is_msb_set();
194 if (edge_case_trigger) {
195 if (x.is_msb_set()) {
196 *this = { other.x, other.y, Fq::one() };
197 }
198 return *this;
199 }
200 }
201
202 // T0 = z1.z1
203 Fq T0 = z.sqr();
204
205 // T1 = x2.t0 - x1 = x2.z1.z1 - x1
206 Fq T1 = other.x * T0;
207 T1 -= x;
208
209 // T2 = T0.z1 = z1.z1.z1
210 // T2 = T2.y2 - y1 = y2.z1.z1.z1 - y1
211 Fq T2 = z * T0;
212 T2 *= other.y;
213 T2 -= y;
214
215 if (__builtin_expect(T1.is_zero(), 0)) {
216 if (T2.is_zero()) {
217 self_dbl();
218 return *this;
219 }
220 self_set_infinity();
221 return *this;
222 }
223
224 // T2 = 2T2 = 2(y2.z1.z1.z1 - y1) = R
225 // z3 = z1 + H
226 T2 += T2;
227 z += T1;
228
229 // T3 = T1*T1 = HH
230 Fq T3 = T1.sqr();
231
232 // z3 = z3 - z1z1 - HH
233 T0 += T3;
234
235 // z3 = (z1 + H)*(z1 + H)
236 z.self_sqr();
237 z -= T0;
238
239 // T3 = 4HH
240 T3 += T3;
241 T3 += T3;
242
243 // T1 = T1*T3 = 4HHH
244 T1 *= T3;
245
246 // T3 = T3 * x1 = 4HH*x1
247 T3 *= x;
248
249 // T0 = 2T3
250 T0 = T3 + T3;
251
252 // T0 = T0 + T1 = 2(4HH*x1) + 4HHH
253 T0 += T1;
254 x = T2.sqr();
255
256 // x3 = x3 - T0 = R*R - 8HH*x1 -4HHH
257 x -= T0;
258
259 // T3 = T3 - x3 = 4HH*x1 - x3
260 T3 -= x;
261
262 T1 *= y;
263 T1 += T1;
264
265 // T3 = T2 * T3 = R*(4HH*x1 - x3)
266 T3 *= T2;
267
268 // y3 = T3 - T1
269 y = T3 - T1;
270 return *this;
271}
272
273template <class Fq, class Fr, class T>
274constexpr element<Fq, Fr, T> element<Fq, Fr, T>::operator+(const affine_element<Fq, Fr, T>& other) const noexcept
275{
276 element result(*this);
277 return (result += other);
278}
279
280template <class Fq, class Fr, class T>
281constexpr element<Fq, Fr, T> element<Fq, Fr, T>::operator-=(const affine_element<Fq, Fr, T>& other) noexcept
282{
283 const affine_element<Fq, Fr, T> to_add{ other.x, -other.y };
284 return operator+=(to_add);
285}
286
287template <class Fq, class Fr, class T>
288constexpr element<Fq, Fr, T> element<Fq, Fr, T>::operator-(const affine_element<Fq, Fr, T>& other) const noexcept
289{
290 element result(*this);
291 return (result -= other);
292}
293
294template <class Fq, class Fr, class T>
296{
297 if constexpr (Fq::modulus.data[3] >= MODULUS_TOP_LIMB_LARGE_THRESHOLD) {
298 bool p1_zero = is_point_at_infinity();
299 bool p2_zero = other.is_point_at_infinity();
300 if (__builtin_expect((p1_zero || p2_zero), 0)) {
301 if (p1_zero && !p2_zero) {
302 *this = other;
303 return *this;
304 }
305 if (p2_zero && !p1_zero) {
306 return *this;
307 }
308 self_set_infinity();
309 return *this;
310 }
311 } else {
312 bool p1_zero = x.is_msb_set();
313 bool p2_zero = other.x.is_msb_set();
314 if (__builtin_expect((p1_zero || p2_zero), 0)) {
315 if (p1_zero && !p2_zero) {
316 *this = other;
317 return *this;
318 }
319 if (p2_zero && !p1_zero) {
320 return *this;
321 }
322 self_set_infinity();
323 return *this;
324 }
325 }
326 Fq Z1Z1(z.sqr());
327 Fq Z2Z2(other.z.sqr());
328 Fq S2(Z1Z1 * z);
329 Fq U2(Z1Z1 * other.x);
330 S2 *= other.y;
331 Fq U1(Z2Z2 * x);
332 Fq S1(Z2Z2 * other.z);
333 S1 *= y;
334
335 Fq F(S2 - S1);
336
337 Fq H(U2 - U1);
338
339 if (__builtin_expect(H.is_zero(), 0)) {
340 if (F.is_zero()) {
341 self_dbl();
342 return *this;
343 }
344 self_set_infinity();
345 return *this;
346 }
347
348 F += F;
349
350 Fq I(H + H);
351 I.self_sqr();
352
353 Fq J(H * I);
354
355 U1 *= I;
356
357 U2 = U1 + U1;
358 U2 += J;
359
360 x = F.sqr();
361
362 x -= U2;
363
364 J *= S1;
365 J += J;
366
367 y = U1 - x;
368
369 y *= F;
370
371 y -= J;
372
373 z += other.z;
374
375 Z1Z1 += Z2Z2;
376
377 z.self_sqr();
378 z -= Z1Z1;
379 z *= H;
380 return *this;
381}
382
383template <class Fq, class Fr, class T>
384constexpr element<Fq, Fr, T> element<Fq, Fr, T>::operator+(const element& other) const noexcept
385{
386 element result(*this);
387 return (result += other);
388}
389
390template <class Fq, class Fr, class T>
392{
393 const element to_add{ other.x, -other.y, other.z };
394 return operator+=(to_add);
395}
396
397template <class Fq, class Fr, class T>
398constexpr element<Fq, Fr, T> element<Fq, Fr, T>::operator-(const element& other) const noexcept
399{
400 element result(*this);
401 return (result -= other);
402}
403
404template <class Fq, class Fr, class T> constexpr element<Fq, Fr, T> element<Fq, Fr, T>::operator-() const noexcept
405{
406 return { x, -y, z };
407}
408
409template <class Fq, class Fr, class T>
411{
412 if constexpr (T::USE_ENDOMORPHISM) {
413 return mul_with_endomorphism(exponent);
414 }
415 return mul_without_endomorphism(exponent);
416}
417
418template <class Fq, class Fr, class T> element<Fq, Fr, T> element<Fq, Fr, T>::operator*=(const Fr& exponent) noexcept
419{
420 *this = operator*(exponent);
421 return *this;
422}
423
424template <class Fq, class Fr, class T>
426{
427 if (engine == nullptr) {
429 }
430
431 // Convert the scalar to canonical u256 form
432 const uint256_t k = uint256_t(scalar);
433
434 // Coron's first DPA countermeasure (J.-S. Coron, "Resistance against Differential Power Analysis
435 // for Elliptic Curve Cryptosystems", CHES 1999, LNCS 1717, pp. 292-302, Section 5.1): blind the
436 // scalar with k' = k + r * n where r is a fresh random 64-bit value sampled per call. Since
437 // n * P = O for any P in the prime-order subgroup, k' * P = k * P. The randomization defeats
438 // DPA: per-bit traces of two signings with the same k decorrelate because the bit pattern of k'
439 // differs across calls.
440 //
441 // We force the high bit of r to be 1 so that r is sampled uniformly from [2^63, 2^64). This
442 // guarantees r * n has a fixed-width range (MSB at position M+63 or M+64 for n with MSB at M),
443 // so the iteration count remains exactly NUM_BITS regardless of the sampled r.
444 const uint64_t r = engine->get_random_uint64() | (UINT64_C(1) << 63);
446 const uint512_t k_blinded = uint512_t(k) + r_times_n;
447
448 // For n with MSB at position M, r * n < 2^(M + 65), so k_blinded < 2^(M + 65) + n < 2^(M + 66).
449 // Iterating M+65 bits is safe because k < n means the additional bit from k cannot push k_blinded
450 // past 2^(M + 65) when n is at the lower end of [2^M, 2^(M+1)); we add one extra bit (M + 66
451 // total) to cover the worst case where n is close to 2^(M+1).
452 constexpr size_t NUM_BITS = static_cast<size_t>(uint256_t(Fr::modulus).get_msb()) + 66;
453
454 // Constant-time conditional swap of two Fq coordinates. `mask` is 0 (no swap) or all-ones (swap),
455 // derived from the secret bit via integer subtraction so no branch is emitted.
456 auto cs_fq = [](Fq& a, Fq& b, uint64_t mask) {
457 constexpr size_t NUM_LIMBS = sizeof(Fq) / sizeof(uint64_t);
458 for (size_t i = 0; i < NUM_LIMBS; ++i) {
459 uint64_t t = mask & (a.data[i] ^ b.data[i]);
460 a.data[i] ^= t;
461 b.data[i] ^= t;
462 }
463 };
464 auto cswap = [&cs_fq](element& a, element& b, uint64_t mask) {
465 cs_fq(a.x, b.x, mask);
466 cs_fq(a.y, b.y, mask);
467 cs_fq(a.z, b.z, mask);
468 };
469
470 // Montgomery ladder. Invariant after each iteration: R1 - R0 = P.
471 // Once R0 first becomes non-infinity (after the first 1-bit of k_blinded is processed), the
472 // invariant guarantees R0 + R1 and 2 * R0 do not hit the doubling/infinity special-case branches.
474 element R1(*this);
475
476 for (size_t i = NUM_BITS; i-- > 0;) {
477 const uint64_t mask = 0ULL - static_cast<uint64_t>(k_blinded.get_bit(i));
478 cswap(R0, R1, mask);
479 R1 = R0 + R1;
480 R0 = R0.dbl();
481 cswap(R0, R1, mask);
482 }
483 return R0;
484}
485
486// Warning: variable-time via the implicit affine conversion above. For
487// secret-input points use `normalize_const_time()`.
488template <class Fq, class Fr, class T> constexpr element<Fq, Fr, T> element<Fq, Fr, T>::normalize() const noexcept
489{
490 const affine_element<Fq, Fr, T> converted = *this;
491 return element(converted);
492}
493
494template <class Fq, class Fr, class T>
496{
497 return element(to_affine_const_time());
498}
499
500template <class Fq, class Fr, class T> element<Fq, Fr, T> element<Fq, Fr, T>::infinity()
501{
504 return e;
505}
506
507template <class Fq, class Fr, class T> constexpr element<Fq, Fr, T> element<Fq, Fr, T>::set_infinity() const noexcept
508{
509 element result(*this);
510 result.self_set_infinity();
511 return result;
512}
513
514template <class Fq, class Fr, class T> constexpr void element<Fq, Fr, T>::self_set_infinity() noexcept
515{
516 if constexpr (Fq::modulus.data[3] >= MODULUS_TOP_LIMB_LARGE_THRESHOLD) {
517 // We set the value of x equal to modulus to represent inifinty
518 x.data[0] = Fq::modulus.data[0];
519 x.data[1] = Fq::modulus.data[1];
520 x.data[2] = Fq::modulus.data[2];
521 x.data[3] = Fq::modulus.data[3];
522
523 // Clear y and z so the infinity representation is canonical regardless of prior state
524 y = Fq::zero();
525 z = Fq::zero();
526 } else {
527 (*this).x = Fq::zero();
528 (*this).y = Fq::zero();
529 (*this).z = Fq::zero();
530 x.self_set_msb();
531 }
532}
533
534template <class Fq, class Fr, class T> constexpr bool element<Fq, Fr, T>::is_point_at_infinity() const noexcept
535{
536 if constexpr (Fq::modulus.data[3] >= MODULUS_TOP_LIMB_LARGE_THRESHOLD) {
537 // We check if the value of x is equal to modulus to represent inifinty
538 return ((x.data[0] ^ Fq::modulus.data[0]) | (x.data[1] ^ Fq::modulus.data[1]) |
539 (x.data[2] ^ Fq::modulus.data[2]) | (x.data[3] ^ Fq::modulus.data[3])) == 0;
540 } else {
541 return (x.is_msb_set());
542 }
543}
544
545template <class Fq, class Fr, class T> constexpr bool element<Fq, Fr, T>::on_curve() const noexcept
546{
547 if (is_point_at_infinity()) {
548 return true;
549 }
550 // We specify the point at inifinity not by (0 \lambda 0), so z should not be 0
551 if (z.is_zero()) {
552 return false;
553 }
554 Fq zz = z.sqr();
555 Fq zzzz = zz.sqr();
556 Fq bz_6 = zzzz * zz * T::b;
557 if constexpr (T::has_a) {
558 bz_6 += (x * T::a) * zzzz;
559 }
560 Fq xxx = x.sqr() * x + bz_6;
561 Fq yy = y.sqr();
562 return (xxx == yy);
563}
564
565template <class Fq, class Fr, class T>
566constexpr bool element<Fq, Fr, T>::operator==(const element& other) const noexcept
567{
568 // If one of points is not on curve, we have no business comparing them.
569 if ((!on_curve()) || (!other.on_curve())) {
570 return false;
571 }
572 bool am_infinity = is_point_at_infinity();
573 bool is_infinity = other.is_point_at_infinity();
574 bool both_infinity = am_infinity && is_infinity;
575 // If just one is infinity, then they are obviously not equal.
576 if ((!both_infinity) && (am_infinity || is_infinity)) {
577 return false;
578 }
579 const Fq lhs_zz = z.sqr();
580 const Fq lhs_zzz = lhs_zz * z;
581 const Fq rhs_zz = other.z.sqr();
582 const Fq rhs_zzz = rhs_zz * other.z;
583
584 const Fq lhs_x = x * rhs_zz;
585 const Fq lhs_y = y * rhs_zzz;
586
587 const Fq rhs_x = other.x * lhs_zz;
588 const Fq rhs_y = other.y * lhs_zzz;
589 return both_infinity || ((lhs_x == rhs_x) && (lhs_y == rhs_y));
590}
591
592template <class Fq, class Fr, class T>
594{
595 if constexpr (T::can_hash_to_curve) {
596 element result = random_coordinates_on_curve(engine);
598 Fq zz = result.z.sqr();
599 Fq zzz = zz * result.z;
600 result.x *= zz;
601 result.y *= zzz;
602 return result;
603 } else {
604 Fr scalar = Fr::random_element(engine);
605 return (element{ T::one_x, T::one_y, Fq::one() } * scalar);
606 }
607}
608
609template <class Fq, class Fr, class T>
611{
612 const uint256_t converted_scalar(scalar);
613
614 if (converted_scalar == 0) {
615 return element::infinity();
616 }
617
618 element accumulator(*this);
619 const uint64_t maximum_set_bit = converted_scalar.get_msb();
620 // NOT constant-time: the loop bound leaks bit-length and the per-bit branch leaks Hamming
621 // weight. This is acceptable only for public scalars; secret scalars must go through
622 // mul_const_time.
623 for (uint64_t i = maximum_set_bit - 1; i < maximum_set_bit; --i) {
624 accumulator.self_dbl();
625 if (converted_scalar.get_bit(i)) {
626 accumulator += *this;
627 }
628 }
629 return accumulator;
630}
631
632namespace detail {
633// Represents the result of `split_into_endomorphism_scalars` — a pair of 128-bit halves
634// (k1, k2) such that `k = k1 - k2·λ (mod r)`, where λ = endomorphism scalar.
636
637// GLV endomorphism multiplication recodes each 128-bit split scalar with signed Booth windows.
638// K1 uses a standard 4-bit grid; batch_mul gives K2 a separate offset grid below.
642
643// Booth window size for the GLV endomorphism path: c=4 over each 128-bit endomorphism
644// half gives ceil(128/4) = 32 windows per half.
645inline constexpr size_t BOOTH_ENDO_WINDOW_BITS = 4;
646static_assert(BOOTH_ENDO_WINDOW_BITS + 1 <= 32);
647inline constexpr size_t BOOTH_ENDO_NUM_WINDOWS = 32;
648// Lookup table holds [1·P, 2·P, ..., 8·P] (= 2^(c-1) entries). Magnitude m ∈ [1, 8]
649// indexes the table at m-1; magnitude 0 skips the window.
650inline constexpr size_t BOOTH_ENDO_LOOKUP_SIZE = 1U << (BOOTH_ENDO_WINDOW_BITS - 1);
651// 128 bits / 64 = 2 uint64 limbs per endomorphism half.
652inline constexpr size_t BOOTH_ENDO_NUM_LIMBS_U64 = 2;
653
654// K2's offset window decomposition: a 2-bit bottom window at bit 0, then 32 × 4-bit
655// windows starting at bit 2. Pairing with K1's standard 4-bit grid at bit 0 yields
656// a union of bit positions {0, 2, 4, ..., 124, 126}, so every transition between
657// adjacent positions is exactly 2 doublings — every (2·dbl + add) pair fuses with
658// batch_affine_combined_double_add_impl, saving 1 doubling per 4-bit chunk vs. the
659// symmetric layout. With K2 < 2^127 (proven via the lattice analysis on BN254/Grumpkin
660// — see Fr/Fq.hpp endomorphism comments), the top 4-bit window covers only bit 126,
661// so its magnitude is in {0, 1, 2} and is empty ~50% of the time.
662inline constexpr size_t BOOTH_ENDO_K2_LOW_WINDOW_BITS = 2;
663static_assert(BOOTH_ENDO_K2_LOW_WINDOW_BITS + 1 <= 32);
664inline constexpr size_t BOOTH_ENDO_K2_NUM_WINDOWS = BOOTH_ENDO_NUM_WINDOWS + 1; // 33
665
666// batch_two_round_fold evaluates the four scalar-multiplication terms below on one shared doubling
667// chain, interleaving their Booth digit windows on offset grids (term j on grid offset j) so each
668// position 1..127 carries exactly one window (position 0 shares all four bottom windows) and each
669// step is a single fused double-and-add.
670// See ipa/ELEMENT_IMPL_FOLD.md for the derivation and cost model.
671//
672// Term → (scalar, base, grid offset):
673// 0: K1 of u1·u2 on P0 (offset 0), 1: u1 on P1 (offset 1),
674// 2: K2 of u1·u2 on φ(P0) (offset 2), 3: u2 on P2 (offset 3).
675//
676// The interleaved scan is compiled into a schedule of these ops, one per accumulator step, replayed
677// over every point in the batch (see ELEMENT_IMPL_FOLD.md, "Phase 1 — schedule generation"):
678// DBL — Horner doubling for a position whose window is zero: accum ← 2·accum.
679// SEED — load a base into a still-empty accumulator: accum ← ±digit·base[term].
680// COMBINED — fused double-and-add owning this position's doubling: accum ← 2·accum + ±digit·base[term].
681// ADD — plain add with no doubling (the extra position-0 windows): accum ← accum + ±digit·base[term].
682enum class FoldOpKind : uint8_t { DBL, SEED, COMBINED, ADD };
683struct FoldOp {
685 uint8_t term;
686 uint32_t digit;
687 bool safe;
688};
689
690} // namespace detail
691
692template <class Fq, class Fr, class T>
694{
695 if (is_point_at_infinity()) {
696 return element::infinity();
697 }
698 const Fr converted_scalar = scalar.from_montgomery_form();
699 if (converted_scalar.is_zero()) {
700 return element::infinity();
701 }
702
703 // Booth lookup: [1·P, 2·P, ..., 8·P]. Magnitude m ∈ [1, 2^(c-1)] indexes at m-1;
704 // magnitude 0 (no contribution) just skips the add.
705 constexpr size_t LOOKUP_SIZE = detail::BOOTH_ENDO_LOOKUP_SIZE;
707 lookup_table[0] = element(*this);
708 for (size_t i = 1; i < LOOKUP_SIZE; ++i) {
709 lookup_table[i] = lookup_table[i - 1] + *this;
710 }
711
712 const detail::EndoScalars endo_scalars = Fr::split_into_endomorphism_scalars(converted_scalar);
713 constexpr auto slice_params = detail::make_booth_slice_params<detail::BOOTH_ENDO_NUM_WINDOWS,
716 const uint64_t* k1 = endo_scalars.first.data();
717 const uint64_t* k2 = endo_scalars.second.data();
718
719 element accumulator{ T::one_x, T::one_y, Fq::one() };
720 accumulator.self_set_infinity();
721 const Fq beta = Fq::cube_root_of_unity();
722
723 // Process windows high-to-low; within each window add k1's digit, then k2's
724 // (with x·=β and the GLV `k = k1 - k2·λ` sign flip on y), then double 4× to
725 // shift the next window's contribution into place. No skew correction needed —
726 // the signed-Booth digits already span the full c-bit range so an even scalar
727 // doesn't require a post-pass.
728 for (size_t w = detail::BOOTH_ENDO_NUM_WINDOWS; w-- > 0;) {
729 for (size_t h = 0; h < 2; ++h) {
730 const uint64_t* s = (h == 0) ? k1 : k2;
731 const uint32_t digit = detail::booth_packed_digit(s, slice_params[w], detail::BOOTH_ENDO_WINDOW_BITS);
732 const uint32_t magnitude = digit & 0x7FFFFFFFU;
733 if (magnitude == 0) {
734 continue;
735 }
736 const bool sign = (digit >> 31) != 0;
737 element to_add = lookup_table[magnitude - 1];
738 to_add.y.self_conditional_negate(sign ^ (h == 1));
739 if (h == 1) {
740 to_add.x *= beta;
741 }
742 accumulator += to_add;
743 }
744 if (w != 0) {
745 for (size_t d = 0; d < detail::BOOTH_ENDO_WINDOW_BITS; ++d) {
747 }
748 }
749 }
750 return accumulator;
751}
752
753template <class Fq, class Fr, class T>
755 std::span<const Fr> scalars) noexcept
756{
757 BB_BENCH_NAME("Element::straus_msm");
758 const size_t n = std::min(points.size(), scalars.size());
759 if (n == 0) {
760 return element::infinity();
761 }
762
763 if constexpr (T::USE_ENDOMORPHISM) {
764 // Endomorphism-Booth path: build a small lookup table per active point and walk
765 // the split-scalar windows high-to-low. The signed-Booth digits span the full
766 // c-bit range, so no post-pass skew correction is needed.
767 constexpr size_t LOOKUP_SIZE = detail::BOOTH_ENDO_LOOKUP_SIZE;
768 constexpr size_t NUM_WINDOWS = detail::BOOTH_ENDO_NUM_WINDOWS;
769 constexpr size_t WINDOW_BITS = detail::BOOTH_ENDO_WINDOW_BITS;
770 constexpr auto slice_params = detail::make_booth_slice_params<detail::BOOTH_ENDO_NUM_WINDOWS,
773
774 struct ActiveScalar {
778 };
779
781 active.reserve(n);
782 for (size_t i = 0; i < n; ++i) {
783 if (points[i].is_point_at_infinity()) {
784 continue;
785 }
786 const Fr converted = scalars[i].from_montgomery_form();
787 if (converted.is_zero()) {
788 continue;
789 }
790 ActiveScalar e;
791 const element pt(points[i]);
792 e.lookup[0] = pt;
793 for (size_t k = 1; k < LOOKUP_SIZE; ++k) {
794 e.lookup[k] = e.lookup[k - 1] + pt;
795 }
797 e.k1 = endo.first;
798 e.k2 = endo.second;
799 active.push_back(std::move(e));
800 }
801 if (active.empty()) {
802 return element::infinity();
803 }
804
805 element accumulator{ T::one_x, T::one_y, Fq::one() };
806 accumulator.self_set_infinity();
807 const Fq beta = Fq::cube_root_of_unity();
808
809 for (size_t w = NUM_WINDOWS; w-- > 0;) {
810 for (size_t h = 0; h < 2; ++h) {
811 for (auto& a : active) {
812 const uint64_t* s = (h == 0) ? a.k1.data() : a.k2.data();
813 const uint32_t digit = detail::booth_packed_digit(s, slice_params[w], WINDOW_BITS);
814 const uint32_t magnitude = digit & 0x7FFFFFFFU;
815 if (magnitude == 0) {
816 continue;
817 }
818 const bool sign = (digit >> 31) != 0;
819 element to_add = a.lookup[magnitude - 1];
820 to_add.y.self_conditional_negate(sign ^ (h == 1));
821 if (h == 1) {
822 to_add.x *= beta;
823 }
824 accumulator += to_add;
825 }
826 }
827 if (w != 0) {
828 for (size_t d = 0; d < WINDOW_BITS; ++d) {
830 }
831 }
832 }
833 return accumulator;
834 } else {
835 // No endomorphism: bit-by-bit simultaneous double-and-add over the active subset.
837 std::vector<uint256_t> active_scalars;
838 active_points.reserve(n);
839 active_scalars.reserve(n);
840 uint64_t max_set_bit = 0;
841 for (size_t i = 0; i < n; ++i) {
842 if (points[i].is_point_at_infinity()) {
843 continue;
844 }
845 uint256_t s(scalars[i]);
846 if (s == 0) {
847 continue;
848 }
849 max_set_bit = std::max(max_set_bit, s.get_msb());
850 active_points.push_back(points[i]);
851 active_scalars.push_back(s);
852 }
853 if (active_points.empty()) {
854 return element::infinity();
855 }
856
858 for (uint64_t bit = max_set_bit + 1; bit-- > 0;) {
859 accumulator.self_dbl();
860 for (size_t i = 0; i < active_points.size(); ++i) {
861 if (active_scalars[i].get_bit(bit)) {
862 accumulator += active_points[i];
863 }
864 }
865 }
866 return accumulator;
867 }
868}
869
884template <typename AffineElement, typename Fq>
885__attribute__((always_inline)) inline void batch_affine_add_impl(const AffineElement* lhs,
886 AffineElement* rhs,
887 const size_t num_pairs,
888 Fq* scratch_space) noexcept
889{
891
892 // Forward pass: prepare batch inversion
893 for (size_t i = 0; i < num_pairs; ++i) {
894 scratch_space[i] = lhs[i].x + rhs[i].x;
895 rhs[i].x -= lhs[i].x;
896 rhs[i].y -= lhs[i].y;
899 }
900
902 throw_or_abort("attempted to invert zero in batch_affine_add_impl");
903 }
905
906 // Backward pass: compute additions
907 for (size_t i = num_pairs - 1; i < num_pairs; --i) {
908 // lambda = (y2 - y1) / (x2 - x1)
911 rhs[i].x = rhs[i].y.sqr();
912 rhs[i].x -= scratch_space[i]; // x3 = lambda^2 - (x1 + x2)
913
914 // y3 = lambda * (x1 - x3) - y1
915 Fq temp = lhs[i].x - rhs[i].x;
916 temp *= rhs[i].y;
917 rhs[i].y = temp - lhs[i].y;
918 }
919}
920
930template <typename AffineElement, typename Fq>
931__attribute__((always_inline)) inline void batch_affine_add_interleaved(AffineElement* points,
932 const size_t num_points,
933 Fq* scratch_space) noexcept
934{
936
937 // Forward pass: accumulate (x2 - x1) products for batch inversion
938 for (size_t i = 0; i < num_points; i += 2) {
939 scratch_space[i >> 1] = points[i].x + points[i + 1].x; // x1 + x2 (saved for later)
940 points[i + 1].x -= points[i].x; // x2 - x1
941 points[i + 1].y -= points[i].y; // y2 - y1
942 points[i + 1].y *= batch_inversion_accumulator;
943 batch_inversion_accumulator *= points[i + 1].x;
944 }
945
947 throw_or_abort("attempted to invert zero in batch_affine_add_interleaved");
948 }
950
951 // Backward pass: complete inversions and compute additions
952 for (size_t i = num_points - 2; i < num_points; i -= 2) {
953 // lambda = (y2 - y1) / (x2 - x1)
954 points[i + 1].y *= batch_inversion_accumulator;
955 batch_inversion_accumulator *= points[i + 1].x;
956 points[i + 1].x = points[i + 1].y.sqr();
957 // x3 = lambda^2 - (x1 + x2)
958 points[(i + num_points) >> 1].x = points[i + 1].x - scratch_space[i >> 1];
959
960 if (i >= 2) {
961 __builtin_prefetch(points + i - 2);
962 __builtin_prefetch(points + i - 1);
963 __builtin_prefetch(points + ((i + num_points - 2) >> 1));
964 __builtin_prefetch(scratch_space + ((i - 2) >> 1));
965 }
966
967 // y3 = lambda * (x1 - x3) - y1
968 points[i].x -= points[(i + num_points) >> 1].x;
969 points[i].x *= points[i + 1].y;
970 points[(i + num_points) >> 1].y = points[i].x - points[i].y;
971 }
972}
973
988template <typename AffineElement, typename Fq, typename T>
989__attribute__((always_inline)) inline void batch_affine_double_impl(AffineElement* points,
990 const size_t num_points,
991 Fq* scratch_space) noexcept
992{
994
995 // Forward pass: prepare batch inversion
996 for (size_t i = 0; i < num_points; ++i) {
997 scratch_space[i] = points[i].x.sqr();
998 if constexpr (T::has_a) {
999 scratch_space[i] += T::a; // adjust slope in numerator
1000 }
1001 scratch_space[i] = scratch_space[i] + scratch_space[i] + scratch_space[i];
1002 scratch_space[i] *= batch_inversion_accumulator;
1003 batch_inversion_accumulator *= (points[i].y + points[i].y);
1004 }
1005
1007 throw_or_abort("attempted to invert zero in batch_affine_double_impl");
1008 }
1010
1011 // Backward pass: compute doublings
1013 for (size_t i_plus_1 = num_points; i_plus_1 > 0; --i_plus_1) {
1014 size_t i = i_plus_1 - 1;
1015
1016 scratch_space[i] *= batch_inversion_accumulator;
1017 batch_inversion_accumulator *= (points[i].y + points[i].y);
1018
1019 temp_x = points[i].x;
1020 points[i].x = scratch_space[i].sqr() - (points[i].x + points[i].x);
1021 points[i].y = scratch_space[i] * (temp_x - points[i].x) - points[i].y;
1022 }
1023}
1024
1053template <typename AffineElement, typename Fq>
1054__attribute__((always_inline)) inline void batch_affine_combined_double_add_impl(const AffineElement* to_add,
1055 AffineElement* accumulator,
1056 const size_t num_pairs,
1059 Fq* scratch_c) noexcept
1060{
1061 // === Phase 1: batch-invert (x2 − x1), produce λ1 and (x3 − x1). ===
1063 for (size_t i = 0; i < num_pairs; ++i) {
1064 // (x1 + x2): retained for x3 = λ1² − (x1 + x2) in the backward pass.
1065 scratch_a[i] = accumulator[i].x + to_add[i].x;
1066 // (x2 − x1): feeds Montgomery's batch inversion product.
1067 scratch_b[i] = to_add[i].x - accumulator[i].x;
1068 // (y2 − y1) × Π_{j<i}(x2_j − x1_j): partial numerator for λ1.
1069 scratch_c[i] = to_add[i].y - accumulator[i].y;
1070 scratch_c[i] *= batch_inv_acc;
1072 }
1074 throw_or_abort("attempted to invert zero in batch_affine_combined_double_add_impl phase 1");
1075 }
1077 for (size_t k = num_pairs; k-- > 0;) {
1078 // λ1 = (y2 − y1) / (x2 − x1).
1079 scratch_c[k] *= batch_inv_acc;
1081 // x3 = λ1² − (x1 + x2); overwrite scratch_b with (x3 − x1) for phase 2.
1082 Fq x3 = scratch_c[k].sqr();
1083 x3 -= scratch_a[k];
1084 scratch_b[k] = x3 - accumulator[k].x;
1085 // scratch_c[k] now holds λ1, retained for phase 2.
1086 }
1087
1088 // === Phase 2: batch-invert (x3 − x1), produce λ2, write x4 and y4. ===
1090 for (size_t i = 0; i < num_pairs; ++i) {
1091 // 2·y1 × Π_{j<i}(x3_j − x1_j): partial numerator for 2·y1 / (x3 − x1).
1092 scratch_a[i] = accumulator[i].y + accumulator[i].y;
1095 }
1096 if (batch_inv_acc == Fq::zero()) {
1097 throw_or_abort("attempted to invert zero in batch_affine_combined_double_add_impl phase 2");
1098 }
1099 batch_inv_acc = batch_inv_acc.invert();
1100 for (size_t k = num_pairs; k-- > 0;) {
1101 // 2·y1 / (x3 − x1).
1104 // λ2 = −λ1 − 2·y1 / (x3 − x1).
1105 Fq lambda2 = -scratch_c[k];
1106 lambda2 -= scratch_a[k];
1107 // x4 = λ2² − x1 − x3, where x3 = (x3 − x1) + x1 = scratch_b[k] + accumulator[k].x.
1108 Fq x4 = lambda2.sqr();
1109 x4 -= accumulator[k].x;
1110 x4 -= (scratch_b[k] + accumulator[k].x);
1111 // y4 = λ2 · (x1 − x4) − y1.
1112 Fq y4 = accumulator[k].x - x4;
1113 y4 *= lambda2;
1114 y4 -= accumulator[k].y;
1115 accumulator[k].x = x4;
1116 accumulator[k].y = y4;
1117 }
1118}
1119
1145template <typename AffineElement, typename Fq>
1146__attribute__((always_inline)) inline void batch_affine_add_indexed_impl(AffineElement* buckets,
1148 const size_t num_pairs,
1149 Fq* scratch_space) noexcept
1150{
1151 if (num_pairs == 0) {
1152 return;
1153 }
1154
1155 // Sparse indexed bucket accesses are hard for hardware prefetchers. A small fixed
1156 // lookahead overlaps the next bucket load with the current pair's field arithmetic.
1157 constexpr size_t PREFETCH_AHEAD = 4;
1158
1160
1161 // Forward pass: prepare batch inversion via the standard Montgomery trick.
1162 // Treats the dst slot as `rhs` (mutated in place) and src slot as `lhs` (read-only).
1163 for (size_t i = 0; i < num_pairs; ++i) {
1164 if (i + PREFETCH_AHEAD < num_pairs) {
1165 __builtin_prefetch(buckets + pairs[i + PREFETCH_AHEAD].first, 1, 3); // dst: write
1166 __builtin_prefetch(buckets + pairs[i + PREFETCH_AHEAD].second, 0, 3); // src: read
1167 }
1168 AffineElement& dst = buckets[pairs[i].first];
1169 const AffineElement& src = buckets[pairs[i].second];
1170 scratch_space[i] = src.x + dst.x; // x1 + x2 (saved for backward pass)
1171 dst.x -= src.x; // x2 - x1 (denominator)
1172 dst.y -= src.y; // y2 - y1 (numerator before scaling)
1175 }
1176
1178 throw_or_abort("attempted to invert zero in batch_affine_add_indexed_impl");
1179 }
1181
1182 // Backward pass: complete each pair's slope and write x3, y3 into the dst slot.
1183 for (size_t j = num_pairs; j > 0; --j) {
1184 const size_t i = j - 1;
1185 if (i >= PREFETCH_AHEAD) {
1186 __builtin_prefetch(buckets + pairs[i - PREFETCH_AHEAD].first, 1, 3); // dst: write
1187 __builtin_prefetch(buckets + pairs[i - PREFETCH_AHEAD].second, 0, 3); // src: read
1188 __builtin_prefetch(scratch_space + (i - PREFETCH_AHEAD), 0, 3);
1189 }
1190 AffineElement& dst = buckets[pairs[i].first];
1191 const AffineElement& src = buckets[pairs[i].second];
1192
1193 // lambda = (y2 - y1) / (x2 - x1)
1196 dst.x = dst.y.sqr();
1197 dst.x -= scratch_space[i]; // x3 = lambda^2 - (x1 + x2)
1198
1199 // y3 = lambda * (x1 - x3) - y1
1200 Fq temp = src.x - dst.x;
1201 temp *= dst.y;
1202 dst.y = temp - src.y;
1203 }
1204}
1205
1222template <typename AffineElement, typename Fq>
1223__attribute__((always_inline)) inline void batch_affine_double_indexed_impl(AffineElement* buckets,
1224 const uint32_t* indices,
1225 const size_t num_points,
1226 Fq* scratch_space) noexcept
1227{
1228 if (num_points == 0) {
1229 return;
1230 }
1231
1232 constexpr size_t PREFETCH_AHEAD = 4;
1233
1235
1236 // Forward pass.
1237 for (size_t i = 0; i < num_points; ++i) {
1238 if (i + PREFETCH_AHEAD < num_points) {
1239 __builtin_prefetch(buckets + indices[i + PREFETCH_AHEAD], 1, 3);
1240 }
1241 AffineElement& p = buckets[indices[i]];
1242 scratch_space[i] = p.x.sqr();
1243 scratch_space[i] = scratch_space[i] + scratch_space[i] + scratch_space[i]; // 3 x^2
1244 scratch_space[i] *= batch_inversion_accumulator;
1245 batch_inversion_accumulator *= (p.y + p.y);
1246 }
1247
1249 throw_or_abort("attempted to invert zero in batch_affine_double_indexed_impl");
1250 }
1252
1253 // Backward pass.
1254 Fq temp_x;
1255 for (size_t j = num_points; j > 0; --j) {
1256 const size_t i = j - 1;
1257 if (i >= PREFETCH_AHEAD) {
1258 __builtin_prefetch(buckets + indices[i - PREFETCH_AHEAD], 1, 3);
1259 __builtin_prefetch(scratch_space + (i - PREFETCH_AHEAD), 0, 3);
1260 }
1261 AffineElement& p = buckets[indices[i]];
1262
1263 scratch_space[i] *= batch_inversion_accumulator;
1264 batch_inversion_accumulator *= (p.y + p.y);
1265
1266 temp_x = p.x;
1267 p.x = scratch_space[i].sqr() - (p.x + p.x);
1268 p.y = scratch_space[i] * (temp_x - p.x) - p.y;
1269 }
1270}
1271
1283template <class Fq, class Fr, class T>
1285 const std::span<affine_element<Fq, Fr, T>>& second_group,
1286 const std::span<affine_element<Fq, Fr, T>>& results) noexcept
1287{
1289 const size_t num_points = first_group.size();
1290 BB_ASSERT_EQ(second_group.size(), first_group.size());
1291
1292 // Space for temporary values
1293 std::vector<Fq> scratch_space(num_points);
1294
1296 num_points, [&](size_t i) { results[i] = first_group[i]; }, thread_heuristics::FF_COPY_COST * 2);
1297
1298 // Perform batch affine addition: (lhs[i], rhs[i]) -> rhs[i]
1300 num_points,
1301 [&](size_t start, size_t end, BB_UNUSED size_t chunk_index) {
1302 batch_affine_add_impl<affine_element, Fq>(
1303 &second_group[start], &results[start], end - start, &scratch_space[start]);
1304 },
1306}
1307
1318template <class Fq, class Fr, class T>
1320 const std::span<const affine_element<Fq, Fr, T>>& points, const Fr& scalar) noexcept
1321{
1322 BB_BENCH();
1324 const size_t num_points = points.size();
1325 if (num_points == 0) {
1326 return {};
1327 }
1328
1329 // Scratch for batch inversions.
1330 // [0 .. N) — single-inversion buffer for batch_affine_add_impl / batch_affine_double_impl
1331 // [0 .. 3N) — batch_affine_combined_double_add_impl needs three N-sized scratches
1332 // (one each for (x1+x2), (x2−x1)/(x3−x1), and the λ1/numerator product).
1333 std::vector<Fq> scratch_space(3 * num_points);
1334 Fq* const scratch_a = &scratch_space[0];
1335 Fq* const scratch_b = &scratch_space[num_points];
1336 Fq* const scratch_c = &scratch_space[2 * num_points];
1337
1338 // p−1 still produces an "infinity" path at the end of the sum under any
1339 // signed-digit encoding (the partial sums hit a doubling edge), so short-circuit
1340 // it here to keep edge-case handling out of the hot loop.
1341 if (scalar == -Fr::one()) {
1343 parallel_for_heuristic(num_points, [&](size_t i) { results[i] = -points[i]; }, thread_heuristics::FF_COPY_COST);
1344 return results;
1345 }
1346 const Fr converted_scalar = scalar.from_montgomery_form();
1347 if (converted_scalar.is_zero()) {
1349 result.self_set_infinity();
1352 return results;
1353 }
1354
1355 constexpr size_t LOOKUP_SIZE = detail::BOOTH_ENDO_LOOKUP_SIZE;
1356 constexpr size_t NUM_WINDOWS = detail::BOOTH_ENDO_NUM_WINDOWS;
1357 constexpr size_t K2_NUM_WINDOWS = detail::BOOTH_ENDO_K2_NUM_WINDOWS;
1358 constexpr size_t WINDOW_BITS = detail::BOOTH_ENDO_WINDOW_BITS;
1359 constexpr size_t K2_LOW_WINDOW_BITS = detail::BOOTH_ENDO_K2_LOW_WINDOW_BITS;
1360 constexpr auto slice_params = detail::make_booth_slice_params<detail::BOOTH_ENDO_NUM_WINDOWS,
1363 constexpr auto k2_slice_params = detail::make_offset_booth_slice_params<detail::BOOTH_ENDO_K2_NUM_WINDOWS,
1367
1368 // K1 keeps the standard 4-bit Booth grid at bit positions {0, 4, ..., 124}.
1369 // K2 uses the offset grid: a 2-bit window at bit 0, then 4-bit windows at
1370 // {2, 6, ..., 126}. The union of K1/K2 bit positions is {0, 2, 4, ..., 126},
1371 // so the main loop visits each position once with 2 doublings between
1372 // adjacent positions — every (2·dbl + add) pair fuses with combined_chunked.
1373 //
1374 // A scalar already below 2^127 (e.g. an unreduced 127-bit transcript challenge, as in the
1375 // IPA SRS fold) takes (k1, k2) = (scalar, 0) directly. An all-zero K2 digit sequence makes the
1376 // main loop run the single-sequence schedule (every K2 position is two plain doublings instead
1377 // of a doubling plus a fused add — about half the add passes), and the IPA fold's cost model
1378 // relies on that. The split would currently produce (scalar, 0) here anyway; bypassing it
1379 // makes the guarantee explicit rather than a property of the split's rounding.
1380 const bool is_short_scalar =
1381 ((converted_scalar.data[2] | converted_scalar.data[3]) == 0) && ((converted_scalar.data[1] >> 63) == 0);
1382 const detail::EndoScalars endo_scalars =
1383 is_short_scalar ? detail::EndoScalars{ { converted_scalar.data[0], converted_scalar.data[1] }, { 0ULL, 0ULL } }
1384 : Fr::split_into_endomorphism_scalars(converted_scalar);
1385 const uint64_t* k1 = endo_scalars.first.data();
1386 const uint64_t* k2 = endo_scalars.second.data();
1387 BB_ASSERT((k2[1] >> 63) == 0, "GLV K2 split must fit below 2^127 for the offset Booth window schedule");
1388
1391 for (size_t w = 0; w < NUM_WINDOWS; ++w) {
1392 k1_digits[w] = detail::booth_packed_digit(k1, slice_params[w], WINDOW_BITS);
1393 }
1394 k2_digits[0] = detail::booth_packed_digit(k2, k2_slice_params[0], K2_LOW_WINDOW_BITS);
1395 for (size_t w = 1; w < K2_NUM_WINDOWS; ++w) {
1396 k2_digits[w] = detail::booth_packed_digit(k2, k2_slice_params[w], WINDOW_BITS);
1397 }
1398
1399 // Precompute, for every chunked-combined/add call below, whether
1400 // batch_affine_combined_double_add_impl's edge conditions could fire.
1401 // Both edges (x(2·accum) == x(to_add), and 2·accum + to_add == O) are a function
1402 // only of the (k1, k2) Booth digit sequence and the (P, φP) basis, so we simulate the
1403 // accumulator's coefficients in that basis as int64s and set one mask bit per call site.
1404 //
1405 // Layout:
1406 // bits 0..61: main loop, step s ↔ pos 124 - 2·s.
1407 // bit 62: pos-0 combined_chunked.
1408 // bit 63: pos-0 trailing add_chunked when both pos-0 digits are non-zero.
1409 // The pos-0 add_chunked on the !initialised seed path is omitted: there the accumulator
1410 // is d0·P and to_add is d1·φP with both digits
1411 // non-zero, so the two affine points cannot share an x-coordinate (P and φP are
1412 // independent generators of the prime-order subgroup), so its check is always false.
1413 auto compute_safe_mask = [&]() -> uint64_t {
1414 uint64_t mask = 0;
1415 int64_t a = 0;
1416 int64_t b = 0;
1417 bool initialised = false;
1418
1419 const auto signed_digit = [](uint32_t packed) -> int64_t {
1420 const int64_t mag = static_cast<int64_t>(packed & 0x7FFFFFFFU);
1421 return ((packed >> 31) != 0) ? -mag : mag;
1422 };
1423
1424 // Edge predicate for combined_chunked (does dbl-then-add internally).
1425 // Pre-call accum = (a)P + (b)φP, internal accumulator post-double = (2a)P + (2b)φP.
1426 // Edge 1: x((2a)P + (2b)φP) == x(d·BASE) ⇔ one of (a, b) is 0 AND 2·other = ±d.
1427 // Edge 2: (4a + d)P + 4b·φP == O ⇔ b=0 AND 4a = -d (K1 case)
1428 // or a=0 AND 4b = -d (K2 case).
1429 const auto edge_for_combined = [&](int64_t d, bool is_k1) -> bool {
1430 if (is_k1) {
1431 if (b != 0) {
1432 return false;
1433 }
1434 if ((d % 2 == 0) && (2 * a == d || 2 * a == -d)) {
1435 return true;
1436 }
1437 return (d % 4 == 0) && (4 * a == -d);
1438 }
1439 if (a != 0) {
1440 return false;
1441 }
1442 if ((d % 2 == 0) && (2 * b == d || 2 * b == -d)) {
1443 return true;
1444 }
1445 return (d % 4 == 0) && (4 * b == -d);
1446 };
1447
1448 // Edge predicate for plain add_chunked: accum = (a)P + (b)φP, to_add = d·BASE.
1449 // x(accum) == x(to_add) ⇔ accum = ±to_add ⇔ the orthogonal basis coord is 0
1450 // AND |the aligned coord| = |d|.
1451 const auto edge_for_add = [&](int64_t d, bool is_k1) -> bool {
1452 if (is_k1) {
1453 return (b == 0) && (a == d || a == -d);
1454 }
1455 return (a == 0) && (b == d || b == -d);
1456 };
1457
1458 // === Pos 126: K2 window 32 (top, 4-bit but value ≤ 2 since K2 < 2^127). ===
1459 {
1460 const uint32_t d126 = k2_digits[K2_NUM_WINDOWS - 1];
1461 if ((d126 & 0x7FFFFFFFU) != 0) {
1462 b = signed_digit(d126);
1463 initialised = true;
1464 }
1465 }
1466
1467 // === Positions 124, 122, ..., 2 (62 iterations). ===
1468 for (size_t step = 0; step < 62; ++step) {
1469 // Once either basis coordinate is non-zero and outside ±4, no later edge predicate can fire:
1470 // same-basis additions are too small to collide, and opposite-basis additions see a non-zero
1471 // orthogonal coordinate. Stop before the coefficient simulation can overflow int64_t.
1472 if (((a != 0) && (std::abs(a) > 4)) || ((b != 0) && (std::abs(b) > 4))) {
1473 break;
1474 }
1475 const size_t pos = 124 - 2 * step;
1476 const bool is_k1 = (pos % 4 == 0);
1477 const uint32_t digit = is_k1 ? k1_digits[pos / 4] : k2_digits[(pos + 2) / 4];
1478 const uint32_t m = digit & 0x7FFFFFFFU;
1479 const int64_t d = signed_digit(digit);
1480
1481 if (!initialised) {
1482 if (m != 0) {
1483 if (is_k1) {
1484 a = d;
1485 } else {
1486 b = d;
1487 }
1488 initialised = true;
1489 }
1490 continue;
1491 }
1492 if (m == 0) {
1493 a *= 4;
1494 b *= 4;
1495 continue;
1496 }
1497 if (edge_for_combined(d, is_k1)) {
1498 mask |= (uint64_t{ 1 } << step);
1499 }
1500 a *= 4;
1501 b *= 4;
1502 if (is_k1) {
1503 a += d;
1504 } else {
1505 b += d;
1506 }
1507 }
1508
1509 // === Pos 0: K1 window 0 (4-bit) + K2 window 0 (2-bit). ===
1510 // Mirrors the runtime pos-0 branch structure below.
1511 {
1512 const uint32_t d0 = k1_digits[0];
1513 const uint32_t d1 = k2_digits[0];
1514 const uint32_t m0 = d0 & 0x7FFFFFFFU;
1515 const uint32_t m1 = d1 & 0x7FFFFFFFU;
1516 const int64_t s0 = signed_digit(d0);
1517 const int64_t s1 = signed_digit(d1);
1518
1519 if (!initialised) {
1520 if (m0 != 0) {
1521 a = s0;
1522 initialised = true;
1523 if (m1 != 0) { // accum=d0·P, to_add=d1·φP. Linear independence ⇒ no x-coordinate collision.
1524 b += s1;
1525 }
1526 } else if (m1 != 0) {
1527 b = s1;
1528 initialised = true;
1529 }
1530 } else if (m0 == 0 && m1 == 0) {
1531 a *= 4;
1532 b *= 4;
1533 } else {
1534 const bool fuse_with_h1 = (m0 == 0);
1535 const int64_t fused_d = fuse_with_h1 ? s1 : s0;
1536 if (edge_for_combined(fused_d, /*is_k1=*/!fuse_with_h1)) {
1537 mask |= (uint64_t{ 1 } << 62);
1538 }
1539 a *= 4;
1540 b *= 4;
1541 if (fuse_with_h1) {
1542 b += fused_d;
1543 } else {
1544 a += fused_d;
1545 }
1546 if (m0 != 0 && m1 != 0) {
1547 // Combined consumed d0 (the K1 contribution); the trailing add_chunked
1548 // stacks d1 (the K2 contribution) on top.
1549 if (edge_for_add(s1, /*is_k1=*/false)) {
1550 mask |= (uint64_t{ 1 } << 63);
1551 }
1552 }
1553 b += s1;
1554 }
1555 }
1556
1557 return mask;
1558 };
1559 const uint64_t safe_mask = compute_safe_mask();
1560
1562 std::array<std::vector<affine_element>, LOOKUP_SIZE> lookup_table;
1563 for (auto& table : lookup_table) {
1564 table.resize(num_points);
1565 }
1566 std::vector<affine_element> temp_point_vector(num_points);
1567
1568 auto execute_range = [&](size_t start, size_t end) {
1569 BB_BENCH_TRACY_NAME("batch_mul_with_endo/execute_range");
1570 const auto add_chunked = [&](const affine_element* lhs, affine_element* rhs) {
1571 batch_affine_add_impl<affine_element, Fq>(&lhs[start], &rhs[start], end - start, &scratch_a[start]);
1572 };
1573 const auto add_safe_chunked = [&](const affine_element* lhs, affine_element* rhs) {
1574 for (size_t i = start; i < end; ++i) {
1575 element acc(rhs[i]);
1576 acc += lhs[i];
1577 rhs[i] = affine_element(acc);
1578 }
1579 };
1580 const auto double_chunked = [&](affine_element* lhs) {
1581 batch_affine_double_impl<affine_element, Fq, T>(&lhs[start], end - start, &scratch_a[start]);
1582 };
1583 // Fused 2·accum + to_add — saves 1 mul + 1 sqr per point vs. (double + add).
1584 const auto combined_chunked = [&](const affine_element* to_add, affine_element* accum) {
1585 batch_affine_combined_double_add_impl<affine_element, Fq>(
1586 &to_add[start], &accum[start], end - start, &scratch_a[start], &scratch_b[start], &scratch_c[start]);
1587 };
1588 const auto combined_safe_chunked = [&](const affine_element* to_add, affine_element* accum) {
1589 for (size_t i = start; i < end; ++i) {
1590 element acc(accum[i]);
1591 acc.self_dbl();
1592 acc += to_add[i];
1593 accum[i] = affine_element(acc);
1594 }
1595 };
1596 // Build lookup table [1·P, 2·P, 3·P, ..., 8·P]. Substitute affine::one()
1597 // for points-at-infinity to keep the batch arithmetic edge-case-free; the
1598 // final pass below sets work_elements[i] to infinity for those slots.
1599 for (size_t i = start; i < end; ++i) {
1600 if (points[i].is_point_at_infinity()) {
1601 lookup_table[0][i] = affine_element::one();
1602 temp_point_vector[i] = affine_element::one();
1603 } else {
1604 lookup_table[0][i] = points[i];
1605 temp_point_vector[i] = points[i];
1606 }
1607 }
1608 // lookup[1] = 2·P via batch double (lookup[1] = lookup[0] + lookup[0] would
1609 // trip the equal-x guard in batch_affine_add_impl).
1610 for (size_t i = start; i < end; ++i) {
1611 lookup_table[1][i] = lookup_table[0][i];
1612 }
1613 double_chunked(&lookup_table[1][0]);
1614 // lookup[j] = lookup[j-1] + P for j ≥ 2 (lookup[j-1] = j·P, never equals P).
1615 for (size_t j = 2; j < LOOKUP_SIZE; ++j) {
1616 for (size_t i = start; i < end; ++i) {
1617 lookup_table[j][i] = lookup_table[j - 1][i];
1618 }
1619 add_chunked(&temp_point_vector[0], &lookup_table[j][0]);
1620 }
1621
1622 constexpr Fq beta = Fq::cube_root_of_unity();
1623
1624 // Materialise lookup[mag-1] (possibly negated, possibly β-twisted) for one
1625 // (window, half). Skips the points-at-infinity slots via the lookup entry
1626 // already being affine::one(); those slots get zeroed at the end.
1627 auto fill_to_add = [&](uint32_t digit, bool half_idx, affine_element* dst) {
1628 const uint32_t magnitude = digit & 0x7FFFFFFFU;
1629 const bool sign = (digit >> 31) != 0;
1630 const bool flip_y = sign ^ half_idx;
1631 for (size_t i = start; i < end; ++i) {
1632 affine_element pt = lookup_table[magnitude - 1][i];
1633 pt.y.self_conditional_negate(flip_y);
1634 if (half_idx) {
1635 pt.x *= beta;
1636 }
1637 dst[i] = pt;
1638 }
1639 };
1640
1641 // Walk K1/K2 bit positions {126, 124, 122, ..., 4, 2, 0} top-to-bottom.
1642 // Mapping: pos % 4 == 0 → K1 window pos/4; pos % 4 == 2 → K2 window (pos+2)/4.
1643 // Pos 126 hosts K2 window 32 (top, 4-bit but value ≤ 2 since K2 < 2^127, so
1644 // empty ~50% of the time). Pos 0 hosts both K1 window 0 and K2 window 0 (2-bit).
1645 //
1646 // Once initialised, every transition between adjacent positions is "2 dbl + add",
1647 // fused as "1 dbl + 1 combined_chunked". Booth digit 0 turns the add into a no-op,
1648 // so a zero digit just becomes 2 unfused doublings to shift past.
1649 bool initialised = false;
1650 const auto update_initialised_from_work = [&]() { initialised = !work_elements[start].is_point_at_infinity(); };
1651 auto seed_or_skip = [&](uint32_t digit, bool half_idx) {
1652 // Pre-init: no doublings (accumulator is conceptually identity).
1653 if ((digit & 0x7FFFFFFFU) != 0) {
1654 fill_to_add(digit, half_idx, &work_elements[0]);
1655 initialised = true;
1656 }
1657 };
1658
1659 // Pos 126: K2 window 32 (top). Magnitude in {0, 1, 2} given K2 < 2^127.
1660 seed_or_skip(k2_digits[K2_NUM_WINDOWS - 1], /*half_idx=*/true);
1661
1662 // Positions 124, 122, ..., 2 (62 positions, alternating K1 / K2).
1663 for (size_t step = 0; step < 62; ++step) {
1664 const size_t pos = 124 - 2 * step;
1665 const bool is_k1 = (pos % 4 == 0);
1666 const uint32_t digit = is_k1 ? k1_digits[pos / 4] : k2_digits[(pos + 2) / 4];
1667 const bool half_idx = !is_k1;
1668 const uint32_t m = digit & 0x7FFFFFFFU;
1669
1670 if (!initialised) {
1671 if (m != 0) {
1672 fill_to_add(digit, half_idx, &work_elements[0]);
1673 initialised = true;
1674 }
1675 continue;
1676 }
1677
1678 if (m == 0) {
1679 // 2 unfused doublings to shift past this empty position.
1680 double_chunked(&work_elements[0]);
1681 double_chunked(&work_elements[0]);
1682 continue;
1683 }
1684
1685 // (2·dbl + add) fused as (1·dbl + combined_chunked).
1686 double_chunked(&work_elements[0]);
1687 fill_to_add(digit, half_idx, &temp_point_vector[0]);
1688 if ((safe_mask >> step) & uint64_t{ 1 }) {
1689 combined_safe_chunked(&temp_point_vector[0], &work_elements[0]);
1690 update_initialised_from_work();
1691 } else {
1692 combined_chunked(&temp_point_vector[0], &work_elements[0]);
1693 }
1694 }
1695
1696 // Pos 0: both K1 window 0 (4-bit) and K2 window 0 (2-bit). Transition from
1697 // pos 2 still requires the standard 2-dbl shift; the second contribution rides
1698 // on top via an extra add_chunked.
1699 {
1700 const uint32_t d0 = k1_digits[0];
1701 const uint32_t d1 = k2_digits[0];
1702 const uint32_t m0 = d0 & 0x7FFFFFFFU;
1703 const uint32_t m1 = d1 & 0x7FFFFFFFU;
1704
1705 if (!initialised) {
1706 if (m0 != 0) {
1707 fill_to_add(d0, /*half_idx=*/false, &work_elements[0]);
1708 initialised = true;
1709 if (m1 != 0) {
1710 // accum = d0·P, to_add = d1·φP with both digits non-zero.
1711 // x-coords cannot collide (linear independence of P, φP), so the
1712 // unsafe batch-add formula is always safe here.
1713 fill_to_add(d1, /*half_idx=*/true, &temp_point_vector[0]);
1714 add_chunked(&temp_point_vector[0], &work_elements[0]);
1715 }
1716 } else if (m1 != 0) {
1717 fill_to_add(d1, /*half_idx=*/true, &work_elements[0]);
1718 initialised = true;
1719 }
1720 } else if (m0 == 0 && m1 == 0) {
1721 double_chunked(&work_elements[0]);
1722 double_chunked(&work_elements[0]);
1723 } else {
1724 double_chunked(&work_elements[0]);
1725 const bool fuse_with_h1 = (m0 == 0);
1726 const uint32_t fused_digit = fuse_with_h1 ? d1 : d0;
1727 fill_to_add(fused_digit, fuse_with_h1, &temp_point_vector[0]);
1728 if ((safe_mask >> 62) & uint64_t{ 1 }) {
1729 combined_safe_chunked(&temp_point_vector[0], &work_elements[0]);
1730 update_initialised_from_work();
1731 } else {
1732 combined_chunked(&temp_point_vector[0], &work_elements[0]);
1733 }
1734 if (m0 != 0 && m1 != 0) {
1735 if (!initialised) {
1736 fill_to_add(d1, /*half_idx=*/true, &work_elements[0]);
1737 initialised = true;
1738 } else {
1739 fill_to_add(d1, /*half_idx=*/true, &temp_point_vector[0]);
1740 if ((safe_mask >> 63) & uint64_t{ 1 }) {
1741 add_safe_chunked(&temp_point_vector[0], &work_elements[0]);
1742 update_initialised_from_work();
1743 } else {
1744 add_chunked(&temp_point_vector[0], &work_elements[0]);
1745 }
1746 }
1747 }
1748 }
1749 }
1750
1751 BB_ASSERT(initialised, "non-zero scalar must produce at least one non-zero Booth digit");
1752
1753 // Restore infinity for slots where the input was at infinity.
1754 for (size_t i = start; i < end; ++i) {
1755 if (points[i].is_point_at_infinity()) {
1756 work_elements[i].self_set_infinity();
1757 }
1758 }
1759 };
1760 parallel_for_range(num_points, execute_range);
1761
1762 return work_elements;
1763}
1764
1765template <class Fq, class Fr, class T>
1767 const std::span<const affine_element<Fq, Fr, T>>& points, const Fr& u1, const Fr& u2) noexcept
1768{
1769 BB_BENCH();
1771
1772 // Four terms, one per SRS quarter: (u1·u2)·P0 + u1·P1 + u2·P2 + P3. Three are scalar-multiplied
1773 // (P0, P1, P2; term 2 reuses P0's table via φ); P3 is the constant tail added at the end.
1774 constexpr size_t NUM_TERMS = 4;
1775 constexpr size_t NUM_MULTIPLIED_BASES = 3;
1776
1777 BB_ASSERT_EQ(points.size() % NUM_TERMS, 0U, "batch_two_round_fold input must split into four equal quarters");
1778 const size_t t = points.size() / NUM_TERMS;
1779 if (t == 0) {
1780 return {};
1781 }
1782
1783 constexpr size_t LOOKUP_SIZE = detail::BOOTH_ENDO_LOOKUP_SIZE;
1784 constexpr size_t WINDOW_BITS = detail::BOOTH_ENDO_WINDOW_BITS;
1785 constexpr size_t NUM_WINDOWS = detail::BOOTH_ENDO_NUM_WINDOWS;
1786 constexpr size_t OFFSET_NUM_WINDOWS = detail::BOOTH_ENDO_K2_NUM_WINDOWS;
1787 // Highest bit position of a < 2^127 challenge; the Booth windows tile positions 0..TOP_BIT.
1788 constexpr size_t TOP_BIT = (WINDOW_BITS * NUM_WINDOWS) - 1; // 4*32 - 1 = 127
1789
1790 // Canonical ([0, p)) value of the scalar inputs. from_montgomery_form() alone is only
1791 // coarse-reduced ([0, 2p)) and returns value+p on WASM's 29-bit-limb Montgomery backend, which
1792 // would feed a non-canonical (full-width-looking) value into the assert and the Booth-digit
1793 // extraction below — tripping the assert / building the wrong fold on WASM.
1794 const Fr u1_conv = u1.from_montgomery_form_reduced();
1795 const Fr u2_conv = u2.from_montgomery_form_reduced();
1796 BB_ASSERT(((u1_conv.data[2] | u1_conv.data[3]) == 0) && ((u1_conv.data[1] >> 63) == 0),
1797 "batch_two_round_fold challenges must be below 2^127");
1798 BB_ASSERT(((u2_conv.data[2] | u2_conv.data[3]) == 0) && ((u2_conv.data[1] >> 63) == 0),
1799 "batch_two_round_fold challenges must be below 2^127");
1800 const Fr u12_conv = (u1 * u2).from_montgomery_form_reduced();
1801 const detail::EndoScalars endo_scalars = Fr::split_into_endomorphism_scalars(u12_conv);
1802 BB_ASSERT_EQ((endo_scalars.first[1] >> 63), 0U, "GLV K1 split must fit below 2^127 for the Booth grid");
1803 BB_ASSERT_EQ((endo_scalars.second[1] >> 63), 0U, "GLV K2 split must fit below 2^127 for the offset Booth grid");
1804
1805 // Per-term Booth digit arrays. Term 0 (K1 of u1·u2) uses the standard 4-bit grid;
1806 // terms 1, 2, 3 use the offset grids with 1-, 2-, 3-bit bottom windows respectively. The
1807 // interleaving below places term j at bit positions ≡ j (mod 4), which only tiles every
1808 // position exactly once when term 2's bottom window is 2 bits wide.
1809 static_assert(detail::BOOTH_ENDO_K2_LOW_WINDOW_BITS == 2,
1810 "the offset-grid interleaving (term j at pos ≡ j mod 4) requires a 2-bit bottom window for term 2");
1811 constexpr auto grid0 =
1812 detail::make_booth_slice_params<NUM_WINDOWS, WINDOW_BITS, detail::BOOTH_ENDO_NUM_LIMBS_U64>();
1813 constexpr auto grid1 =
1814 detail::make_offset_booth_slice_params<OFFSET_NUM_WINDOWS, WINDOW_BITS, 1, detail::BOOTH_ENDO_NUM_LIMBS_U64>();
1815 constexpr auto grid2 = detail::make_offset_booth_slice_params<OFFSET_NUM_WINDOWS,
1816 WINDOW_BITS,
1819 constexpr auto grid3 =
1820 detail::make_offset_booth_slice_params<OFFSET_NUM_WINDOWS, WINDOW_BITS, 3, detail::BOOTH_ENDO_NUM_LIMBS_U64>();
1821
1826 for (size_t w = 0; w < NUM_WINDOWS; ++w) {
1827 digits0[w] = detail::booth_packed_digit(endo_scalars.first.data(), grid0[w], WINDOW_BITS);
1828 }
1829 digits1[0] = detail::booth_packed_digit(u1_conv.data, grid1[0], 1);
1830 digits2[0] =
1831 detail::booth_packed_digit(endo_scalars.second.data(), grid2[0], detail::BOOTH_ENDO_K2_LOW_WINDOW_BITS);
1832 digits3[0] = detail::booth_packed_digit(u2_conv.data, grid3[0], 3);
1833 for (size_t w = 1; w < OFFSET_NUM_WINDOWS; ++w) {
1834 digits1[w] = detail::booth_packed_digit(u1_conv.data, grid1[w], WINDOW_BITS);
1835 digits2[w] = detail::booth_packed_digit(endo_scalars.second.data(), grid2[w], WINDOW_BITS);
1836 digits3[w] = detail::booth_packed_digit(u2_conv.data, grid3[w], WINDOW_BITS);
1837 }
1838 // digit_at(j, m) = d^(j)_m, the packed Booth digit of term j's window m (see ELEMENT_IMPL_FOLD.md);
1839 // signed_delta turns it into the signed δ added to base B_j.
1840 const auto digit_at = [&](size_t term, size_t window) -> uint32_t {
1841 switch (term) {
1842 case 0:
1843 return digits0[window];
1844 case 1:
1845 return digits1[window];
1846 case 2:
1847 return digits2[window];
1848 default:
1849 return digits3[window];
1850 }
1851 };
1852
1853 // Build the FoldOp schedule from the scalars (no points touched). The batch-affine formulas need
1854 // generic position, so each op carries a `safe` flag, computed by simulating the accumulator's
1855 // integer coordinates. See ipa/ELEMENT_IMPL_FOLD.md (Phase 1, The safe flags).
1857 ops.reserve(TOP_BIT + NUM_TERMS + 1); // one op per bit position 1..TOP_BIT, plus position 0's windows
1858 bool final_initialised = false;
1859 {
1860 // Horner double-and-add, MSB→LSB, simulated on integers only: the accumulator is the vector
1861 // `coeff` (= c in the spec) over the per-term bases (P0, P1, φP0, P2), i.e. accum = Σ_i coeff[i]·base[i].
1862 std::array<int64_t, NUM_TERMS> coeff{ 0, 0, 0, 0 };
1863 bool initialised = false; // false until the first non-zero digit seeds the accumulator
1864
1865 // Clamp coeff to ±CAP. CAP is arbitrary — any value well above the ±8 digit window and well
1866 // below int64 overflow works; see ipa/ELEMENT_IMPL_FOLD.md ("Clamping the simulation") for the
1867 // bounds and the soundness argument.
1868 constexpr int64_t CAP = int64_t{ 1 } << 40;
1869 const auto clamp_coeff = [&]() {
1870 for (auto& c : coeff) {
1871 c = std::clamp(c, -CAP, CAP);
1872 }
1873 };
1874 // Signed magnitude actually added to the accumulator. `digit` is a packed signed-Booth digit
1875 // (Constantine convention, as produced by booth_packed_digit): bit 31 is the sign and the low
1876 // 31 bits are the magnitude. Term 2 is the GLV K2 component on φ(P0); u1·u2 = K1 − K2·λ
1877 // decomposes with a minus on K2, so term 2's sign is flipped.
1878 const auto signed_delta = [](uint32_t digit, size_t term) -> int64_t {
1879 const auto mag = static_cast<int64_t>(digit & 0x7FFFFFFFU);
1880 const bool neg = ((digit >> 31) != 0) ^ (term == 2);
1881 return neg ? -mag : mag;
1882 };
1883 // An edge needs accum = ±d·base[j]; the bases are independent, so all other coords must be 0.
1884 const auto others_zero = [&](size_t j) {
1885 for (size_t i = 0; i < NUM_TERMS; ++i) {
1886 if (i != j && coeff[i] != 0) {
1887 return false;
1888 }
1889 }
1890 return true;
1891 };
1892 // accum == O (the running point is the identity) — batch ops can't operate on it, so re-seed.
1893 const auto all_zero = [&]() { return coeff[0] == 0 && coeff[1] == 0 && coeff[2] == 0 && coeff[3] == 0; };
1894
1895 // Apply one term's digit to `coeff` and record its op. `transition` = this digit owns the
1896 // position's doubling (→ COMBINED, 2·accum+base); false only for extra position-0 windows (→ ADD).
1897 const auto accumulate_digit = [&](size_t term, uint32_t digit, bool transition) {
1898 const int64_t delta = signed_delta(digit, term);
1899 const int64_t mag = std::abs(delta);
1900 if (!initialised) {
1901 // Nothing to double or add onto yet: the first non-zero digit just loads its base.
1902 ops.push_back({ detail::FoldOpKind::SEED, static_cast<uint8_t>(term), digit, false });
1903 coeff[term] = delta;
1904 initialised = true;
1905 return;
1906 }
1907 if (transition) {
1908 // COMBINED computes 2·accum + to_add. Batch-affine edge (shared x-coordinate) when
1909 // to_add = ±accum, or when 2·accum + to_add = O. See ipa/ELEMENT_IMPL_FOLD.md.
1910 const bool safe = others_zero(term) && (std::abs(coeff[term]) == mag || 2 * coeff[term] == -delta);
1911 ops.push_back({ detail::FoldOpKind::COMBINED, static_cast<uint8_t>(term), digit, safe });
1912 for (auto& c : coeff) {
1913 c *= 2;
1914 }
1915 } else {
1916 // Plain ADD edge: to_add = ±accum.
1917 const bool safe = others_zero(term) && std::abs(coeff[term]) == mag;
1918 ops.push_back({ detail::FoldOpKind::ADD, static_cast<uint8_t>(term), digit, safe });
1919 }
1920 coeff[term] += delta;
1921 clamp_coeff();
1922 if (all_zero()) {
1923 // Only a safe op can reach the identity; re-seed at the next non-zero digit.
1924 initialised = false;
1925 }
1926 };
1927
1928 // Iterate over bit positions 127→1. The offset grids place exactly one term's window at each
1929 // position: term = pos mod 4. Terms 1/2/3 keep their lowest window at position 0, so a window
1930 // at pos>0 is window (pos-term)/4 + 1; term 0 (no offset) uses pos/4.
1931 for (size_t pos = TOP_BIT; pos >= 1; --pos) {
1932 const size_t term = pos % NUM_TERMS;
1933 const size_t window = (term == 0) ? pos / WINDOW_BITS : (pos - term) / WINDOW_BITS + 1;
1934 const uint32_t digit = digit_at(term, window);
1935 if ((digit & 0x7FFFFFFFU) == 0) {
1936 // Zero digit: just the Horner doubling for this position — skipped while unseeded,
1937 // since there is no running point to double yet.
1938 if (initialised) {
1939 ops.push_back({ detail::FoldOpKind::DBL, 0, 0, false });
1940 for (auto& c : coeff) {
1941 c *= 2;
1942 }
1943 clamp_coeff();
1944 }
1945 continue;
1946 }
1947 accumulate_digit(term, digit, /*transition=*/true);
1948 }
1949
1950 // Position 0 holds all four lowest windows (term 0's window 0 plus the 1/2/3-bit bottoms of
1951 // terms 1/2/3). The step-in doubling fuses into the first non-zero one (COMBINED); the rest are ADDs.
1952 bool transition_pending = initialised;
1953 for (size_t term = 0; term < NUM_TERMS; ++term) {
1954 const uint32_t digit = digit_at(term, 0);
1955 if ((digit & 0x7FFFFFFFU) == 0) {
1956 continue;
1957 }
1958 accumulate_digit(term, digit, transition_pending);
1959 transition_pending = false;
1960 }
1961 // The step into position 0 must still happen if every window here was zero
1962 // (Horner: result = prev·2 + 0), so emit the trailing doubling when nothing consumed it.
1963 if (transition_pending) {
1964 ops.push_back({ detail::FoldOpKind::DBL, 0, 0, false });
1965 }
1966 final_initialised = initialised; // false ⟺ all terms cancelled, i.e. the chain's result is O
1967 }
1968
1969 // Execute the schedule over the point batch.
1970 std::vector<affine_element> work_elements(t);
1971
1972 if (!final_initialised) {
1973 // All four scalars vanish iff u1 = u2 = 0 (never for real Fiat–Shamir challenges); the
1974 // output is then the constant P3 quarter.
1976 t, [&](size_t i) { work_elements[i] = points[3 * t + i]; }, thread_heuristics::FF_COPY_COST);
1977 return work_elements;
1978 }
1979
1980 std::vector<Fq> scratch_space(3 * t);
1981 Fq* const scratch_a = &scratch_space[0];
1982 Fq* const scratch_b = &scratch_space[t];
1983 Fq* const scratch_c = &scratch_space[2 * t];
1984
1985 // Lookup tables [1·B, ..., 8·B] for the three multiplied bases (term 2 reuses base 0's
1986 // table with the on-the-fly β twist, as in batch_mul_with_endomorphism).
1987 std::array<std::array<std::vector<affine_element>, LOOKUP_SIZE>, NUM_MULTIPLIED_BASES> lookup_tables;
1988 for (auto& base_table : lookup_tables) {
1989 for (auto& table : base_table) {
1990 table.resize(t);
1991 }
1992 }
1993 std::vector<affine_element> temp_point_vector(t);
1994
1995 // Produce the folded outputs work_elements[i] = (u1·u2)·P0_i + u1·P1_i + u2·P2_i + P3_i for the
1996 // index range [start, end): build the per-base lookup tables over this slice, replay the op
1997 // schedule (one batched-affine group op per FoldOp), then add the constant P3 quarter. The same
1998 // schedule runs on every range; the split is for parallelism and batched-inversion amortisation.
1999 auto fold_point_range = [&](size_t start, size_t end) {
2000 const auto add_chunked = [&](const affine_element* lhs, affine_element* rhs) {
2001 batch_affine_add_impl<affine_element, Fq>(&lhs[start], &rhs[start], end - start, &scratch_a[start]);
2002 };
2003 const auto double_chunked = [&](affine_element* pts) {
2004 batch_affine_double_impl<affine_element, Fq, T>(&pts[start], end - start, &scratch_a[start]);
2005 };
2006 const auto combined_chunked = [&](const affine_element* to_add, affine_element* accum) {
2007 batch_affine_combined_double_add_impl<affine_element, Fq>(
2008 &to_add[start], &accum[start], end - start, &scratch_a[start], &scratch_b[start], &scratch_c[start]);
2009 };
2010 const auto add_safe_chunked = [&](const affine_element* lhs, affine_element* rhs) {
2011 for (size_t i = start; i < end; ++i) {
2012 element acc(rhs[i]);
2013 acc += lhs[i];
2014 rhs[i] = affine_element(acc);
2015 }
2016 };
2017 const auto combined_safe_chunked = [&](const affine_element* to_add, affine_element* accum) {
2018 for (size_t i = start; i < end; ++i) {
2019 element acc(accum[i]);
2020 acc.self_dbl();
2021 acc += to_add[i];
2022 accum[i] = affine_element(acc);
2023 }
2024 };
2025
2026 // Build the lookup tables.
2027 for (size_t base = 0; base < NUM_MULTIPLIED_BASES; ++base) {
2028 const affine_element* base_points = &points[base * t];
2029 auto& table = lookup_tables[base];
2030 for (size_t i = start; i < end; ++i) {
2031 table[0][i] = base_points[i];
2032 table[1][i] = base_points[i];
2033 temp_point_vector[i] = base_points[i];
2034 }
2035 double_chunked(&table[1][0]);
2036 for (size_t j = 2; j < LOOKUP_SIZE; ++j) {
2037 for (size_t i = start; i < end; ++i) {
2038 table[j][i] = table[j - 1][i];
2039 }
2040 add_chunked(&temp_point_vector[0], &table[j][0]);
2041 }
2042 }
2043
2044 constexpr Fq beta = Fq::cube_root_of_unity();
2045 const auto fill_to_add = [&](size_t term, uint32_t digit, affine_element* dst) {
2046 const uint32_t magnitude = digit & 0x7FFFFFFFU;
2047 const bool flip_y = ((digit >> 31) != 0) ^ (term == 2);
2048 // term → multiplied-base index. Term 2 (the GLV K2 component on φ(P0)) shares base 0's
2049 // table via the β twist below, so it maps to 0 rather than a base of its own.
2050 static constexpr std::array<size_t, NUM_TERMS> term_to_base{ 0, 1, 0, 2 };
2051 const auto& table = lookup_tables[term_to_base[term]][magnitude - 1];
2052 for (size_t i = start; i < end; ++i) {
2053 affine_element pt = table[i];
2054 pt.y.self_conditional_negate(flip_y);
2055 if (term == 2) {
2056 pt.x *= beta;
2057 }
2058 dst[i] = pt;
2059 }
2060 };
2061
2062 for (const detail::FoldOp& op : ops) {
2063 switch (op.kind) {
2065 double_chunked(&work_elements[0]);
2066 break;
2068 fill_to_add(op.term, op.digit, &work_elements[0]);
2069 break;
2071 fill_to_add(op.term, op.digit, &temp_point_vector[0]);
2072 if (op.safe) {
2073 combined_safe_chunked(&temp_point_vector[0], &work_elements[0]);
2074 } else {
2075 combined_chunked(&temp_point_vector[0], &work_elements[0]);
2076 }
2077 break;
2079 fill_to_add(op.term, op.digit, &temp_point_vector[0]);
2080 if (op.safe) {
2081 add_safe_chunked(&temp_point_vector[0], &work_elements[0]);
2082 } else {
2083 add_chunked(&temp_point_vector[0], &work_elements[0]);
2084 }
2085 break;
2086 }
2087 }
2088
2089 // Constant tail: + P3. The chain result carries no P3 component, so a collision would
2090 // require a small-coefficient relation between independent SRS points.
2091 add_chunked(&points[3 * t], &work_elements[0]);
2092 };
2093 parallel_for_range(t, fold_point_range);
2094
2095 return work_elements;
2096}
2097
2098template <typename Fq, typename Fr, typename T>
2099void element<Fq, Fr, T>::batch_normalize(element* elements, const size_t num_elements) noexcept
2100{
2101 std::vector<Fq> temporaries;
2102 temporaries.reserve(num_elements * 2);
2104
2105 // Iterate over the points, computing the product of their z-coordinates.
2106 // At each iteration, store the currently-accumulated z-coordinate in `temporaries`
2107 for (size_t i = 0; i < num_elements; ++i) {
2108 temporaries.emplace_back(accumulator);
2109 if (!elements[i].is_point_at_infinity()) {
2110 accumulator *= elements[i].z;
2111 }
2112 }
2113 // For the rest of this method we refer to the product of all z-coordinates as the 'global' z-coordinate
2114 // Invert the global z-coordinate and store in `accumulator`
2115 accumulator = accumulator.invert();
2116
2139 for (size_t i = num_elements - 1; i < num_elements; --i) {
2140 if (!elements[i].is_point_at_infinity()) {
2141 Fq z_inv = accumulator * temporaries[i];
2142 Fq zz_inv = z_inv.sqr();
2143 elements[i].x *= zz_inv;
2144 elements[i].y *= (zz_inv * z_inv);
2145 accumulator *= elements[i].z;
2146 }
2147 elements[i].z = Fq::one();
2148 }
2149}
2150
2151template <typename Fq, typename Fr, typename T>
2152template <typename>
2154{
2155 bool found_one = false;
2156 Fq yy;
2157 Fq x;
2158 Fq y;
2159 while (!found_one) {
2161 yy = x.sqr() * x + T::b;
2162 if constexpr (T::has_a) {
2163 yy += (x * T::a);
2164 }
2165 auto [found_root, y1] = yy.sqrt();
2166 y = y1;
2167 found_one = found_root;
2168 }
2169 return { x, y, Fq::one() };
2170}
2171
2172} // namespace bb::group_elements
2173// NOLINTEND(readability-implicit-bool-conversion, cppcoreguidelines-avoid-c-arrays)
#define BB_ASSERT(expression,...)
Definition assert.hpp:70
#define BB_ASSERT_EQ(actual, expected,...)
Definition assert.hpp:83
#define BB_BENCH_NAME(name)
Definition bb_bench.hpp:264
#define BB_BENCH_TRACY_NAME(name)
Definition bb_bench.hpp:256
#define BB_BENCH()
Definition bb_bench.hpp:268
constexpr bool is_point_at_infinity() const noexcept
static constexpr affine_element one() noexcept
element class. Implements ecc group arithmetic using Jacobian coordinates See https://hyperelliptic....
Definition element.hpp:35
element operator*=(const Fr &exponent) noexcept
BB_INLINE constexpr element set_infinity() const noexcept
element mul_with_endomorphism(const Fr &scalar) const noexcept
static std::vector< affine_element< Fq, Fr, Params > > batch_mul_with_endomorphism(const std::span< const affine_element< Fq, Fr, Params > > &points, const Fr &scalar) noexcept
Multiply each point by the same scalar.
constexpr element operator-=(const element &other) noexcept
constexpr element operator-() const noexcept
constexpr affine_element< Fq, Fr, Params > to_affine_const_time() const noexcept
friend constexpr element operator+(const affine_element< Fq, Fr, Params > &left, const element &right) noexcept
Definition element.hpp:76
static std::vector< affine_element< Fq, Fr, Params > > batch_two_round_fold(const std::span< const affine_element< Fq, Fr, Params > > &points, const Fr &u1, const Fr &u2) noexcept
Fused two-round IPA SRS fold: out[i] = (u1·u2)·P[i] + u1·P[i+t] + u2·P[i+2t] + P[i+3t].
constexpr element dbl() const noexcept
constexpr element normalize() const noexcept
constexpr void self_dbl() noexcept
static element random_element(numeric::RNG *engine=nullptr) noexcept
static void batch_normalize(element *elements, size_t num_elements) noexcept
constexpr element operator+=(const element &other) noexcept
static void batch_affine_add(const std::span< affine_element< Fq, Fr, Params > > &first_group, const std::span< affine_element< Fq, Fr, Params > > &second_group, const std::span< affine_element< Fq, Fr, Params > > &results) noexcept
Pairwise affine add points in first and second group.
element mul_const_time(const Fr &scalar, numeric::RNG *engine=nullptr) const noexcept
Constant-time scalar multiplication intended for secret scalars (e.g. ECDSA / Schnorr nonces).
BB_INLINE constexpr bool on_curve() const noexcept
BB_INLINE constexpr bool operator==(const element &other) const noexcept
element operator*(const Fr &exponent) const noexcept
static element straus_msm(std::span< const affine_element< Fq, Fr, Params > > points, std::span< const Fr > scalars) noexcept
Straus-style multi-scalar multiplication.
element() noexcept=default
static element random_coordinates_on_curve(numeric::RNG *engine=nullptr) noexcept
element mul_without_endomorphism(const Fr &scalar) const noexcept
constexpr element & operator=(const element &other) noexcept
BB_INLINE constexpr void self_set_infinity() noexcept
constexpr element normalize_const_time() const noexcept
BB_INLINE constexpr bool is_point_at_infinity() const noexcept
constexpr bool get_bit(uint64_t bit_index) const
constexpr uint64_t get_msb() const
bool get_bit(uint64_t bit_index) const
#define BB_UNUSED
FF a
FF b
numeric::RNG & engine
constexpr std::array< BoothSliceParams, NUM_WINDOWS > make_offset_booth_slice_params() noexcept
constexpr std::array< BoothSliceParams, NUM_WINDOWS > make_booth_slice_params() noexcept
uint32_t booth_packed_digit(const uint64_t *s, const BoothSliceParams &sp, size_t window_bits) noexcept
Read a (window_bits+1)-bit window from s[] (uint64 limbs) and apply Constantine's signedWindowEncodin...
constexpr size_t BOOTH_ENDO_K2_NUM_WINDOWS
std::pair< std::array< uint64_t, 2 >, std::array< uint64_t, 2 > > EndoScalars
constexpr size_t BOOTH_ENDO_K2_LOW_WINDOW_BITS
constexpr size_t BOOTH_ENDO_WINDOW_BITS
constexpr size_t BOOTH_ENDO_LOOKUP_SIZE
constexpr size_t BOOTH_ENDO_NUM_WINDOWS
constexpr size_t BOOTH_ENDO_NUM_LIMBS_U64
AffineElement const size_t Fq *scratch_space noexcept
AffineElement const size_t num_pairs
__attribute__((always_inline)) inline void batch_affine_add_impl(const AffineElement *lhs
Batch affine addition for parallel arrays: (lhs[i], rhs[i]) → rhs[i].
const size_t num_points
AffineElement const size_t Fq Fq * scratch_b
const uint32_t * indices
AffineElement * rhs
AffineElement * accumulator
AffineElement const size_t Fq * scratch_a
const std::pair< uint32_t, uint32_t > * pairs
uintx< uint256_t > uint512_t
Definition uintx.hpp:309
RNG & get_randomness()
Definition engine.cpp:258
std::conditional_t< IsGoblinBigGroup< C, Fq, Fr, G >, element_goblin::goblin_element< C, goblin_field< C >, Fr, G >, element_default::element< C, Fq, Fr, G > > element
element wraps either element_default::element or element_goblin::goblin_element depending on parametr...
constexpr size_t FF_COPY_COST
Definition thread.hpp:144
constexpr size_t FF_ADDITION_COST
Definition thread.hpp:132
constexpr size_t FF_MULTIPLICATION_COST
Definition thread.hpp:134
Univariate< Fr, domain_end > operator*(const Fr &ff, const Univariate< Fr, domain_end > &uv)
void parallel_for_heuristic(size_t num_points, const std::function< void(size_t, size_t, size_t)> &func, size_t heuristic_cost)
Split a loop into several loops running in parallel based on operations in 1 iteration.
Definition thread.cpp:172
void parallel_for_range(size_t num_points, const std::function< void(size_t, size_t)> &func, size_t no_multhreading_if_less_or_equal)
Split a loop into several loops running in parallel.
Definition thread.cpp:142
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
bb::VectorAffineElementPushSpan< BaseParams > lhs
grumpkin::fq Fq
static constexpr field cube_root_of_unity()
BB_INLINE constexpr field from_montgomery_form_reduced() const noexcept
static constexpr field one()
static constexpr uint256_t modulus
static void split_into_endomorphism_scalars(const field &k, field &k1, field &k2)
Full-width endomorphism decomposition: k ≡ k1 - k2·λ (mod r). Modifies the field elements k1 and k2.
BB_INLINE constexpr void self_sqr() &noexcept
constexpr field invert() const noexcept
BB_INLINE constexpr bool is_msb_set() const noexcept
static field random_element(numeric::RNG *engine=nullptr) noexcept
BB_INLINE constexpr field sqr() const noexcept
BB_INLINE constexpr bool is_zero() const noexcept
BB_INLINE constexpr field from_montgomery_form() const noexcept
static constexpr field zero()
constexpr field invert_const_time() const noexcept
void throw_or_abort(std::string const &err)
VectorField result