Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
polynomial_arithmetic.test.cpp
Go to the documentation of this file.
8#include "polynomial.hpp"
9#include <algorithm>
10#include <array>
11#include <cstddef>
12#include <gtest/gtest.h>
13#include <limits>
14#include <span>
15#include <utility>
16#include <vector>
17
18using namespace bb;
19
23TEST(polynomials, evaluate)
24{
25 auto poly1 = Polynomial<fr>(15); // non power of 2
26 auto poly2 = Polynomial<fr>(64);
27 for (size_t i = 0; i < poly1.size(); ++i) {
28 poly1.at(i) = fr::random_element();
29 poly2.at(i) = poly1[i];
30 }
31
32 auto challenge = fr::random_element();
33 auto eval1 = poly1.evaluate(challenge);
34 auto eval2 = poly2.evaluate(challenge);
35
36 EXPECT_EQ(eval1, eval2);
37}
38
39TEST(polynomials, ifft_consistency)
40{
41 constexpr size_t n = 16;
42 auto domain = evaluation_domain(n);
43 domain.compute_lookup_table();
44
45 std::array<fr, n> coeffs;
46 std::array<fr, n> values;
47 std::array<fr, n> values_copy;
48 std::array<fr, n> recovered;
49
50 for (size_t k = 0; k < n; ++k) {
51 coeffs[k] = fr::random_element();
52 values[k] = fr::zero();
53 recovered[k] = fr::zero();
54 }
55
56 // compute values[j] = sum_k coeffs[k] * ω^{j*k}
57 for (size_t j = 0; j < n; ++j) {
58 fr acc = fr::zero();
59 for (size_t k = 0; k < n; ++k) {
60 fr w = domain.root.pow(static_cast<uint64_t>(j * k));
61 acc += coeffs[k] * w;
62 }
63 values[j] = acc;
64 values_copy[j] = values[j];
65 }
66
67 // compute ifft of values, which should recover coeffs
68 polynomial_arithmetic::ifft(values.data(), recovered.data(), domain);
69
70 for (size_t k = 0; k < n; ++k) {
71 EXPECT_EQ(recovered[k], coeffs[k]); // check that ifft recovers coeffs
72 EXPECT_EQ(values[k], values_copy[k]); // check that ifft does not modify input values
73 }
74}
75
76template <typename FF> class PolynomialTests : public ::testing::Test {};
77
78using FieldTypes = ::testing::Types<bb::fr, grumpkin::fr>;
79
81
82TYPED_TEST(PolynomialTests, linear_poly_product)
83{
84 using FF = TypeParam;
85 // Cover both BN254 and Grumpkin for the production SUBGROUP_SIZE range (87 Grumpkin, 256 BN254).
86 constexpr size_t n = 256;
88
90 FF expected = 1;
91 for (size_t i = 0; i < n; ++i) {
92 roots[i] = FF::random_element();
93 expected *= (z - roots[i]);
94 }
95
98 FF result = polynomial_arithmetic::evaluate(dest.data(), z, n + 1);
99
100 EXPECT_EQ(result, expected);
101}
102
103// compute_linear_polynomial_product handles the n=1 and n=2 boundaries of the incremental update.
104TYPED_TEST(PolynomialTests, LinearPolyProductSmallN)
105{
106 using FF = TypeParam;
107 // n=1: dest should represent (X - roots[0]) = [-r0, 1].
108 {
109 std::array<FF, 1> roots = { FF(7) };
110 std::array<FF, 2> dest{};
112 EXPECT_EQ(dest[0], -FF(7));
113 EXPECT_EQ(dest[1], FF(1));
114 }
115 // n=2: (X - 1)(X - 2) = X^2 - 3X + 2 → [2, -3, 1].
116 {
117 std::array<FF, 2> roots = { FF(1), FF(2) };
118 std::array<FF, 3> dest{};
120 EXPECT_EQ(dest[0], FF(2));
121 EXPECT_EQ(dest[1], -FF(3));
122 EXPECT_EQ(dest[2], FF(1));
123 }
124}
125
127{
128 using FF = TypeParam;
129 constexpr size_t n = 256;
130 auto domain = EvaluationDomain<FF>(n);
131
132 EXPECT_EQ(domain.size, 256UL);
133 EXPECT_EQ(domain.log2_size, 8UL);
134}
135
136// EvaluationDomain::operator=(EvaluationDomain&&) is a no-op under self-assignment and preserves
137// the precomputed round-roots tables.
138TYPED_TEST(PolynomialTests, EvaluationDomainMoveSelfAssign)
139{
140 using FF = TypeParam;
141 auto domain = EvaluationDomain<FF>(256);
142 domain.compute_lookup_table();
143 const size_t round_roots_before = domain.get_round_roots().size();
144 EXPECT_GT(round_roots_before, 0UL);
145
146 domain = std::move(domain);
147
148 EXPECT_EQ(domain.size, 256UL);
149 EXPECT_EQ(domain.get_round_roots().size(), round_roots_before);
150}
151
152// EvaluationDomain::operator=(EvaluationDomain&&) leaves the moved-from object in the empty
153// default-constructed state (size == 0, round-root tables empty), restoring the invariant
154// `size > 0 implies roots tables populated`. Without this, a moved-from domain would report
155// size > 0 with empty round-roots, which is the partial-validity pattern flagged by audit.
156TYPED_TEST(PolynomialTests, EvaluationDomainMoveAssignClearsSource)
157{
158 using FF = TypeParam;
159 constexpr size_t n = 256;
160 auto src = EvaluationDomain<FF>(n);
161 src.compute_lookup_table();
162 EXPECT_GT(src.get_round_roots().size(), 0UL);
163
165 dst = std::move(src);
166
167 EXPECT_EQ(dst.size, n);
168 EXPECT_GT(dst.get_round_roots().size(), 0UL);
169
170 EXPECT_EQ(src.size, 0UL);
171 EXPECT_EQ(src.num_threads, 0UL);
172 EXPECT_EQ(src.thread_size, 0UL);
173 EXPECT_EQ(src.log2_size, 0UL);
174 EXPECT_EQ(src.log2_thread_size, 0UL);
175 EXPECT_EQ(src.log2_num_threads, 0UL);
176 EXPECT_EQ(src.generator_size, 0UL);
177 EXPECT_EQ(src.get_round_roots().size(), 0UL);
178 EXPECT_EQ(src.get_inverse_round_roots().size(), 0UL);
179}
180
182{
183 using FF = TypeParam;
184 constexpr size_t n = 256;
185 auto domain = EvaluationDomain<FF>(n);
186
187 FF result;
188 FF expected;
189 expected = FF::one();
190 result = domain.root.pow(static_cast<uint64_t>(n));
191
192 EXPECT_EQ((result == expected), true);
193}
194
195TYPED_TEST(PolynomialTests, evaluation_domain_roots)
196{
197 using FF = TypeParam;
198 constexpr size_t n = 16;
199 EvaluationDomain<FF> domain(n);
200 domain.compute_lookup_table();
201 std::vector<FF*> root_table = domain.get_round_roots();
202 std::vector<FF*> inverse_root_table = domain.get_inverse_round_roots();
203 FF* roots = root_table[root_table.size() - 1];
204 FF* inverse_roots = inverse_root_table[inverse_root_table.size() - 1];
205 for (size_t i = 0; i < (n - 1) / 2; ++i) {
206 EXPECT_EQ(roots[i] * domain.root, roots[i + 1]);
207 EXPECT_EQ(inverse_roots[i] * domain.root_inverse, inverse_roots[i + 1]);
208 EXPECT_EQ(roots[i] * inverse_roots[i], FF::one());
209 }
210}
211
212TYPED_TEST(PolynomialTests, compute_efficient_interpolation)
213{
214 using FF = TypeParam;
215 constexpr size_t n = 250;
216 std::array<FF, n> src, poly, x;
217
218 for (size_t i = 0; i < n; ++i) {
219 poly[i] = FF::random_element();
220 }
221
222 for (size_t i = 0; i < n; ++i) {
223 x[i] = FF::random_element();
224 src[i] = polynomial_arithmetic::evaluate(poly.data(), x[i], n);
225 }
226 polynomial_arithmetic::compute_efficient_interpolation(src.data(), src.data(), x.data(), n);
227
228 for (size_t i = 0; i < n; ++i) {
229 EXPECT_EQ(src[i], poly[i]);
230 }
231}
232// Test efficient Lagrange interpolation when interpolation points contain 0
233TYPED_TEST(PolynomialTests, compute_efficient_interpolation_domain_with_zero)
234{
235 using FF = TypeParam;
236 constexpr size_t n = 15;
237 std::array<FF, n> src, poly, x;
238
239 for (size_t i = 0; i < n; ++i) {
240 poly[i] = FF(i + 1);
241 }
242
243 for (size_t i = 0; i < n; ++i) {
244 x[i] = FF(i);
245 src[i] = polynomial_arithmetic::evaluate(poly.data(), x[i], n);
246 }
247 polynomial_arithmetic::compute_efficient_interpolation(src.data(), src.data(), x.data(), n);
248
249 for (size_t i = 0; i < n; ++i) {
250 EXPECT_EQ(src[i], poly[i]);
251 }
252 // Test for the domain (-n/2, ..., 0, ... , n/2)
253
254 for (size_t i = 0; i < n; ++i) {
255 poly[i] = FF(i + 54);
256 }
257
258 for (size_t i = 0; i < n; ++i) {
259 x[i] = FF(i - n / 2);
260 src[i] = polynomial_arithmetic::evaluate(poly.data(), x[i], n);
261 }
262 polynomial_arithmetic::compute_efficient_interpolation(src.data(), src.data(), x.data(), n);
263
264 for (size_t i = 0; i < n; ++i) {
265 EXPECT_EQ(src[i], poly[i]);
266 }
267
268 // Test for the domain (-n+1, ..., 0)
269
270 for (size_t i = 0; i < n; ++i) {
271 poly[i] = FF(i * i + 57);
272 }
273
274 for (size_t i = 0; i < n; ++i) {
275 x[i] = FF(i - (n - 1));
276 src[i] = polynomial_arithmetic::evaluate(poly.data(), x[i], n);
277 }
278 polynomial_arithmetic::compute_efficient_interpolation(src.data(), src.data(), x.data(), n);
279
280 for (size_t i = 0; i < n; ++i) {
281 EXPECT_EQ(src[i], poly[i]);
282 }
283}
284
285TYPED_TEST(PolynomialTests, interpolation_constructor_single)
286{
287 using FF = TypeParam;
288
289 auto root = std::array{ FF(3) };
290 auto eval = std::array{ FF(4) };
291 Polynomial<FF> t(root, eval, 1);
292 ASSERT_EQ(t.size(), 1);
293 ASSERT_EQ(t[0], eval[0]);
294}
295
296TYPED_TEST(PolynomialTests, interpolation_constructor)
297{
298 using FF = TypeParam;
299
300 constexpr size_t N = 32;
301 std::array<FF, N> roots;
302 std::array<FF, N> evaluations;
303 for (size_t i = 0; i < N; ++i) {
304 roots[i] = FF::random_element();
305 evaluations[i] = FF::random_element();
306 }
307
308 auto roots_copy(roots);
309 auto evaluations_copy(evaluations);
310
311 Polynomial<FF> interpolated(roots, evaluations, N);
312
313 ASSERT_EQ(interpolated.size(), N);
314 ASSERT_EQ(roots, roots_copy);
315 ASSERT_EQ(evaluations, evaluations_copy);
316
317 for (size_t i = 0; i < N; ++i) {
318 FF eval = interpolated.evaluate(roots[i]);
319 ASSERT_EQ(eval, evaluations[i]);
320 }
321}
322
323TYPED_TEST(PolynomialTests, evaluate_mle_legacy)
324{
325 using FF = TypeParam;
326
327 auto test_case = [](size_t N) {
329 const size_t m = numeric::get_msb(N);
330 EXPECT_EQ(N, 1 << m);
331 Polynomial<FF> poly(N);
332 for (size_t i = 1; i < N - 1; ++i) {
333 poly.at(i) = FF::random_element(&engine);
334 }
335 poly.at(N - 1) = FF::zero();
336
337 EXPECT_TRUE(poly[0].is_zero());
338
339 // sample u = (u₀,…,uₘ₋₁)
340 std::vector<FF> u(m);
341 for (size_t l = 0; l < m; ++l) {
342 u[l] = FF::random_element(&engine);
343 }
344
345 std::vector<FF> lagrange_evals(N, FF(1));
346 for (size_t i = 0; i < N; ++i) {
347 auto& coef = lagrange_evals[i];
348 for (size_t l = 0; l < m; ++l) {
349 size_t mask = (1 << l);
350 if ((i & mask) == 0) {
351 coef *= (FF(1) - u[l]);
352 } else {
353 coef *= u[l];
354 }
355 }
356 }
357
358 // check eval by computing scalar product between
359 // lagrange evaluations and coefficients
360 FF real_eval(0);
361 for (size_t i = 0; i < N; ++i) {
362 real_eval += poly[i] * lagrange_evals[i];
363 }
364 FF computed_eval = poly.evaluate_mle(u);
365 EXPECT_EQ(real_eval, computed_eval);
366
367 // also check shifted eval
368 FF real_eval_shift(0);
369 for (size_t i = 1; i < N; ++i) {
370 real_eval_shift += poly[i] * lagrange_evals[i - 1];
371 }
372 FF computed_eval_shift = poly.evaluate_mle(u, true);
373 EXPECT_EQ(real_eval_shift, computed_eval_shift);
374 };
375 test_case(32);
376 test_case(4);
377 test_case(2);
378}
379
384TYPED_TEST(PolynomialTests, move_construct_and_assign)
385{
386 using FF = TypeParam;
387
388 // construct a poly with some arbitrary data
389 size_t num_coeffs = 64;
390 Polynomial<FF> polynomial_a(num_coeffs);
391 for (size_t i = 0; i < num_coeffs; i++) {
392 polynomial_a.at(i) = FF::random_element();
393 }
394
395 // construct a new poly from the original via the move constructor
396 Polynomial<FF> polynomial_b(std::move(polynomial_a));
397
398 // The moved-from polynomial must report itself as empty in every accessor: size() == 0,
399 // virtual_size() == 0, is_empty() == true, data() == nullptr. Failure modes here are the
400 // inconsistent moved-from state flagged by audit (size > 0 with data == nullptr).
401 EXPECT_EQ(polynomial_a.data(), nullptr);
402 EXPECT_EQ(polynomial_a.size(), 0UL);
403 EXPECT_EQ(polynomial_a.virtual_size(), 0UL);
404 EXPECT_TRUE(polynomial_a.is_empty());
405
406 // construct another poly; this will also use the move constructor!
407 auto polynomial_c = std::move(polynomial_b);
408
409 EXPECT_EQ(polynomial_b.data(), nullptr);
410 EXPECT_EQ(polynomial_b.size(), 0UL);
411 EXPECT_EQ(polynomial_b.virtual_size(), 0UL);
412 EXPECT_TRUE(polynomial_b.is_empty());
413
414 // define a poly with some arbitrary coefficients
415 Polynomial<FF> polynomial_d(num_coeffs);
416 for (size_t i = 0; i < num_coeffs; i++) {
417 polynomial_d.at(i) = FF::random_element();
418 }
419
420 // reset its data using move assignment
421 polynomial_d = std::move(polynomial_c);
422
423 EXPECT_EQ(polynomial_c.data(), nullptr);
424 EXPECT_EQ(polynomial_c.size(), 0UL);
425 EXPECT_EQ(polynomial_c.virtual_size(), 0UL);
426 EXPECT_TRUE(polynomial_c.is_empty());
427}
428
429TYPED_TEST(PolynomialTests, default_construct_then_assign)
430{
431 using FF = TypeParam;
432
433 // construct an arbitrary but non-empty polynomial
434 size_t num_coeffs = 64;
435 Polynomial<FF> interesting_poly(num_coeffs);
436 for (size_t i = 0; i < num_coeffs; i++) {
437 interesting_poly.at(i) = FF::random_element();
438 }
439
440 // construct an empty poly via the default constructor
441 Polynomial<FF> poly;
442
443 EXPECT_EQ(poly.is_empty(), true);
444
445 // fill the empty poly using the assignment operator
446 poly = interesting_poly;
447
448 // coefficients and size should be equal in value
449 for (size_t i = 0; i < num_coeffs; ++i) {
450 EXPECT_EQ(poly[i], interesting_poly[i]);
451 }
452 EXPECT_EQ(poly.size(), interesting_poly.size());
453}
454
455// factor_roots produces the correct quotient when (X - r) divides p(X) cleanly.
456TEST(polynomials, FactorRootsExactDivisionRegression)
457{
458 using FF = fr;
459 // p(X) = X^2 - 1 = (X - 1)(X + 1): exact division by (X - 1) produces q(X) = X + 1.
460 std::array<FF, 3> exact_poly = { -FF(1), FF(0), FF(1) };
462 EXPECT_EQ(exact_poly[0], FF(1));
463 EXPECT_EQ(exact_poly[1], FF(1));
464 EXPECT_EQ(exact_poly[2], FF(0));
465}
466
467// factor_roots asserts when the exact-divisibility precondition is violated.
468TEST(polynomials, FactorRootsNonExactDivisionAsserts)
469{
470 GTEST_FLAG_SET(death_test_style, "threadsafe");
471 using FF = fr;
472 std::array<FF, 3> bad_poly = { FF(1), FF(0), FF(1) }; // p(X) = X^2 + 1, p(1) = 2 != 0
474}
475
476// Polynomial's interpolation constructor asserts when interpolation_points and evaluations differ in size.
477TEST(polynomials, InterpolationCtorMismatchedSpansAsserts)
478{
479 GTEST_FLAG_SET(death_test_style, "threadsafe");
480 using FF = fr;
481 std::vector<FF> points = { FF(1), FF(2), FF(3), FF(4) };
482 std::vector<FF> evals = { FF(10), FF(20) }; // shorter than points
484 bb::Polynomial<FF>(std::span<const FF>(points), std::span<const FF>(evals), /*virtual_size=*/4), ".*");
485}
486
487// parse_size_string throws when value * multiplier overflows size_t.
488TEST(polynomials, ParseSizeStringOverflowAsserts)
489{
490 // Sanity: well-formed inputs still parse correctly.
491 EXPECT_EQ(parse_size_string("1k"), 1024U);
492 EXPECT_EQ(parse_size_string("2g"), 2UL * 1024 * 1024 * 1024);
493
494 // 2^54 * 1024 == 2^64 wraps to 0 without a guard.
495 ASSERT_THROW_OR_ABORT(parse_size_string("18014398509481984k"), ".*");
496}
497
498// compute_efficient_interpolation asserts (always-on, not _DEBUG) when evaluation points are not all distinct,
499// because batch_invert silently skips zero entries and would otherwise produce wrong output.
500TEST(polynomials, ComputeEfficientInterpolationDuplicatePointsAsserts)
501{
502 GTEST_FLAG_SET(death_test_style, "threadsafe");
503 using FF = fr;
504 constexpr size_t n = 3;
505 std::array<FF, n> src = { FF(10), FF(20), FF(30) };
506 std::array<FF, n> dest{};
507 std::array<FF, n> points = { FF(1), FF(2), FF(2) }; // duplicate
509 polynomial_arithmetic::compute_efficient_interpolation<FF>(src.data(), dest.data(), points.data(), n), ".*");
510}
511
512// fft_inner_parallel asserts when called in-place (coeffs == target).
513TEST(polynomials, FftInnerParallelInPlaceAsserts)
514{
515 GTEST_FLAG_SET(death_test_style, "threadsafe");
516 using FF = fr;
517 constexpr size_t n = 16;
518 auto domain = bb::EvaluationDomain<FF>(n);
519 domain.compute_lookup_table();
520
521 std::array<FF, n> coeffs{};
522 for (size_t i = 0; i < n; ++i) {
523 coeffs[i] = FF(i + 1);
524 }
526 polynomial_arithmetic::fft_inner_parallel(coeffs.data(), coeffs.data(), domain, FF(), domain.get_round_roots()),
527 ".*");
528}
#define ASSERT_THROW_OR_ABORT(statement, matcher)
Definition assert.hpp:191
size_t parse_size_string(const std::string &size_str)
const std::vector< FF * > & get_round_roots() const
const std::vector< FF * > & get_inverse_round_roots() const
Structured polynomial class that represents the coefficients 'a' of a_0 + a_1 x .....
bool is_empty() const
std::size_t virtual_size() const
Fr evaluate(const Fr &z) const
Fr evaluate_mle(std::span< const Fr > evaluation_points, bool shift=false) const
evaluate multi-linear extension p(X_0,…,X_{n-1}) = \sum_i a_i*L_i(X_0,…,X_{n-1}) at u = (u_0,...
Fr & at(size_t index)
Our mutable accessor, unlike operator[]. We abuse precedent a bit to differentiate at() and operator[...
std::size_t size() const
numeric::RNG & engine
testing::Types< bb::fr > FieldTypes
constexpr T get_msb(const T in)
Definition get_msb.hpp:50
RNG & get_debug_randomness(bool reset, std::uint_fast64_t seed)
Definition engine.cpp:245
void ifft(Fr *coeffs, Fr *target, const EvaluationDomain< Fr > &domain)
void compute_linear_polynomial_product(const Fr *roots, Fr *dest, const size_t n)
Fr evaluate(const Fr *coeffs, const Fr &z, const size_t n)
void factor_roots(std::span< Fr > polynomial, const Fr &root)
Divides p(X) by (X-r) in-place.
void fft_inner_parallel(Fr *coeffs, Fr *target, const EvaluationDomain< Fr > &domain, const Fr &, const std::vector< Fr * > &root_table)
void compute_efficient_interpolation(const Fr *src, Fr *dest, const Fr *evaluation_points, const size_t n)
Entry point for Barretenberg command-line interface.
Definition api.hpp:5
EvaluationDomain< bb::fr > evaluation_domain
TYPED_TEST_SUITE(CommitmentKeyTest, Curves)
field< Bn254FrParams > fr
Definition fr.hpp:155
TYPED_TEST(CommitmentKeyTest, CommitToZeroPoly)
TEST(BoomerangMegaCircuitBuilder, BasicCircuit)
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
BB_INLINE constexpr field pow(const uint256_t &exponent) const noexcept
static field random_element(numeric::RNG *engine=nullptr) noexcept
static constexpr field zero()