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