Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
affine_element.test.cpp
Go to the documentation of this file.
2
13
14#include "gmock/gmock.h"
15#include <algorithm>
16#include <fstream>
17#include <gtest/gtest.h>
18#include <iterator>
19#include <tuple>
20
21using ::testing::Each;
22using ::testing::ElementsAreArray;
23using ::testing::Eq;
24using ::testing::Property;
25
26using namespace bb;
27
28namespace {
29template <typename G1> class TestAffineElement : public testing::Test {
30 using element = typename G1::element;
31 using affine_element = typename G1::affine_element;
32 using Fr = typename G1::Fr;
33
34 public:
35 static void test_read_write_buffer()
36 {
37 // a generic point
38 {
39 affine_element P = affine_element(element::random_element());
40 affine_element R;
41
42 std::vector<uint8_t> v(64);
43 uint8_t* ptr = v.data();
44 affine_element::serialize_to_buffer(P, ptr);
45
46 R = affine_element::serialize_from_buffer(ptr);
47 ASSERT_TRUE(R.on_curve());
48 ASSERT_TRUE(P == R);
49 }
50
51 // point at infinity
52 {
53 affine_element P = affine_element(element::random_element());
54 P.self_set_infinity();
55 affine_element R;
56
57 std::vector<uint8_t> v(64);
58 uint8_t* ptr = v.data();
59 affine_element::serialize_to_buffer(P, ptr);
60
61 R = affine_element::serialize_from_buffer(ptr);
62 ASSERT_TRUE(R.is_point_at_infinity());
63 ASSERT_TRUE(P == R);
64 }
65 }
66
67 // Verify that serialize_from_buffer rejects off-curve bytes by throwing.
68 static void test_deserialize_off_curve_throws()
69 {
70 using Fq = typename G1::Fq;
71 // Take a valid on-curve point and corrupt its y-coordinate.
72 // P.y + 1 satisfies (y+1)^2 != y^2 (i.e. off-curve) unless 2y + 1 = 0 (prob ~1/p).
73 affine_element P = affine_element(element::random_element());
74 affine_element off_curve;
75 off_curve.x = P.x;
76 off_curve.y = P.y + Fq::one();
77
78 std::vector<uint8_t> v(sizeof(affine_element));
79 uint8_t* ptr = v.data();
80 affine_element::serialize_to_buffer(off_curve, ptr);
81
82 if (!off_curve.on_curve()) {
83#ifndef __wasm__
84 EXPECT_THROW_OR_ABORT(affine_element::serialize_from_buffer(ptr), "not on the curve");
85#endif
86 }
87 }
88
89 static void test_read_and_write()
90 {
91 // a generic point
92 {
93 affine_element P = affine_element(element::random_element());
94 [[maybe_unused]] affine_element R;
95
96 std::vector<uint8_t> v(sizeof(R));
97 uint8_t* ptr = v.data();
98 write(ptr, P);
99 ASSERT_TRUE(P.on_curve());
100
101 // // Reset to start?
102 // ptr = v.data();
103
104 const uint8_t* read_ptr = v.data();
105 // good read
106 read(read_ptr, R);
107 ASSERT_TRUE(R.on_curve());
108 ASSERT_TRUE(P == R);
109 }
110 }
111
112 static void test_msgpack_serialization()
113 {
114 // a generic point
115 {
116 affine_element P = affine_element(element::random_element());
117
118 // Serialize using msgpack
119 msgpack::sbuffer sbuf;
120 msgpack::pack(sbuf, P);
121
122 // Deserialize using msgpack
123 msgpack::object_handle oh = msgpack::unpack(sbuf.data(), sbuf.size());
124 msgpack::object deserialized = oh.get();
125
126 affine_element R;
127 deserialized.convert(R);
128
129 ASSERT_TRUE(R.on_curve() && !R.is_point_at_infinity());
130 ASSERT_TRUE(P == R);
131 }
132
133 // point at infinity
134 {
135 affine_element P = affine_element(element::random_element());
136 P.self_set_infinity();
137
138 // Serialize using msgpack
139 msgpack::sbuffer sbuf;
140 msgpack::pack(sbuf, P);
141
142 // Deserialize using msgpack
143 msgpack::object_handle oh = msgpack::unpack(sbuf.data(), sbuf.size());
144 msgpack::object deserialized = oh.get();
145
146 affine_element R;
147 deserialized.convert(R);
148
149 ASSERT_TRUE(R.is_point_at_infinity());
150 ASSERT_TRUE(P == R);
151 }
152 }
153
154 static void test_point_compression()
155 {
156 for (size_t i = 0; i < 10; i++) {
157 affine_element P = affine_element(element::random_element());
158 uint256_t compressed = uint256_t(P.x);
159 if (uint256_t(P.y).get_bit(0)) {
160 compressed.data[3] |= group_elements::UINT256_TOP_LIMB_MSB;
161 }
162 affine_element Q = affine_element::from_compressed(compressed);
163 EXPECT_EQ(P, Q);
164 }
165 }
166
167 static void test_point_compression_unsafe()
168 {
169 for (size_t i = 0; i < 100; i++) {
170 affine_element P = affine_element(element::random_element());
171 uint256_t compressed = uint256_t(P.x);
172
173 // Note that we do not check the point Q_points[1] because its highly unlikely to hit a point P on the curve
174 // such that r < P.x < q.
175 std::array<affine_element, 2> Q_points = affine_element::from_compressed_unsafe(compressed);
176 EXPECT_EQ(P, Q_points[0]);
177 }
178 }
179
180 static void test_add_affine()
181 {
182 element lhs = element::random_element();
183 affine_element lhs_affine(lhs);
184
185 element rhs = element::random_element();
186 affine_element rhs_affine(rhs);
187
188 element expected = lhs + rhs;
189 affine_element result = lhs_affine + rhs_affine;
190 EXPECT_EQ(element(result) == expected, true);
191 }
192
193 // Regression test for the large-modulus mixed-addition path: element +/- affine_element must
194 // detect when the affine operand is the infinity sentinel (x = modulus, y = 0). Previously the
195 // operator only checked whether `*this` was infinity, so adding the infinity sentinel to a
196 // normal point fell through to the arithmetic and produced an off-curve garbage result.
197 // operator-=(affine) inherits the bug via its `to_add{other.x, -other.y}` delegation.
198 static void test_mixed_add_infinity_regression()
199 {
200 const element P = element::random_element();
201 const affine_element Q_inf = affine_element::infinity();
202
203 // P (+/-) infinity == P, both as out-of-place and compound-assignment.
204 EXPECT_EQ(P + Q_inf, P);
205 EXPECT_EQ(P - Q_inf, P);
206 {
207 element acc = P;
208 acc += Q_inf;
209 EXPECT_EQ(acc, P);
210 }
211 {
212 element acc = P;
213 acc -= Q_inf;
214 EXPECT_EQ(acc, P);
215 }
216
217 // infinity (+/-) P == +/-P
218 EXPECT_EQ(Q_inf + P, P);
219 EXPECT_EQ(Q_inf - P, -P);
220
221 // *this = infinity, other = infinity must remain infinity (not become {modulus, 0, 1}).
222 element inf_elem = element::zero();
223 ASSERT_TRUE(inf_elem.is_point_at_infinity());
224 EXPECT_TRUE((inf_elem + Q_inf).is_point_at_infinity());
225 EXPECT_TRUE((inf_elem - Q_inf).is_point_at_infinity());
226
227 // The result of mixing a normal point with the infinity sentinel must remain on-curve.
228 EXPECT_TRUE((P + Q_inf).on_curve());
229 EXPECT_TRUE((P - Q_inf).on_curve());
230 }
231
232 // Regression test to ensure that the point at infinity is not equal to its coordinate-wise reduction, which may lie
233 // on the curve, depending on the y-coordinate.
234 static void test_infinity_regression()
235 {
236 affine_element P;
237 P.self_set_infinity();
238 affine_element R(0, P.y);
239 ASSERT_FALSE(P == R);
240 }
241 static void test_infinity_ordering_regression()
242 {
243 affine_element P(0, 1);
244 affine_element Q(0, 1);
245
246 P.self_set_infinity();
247 EXPECT_NE(P < Q, Q < P);
248 }
249
250 // Regression test: from_compressed must reject non-canonical encodings (x_coordinate >= modulus).
251 // Without the range check, Fq(x) silently reduces mod p, so distinct compressed bytestrings whose
252 // x values differ by a multiple of p would decompress to the same point (encoding malleability).
253 static void test_point_compression_non_canonical_x()
254 {
255 using Fq = typename G1::Fq;
256 // x1 = 1 and x2 = 1 + p both fit in 255 bits because p_BN254 < 2^254 and p_Grumpkin < 2^254.
257 // They are distinct as 255-bit integers but equal mod p.
258 uint256_t x1 = uint256_t(1);
260 ASSERT_NE(x1, x2);
261 ASSERT_LT(x2, uint256_t(1) << 255);
262
263 affine_element pt1 = affine_element::from_compressed(x1);
264 affine_element pt2 = affine_element::from_compressed(x2);
265
266 // Canonical input (x = 1) decompresses to a valid point on these curves.
267 EXPECT_TRUE(pt1.on_curve());
268 // Non-canonical input must return the (0, 0) sentinel rather than the same point as x1.
269 EXPECT_EQ(pt2.x, Fq::zero());
270 EXPECT_EQ(pt2.y, Fq::zero());
271 EXPECT_NE(pt1, pt2);
272 }
273
274 // Verify that from_compressed with an x that has no y on the curve returns the (0,0) sentinel.
275 static void test_point_compression_invalid_x()
276 {
277 using Fq = typename G1::Fq;
278 size_t invalid_count = 0;
279 for (size_t i = 0; i < 20; ++i) {
280 affine_element result = affine_element::from_compressed(uint256_t(Fq::random_element()));
281 if (!result.on_curve()) {
282 ++invalid_count;
283 // from_compressed returns (0, 0) when x has no valid y
284 EXPECT_EQ(result.x, Fq::zero());
285 EXPECT_EQ(result.y, Fq::zero());
286 }
287 }
288 // With 20 trials ~10 should have no valid y, so we almost certainly exercise this path
289 EXPECT_GT(invalid_count, 0U);
290 }
291
296 static void test_batch_endomorphism_by_minus_one()
297 {
298 constexpr size_t num_points = 2;
299 std::vector<affine_element> affine_points(num_points, affine_element::one());
300
302 element::batch_mul_with_endomorphism(affine_points, -affine_element::Fr::one());
303
304 for (size_t i = 0; i < num_points; i++) {
305 EXPECT_EQ(affine_points[i], -result[i]);
306 }
307 }
308
313 static void test_fixed_point_at_infinity()
314 {
315 using Fq = affine_element::Fq;
316 affine_element P = affine_element::infinity();
317 affine_element Q(Fq::zero(), Fq::zero());
318 Q.x.self_set_msb();
319 affine_element R = affine_element(element::random_element());
320 EXPECT_EQ(P, Q);
321 EXPECT_NE(P, R);
322 }
323
324 static void test_infinity_mul_by_scalar_is_infinity()
325 {
326 auto result = affine_element::infinity() * Fr::random_element();
327 EXPECT_TRUE(result.is_point_at_infinity());
328 }
329
330 static void test_batch_mul_matches_non_batch_mul()
331 {
332 constexpr size_t num_points = 512;
333 std::vector<affine_element> affine_points(num_points - 1, affine_element::infinity());
334 affine_points.push_back(affine_element::infinity());
335 Fr exponent = Fr::random_element();
337 std::transform(affine_points.begin(),
338 affine_points.end(),
339 std::back_inserter(expected),
340 [exponent](const auto& el) { return el * exponent; });
341 std::vector<affine_element> result = element::batch_mul_with_endomorphism(affine_points, exponent);
342 EXPECT_THAT(result, ElementsAreArray(expected));
343 }
344
345 static void test_infinity_batch_mul_by_scalar_is_infinity()
346 {
347 constexpr size_t num_points = 1024;
348 std::vector<affine_element> affine_points(num_points, affine_element::infinity());
349 std::vector<affine_element> result = element::batch_mul_with_endomorphism(affine_points, Fr::random_element());
350 EXPECT_THAT(result, Each(Property(&affine_element::is_point_at_infinity, Eq(true))));
351 }
352
353 static void test_batch_mul_endomorphism_even_scalars()
354 {
355 const affine_element P = affine_element::one();
356 const std::vector<affine_element> points(4, P);
357 for (const Fr scalar : { Fr(0), Fr(2), Fr(4), Fr(6), Fr(8) }) {
358 const auto result = element::batch_mul_with_endomorphism(points, scalar);
359 const affine_element expected(element(P) * scalar);
360 for (size_t i = 0; i < points.size(); ++i) {
361 EXPECT_EQ(result[i], expected);
362 }
363 }
364 }
365
366 // === helpers for K2-bit-width coverage of batch_mul_with_endomorphism ===
367
368 // bit_length of the K2 half of the GLV split of `scalar` (0 for zero).
369 static size_t k2_bit_length(const Fr& scalar)
370 {
371 const Fr conv = scalar.from_montgomery_form();
372 if (conv.is_zero()) {
373 return 0;
374 }
375 const auto endo = Fr::split_into_endomorphism_scalars(conv);
376 const auto& k2 = endo.second;
377 if (k2[1] != 0) {
378 return 128 - static_cast<size_t>(__builtin_clzll(k2[1]));
379 }
380 if (k2[0] != 0) {
381 return 64 - static_cast<size_t>(__builtin_clzll(k2[0]));
382 }
383 return 0;
384 }
385
386 // Search random scalars until one decomposes to K2 of exactly `target_bits` bits.
387 // K2 ≤ 127 bits is proven, so any target in [0, 127] is reachable; populations:
388 // 127 bits ≈ 50%, 126 bits ≈ 25%, 125 bits ≈ 12.5% — all easily found.
389 static Fr find_scalar_with_k2_bits(size_t target_bits, size_t max_attempts = 2000)
390 {
391 for (size_t i = 0; i < max_attempts; ++i) {
392 const Fr s = Fr::random_element();
393 if (k2_bit_length(s) == target_bits) {
394 return s;
395 }
396 }
397 throw_or_abort("could not find scalar with desired K2 bit-width");
398 }
399
400 // Run batch_mul_with_endomorphism on `num_points` independent random generators
401 // and assert it matches per-point projective multiplication.
402 static void check_batch_mul_against_naive(size_t num_points, const Fr& scalar)
403 {
405 points.reserve(num_points);
406 for (size_t i = 0; i < num_points; ++i) {
407 points.push_back(affine_element(element::random_element()));
408 }
410 expected.reserve(num_points);
411 for (const auto& p : points) {
412 expected.push_back(affine_element(element(p) * scalar));
413 }
414 const std::vector<affine_element> result = element::batch_mul_with_endomorphism(points, scalar);
415 ASSERT_EQ(result.size(), expected.size());
416 EXPECT_THAT(result, ElementsAreArray(expected));
417 }
418
419 // === 9 coverage tests for batch_mul_with_endomorphism ===
420
421 // (0) scalar = 0 ⇒ every output is the point at infinity.
422 static void test_batch_mul_zero_scalar()
423 {
424 constexpr size_t num_points = 64;
426 points.reserve(num_points);
427 for (size_t i = 0; i < num_points; ++i) {
428 points.push_back(affine_element(element::random_element()));
429 }
430 const std::vector<affine_element> result = element::batch_mul_with_endomorphism(points, Fr(0));
431 ASSERT_EQ(result.size(), num_points);
432 for (const auto& r : result) {
433 EXPECT_TRUE(r.is_point_at_infinity());
434 }
435 }
436
437 // (1) num_points coprime to typical num_threads.
438 static void test_batch_mul_num_points_not_multiple_of_threads()
439 {
440 check_batch_mul_against_naive(17, Fr::random_element());
441 }
442
443 // (2) scalar < 2^127 ⇒ GLV gives k1 = scalar, k2 = 0 (proven: c1=c2=0 when k<r/|b1|).
444 static void test_batch_mul_scalar_under_127_bits()
445 {
446 // Top nibble of the upper 64-bit limb is 0x3 ⇒ bit 127 = 0 and bit_length(scalar) = 126.
447 const Fr scalar(uint256_t{ 0xdeadbeefcafef00dULL, 0x3edcba98765432f1ULL, 0, 0 });
448 ASSERT_EQ(k2_bit_length(scalar), 0U);
449 check_batch_mul_against_naive(64, scalar);
450 }
451
452 // (3) scalar's bottom 127 bits all zero (= 2^127).
453 static void test_batch_mul_scalar_low_127_bits_zero()
454 {
455 const Fr scalar(uint256_t{ 0, 0, 1, 0 });
456 check_batch_mul_against_naive(64, scalar);
457 }
458
459 // (4) K2 = 128 bits — must never occur (K2 < 2^127 proven for BN254/Grumpkin GLV).
460 static void test_batch_mul_k2_128_bits_never_occurs()
461 {
462 for (size_t i = 0; i < 10000; ++i) {
463 const Fr s = Fr::random_element();
464 const size_t bits = k2_bit_length(s);
465 ASSERT_LE(bits, 127U) << "GLV split must produce K2 ≤ 127 bits; got " << bits << " bits on sample " << i;
466 }
467 }
468
469 // (5) K2 = 127 bits — init from pos-126 K2 window (top window magnitude ≥ 1).
470 static void test_batch_mul_k2_127_bits()
471 {
472 const Fr scalar = find_scalar_with_k2_bits(127);
473 ASSERT_EQ(k2_bit_length(scalar), 127U);
474 check_batch_mul_against_naive(64, scalar);
475 }
476
477 // (6) K2 = 126 bits — Booth carry from bit-125 lookback still gives top-window magnitude 1.
478 static void test_batch_mul_k2_126_bits()
479 {
480 const Fr scalar = find_scalar_with_k2_bits(126);
481 ASSERT_EQ(k2_bit_length(scalar), 126U);
482 check_batch_mul_against_naive(64, scalar);
483 }
484
485 // (7) K2 = 125 bits — top K2 window is empty; init falls through to pos-124 K1 window.
486 static void test_batch_mul_k2_125_bits()
487 {
488 const Fr scalar = find_scalar_with_k2_bits(125);
489 ASSERT_EQ(k2_bit_length(scalar), 125U);
490 check_batch_mul_against_naive(64, scalar);
491 }
492
493 // (8) empty points span.
494 static void test_batch_mul_empty_input()
495 {
496 const std::vector<affine_element> points;
497 const std::vector<affine_element> result = element::batch_mul_with_endomorphism(points, Fr::random_element());
498 EXPECT_TRUE(result.empty());
499 }
500
501 // (9) num_points < num_threads.
502 static void test_batch_mul_size_less_than_num_threads()
503 {
504 for (size_t sz : { size_t{ 1 }, size_t{ 2 }, size_t{ 3 } }) {
505 check_batch_mul_against_naive(sz, Fr::random_element());
506 }
507 }
508
509 // (10) Small scalars exercise the predicate-true path. The hoisted edge mask
510 // replaces the run-time `x == x` / `2·A + B == O` probes with a precomputed
511 // uint64; a regression here would either falsely set or falsely clear a bit
512 // and produce a wrong result against naive multiplication. Sweeping scalars
513 // with K2 = 0 hits all (a, b) recurrence states where |b| stays 0 — exactly
514 // the regime where Edge 1 / Edge 2 fire.
515 static void test_batch_mul_small_scalars_edge_predicate()
516 {
517 constexpr size_t num_points = 8;
519 points.reserve(num_points);
520 for (size_t i = 0; i < num_points; ++i) {
521 points.push_back(affine_element(element::random_element()));
522 }
523 // Cover [-32, 32] (Booth digits range ±1..±8 per window, so small scalars
524 // exercise every magnitude-comparison branch of edge_for_combined/_add).
525 for (int64_t s = -32; s <= 32; ++s) {
526 const Fr scalar = (s >= 0) ? Fr(static_cast<uint64_t>(s)) : -Fr(static_cast<uint64_t>(-s));
528 expected.reserve(num_points);
529 for (const auto& p : points) {
530 expected.push_back(affine_element(element(p) * scalar));
531 }
532 const std::vector<affine_element> result = element::batch_mul_with_endomorphism(points, scalar);
533 ASSERT_EQ(result.size(), expected.size());
534 for (size_t i = 0; i < num_points; ++i) {
535 EXPECT_EQ(result[i], expected[i]) << "scalar = " << s << ", point index = " << i;
536 }
537 }
538 }
539
540 static void test_batch_mul_randomized_matches_naive()
541 {
542 for (size_t trial = 0; trial < 24; ++trial) {
543 const size_t num_points = 1 + (trial % 19);
544 Fr scalar = Fr::random_element();
545 if ((trial % 8) == 0) {
546 scalar = Fr(static_cast<uint64_t>(trial + 1));
547 }
548 check_batch_mul_against_naive(num_points, scalar);
549 }
550 }
551
552 // === coverage for batch_two_round_fold (fused IPA SRS fold) ===
553
554 // Build a random 127-bit scalar, as produced by the IPA transcript for round challenges.
555 static Fr random_short_scalar()
556 {
557 const Fr full = Fr::random_element();
558 const Fr conv = full.from_montgomery_form();
559 return Fr(uint256_t{ conv.data[0], conv.data[1] & 0x7FFFFFFFFFFFFFFFULL, 0, 0 });
560 }
561
562 // Check the fused two-round fold against per-point projective arithmetic:
563 // out[i] = (u1·u2)·P[i] + u1·P[i+t] + u2·P[i+2t] + P[i+3t].
564 static void check_two_round_fold_against_naive(size_t t, const Fr& u1, const Fr& u2)
565 {
567 points.reserve(4 * t);
568 for (size_t i = 0; i < 4 * t; ++i) {
569 points.push_back(affine_element(element::random_element()));
570 }
571 const Fr u12 = u1 * u2;
573 expected.reserve(t);
574 for (size_t i = 0; i < t; ++i) {
575 element acc = element(points[i]) * u12;
576 acc += element(points[i + t]) * u1;
577 acc += element(points[i + 2 * t]) * u2;
578 acc += points[i + 3 * t];
579 expected.push_back(affine_element(acc));
580 }
581 const std::vector<affine_element> result = element::batch_two_round_fold(points, u1, u2);
582 ASSERT_EQ(result.size(), expected.size());
583 for (size_t i = 0; i < t; ++i) {
584 EXPECT_EQ(result[i], expected[i]) << "index " << i;
585 }
586 }
587
588 static void test_two_round_fold_random_challenges()
589 {
590 check_two_round_fold_against_naive(64, random_short_scalar(), random_short_scalar());
591 check_two_round_fold_against_naive(17, random_short_scalar(), random_short_scalar());
592 for (size_t t : { size_t{ 1 }, size_t{ 2 }, size_t{ 3 } }) {
593 check_two_round_fold_against_naive(t, random_short_scalar(), random_short_scalar());
594 }
595 }
596
597 // Small challenges produce low-magnitude Booth digit streams in which the running accumulator
598 // frequently equals ±(the next digit·base) — exactly the doubling/addition edge (shared
599 // x-coordinate, or a result at infinity) where the unsafe batch-affine formulas are invalid and
600 // the schedule must fall back to the safe (Jacobian) ops. The values below are chosen to land in
601 // that regime; the naive cross-check then guarantees the fallback path is itself correct.
602 static void test_two_round_fold_small_challenges()
603 {
604 for (uint64_t s1 : { 1ULL, 2ULL, 3ULL, 8ULL, 15ULL, 16ULL }) {
605 for (uint64_t s2 : { 1ULL, 2ULL, 4ULL, 7ULL, 16ULL }) {
606 check_two_round_fold_against_naive(4, Fr(s1), Fr(s2));
607 }
608 }
609 }
610
611 // The fused fold must agree with two sequential production folds:
612 // round 1: H = u1·G_lo + G_hi; round 2: u2·H_lo + H_hi.
613 static void test_two_round_fold_matches_sequential_folds()
614 {
615 constexpr size_t t = 32;
617 points.reserve(4 * t);
618 for (size_t i = 0; i < 4 * t; ++i) {
619 points.push_back(affine_element(element::random_element()));
620 }
621 const Fr u1 = random_short_scalar();
622 const Fr u2 = random_short_scalar();
623
625 element::batch_mul_with_endomorphism(std::span<const affine_element>(points.data(), 2 * t), u1);
626 element::batch_affine_add(std::span<affine_element>(round1.data(), 2 * t),
627 std::span<affine_element>(points.data() + 2 * t, 2 * t),
628 std::span<affine_element>(round1.data(), 2 * t));
630 element::batch_mul_with_endomorphism(std::span<const affine_element>(round1.data(), t), u2);
631 element::batch_affine_add(std::span<affine_element>(round2.data(), t),
632 std::span<affine_element>(round1.data() + t, t),
633 std::span<affine_element>(round2.data(), t));
634
635 const std::vector<affine_element> fused = element::batch_two_round_fold(points, u1, u2);
636 ASSERT_EQ(fused.size(), round2.size());
637 for (size_t i = 0; i < t; ++i) {
638 EXPECT_EQ(fused[i], round2[i]) << "index " << i;
639 }
640 }
641
642 static void test_frc_codec_round_trip()
643 {
644 using FrField = FrCodec::DataType;
645 affine_element point = affine_element::random_element();
648 affine_element::PUBLIC_INPUTS_SIZE);
649 auto reconstructed = FrCodec::deserialize_from_fields<affine_element>(limbs);
650 EXPECT_EQ(reconstructed, point);
651 }
652
653 // The point at infinity, the generator, and any scalar multiple of the generator must all be
654 // recognized as members of the prime-order subgroup.
655 static void test_is_in_prime_subgroup_accepts_subgroup_points()
656 {
657 EXPECT_TRUE(affine_element::infinity().is_in_prime_subgroup());
658 EXPECT_TRUE(affine_element::one().is_in_prime_subgroup());
659
660 for (size_t i = 0; i < 8; ++i) {
661 affine_element P = affine_element(element::random_element());
662 EXPECT_TRUE(P.is_in_prime_subgroup());
663 }
664 }
665};
666
667// using TestTypes = testing::Types<bb::g1>;
668using TestTypes = testing::Types<bb::g1, grumpkin::g1, secp256k1::g1, secp256r1::g1>;
669} // namespace
670
671TYPED_TEST_SUITE(TestAffineElement, TestTypes);
672
673TYPED_TEST(TestAffineElement, AddAffine)
674{
675 TestFixture::test_add_affine();
676}
677
678// Regression test for `element +/- affine_element` when the affine operand is the infinity sentinel.
679// Exercises both the large-modulus and small-modulus branches of `element::operator+=(affine)`.
680TYPED_TEST(TestAffineElement, MixedAddInfinityRegression)
681{
682 TestFixture::test_mixed_add_infinity_regression();
683}
684
685TYPED_TEST(TestAffineElement, ReadWrite)
686{
687 TestFixture::test_read_and_write();
688}
689
690TYPED_TEST(TestAffineElement, ReadWriteBuffer)
691{
692 TestFixture::test_read_write_buffer();
693 TestFixture::test_msgpack_serialization();
694}
695
696TYPED_TEST(TestAffineElement, PointCompression)
697{
698 if constexpr (TypeParam::Fq::modulus.data[3] >= MODULUS_TOP_LIMB_LARGE_THRESHOLD) {
699 GTEST_SKIP();
700 } else {
701 TestFixture::test_point_compression();
702 }
703}
704
705TYPED_TEST(TestAffineElement, FixedInfinityPoint)
706{
707 if constexpr (TypeParam::Fq::modulus.data[3] >= MODULUS_TOP_LIMB_LARGE_THRESHOLD) {
708 GTEST_SKIP();
709 } else {
710 TestFixture::test_fixed_point_at_infinity();
711 }
712}
713
714TYPED_TEST(TestAffineElement, PointCompressionUnsafe)
715{
716 if constexpr (TypeParam::Fq::modulus.data[3] >= MODULUS_TOP_LIMB_LARGE_THRESHOLD) {
717 TestFixture::test_point_compression_unsafe();
718 } else {
719 GTEST_SKIP();
720 }
721}
722
723TYPED_TEST(TestAffineElement, InfinityOrderingRegression)
724{
725 TestFixture::test_infinity_ordering_regression();
726}
727
728namespace bb::group_elements {
729// mul_with_endomorphism and mul_without_endomorphism are private in affine_element.
730// We could make those public to test or create other public utilities, but to keep the API intact we
731// instead mark TestElementPrivate as a friend class so that our test functions can have access.
733 public:
734 template <typename Element, typename Scalar>
735 static Element mul_without_endomorphism(const Element& element, const Scalar& scalar)
736 {
737 return element.mul_without_endomorphism(scalar);
738 }
739 template <typename Element, typename Scalar>
740 static Element mul_with_endomorphism(const Element& element, const Scalar& scalar)
741 {
742 return element.mul_with_endomorphism(scalar);
743 }
744};
745} // namespace bb::group_elements
746
747// Endomorphism-specialized multiplication should match generic multiplication on every curve that supports it.
748TYPED_TEST(TestAffineElement, MulWithEndomorphismMatchesMulWithoutEndomorphism)
749{
750 if constexpr (!TypeParam::USE_ENDOMORPHISM) {
751 GTEST_SKIP();
752 } else {
753 using element_t = typename TypeParam::element;
754 using Fr = typename TypeParam::Fr;
755 for (int i = 0; i < 100; i++) {
756 element_t x1(element_t::random_element());
757 Fr f1 = Fr::random_element();
760 EXPECT_EQ(r1, r2);
761 }
762 }
763}
764
765TYPED_TEST(TestAffineElement, MulWithEndomorphismEdgeCasesMatchMulWithoutEndomorphism)
766{
767 if constexpr (!TypeParam::USE_ENDOMORPHISM) {
768 GTEST_SKIP();
769 } else {
770 using element_t = typename TypeParam::element;
771 using Fr = typename TypeParam::Fr;
772
773 const element_t point(element_t::random_element());
774 std::vector<Fr> scalars;
775 scalars.reserve(96);
776 for (uint64_t i = 0; i <= 64; ++i) {
777 scalars.emplace_back(i);
778 }
779 scalars.push_back(-Fr::one());
780 for (const size_t bit : { 125UL, 126UL, 127UL }) {
781 const uint256_t power = uint256_t(1) << bit;
782 for (const uint64_t delta : { 0UL, 1UL, 2UL, 7UL, 8UL, 15UL, 16UL }) {
783 scalars.emplace_back(power + delta);
784 if (delta != 0) {
785 scalars.emplace_back(power - delta);
786 }
787 }
788 }
789
790 for (const Fr& scalar : scalars) {
791 const element_t expected = bb::group_elements::TestElementPrivate::mul_without_endomorphism(point, scalar);
792 EXPECT_EQ(bb::group_elements::TestElementPrivate::mul_with_endomorphism(point, scalar), expected);
793 EXPECT_EQ(point * scalar, expected);
794 EXPECT_EQ(point.mul_const_time(scalar), expected);
795 }
796 }
797}
798
799// mul_const_time must agree with operator* on every input, including edge cases (0, 1, n-1, low and
800// high Hamming weight).
801TYPED_TEST(TestAffineElement, MulConstTimeMatchesOperatorMul)
802{
803 using element_t = typename TypeParam::element;
804 using Fr = typename TypeParam::Fr;
805 element_t G(element_t::random_element());
806
807 // Edge-case scalars
808 for (Fr s : { Fr::zero(), Fr::one(), -Fr::one(), Fr(2), Fr(uint256_t(1) << 128) }) {
809 EXPECT_EQ(G.mul_const_time(s), G * s);
810 }
811 // Random scalars
812 for (int i = 0; i < 50; ++i) {
814 EXPECT_EQ(G.mul_const_time(s), G * s);
815 }
816}
817
818// FrCodec is defined only for BN254 and Grumpkin (the two curves whose points appear in transcripts).
819TYPED_TEST(TestAffineElement, FrCodecRoundTrip)
820{
822 TestFixture::test_frc_codec_round_trip();
823 } else {
824 GTEST_SKIP();
825 }
826}
827
828// Even scalars exercise zero and low-magnitude Booth digits in batch_mul_with_endomorphism.
829// The results should match ordinary point multiplication for every point in the batch.
830TYPED_TEST(TestAffineElement, BatchMulEndomorphismEvenScalars)
831{
832 if constexpr (!TypeParam::USE_ENDOMORPHISM) {
833 GTEST_SKIP();
834 } else {
835 TestFixture::test_batch_mul_endomorphism_even_scalars();
836 }
837}
838
839// Multiplication of a point at infinity by a scalar should be a point at infinity
840TYPED_TEST(TestAffineElement, InfinityMulByScalarIsInfinity)
841{
842 TestFixture::test_infinity_mul_by_scalar_is_infinity();
843}
844
845// Batched multiplication of points should match non-batched multiplication
846TYPED_TEST(TestAffineElement, BatchMulMatchesNonBatchMul)
847{
848 if constexpr (!TypeParam::USE_ENDOMORPHISM) {
849 GTEST_SKIP();
850 } else {
851 TestFixture::test_batch_mul_matches_non_batch_mul();
852 }
853}
854
855// Batched multiplication of a point at infinity by a scalar should result in points at infinity
856TYPED_TEST(TestAffineElement, InfinityBatchMulByScalarIsInfinity)
857{
858 if constexpr (!TypeParam::USE_ENDOMORPHISM) {
859 GTEST_SKIP();
860 } else {
861 TestFixture::test_infinity_batch_mul_by_scalar_is_infinity();
862 }
863}
864
865TYPED_TEST(TestAffineElement, BatchEndomoprhismByMinusOne)
866{
867 if constexpr (TypeParam::USE_ENDOMORPHISM) {
868 TestFixture::test_batch_endomorphism_by_minus_one();
869 } else {
870 GTEST_SKIP();
871 }
872}
873
874// Coverage of batch_mul_with_endomorphism — exercises the K1/K2-interleaved Booth
875// main loop's accumulator-init paths and the standard edge cases (thread-divisor
876// quirks, empty inputs, tiny inputs).
877TYPED_TEST(TestAffineElement, BatchMulZeroScalar)
878{
879 if constexpr (!TypeParam::USE_ENDOMORPHISM) {
880 GTEST_SKIP();
881 } else {
882 TestFixture::test_batch_mul_zero_scalar();
883 }
884}
885
886TYPED_TEST(TestAffineElement, BatchMulNumPointsNotMultipleOfThreads)
887{
888 if constexpr (!TypeParam::USE_ENDOMORPHISM) {
889 GTEST_SKIP();
890 } else {
891 TestFixture::test_batch_mul_num_points_not_multiple_of_threads();
892 }
893}
894
895// Fused two-round IPA SRS fold: out[i] = (u1·u2)·P[i] + u1·P[i+t] + u2·P[i+2t] + P[i+3t].
896TYPED_TEST(TestAffineElement, TwoRoundFoldRandomChallenges)
897{
898 if constexpr (!TypeParam::USE_ENDOMORPHISM) {
899 GTEST_SKIP();
900 } else {
901 TestFixture::test_two_round_fold_random_challenges();
902 }
903}
904
905TYPED_TEST(TestAffineElement, TwoRoundFoldSmallChallenges)
906{
907 if constexpr (!TypeParam::USE_ENDOMORPHISM) {
908 GTEST_SKIP();
909 } else {
910 TestFixture::test_two_round_fold_small_challenges();
911 }
912}
913
914TYPED_TEST(TestAffineElement, TwoRoundFoldMatchesSequentialFolds)
915{
916 if constexpr (!TypeParam::USE_ENDOMORPHISM) {
917 GTEST_SKIP();
918 } else {
919 TestFixture::test_two_round_fold_matches_sequential_folds();
920 }
921}
922
923TYPED_TEST(TestAffineElement, BatchMulScalarUnder127Bits)
924{
925 if constexpr (!TypeParam::USE_ENDOMORPHISM) {
926 GTEST_SKIP();
927 } else {
928 TestFixture::test_batch_mul_scalar_under_127_bits();
929 }
930}
931
932TYPED_TEST(TestAffineElement, BatchMulScalarLow127BitsZero)
933{
934 if constexpr (!TypeParam::USE_ENDOMORPHISM) {
935 GTEST_SKIP();
936 } else {
937 TestFixture::test_batch_mul_scalar_low_127_bits_zero();
938 }
939}
940
941TYPED_TEST(TestAffineElement, BatchMulK2128BitsNeverOccurs)
942{
943 if constexpr (!TypeParam::USE_ENDOMORPHISM) {
944 GTEST_SKIP();
945 } else {
946 TestFixture::test_batch_mul_k2_128_bits_never_occurs();
947 }
948}
949
950TYPED_TEST(TestAffineElement, BatchMulK2127Bits)
951{
952 if constexpr (!TypeParam::USE_ENDOMORPHISM) {
953 GTEST_SKIP();
954 } else {
955 TestFixture::test_batch_mul_k2_127_bits();
956 }
957}
958
959TYPED_TEST(TestAffineElement, BatchMulK2126Bits)
960{
961 if constexpr (!TypeParam::USE_ENDOMORPHISM) {
962 GTEST_SKIP();
963 } else {
964 TestFixture::test_batch_mul_k2_126_bits();
965 }
966}
967
968TYPED_TEST(TestAffineElement, BatchMulK2125Bits)
969{
970 if constexpr (!TypeParam::USE_ENDOMORPHISM) {
971 GTEST_SKIP();
972 } else {
973 TestFixture::test_batch_mul_k2_125_bits();
974 }
975}
976
977TYPED_TEST(TestAffineElement, BatchMulEmptyInput)
978{
979 if constexpr (!TypeParam::USE_ENDOMORPHISM) {
980 GTEST_SKIP();
981 } else {
982 TestFixture::test_batch_mul_empty_input();
983 }
984}
985
986TYPED_TEST(TestAffineElement, BatchMulSizeLessThanNumThreads)
987{
988 if constexpr (!TypeParam::USE_ENDOMORPHISM) {
989 GTEST_SKIP();
990 } else {
991 TestFixture::test_batch_mul_size_less_than_num_threads();
992 }
993}
994
995TYPED_TEST(TestAffineElement, BatchMulRandomizedMatchesNaive)
996{
997 if constexpr (!TypeParam::USE_ENDOMORPHISM) {
998 GTEST_SKIP();
999 } else {
1000 TestFixture::test_batch_mul_randomized_matches_naive();
1001 }
1002}
1003
1004TYPED_TEST(TestAffineElement, BatchMulSmallScalarsEdgePredicate)
1005{
1006 if constexpr (!TypeParam::USE_ENDOMORPHISM) {
1007 GTEST_SKIP();
1008 } else {
1009 TestFixture::test_batch_mul_small_scalars_edge_predicate();
1010 }
1011}
1012
1013// Verify that serialize_from_buffer rejects off-curve bytes by throwing (tests the invalid-curve attack fix).
1014TYPED_TEST(TestAffineElement, DeserializeOffCurveThrows)
1015{
1016 TestFixture::test_deserialize_off_curve_throws();
1017}
1018
1019// Verify is_in_prime_subgroup accepts known prime-order subgroup points
1020TYPED_TEST(TestAffineElement, IsInPrimeSubgroupAcceptsSubgroupPoints)
1021{
1022 TestFixture::test_is_in_prime_subgroup_accepts_subgroup_points();
1023}
1024
1025// Verify that from_compressed returns the (0,0) sentinel for x values with no valid y.
1026TYPED_TEST(TestAffineElement, PointCompressionInvalidX)
1027{
1028 if constexpr (TypeParam::Fq::modulus.data[3] >= MODULUS_TOP_LIMB_LARGE_THRESHOLD) {
1029 GTEST_SKIP(); // from_compressed is not used on large-modulus curves
1030 } else {
1031 TestFixture::test_point_compression_invalid_x();
1032 }
1033}
1034
1035// Regression test: from_compressed must reject non-canonical x >= modulus.
1036TYPED_TEST(TestAffineElement, PointCompressionNonCanonicalX)
1037{
1038 if constexpr (TypeParam::Fq::modulus.data[3] >= MODULUS_TOP_LIMB_LARGE_THRESHOLD) {
1039 GTEST_SKIP(); // from_compressed is not used on large-modulus curves
1040 } else {
1041 TestFixture::test_point_compression_non_canonical_x();
1042 }
1043}
1044
1045TEST(AffineElement, HashToCurve)
1046{
1048 test_vectors.emplace_back(std::vector<uint8_t>(),
1050 fr(uint256_t("24c4cb9c1206ab5470592f237f1698abe684dadf0ab4d7a132c32b2134e2c12e")),
1051 fr(uint256_t("0668b8d61a317fb34ccad55c930b3554f1828a0e5530479ecab4defe6bbc0b2e"))));
1052
1053 test_vectors.emplace_back(std::vector<uint8_t>{ 1 },
1055 fr(uint256_t("107f1b633c6113f3222f39f6256f0546b41a4880918c86864b06471afb410454")),
1056 fr(uint256_t("050cd3823d0c01590b6a50adcc85d2ee4098668fd28805578aa05a423ea938c6"))));
1057
1058 // "hello world"
1059 test_vectors.emplace_back(std::vector<uint8_t>{ 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64 },
1061 fr(uint256_t("037c5c229ae495f6e8d1b4bf7723fafb2b198b51e27602feb8a4d1053d685093")),
1062 fr(uint256_t("10cf9596c5b2515692d930efa2cf3817607e4796856a79f6af40c949b066969f"))));
1063
1064 for (std::tuple<std::vector<uint8_t>, grumpkin::g1::affine_element> test_case : test_vectors) {
1065 auto result = grumpkin::g1::affine_element::hash_to_curve(std::get<0>(test_case), 0);
1066 auto expected_result = std::get<1>(test_case);
1068 EXPECT_TRUE(result == expected_result);
1069 }
1070}
#define EXPECT_THROW_OR_ABORT(statement, matcher)
Definition assert.hpp:223
static std::vector< fr > serialize_to_fields(const T &val)
Conversion from transcript values to bb::frs.
static Element mul_without_endomorphism(const Element &element, const Scalar &scalar)
static Element mul_with_endomorphism(const Element &element, const Scalar &scalar)
element class. Implements ecc group arithmetic using Jacobian coordinates See https://hyperelliptic....
Definition element.hpp:35
element mul_with_endomorphism(const Fr &scalar) const noexcept
element mul_without_endomorphism(const Fr &scalar) const noexcept
group_elements::affine_element< Fq, Fr, Params > affine_element
Definition group.hpp:44
constexpr bool get_bit(uint64_t bit_index) const
bool expected_result
#define G(r, i, a, b, c, d)
Definition blake2s.cpp:116
test_vector test_vectors[]
const size_t num_points
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...
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
void read(B &it, field2< base_field, Params > &value)
TYPED_TEST_SUITE(CommitmentKeyTest, Curves)
field< Bn254FrParams > fr
Definition fr.hpp:155
void write(B &buf, field2< base_field, Params > const &value)
TYPED_TEST(CommitmentKeyTest, CommitToZeroPoly)
TEST(BoomerangMegaCircuitBuilder, BasicCircuit)
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
testing::Types< VKTestParams< UltraFlavor, stdlib::recursion::honk::DefaultIO< UltraCircuitBuilder > >, VKTestParams< UltraFlavor, stdlib::recursion::honk::RollupIO >, VKTestParams< UltraKeccakFlavor, stdlib::recursion::honk::DefaultIO< UltraCircuitBuilder > >, VKTestParams< MegaFlavor, stdlib::recursion::honk::DefaultIO< MegaCircuitBuilder > > > TestTypes
bb::VectorAffineElementPushSpan< BaseParams > lhs
bb::VectorAffineElementPushSpan< BaseParams > rhs
Curve::Element Element
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.
static field random_element(numeric::RNG *engine=nullptr) noexcept
BB_INLINE constexpr bool is_zero() const noexcept
BB_INLINE constexpr field from_montgomery_form() const noexcept
static constexpr field zero()
void throw_or_abort(std::string const &err)
VectorField result