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.
11#include <array>
12#include <bit>
13#include <filesystem>
14#include <gtest/gtest.h>
15
16using namespace bb;
17
18namespace {
20
21// Walks the actual Zone P / Zone W / Zone S allocator for a representative BN254
22// MSM shape and asserts the result fits in `compute_arena_bytes_for_msm`'s promise.
23// Mirrors the live allocator inside `pippenger_round_parallel` exactly; the only
24// historical drift bugs (cluster_offsets miscount, wasm aligned_local overflow,
25// NO_GLV abort, t1 abort) all came from this walk falling out of sync.
26bool pippenger_bn254_arena_layout_fits_for_test(size_t n_input,
27 bool external_glv_provided = false,
28 bool dedup_active = false,
29 size_t effective_num_bits_for_test = 0) noexcept
30{
31 using Curve = curve::BN254;
32 using ScalarField = typename Curve::ScalarField;
33 using Element = typename Curve::Element;
34 using AffineElement = typename Curve::AffineElement;
35 namespace rpd = scalar_multiplication::round_parallel_detail;
36
37 constexpr size_t FULL_NUM_BITS = ScalarField::modulus.get_msb() + 1;
38 if (n_input < 4) {
39 return true;
40 }
41
42 const bool use_glv = external_glv_provided || (n_input <= rpd::GLV_SMALL_N_THRESHOLD);
43 const bool inline_glv_double = use_glv && !external_glv_provided;
44 const size_t n = use_glv ? 2 * n_input : n_input;
45 const size_t NUM_BITS = use_glv ? size_t{ 128 } : FULL_NUM_BITS;
46 const size_t arena_capacity =
47 scalar_multiplication::compute_arena_bytes_for_msm<Curve>(n_input, external_glv_provided, dedup_active);
48 if (arena_capacity == 0) {
49 return true;
50 }
51
52 const size_t actual_num_bits = (effective_num_bits_for_test == 0 || effective_num_bits_for_test > NUM_BITS)
53 ? NUM_BITS
54 : effective_num_bits_for_test;
55 const size_t num_logical_threads_for_c =
57 const size_t window_bits = rpd::choose_window_bits(n, actual_num_bits, n_input, num_logical_threads_for_c);
58 const auto sched = rpd::build_window_schedule(actual_num_bits, window_bits);
59 const size_t num_buckets = (size_t{ 1 } << (window_bits - 1)) + 1;
60
61 using rpd::BATCH_CAPACITY;
62 constexpr size_t MIN_BATCH_CAPACITY = 32;
63 constexpr size_t BATCH_MEM_BUDGET = 32ULL * 1024ULL * 1024ULL;
64 constexpr size_t SUBCHUNK_ENTRIES_CAP = 2048;
65
66 const size_t desired_threads = std::max<size_t>(1, bb::get_num_cpus());
67 const size_t max_threads_for_min_batch = std::max<size_t>(1, n / MIN_BATCH_CAPACITY);
68 const size_t num_threads = std::min(desired_threads, max_threads_for_min_batch);
69 const size_t profile_threads = std::max<size_t>(1, bb::get_num_cpus());
70 const size_t worker_total = num_threads;
71
72 // Uniform schedule: the widest window's bucket count is the per-window cap.
73 const size_t B_eff = num_buckets;
74 const size_t dense_stride_est =
75 std::max<size_t>(2, std::bit_ceil((B_eff > 1) ? ((B_eff - 1 + num_threads - 1) / num_threads) : size_t{ 1 }));
76 const size_t bucket_partials_per_window_max = (B_eff > 0) ? (B_eff - 1 + num_threads - 1) : 0;
77 const size_t hist_h_bytes_pw_shared = (size_t{ 4 } * num_threads * B_eff);
78 const size_t hist_o_bytes_pw_shared =
79 (sizeof(rpd::ChunkOutput<Curve>) * num_threads) + (size_t{ 96 } * num_threads);
80 const size_t hist_slot_bytes_pw_shared = std::max(hist_h_bytes_pw_shared, hist_o_bytes_pw_shared);
81 const size_t dense_slot_bytes_pw_shared = (size_t{ 65 } * bucket_partials_per_window_max);
82 const size_t per_window_bytes_shared =
83 hist_slot_bytes_pw_shared + dense_slot_bytes_pw_shared + (size_t{ 8 } * (B_eff + 1)) +
84 (size_t{ 8 } * (num_threads + 1)) + (size_t{ 8 } * (num_threads + 1)) + (size_t{ 8 } * num_threads) +
85 (size_t{ 8 } * num_threads) + (size_t{ 8 } * num_threads) + (size_t{ 16 } * worker_total) +
86 (size_t{ 8 } * num_threads) + (size_t{ 87 } * worker_total * dense_stride_est);
87 const size_t capacity_lo = n;
88 const size_t per_window_bytes_lo = (size_t{ 4 } * capacity_lo) + per_window_bytes_shared;
89
90 const size_t global_max_chunk_len = (n + num_threads - 1) / num_threads;
91 const size_t global_max_overflow_per_window =
92 (global_max_chunk_len + SUBCHUNK_ENTRIES_CAP - 1) / SUBCHUNK_ENTRIES_CAP;
93 const size_t chunk_capacity = std::max(SUBCHUNK_ENTRIES_CAP, 2 * global_max_overflow_per_window);
94
95 const size_t phase_a_cluster_members_cap = std::min(rpd::DEDUP_MAX_MEMBERS, n);
96 const size_t phase_a_cluster_offsets_cap = (rpd::DEDUP_MAX_CLUSTERS / num_threads) + 2;
97
98 const size_t phase_one_prologue_bytes = n + (use_glv ? size_t{ 32 } * n : size_t{ 0 }) +
99 (inline_glv_double ? size_t{ 64 } * n : size_t{ 0 }) +
100 (profile_threads * size_t{ 1024 });
101
102 const rpd::PerWorkerArenaLayout<Curve> budget_layout(
103 /*chunk_capacity=*/SUBCHUNK_ENTRIES_CAP,
104 global_max_overflow_per_window,
105 dedup_active,
106 phase_a_cluster_members_cap,
107 phase_a_cluster_offsets_cap,
108 /*windows_per_batch=*/0,
109 /*dense_stride_est=*/0);
110 const size_t worker_union_bytes_for_budget = budget_layout.per_worker_union_bytes;
111 const size_t fixed_overhead = (worker_union_bytes_for_budget * worker_total) +
112 (size_t{ 96 } * rpd::MAX_SCHEDULE_WINDOWS) + (size_t{ 8 } * (num_threads + 1)) +
113 phase_one_prologue_bytes;
114 const size_t available_budget =
115 (BATCH_MEM_BUDGET > fixed_overhead) ? (BATCH_MEM_BUDGET - fixed_overhead) : size_t{ 0 };
116 const size_t windows_per_batch = (per_window_bytes_lo == 0 || available_budget == 0)
117 ? std::max<size_t>(1, sched.num_windows)
118 : std::min(std::max<size_t>(1, available_budget / per_window_bytes_lo),
119 static_cast<size_t>(sched.num_windows));
120
121 auto align_up = [](size_t off, size_t align) -> size_t { return (off + align - 1) & ~(align - 1); };
122 auto layout_add = [&](size_t& off, size_t bytes, size_t align) { off = align_up(off, align) + bytes; };
123 auto bump_fits = [&](size_t count,
124 size_t size,
125 size_t align,
126 size_t& cursor,
127 size_t bound,
128 size_t base_offset,
129 size_t base_misalign) {
130 const size_t cur_addr_mod = (base_misalign + base_offset + cursor) & (align - 1);
131 const size_t align_delta = (cur_addr_mod == 0) ? size_t{ 0 } : (align - cur_addr_mod);
132 const size_t aligned_local = cursor + align_delta;
133 const size_t bytes = count * size;
134 if (aligned_local + bytes > bound) {
135 return false;
136 }
137 cursor = aligned_local + bytes;
138 return true;
139 };
140
141 for (size_t base_misalign = 0; base_misalign < alignof(AffineElement); ++base_misalign) {
142 size_t arena_cursor = 0;
143 if (!bump_fits(n, sizeof(uint8_t), alignof(uint8_t), arena_cursor, arena_capacity, 0, base_misalign)) {
144 return false;
145 }
146 if (!bump_fits(profile_threads,
149 arena_cursor,
150 arena_capacity,
151 0,
152 base_misalign)) {
153 return false;
154 }
155 if (use_glv) {
156 if (!bump_fits(
157 n, sizeof(ScalarField), alignof(ScalarField), arena_cursor, arena_capacity, 0, base_misalign)) {
158 return false;
159 }
160 if (inline_glv_double &&
161 !bump_fits(
162 n, sizeof(AffineElement), alignof(AffineElement), arena_cursor, arena_capacity, 0, base_misalign)) {
163 return false;
164 }
165 }
166 const size_t bytes_P_prefix = arena_cursor;
167
168 const rpd::PerWorkerArenaLayout<Curve> worker_layout(chunk_capacity,
169 global_max_overflow_per_window,
170 dedup_active,
171 phase_a_cluster_members_cap,
172 phase_a_cluster_offsets_cap,
173 windows_per_batch,
174 dense_stride_est);
175 constexpr size_t WORKER_SLAB_ALIGN = rpd::PerWorkerArenaLayout<Curve>::WORKER_SLAB_ALIGN;
176 const size_t per_worker_bytes = worker_layout.per_worker_bytes;
177
178 size_t bytes_P_extra_layout = 0;
179 layout_add(bytes_P_extra_layout, sizeof(Element) * rpd::MAX_SCHEDULE_WINDOWS, alignof(Element));
180 if (dedup_active) {
181 layout_add(bytes_P_extra_layout, sizeof(uint32_t) * n, alignof(uint32_t));
182 layout_add(bytes_P_extra_layout, sizeof(AffineElement) * rpd::DEDUP_MAX_CLUSTERS, alignof(AffineElement));
183 }
184 const size_t bytes_P_min = align_up(bytes_P_prefix, alignof(Element)) + bytes_P_extra_layout;
185 const size_t bytes_P = align_up(bytes_P_min + base_misalign, WORKER_SLAB_ALIGN) - base_misalign;
186 const size_t bytes_W = per_worker_bytes * worker_total;
187 if (bytes_P + bytes_W > arena_capacity) {
188 return false;
189 }
190 const size_t bytes_S_total = arena_capacity - bytes_P - bytes_W;
191 size_t zone_S_cursor = 0;
192 const size_t zone_S_base = bytes_P + bytes_W;
193
194 const size_t schedule_total = windows_per_batch * capacity_lo;
195 if (!bump_fits(schedule_total,
196 sizeof(uint32_t),
197 alignof(uint32_t),
198 zone_S_cursor,
199 bytes_S_total,
200 zone_S_base,
201 base_misalign)) {
202 return false;
203 }
204 const size_t hist_h_bytes_total = size_t{ 4 } * windows_per_batch * num_threads * B_eff;
205 size_t o_layout_cur = 0;
206 o_layout_cur = align_up(o_layout_cur, alignof(rpd::ChunkOutput<Curve>));
207 o_layout_cur += sizeof(rpd::ChunkOutput<Curve>) * windows_per_batch * num_threads;
208 o_layout_cur = align_up(o_layout_cur, alignof(Element));
209 o_layout_cur += sizeof(Element) * num_threads * windows_per_batch;
210 const size_t hist_slot_cells =
211 (std::max(hist_h_bytes_total, o_layout_cur) + sizeof(AffineElement) - 1) / sizeof(AffineElement);
212 const size_t dense_slot_cells =
213 ((size_t{ 65 } * windows_per_batch * bucket_partials_per_window_max) + sizeof(AffineElement) - 1) /
214 sizeof(AffineElement);
215 if (!bump_fits(hist_slot_cells,
216 sizeof(AffineElement),
217 alignof(AffineElement),
218 zone_S_cursor,
219 bytes_S_total,
220 zone_S_base,
221 base_misalign) ||
222 !bump_fits(dense_slot_cells,
223 sizeof(AffineElement),
224 alignof(AffineElement),
225 zone_S_cursor,
226 bytes_S_total,
227 zone_S_base,
228 base_misalign) ||
229 !bump_fits(windows_per_batch * (B_eff + 1),
230 sizeof(size_t),
231 alignof(size_t),
232 zone_S_cursor,
233 bytes_S_total,
234 zone_S_base,
235 base_misalign) ||
236 !bump_fits(windows_per_batch * (num_threads + 1),
237 sizeof(size_t),
238 alignof(size_t),
239 zone_S_cursor,
240 bytes_S_total,
241 zone_S_base,
242 base_misalign) ||
243 !bump_fits(windows_per_batch * (num_threads + 1),
244 sizeof(size_t),
245 alignof(size_t),
246 zone_S_cursor,
247 bytes_S_total,
248 zone_S_base,
249 base_misalign) ||
250 !bump_fits(windows_per_batch * num_threads,
251 sizeof(size_t),
252 alignof(size_t),
253 zone_S_cursor,
254 bytes_S_total,
255 zone_S_base,
256 base_misalign) ||
257 !bump_fits((num_threads * windows_per_batch) + 1,
258 sizeof(size_t),
259 alignof(size_t),
260 zone_S_cursor,
261 bytes_S_total,
262 zone_S_base,
263 base_misalign) ||
264 !bump_fits(num_threads + 1,
265 sizeof(size_t),
266 alignof(size_t),
267 zone_S_cursor,
268 bytes_S_total,
269 zone_S_base,
270 base_misalign) ||
271 !bump_fits(windows_per_batch * num_threads,
272 sizeof(size_t),
273 alignof(size_t),
274 zone_S_cursor,
275 bytes_S_total,
276 zone_S_base,
277 base_misalign) ||
278 !bump_fits(windows_per_batch * num_threads,
279 sizeof(size_t),
280 alignof(size_t),
281 zone_S_cursor,
282 bytes_S_total,
283 zone_S_base,
284 base_misalign)) {
285 return false;
286 }
287 }
288 return true;
289}
290} // namespace
291
292template <class Curve> class ScalarMultiplicationTest : public ::testing::Test {
293 public:
294 using Group = typename Curve::Group;
295 using Element = typename Curve::Element;
298
299 static constexpr size_t num_points = 31013;
300
301 // Bounds used by test_batch_multi_scalar_mul. Kept small so num_points (and therefore
302 // SetUpTestSuite, which builds num_points random EC points) stays cheap — especially under wasm,
303 // where the fixture build previously dominated the whole ecc_tests run.
304 static constexpr size_t kMaxBatchMSMs = 32;
305 static constexpr size_t kMaxBatchPointsPerMSM = 400;
306
307 // Pinning invariants: these tests walk generators[]/scalars[] without bounds checks beyond an
308 // occasional runtime ASSERT_LT. Pin the relationships at compile time so changing any one of
309 // these constants in isolation cannot regress into an out-of-bounds walk.
311 "test_batch_multi_scalar_mul can exceed num_points; "
312 "raise num_points or lower kMaxBatchMSMs / kMaxBatchPointsPerMSM");
313
314 static inline std::vector<AffineElement> generators{};
315 static inline std::vector<ScalarField> scalars{};
316
317 static AffineElement naive_msm(std::span<ScalarField> input_scalars, std::span<const AffineElement> input_points)
318 {
319 size_t total_points = input_scalars.size();
320 size_t num_threads = get_num_cpus();
321 std::vector<Element> expected_accs(num_threads);
322 size_t range_per_thread = (total_points + num_threads - 1) / num_threads;
323 parallel_for(num_threads, [&](size_t thread_idx) {
324 Element expected_thread_acc;
325 expected_thread_acc.self_set_infinity();
326 size_t start = thread_idx * range_per_thread;
327 size_t end = ((thread_idx + 1) * range_per_thread > total_points) ? total_points
328 : (thread_idx + 1) * range_per_thread;
329 bool skip = start >= total_points;
330 if (!skip) {
331 for (size_t i = start; i < end; ++i) {
332 expected_thread_acc += input_points[i] * input_scalars[i];
333 }
334 }
335 expected_accs[thread_idx] = expected_thread_acc;
336 });
337
338 Element expected_acc = Element();
339 expected_acc.self_set_infinity();
340 for (auto& acc : expected_accs) {
341 expected_acc += acc;
342 }
343 return AffineElement(expected_acc);
344 }
345
346 static std::vector<AffineElement> make_repeated_test_points(size_t num_pts)
347 {
348 std::vector<AffineElement> points(num_pts);
349 for (size_t i = 0; i < num_pts; ++i) {
350 points[i] = generators[i % generators.size()];
351 }
352 return points;
353 }
354
355 static void SetUpTestSuite()
356 {
357 generators.resize(num_points);
358 scalars.resize(num_points);
359 parallel_for_range(num_points, [&](size_t start, size_t end) {
360 for (size_t i = start; i < end; ++i) {
361 generators[i] = Group::one * Curve::ScalarField::random_element(&engine);
362 scalars[i] = Curve::ScalarField::random_element(&engine);
363 }
364 });
365 for (size_t i = 0; i < num_points - 1; ++i) {
366 ASSERT_EQ(generators[i].x == generators[i + 1].x, false);
367 }
368 };
369
370 // ======================= Test Methods =======================
371
373 {
374 std::span<ScalarField> test_scalars(&scalars[0], num_points);
377 AffineElement expected = naive_msm(test_scalars, generators);
378 EXPECT_EQ(result, expected);
379 }
380
382 {
383 BB_BENCH_NAME("BatchMultiScalarMul");
384
385 const size_t num_msms = static_cast<size_t>(engine.get_random_uint8()) % kMaxBatchMSMs;
386 std::vector<AffineElement> expected(num_msms);
387
388 std::vector<std::vector<ScalarField>> batch_scalars_copies(num_msms);
389 std::vector<size_t> start_indices(num_msms);
390 std::vector<PolynomialSpan<ScalarField>> batch_scalars_spans;
391
392 size_t vector_offset = 0;
393 for (size_t k = 0; k < num_msms; ++k) {
394 const size_t num_pts = static_cast<size_t>(engine.get_random_uint16()) % kMaxBatchPointsPerMSM;
395
396 ASSERT_LT(vector_offset + num_pts, num_points);
397
398 batch_scalars_copies[k].resize(num_pts);
399 for (size_t i = 0; i < num_pts; ++i) {
400 batch_scalars_copies[k][i] = scalars[vector_offset + i];
401 }
402
403 start_indices[k] = vector_offset;
404 batch_scalars_spans.emplace_back(vector_offset, std::span<ScalarField>(batch_scalars_copies[k]));
405 vector_offset += num_pts;
406
407 std::span<const AffineElement> batch_points(&generators[start_indices[k]], num_pts);
408 expected[k] = naive_msm(batch_scalars_copies[k], batch_points);
409 }
410
411 std::vector<AffineElement> result =
413
414 EXPECT_EQ(result, expected);
415 }
416
418 {
419 const size_t num_msms = 10;
420 std::vector<AffineElement> expected(num_msms);
421
422 std::vector<std::vector<ScalarField>> batch_scalars(num_msms);
423 std::vector<PolynomialSpan<ScalarField>> batch_scalars_spans;
424
425 for (size_t k = 0; k < num_msms; ++k) {
426 const size_t num_pts = 33;
427 auto& test_scalars = batch_scalars[k];
428
429 test_scalars.resize(num_pts);
430
431 size_t fixture_offset = k * num_pts;
432
433 std::span<const AffineElement> batch_points(&generators[fixture_offset], num_pts);
434 for (size_t i = 0; i < 13; ++i) {
435 test_scalars[i] = 0;
436 }
437 for (size_t i = 13; i < 23; ++i) {
438 test_scalars[i] = scalars[fixture_offset + i + 13];
439 }
440 for (size_t i = 23; i < num_pts; ++i) {
441 test_scalars[i] = 0;
442 }
443 batch_scalars_spans.emplace_back(fixture_offset, std::span<ScalarField>(batch_scalars[k]));
444
445 expected[k] = naive_msm(batch_scalars[k], batch_points);
446 }
447
448 std::vector<AffineElement> result =
450
451 EXPECT_EQ(result, expected);
452 }
453
454 // Larger workload that crosses the batched dispatcher's `total_nonzero > 4096` eligibility
455 // threshold so the multi-MSM Phases 1-6b pipeline (REBALANCE path) is exercised, not the
456 // per-MSM delegation fallback.
458 {
459 constexpr size_t num_msms = 4;
460 constexpr size_t per_msm_n = 1 << 13; // 8192 points per MSM, total = 32768
461
462 std::vector<AffineElement> expected(num_msms);
463 std::vector<std::vector<ScalarField>> batch_scalars(num_msms);
464 std::vector<PolynomialSpan<ScalarField>> batch_scalars_spans;
465
466 for (size_t k = 0; k < num_msms; ++k) {
467 batch_scalars[k].resize(per_msm_n);
468 for (size_t i = 0; i < per_msm_n; ++i) {
469 // num_msms * per_msm_n = 32768 > num_points (31013); wrap to stay in bounds
470 // (matches the ragged test's indexing). Caught as an out-of-bounds read by the
471 // _GLIBCXX_DEBUG / ASAN build otherwise.
472 batch_scalars[k][i] = scalars[(k * per_msm_n + i) % num_points];
473 }
474 std::span<const AffineElement> pts(&generators[0], per_msm_n);
475 batch_scalars_spans.emplace_back(0, std::span<ScalarField>(batch_scalars[k]));
476 expected[k] = naive_msm(batch_scalars[k], pts);
477 }
478
479 std::vector<AffineElement> result =
481
482 for (size_t k = 0; k < num_msms; ++k) {
483 EXPECT_EQ(result[k], expected[k]) << "MSM " << k << " mismatched";
484 }
485 }
486
487 // Ragged batch with mixed densities — the workload pattern for translator wires + databus.
488 // K=5 MSMs of varying sizes, varying zero density, all sharing the same SRS prefix.
490 {
491 const std::vector<size_t> sizes = { 16384, 4096, 8192, 1024, 12000 };
492 const size_t num_msms = sizes.size();
493
494 std::vector<AffineElement> expected(num_msms);
495 std::vector<std::vector<ScalarField>> batch_scalars(num_msms);
496 std::vector<PolynomialSpan<ScalarField>> batch_scalars_spans;
497
498 for (size_t k = 0; k < num_msms; ++k) {
499 const size_t n = sizes[k];
500 batch_scalars[k].resize(n);
501 for (size_t i = 0; i < n; ++i) {
502 if ((k == 1 || k == 3) && (i % 4 != 0)) {
503 batch_scalars[k][i] = ScalarField::zero();
504 } else {
505 batch_scalars[k][i] = scalars[(k * 17 + i) % num_points];
506 }
507 }
508 std::span<const AffineElement> pts(&generators[0], n);
509 batch_scalars_spans.emplace_back(0, std::span<ScalarField>(batch_scalars[k]));
510 expected[k] = naive_msm(batch_scalars[k], pts);
511 }
512
513 std::vector<AffineElement> result =
515
516 for (size_t k = 0; k < num_msms; ++k) {
517 EXPECT_EQ(result[k], expected[k]) << "MSM " << k << " (n=" << sizes[k] << ") mismatched";
518 }
519 }
520
530 {
531 std::vector<size_t> sizes;
532 for (size_t k = 0; k < 24; ++k) {
533 sizes.push_back(600 + (257 * k));
534 }
535 // Add members straddling the driver's small/large boundary (MSM_MIN_PTS_PER_THREAD * pool_width)
536 // to exercise both dispatch paths. On wasm MSM_MIN_PTS_PER_THREAD is SIZE_MAX and a single-core
537 // pool has no concurrent path — neither has a finite boundary, so there the cluster above is the
538 // whole batch (and 256 * pool_width never overflows).
539 const size_t pool_width = get_num_cpus();
540 if (scalar_multiplication::MSM_MIN_PTS_PER_THREAD != std::numeric_limits<size_t>::max() && pool_width > 1) {
541 const size_t boundary = scalar_multiplication::MSM_MIN_PTS_PER_THREAD * pool_width;
542 sizes.push_back(boundary - 1);
543 sizes.push_back(boundary);
544 sizes.push_back(boundary * 2);
545 }
546 const size_t num_msms = sizes.size();
547
548 const uint256_t high_bit(0, 0, 0, uint64_t{ 1 } << (200 - 192));
549 std::vector<AffineElement> expected(num_msms);
550 std::vector<std::vector<ScalarField>> batch_scalars(num_msms);
551 std::vector<PolynomialSpan<ScalarField>> batch_scalars_spans;
552 std::vector<uint32_t> dedup_infos(num_msms, 0);
553
554 for (size_t k = 0; k < num_msms; ++k) {
555 const size_t n = sizes[k];
556 batch_scalars[k].resize(n);
557 const bool dup_heavy = (k % 3 == 0);
558 uint32_t dup_count = 0;
559 for (size_t i = 0; i < n; ++i) {
560 if (k % 5 == 4 && i % 3 != 0) {
561 batch_scalars[k][i] = ScalarField::zero();
562 } else if (dup_heavy) {
563 // Runs of 8 equal dedup-eligible values (msb >= any window the dispatch picks).
564 batch_scalars[k][i] = ScalarField(high_bit + uint256_t((i / 8) + 1));
565 dup_count += static_cast<uint32_t>(i % 8 != 0);
566 } else {
567 batch_scalars[k][i] = scalars[((k * 31) + i) % num_points];
568 }
569 }
570 if (dup_heavy) {
571 // Alternate measured-count and bare (no-estimate) hints through the channel.
572 dedup_infos[k] = (k % 2 == 0) ? dup_count : uint32_t{ 1 };
573 }
574 std::span<const AffineElement> pts(&generators[0], n);
575 batch_scalars_spans.emplace_back(0, std::span<ScalarField>(batch_scalars[k]));
576 expected[k] = naive_msm(batch_scalars[k], pts);
577 }
578
580 generators, batch_scalars_spans, /*handle_edge_cases=*/false, dedup_infos);
581
582 for (size_t k = 0; k < num_msms; ++k) {
583 EXPECT_EQ(result[k], expected[k]) << "MSM " << k << " (n=" << sizes[k] << ") mismatched";
584 }
585 }
586
587 void test_msm()
588 {
589 const size_t start_index = 1234;
590 const size_t num_pts = num_points - start_index;
591
592 PolynomialSpan<ScalarField> scalar_span =
595
596 std::span<AffineElement> points(&generators[start_index], num_pts);
597 AffineElement expected = naive_msm(scalar_span.span, points);
598 EXPECT_EQ(result, expected);
599 }
600
602 {
603 const size_t start_index = 1234;
604 const size_t num_pts = num_points - start_index;
605 std::vector<ScalarField> test_scalars(num_pts, ScalarField::zero());
606
607 PolynomialSpan<ScalarField> scalar_span = PolynomialSpan<ScalarField>(start_index, test_scalars);
609
610 EXPECT_EQ(result, Group::affine_point_at_infinity);
611 }
612
614 {
615 std::vector<ScalarField> test_scalars;
616 std::vector<AffineElement> input_points;
617 PolynomialSpan<ScalarField> scalar_span = PolynomialSpan<ScalarField>(0, test_scalars);
619
620 EXPECT_EQ(result, Group::affine_point_at_infinity);
621 }
622
624 {
625 const size_t num_pts = 100;
626 std::vector<ScalarField> test_scalars(num_pts);
627 std::vector<ScalarField> scalars_copy(num_pts);
628
629 for (size_t i = 0; i < num_pts; ++i) {
630 test_scalars[i] = scalars[i];
631 scalars_copy[i] = test_scalars[i];
632 }
633
634 std::span<const AffineElement> points(&generators[0], num_pts);
635 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
636
637 scalar_multiplication::MSM<Curve>::msm(points, scalar_span);
638
639 for (size_t i = 0; i < num_pts; ++i) {
640 EXPECT_EQ(test_scalars[i], scalars_copy[i]) << "Scalar at index " << i << " was modified";
641 }
642 }
643
645 {
646 const size_t num_msms = 3;
647 const size_t num_pts = 100;
648
649 std::vector<std::vector<ScalarField>> batch_scalars(num_msms);
650 std::vector<std::vector<ScalarField>> scalars_copies(num_msms);
651 std::vector<PolynomialSpan<ScalarField>> batch_scalar_spans;
652
653 for (size_t k = 0; k < num_msms; ++k) {
654 batch_scalars[k].resize(num_pts);
655 scalars_copies[k].resize(num_pts);
656
657 for (size_t i = 0; i < num_pts; ++i) {
658 batch_scalars[k][i] = scalars[k * num_pts + i];
659 scalars_copies[k][i] = batch_scalars[k][i];
660 }
661
662 batch_scalar_spans.emplace_back(k * num_pts, std::span<ScalarField>(batch_scalars[k]));
663 }
664
666
667 for (size_t k = 0; k < num_msms; ++k) {
668 for (size_t i = 0; i < num_pts; ++i) {
669 EXPECT_EQ(batch_scalars[k][i], scalars_copies[k][i])
670 << "Scalar at MSM " << k << ", index " << i << " was modified";
671 }
672 }
673 }
674
676 {
677#ifdef __wasm__
678 GTEST_SKIP() << "WASM GLV threshold exceeds the fixture size; non-GLV restoration is native-only here.";
679#else
680 namespace rpd = scalar_multiplication::round_parallel_detail;
681 const size_t num_pts = rpd::GLV_SMALL_N_THRESHOLD + 257;
682 ASSERT_LE(num_pts, num_points);
683
684 std::vector<ScalarField> test_scalars(num_pts);
685 std::vector<ScalarField> scalars_copy(num_pts);
686 for (size_t i = 0; i < num_pts; ++i) {
687 test_scalars[i] = scalars[i];
688 scalars_copy[i] = test_scalars[i];
689 }
690
691 std::span<const AffineElement> points(&generators[0], num_pts);
692 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
693 scalar_multiplication::MSM<Curve>::msm(points, scalar_span, /*handle_edge_cases=*/false);
694
695 for (size_t i = 0; i < num_pts; ++i) {
696 EXPECT_EQ(test_scalars[i], scalars_copy[i]) << "non-GLV scalar at index " << i << " was modified";
697 }
698#endif
699 }
700
702 {
703 const size_t num_pts = 5;
704 std::vector<ScalarField> test_scalars(num_pts, ScalarField::one());
705 std::span<const AffineElement> points(&generators[0], num_pts);
706
707 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
709
710 Element expected;
711 expected.self_set_infinity();
712 for (size_t i = 0; i < num_pts; ++i) {
713 expected += points[i];
714 }
715
716 EXPECT_EQ(result, AffineElement(expected));
717 }
718
720 {
721 const size_t num_pts = 5;
722 std::vector<ScalarField> test_scalars(num_pts, -ScalarField::one());
723 std::span<const AffineElement> points(&generators[0], num_pts);
724
725 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
727
728 Element expected;
729 expected.self_set_infinity();
730 for (size_t i = 0; i < num_pts; ++i) {
731 expected -= points[i];
732 }
733
734 EXPECT_EQ(result, AffineElement(expected));
735 }
736
738 {
739 std::vector<ScalarField> test_scalars = { scalars[0] };
740 std::span<const AffineElement> points(&generators[0], 1);
741
742 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
744
745 AffineElement expected(points[0] * test_scalars[0]);
746 EXPECT_EQ(result, expected);
747 }
748
750 {
751 std::vector<size_t> test_sizes = { 1, 2, 15, 16, 17, 50, 127, 128, 129, 256, 512 };
752
753 for (size_t num_pts : test_sizes) {
754 ASSERT_LE(num_pts, num_points);
755
756 std::vector<ScalarField> test_scalars(num_pts);
757 for (size_t i = 0; i < num_pts; ++i) {
758 test_scalars[i] = scalars[i];
759 }
760
761 std::span<const AffineElement> points(&generators[0], num_pts);
762 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
763
765 AffineElement expected = naive_msm(test_scalars, points);
766
767 EXPECT_EQ(result, expected) << "Failed for size " << num_pts;
768 }
769 }
770
772 {
773 // Use enough points to trigger Pippenger (> PIPPENGER_THRESHOLD = 16)
774 const size_t num_pts = 32;
775 AffineElement base_point = generators[0];
776
777 std::vector<AffineElement> points(num_pts, base_point);
778 std::vector<ScalarField> test_scalars(num_pts);
779 ScalarField scalar_sum = ScalarField::zero();
780
781 for (size_t i = 0; i < num_pts; ++i) {
782 test_scalars[i] = scalars[i];
783 scalar_sum += test_scalars[i];
784 }
785
786 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
787 // Duplicate points are an edge case (P + P requires doubling, not addition).
788 // Must use handle_edge_cases=true for correctness with Pippenger.
789 AffineElement result = scalar_multiplication::MSM<Curve>::msm(points, scalar_span, /*handle_edge_cases=*/true);
790
791 AffineElement expected(base_point * scalar_sum);
792 EXPECT_EQ(result, expected);
793 }
794
796 {
797 const size_t num_pts = 100;
798 std::vector<ScalarField> test_scalars(num_pts);
799 Element expected;
800 expected.self_set_infinity();
801
802 for (size_t i = 0; i < num_pts; ++i) {
803 if (i % 2 == 0) {
804 test_scalars[i] = ScalarField::zero();
805 } else {
806 test_scalars[i] = scalars[i];
807 expected += generators[i] * test_scalars[i];
808 }
809 }
810
811 std::span<const AffineElement> points(&generators[0], num_pts);
812 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
813
815 EXPECT_EQ(result, AffineElement(expected));
816 }
817
819 {
820 const size_t num_pts = 200;
821 std::vector<ScalarField> test_scalars(num_pts);
822 for (size_t i = 0; i < num_pts; ++i) {
823 test_scalars[i] = scalars[i];
824 }
825
826 std::span<const AffineElement> points(&generators[0], num_pts);
827 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
828
829 auto result = scalar_multiplication::pippenger<Curve>(scalar_span, points);
830
831 AffineElement expected = naive_msm(test_scalars, points);
832 EXPECT_EQ(AffineElement(result), expected);
833 }
834
836 {
837 const size_t num_pts = 200;
838 std::vector<ScalarField> test_scalars(num_pts);
839 for (size_t i = 0; i < num_pts; ++i) {
840 test_scalars[i] = scalars[i];
841 }
842
843 std::span<const AffineElement> points(&generators[0], num_pts);
844 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
845
846 auto result = scalar_multiplication::pippenger_unsafe<Curve>(scalar_span, points);
847
848 AffineElement expected = naive_msm(test_scalars, points);
849 EXPECT_EQ(AffineElement(result), expected);
850 }
851
859 void test_offset_span(size_t n_total, size_t start_index, size_t n_used, uint64_t seed)
860 {
861 auto& rng = numeric::get_debug_randomness(true, seed);
862 std::vector<ScalarField> test_scalars(n_total);
863 std::vector<AffineElement> input_points(start_index + n_used);
864 for (size_t i = 0; i < n_total; ++i) {
865 test_scalars[i] = ScalarField::random_element(&rng);
866 }
867 for (size_t i = 0; i < input_points.size(); ++i) {
868 input_points[i] = AffineElement(Element::random_element(&rng));
869 }
870
872 start_index, std::span<const ScalarField>{ test_scalars.data() + start_index, n_used }
873 };
874
875 Element actual = scalar_multiplication::pippenger_unsafe<Curve>(scalar_span, input_points);
876
877 Element expected;
878 expected.self_set_infinity();
879 for (size_t i = 0; i < n_used; ++i) {
880 expected += input_points[start_index + i] * test_scalars[start_index + i];
881 }
882 EXPECT_EQ(AffineElement(actual), AffineElement(expected))
883 << "Offset MSM mismatch at n_total=" << n_total << " start_index=" << start_index << " n_used=" << n_used;
884 }
885
891 {
893 auto& rng = numeric::get_debug_randomness(true, 0x5eedu + 35);
894 std::vector<AffineElement> points(num_pts);
895 std::vector<ScalarField> test_scalars(num_pts);
896 for (size_t i = 0; i < num_pts; ++i) {
897 points[i] = AffineElement(Element::random_element(&rng));
898 test_scalars[i] = ScalarField::random_element(&rng);
899 }
900
901 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
903 AffineElement expected = naive_msm(test_scalars, points);
904 EXPECT_EQ(result, expected);
905 }
906
920 {
921 const size_t num_pts = 100000;
922 auto& rng = numeric::get_debug_randomness(true, 0x5eedu + 36);
923 std::vector<AffineElement> points(num_pts);
924 for (size_t i = 0; i < num_pts; ++i) {
925 points[i] = AffineElement(Element::random_element(&rng));
926 }
927 std::vector<ScalarField> uniform_scalars(num_pts, ScalarField(7));
928 PolynomialSpan<ScalarField> scalar_span(0, uniform_scalars);
929
931 AffineElement expected =
932 naive_msm(std::span<ScalarField>(uniform_scalars), std::span<const AffineElement>(points));
933 EXPECT_EQ(result, expected);
934 }
935
954 {
955 const size_t num_pts = 50000;
956 // Pick a dedup-eligible scalar: msb >= c (c ≈ 11 for n ≈ 50 000), so any value
957 // ≥ 2^11 works. Use 2^200 so msb is firmly large for any c the dispatch picks.
958 const ScalarField val = ScalarField(uint256_t(0, 0, 0, uint64_t{ 1 } << (200 - 192))); // 2^200
959 std::vector<ScalarField> uniform_scalars(num_pts, val);
960 std::vector<AffineElement> points = make_repeated_test_points(num_pts);
961 PolynomialSpan<ScalarField> scalar_span(0, uniform_scalars);
962
964 points, scalar_span, /*handle_edge_cases=*/false, /*dedup_hint=*/true);
965
966 AffineElement expected =
967 naive_msm(std::span<ScalarField>(uniform_scalars), std::span<const AffineElement>(points));
968 EXPECT_EQ(result, expected);
969 }
970
980 {
981 constexpr size_t NUM_CLUSTERS = 12000;
982 constexpr size_t CLUSTER_SIZE = 3;
983 const size_t num_pts = NUM_CLUSTERS * CLUSTER_SIZE;
984
985 std::vector<ScalarField> scalars;
986 scalars.reserve(num_pts);
987 const uint256_t high_bit(0, 0, 0, uint64_t{ 1 } << (200 - 192));
988 for (size_t i = 0; i < NUM_CLUSTERS; ++i) {
989 const ScalarField val = ScalarField(high_bit + uint256_t(i + 1));
990 for (size_t j = 0; j < CLUSTER_SIZE; ++j) {
991 scalars.push_back(val);
992 }
993 }
994
995 std::vector<AffineElement> points = make_repeated_test_points(num_pts);
996 PolynomialSpan<ScalarField> scalar_span(0, scalars);
997
999 scalar_multiplication::MSM<Curve>::msm(points, scalar_span, /*handle_edge_cases=*/false, true);
1000 AffineElement expected = naive_msm(std::span<ScalarField>(scalars), std::span<const AffineElement>(points));
1001 EXPECT_EQ(result, expected);
1002 }
1003
1004 // ============================================================================
1005 // Dispatch-coverage tests for `pippenger_round_parallel`.
1006 //
1007 // The function has several branches that need to all be exercised:
1008 // * `n_input == 0` → infinity
1009 // * `pts_per_thread < MIN_PTS_PER_THREAD_FOR_PIPPENGER` → trivial_msm_threaded
1010 // (single-thread → trivial_msm, otherwise straus_msm per worker)
1011 // * Otherwise → main pippenger pipeline
1012 // - use_glv=true (n_input ≤ GLV_SMALL_N_THRESHOLD)
1013 // - use_glv=false (n_input > GLV_SMALL_N_THRESHOLD; only on huge N)
1014 // * `external_glv_doubled` provided vs not (drives one of the GLV-split branches)
1015 //
1016 // Each test below restores `bb::set_parallel_for_concurrency` to its original
1017 // value before returning, even if the assertion fails, so subsequent tests are
1018 // unaffected.
1019 // ============================================================================
1020
1037
1041 void check_internal_against_naive(size_t n, size_t start_index, const char* label)
1042 {
1043 ASSERT_LE(start_index + n, num_points) << label;
1044
1045 std::span<ScalarField> scalar_subspan(&scalars[start_index], n);
1046 std::span<const AffineElement> point_subspan(&generators[0], start_index + n);
1047 PolynomialSpan<const ScalarField> scalar_span{ start_index, scalar_subspan };
1048
1049 Element actual = scalar_multiplication::pippenger_round_parallel<Curve>(scalar_span, point_subspan);
1050
1051 Element expected;
1052 expected.self_set_infinity();
1053 for (size_t i = 0; i < n; ++i) {
1054 expected += point_subspan[start_index + i] * scalar_subspan[i];
1055 }
1056
1057 EXPECT_EQ(AffineElement(actual), AffineElement(expected))
1058 << label << " (n=" << n << ", start_index=" << start_index << ")";
1059 }
1060
1065 {
1066 ConcurrencyScope scope(1);
1067 // n_input == 0: infinity short-circuit.
1068 {
1069 std::span<const AffineElement> empty_points;
1070 std::span<ScalarField> empty_scalars;
1071 PolynomialSpan<const ScalarField> empty_span{ 0, empty_scalars };
1072 Element r = scalar_multiplication::pippenger_round_parallel<Curve>(empty_span, empty_points);
1073 EXPECT_TRUE(r.is_point_at_infinity());
1074 }
1075 // Walk N across all dispatch boundaries with a single thread. With 1 thread,
1076 // pts_per_thread == n; the trivial dispatch fires up to N=23, falls through
1077 // at N=24+. The fall-through path then runs the affine pippenger with
1078 // num_threads=1.
1079 for (size_t n : { size_t{ 1 },
1080 size_t{ 2 },
1081 size_t{ 3 },
1082 size_t{ 4 },
1083 size_t{ 23 },
1084 size_t{ 24 },
1085 size_t{ 25 },
1086 size_t{ 32 },
1087 size_t{ 64 },
1088 size_t{ 100 },
1089 size_t{ 192 },
1090 size_t{ 1000 } }) {
1091 check_internal_against_naive(n, 0, "single_thread");
1092 }
1093 }
1094
1098 {
1099 ConcurrencyScope scope(1);
1100 constexpr size_t kThreshold = scalar_multiplication::MIN_PTS_PER_THREAD_FOR_PIPPENGER;
1101 check_internal_against_naive(kThreshold + 1, 0, "single_thread n=Threshold+1");
1102 // Also exercise N just below where `chunk_len = n / num_threads = n / 1 = n`
1103 // approaches MIN_BATCH_CAPACITY=32 — the (now-removed) brittle fallback used
1104 // to fire here; we want the affine path to still run and produce correct
1105 // output even with very small chunks.
1106 for (size_t n : { kThreshold + 1, size_t{ 32 }, size_t{ 33 }, size_t{ 50 }, size_t{ 100 } }) {
1107 check_internal_against_naive(n, 0, "single_thread small-chunk");
1108 }
1109 }
1110
1115 {
1116 constexpr size_t kThreshold = scalar_multiplication::MIN_PTS_PER_THREAD_FOR_PIPPENGER;
1117 for (size_t threads : { size_t{ 2 }, size_t{ 4 }, size_t{ 8 }, size_t{ 16 } }) {
1118 ConcurrencyScope scope(threads);
1119 // Dispatch boundary is at n = threads * kThreshold (= pts_per_thread = 24).
1120 const size_t boundary = threads * kThreshold;
1121 for (size_t n : { boundary - 1, boundary, boundary + 1 }) {
1122 check_internal_against_naive(n, 0, "dispatch_boundary");
1123 }
1124 }
1125 }
1126
1131 {
1132 ConcurrencyScope scope(8);
1133 // Small N (will dispatch to trivial_msm_threaded).
1134 check_internal_against_naive(/*n=*/64, /*start_index=*/17, "offset small-N");
1135 // Just above dispatch threshold (8 threads → boundary at 192).
1136 check_internal_against_naive(/*n=*/200, /*start_index=*/13, "offset just-above-boundary");
1137 // Mid-N falls through into pippenger.
1138 check_internal_against_naive(/*n=*/1024, /*start_index=*/41, "offset mid-N");
1139 }
1140
1145 {
1146 ConcurrencyScope scope(8);
1147 // Save and restore the global scalars buffer.
1148 std::vector<ScalarField> saved(scalars.begin(), scalars.begin() + 1024);
1149 for (size_t i = 0; i < saved.size(); ++i) {
1150 scalars[i] = ScalarField::zero();
1151 }
1152 for (size_t n : { size_t{ 1 }, size_t{ 24 }, size_t{ 100 }, size_t{ 1000 } }) {
1153 std::span<ScalarField> sub(&scalars[0], n);
1154 std::span<const AffineElement> pts(&generators[0], n);
1156 Element r = scalar_multiplication::pippenger_round_parallel<Curve>(sp, pts);
1157 EXPECT_TRUE(r.is_point_at_infinity()) << "all-zero n=" << n;
1158 }
1159 // Restore.
1160 for (size_t i = 0; i < saved.size(); ++i) {
1161 scalars[i] = saved[i];
1162 }
1163 }
1164
1168 {
1169 ConcurrencyScope scope(8);
1170 std::vector<ScalarField> saved(scalars.begin(), scalars.begin() + 1024);
1171 // Zero out every other scalar.
1172 for (size_t i = 0; i < 1024; i += 2) {
1173 scalars[i] = ScalarField::zero();
1174 }
1175 for (size_t n : { size_t{ 24 }, size_t{ 100 }, size_t{ 1024 } }) {
1176 check_internal_against_naive(n, 0, "mixed-zero");
1177 }
1178 // Restore.
1179 for (size_t i = 0; i < saved.size(); ++i) {
1180 scalars[i] = saved[i];
1181 }
1182 }
1183
1188 {
1189 ConcurrencyScope scope(8);
1190 std::vector<ScalarField> saved(scalars.begin(), scalars.begin() + 256);
1191
1192 // Scalar = 1
1193 for (auto& s : saved) {
1194 (void)s;
1195 }
1196 for (size_t i = 0; i < 256; ++i) {
1197 scalars[i] = ScalarField::one();
1198 }
1199 check_internal_against_naive(256, 0, "scalar=1");
1200
1201 // Scalar = -1
1202 for (size_t i = 0; i < 256; ++i) {
1203 scalars[i] = -ScalarField::one();
1204 }
1205 check_internal_against_naive(256, 0, "scalar=-1");
1206
1207 // Restore.
1208 for (size_t i = 0; i < saved.size(); ++i) {
1209 scalars[i] = saved[i];
1210 }
1211 }
1212
1217 {
1218 for (size_t threads : { size_t{ 1 }, size_t{ 2 }, size_t{ 4 }, size_t{ 8 } }) {
1219 ConcurrencyScope scope(threads);
1220 for (size_t n : { size_t{ 1 }, size_t{ 2 }, size_t{ 8 }, size_t{ 32 }, size_t{ 80 }, size_t{ 160 } }) {
1221 std::span<ScalarField> sub(&scalars[0], n);
1222 std::span<const AffineElement> pts(&generators[0], n);
1224 Element actual = scalar_multiplication::trivial_msm_threaded<Curve>(sp, pts);
1225 Element expected;
1226 expected.self_set_infinity();
1227 for (size_t i = 0; i < n; ++i) {
1228 expected += pts[i] * sub[i];
1229 }
1230 EXPECT_EQ(AffineElement(actual), AffineElement(expected))
1231 << "trivial_msm_threaded threads=" << threads << " n=" << n;
1232 }
1233 }
1234 }
1235
1241 {
1242 ConcurrencyScope scope(8);
1243#ifdef __wasm__
1244 constexpr size_t glv_threshold = size_t{ 1 } << 16;
1245#else
1246 constexpr size_t glv_threshold = size_t{ 1 } << 13;
1247#endif
1248 if (glv_threshold >= num_points) {
1249 GTEST_SKIP() << "GLV threshold " << glv_threshold << " not exercisable with " << num_points
1250 << " precomputed points";
1251 }
1252 // Just below threshold: use_glv=true.
1253 check_internal_against_naive(glv_threshold - 1, 0, "glv-boundary minus-1 (use_glv=true)");
1254 // Exactly at threshold: use_glv=true (≤ comparison).
1255 check_internal_against_naive(glv_threshold, 0, "glv-boundary exact (use_glv=true)");
1256 // Just above: use_glv=false.
1257 check_internal_against_naive(glv_threshold + 1, 0, "glv-boundary plus-1 (use_glv=false)");
1258 }
1259
1271 {
1272 ConcurrencyScope scope(1);
1273 constexpr size_t kThreshold = scalar_multiplication::MIN_PTS_PER_THREAD_FOR_PIPPENGER;
1274 for (size_t n : { kThreshold + 1, size_t{ 50 }, size_t{ 100 }, size_t{ 256 } }) {
1275 std::span<ScalarField> scalar_subspan(&scalars[0], n);
1276 std::span<const AffineElement> point_subspan(&generators[0], n);
1277 PolynomialSpan<const ScalarField> scalar_span{ 0, scalar_subspan };
1278
1279 constexpr size_t kArenaCapacity = size_t{ 64 } * 1024 * 1024;
1280 std::vector<std::byte> raw(kArenaCapacity + 64);
1281 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
1282 const auto base = reinterpret_cast<uintptr_t>(raw.data());
1283 const uintptr_t aligned32 = (base + 31) & ~uintptr_t{ 31 };
1284 std::byte* misaligned = raw.data() + (aligned32 - base) + 16;
1285 ASSERT_EQ(reinterpret_cast<uintptr_t>(misaligned) % 32, size_t{ 16 });
1286 std::span<std::byte> external_arena(misaligned, kArenaCapacity);
1287
1288 Element actual = scalar_multiplication::pippenger_round_parallel<Curve>(
1289 scalar_span, point_subspan, /*dedup_hint=*/false, {}, external_arena);
1290
1291 Element expected;
1292 expected.self_set_infinity();
1293 for (size_t i = 0; i < n; ++i) {
1294 expected += point_subspan[i] * scalar_subspan[i];
1295 }
1296 EXPECT_EQ(AffineElement(actual), AffineElement(expected)) << "misaligned external arena (n=" << n << ")";
1297 }
1298 }
1299
1300 // ============================================================================
1301 // Degenerate-input edge cases for the `handle_edge_cases=true` (Jacobian) path,
1302 // at LARGE N and forced multi-threading.
1303 //
1304 // `scalar_multiplication_safe_mode.test.cpp` already covers point-at-infinity,
1305 // P/-P negation, and all-infinity — but only at tiny N (≤ 60 points), which on
1306 // native stays single-threaded (the Jacobian path multi-threads only at
1307 // n ≥ 512). These tests push the same degenerate inputs through the
1308 // multi-threaded Jacobian split + cross-thread reduction, where a per-slice
1309 // infinity / equal-x collision is folded into a per-thread partial before the
1310 // final cross-thread sum. We pin 8 threads so the multi-thread path runs
1311 // regardless of the CI machine's core count.
1312 // ============================================================================
1313
1318 {
1319 ConcurrencyScope scope(8);
1320 auto& rng = numeric::get_debug_randomness(true, 0x5eedu + 101);
1321#ifdef __wasm__
1322 const std::vector<size_t> sizes = { 64 };
1323#else
1324 // 3000 crosses the Jacobian path's native multi-thread split (>256 pts/thread).
1325 const std::vector<size_t> sizes = { 64, 3000 };
1326#endif
1327 for (size_t n : sizes) {
1328 std::vector<AffineElement> points(n);
1329 std::vector<ScalarField> test_scalars(n);
1330 for (size_t i = 0; i < n; ++i) {
1331 points[i] = AffineElement(Element::random_element(&rng));
1332 test_scalars[i] = ScalarField::random_element(&rng);
1333 }
1334 for (size_t idx : { size_t{ 0 }, n / 2, n - 1 }) {
1335 points[idx].self_set_infinity();
1336 }
1337 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
1339 scalar_multiplication::MSM<Curve>::msm(points, scalar_span, /*handle_edge_cases=*/true);
1340 AffineElement expected = naive_msm(test_scalars, points);
1341 EXPECT_EQ(result, expected) << "point-at-infinity inputs (n=" << n << ")";
1342 }
1343 }
1344
1349 {
1350 ConcurrencyScope scope(8);
1351 auto& rng = numeric::get_debug_randomness(true, 0x5eedu + 102);
1352#ifdef __wasm__
1353 const std::vector<size_t> pair_counts = { 40 };
1354#else
1355 const std::vector<size_t> pair_counts = { 40, 800 };
1356#endif
1357 for (size_t pairs : pair_counts) {
1358 const size_t n = (2 * pairs) + 3; // + a few linearly-independent singletons
1359 std::vector<AffineElement> points(n);
1360 std::vector<ScalarField> test_scalars(n);
1361 for (size_t p = 0; p < pairs; ++p) {
1362 AffineElement r(Element::random_element(&rng));
1363 AffineElement neg_r;
1364 neg_r.x = r.x;
1365 neg_r.y = -r.y;
1366 const ScalarField s = ScalarField::random_element(&rng);
1367 points[2 * p] = r;
1368 points[(2 * p) + 1] = neg_r;
1369 test_scalars[2 * p] = s;
1370 test_scalars[(2 * p) + 1] = s;
1371 }
1372 for (size_t i = 2 * pairs; i < n; ++i) {
1373 points[i] = AffineElement(Element::random_element(&rng));
1374 test_scalars[i] = ScalarField::random_element(&rng);
1375 }
1376 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
1378 scalar_multiplication::MSM<Curve>::msm(points, scalar_span, /*handle_edge_cases=*/true);
1379 AffineElement expected = naive_msm(test_scalars, points);
1380 EXPECT_EQ(result, expected) << "inverse-pair bucket collisions (pairs=" << pairs << ")";
1381 }
1382 }
1383
1391 {
1392 using BaseField = typename Curve::BaseField;
1393 const BaseField beta = BaseField::cube_root_of_unity();
1394 namespace rpd = scalar_multiplication::round_parallel_detail;
1395 const std::vector<size_t> sizes = { 50, 1000, rpd::GLV_SMALL_N_THRESHOLD + 64 };
1396 for (size_t n : sizes) {
1397 ASSERT_LE(n, num_points);
1398 std::span<const AffineElement> points(&generators[0], n);
1399 std::vector<ScalarField> test_scalars(n);
1400 for (size_t i = 0; i < n; ++i) {
1401 test_scalars[i] = scalars[i];
1402 }
1403 std::vector<AffineElement> doubled(2 * n);
1404 for (size_t i = 0; i < n; ++i) {
1405 doubled[2 * i] = points[i];
1406 doubled[(2 * i) + 1].x = points[i].x * beta;
1407 doubled[(2 * i) + 1].y = -points[i].y;
1408 }
1409 PolynomialSpan<const ScalarField> scalar_span(0, std::span<const ScalarField>(test_scalars));
1410 Element result =
1411 scalar_multiplication::pippenger_round_parallel<Curve>(scalar_span,
1412 points,
1413 /*dedup_hint=*/false,
1414 std::span<const AffineElement>(doubled));
1415 AffineElement expected = naive_msm(test_scalars, points);
1416 EXPECT_EQ(AffineElement(result), expected) << "external_glv_doubled (n=" << n << ")";
1417 }
1418 }
1419
1420 // ============================================================================
1421 // GLV split / signed-Booth recoder edge cases.
1422 //
1423 // The GLV path (use_glv) feeds `split_into_endomorphism_scalars` output into the
1424 // round-parallel pipeline as 2-limb (≤128-bit) halves k1, k2 with limbs[2],[3]
1425 // forced to 0 (scalar_multiplication_fast.cpp ~1400). The window schedule is built
1426 // for NUM_BITS=128 (+2 carry). If any half's true magnitude needed bit 128+ — or if
1427 // the top signed-Booth window digit carried past the final window — the recoder would
1428 // silently drop those bits and the MSM result would be wrong while no assert trips.
1429 //
1430 // `PippengerInternalExtremeScalars` already pins scalar=±1; this widens coverage to
1431 // the values that maximise |k1| / |k2|: r-1, (r-1)/2, λ, λ±1, the k2-negative-fix
1432 // boundary (data[1] high bit set just below 2^128), and a deterministic sweep that
1433 // checks every split half stays ≤128 bits before trusting the pipeline output. We run
1434 // every value through the real internal path at an N below the GLV threshold so
1435 // use_glv=true, comparing to a naive reference.
1436 // ============================================================================
1438 {
1439 ConcurrencyScope scope(8);
1440 namespace rpd = scalar_multiplication::round_parallel_detail;
1441 // N small enough to force use_glv=true on both native (2^13) and wasm (2^16),
1442 // but large enough to clear MIN_PTS_PER_THREAD_FOR_PIPPENGER and run the affine
1443 // pippenger (not the trivial bail).
1444 constexpr size_t n = 600;
1445 static_assert(n <= 256 + 4096, "keep n below the native GLV threshold");
1446
1447 const uint256_t r = ScalarField::modulus;
1448
1449 // Worst-case magnitude probes. Each is reduced mod r by the uint256_t ctor.
1450 std::vector<ScalarField> probes;
1451 probes.push_back(-ScalarField::one()); // r - 1
1452 probes.push_back(ScalarField(uint256_t(1))); // 1
1453 probes.push_back(ScalarField(r - uint256_t(1)) * ScalarField(uint256_t(2)).invert()); // (r-1)/2
1454 // Powers-of-two boundaries around the 128-bit split width.
1455 for (size_t bit : { size_t{ 126 }, size_t{ 127 }, size_t{ 128 }, size_t{ 129 }, size_t{ 253 } }) {
1456 probes.push_back(ScalarField(uint256_t(1) << bit));
1457 probes.push_back(ScalarField((uint256_t(1) << bit) - uint256_t(1)));
1458 probes.push_back(ScalarField((uint256_t(1) << bit) + uint256_t(1)));
1459 }
1460 // r/2 ± small, and r - small: the region where compute_endomorphism_k2 can drive
1461 // k2 slightly negative (the data[2]||data[3] != 0 → +endo_minus_b1 correction).
1462 for (uint64_t delta = 0; delta < 8; ++delta) {
1463 probes.push_back(ScalarField(r - uint256_t(delta + 1)));
1464 probes.push_back(ScalarField((r >> 1) + uint256_t(delta)));
1465 }
1466 // Deterministic pseudo-random fill of additional probes (Knuth multiplicative
1467 // hash) so the sweep also covers interior values, not just the curated extremes.
1468 for (uint64_t i = 1; i <= 16; ++i) {
1469 const uint64_t h0 = i * uint64_t{ 0x9E3779B97F4A7C15ULL };
1470 const uint64_t h1 = (i + 1) * uint64_t{ 0xC2B2AE3D27D4EB4FULL };
1471 const uint64_t h2 = (i + 2) * uint64_t{ 0x165667B19E3779F9ULL };
1472 const uint64_t h3 = (i + 3) * uint64_t{ 0xD6E8FEB86659FD93ULL };
1473 probes.push_back(ScalarField(uint256_t(h0, h1, h2, h3)));
1474 }
1475
1476 // Invariant the whole GLV path leans on: the two 128-bit halves the pipeline stores
1477 // (limbs[2]/[3] forced to 0) recombine — via the SAME doubled-point convention the
1478 // MSM uses, φP = (β·x, −y) — back to k·P. If the field ever produced a half needing
1479 // bit 128+, the 2-limb storage at ~line 1400 would truncate it and this point identity
1480 // would break, catching the silent wrong result independent of the GLV λ-sign details.
1481 {
1482 using BaseField = typename Curve::BaseField;
1483 const BaseField beta = BaseField::cube_root_of_unity();
1484 const AffineElement base = generators[0];
1485 AffineElement phi;
1486 phi.x = base.x * beta;
1487 phi.y = -base.y;
1488 for (const ScalarField& s : probes) {
1489 const ScalarField canonical = s.from_montgomery_form_reduced();
1490 const auto split = ScalarField::split_into_endomorphism_scalars(canonical);
1491 // Rebuild k1, k2 from ONLY the 2 returned limbs (exactly what the pipeline does).
1492 const ScalarField k1 = ScalarField(uint256_t(split.first[0], split.first[1], 0, 0));
1493 const ScalarField k2 = ScalarField(uint256_t(split.second[0], split.second[1], 0, 0));
1494 const Element recombined = Element(base) * k1 + Element(phi) * k2;
1495 EXPECT_EQ(AffineElement(recombined), AffineElement(Element(base) * s))
1496 << "GLV split half exceeded 128-bit storage or mis-signed";
1497 }
1498 }
1499
1500 // Now run each probe through the real internal pipeline (use_glv=true) and compare
1501 // to naive. We fill all n scalars with the probe so every working half hits the
1502 // same extreme window, maximising any top-window carry interaction.
1503 std::vector<ScalarField> saved(scalars.begin(), scalars.begin() + n);
1504 for (size_t p = 0; p < probes.size(); ++p) {
1505 for (size_t i = 0; i < n; ++i) {
1506 scalars[i] = probes[p];
1507 }
1508 check_internal_against_naive(n, 0, "glv-extreme-magnitude");
1509 }
1510 // Mixed: alternate r-1 and (interior) probes so k1/k2 of adjacent working scalars
1511 // land in different windows within one MSM.
1512 for (size_t i = 0; i < n; ++i) {
1513 scalars[i] = (i & 1) ? -ScalarField::one() : probes[probes.size() - 1 - (i % probes.size())];
1514 }
1515 check_internal_against_naive(n, 0, "glv-extreme-mixed");
1516
1517 for (size_t i = 0; i < saved.size(); ++i) {
1518 scalars[i] = saved[i];
1519 }
1520 }
1521
1522 // ============================================================================
1523 // effective_num_bits schedule divergence (sizer vs live allocator).
1524 //
1525 // The live path (scalar_multiplication_fast.cpp ~1474) shrinks the window-bit budget
1526 // to the highest observed scalar msb, then re-runs choose_window_bits — which can pick
1527 // a SMALLER window_bits (⇒ MORE windows ⇒ a larger wpb·per_window_bytes Zone S) than the
1528 // full-NUM_BITS pre-sizer. `compute_arena_bytes_for_msm`'s defensive bit-budget sweep
1529 // (lines 1246-1250) only runs for use_glv OR n_input ≥ 2^17. For the native non-GLV
1530 // mid-band 2^13 < n < 2^17 the sweep is SKIPPED, so a workload of uniformly small-msb
1531 // scalars selects a schedule the sizer never sized for. If the live Zone S then exceeds
1532 // the arena, this is a guaranteed out-of-bounds write (SIGSEGV), not a wrong result.
1533 //
1534 // We drive the real MSM in that band with all-small scalars (msb ≪ 254) and compare to
1535 // naive. A correct sizer makes this pass; an under-count crashes under ASAN / corrupts
1536 // the answer. Native-only: the band does not exist on wasm (GLV up to 2^16).
1537 // ============================================================================
1539 {
1540#ifdef __wasm__
1541 GTEST_SKIP() << "non-GLV mid-band (2^13<n<2^17) does not exist on wasm";
1542#else
1543 ConcurrencyScope scope(8);
1544 namespace rpd = scalar_multiplication::round_parallel_detail;
1545 const size_t glv_threshold = rpd::GLV_SMALL_N_THRESHOLD; // 2^13 native
1546 // Sizes just above the GLV threshold (use_glv=false) and inside the sweep-skipped
1547 // band. num_points is 31013, so every size below stays in-bounds.
1548 const std::vector<size_t> sizes = { glv_threshold + 1, size_t{ 1 } << 14 };
1549 // Bit budgets to drive effective_num_bits to representative small/mid schedules.
1550 const std::vector<size_t> bit_widths = { 1, 64, 120 };
1551
1552 size_t max_size = 0;
1553 for (size_t s : sizes) {
1554 if (s <= num_points) {
1555 max_size = std::max(max_size, s);
1556 }
1557 }
1558 std::vector<ScalarField> saved(scalars.begin(), scalars.begin() + static_cast<std::ptrdiff_t>(max_size));
1559 auto& rng = numeric::get_debug_randomness(true, 0x5eedu + 909);
1560 for (size_t n : sizes) {
1561 if (n > num_points) {
1562 continue;
1563 }
1564 for (size_t bits : bit_widths) {
1565 const uint256_t mask = (bits >= 256) ? ~uint256_t(0) : ((uint256_t(1) << bits) - uint256_t(1));
1566 for (size_t i = 0; i < n; ++i) {
1567 // Random value masked to `bits` bits; force the top bit so msb == bits-1
1568 // for at least some scalars, pinning effective_num_bits == bits.
1569 uint256_t v(rng.get_random_uint64(),
1570 rng.get_random_uint64(),
1571 rng.get_random_uint64(),
1572 rng.get_random_uint64());
1573 v = v & mask;
1574 if (i == 0 && bits >= 1) {
1575 v = v | (uint256_t(1) << (bits - 1));
1576 }
1577 scalars[i] = ScalarField(v);
1578 }
1579 check_internal_against_naive(n, 0, "effective-num-bits-band");
1580 }
1581 }
1582 for (size_t i = 0; i < saved.size(); ++i) {
1583 scalars[i] = saved[i];
1584 }
1585#endif
1586 }
1587
1588 // ============================================================================
1589 // Dedup multi-chunk tree-reduce carry + cap fallback.
1590 //
1591 // pippenger_dedup.hpp's Phase A tree-reduce (lines ~470-520) consolidates each
1592 // equal-value cluster's base points. A cluster larger than DEDUP_MAX_CHUNK_MEMBERS
1593 // (2048) is split across chunks and its partial sum threaded via the `carry` slot;
1594 // a cluster larger than the per-bucket staged cap (~1024) is only PARTIALLY deduped,
1595 // with the overflow members falling through to the normal pippenger path with their
1596 // original signed digits. Both the carry-threading and the partial-consolidation
1597 // fallback must produce the same sum as no dedup at all.
1598 //
1599 // We build inputs with one (or a few) huge equal-value clusters of identical points
1600 // so a single combined point + many fall-through members coexist in one MSM, then
1601 // compare dedup_hint=true against naive. Sizes are chosen to exceed both the chunk
1602 // cap (2048) and the staged cap, exercising the carry and the cap fallback in the
1603 // same run. Native-only for the largest sizes (wasm runs the smaller ones).
1604 // ============================================================================
1606 {
1607 ConcurrencyScope scope(8);
1608 auto& rng = numeric::get_debug_randomness(true, 0x5eedu + 303);
1609
1610#ifdef __wasm__
1611 const std::vector<size_t> cluster_sizes = { 2100 };
1612#else
1613 // 2100 > DEDUP_MAX_CHUNK_MEMBERS(2048) → multi-chunk carry.
1614 // 5000 → several carries + staged-cap partial-consolidation fallback.
1615 const std::vector<size_t> cluster_sizes = { 2100, 5000 };
1616#endif
1617 for (size_t cluster : cluster_sizes) {
1618 // Dedup clusters by equal scalar VALUE, combining the cluster's DISTINCT base
1619 // points into one (rep, Σpoints) pair. So the giant cluster is `cluster` distinct
1620 // generators all sharing scalar s_big — the realistic dedup trigger (range-check /
1621 // counter polynomials) and a valid input for the affine path (distinct x). The
1622 // tree-reduce then sums distinct points (no equal-x affine-add edge case), and the
1623 // cluster size drives the multi-chunk carry + staged-cap fallback.
1624 const size_t singles = 400;
1625 const size_t medium = 600;
1626 const size_t n = cluster + medium + singles;
1627 ASSERT_LE(n, num_points);
1628
1629 std::vector<ScalarField> test_scalars(n);
1630 std::vector<AffineElement> points(n);
1631
1632 const ScalarField s_big = ScalarField::random_element(&rng);
1633 const ScalarField s_med = ScalarField::random_element(&rng);
1634 ASSERT_NE(s_big, s_med);
1635
1636 for (size_t i = 0; i < cluster; ++i) {
1637 points[i] = generators[i];
1638 test_scalars[i] = s_big;
1639 }
1640 for (size_t i = 0; i < medium; ++i) {
1641 points[cluster + i] = generators[cluster + i];
1642 test_scalars[cluster + i] = s_med;
1643 }
1644 for (size_t i = 0; i < singles; ++i) {
1645 points[cluster + medium + i] = generators[cluster + medium + i];
1646 test_scalars[cluster + medium + i] = ScalarField::random_element(&rng);
1647 }
1648
1649 PolynomialSpan<ScalarField> scalar_span(0, test_scalars);
1650 // dedup_hint=true forces Phase A; handle_edge_cases=false stays on the affine
1651 // path (all points/distinct-x are valid for it).
1653 points, scalar_span, /*handle_edge_cases=*/false, /*dedup_hint=*/true);
1654 AffineElement expected = naive_msm(test_scalars, points);
1655 EXPECT_EQ(deduped, expected) << "dedup large-cluster carry/caps (cluster=" << cluster << ", n=" << n << ")";
1656
1657 // Cross-check: the same input with dedup_hint=false must agree (dedup is a pure
1658 // optimisation; a mismatch isolates the regression to the dedup pre-pass).
1659 PolynomialSpan<ScalarField> scalar_span2(0, test_scalars);
1661 points, scalar_span2, /*handle_edge_cases=*/false, /*dedup_hint=*/false);
1662 EXPECT_EQ(deduped, undeduped) << "dedup vs no-dedup divergence (cluster=" << cluster << ")";
1663 }
1664 }
1665
1680 void run_batch_driver_paths(bool handle_edge_cases)
1681 {
1682 // 16384 > native GLV threshold (2^13) → its own non-GLV group; the rest are GLV.
1683 const std::vector<size_t> sizes = { 0, 1, 5, 0, 4096, 33, 0, 16384, 64, 2 };
1684 const size_t num_msms = sizes.size();
1685
1686 std::vector<std::vector<ScalarField>> batch_scalars(num_msms);
1687 std::vector<std::vector<ScalarField>> scalar_copies(num_msms);
1689 std::vector<AffineElement> expected(num_msms);
1690 std::vector<uint32_t> dedup_hints(num_msms, 0);
1691
1692 size_t offset = 0;
1693 for (size_t k = 0; k < num_msms; ++k) {
1694 const size_t n = sizes[k];
1695 ASSERT_LE(offset + n, num_points);
1696 batch_scalars[k].resize(n);
1697 scalar_copies[k].resize(n);
1698 const bool all_zero = (k % 4 == 2); // a couple of fully-zero MSMs → infinity result
1699 for (size_t i = 0; i < n; ++i) {
1700 batch_scalars[k][i] = all_zero ? ScalarField::zero() : scalars[offset + i];
1701 scalar_copies[k][i] = batch_scalars[k][i];
1702 }
1703 dedup_hints[k] = (k % 2 == 0) ? 1U : 0U;
1704 spans.emplace_back(offset, std::span<ScalarField>(batch_scalars[k]));
1705 std::span<const AffineElement> pts(&generators[offset], n);
1706 expected[k] = naive_msm(batch_scalars[k], pts);
1707 offset += n;
1708 }
1709
1711 generators, spans, handle_edge_cases, std::span<const uint32_t>(dedup_hints));
1712
1713 ASSERT_EQ(result.size(), num_msms);
1714 for (size_t k = 0; k < num_msms; ++k) {
1715 EXPECT_EQ(result[k], expected[k]) << "batch MSM " << k << " (n=" << sizes[k]
1716 << ", handle_edge_cases=" << handle_edge_cases << ") mismatched";
1717 EXPECT_EQ(batch_scalars[k], scalar_copies[k]) << "batch MSM " << k << " scalars were not restored";
1718 }
1719 }
1720
1721 void test_batch_driver_shared_path() { run_batch_driver_paths(/*handle_edge_cases=*/false); }
1722};
1723
1724using CurveTypes = ::testing::Types<bb::curve::BN254, bb::curve::Grumpkin>;
1726
1727TEST(ScalarMultiplicationArenaTest, LargeBn254RecursionVkShapeFitsComputedArena)
1728{
1729 const size_t saved_threads = bb::get_num_cpus();
1730
1731 // CI regression from HonkRecursionConstraintTestWithoutPredicate/2.GenerateVKFromConstraints:
1732 // Zone S attempted a uint32_t schedule allocation whose aligned end was 26,454,272
1733 // bytes after the computed arena left only 25,505,329 bytes in Zone S. The log does
1734 // not expose windows_per_batch, so cover every plausible n_input divisor for that
1735 // schedule size.
1736 constexpr size_t schedule_slots = size_t{ 26454272 } / sizeof(uint32_t);
1737 constexpr std::array<size_t, 8> candidate_window_batches{ 1, 2, 4, 8, 13, 16, 26, 32 };
1738 for (const size_t threads : { size_t{ 4 }, size_t{ 32 } }) {
1740 for (const size_t windows_per_batch : candidate_window_batches) {
1741 const size_t n = schedule_slots / windows_per_batch;
1742 for (size_t effective_num_bits = 1; effective_num_bits <= 254; ++effective_num_bits) {
1743 EXPECT_TRUE(pippenger_bn254_arena_layout_fits_for_test(
1744 n, /*external_glv_provided=*/false, /*dedup_active=*/false, effective_num_bits))
1745 << "threads=" << threads << " windows_per_batch=" << windows_per_batch << " n=" << n
1746 << " effective_num_bits=" << effective_num_bits;
1747 }
1748 }
1749 }
1750
1751 bb::set_parallel_for_concurrency(saved_threads);
1752}
1753
1754// Sweeps the sizer/allocator agreement (the historical "arena drift" bug class) across the
1755// full dispatch parameter space rather than the single recursion-VK shape above: thread count,
1756// N around every dispatch boundary, GLV-provided vs not, dedup-active vs not, and a range of
1757// effective bit budgets. `pippenger_bn254_arena_layout_fits_for_test` walks the live
1758// Zone P / Zone W / Zone S allocator and must always fit inside `compute_arena_bytes_for_msm`'s
1759// promise; any `false` here is an under-count (a guaranteed Zone overflow / SIGSEGV at runtime).
1760// Notably this is the first coverage of `dedup_active=true` and `external_glv_provided=true`
1761// against the sizer.
1762TEST(ScalarMultiplicationArenaTest, ArenaLayoutFitsAcrossDispatchSpace)
1763{
1764 const size_t saved_threads = bb::get_num_cpus();
1765
1766 constexpr std::array<size_t, 6> thread_counts{ 1, 3, 8, 16, 32, 64 };
1767 // Dispatch boundaries: MIN_PTS_PER_THREAD (24), powers of two ±1, and both GLV thresholds
1768 // (2^13 native, 2^16 wasm), up to a large 2^18 shape.
1769 constexpr std::array<size_t, 21> ns{ 4, 5, 23, 24, 25, 31, 32, 33, 63, 64, 65,
1770 255, 256, 257, 4095, 4096, 4097, 8191, 8192, 8193, 262144 };
1771 constexpr std::array<size_t, 4> bit_budgets{ 0, 1, 128, 254 };
1772
1773 for (const size_t threads : thread_counts) {
1775 for (const size_t n : ns) {
1776 for (const bool ext_glv : { false, true }) {
1777 for (const bool dedup : { false, true }) {
1778 for (const size_t bits : bit_budgets) {
1779 EXPECT_TRUE(pippenger_bn254_arena_layout_fits_for_test(n, ext_glv, dedup, bits))
1780 << "threads=" << threads << " n=" << n << " ext_glv=" << ext_glv << " dedup=" << dedup
1781 << " bits=" << bits;
1782 }
1783 }
1784 }
1785 }
1786 }
1787
1788 // Deterministic pseudo-random N (Knuth multiplicative hash) to catch under-counts that do
1789 // not sit on a curated boundary. Date/Math.random are unavailable; the hash keeps CI runs
1790 // reproducible.
1791 for (const size_t threads : { size_t{ 4 }, size_t{ 8 }, size_t{ 16 }, size_t{ 32 } }) {
1793 for (size_t i = 1; i <= 32; ++i) {
1794 const size_t n = 4 + ((i * size_t{ 2654435761ULL }) % (size_t{ 1 } << 20));
1795 for (const bool dedup : { false, true }) {
1796 EXPECT_TRUE(pippenger_bn254_arena_layout_fits_for_test(n, /*external_glv_provided=*/false, dedup, 254))
1797 << "random n=" << n << " threads=" << threads << " dedup=" << dedup;
1798 }
1799 }
1800 }
1801
1802 bb::set_parallel_for_concurrency(saved_threads);
1803}
1804
1805// Non-GLV mid-band (GLV_SMALL_N_THRESHOLD < n < 2^17) arena-sizing coverage. The live allocator
1806// shrinks the window-bit budget to the observed scalar msb, which can pick a heavier schedule than
1807// the full-bit pre-sizer; `compute_arena_bytes_for_msm` must upper-bound the arena across every
1808// effective_num_bits. Regression for the `bb prove` abort `Assertion failed: (aligned_local + bytes
1809// <= bound)` on UltraHonk simple_shield (~28,696-point commitment MSM, 8 threads): that n sits in
1810// this band, which the dispatch sweep (probing only 8193 and 262144) and the small-scalar band test
1811// (n <= 16384) both miss.
1812TEST(ScalarMultiplicationArenaTest, MidBandArenaSizerCoversAllEffectiveNumBits)
1813{
1814 const size_t saved_threads = bb::get_num_cpus();
1816
1817 bool found_undersize = false;
1818 for (const size_t n :
1819 { size_t{ 28696 }, size_t{ 8193 }, size_t{ 1 } << 14, size_t{ 1 } << 15, size_t{ 1 } << 16 }) {
1820 for (size_t bits = 1; bits <= 254; ++bits) {
1821 if (!pippenger_bn254_arena_layout_fits_for_test(n,
1822 /*external_glv_provided=*/false,
1823 /*dedup_active=*/false,
1824 bits)) {
1825 info("UNDERSIZE: n=", n, " effective_num_bits=", bits, " threads=8");
1826 found_undersize = true;
1827 }
1828 }
1829 }
1830
1831 bb::set_parallel_for_concurrency(saved_threads);
1832 EXPECT_FALSE(found_undersize) << "arena sizer under-counts in the non-GLV mid-band";
1833}
1834
1835// ======================= Test Wrappers =======================
1836
1838{
1839 this->test_pippenger_low_memory();
1840}
1842{
1843 this->test_batch_multi_scalar_mul();
1844}
1845TYPED_TEST(ScalarMultiplicationTest, BatchMultiScalarMulSparse)
1846{
1847 this->test_batch_multi_scalar_mul_sparse();
1848}
1849TYPED_TEST(ScalarMultiplicationTest, BatchMultiScalarMulLargeDense)
1850{
1851 this->test_batch_multi_scalar_mul_large_dense();
1852}
1853TYPED_TEST(ScalarMultiplicationTest, BatchMultiScalarMulRagged)
1854{
1855 this->test_batch_multi_scalar_mul_ragged();
1856}
1857TYPED_TEST(ScalarMultiplicationTest, BatchMultiScalarMulSmallMemberDispatch)
1858{
1859 this->test_batch_multi_scalar_mul_small_member_dispatch();
1860}
1862{
1863 this->test_msm();
1864}
1866{
1867 this->test_msm_all_zeroes();
1868}
1870{
1871 this->test_msm_empty_polynomial();
1872}
1873TYPED_TEST(ScalarMultiplicationTest, ScalarsUnchangedAfterMSM)
1874{
1875 this->test_scalars_unchanged_after_msm();
1876}
1877TYPED_TEST(ScalarMultiplicationTest, ScalarsUnchangedAfterBatchMultiScalarMul)
1878{
1879 this->test_scalars_unchanged_after_batch_multi_scalar_mul();
1880}
1881TYPED_TEST(ScalarMultiplicationTest, ScalarsUnchangedAfterLargeNonGlvMSM)
1882{
1883 this->test_scalars_unchanged_after_large_non_glv_msm();
1884}
1886{
1887 this->test_scalar_one();
1888}
1890{
1891 this->test_scalar_minus_one();
1892}
1894{
1895 this->test_single_point();
1896}
1898{
1899 this->test_size_thresholds();
1900}
1902{
1903 this->test_duplicate_points();
1904}
1906{
1907 this->test_mixed_zero_scalars();
1908}
1910{
1911 this->test_pippenger_free_function();
1912}
1913TYPED_TEST(ScalarMultiplicationTest, PippengerUnsafeFreeFunction)
1914{
1915 this->test_pippenger_unsafe_free_function();
1916}
1918{
1919 this->test_offset_span(/*n_total=*/4096, /*start_index=*/7, /*n_used=*/512, 0x5eedu + 33);
1920 this->test_offset_span(/*n_total=*/8192, /*start_index=*/4097, /*n_used=*/2048, 0x5eedu + 34);
1921}
1923{
1924#ifdef __wasm__
1925 GTEST_SKIP() << "Large synthetic MSM coverage is native-only; WASM coverage comes from integration flows.";
1926#endif
1927 this->test_large_n_non_glv();
1928}
1930{
1931#ifdef __wasm__
1932 GTEST_SKIP() << "Large synthetic MSM coverage is native-only; WASM coverage comes from integration flows.";
1933#endif
1934 this->test_msm_single_digit_mega_run();
1935}
1937{
1938#ifdef __wasm__
1939 GTEST_SKIP() << "Large synthetic MSM coverage is native-only; WASM coverage comes from integration flows.";
1940#endif
1941 this->test_msm_dedup_cap_and_carry();
1942}
1943TYPED_TEST(ScalarMultiplicationTest, MSMDedupManySmallClustersCap)
1944{
1945#ifdef __wasm__
1946 GTEST_SKIP() << "Large synthetic MSM coverage is native-only; WASM coverage comes from integration flows.";
1947#endif
1948 this->test_msm_dedup_many_small_clusters_cap();
1949}
1950
1951// Dispatch-coverage tests for `pippenger_round_parallel`.
1952TYPED_TEST(ScalarMultiplicationTest, PippengerInternalSingleThread)
1953{
1954 this->test_pippenger_internal_single_thread();
1955}
1956TYPED_TEST(ScalarMultiplicationTest, PippengerInternalSingleThreadAtDispatchThresholdPlusOne)
1957{
1958 this->test_pippenger_internal_single_thread_at_dispatch_threshold_plus_one();
1959}
1960TYPED_TEST(ScalarMultiplicationTest, PippengerInternalDispatchThresholdPerThreadCount)
1961{
1962 this->test_pippenger_internal_dispatch_threshold_per_thread_count();
1963}
1964TYPED_TEST(ScalarMultiplicationTest, PippengerInternalOffsetSpanDispatch)
1965{
1966 this->test_pippenger_internal_offset_span_dispatch();
1967}
1968TYPED_TEST(ScalarMultiplicationTest, PippengerInternalAllZeroScalars)
1969{
1970 this->test_pippenger_internal_all_zero_scalars();
1971}
1972TYPED_TEST(ScalarMultiplicationTest, PippengerInternalMixedZeroScalars)
1973{
1974 this->test_pippenger_internal_mixed_zero_scalars();
1975}
1976TYPED_TEST(ScalarMultiplicationTest, PippengerInternalExtremeScalars)
1977{
1978 this->test_pippenger_internal_extreme_scalars();
1979}
1980TYPED_TEST(ScalarMultiplicationTest, TrivialMsmThreadedPerWorkerPaths)
1981{
1982 this->test_trivial_msm_threaded_per_worker_paths();
1983}
1984TYPED_TEST(ScalarMultiplicationTest, PippengerInternalGlvBoundary)
1985{
1986 this->test_pippenger_internal_glv_boundary();
1987}
1988TYPED_TEST(ScalarMultiplicationTest, PippengerInternalMisalignedExternalArena)
1989{
1990 this->test_pippenger_internal_misaligned_external_arena();
1991}
1992TYPED_TEST(ScalarMultiplicationTest, HandleEdgeCasesPointAtInfinity)
1993{
1994 this->test_handle_edge_cases_point_at_infinity();
1995}
1996TYPED_TEST(ScalarMultiplicationTest, HandleEdgeCasesInversePairs)
1997{
1998 this->test_handle_edge_cases_inverse_pairs();
1999}
2000TYPED_TEST(ScalarMultiplicationTest, ExternalGlvDoubledDirect)
2001{
2002#ifdef __wasm__
2003 GTEST_SKIP() << "external_glv_doubled direct coverage is native-only; WASM coverage comes from batch flows.";
2004#endif
2005 this->test_external_glv_doubled_matches_naive();
2006}
2007TYPED_TEST(ScalarMultiplicationTest, GlvExtremeMagnitudeScalars)
2008{
2009#ifdef __wasm__
2010 GTEST_SKIP() << "GLV extreme-magnitude sweep is native-only; the ~50-probe naive comparison times out on wasm.";
2011#endif
2012 this->test_glv_extreme_magnitude_scalars();
2013}
2014TYPED_TEST(ScalarMultiplicationTest, EffectiveNumBitsBandSmallScalars)
2015{
2016 this->test_effective_num_bits_band_small_scalars();
2017}
2018TYPED_TEST(ScalarMultiplicationTest, DedupLargeClusterCarryAndCaps)
2019{
2020 this->test_dedup_large_cluster_carry_and_caps();
2021}
2022TYPED_TEST(ScalarMultiplicationTest, BatchDriverSharedPathRagged)
2023{
2024#ifdef __wasm__
2025 GTEST_SKIP() << "Large ragged batch coverage is native-only; WASM coverage comes from integration flows.";
2026#endif
2027 this->test_batch_driver_shared_path();
2028}
2029
2030// NOTE: the curve-independent `PartitionByWeight` unit tests that previously lived here
2031// exercised `MSM<>::MSMWorkUnit` / `MSM<>::partition_by_weight` from the OLD radix-sort +
2032// bucket-accumulator pippenger. Both were removed in the round-parallel refactor; the
2033// equivalent multi-MSM work-unit balancing logic has not yet been built (will live in
2034// `pippenger_round_parallel_batched` once Phases 1-6b are complete). The tests are left
2035// out for now and will be rewritten against the batched dispatcher's partitioner.
2036
2037// Variable-c (split-c) Pippenger dispatch — synthetic distributions per spec §"Validation".
2038// These force SPLIT to fire (cliff / decaying / half-zero / all-large) or to fall through
2039// (uniform-random / all-zero) and validate the result against `naive_msm`.
2040template <class Curve> class VariableWindowSplitDispatchTest : public ::testing::Test {
2041 public:
2042 using Group = typename Curve::Group;
2043 using Element = typename Curve::Element;
2046
2047 static AffineElement naive_msm(std::span<ScalarField> input_scalars, std::span<const AffineElement> input_points)
2048 {
2049 return ScalarMultiplicationTest<Curve>::naive_msm(input_scalars, input_points);
2050 }
2051
2052 static std::vector<AffineElement> make_points(size_t n)
2053 {
2054 std::vector<AffineElement> pts(n);
2055 parallel_for_range(n, [&](size_t s, size_t e) {
2056 for (size_t i = s; i < e; ++i) {
2057 pts[i] = Group::one * Curve::ScalarField::random_element(&engine);
2058 }
2059 });
2060 return pts;
2061 }
2062
2063 static ScalarField scalar_below_2pow(size_t bits)
2064 {
2065 // Random scalar with canonical-form msb < `bits`. We pull a random ScalarField
2066 // (Montgomery), reduce to canonical, mask the canonical representation, and
2067 // reconstruct via the canonical-uint256_t constructor (which re-Montgomery-encodes).
2068 // Masking the .data field directly would mask the Montgomery form, producing garbage.
2069 if (bits >= 254) {
2070 return ScalarField::random_element(&engine);
2071 }
2072 ScalarField r = ScalarField::random_element(&engine);
2073 ScalarField canonical = r.from_montgomery_form_reduced();
2074 auto& d = canonical.data;
2075 size_t bits_remaining = bits;
2076 for (size_t l = 0; l < 4; ++l) {
2077 const size_t take = std::min<size_t>(64, bits_remaining);
2078 const uint64_t mask = (take == 64) ? ~uint64_t{ 0 }
2079 : (take == 0) ? uint64_t{ 0 }
2080 : ((uint64_t{ 1 } << take) - 1);
2081 d[l] &= mask;
2082 if (bits_remaining > take) {
2083 bits_remaining -= take;
2084 } else {
2085 bits_remaining = 0;
2086 }
2087 }
2088 return ScalarField(uint256_t(d[0], d[1], d[2], d[3]));
2089 }
2090
2091 static void check_against_naive(std::span<ScalarField> scalars, std::span<const AffineElement> points)
2092 {
2093 AffineElement expected = naive_msm(scalars, points);
2095 EXPECT_EQ(actual, expected);
2096 }
2097
2098 static constexpr size_t kN = 131072;
2099
2101 {
2102 // All scalars < 2^30 plus 16 large scalars (full 254-bit). SPLIT must fire.
2103 constexpr size_t large_count = 16;
2104 auto pts = make_points(kN);
2105 std::vector<ScalarField> ss(kN);
2106 for (size_t i = 0; i < kN - large_count; ++i) {
2107 ss[i] = scalar_below_2pow(30);
2108 }
2109 for (size_t i = kN - large_count; i < kN; ++i) {
2110 ss[i] = ScalarField::random_element(&engine);
2111 }
2112 check_against_naive(ss, pts);
2113 }
2114
2116 {
2117 // Half below-128 + half below-160.
2118 auto pts = make_points(kN);
2119 std::vector<ScalarField> ss(kN);
2120 for (size_t k = 0; k < kN / 2; ++k) {
2121 ss[k] = scalar_below_2pow(128);
2122 }
2123 for (size_t k = kN / 2; k < kN; ++k) {
2124 ss[k] = scalar_below_2pow(160);
2125 }
2126 check_against_naive(ss, pts);
2127 }
2128
2130 {
2131 // Standard random scalars — must hit the NO_SPLIT fall-through.
2132 auto pts = make_points(kN);
2133 std::vector<ScalarField> ss(kN);
2134 for (size_t k = 0; k < kN; ++k) {
2135 ss[k] = ScalarField::random_element(&engine);
2136 }
2137 check_against_naive(ss, pts);
2138 }
2139
2141 {
2142 auto pts = make_points(kN);
2143 std::vector<ScalarField> ss(kN, ScalarField::zero());
2144 AffineElement actual =
2146 EXPECT_TRUE(actual.is_point_at_infinity());
2147 }
2148
2150 {
2151 // Half zero, half full-random.
2152 auto pts = make_points(kN);
2153 std::vector<ScalarField> ss(kN, ScalarField::zero());
2154 for (size_t k = 0; k < kN / 2; ++k) {
2155 ss[k] = ScalarField::random_element(&engine);
2156 }
2157 check_against_naive(ss, pts);
2158 }
2159
2161 {
2162 // Every scalar full-range — NO_SPLIT (Guard A rejects).
2163 auto pts = make_points(kN);
2164 std::vector<ScalarField> ss(kN);
2165 for (size_t k = 0; k < kN; ++k) {
2166 ss[k] = ScalarField::random_element(&engine);
2167 }
2168 check_against_naive(ss, pts);
2169 }
2170
2171 // Mixed-magnitude coverage: half the scalars have msb < 64, half are full-range.
2173 {
2174 auto pts = make_points(kN);
2175 std::vector<ScalarField> ss(kN);
2176 for (size_t k = 0; k < kN / 2; ++k) {
2177 ss[k] = scalar_below_2pow(60);
2178 }
2179 for (size_t k = kN / 2; k < kN; ++k) {
2180 ss[k] = ScalarField::random_element(&engine);
2181 }
2182 check_against_naive(ss, pts);
2183 }
2184
2185 // All scalars with canonical msb < 192. Triggers the GLV path's regular (non-shortcut) lattice
2186 // reduction for inputs that fit in 192 bits but not 128.
2188 {
2189 auto pts = make_points(kN);
2190 std::vector<ScalarField> ss(kN);
2191 for (size_t k = 0; k < kN; ++k) {
2192 ss[k] = scalar_below_2pow(192);
2193 }
2194 check_against_naive(ss, pts);
2195 }
2196
2197 // Coverage at the 160-bit magnitude boundary: scalars below 2^160 exercise the GLV lattice
2198 // reduction and window decomposition for non-trivial-msb inputs, checked against the naive MSM.
2200 {
2201 auto pts = make_points(kN);
2202 std::vector<ScalarField> ss(kN);
2203 for (size_t k = 0; k < kN; ++k) {
2204 ss[k] = scalar_below_2pow(160);
2205 }
2206 check_against_naive(ss, pts);
2207 }
2208};
2209
2210#ifndef __wasm__
2211using VariableWindowCurveTypes = ::testing::Types<bb::curve::BN254, bb::curve::Grumpkin>;
2213
2215{
2216 this->test_cliff();
2217}
2219{
2220 this->test_decaying();
2221}
2223{
2224 this->test_uniform_random();
2225}
2227{
2228 this->test_all_zero();
2229}
2231{
2232 this->test_half_zero();
2233}
2235{
2236 this->test_all_large();
2237}
2239{
2240 this->test_mid_distribution();
2241}
2243{
2244 this->test_below_192();
2245}
2247{
2248 this->test_force_split_bitwise_identity();
2249}
2250#endif
2251
2252// Non-templated test for explicit small inputs
2253TEST(ScalarMultiplication, SmallInputsExplicit)
2254{
2255 uint256_t x0(0x68df84429941826a, 0xeb08934ed806781c, 0xc14b6a2e4f796a73, 0x08dc1a9a11a3c8db);
2256 uint256_t y0(0x8ae5c31aa997f141, 0xe85f20c504f2c11b, 0x81a94193f3b1ce2b, 0x26f2c37372adb5b7);
2257 uint256_t x1(0x80f5a592d919d32f, 0x1362652b984e51ca, 0xa0b26666f770c2a1, 0x142c6e1964e5c3c5);
2258 uint256_t y1(0xb6c322ebb5ae4bc5, 0xf9fef6c7909c00f8, 0xb37ca1cc9af3b421, 0x1e331c7fa73d6a59);
2259 uint256_t s0(0xe48bf12a24272e08, 0xf8dd0182577f3567, 0xec8fd222b8a6becb, 0x102d76b945612c9b);
2260 uint256_t s1(0x098ae8d69f1e4e9e, 0xb5c8313c0f6040ed, 0xf78041e30cc46c44, 0x1d1e6e0c21892e13);
2261
2262 std::vector<grumpkin::fr> scalars{ s0, s1 };
2263
2266
2268
2269 auto result = scalar_multiplication::MSM<curve::Grumpkin>::msm(points, scalar_span);
2270
2271 grumpkin::g1::element expected = (points[0] * scalars[0]) + (points[1] * scalars[1]);
2272
2273 EXPECT_EQ(result, grumpkin::g1::affine_element(expected));
2274}
#define BB_BENCH_NAME(name)
Definition bb_bench.hpp:264
RAII helper to scope a bb::set_parallel_for_concurrency change to one test.
ConcurrencyScope & operator=(const ConcurrencyScope &)=delete
ConcurrencyScope & operator=(ConcurrencyScope &&)=delete
ConcurrencyScope(const ConcurrencyScope &)=delete
ConcurrencyScope(ConcurrencyScope &&)=delete
void test_offset_span(size_t n_total, size_t start_index, size_t n_used, uint64_t seed)
Validate that a non-zero start_index in the PolynomialSpan is honoured.
void run_batch_driver_paths(bool handle_edge_cases)
typename Curve::ScalarField ScalarField
static std::vector< AffineElement > generators
void test_batch_multi_scalar_mul_small_member_dispatch()
Pin the concurrent small-member dispatch in the batch driver.
void test_large_n_non_glv()
Coverage at very large N (exercises the non-GLV path on WASM, where n_input > 2^16 disables the GLV d...
void test_msm_dedup_many_small_clusters_cap()
Stress-test dedup cap fallback across many small clusters.
void test_msm_dedup_cap_and_carry()
Stress-test the dedup pass's worst-case caps and the split-cluster carry.
static std::vector< AffineElement > make_repeated_test_points(size_t num_pts)
static constexpr size_t kMaxBatchPointsPerMSM
static std::vector< ScalarField > scalars
typename Curve::AffineElement AffineElement
static AffineElement naive_msm(std::span< ScalarField > input_scalars, std::span< const AffineElement > input_points)
void test_pippenger_internal_single_thread_at_dispatch_threshold_plus_one()
void test_msm_single_digit_mega_run()
Force every Pippenger window to contain a single mega-run of one digit.
void check_internal_against_naive(size_t n, size_t start_index, const char *label)
static void check_against_naive(std::span< ScalarField > scalars, std::span< const AffineElement > points)
static AffineElement naive_msm(std::span< ScalarField > input_scalars, std::span< const AffineElement > input_points)
typename Curve::AffineElement AffineElement
static ScalarField scalar_below_2pow(size_t bits)
static std::vector< AffineElement > make_points(size_t n)
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
static AffineElement msm(std::span< const AffineElement > points, PolynomialSpan< const ScalarField > scalars, bool handle_edge_cases=false, size_t dedup_info=0) noexcept
static std::vector< AffineElement > batch_multi_scalar_mul(std::span< const AffineElement > points, std::span< PolynomialSpan< ScalarField > > scalars, bool handle_edge_cases=true, std::span< const uint32_t > dedup_infos={}) noexcept
#define info(...)
Definition log.hpp:93
std::string label
::testing::Types< bb::curve::BN254, bb::curve::Grumpkin > VariableWindowCurveTypes
numeric::RNG & engine
ssize_t offset
Definition engine.cpp:62
RNG & get_debug_randomness(bool reset, std::uint_fast64_t seed)
Definition engine.cpp:245
RNG & get_randomness()
Definition engine.cpp:258
size_t window_bits_tuning_oversub_factor(size_t n_input)
N-dependent oversubscription factor used ONLY for choose_window_bits' target_load formula (not for ac...
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
TYPED_TEST_SUITE(CommitmentKeyTest, Curves)
size_t get_num_cpus()
Definition thread.cpp:34
::testing::Types< curve::BN254, curve::Grumpkin > CurveTypes
TYPED_TEST(CommitmentKeyTest, CommitToZeroPoly)
TEST(BoomerangMegaCircuitBuilder, BasicCircuit)
void set_parallel_for_concurrency(size_t num_cores)
Definition thread.cpp:24
void parallel_for(size_t num_iterations, const std::function< void(size_t)> &func)
Definition thread.cpp:112
void parallel_for_range(size_t num_points, const std::function< void(size_t, size_t)> &func, size_t no_multhreading_if_less_or_equal)
Split a loop into several loops running in parallel.
Definition thread.cpp:142
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
Curve::Element Element
std::span< Fr > span
constexpr field invert() const noexcept
VectorField result