Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
polynomial_arithmetic.cpp
Go to the documentation of this file.
1// === AUDIT STATUS ===
2// internal: { status: Complete, auditors: [Nishat], commit: 94f596f8b3bbbc216f9ad7dc33253256141156b2 }
3// external_1: { status: not started, auditors: [], commit: }
4// external_2: { status: not started, auditors: [], commit: }
5// =====================
6
15#include <math.h>
16#include <memory.h>
17
19
20inline uint32_t reverse_bits(uint32_t x, uint32_t bit_length)
21{
22 x = (((x & 0xaaaaaaaa) >> 1) | ((x & 0x55555555) << 1));
23 x = (((x & 0xcccccccc) >> 2) | ((x & 0x33333333) << 2));
24 x = (((x & 0xf0f0f0f0) >> 4) | ((x & 0x0f0f0f0f) << 4));
25 x = (((x & 0xff00ff00) >> 8) | ((x & 0x00ff00ff) << 8));
26 return (((x >> 16) | (x << 16))) >> (32 - bit_length);
27}
28
29inline bool is_power_of_two(uint64_t x)
30{
31 return x && !(x & (x - 1));
32}
33
34template <typename Fr>
36 Fr* target,
37 const EvaluationDomain<Fr>& domain,
38 const Fr& generator_start,
39 const Fr& generator_shift,
40 const size_t generator_size)
41{
42 BB_ASSERT(generator_size % domain.num_threads == 0,
43 "generator_size must be divisible by num_threads to avoid silently skipping elements");
44 parallel_for(domain.num_threads, [&](size_t j) {
45 Fr thread_shift = generator_shift.pow(static_cast<uint64_t>(j * (generator_size / domain.num_threads)));
46 Fr work_generator = generator_start * thread_shift;
47 const size_t offset = j * (generator_size / domain.num_threads);
48 const size_t end = offset + (generator_size / domain.num_threads);
49 for (size_t i = offset; i < end; ++i) {
50 target[i] = coeffs[i] * work_generator;
51 work_generator *= generator_shift;
52 }
53 });
54}
55
56template <typename Fr>
57 requires SupportsFFT<Fr>
59 Fr* coeffs, Fr* target, const EvaluationDomain<Fr>& domain, const Fr&, const std::vector<Fr*>& root_table)
60{
61 BB_ASSERT(coeffs != target, "fft_inner_parallel does not support in-place operation");
62 parallel_for(domain.num_threads, [&](size_t j) {
63 Fr temp_1;
64 Fr temp_2;
65 for (size_t i = (j * domain.thread_size); i < ((j + 1) * domain.thread_size); i += 2) {
66 uint32_t next_index_1 = (uint32_t)reverse_bits((uint32_t)i + 2, (uint32_t)domain.log2_size);
67 uint32_t next_index_2 = (uint32_t)reverse_bits((uint32_t)i + 3, (uint32_t)domain.log2_size);
68 __builtin_prefetch(&coeffs[next_index_1]);
69 __builtin_prefetch(&coeffs[next_index_2]);
70
71 uint32_t swap_index_1 = (uint32_t)reverse_bits((uint32_t)i, (uint32_t)domain.log2_size);
72 uint32_t swap_index_2 = (uint32_t)reverse_bits((uint32_t)i + 1, (uint32_t)domain.log2_size);
73
74 Fr::__copy(coeffs[swap_index_1], temp_1);
75 Fr::__copy(coeffs[swap_index_2], temp_2);
76 target[i + 1] = temp_1 - temp_2;
77 target[i] = temp_1 + temp_2;
78 }
79 });
80
81 // outer FFT loop
82 for (size_t m = 2; m < (domain.size); m <<= 1) {
83 parallel_for(domain.num_threads, [&](size_t j) {
84 Fr temp;
85
86 // Ok! So, what's going on here? This is the inner loop of the FFT algorithm, and we want to break it
87 // out into multiple independent threads. For `num_threads`, each thread will evaluation `domain.size /
88 // num_threads` of the polynomial. The actual iteration length will be half of this, because we leverage
89 // the fact that \omega^{n/2} = -\omega (where \omega is a root of unity)
90
91 // Here, `start` and `end` are used as our iterator limits, so that we can use our iterator `i` to
92 // directly access the roots of unity lookup table
93 const size_t start = j * (domain.thread_size >> 1);
94 const size_t end = (j + 1) * (domain.thread_size >> 1);
95
96 // For all but the last round of our FFT, the roots of unity that we need, will be a subset of our
97 // lookup table. e.g. for a size 2^n FFT, the 2^n'th roots create a multiplicative subgroup of order 2^n
98 // the 1st round will use the roots from the multiplicative subgroup of order 2 : the 2'th roots of
99 // unity the 2nd round will use the roots from the multiplicative subgroup of order 4 : the 4'th
100 // roots of unity
101 // i.e. each successive FFT round will double the set of roots that we need to index.
102 // We have already laid out the `root_table` container so that each FFT round's roots are linearly
103 // ordered in memory. For all FFT rounds, the number of elements we're iterating over is greater than
104 // the size of our lookup table. We need to access this table in a cyclical fasion - i.e. for a subgroup
105 // of size x, the first x iterations will index the subgroup elements in order, then for the next x
106 // iterations, we loop back to the start.
107
108 // We could implement the algorithm by having 2 nested loops (where the inner loop iterates over the
109 // root table), but we want to flatten this out - as for the first few rounds, the inner loop will be
110 // tiny and we'll have quite a bit of unneccesary branch checks For each iteration of our flattened
111 // loop, indexed by `i`, the element of the root table we need to access will be `i % (current round
112 // subgroup size)` Given that each round subgroup size is `m`, which is a power of 2, we can index the
113 // root table with a very cheap `i & (m - 1)` Which is why we have this odd `block_mask` variable
114 const size_t block_mask = m - 1;
115
116 // The next problem to tackle, is we now need to efficiently index the polynomial element in
117 // `scratch_space` in our flattened loop If we used nested loops, the outer loop (e.g. `y`) iterates
118 // from 0 to 'domain size', in steps of 2 * m, with the inner loop (e.g. `z`) iterating from 0 to m. We
119 // have our inner loop indexer with `i & (m - 1)`. We need to add to this our outer loop indexer, which
120 // is equivalent to taking our indexer `i`, masking out the bits used in the 'inner loop', and doubling
121 // the result. i.e. polynomial indexer = (i & (m - 1)) + ((i & ~(m - 1)) >> 1) To simplify this, we
122 // cache index_mask = ~block_mask, meaning that our indexer is just `((i & index_mask) << 1 + (i &
123 // block_mask)`
124 const size_t index_mask = ~block_mask;
125
126 // `round_roots` fetches the pointer to this round's lookup table. We use `numeric::get_msb(m) - 1` as
127 // our indexer, because we don't store the precomputed root values for the 1st round (because they're
128 // all 1).
129 const Fr* round_roots = root_table[static_cast<size_t>(numeric::get_msb(m)) - 1];
130
131 // Finally, we want to treat the final round differently from the others,
132 // so that we can reduce out of our 'coarse' reduction and store the output in `coeffs` instead of
133 // `scratch_space`
134 for (size_t i = start; i < end; ++i) {
135 size_t k1 = (i & index_mask) << 1;
136 size_t j1 = i & block_mask;
137 temp = round_roots[j1] * target[k1 + j1 + m];
138 target[k1 + j1 + m] = target[k1 + j1] - temp;
139 target[k1 + j1] += temp;
140 }
141 });
142 }
143}
144
145template <typename Fr>
146 requires SupportsFFT<Fr>
147void ifft(Fr* coeffs, Fr* target, const EvaluationDomain<Fr>& domain)
148{
149 fft_inner_parallel(coeffs, target, domain, domain.root_inverse, domain.get_inverse_round_roots());
150
151 parallel_for(domain.num_threads, [&](size_t j) {
152 const size_t start = j * domain.thread_size;
153 const size_t end = (j + 1) * domain.thread_size;
154 for (size_t i = start; i < end; ++i) {
155 target[i] *= domain.domain_inverse;
156 }
157 });
158}
159
160template <typename Fr> Fr evaluate(const Fr* coeffs, const Fr& z, const size_t n)
161{
162 const size_t num_threads = get_num_cpus();
163 std::vector<Fr> evaluations(num_threads, Fr::zero());
164 parallel_for([&](const ThreadChunk& chunk) {
165 // parallel_for with ThreadChunk uses get_num_cpus() threads
166 BB_ASSERT_EQ(chunk.total_threads, evaluations.size());
167 auto range = chunk.range(n);
168 if (range.empty()) {
169 return;
170 }
171 size_t start = *range.begin();
172 Fr z_acc = z.pow(static_cast<uint64_t>(start));
173 for (size_t i : range) {
174 Fr work_var = z_acc * coeffs[i];
175 evaluations[chunk.thread_index] += work_var;
176 z_acc *= z;
177 }
178 });
179
180 Fr r = Fr::zero();
181 for (const auto& eval : evaluations) {
182 r += eval;
183 }
184 return r;
185}
186
187// This function computes sum of all scalars in a given array.
188//
189// Loop abstraction: vectorized_for<VECTOR_FIELD_WIDTH> emits
190// ContiguousVectorIndex<W> in the bulk and ScalarIndex in the tail. The
191// kernel reads from a non-owning PolynomialSpan view of the input array;
192// span[ctx] returns Fr for ScalarIndex and a VectorField for
193// ContiguousVectorIndex<W>. Accumulator<Fr> dispatches its operator+= on
194// the argument type, so the kernel body stays as one line. reduce()
195// horizontal-adds the W vector lanes together with the scalar slot.
196template <typename Fr> Fr compute_sum(const Fr* src, const size_t n)
197{
199 Accumulator<Fr> acc;
200 vectorized_for<VECTOR_FIELD_WIDTH, Fr>(0, n, [&](auto ctx) { acc += view[ctx]; });
201 return acc.reduce();
202}
203
204// This function computes the polynomial (x - a)(x - b)(x - c)... given n distinct roots (a, b, c, ...).
205//
206// Build the product incrementally by multiplying in one root at a time. After the i-th iteration,
207// dest[0..i+1] holds the coefficients of ∏_{k=0}^{i} (X - roots[k]). Multiplying the existing
208// polynomial P(X) by (X - r) gives
209// P(X) · X shifts every coefficient up by one index, and
210// -P(X) · r scales every coefficient by -r.
211// Walking k high-to-low allows writing the shift-and-combine update in place with total cost O(n^2).
212template <typename Fr> void compute_linear_polynomial_product(const Fr* roots, Fr* dest, const size_t n)
213{
214 if (n == 0) {
215 return;
216 }
217
218 dest[0] = -roots[0];
219 dest[1] = Fr(1);
220 for (size_t i = 1; i < n; ++i) {
221 const Fr r = roots[i];
222 dest[i + 1] = dest[i];
223 for (size_t k = i; k >= 1; --k) {
224 dest[k] = dest[k - 1] - r * dest[k];
225 }
226 dest[0] = -r * dest[0];
227 }
228}
229
230template <typename Fr> Fr compute_linear_polynomial_product_evaluation(const Fr* roots, const Fr z, const size_t n)
231{
232 Fr result = 1;
233 for (size_t i = 0; i < n; ++i) {
234 result *= (z - roots[i]);
235 }
236 return result;
237}
238
239template <typename Fr>
240void compute_efficient_interpolation(const Fr* src, Fr* dest, const Fr* evaluation_points, const size_t n)
241{
242 /*
243 We use Lagrange technique to compute polynomial interpolation.
244 Given: (x_i, y_i) for i ∈ {0, 1, ..., n} =: [n]
245 Compute function f(X) such that f(x_i) = y_i for all i ∈ [n].
246 (X - x1)(X - x2)...(X - xn) (X - x0)(X - x2)...(X - xn)
247 F(X) = y0-------------------------------- + y1---------------------------------- + ...
248 (x0 - x_1)(x0 - x_2)...(x0 - xn) (x1 - x_0)(x1 - x_2)...(x1 - xn)
249 We write this as:
250 [ yi ]
251 F(X) = N(X) * |∑_i --------------- |
252 [ (X - xi) * di ]
253 where:
254 N(X) = ∏_{i \in [n]} (X - xi),
255 di = ∏_{j != i} (xi - xj)
256 For division of N(X) by (X - xi), we use the same trick that was used in compute_opening_polynomial()
257 function in the Kate commitment scheme.
258 We denote
259 q_{x_i} = N(X)/(X-x_i) * y_i * (d_i)^{-1} = q_{x_i,0}*1 + ... + q_{x_i,n-1} * X^{n-1} for i=0,..., n-1.
260
261 The computation of F(X) is split into two cases:
262
263 - if 0 is not in the interpolation domain, then the numerator polynomial N(X) has a non-zero constant term
264 that is used to initialize the division algorithm mentioned above; the monomial coefficients q_{x_i, j} of
265 q_{x_i} are accumulated into dest[j] for j=0,..., n-1
266
267 - if 0 is in the domain at index i_0, the constant term of N(X) is 0 and the division algorithm computing
268 q_{x_i} for i != i_0 is initialized with the constant term of N(X)/X. Note that its coefficients are given
269 by numerator_polynomial[j] for j=1,...,n. The monomial coefficients of q_{x_i} are then accumuluated in
270 dest[j] for j=1,..., n-1. Whereas the coefficients of
271 q_{0} = N(X)/X * f(0) * (d_{i_0})^{-1}
272 are added to dest[j] for j=0,..., n-1. Note that these coefficients do not require performing the division
273 algorithm used in Kate commitment scheme, as the coefficients of N(X)/X are given by numerator_polynomial[j]
274 for j=1,...,n.
275 */
276 // Lagrange interpolation is mathematically ill-defined when any two evaluation points coincide:
277 // the denominator d_i contains a zero factor. batch_invert silently skips zero entries, so
278 // without this check duplicate points produce an incorrect result.
279 for (size_t i = 0; i < n; ++i) {
280 for (size_t j = i + 1; j < n; ++j) {
281 BB_ASSERT(evaluation_points[i] != evaluation_points[j],
282 "compute_efficient_interpolation requires distinct evaluation points");
283 }
284 }
285
286 std::vector<Fr> numerator_polynomial(n + 1);
287 polynomial_arithmetic::compute_linear_polynomial_product(evaluation_points, numerator_polynomial.data(), n);
288 // First half contains roots, second half contains denominators (to be inverted)
289 std::vector<Fr> roots_and_denominators(2 * n);
290 std::vector<Fr> temp_src(n);
291 for (size_t i = 0; i < n; ++i) {
292 roots_and_denominators[i] = -evaluation_points[i];
293 temp_src[i] = src[i];
294 dest[i] = 0;
295 // compute constant denominators
296 roots_and_denominators[n + i] = 1;
297 for (size_t j = 0; j < n; ++j) {
298 if (j == i) {
299 continue;
300 }
301 roots_and_denominators[n + i] *= (evaluation_points[i] - evaluation_points[j]);
302 }
303 }
304 // at this point roots_and_denominators is populated as follows
305 // (x_0,\ldots, x_{n-1}, d_0, \ldots, d_{n-1})
306 Fr::batch_invert(roots_and_denominators.data(), 2 * n);
307
308 Fr z, multiplier;
309 std::vector<Fr> temp_dest(n);
310 size_t idx_zero = 0;
311 bool interpolation_domain_contains_zero = false;
312 // if the constant term of the numerator polynomial N(X) is 0, then the interpolation domain contains 0
313 // we find the index i_0, such that x_{i_0} = 0
314 if (numerator_polynomial[0] == Fr(0)) {
315 for (size_t i = 0; i < n; ++i) {
316 if (evaluation_points[i] == Fr(0)) {
317 idx_zero = i;
318 interpolation_domain_contains_zero = true;
319 break;
320 }
321 }
322 };
323
324 if (!interpolation_domain_contains_zero) {
325 for (size_t i = 0; i < n; ++i) {
326 // set z = - 1/x_i for x_i <> 0
327 z = roots_and_denominators[i];
328 // temp_src[i] is y_i, it gets multiplied by 1/d_i
329 multiplier = temp_src[i] * roots_and_denominators[n + i];
330 temp_dest[0] = multiplier * numerator_polynomial[0];
331 temp_dest[0] *= z;
332 dest[0] += temp_dest[0];
333 for (size_t j = 1; j < n; ++j) {
334 temp_dest[j] = multiplier * numerator_polynomial[j] - temp_dest[j - 1];
335 temp_dest[j] *= z;
336 dest[j] += temp_dest[j];
337 }
338 }
339 } else {
340 for (size_t i = 0; i < n; ++i) {
341 if (i == idx_zero) {
342 // the contribution from the term corresponding to i_0 is computed separately
343 continue;
344 }
345 // get the next inverted root
346 z = roots_and_denominators[i];
347 // compute f(x_i) * d_{x_i}^{-1}
348 multiplier = temp_src[i] * roots_and_denominators[n + i];
349 // get x_i^{-1} * f(x_i) * d_{x_i}^{-1} into the "free" term
350 temp_dest[1] = multiplier * numerator_polynomial[1];
351 temp_dest[1] *= z;
352 // correct the first coefficient as it is now accumulating free terms from
353 // f(x_i) d_i^{-1} prod_(X-x_i, x_i != 0) (X-x_i) * 1/(X-x_i)
354 dest[1] += temp_dest[1];
355 // compute the quotient N(X)/(X-x_i) f(x_i)/d_{x_i} and its contribution to the target coefficients
356 for (size_t j = 2; j < n; ++j) {
357 temp_dest[j] = multiplier * numerator_polynomial[j] - temp_dest[j - 1];
358 temp_dest[j] *= z;
359 dest[j] += temp_dest[j];
360 };
361 }
362 // correct the target coefficients by the contribution from q_{0} = N(X)/X * d_{i_0}^{-1} * f(0)
363 for (size_t i = 0; i < n; ++i) {
364 dest[i] += temp_src[idx_zero] * roots_and_denominators[n + idx_zero] * numerator_polynomial[i + 1];
365 }
366 }
367}
368
369template fr evaluate<fr>(const fr*, const fr&, const size_t);
370template void fft_inner_parallel<fr>(fr*, fr*, const EvaluationDomain<fr>&, const fr&, const std::vector<fr*>&);
371template void ifft<fr>(fr*, fr*, const EvaluationDomain<fr>&);
372template fr compute_sum<fr>(const fr*, const size_t);
373template void compute_linear_polynomial_product<fr>(const fr*, fr*, const size_t);
374template void compute_efficient_interpolation<fr>(const fr*, fr*, const fr*, const size_t);
375
376template grumpkin::fr evaluate<grumpkin::fr>(const grumpkin::fr*, const grumpkin::fr&, const size_t);
377template grumpkin::fr compute_sum<grumpkin::fr>(const grumpkin::fr*, const size_t);
378template void compute_linear_polynomial_product<grumpkin::fr>(const grumpkin::fr*, grumpkin::fr*, const size_t);
379template void compute_efficient_interpolation<grumpkin::fr>(const grumpkin::fr*,
381 const grumpkin::fr*,
382 const size_t);
383
384} // namespace bb::polynomial_arithmetic
#define BB_ASSERT(expression,...)
Definition assert.hpp:70
#define BB_ASSERT_EQ(actual, expected,...)
Definition assert.hpp:83
const std::vector< FF * > & get_inverse_round_roots() const
Fr compute_linear_polynomial_product_evaluation(const Fr *roots, const Fr z, const size_t n)
uint32_t reverse_bits(uint32_t x, uint32_t bit_length)
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 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)
void scale_by_generator(Fr *coeffs, Fr *target, const EvaluationDomain< Fr > &domain, const Fr &generator_start, const Fr &generator_shift, const size_t generator_size)
Fr compute_sum(const Fr *src, const size_t n)
size_t get_num_cpus()
Definition thread.cpp:34
void parallel_for(size_t num_iterations, const std::function< void(size_t)> &func)
Definition thread.cpp:112
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
std::vector< Instruction > target
Curve::ScalarField Fr
Fr reduce() const
size_t total_threads
Definition thread.hpp:151
size_t thread_index
Definition thread.hpp:150
auto range(size_t size, size_t offset=0) const
Definition thread.hpp:152
BB_INLINE constexpr field pow(const uint256_t &exponent) const noexcept
static void batch_invert(C &coeffs) noexcept
Batch invert a collection of field elements using Montgomery's trick.
static constexpr field zero()
VectorField result