Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
cycle_group.cpp
Go to the documentation of this file.
1// === AUDIT STATUS ===
2// internal: { status: Complete, auditors: [Luke], commit: a48c205d6dcd4338f5b83b4fda18bff6015be07b}
3// external_1: { status: Complete, auditors: [Sherlock], commit: 13c1b927c6c }
4// external_2: { status: not started, auditors: [], commit: }
5// =====================
6
7#include "../field/field.hpp"
8#include "../field/field_utils.hpp"
13
14#include "./cycle_group.hpp"
20namespace bb::stdlib {
21
27template <typename Builder> cycle_group<Builder>::cycle_group(Builder* _context)
28{
29 *this = constant_infinity(_context);
30}
31
39template <typename Builder>
40cycle_group<Builder>::cycle_group(const field_t& x, const field_t& y, bool assert_on_curve)
41 : _x(x)
42 , _y(y)
43 , context(nullptr)
44{
45 if (_x.get_context() != nullptr) {
47 } else if (_y.get_context() != nullptr) {
49 }
50
51 // Auto-detect infinity: point is at infinity iff both coordinates are zero
52 // we can check this efficiently using the observation that:
53 // (x^2 + 5 * y^2 = 0) has no non-trivial solutions in fr, since fr modulus p == 2 mod 5
54 const field_t x_sqr = _x.sqr();
55 const field_t five_y = _y * bb::fr(5);
56 _is_infinity = (_y.madd(five_y, x_sqr).is_zero());
57
58 // For the simplicity of methods in this class, we ensure that the coordinates of a point always have the same
59 // constancy. If they don't, we convert the non-constant coordinate to a fixed witness. Should be rare.
60 if (_x.is_constant() != _y.is_constant()) {
61 if (_x.is_constant()) {
63 } else {
65 }
66 }
67
68 // Elements are always expected to be on the curve but may or may not be constrained as such.
69 BB_ASSERT(get_value().on_curve(), "cycle_group: Point is not on curve");
70 if (assert_on_curve) {
72 }
73}
74
84template <typename Builder>
85cycle_group<Builder>::cycle_group(const field_t& x, const field_t& y, bool_t is_infinity, bool assert_on_curve)
86 : _x(x)
87 , _y(y)
88 , _is_infinity(is_infinity)
89{
90 if (_x.get_context() != nullptr) {
91 context = _x.get_context();
92 } else if (_y.get_context() != nullptr) {
93 context = _y.get_context();
94 } else {
95 context = is_infinity.get_context();
96 }
97
98 if (is_infinity.is_constant() && is_infinity.get_value()) {
99 *this = constant_infinity(this->context);
100 }
102 // For the simplicity of methods in this class, we ensure that the coordinates of a point always have the same
103 // constancy. If they don't, we convert the non-constant coordinate to a fixed witness. Should be rare.
104 if (_x.is_constant() != _y.is_constant()) {
105 if (_x.is_constant()) {
106 _x.convert_constant_to_fixed_witness(context);
107 } else {
108 _y.convert_constant_to_fixed_witness(context);
110 }
112 // If both coordinates are constant, is_infinity must also be constant.
113 BB_ASSERT(!(_x.is_constant() && _y.is_constant() && !_is_infinity.is_constant()),
114 "cycle_group: constant coordinates with non-constant infinity flag");
116 // Elements are always expected to be on the curve but may or may not be constrained as such.
117 BB_ASSERT(get_value().on_curve(), "cycle_group: Point is not on curve");
118 if (assert_on_curve) {
119 validate_on_curve();
120 }
121}
122
132template <typename Builder>
134 : _x(_in.is_point_at_infinity() ? 0 : _in.x)
135 , _y(_in.is_point_at_infinity() ? 0 : _in.y)
136 , _is_infinity(_in.is_point_at_infinity())
137 , context(nullptr)
138{}
139
147template <typename Builder> cycle_group<Builder> cycle_group<Builder>::one(Builder* _context)
148{
149 field_t x(_context, Group::one.x);
150 field_t y(_context, Group::one.y);
151 // Generator point is known to be on the curve
152 return cycle_group<Builder>(x, y, /*assert_on_curve=*/false);
153}
154
160{
161 // Use the AffineElement constructor with an infinity point
162 // This properly sets (0, 0) coordinates and is_infinity = true
163 cycle_group result(AffineElement::infinity());
164
165 // If context provided, create field_t/bool_t with that context
166 if (_context != nullptr) {
167 result._x = field_t(_context, bb::fr(0));
168 result._y = field_t(_context, bb::fr(0));
169 result._is_infinity = bool_t(_context, true);
170 result.context = _context;
171 }
172
173 return result;
174}
175
187template <typename Builder>
189{
190 // By convention we set the coordinates of the point at infinity to (0,0).
192 : field_t::from_witness(_context, _in.x);
194 : field_t::from_witness(_context, _in.y);
195 // The constructor auto-detects infinity from coordinates and validates on curve.
196 cycle_group result(x_val, y_val, /*assert_on_curve=*/true);
197 result.set_free_witness_tag();
198 return result;
199}
200
213template <typename Builder>
215{
216 cycle_group result(_context);
217
218 if (_in.is_point_at_infinity()) {
219 result = constant_infinity(_context);
220 } else {
221 result._x = field_t::from_witness(_context, _in.x);
222 result._y = field_t::from_witness(_context, _in.y);
223 result._x.assert_equal(result._x.get_value());
224 result._y.assert_equal(result._y.get_value());
226 // point at infinity is circuit constant
227 result._is_infinity = _in.is_point_at_infinity();
228 result.unset_free_witness_tag();
229 return result;
230}
231
232template <typename Builder> Builder* cycle_group<Builder>::get_context(const cycle_group& other) const
233{
234 if (get_context() != nullptr) {
235 return get_context();
236 }
237 return other.get_context();
238}
239
241{
242 AffineElement result(x().get_value(), y().get_value());
243 if (is_point_at_infinity().get_value()) {
244 result.self_set_infinity();
245 }
246 return result;
247}
248
256template <typename Builder> void cycle_group<Builder>::validate_on_curve() const
257{
258 // This class is for short Weierstrass curves only!
259 static_assert(Group::curve_a == 0);
260 auto xx = _x * _x;
261 auto xxx = xx * _x;
262 auto res = _y.madd(_y, -xxx - Group::curve_b);
263 // If this is the point at infinity, then res is changed to 0, otherwise it remains unchanged
264 res *= !is_point_at_infinity();
265 res.assert_is_zero();
266}
267
273template <typename Builder> void cycle_group<Builder>::standardize()
274{
275 // Set coordinates to (0, 0) if point is at infinity for canonical representation
276 this->_x = field_t::conditional_assign(this->_is_infinity, 0, this->_x);
277 this->_y = field_t::conditional_assign(this->_is_infinity, 0, this->_y);
278 // Mark witnesses as used to prevent boomerang detection false positives when
279 // this is the final operation (e.g., final batch_mul output)
280 mark_witness_as_used(this->_x);
281 mark_witness_as_used(this->_y);
282}
283
296template <typename Builder>
298{
299 // If this is a constant point at infinity, return early
300 if (this->is_constant_point_at_infinity()) {
301 return *this;
302 }
303
304 // To support the point at infinity, we conditionally modify y to be 1 to avoid division by zero in the
305 // doubling formula
306 auto modified_y = field_t::conditional_assign(is_point_at_infinity(), 1, _y);
307
308 // Compute the doubled point coordinates (either from hint or by native calculation)
309 bb::fr x3;
310 bb::fr y3;
311 if (hint.has_value()) {
312 x3 = hint.value().x;
313 y3 = hint.value().y;
314 } else {
315 const bb::fr x1 = _x.get_value();
316 const bb::fr y1 = modified_y.get_value();
317
318 // N.B. the formula to derive the witness value for x3 mirrors the formula in elliptic_relation.hpp
319 // Specifically, we derive x^4 via the Short Weierstrass curve formula y^2 = x^3 + b i.e. x^4 = x * (y^2 - b)
320 // We must follow this pattern exactly to support the edge-case where the input is the point at infinity.
321 const bb::fr y_pow_2 = y1.sqr();
322 const bb::fr x_pow_4 = x1 * (y_pow_2 - Group::curve_b);
323 const bb::fr lambda_squared = (x_pow_4 * 9) / (y_pow_2 * 4);
324 const bb::fr lambda = (x1 * x1 * 3) / (y1 + y1);
325 x3 = lambda_squared - x1 - x1;
326 y3 = lambda * (x1 - x3) - y1;
327 }
328
329 // Construct the doubled point based on whether this is a constant or witness
330 cycle_group result;
331 if (is_constant()) {
332 result = cycle_group(x3, y3, is_point_at_infinity(), /*assert_on_curve=*/false);
333 // Propagate the origin tag as-is
334 result.set_origin_tag(get_origin_tag());
335 } else {
336 // Create result witness and construct ECC double constraint
337 result = cycle_group(
338 witness_t(context, x3), witness_t(context, y3), is_point_at_infinity(), /*assert_on_curve=*/false);
339
340 context->create_ecc_dbl_gate(bb::ecc_dbl_gate_<bb::fr>{
341 .x1 = _x.get_witness_index(),
342 .y1 = modified_y.get_witness_index(),
343 .x3 = result._x.get_witness_index(),
344 .y3 = result._y.get_witness_index(),
345 });
346
347 // Merge the x and y origin tags since the output depends on both
348 result._x.set_origin_tag(OriginTag(_x.get_origin_tag(), _y.get_origin_tag()));
349 result._y.set_origin_tag(OriginTag(_x.get_origin_tag(), _y.get_origin_tag()));
350 }
351
352 return result;
353}
354
367template <typename Builder>
369 bool is_addition,
370 const std::optional<AffineElement> hint) const
371{
372 // This method should not be called on known points at infinity
373 BB_ASSERT(!this->is_constant_point_at_infinity(),
374 "cycle_group::_unconditional_add_or_subtract called on constant point at infinity");
376 "cycle_group::_unconditional_add_or_subtract called on constant point at infinity");
377
378 auto context = get_context(other);
379
380 // If one point is a witness and the other is a constant, convert the constant to a fixed witness then call this
381 // method again so we can use the ecc_add gate
382 const bool lhs_constant = is_constant();
383 const bool rhs_constant = other.is_constant();
384
385 if (lhs_constant && !rhs_constant) {
386 auto lhs = cycle_group::from_constant_witness(context, get_value());
387 lhs.set_origin_tag(get_origin_tag()); // Maintain the origin tag
388 return lhs._unconditional_add_or_subtract(other, is_addition, hint);
389 }
390 if (!lhs_constant && rhs_constant) {
392 rhs.set_origin_tag(other.get_origin_tag()); // Maintain the origin tag
393 return _unconditional_add_or_subtract(rhs, is_addition, hint);
394 }
395
396 // Compute the result coordinates (either from hint or by native calculation)
397 bb::fr x3;
398 bb::fr y3;
399 if (hint.has_value()) {
400 x3 = hint.value().x;
401 y3 = hint.value().y;
402 } else {
403 const AffineElement p1 = get_value();
404 const AffineElement p2 = other.get_value();
405 AffineElement p3 = is_addition ? (Element(p1) + Element(p2)) : (Element(p1) - Element(p2));
406 x3 = p3.x;
407 y3 = p3.y;
408 }
409
410 // Construct the result based on whether inputs are constant or witness
411 // Note: this code path is for non-infinity points, so result cannot be at infinity
412 // We use the private 4-arg constructor to explicitly set is_infinity=false, avoiding the
413 // auto-detection gates that the 2-arg constructor would add.
414 cycle_group result;
415 if (lhs_constant && rhs_constant) {
416 result = cycle_group(x3, y3, /*is_infinity=*/bool_t(false), /*assert_on_curve=*/false);
417 } else {
418 // Both points are witnesses - create result witness and construct ECC add constraint
419 result = cycle_group(
420 witness_t(context, x3), witness_t(context, y3), /*is_infinity=*/false, /*assert_on_curve=*/false);
421
422 context->create_ecc_add_gate(bb::ecc_add_gate_{
423 .x1 = _x.get_witness_index(),
424 .y1 = _y.get_witness_index(),
425 .x2 = other._x.get_witness_index(),
426 .y2 = other._y.get_witness_index(),
427 .x3 = result._x.get_witness_index(),
428 .y3 = result._y.get_witness_index(),
429 .is_addition = is_addition,
430 });
431 }
432
433 // Merge the origin tags from both inputs
434 result.set_origin_tag(OriginTag(get_origin_tag(), other.get_origin_tag()));
435
436 return result;
437}
438
445template <typename Builder>
447 const std::optional<AffineElement> hint) const
448{
449 return _unconditional_add_or_subtract(other, /*is_addition=*/true, hint);
450}
451
458template <typename Builder>
460 const std::optional<AffineElement> hint) const
461{
462 return _unconditional_add_or_subtract(other, /*is_addition=*/false, hint);
463}
464
476template <typename Builder>
478 const std::optional<AffineElement> hint) const
479{
480 const field_t x_delta = this->_x - other._x;
481 if (x_delta.is_constant()) {
482 BB_ASSERT(x_delta.get_value() != 0);
483 } else {
484 x_delta.assert_is_not_zero("cycle_group::checked_unconditional_add, x-coordinate collision");
485 }
486 return unconditional_add(other, hint);
487}
488
501template <typename Builder>
503 const std::optional<AffineElement> hint) const
504{
505 const field_t x_delta = this->_x - other._x;
506 if (x_delta.is_constant()) {
507 BB_ASSERT(x_delta.get_value() != 0);
508 } else {
509 x_delta.assert_is_not_zero("cycle_group::checked_unconditional_subtract, x-coordinate collision");
510 }
511 return unconditional_subtract(other, hint);
512}
513
529template <typename Builder> cycle_group<Builder> cycle_group<Builder>::operator+(const cycle_group& other) const
530{
531 // If lhs is constant point at infinity, return the rhs and vice versa
532 if (this->is_constant_point_at_infinity()) {
533 return other;
534 }
535 if (other.is_constant_point_at_infinity()) {
536 return *this;
537 }
538
539 const bool_t x_coordinates_match = (_x == other._x);
540 const bool_t y_coordinates_match = (_y == other._y);
541
542 const field_t x1 = _x;
543 const field_t y1 = _y;
544 const field_t x2 = other._x;
545 const field_t y2 = other._y;
546
547 // Execute point addition with modified lambda = (y2 - y1)/(x2 - x1 + x_coordinates_match) to avoid the possibility
548 // of division by zero.
549 const field_t x_diff = x2.add_two(-x1, x_coordinates_match);
550 // Compute lambda in one of two ways depending on whether either numerator or denominator is constant or not
551 field_t lambda;
552 if ((y1.is_constant() && y2.is_constant()) || x_diff.is_constant()) {
553 lambda = (y2 - y1).divide_no_zero_check(x_diff);
554 } else {
555 // Note: branch saves one gate vs just using divide_no_zero_check because we avoid computing y2 - y1 in circuit
556 Builder* context = get_context(other);
557 lambda = field_t::from_witness(context, (y2.get_value() - y1.get_value()) / x_diff.get_value());
558 // We need to manually propagate the origin tag
560 // Constrain x_diff * lambda = y2 - y1
561 field_t::evaluate_polynomial_identity(x_diff, lambda, -y2, y1);
562 }
563 const field_t add_result_x = lambda.madd(lambda, -(x2 + x1)); // x3 = lambda^2 - x1 - x2
564 const field_t add_result_y = lambda.madd(x1 - add_result_x, -y1); // y3 = lambda * (x1 - x3) - y1
565
566 // Compute the doubling result
567 const cycle_group dbl_result = dbl();
568
569 // If the addition amounts to a doubling then the result is the doubling result, else the addition result.
570 const bool_t double_predicate = (x_coordinates_match && y_coordinates_match);
571 auto result_x = field_t::conditional_assign(double_predicate, dbl_result._x, add_result_x);
572 auto result_y = field_t::conditional_assign(double_predicate, dbl_result._y, add_result_y);
573
574 // If the lhs is the point at infinity, return rhs
575 const bool_t lhs_infinity = is_point_at_infinity();
576 result_x = field_t::conditional_assign(lhs_infinity, other._x, result_x);
577 result_y = field_t::conditional_assign(lhs_infinity, other._y, result_y);
578
579 // If the rhs is the point at infinity, return lhs
580 const bool_t rhs_infinity = other.is_point_at_infinity();
581 result_x = field_t::conditional_assign(rhs_infinity, _x, result_x);
582 result_y = field_t::conditional_assign(rhs_infinity, _y, result_y);
583
584 // The result is the point at infinity if:
585 // (lhs._x, lhs._y) == (rhs._x, -rhs._y) and neither are infinity, OR both are the point at infinity
586 const bool_t infinity_predicate = (x_coordinates_match && !y_coordinates_match);
587 bool_t result_is_infinity = infinity_predicate && (!lhs_infinity && !rhs_infinity);
588 result_is_infinity = result_is_infinity || (lhs_infinity && rhs_infinity);
589
590 return cycle_group(result_x, result_y, /*is_infinity=*/result_is_infinity, /*assert_on_curve=*/false);
591}
592
608template <typename Builder> cycle_group<Builder> cycle_group<Builder>::operator-(const cycle_group& other) const
609{
610 // If lhs is constant point at infinity, return -rhs
611 if (this->is_constant_point_at_infinity()) {
612 return -other;
613 }
614 // If rhs is constant point at infinity, return the lhs
615 if (other.is_constant_point_at_infinity()) {
616 return *this;
617 }
618
619 Builder* context = get_context(other);
620
621 const bool_t x_coordinates_match = (_x == other._x);
622 const bool_t y_coordinates_match = (_y == other._y);
623
624 const field_t x1 = _x;
625 const field_t y1 = _y;
626 const field_t x2 = other._x;
627 const field_t y2 = other._y;
628
629 // Execute point addition with modified lambda = (-y2 - y1)/(x2 - x1 + x_coordinates_match) to avoid the possibility
630 // of division by zero.
631 const field_t x_diff = x2.add_two(-x1, x_coordinates_match);
632 // Compute lambda in one of two ways depending on whether either numerator or denominator is constant or not
633 field_t lambda;
634 if ((y1.is_constant() && y2.is_constant()) || x_diff.is_constant()) {
635 lambda = (-y2 - y1).divide_no_zero_check(x_diff);
636 } else {
637 // Note: branch saves one gate vs using divide_no_zero_check because we avoid computing (-y2 - y1) in circuit
638 lambda = field_t::from_witness(context, (-y2.get_value() - y1.get_value()) / x_diff.get_value());
639 // We need to manually propagate the origin tag
641 // Constrain x_diff * lambda = -y2 - y1
642 field_t::evaluate_polynomial_identity(x_diff, lambda, y2, y1);
643 }
644 const field_t sub_result_x = lambda.madd(lambda, -(x2 + x1)); // x3 = lambda^2 - x1 - x2
645 const field_t sub_result_y = lambda.madd(x1 - sub_result_x, -y1); // y3 = lambda * (x1 - x3) - y1
646
647 // Compute the doubling result
648 const cycle_group dbl_result = dbl();
649
650 // If the subtraction amounts to a doubling then the result is the doubling result, else the subtraction result.
651 // Note: The assumption here is that x1 == x2 && y1 != y2 implies y1 == -y2 which is true assuming that the points
652 // are both on the curve.
653 const bool_t double_predicate = (x_coordinates_match && !y_coordinates_match);
654 auto result_x = field_t::conditional_assign(double_predicate, dbl_result._x, sub_result_x);
655 auto result_y = field_t::conditional_assign(double_predicate, dbl_result._y, sub_result_y);
656
657 mark_witness_as_used(result_x);
658 mark_witness_as_used(result_y);
659
660 // If the lhs is the point at infinity, return -rhs
661 const bool_t lhs_infinity = is_point_at_infinity();
662 result_x = field_t::conditional_assign(lhs_infinity, other._x, result_x);
663 result_y = field_t::conditional_assign(lhs_infinity, (-other._y), result_y);
664
665 // If the rhs is the point at infinity, return lhs
666 const bool_t rhs_infinity = other.is_point_at_infinity();
667 result_x = field_t::conditional_assign(rhs_infinity, _x, result_x);
668 result_y = field_t::conditional_assign(rhs_infinity, _y, result_y);
669
670 // The result is the point at infinity if:
671 // (lhs.x, lhs.y) == (rhs.x, rhs.y) and neither are infinity, OR both are the point at infinity
672 const bool_t infinity_predicate = (x_coordinates_match && y_coordinates_match);
673 mark_witness_as_used(field_t(infinity_predicate));
674 bool_t result_is_infinity = infinity_predicate && (!lhs_infinity && !rhs_infinity);
675 result_is_infinity = result_is_infinity || (lhs_infinity && rhs_infinity);
676
677 return cycle_group(result_x, result_y, /*is_infinity=*/result_is_infinity, /*assert_on_curve=*/false);
678}
679
687template <typename Builder> cycle_group<Builder> cycle_group<Builder>::operator-() const
688{
689 cycle_group result(*this);
690 // We have to normalize immediately. All the methods, related to
691 // elliptic curve operations, assume that the coordinates are in normalized form and
692 // don't perform any extra normalizations
693 result._y = (-_y).normalize();
694 return result;
695}
696
697template <typename Builder> cycle_group<Builder>& cycle_group<Builder>::operator+=(const cycle_group& other)
698{
699 *this = *this + other;
700 return *this;
701}
702
703template <typename Builder> cycle_group<Builder>& cycle_group<Builder>::operator-=(const cycle_group& other)
704{
705 *this = *this - other;
706 return *this;
707}
708
729template <typename Builder>
731 const std::span<cycle_scalar> scalars,
732 const std::span<cycle_group> base_points,
733 const std::span<AffineElement const> offset_generators,
734 const bool unconditional_add)
735{
736 BB_ASSERT_EQ(!scalars.empty(), true, "Empty scalars provided to variable base batch mul!");
737 BB_ASSERT_EQ(scalars.size(), base_points.size(), "Points/scalars size mismatch in variable base batch mul!");
738 BB_ASSERT_EQ(offset_generators.size(), base_points.size() + 1, "Too few offset generators provided!");
739 const size_t num_points = scalars.size();
740
741 // Extract the circuit context from any scalar or point
742 Builder* context = nullptr;
743 for (auto [scalar, point] : zip_view(scalars, base_points)) {
744 if (context = scalar.get_context(); context != nullptr) {
745 break;
746 }
747 if (context = point.get_context(); context != nullptr) {
748 break;
749 }
750 }
751
752 // All cycle_scalars are guaranteed to be 254 bits
753 constexpr size_t num_rounds = numeric::ceil_div(cycle_scalar::NUM_BITS, ROM_TABLE_BITS);
754
755 // Decompose each scalar into 4-bit slices. Note: This operation enforces range constraints on the lo/hi limbs of
756 // each scalar (LO_BITS and (num_bits - LO_BITS) respectively).
758 scalar_slices.reserve(num_points);
759 for (const auto& scalar : scalars) {
760 scalar_slices.emplace_back(context, scalar, ROM_TABLE_BITS);
761 }
762
763 // Compute the result of each point addition involved in the Straus MSM algorithm natively so they can be used as
764 // "hints" in the in-circuit Straus algorithm. This includes the additions needed to construct the point tables and
765 // those needed to compute the MSM via Straus. Points are computed as Element types with a Z-coordinate then
766 // batch-converted to AffineElement types. This avoids the need to compute modular inversions for every group
767 // operation, which dramatically reduces witness generation times.
768 std::vector<Element> operation_transcript;
769 Element offset_generator_accumulator = offset_generators[0];
770 {
771 // For each point, construct native straus lookup table of the form {G , G + [1]P, G + [2]P, ... , G + [15]P}
772 std::vector<std::vector<Element>> native_straus_tables;
773 for (size_t i = 0; i < num_points; ++i) {
774 AffineElement point = base_points[i].get_value();
775 AffineElement offset = offset_generators[i + 1];
776 std::vector<Element> table = straus_lookup_table::compute_native_table(point, offset, ROM_TABLE_BITS);
777 // Copy all but the first entry (the offset generator) into the operation transcript for use as hints
778 std::copy(table.begin() + 1, table.end(), std::back_inserter(operation_transcript));
779 native_straus_tables.emplace_back(std::move(table));
780 }
781
782 // Perform the Straus algorithm natively to generate the witness values (hints) for all intermediate points
783 Element accumulator = offset_generators[0];
784 for (size_t i = 0; i < num_rounds; ++i) {
785 if (i != 0) {
786 // Perform doublings of the accumulator and offset generator accumulator
787 for (size_t j = 0; j < ROM_TABLE_BITS; ++j) {
788 accumulator = accumulator.dbl();
789 operation_transcript.push_back(accumulator);
790 offset_generator_accumulator = offset_generator_accumulator.dbl();
791 }
792 }
793 for (size_t j = 0; j < num_points; ++j) {
794 // Look up and accumulate the appropriate point for this scalar slice
795 auto slice_value = static_cast<size_t>(scalar_slices[j].slices_native[num_rounds - i - 1]);
796 const Element point = native_straus_tables[j][slice_value];
797 accumulator += point;
798
799 // Populate hint and update offset generator accumulator
800 operation_transcript.push_back(accumulator);
801 offset_generator_accumulator += Element(offset_generators[j + 1]);
802 }
803 }
804 }
805
806 // Normalize the computed witness points and convert them into AffineElements
807 Element::batch_normalize(&operation_transcript[0], operation_transcript.size());
808 std::vector<AffineElement> operation_hints;
809 operation_hints.reserve(operation_transcript.size());
810 for (const Element& element : operation_transcript) {
811 operation_hints.emplace_back(element.x, element.y);
812 }
813
814 // Construct an in-circuit Straus lookup table for each point
816 point_tables.reserve(num_points);
817 const size_t hints_per_table = (1ULL << ROM_TABLE_BITS) - 1;
818 for (size_t i = 0; i < num_points; ++i) {
819 // Construct Straus table
820 std::span<AffineElement> table_hints(&operation_hints[i * hints_per_table], hints_per_table);
821 straus_lookup_table table(context, base_points[i], offset_generators[i + 1], ROM_TABLE_BITS, table_hints);
822 point_tables.push_back(std::move(table));
823 }
824
825 // Initialize pointer to the precomputed Straus algorithm hints (stored just after the table construction hints)
826 AffineElement* hint_ptr = &operation_hints[num_points * hints_per_table];
827 cycle_group accumulator = offset_generators[0];
828
829 // Execute Straus algorithm in-circuit using the precomputed hints.
830 // If unconditional_add == false, accumulate x-coordinate differences to batch-validate no collisions.
831 field_t coordinate_check_product = 1;
832 for (size_t i = 0; i < num_rounds; ++i) {
833 // Double the accumulator ROM_TABLE_BITS times (except in first round)
834 if (i != 0) {
835 for (size_t j = 0; j < ROM_TABLE_BITS; ++j) {
836 accumulator = accumulator.dbl(*hint_ptr);
837 hint_ptr++;
838 }
839 }
840 // Add the contribution from each point's scalar slice for this round
841 for (size_t j = 0; j < num_points; ++j) {
842 const field_t scalar_slice = scalar_slices[j][num_rounds - i - 1];
843 BB_ASSERT_EQ(scalar_slice.get_value(), scalar_slices[j].slices_native[num_rounds - i - 1]); // Sanity check
844 const cycle_group point = point_tables[j].read(scalar_slice);
845 if (!unconditional_add) {
846 coordinate_check_product *= point._x - accumulator._x;
847 }
848 accumulator = accumulator.unconditional_add(point, *hint_ptr);
849 hint_ptr++;
850 }
851 }
852
853 // Batch-validate no x-coordinate collisions occurred. We batch because each assert_is_not_zero
854 // requires an expensive modular inversion during witness generation.
855 if (!unconditional_add) {
856 coordinate_check_product.assert_is_not_zero("_variable_base_batch_mul_internal x-coordinate collision");
857 }
858
859 // Set CONSTANT tag on accumulator to avoid tag conflicts during intermediate operations
860 // The final tag will be set in batch_mul from result_tag
861 accumulator.set_origin_tag(OriginTag::constant());
862
863 // Note: offset_generator_accumulator represents the sum of all the offset generator terms present in `accumulator`.
864 // We don't subtract it off yet as we may be able to combine it with other constant terms in `batch_mul` before
865 // performing the subtraction.
866 return { accumulator, AffineElement(offset_generator_accumulator) };
867}
868
892template <typename Builder>
894 const std::span<cycle_scalar> scalars, const std::span<AffineElement> base_points)
895{
896 BB_ASSERT_EQ(scalars.size(), base_points.size(), "Points/scalars size mismatch in fixed-base batch mul");
897
901
902 std::vector<MultiTableId> multitable_ids;
903 std::vector<field_t> scalar_limbs;
904
905 for (const auto [point, scalar] : zip_view(base_points, scalars)) {
906 std::array<MultiTableId, 2> table_id = table::get_lookup_table_ids_for_point(point);
907 multitable_ids.push_back(table_id[0]);
908 multitable_ids.push_back(table_id[1]);
909 scalar_limbs.push_back(scalar.lo());
910 scalar_limbs.push_back(scalar.hi());
911 }
912
913 // Look up the multiples of each slice of each lo/hi scalar limb in the corresponding plookup table.
914 std::vector<cycle_group> lookup_points;
915 Element offset_generator_accumulator = Group::point_at_infinity;
916 for (const auto [table_id, scalar] : zip_view(multitable_ids, scalar_limbs)) {
917 // Each lookup returns multiple EC points corresponding to different bit slices of the scalar.
918 // For a scalar slice s_i at bit position (table_bits*i), the table stores the point:
919 // P_i = [offset_generator_i] + (s_i * 2^(table_bits*i)) * [base_point]
921 for (size_t j = 0; j < lookup_data[ColumnIdx::C2].size(); ++j) {
922 const field_t x = lookup_data[ColumnIdx::C2][j];
923 const field_t y = lookup_data[ColumnIdx::C3][j];
924 // Lookup table points are never at infinity (they are precomputed points on the curve).
925 // Use private constructor to avoid auto-detection gates.
926 auto is_infinity = bool_t(x.get_context(), false);
927 lookup_points.push_back(cycle_group(x, y, is_infinity, /*assert_on_curve=*/false));
928 }
929 // Update offset accumulator with the total offset for the corresponding multitable
930 offset_generator_accumulator += table::get_generator_offset_for_table_id(table_id);
931 }
932
933 // Compute the witness values of the batch_mul algorithm natively, as Element types with a Z-coordinate.
934 std::vector<Element> operation_transcript;
935 {
936 Element accumulator = lookup_points[0].get_value();
937 for (size_t i = 1; i < lookup_points.size(); ++i) {
938 accumulator += (lookup_points[i].get_value());
939 operation_transcript.push_back(accumulator);
940 }
941 }
942 // Batch-convert to AffineElement types, and feed these points as "hints" into the in-circuit addition. This avoids
943 // the need to compute modular inversions for every group operation, which dramatically reduces witness generation
944 // times.
945 Element::batch_normalize(&operation_transcript[0], operation_transcript.size());
946 std::vector<AffineElement> operation_hints;
947 operation_hints.reserve(operation_transcript.size());
948 for (const Element& element : operation_transcript) {
949 operation_hints.emplace_back(element.x, element.y);
950 }
951
952 // Perform the in-circuit point additions sequentially. Each addition costs 1 gate iff additions are chained such
953 // that the output of each addition is the input to the next. Otherwise, each addition costs 2 gates.
954 // Set all lookup_points to have CONSTANT tags to avoid tag conflicts during intermediate operations
955 // The final tag will be set in batch_mul from result_tag
956 auto const_tag = OriginTag::constant();
957 for (auto& point : lookup_points) {
958 point.set_origin_tag(const_tag);
959 }
960 cycle_group accumulator = lookup_points[0];
961 for (size_t i = 1; i < lookup_points.size(); ++i) {
962 accumulator = accumulator.unconditional_add(lookup_points[i], operation_hints[i - 1]);
963 }
964
965 // The offset_generator_accumulator represents the sum of all the offset generator terms present in `accumulator`.
966 // We don't subtract off yet, as we may be able to combine `offset_generator_accumulator` with other constant
967 // terms in `batch_mul` before performing the subtraction.
968 return { accumulator, offset_generator_accumulator };
969}
970
1007template <typename Builder>
1009 const std::vector<cycle_scalar>& scalars,
1011{
1012 BB_ASSERT_EQ(scalars.size(), base_points.size(), "Points/scalars size mismatch in batch mul!");
1013
1014 if (scalars.empty()) {
1015 cycle_group result{ Group::point_at_infinity };
1016 return result;
1017 }
1018
1019 std::vector<cycle_scalar> variable_base_scalars;
1020 std::vector<cycle_group> variable_base_points;
1021 std::vector<cycle_scalar> fixed_base_scalars;
1022 std::vector<AffineElement> fixed_base_points;
1023
1024 // Merge all tags
1025 OriginTag result_tag = OriginTag::constant(); // Initialize as CONSTANT so merging with input tags works correctly
1026 for (auto [point, scalar] : zip_view(base_points, scalars)) {
1027 result_tag = OriginTag(result_tag, OriginTag(point.get_origin_tag(), scalar.get_origin_tag()));
1028 }
1029
1030 // We can unconditionally add in the variable-base algorithm iff all of the input points are fixed-base points (i.e.
1031 // we are doing fixed-base mul over points not present in our plookup tables)
1032 bool can_unconditional_add = true;
1033 bool has_non_constant_component = false;
1034 Element constant_acc = Group::point_at_infinity;
1035 for (const auto [point, scalar] : zip_view(base_points, scalars)) {
1036 if (scalar.is_constant() && point.is_constant()) {
1037 // Case 1: both point and scalar are constant; update constant accumulator without adding gates
1038 constant_acc += (point.get_value()) * (scalar.get_value());
1039 } else if (!scalar.is_constant() && point.is_constant()) {
1040 if (point.get_value().is_point_at_infinity()) {
1041 // oi mate, why are you creating a circuit that multiplies a known point at infinity?
1042#ifndef FUZZING_DISABLE_WARNINGS
1043 info("Warning: Performing batch mul with constant point at infinity!");
1044#endif
1045 // Constant infinity * witness scalar contributes nothing to the result, however, we must still apply
1046 // the range constraints that the cycle_scalar constructor defers to this method.
1047 auto* ctx = scalar.get_context();
1048 ctx->create_limbed_range_constraint(scalar.lo().get_witness_index(),
1050 ROM_TABLE_BITS,
1051 "batch_mul: lo range constraint for scalar with constant "
1052 "infinity");
1053 ctx->create_limbed_range_constraint(scalar.hi().get_witness_index(),
1055 ROM_TABLE_BITS,
1056 "batch_mul: hi range constraint for scalar with constant "
1057 "infinity");
1058 continue;
1059 }
1061 // Case 2A: constant point is one of two for which we have plookup tables; use fixed-base Straus
1062 fixed_base_scalars.push_back(scalar);
1063 fixed_base_points.push_back(point.get_value());
1064 } else {
1065 // Case 2B: constant point but no precomputed lookup tables; use variable-base Straus with ROM tables
1066 variable_base_scalars.push_back(scalar);
1067 variable_base_points.push_back(point);
1068 }
1069 has_non_constant_component = true;
1070 } else {
1071 // Case 3: point is a witness; use variable-base Straus with ROM tables
1072 variable_base_scalars.push_back(scalar);
1073 variable_base_points.push_back(point);
1074 can_unconditional_add = false;
1075 has_non_constant_component = true;
1076 }
1077 }
1078
1079 // If all inputs are constant, return the computed constant component and call it a day.
1080 if (!has_non_constant_component) {
1081 auto result = cycle_group(constant_acc);
1082 result.set_origin_tag(result_tag);
1083 return result;
1084 }
1085
1086 // Add the constant component into our offset accumulator. (Note: we'll subtract `offset_accumulator` from the MSM
1087 // output later on so we negate here to counter that future negation).
1088 Element offset_accumulator = -constant_acc;
1089 const bool has_variable_points = !variable_base_points.empty();
1090 const bool has_fixed_points = !fixed_base_points.empty();
1091
1092 cycle_group result;
1093 if (has_fixed_points) {
1094 // Compute the result of the fixed-base portion of the MSM and update the offset accumulator
1095 const auto [fixed_accumulator, offset_generator_delta] =
1096 _fixed_base_batch_mul_internal(fixed_base_scalars, fixed_base_points);
1097 offset_accumulator += offset_generator_delta;
1098 result = fixed_accumulator;
1099 }
1100
1101 if (has_variable_points) {
1102 // Compute required offset generators; one per point plus one extra for the initial accumulator
1103 const size_t num_offset_generators = variable_base_points.size() + 1;
1104 const std::span<AffineElement const> offset_generators =
1105 context.generators->get(num_offset_generators, 0, OFFSET_GENERATOR_DOMAIN_SEPARATOR);
1106
1107 // Compute the result of the variable-base portion of the MSM and update the offset accumulator
1108 const auto [variable_accumulator, offset_generator_delta] = _variable_base_batch_mul_internal(
1109 variable_base_scalars, variable_base_points, offset_generators, can_unconditional_add);
1110 offset_accumulator += offset_generator_delta;
1111
1112 // Combine the variable-base result with the fixed-base result (if present)
1113 if (has_fixed_points) {
1114 result = can_unconditional_add ? result.unconditional_add(variable_accumulator)
1115 : result.checked_unconditional_add(variable_accumulator);
1116 } else {
1117 result = variable_accumulator;
1118 }
1119 }
1120
1121 // Update `result` to remove the offset generator terms, and add in any constant terms from `constant_acc`.
1122 // We have two potential modes here:
1123 // 1. All inputs are fixed-base and constant_acc is not the point at infinity
1124 // 2. Everything else.
1125 // Case 1 is a special case, as we *know* we cannot hit incomplete addition edge cases, under the assumption that
1126 // all input points are linearly independent of one another. Because constant_acc is not the point at infinity we
1127 // know that at least 1 input scalar was not zero, i.e. the output will not be the point at infinity. We also know
1128 // that, under case 1, we won't trigger the doubling formula either, as every point is linearly independent of every
1129 // other point (including offset generators).
1130 if (!constant_acc.is_point_at_infinity() && can_unconditional_add) {
1131 result = result.unconditional_add(AffineElement(-offset_accumulator));
1132 } else {
1133 // For case 2, we must use a full subtraction operation that handles all possible edge cases, as the output
1134 // point may be the point at infinity.
1135 // Note about optimizations for posterity: An honest prover might hit the point at infinity, but won't
1136 // trigger the doubling edge case (since doubling edge case implies input points are also the offset generator
1137 // points). We could do the following which would be slightly cheaper than operator-:
1138 // 1. If x-coords match, assert y-coords do not match
1139 // 2. If x-coords match, return point at infinity, else unconditionally compute result - offset_accumulator.
1140 result = result - cycle_group(AffineElement(offset_accumulator));
1141 }
1142 // Ensure the tag of the result is a union of all inputs
1143 result.set_origin_tag(result_tag);
1144 return result;
1145}
1146
1147template <typename Builder> cycle_group<Builder> cycle_group<Builder>::operator*(const cycle_scalar& scalar) const
1148{
1149 return batch_mul({ *this }, { scalar });
1150}
1151
1152template <typename Builder> cycle_group<Builder>& cycle_group<Builder>::operator*=(const cycle_scalar& scalar)
1153{
1154 *this = operator*(scalar);
1155 return *this;
1156}
1157
1158template <typename Builder> cycle_group<Builder> cycle_group<Builder>::operator*(const BigScalarField& scalar) const
1159{
1160 return batch_mul({ *this }, { scalar });
1161}
1162
1164{
1165 *this = operator*(scalar);
1166 return *this;
1167}
1168
1170{
1171 this->standardize();
1172 other.standardize();
1173 const auto equal = (_x == other._x) && (_y == other._y) && (this->_is_infinity == other._is_infinity);
1174 return equal;
1175}
1176
1177template <typename Builder> void cycle_group<Builder>::assert_equal(cycle_group& other, std::string const& msg)
1178{
1179 this->standardize();
1180 other.standardize();
1181 _x.assert_equal(other._x, msg);
1182 _y.assert_equal(other._y, msg);
1183 this->_is_infinity.assert_equal(other._is_infinity);
1184}
1185
1186template <typename Builder>
1188 const cycle_group& lhs,
1189 const cycle_group& rhs)
1190{
1191 auto x_res = field_t::conditional_assign(predicate, lhs._x, rhs._x);
1192 auto y_res = field_t::conditional_assign(predicate, lhs._y, rhs._y);
1193 auto _is_infinity_res =
1194 bool_t::conditional_assign(predicate, lhs.is_point_at_infinity(), rhs.is_point_at_infinity());
1195
1196 cycle_group<Builder> result(x_res, y_res, _is_infinity_res, /*assert_on_curve=*/false);
1197 return result;
1198};
1199
1202
1203} // namespace bb::stdlib
#define BB_ASSERT(expression,...)
Definition assert.hpp:70
#define BB_ASSERT_EQ(actual, expected,...)
Definition assert.hpp:83
constexpr bool is_point_at_infinity() const noexcept
constexpr void self_set_infinity() noexcept
element class. Implements ecc group arithmetic using Jacobian coordinates See https://hyperelliptic....
Definition element.hpp:33
constexpr element dbl() const noexcept
BB_INLINE constexpr bool is_point_at_infinity() const noexcept
Container for lookup accumulator values and table reads.
Definition types.hpp:357
Generates plookup tables required to perform fixed-base scalar multiplication over a fixed number of ...
static bool lookup_table_exists_for_point(const affine_element &input)
Returns true iff provided point is one of the two for which we have a precomputed lookup table.
Implements boolean logic in-circuit.
Definition bool.hpp:60
bool get_value() const
Definition bool.hpp:125
bool is_constant() const
Definition bool.hpp:127
static bool_t conditional_assign(const bool_t< Builder > &predicate, const bool_t &lhs, const bool_t &rhs)
Conditionally assign lhs or rhs based on predicate, always returns normalized result.
Definition bool.cpp:478
Builder * get_context() const
Definition bool.hpp:152
cycle_group represents a group Element of the proving system's embedded curve, i.e....
cycle_group dbl(const std::optional< AffineElement > hint=std::nullopt) const
Evaluates a point doubling using Ultra ECC double gate (if non-constant)
static cycle_group from_constant_witness(Builder *_context, const AffineElement &_in)
Converts a native AffineElement into a witness, but constrains the witness values to be known constan...
cycle_group & operator*=(const cycle_scalar &scalar)
void standardize()
Convert the point to standard form.
cycle_group unconditional_add(const cycle_group &other, const std::optional< AffineElement > hint=std::nullopt) const
Evaluate incomplete ECC point addition over *this and other.
void validate_on_curve() const
On-curve check.
bool_t operator==(cycle_group &other)
Builder * get_context(const cycle_group &other) const
cycle_group & operator-=(const cycle_group &other)
static cycle_group conditional_assign(const bool_t &predicate, const cycle_group &lhs, const cycle_group &rhs)
void unset_free_witness_tag()
Unset the free witness flag for the cycle_group's tags.
cycle_group checked_unconditional_subtract(const cycle_group &other, const std::optional< AffineElement > hint=std::nullopt) const
Evaluate incomplete ECC point subtraction over *this and other, with x-coordinate collision checks.
cycle_group _unconditional_add_or_subtract(const cycle_group &other, bool is_addition, const std::optional< AffineElement > hint) const
Will evaluate ECC point addition or subtraction over *this and other.
static cycle_group from_witness(Builder *_context, const AffineElement &_in)
Converts an AffineElement into a circuit witness.
cycle_group operator-() const
Negates a point.
static cycle_group one(Builder *_context)
Construct a constant cycle_group representation of Group::one.
void set_free_witness_tag()
Set the free witness flag for the cycle_group's tags.
void set_origin_tag(OriginTag tag) const
Set the origin tag for x, y and _is_infinity members of cycle_group.
cycle_group & operator+=(const cycle_group &other)
static cycle_group constant_infinity(Builder *_context=nullptr)
Construct a constant point at infinity.
bool is_constant_point_at_infinity() const
bool_t is_point_at_infinity() const
static batch_mul_internal_output _variable_base_batch_mul_internal(std::span< cycle_scalar > scalars, std::span< cycle_group > base_points, std::span< AffineElement const > offset_generators, bool unconditional_add)
Internal logic to perform a variable-base batch mul using the Straus MSM algorithm.
cycle_group(Builder *_context=nullptr)
Construct a new constant point at infinity cycle group object.
AffineElement get_value() const
OriginTag get_origin_tag() const
Get the origin tag of cycle_group (a merege of origin tags of x, y and _is_infinity members)
cycle_group operator*(const cycle_scalar &scalar) const
static batch_mul_internal_output _fixed_base_batch_mul_internal(std::span< cycle_scalar > scalars, std::span< AffineElement > base_points)
Internal algorithm to perform a fixed-base batch mul.
void assert_equal(cycle_group &other, std::string const &msg="cycle_group::assert_equal")
cycle_group operator+(const cycle_group &other) const
Evaluate ECC point addition over *this and other.
cycle_group unconditional_subtract(const cycle_group &other, const std::optional< AffineElement > hint=std::nullopt) const
Evaluate incomplete ECC point subtraction over *this and other.
Builder * get_context() const
cycle_group checked_unconditional_add(const cycle_group &other, const std::optional< AffineElement > hint=std::nullopt) const
Evaluate incomplete ECC point addition over *this and other, with x-coordinate collision checks.
static cycle_group batch_mul(const std::vector< cycle_group > &base_points, const std::vector< BigScalarField > &scalars, GeneratorContext context={})
Represents a member of the Grumpkin curve scalar field (i.e. BN254 base field).
static constexpr size_t NUM_BITS
static constexpr size_t LO_BITS
static constexpr size_t HI_BITS
void assert_equal(const field_t &rhs, std::string const &msg="field_t::assert_equal") const
Copy constraint: constrain that *this field is equal to rhs element.
Definition field.cpp:940
field_t madd(const field_t &to_mul, const field_t &to_add) const
Definition field.cpp:518
static void evaluate_polynomial_identity(const field_t &a, const field_t &b, const field_t &c, const field_t &d, const std::string &msg="field_t::evaluate_polynomial_identity")
Given a, b, c, d, constrain a * b + c + d = 0 by creating a big_mul_gate.
Definition field.cpp:1135
Builder * get_context() const
Definition field.hpp:431
field_t sqr() const
Definition field.hpp:283
OriginTag get_origin_tag() const
Definition field.hpp:358
bb::fr get_value() const
Given a := *this, compute its value given by a.v * a.mul + a.add.
Definition field.cpp:836
static field_t from_witness(Builder *ctx, const bb::fr &input)
Definition field.hpp:466
bool_t< Builder > is_zero() const
Validate whether a field_t element is zero.
Definition field.cpp:783
void convert_constant_to_fixed_witness(Builder *ctx)
Definition field.hpp:456
bool is_constant() const
Definition field.hpp:441
void set_origin_tag(const OriginTag &new_tag) const
Definition field.hpp:357
field_t add_two(const field_t &add_b, const field_t &add_c) const
Efficiently compute (this + a + b) using big_mul gate.
Definition field.cpp:583
static field_t conditional_assign(const bool_t< Builder > &predicate, const field_t &lhs, const field_t &rhs)
Definition field.hpp:384
void assert_is_not_zero(std::string const &msg="field_t::assert_is_not_zero") const
Constrain *this to be non-zero by establishing that it has an inverse.
Definition field.cpp:718
uint32_t get_witness_index() const
Get the witness index of the current field element.
Definition field.hpp:518
static plookup::ReadData< field_pt > get_lookup_accumulators(const plookup::MultiTableId id, const field_pt &key_a, const field_pt &key_b=0, const bool is_2_to_1_lookup=false)
Definition plookup.cpp:19
straus_lookup_table computes a lookup table of size 1 << table_bits
static std::vector< Element > compute_native_table(const Element &base_point, const Element &offset_generator, size_t table_bits)
Compute the output points generated when computing the Straus lookup table.
#define info(...)
Definition log.hpp:93
StrictMock< MockContext > context
bb::curve::BN254::Element Element
ssize_t offset
Definition engine.cpp:52
constexpr T ceil_div(const T &numerator, const T &denominator)
Computes the ceiling of the division of two integral types.
Definition general.hpp:23
std::conditional_t< IsGoblinBigGroup< C, Fq, Fr, G >, element_goblin::goblin_element< C, goblin_field< C >, Fr, G >, element_default::element< C, Fq, Fr, G > > element
element wraps either element_default::element or element_goblin::goblin_element depending on parametr...
void mark_witness_as_used(const field_t< Builder > &field)
Mark a field_t witness as used (for UltraBuilder only).
field< Bn254FrParams > fr
Definition fr.hpp:155
Univariate< Fr, domain_end > operator*(const Fr &ff, const Univariate< Fr, domain_end > &uv)
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
This file contains part of the logic for the Origin Tag mechanism that tracks the use of in-circuit p...
static OriginTag constant()
BB_INLINE constexpr field sqr() const noexcept
static constexpr field zero()
Stores temporary variables produced by internal multiplication algorithms.