Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
scalar_multiplication.test.cpp
Go to the documentation of this file.
10#include <filesystem>
11#include <gtest/gtest.h>
12
13using namespace bb;
14
15namespace {
17} // namespace
18
19template <class Curve> class ScalarMultiplicationTest : public ::testing::Test {
20 public:
21 using Group = typename Curve::Group;
22 using Element = typename Curve::Element;
25
26 static constexpr size_t num_points = 31013;
27
28 // Bounds used by test_batch_multi_scalar_mul. Kept small so num_points (and therefore
29 // SetUpTestSuite, which builds num_points random EC points) stays cheap — especially under wasm,
30 // where the fixture build previously dominated the whole ecc_tests run.
31 static constexpr size_t kMaxBatchMSMs = 32;
32 static constexpr size_t kMaxBatchPointsPerMSM = 400;
33
34 // Used by test_consume_point_batch{,_and_accumulate}, which read generators[0..kMaxBucketTestPoints).
35 static constexpr size_t kMaxBucketTestPoints = 30071;
36
37 // Pinning invariants: these tests walk generators[]/scalars[] without bounds checks beyond an
38 // occasional runtime ASSERT_LT. Pin the relationships at compile time so changing any one of
39 // these constants in isolation cannot regress into an out-of-bounds walk.
41 "test_batch_multi_scalar_mul can exceed num_points; "
42 "raise num_points or lower kMaxBatchMSMs / kMaxBatchPointsPerMSM");
43 static_assert(kMaxBucketTestPoints <= num_points,
44 "test_consume_point_batch* reads past end of generators; "
45 "raise num_points or lower kMaxBucketTestPoints");
46
47 static inline std::vector<AffineElement> generators{};
48 static inline std::vector<ScalarField> scalars{};
49
50 static AffineElement naive_msm(std::span<ScalarField> input_scalars, std::span<const AffineElement> input_points)
51 {
52 size_t total_points = input_scalars.size();
53 size_t num_threads = get_num_cpus();
54 std::vector<Element> expected_accs(num_threads);
55 size_t range_per_thread = (total_points + num_threads - 1) / num_threads;
56 parallel_for(num_threads, [&](size_t thread_idx) {
57 Element expected_thread_acc;
58 expected_thread_acc.self_set_infinity();
59 size_t start = thread_idx * range_per_thread;
60 size_t end = ((thread_idx + 1) * range_per_thread > total_points) ? total_points
61 : (thread_idx + 1) * range_per_thread;
62 bool skip = start >= total_points;
63 if (!skip) {
64 for (size_t i = start; i < end; ++i) {
65 expected_thread_acc += input_points[i] * input_scalars[i];
66 }
67 }
68 expected_accs[thread_idx] = expected_thread_acc;
69 });
70
71 Element expected_acc = Element();
72 expected_acc.self_set_infinity();
73 for (auto& acc : expected_accs) {
74 expected_acc += acc;
75 }
76 return AffineElement(expected_acc);
77 }
78
79 static void SetUpTestSuite()
80 {
81 generators.resize(num_points);
82 scalars.resize(num_points);
83 parallel_for_range(num_points, [&](size_t start, size_t end) {
84 for (size_t i = start; i < end; ++i) {
85 generators[i] = Group::one * Curve::ScalarField::random_element(&engine);
86 scalars[i] = Curve::ScalarField::random_element(&engine);
87 }
88 });
89 for (size_t i = 0; i < num_points - 1; ++i) {
90 ASSERT_EQ(generators[i].x == generators[i + 1].x, false);
91 }
92 };
93
94 // ======================= Test Methods =======================
95
97 {
98 constexpr uint32_t fr_size = 254;
99 constexpr uint32_t slice_bits = 7;
100 constexpr uint32_t num_slices = (fr_size + 6) / 7;
101 constexpr uint32_t last_slice_bits = fr_size - ((num_slices - 1) * slice_bits);
102
103 for (size_t x = 0; x < 100; ++x) {
104 uint256_t input_u256 = engine.get_random_uint256();
105 input_u256.data[3] = input_u256.data[3] & 0x3FFFFFFFFFFFFFFF; // 254 bits
106 while (input_u256 > ScalarField::modulus) {
107 input_u256 -= ScalarField::modulus;
108 }
109 std::vector<uint32_t> slices(num_slices);
110
111 uint256_t acc = input_u256;
112 for (uint32_t i = 0; i < num_slices; ++i) {
113 uint32_t mask = ((1U << slice_bits) - 1U);
114 uint32_t shift = slice_bits;
115 if (i == 0) {
116 mask = ((1U << last_slice_bits) - 1U);
117 shift = last_slice_bits;
118 }
119 slices[num_slices - 1 - i] = static_cast<uint32_t>((acc & mask).data[0]);
120 acc = acc >> shift;
121 }
122
123 ScalarField input(input_u256);
124 input.self_from_montgomery_form_reduced();
125
126 ASSERT_EQ(input.data[0], input_u256.data[0]);
127 ASSERT_EQ(input.data[1], input_u256.data[1]);
128 ASSERT_EQ(input.data[2], input_u256.data[2]);
129 ASSERT_EQ(input.data[3], input_u256.data[3]);
130
131 for (uint32_t i = 0; i < num_slices; ++i) {
132 uint32_t result = scalar_multiplication::MSM<Curve>::get_scalar_slice(input, i, slice_bits);
133 EXPECT_EQ(result, slices[i]);
134 }
135 }
136 }
137
139 {
140 const size_t total_points = kMaxBucketTestPoints;
141 const size_t num_buckets = 128;
142
143 std::vector<uint64_t> input_point_schedule;
144 for (size_t i = 0; i < total_points; ++i) {
145 uint64_t bucket = static_cast<uint64_t>(engine.get_random_uint8()) & 0x7f;
146 uint64_t schedule = static_cast<uint64_t>(bucket) + (static_cast<uint64_t>(i) << 32);
147 input_point_schedule.push_back(schedule);
148 }
150 typename scalar_multiplication::MSM<Curve>::BucketAccumulators bucket_data(num_buckets);
152 input_point_schedule, generators, affine_data, bucket_data);
153
154 std::vector<Element> expected_buckets(num_buckets);
155 for (auto& e : expected_buckets) {
156 e.self_set_infinity();
157 }
158 for (size_t i = 0; i < total_points; ++i) {
159 uint64_t bucket = input_point_schedule[i] & 0xFFFFFFFF;
160 EXPECT_LT(static_cast<size_t>(bucket), num_buckets);
161 expected_buckets[static_cast<size_t>(bucket)] += generators[i];
162 }
163 for (size_t i = 0; i < num_buckets; ++i) {
164 if (!expected_buckets[i].is_point_at_infinity()) {
165 AffineElement expected(expected_buckets[i]);
166 EXPECT_EQ(expected, bucket_data.buckets[i]);
167 } else {
168 EXPECT_FALSE(bucket_data.bucket_exists.get(i));
169 }
170 }
171 }
172
174 {
175 const size_t total_points = kMaxBucketTestPoints;
176 const size_t num_buckets = 128;
177
178 std::vector<uint64_t> input_point_schedule;
179 for (size_t i = 0; i < total_points; ++i) {
180 uint64_t bucket = static_cast<uint64_t>(engine.get_random_uint8()) & 0x7f;
181 uint64_t schedule = static_cast<uint64_t>(bucket) + (static_cast<uint64_t>(i) << 32);
182 input_point_schedule.push_back(schedule);
183 }
185 typename scalar_multiplication::MSM<Curve>::BucketAccumulators bucket_data(num_buckets);
187 input_point_schedule, generators, affine_data, bucket_data);
188
190
191 Element expected_acc;
192 expected_acc.self_set_infinity();
193 size_t num_threads = get_num_cpus();
194 std::vector<Element> expected_accs(num_threads);
195 size_t range_per_thread = (total_points + num_threads - 1) / num_threads;
196 parallel_for(num_threads, [&](size_t thread_idx) {
197 Element expected_thread_acc;
198 expected_thread_acc.self_set_infinity();
199 size_t start = thread_idx * range_per_thread;
200 size_t end = (thread_idx == num_threads - 1) ? total_points : (thread_idx + 1) * range_per_thread;
201 bool skip = start >= total_points;
202 if (!skip) {
203 for (size_t i = start; i < end; ++i) {
204 ScalarField scalar = input_point_schedule[i] & 0xFFFFFFFF;
205 expected_thread_acc += generators[i] * scalar;
206 }
207 }
208 expected_accs[thread_idx] = expected_thread_acc;
209 });
210
211 for (size_t i = 0; i < num_threads; ++i) {
212 expected_acc += expected_accs[i];
213 }
214 AffineElement expected(expected_acc);
215 EXPECT_EQ(AffineElement(result), expected);
216 }
217
219 {
220 const size_t total_points = 30071;
221
222 std::vector<uint64_t> input_point_schedule;
223 for (size_t i = 0; i < total_points; ++i) {
224 uint64_t bucket = static_cast<uint64_t>(engine.get_random_uint8()) & 0x7f;
225 uint64_t schedule = static_cast<uint64_t>(bucket) + (static_cast<uint64_t>(i) << 32);
226 input_point_schedule.push_back(schedule);
227 }
228
230 &input_point_schedule[0], input_point_schedule.size(), 7);
231
232 // Verify zero entry count is correct
233 size_t expected = 0;
234 for (size_t i = 0; i < total_points; ++i) {
235 expected += static_cast<size_t>((input_point_schedule[i] & 0xFFFFFFFF) == 0);
236 }
237 EXPECT_EQ(result, expected);
238
239 // Verify the array is sorted by bucket index (lower 32 bits)
240 for (size_t i = 1; i < total_points; ++i) {
241 uint32_t prev_bucket = static_cast<uint32_t>(input_point_schedule[i - 1]);
242 uint32_t curr_bucket = static_cast<uint32_t>(input_point_schedule[i]);
243 EXPECT_LE(prev_bucket, curr_bucket) << "Array not sorted at index " << i;
244 }
245 }
246
247 // Regression test: radix sort zero-counting bug for bucket_index_bits > 16 (3+ recursion levels).
248 // The recursive call passes `keys` instead of `top_level_keys`, causing num_zero_entries to be
249 // overwritten by non-zero-bucket counts when the MSD radix sort recurses 3+ levels deep.
251 {
252 // Use bucket_index_bits = 17, which pads to 24 bits → 3 recursion levels (shift: 16→8→0).
253 // At the 3rd level, the top_level_keys bug causes zero-counting to fire for every
254 // level-0 bucket's sub-bucket-0, not just the bucket-0 chain.
255 constexpr uint32_t bucket_index_bits = 17;
256 constexpr size_t num_entries = 1000;
257
258 std::vector<uint64_t> schedule(num_entries);
259
260 // Place some entries with bucket_index = 0 (true zero-bucket entries)
261 const size_t num_true_zeros = 10;
262 for (size_t i = 0; i < num_true_zeros; ++i) {
263 schedule[i] = static_cast<uint64_t>(i) << 32; // point_index=i, bucket_index=0
264 }
265
266 // Place entries with bucket_index = 65536 (= 1 << 16). These have bits [0:16) all zero,
267 // so the buggy code counts them as zero-bucket entries after the final recursion level
268 // overwrites num_zero_entries from the level-0 bucket 1 path.
269 const size_t num_false_zeros = 20;
270 for (size_t i = 0; i < num_false_zeros; ++i) {
271 size_t idx = num_true_zeros + i;
272 schedule[idx] = (static_cast<uint64_t>(idx) << 32) | 65536ULL;
273 }
274
275 // Fill remaining entries with random non-zero bucket indices that won't confuse the count
276 for (size_t i = num_true_zeros + num_false_zeros; i < num_entries; ++i) {
277 uint32_t bucket = (engine.get_random_uint32() % ((1U << bucket_index_bits) - 1)) + 1;
278 // Avoid bucket_index values with all lower 16 bits zero (i.e., multiples of 65536)
279 if ((bucket & 0xFFFF) == 0) {
280 bucket |= 1;
281 }
282 schedule[i] = (static_cast<uint64_t>(i) << 32) | static_cast<uint64_t>(bucket);
283 }
284
286 schedule.data(), num_entries, bucket_index_bits);
287
288 // Count actual zero-bucket entries after sort
289 size_t expected = 0;
290 for (size_t i = 0; i < num_entries; ++i) {
291 if ((schedule[i] & scalar_multiplication::BUCKET_INDEX_MASK) == 0) {
292 expected++;
293 }
294 }
295
296 EXPECT_EQ(result, expected) << "Zero-bucket count is wrong for bucket_index_bits=" << bucket_index_bits
297 << ". Got " << result << ", expected " << expected
298 << " (likely overwritten by count from a non-zero bucket)";
299
300 // Also verify the array is sorted
301 for (size_t i = 1; i < num_entries; ++i) {
302 uint32_t prev = static_cast<uint32_t>(schedule[i - 1]);
303 uint32_t curr = static_cast<uint32_t>(schedule[i]);
304 EXPECT_LE(prev, curr) << "Array not sorted at index " << i;
305 }
306 }
307
309 {
310 std::span<ScalarField> test_scalars(&scalars[0], num_points);
311 AffineElement result =
313 AffineElement expected = naive_msm(test_scalars, generators);
314 EXPECT_EQ(result, expected);
315 }
316
318 {
319 BB_BENCH_NAME("BatchMultiScalarMul");
320
321 const size_t num_msms = static_cast<size_t>(engine.get_random_uint8()) % kMaxBatchMSMs;
322 std::vector<AffineElement> expected(num_msms);
323
324 std::vector<std::vector<ScalarField>> batch_scalars_copies(num_msms);
326 std::vector<std::span<ScalarField>> batch_scalars_spans;
327
328 size_t vector_offset = 0;
329 for (size_t k = 0; k < num_msms; ++k) {
330 const size_t num_pts = static_cast<size_t>(engine.get_random_uint16()) % kMaxBatchPointsPerMSM;
331
332 ASSERT_LT(vector_offset + num_pts, num_points);
333 std::span<const AffineElement> batch_points(&generators[vector_offset], num_pts);
334
335 batch_scalars_copies[k].resize(num_pts);
336 for (size_t i = 0; i < num_pts; ++i) {
337 batch_scalars_copies[k][i] = scalars[vector_offset + i];
338 }
339
340 vector_offset += num_pts;
341 batch_points_span.push_back(batch_points);
342 batch_scalars_spans.push_back(batch_scalars_copies[k]);
343
344 expected[k] = naive_msm(batch_scalars_spans[k], batch_points_span[k]);
345 }
346
347 std::vector<AffineElement> result =
348 scalar_multiplication::MSM<Curve>::batch_multi_scalar_mul(batch_points_span, batch_scalars_spans);
349
350 EXPECT_EQ(result, expected);
351 }
352
354 {
355 const size_t num_msms = 10;
356 std::vector<AffineElement> expected(num_msms);
357
358 std::vector<std::vector<ScalarField>> batch_scalars(num_msms);
360 std::vector<std::span<ScalarField>> batch_scalars_spans;
361
362 for (size_t k = 0; k < num_msms; ++k) {
363 const size_t num_pts = 33;
364 auto& test_scalars = batch_scalars[k];
365
366 test_scalars.resize(num_pts);
367
368 size_t fixture_offset = k * num_pts;
369
370 std::span<AffineElement> batch_points(&generators[fixture_offset], num_pts);
371 for (size_t i = 0; i < 13; ++i) {
372 test_scalars[i] = 0;
373 }
374 for (size_t i = 13; i < 23; ++i) {
375 test_scalars[i] = scalars[fixture_offset + i + 13];
376 }
377 for (size_t i = 23; i < num_pts; ++i) {
378 test_scalars[i] = 0;
379 }
380 batch_points_span.push_back(batch_points);
381 batch_scalars_spans.push_back(batch_scalars[k]);
382
383 expected[k] = naive_msm(batch_scalars[k], batch_points);
384 }
385
386 std::vector<AffineElement> result =
387 scalar_multiplication::MSM<Curve>::batch_multi_scalar_mul(batch_points_span, batch_scalars_spans);
388
389 EXPECT_EQ(result, expected);
390 }
391
392 void test_msm()
393 {
394 const size_t start_index = 1234;
395 const size_t num_pts = num_points - start_index;
396
397 PolynomialSpan<ScalarField> scalar_span =
400
401 std::span<AffineElement> points(&generators[start_index], num_pts);
402 AffineElement expected = naive_msm(scalar_span.span, points);
403 EXPECT_EQ(result, expected);
404 }
405
407 {
408 const size_t start_index = 1234;
409 const size_t num_pts = num_points - start_index;
410 std::vector<ScalarField> test_scalars(num_pts, ScalarField::zero());
411
412 PolynomialSpan<ScalarField> scalar_span = PolynomialSpan<ScalarField>(start_index, test_scalars);
414
415 EXPECT_EQ(result, Group::affine_point_at_infinity);
416 }
417
419 {
420 std::vector<ScalarField> test_scalars;
421 std::vector<AffineElement> input_points;
422 PolynomialSpan<ScalarField> scalar_span = PolynomialSpan<ScalarField>(0, test_scalars);
423 AffineElement result = scalar_multiplication::MSM<Curve>::msm(input_points, scalar_span);
424
425 EXPECT_EQ(result, Group::affine_point_at_infinity);
426 }
427
429 {
430 const size_t num_pts = 100;
431 std::vector<ScalarField> test_scalars(num_pts);
432 std::vector<ScalarField> scalars_copy(num_pts);
433
434 for (size_t i = 0; i < num_pts; ++i) {
435 test_scalars[i] = scalars[i];
436 scalars_copy[i] = test_scalars[i];
437 }
438
439 std::span<const AffineElement> points(&generators[0], num_pts);
440 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
441
442 scalar_multiplication::MSM<Curve>::msm(points, scalar_span);
443
444 for (size_t i = 0; i < num_pts; ++i) {
445 EXPECT_EQ(test_scalars[i], scalars_copy[i]) << "Scalar at index " << i << " was modified";
446 }
447 }
448
450 {
451 const size_t num_msms = 3;
452 const size_t num_pts = 100;
453
454 std::vector<std::vector<ScalarField>> batch_scalars(num_msms);
455 std::vector<std::vector<ScalarField>> scalars_copies(num_msms);
457 std::vector<std::span<ScalarField>> batch_scalar_spans;
458
459 for (size_t k = 0; k < num_msms; ++k) {
460 batch_scalars[k].resize(num_pts);
461 scalars_copies[k].resize(num_pts);
462
463 for (size_t i = 0; i < num_pts; ++i) {
464 batch_scalars[k][i] = scalars[k * num_pts + i];
465 scalars_copies[k][i] = batch_scalars[k][i];
466 }
467
468 batch_points.push_back(std::span<const AffineElement>(&generators[k * num_pts], num_pts));
469 batch_scalar_spans.push_back(batch_scalars[k]);
470 }
471
472 scalar_multiplication::MSM<Curve>::batch_multi_scalar_mul(batch_points, batch_scalar_spans);
473
474 for (size_t k = 0; k < num_msms; ++k) {
475 for (size_t i = 0; i < num_pts; ++i) {
476 EXPECT_EQ(batch_scalars[k][i], scalars_copies[k][i])
477 << "Scalar at MSM " << k << ", index " << i << " was modified";
478 }
479 }
480 }
481
483 {
484 const size_t num_pts = 5;
485 std::vector<ScalarField> test_scalars(num_pts, ScalarField::one());
486 std::span<const AffineElement> points(&generators[0], num_pts);
487
488 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
489 AffineElement result = scalar_multiplication::MSM<Curve>::msm(points, scalar_span);
490
491 Element expected;
492 expected.self_set_infinity();
493 for (size_t i = 0; i < num_pts; ++i) {
494 expected += points[i];
495 }
496
497 EXPECT_EQ(result, AffineElement(expected));
498 }
499
501 {
502 const size_t num_pts = 5;
503 std::vector<ScalarField> test_scalars(num_pts, -ScalarField::one());
504 std::span<const AffineElement> points(&generators[0], num_pts);
505
506 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
507 AffineElement result = scalar_multiplication::MSM<Curve>::msm(points, scalar_span);
508
509 Element expected;
510 expected.self_set_infinity();
511 for (size_t i = 0; i < num_pts; ++i) {
512 expected -= points[i];
513 }
514
515 EXPECT_EQ(result, AffineElement(expected));
516 }
517
519 {
520 std::vector<ScalarField> test_scalars = { scalars[0] };
521 std::span<const AffineElement> points(&generators[0], 1);
522
523 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
524 AffineElement result = scalar_multiplication::MSM<Curve>::msm(points, scalar_span);
525
526 AffineElement expected(points[0] * test_scalars[0]);
527 EXPECT_EQ(result, expected);
528 }
529
531 {
532 std::vector<size_t> test_sizes = { 1, 2, 15, 16, 17, 50, 127, 128, 129, 256, 512 };
533
534 for (size_t num_pts : test_sizes) {
535 ASSERT_LE(num_pts, num_points);
536
537 std::vector<ScalarField> test_scalars(num_pts);
538 for (size_t i = 0; i < num_pts; ++i) {
539 test_scalars[i] = scalars[i];
540 }
541
542 std::span<const AffineElement> points(&generators[0], num_pts);
543 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
544
545 AffineElement result = scalar_multiplication::MSM<Curve>::msm(points, scalar_span);
546 AffineElement expected = naive_msm(test_scalars, points);
547
548 EXPECT_EQ(result, expected) << "Failed for size " << num_pts;
549 }
550 }
551
553 {
554 // Use enough points to trigger Pippenger (> PIPPENGER_THRESHOLD = 16)
555 const size_t num_pts = 32;
556 AffineElement base_point = generators[0];
557
558 std::vector<AffineElement> points(num_pts, base_point);
559 std::vector<ScalarField> test_scalars(num_pts);
560 ScalarField scalar_sum = ScalarField::zero();
561
562 for (size_t i = 0; i < num_pts; ++i) {
563 test_scalars[i] = scalars[i];
564 scalar_sum += test_scalars[i];
565 }
566
567 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
568 // Duplicate points are an edge case (P + P requires doubling, not addition).
569 // Must use handle_edge_cases=true for correctness with Pippenger.
570 AffineElement result = scalar_multiplication::MSM<Curve>::msm(points, scalar_span, /*handle_edge_cases=*/true);
571
572 AffineElement expected(base_point * scalar_sum);
573 EXPECT_EQ(result, expected);
574 }
575
577 {
578 const size_t num_pts = 100;
579 std::vector<ScalarField> test_scalars(num_pts);
580 Element expected;
581 expected.self_set_infinity();
582
583 for (size_t i = 0; i < num_pts; ++i) {
584 if (i % 2 == 0) {
585 test_scalars[i] = ScalarField::zero();
586 } else {
587 test_scalars[i] = scalars[i];
588 expected += generators[i] * test_scalars[i];
589 }
590 }
591
592 std::span<const AffineElement> points(&generators[0], num_pts);
593 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
594
595 AffineElement result = scalar_multiplication::MSM<Curve>::msm(points, scalar_span);
596 EXPECT_EQ(result, AffineElement(expected));
597 }
598
600 {
601 const size_t num_pts = 200;
602 std::vector<ScalarField> test_scalars(num_pts);
603 for (size_t i = 0; i < num_pts; ++i) {
604 test_scalars[i] = scalars[i];
605 }
606
607 std::span<const AffineElement> points(&generators[0], num_pts);
608 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
609
610 auto result = scalar_multiplication::pippenger<Curve>(scalar_span, points);
611
612 AffineElement expected = naive_msm(test_scalars, points);
613 EXPECT_EQ(AffineElement(result), expected);
614 }
615
617 {
618 const size_t num_pts = 200;
619 std::vector<ScalarField> test_scalars(num_pts);
620 for (size_t i = 0; i < num_pts; ++i) {
621 test_scalars[i] = scalars[i];
622 }
623
624 std::span<const AffineElement> points(&generators[0], num_pts);
625 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
626
627 auto result = scalar_multiplication::pippenger_unsafe<Curve>(scalar_span, points);
628
629 AffineElement expected = naive_msm(test_scalars, points);
630 EXPECT_EQ(AffineElement(result), expected);
631 }
632};
633
634using CurveTypes = ::testing::Types<bb::curve::BN254, bb::curve::Grumpkin>;
636
637// ======================= Test Wrappers =======================
638
640{
641 this->test_get_scalar_slice();
642}
644{
645 this->test_consume_point_batch();
646}
647TYPED_TEST(ScalarMultiplicationTest, ConsumePointBatchAndAccumulate)
648{
649 this->test_consume_point_batch_and_accumulate();
650}
651TYPED_TEST(ScalarMultiplicationTest, RadixSortCountZeroEntries)
652{
653 this->test_radix_sort_count_zero_entries();
654}
655TYPED_TEST(ScalarMultiplicationTest, RadixSortCountZeroEntriesWideBuckets)
656{
657 this->test_radix_sort_count_zero_entries_wide_buckets();
658}
660{
661 this->test_pippenger_low_memory();
662}
664{
665 this->test_batch_multi_scalar_mul();
666}
667TYPED_TEST(ScalarMultiplicationTest, BatchMultiScalarMulSparse)
668{
669 this->test_batch_multi_scalar_mul_sparse();
670}
672{
673 this->test_msm();
674}
676{
677 this->test_msm_all_zeroes();
678}
680{
681 this->test_msm_empty_polynomial();
682}
683TYPED_TEST(ScalarMultiplicationTest, ScalarsUnchangedAfterMSM)
684{
685 this->test_scalars_unchanged_after_msm();
686}
687TYPED_TEST(ScalarMultiplicationTest, ScalarsUnchangedAfterBatchMultiScalarMul)
688{
689 this->test_scalars_unchanged_after_batch_multi_scalar_mul();
690}
692{
693 this->test_scalar_one();
694}
696{
697 this->test_scalar_minus_one();
698}
700{
701 this->test_single_point();
702}
704{
705 this->test_size_thresholds();
706}
708{
709 this->test_duplicate_points();
710}
712{
713 this->test_mixed_zero_scalars();
714}
716{
717 this->test_pippenger_free_function();
718}
719TYPED_TEST(ScalarMultiplicationTest, PippengerUnsafeFreeFunction)
720{
721 this->test_pippenger_unsafe_free_function();
722}
723
724// Curve-independent unit tests for the work-unit partitioner.
725// partition_by_weight is the load-bearing balancing logic in get_work_units; pinning its
726// behavior with synthetic weights makes regressions in the partition algorithm visible
727// without needing a full MSM run.
728namespace {
729
731using WorkUnit = PartitionMSM::MSMWorkUnit;
732
733// Total weight assigned to a thread (sum of WorkUnit sizes weighted by the input vector).
734size_t thread_weight(const std::vector<WorkUnit>& units, const std::vector<std::vector<uint16_t>>& weights)
735{
736 size_t total = 0;
737 for (const auto& u : units) {
738 for (size_t k = 0; k < u.size; ++k) {
739 total += weights[u.batch_msm_index][u.start_index + k];
740 }
741 }
742 return total;
743}
744
745} // namespace
746
747TEST(PartitionByWeight, NoMsmsReturnsEmptyThreads)
748{
749 auto units = PartitionMSM::partition_by_weight({}, 8);
750 ASSERT_EQ(units.size(), 8U);
751 for (const auto& t : units) {
752 EXPECT_TRUE(t.empty());
753 }
754}
755
756TEST(PartitionByWeight, AllEmptyMsmsReturnsEmptyThreads)
757{
758 std::vector<std::vector<uint16_t>> weights{ {}, {}, {} };
759 auto units = PartitionMSM::partition_by_weight(weights, 4);
760 ASSERT_EQ(units.size(), 4U);
761 for (const auto& t : units) {
762 EXPECT_TRUE(t.empty());
763 }
764}
765
766TEST(PartitionByWeight, SingleThreadGetsEverything)
767{
768 std::vector<std::vector<uint16_t>> weights{ { 5, 5, 5, 5, 5 } };
769 auto units = PartitionMSM::partition_by_weight(weights, 1);
770 ASSERT_EQ(units.size(), 1U);
771 ASSERT_EQ(units[0].size(), 1U);
772 EXPECT_EQ(units[0][0].batch_msm_index, 0U);
773 EXPECT_EQ(units[0][0].start_index, 0U);
774 EXPECT_EQ(units[0][0].size, 5U);
775}
776
777TEST(PartitionByWeight, EvenSplitAcrossThreads)
778{
779 // 8 weights of 5 => total 40, target 10 per thread (4 threads), so 2 weights per thread.
780 std::vector<std::vector<uint16_t>> weights{ { 5, 5, 5, 5, 5, 5, 5, 5 } };
781 auto units = PartitionMSM::partition_by_weight(weights, 4);
782 ASSERT_EQ(units.size(), 4U);
783 for (size_t t = 0; t < 4; ++t) {
784 ASSERT_EQ(units[t].size(), 1U) << "thread " << t;
785 EXPECT_EQ(units[t][0].size, 2U) << "thread " << t;
786 EXPECT_EQ(thread_weight(units[t], weights), 10U) << "thread " << t;
787 }
788}
789
790TEST(PartitionByWeight, HeavyFirstWeightClosesFirstThreadEarly)
791{
792 // First weight alone exceeds the per-thread target; remainder is evenly split.
793 std::vector<std::vector<uint16_t>> weights{ { 100, 5, 5, 5, 5 } };
794 auto units = PartitionMSM::partition_by_weight(weights, 4);
795 ASSERT_EQ(units.size(), 4U);
796 // Thread 0 should close after the heavy weight.
797 ASSERT_FALSE(units[0].empty());
798 EXPECT_EQ(units[0][0].start_index, 0U);
799 EXPECT_EQ(units[0][0].size, 1U);
800 // Total assigned across all threads must equal n.
801 size_t total_assigned = 0;
802 for (const auto& t : units) {
803 for (const auto& u : t) {
804 total_assigned += u.size;
805 }
806 }
807 EXPECT_EQ(total_assigned, 5U);
808}
809
810TEST(PartitionByWeight, BoundaryStraddlesMsm)
811{
812 // Two MSMs of 4 weights of 5 each => total 40, 4 threads, target 10.
813 // Boundary should land mid-MSM if weights cross between MSMs.
814 std::vector<std::vector<uint16_t>> weights{ { 5, 5, 5, 5 }, { 5, 5, 5, 5 } };
815 auto units = PartitionMSM::partition_by_weight(weights, 4);
816 ASSERT_EQ(units.size(), 4U);
817 size_t total_assigned = 0;
818 for (const auto& t : units) {
819 for (const auto& u : t) {
820 total_assigned += u.size;
821 }
822 }
823 EXPECT_EQ(total_assigned, 8U);
824 // Each thread should carry exactly weight 10.
825 for (size_t t = 0; t < 4; ++t) {
826 EXPECT_EQ(thread_weight(units[t], weights), 10U) << "thread " << t;
827 }
828}
829
830TEST(PartitionByWeight, LastThreadAbsorbsRemainder)
831{
832 // weights {7,7,1}, num_threads=3 => total 15, target = ceil(15/3) = 5.
833 // Walk: T0 closes after weight 7, T1 closes after weight 7, then weight 1 trails.
834 // Without the "current_thread_idx < num_threads - 1" guard the partitioner would
835 // refuse to close T2 (running weight 1 < target 5) and the trailing weight would
836 // be lost. The guard makes T2 absorb it via the post-loop push.
837 std::vector<std::vector<uint16_t>> weights{ { 7, 7, 1 } };
838 auto units = PartitionMSM::partition_by_weight(weights, 3);
839 ASSERT_EQ(units.size(), 3U);
840 size_t total_assigned = 0;
841 for (const auto& t : units) {
842 for (const auto& u : t) {
843 total_assigned += u.size;
844 }
845 }
846 EXPECT_EQ(total_assigned, 3U);
847 ASSERT_EQ(units[2].size(), 1U);
848 EXPECT_EQ(units[2][0].start_index, 2U);
849 EXPECT_EQ(units[2][0].size, 1U);
850 EXPECT_EQ(thread_weight(units[2], weights), 1U);
851}
852
853TEST(PartitionByWeight, MoreThreadsThanScalars)
854{
855 // 3 weights of 5 => total 15, 8 threads, target ceil(15/8)=2.
856 // Each weight (5) immediately crosses target => first 3 threads each get one scalar.
857 std::vector<std::vector<uint16_t>> weights{ { 5, 5, 5 } };
858 auto units = PartitionMSM::partition_by_weight(weights, 8);
859 ASSERT_EQ(units.size(), 8U);
860 for (size_t t = 0; t < 3; ++t) {
861 ASSERT_EQ(units[t].size(), 1U) << "thread " << t;
862 EXPECT_EQ(units[t][0].size, 1U);
863 }
864 for (size_t t = 3; t < 8; ++t) {
865 EXPECT_TRUE(units[t].empty()) << "thread " << t;
866 }
867}
868
869// Non-templated test for explicit small inputs
870TEST(ScalarMultiplication, SmallInputsExplicit)
871{
872 uint256_t x0(0x68df84429941826a, 0xeb08934ed806781c, 0xc14b6a2e4f796a73, 0x08dc1a9a11a3c8db);
873 uint256_t y0(0x8ae5c31aa997f141, 0xe85f20c504f2c11b, 0x81a94193f3b1ce2b, 0x26f2c37372adb5b7);
874 uint256_t x1(0x80f5a592d919d32f, 0x1362652b984e51ca, 0xa0b26666f770c2a1, 0x142c6e1964e5c3c5);
875 uint256_t y1(0xb6c322ebb5ae4bc5, 0xf9fef6c7909c00f8, 0xb37ca1cc9af3b421, 0x1e331c7fa73d6a59);
876 uint256_t s0(0xe48bf12a24272e08, 0xf8dd0182577f3567, 0xec8fd222b8a6becb, 0x102d76b945612c9b);
877 uint256_t s1(0x098ae8d69f1e4e9e, 0xb5c8313c0f6040ed, 0xf78041e30cc46c44, 0x1d1e6e0c21892e13);
878
879 std::vector<grumpkin::fr> scalars{ s0, s1 };
880
883
885
886 auto result = scalar_multiplication::MSM<curve::Grumpkin>::msm(points, scalar_span);
887
888 grumpkin::g1::element expected = (points[0] * scalars[0]) + (points[1] * scalars[1]);
889
890 EXPECT_EQ(result, grumpkin::g1::affine_element(expected));
891}
#define BB_BENCH_NAME(name)
Definition bb_bench.hpp:264
BB_INLINE bool get(size_t index) const noexcept
Definition bitvector.hpp:44
typename Curve::ScalarField ScalarField
static std::vector< AffineElement > generators
static constexpr size_t kMaxBatchMSMs
static constexpr size_t kMaxBatchPointsPerMSM
static std::vector< ScalarField > scalars
static constexpr size_t kMaxBucketTestPoints
typename Curve::AffineElement AffineElement
static AffineElement naive_msm(std::span< ScalarField > input_scalars, std::span< const AffineElement > input_points)
typename Group::element Element
Definition grumpkin.hpp:63
typename grumpkin::g1 Group
Definition grumpkin.hpp:62
typename Group::affine_element AffineElement
Definition grumpkin.hpp:64
element class. Implements ecc group arithmetic using Jacobian coordinates See https://hyperelliptic....
Definition element.hpp:35
group_elements::affine_element< Fq, Fr, Params > affine_element
Definition group.hpp:44
virtual uint8_t get_random_uint8()=0
virtual uint16_t get_random_uint16()=0
virtual uint32_t get_random_uint32()=0
virtual uint256_t get_random_uint256()=0
static Element accumulate_buckets(BucketType &bucket_accumulators) noexcept
Reduce buckets to single point using running (suffix) sum from high to low: R = sum(k * B_k)
static uint32_t get_scalar_slice(const ScalarField &scalar, size_t round, size_t slice_size) noexcept
Extract c-bit slice from scalar for bucket index computation.
static AffineElement msm(std::span< const AffineElement > points, PolynomialSpan< const ScalarField > scalars, bool handle_edge_cases=false) noexcept
Main entry point for single MSM computation.
static std::vector< AffineElement > batch_multi_scalar_mul(std::span< std::span< const AffineElement > > points, std::span< std::span< ScalarField > > scalars, bool handle_edge_cases=true) noexcept
Compute multiple MSMs in parallel with work balancing.
static void batch_accumulate_points_into_buckets(std::span< const uint64_t > point_schedule, std::span< const AffineElement > points, AffineAdditionData &affine_data, BucketAccumulators &bucket_data) noexcept
Process sorted point schedule into bucket accumulators using batched affine additions.
const std::vector< MemoryValue > data
numeric::RNG & engine
RNG & get_randomness()
Definition engine.cpp:258
size_t sort_point_schedule_and_count_zero_buckets(uint64_t *point_schedule, const size_t num_entries, const uint32_t bucket_index_bits) noexcept
Sort point schedule by bucket index and count zero-bucket entries.
constexpr uint64_t BUCKET_INDEX_MASK
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
TYPED_TEST_SUITE(CommitmentKeyTest, Curves)
size_t get_num_cpus()
Definition thread.cpp:33
::testing::Types< curve::BN254, curve::Grumpkin > CurveTypes
TYPED_TEST(CommitmentKeyTest, CommitToZeroPoly)
TEST(BoomerangMegaCircuitBuilder, BasicCircuit)
void parallel_for(size_t num_iterations, const std::function< void(size_t)> &func)
Definition thread.cpp:111
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:141
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
std::span< Fr > span
Scratch space for batched affine point additions (one per thread)
Affine bucket accumulators for the fast affine-trick Pippenger variant.