Skip to content

Commit 2166a06

Browse files
authored
fix: bigfield veridise audit fixes (#16842)
Summary of the fixes based on the audit of the `bigfield` class by Veridise (on commit [480f49d](https://github.com/AztecProtocol/aztec-packages/tree/480f49dad314ea4e753ff1e5180992a16926d2b3)): #### [V-AZT-VUL-001: Missing zero-check for division by constant]() Added an assertion that triggers when the divisor is a constant zero and the flag `check_for_zero` is set to true. Also added a test that validates this behaviour. Response: Fixed in bbf2f73 #### [V-AZT-VUL-002: Incomplete ‘assert_is_not_equal‘ assumes random distribution]() The function `assert_is_not_equal` is no longer exposed as a public function (via noir). In fact, none of the non-native field methods (and group operations) are exposed to developers via noir. Noir instead uses the natively written [noir-bignum](https://github.com/noir-lang/noir-bignum) library. (The `bigint_constraint.cpp` file that contains the interface for non-native field methods between noir and barretenberg is yet to be removed from the repository, this will be done in an upcoming PR.) Response: Acknowledged #### [V-AZT-VUL-003: borrow_lo rounds down]() Updated the computation of `borrow_lo_value` to use ceiling instead of floor: ```cpp uint256_t borrow_lo_value = (max_remainders_lo + ((uint256_t(1) << (2 * NUM_LIMB_BITS)) - 1)) >> (2 * NUM_LIMB_BITS); ``` Response: Fixed in c1bdd88 #### [V-AZT-VUL-004: Unsafe default for `msub_div()`]() Changed the default value of `enable_divisor_nz_check` to `true` and explicitly pass it as `false` in its usage in the `biggroup` class. In a few instances, passing this parameter as `false` can lead to incorrect handling of the point at infinity, added TODOs at such places. These instances will be analysed separately to avoid any unexpected behaviour. Response: Fixed in 25e820c, usage of `msub_div` will be analysed separately #### [V-AZT-VUL-005: Limbs with a maximum value of 0 trigger compilation failure]() In the function `decompose_into_default_range` we have an assertion: ```cpp ASSERT(num_bits > 0) ``` because number of bits (of a circuit variable) being 0 does not really make sense. The only case when `carry_hi_msb` or `carry_lo_msb` can be 0 is when: ```cpp template <typename Builder, typename T> void bigfield<Builder, T>::unsafe_evaluate_multiply_add(const bigfield& input_left, const bigfield& input_to_mul, const std::vector<bigfield>& to_add, const bigfield& input_quotient, const std::vector<bigfield>& input_remainders) ``` is called with `input_to_mul` as constant 0 and/or `input_quotient` as constant 0. In the usage of the function `unsafe_evaluate_multiply_add` (or `unsafe_evaluate_multiple_multiply_add`) the `input_quotient` is always a circuit witness. This ensures that the `carry_lo_msb > 0` and `carry_hi_msb > 0` (added assertions to verify this empirically). Response: We think it does not make sense to support `num_bits = 0` in range constraint functions, but open to discussion on this if there's any way `carry_lo_msb` or `carry_hi_msb` can actually be 0 in practice. #### [V-AZT-VUL-006: Off-by-one comparison of maximum_unreduced_limb_value]() Changed the definition of `get_maximum_unreduced_limb_value()` to $2^Q - 1$ (from $2^Q$). Response: Fixed in 3da35ba #### [V-AZT-VUL-007: Maintainability Improvements]() 1. Removed unused constants: - `bigfield.prime_basis_maximum_limb` - `bigfield.shift_right_1` - `bigfield.shift_right_2` 2. Renamed `negative_prime_modulus_mod_binary_basis` to `negative_prime_modulus_mod_native_basis` and use that while checking the prime limb consistency (in `evaluate_polynomial_identity`) 3. Used `NUM_LIMBS` instead of hard-coded 4. 4. Fixed comments (using $L$ instead of $t$) 5. Changed the definition of `get_maximum_unreduced_value()` to a form $(2^N - 1)$ and use the comparison operators correctly 6. Fixed `static constexpr uint64_t MAX_UNREDUCED_LIMB_BITS = NUM_LIMB_BITS + MAX_ADDITION_LOG` 7. Got rid of variable `neg_prime` 8. Used `is_constant()` instead of iterating over each limb to check constant-ness in `operator+` and `operator-`. 9. Removed duplicate/redundant assertion: `ASSERT(input_left.size() == input_right.size() && input_left.size() < 1024);` 10. Fixed misc comments Response: Fixed in e2024df #### [V-AZT-VUL-008: Incorrect term used during calculation on bound of upper limb]() Corrected the calculation of the resultant maximum bound of the upper and lower limbs: $$D_{\textsf{hi}} < 2^{2Q + L + \textcolor{red}{3}},$$ $$D_{\textsf{lo}} < 2^{2Q + L + \textcolor{red}{2}}.$$ Response: Fixed in f41813e #### [V-AZT-VUL-009: Optimization Improvements]() 1. In the function `add_two`, we could add another `if` condition when two of the three inputs are constant and reduced by the prime. But the current implementation should handle that implicitly on calling `add_two` on each limb. Note that no additional gates would be added since each `add_two` call on limbs would just update additive constants on the witness. 2. Agree with the observation but we think its fine to have redundant reductions in constant case as that would cause negligible impact on prover performance. 3. It is a good suggestion to use a tighter bound for range constraint on the quotient when reduction is not necessary. However, while creating the quotient witness using the constructor: ```cpp template <typename Builder, typename T> bigfield<Builder, T> bigfield<Builder, T>::create_from_u512_as_witness(Builder* ctx, const uint512_t& value, const bool can_overflow, const size_t maximum_bitlength) ``` when `can_overflow` is set to `false`, `maximum_bitlength` must be greater than $3L$. This assertion breaks if we use tighter range constraint on the quotient. This needs to be investigated further.
1 parent 0d1e742 commit 2166a06

File tree

8 files changed

+268
-128
lines changed

8 files changed

+268
-128
lines changed

barretenberg/cpp/scripts/test_civc_standalone_vks_havent_changed.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ cd ..
1313
# - Generate a hash for versioning: sha256sum bb-civc-inputs.tar.gz
1414
# - Upload the compressed results: aws s3 cp bb-civc-inputs.tar.gz s3://aztec-ci-artifacts/protocol/bb-civc-inputs-[hash(0:8)].tar.gz
1515
# Note: In case of the "Test suite failed to run ... Unexpected token 'with' " error, need to run: docker pull aztecprotocol/build:3.0
16-
pinned_short_hash="e70f08be"
16+
pinned_short_hash="8ba25b76"
1717
pinned_civc_inputs_url="https://aztec-ci-artifacts.s3.us-east-2.amazonaws.com/protocol/bb-civc-inputs-${pinned_short_hash}.tar.gz"
1818

1919
function compress_and_upload {

barretenberg/cpp/src/barretenberg/stdlib/eccvm_verifier/eccvm_recursive_verifier.test.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ class ECCVMRecursiveTests : public ::testing::Test {
139139
}
140140

141141
// Check that the size of the recursive verifier is consistent with historical expectation
142-
uint32_t NUM_GATES_EXPECTED = 213775;
142+
uint32_t NUM_GATES_EXPECTED = 216360;
143143
ASSERT_EQ(static_cast<uint32_t>(outer_circuit.get_num_finalized_gates()), NUM_GATES_EXPECTED)
144144
<< "Ultra-arithmetized ECCVM Recursive verifier gate count changed! Update this value if you are sure this "
145145
"is expected.";

barretenberg/cpp/src/barretenberg/stdlib/honk_verifier/ultra_recursive_verifier.test.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ template <typename RecursiveFlavor> class RecursiveVerifierTest : public testing
293293
}
294294
// Check the size of the recursive verifier
295295
if constexpr (std::same_as<RecursiveFlavor, MegaZKRecursiveFlavor_<UltraCircuitBuilder>>) {
296-
uint32_t NUM_GATES_EXPECTED = 801953;
296+
uint32_t NUM_GATES_EXPECTED = 808803;
297297
ASSERT_EQ(static_cast<uint32_t>(outer_circuit.get_num_finalized_gates()), NUM_GATES_EXPECTED)
298298
<< "MegaZKHonk Recursive verifier changed in Ultra gate count! Update this value if you "
299299
"are sure this is expected.";

barretenberg/cpp/src/barretenberg/stdlib/primitives/bigfield/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -413,34 +413,34 @@ $$
413413
>
414414
> $$
415415
> \begin{aligned}
416-
> \textcolor{orange}{D_{\textsf{lo}}} &= (2^{Q} - 1)^2 + 2 \cdot (2^{Q} - 1)^2 \cdot 2^L \ < \ 2^{2Q + L + 1}, \\
417-
> \textcolor{orange}{D_{\textsf{hi}}} &= 3 \cdot (2^{Q} - 1)^2 + 4 \cdot (2^{Q} - 1)^2 \cdot 2^L \ < \ 2^{2Q + L + 2}.
416+
> \textcolor{orange}{D_{\textsf{lo}}} &= (2^{Q} - 1)^2 + 2 \cdot (2^{Q} - 1)^2 \cdot 2^L \ < \ 2^{2Q + L + 2}, \\
417+
> \textcolor{orange}{D_{\textsf{hi}}} &= 3 \cdot (2^{Q} - 1)^2 + 4 \cdot (2^{Q} - 1)^2 \cdot 2^L \ < \ 2^{2Q + L + 3}.
418418
> \end{aligned}
419419
> $$
420420
>
421421
> Clearly, maximum value of $\textcolor{orange}{D_{\textsf{hi}}}$ determines the overall maximum of the two outputs of multiplication:
422422
>
423423
> $$
424-
> \textsf{max}(\textcolor{orange}{D_{\textsf{lo}}}, \textcolor{orange}{D_{\textsf{hi}}}) < 2^{2Q + L + 2}.
424+
> \textsf{max}(\textcolor{orange}{D_{\textsf{lo}}}, \textcolor{orange}{D_{\textsf{hi}}}) < 2^{2Q + L + 3}.
425425
> $$
426426
>
427427
> This is the maximum value of the resulting limbs of a single product $d = a \cdot b$. Suppose we have $2^k$ such products, then the maximum value of the limbs of the sum of these products would be:
428428
>
429429
> $$
430430
> \begin{aligned}
431-
> \sum_{i=1}^{2^k} \textcolor{orange}{D_{\textsf{hi}, i}} & \ < \ 2^{k + 2Q + L + 2}.
431+
> \sum_{i=1}^{2^k} \textcolor{orange}{D_{\textsf{hi}, i}} & \ < \ 2^{k + 2Q + L + 3}.
432432
> \end{aligned}
433433
> $$
434434
>
435435
> We require that this value must not exceed the native field modulus $n$. Hence, we need to ensure that:
436436
>
437437
> $$
438438
> \begin{aligned}
439-
> 2^{k + 2Q + L + 2} &< n \quad \implies \quad \boxed{Q < \frac{\text{log}_2(n) - L - k - 2}{2}}.
439+
> 2^{k + 2Q + L + 3} &< n \quad \implies \quad \boxed{Q < \frac{\text{log}_2(n) - L - k - 3}{2}}.
440440
> \end{aligned}
441441
> $$
442442
>
443-
> This means that the maximum limb size that must be allowed is $Q = \left\lfloor\frac{253.5 - 68 - 10 - 2}{2}\right\rfloor = 86$.
443+
> This means that the maximum limb size that must be allowed is $Q = \left\lfloor\frac{253.5 - 68 - 10 - 3}{2}\right\rfloor = 86$.
444444
>
445445
> .
446446

barretenberg/cpp/src/barretenberg/stdlib/primitives/bigfield/bigfield.hpp

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ template <typename Builder, typename T> class bigfield {
5555
}
5656
friend std::ostream& operator<<(std::ostream& os, const Limb& a)
5757
{
58-
os << "{ " << a.element << " < " << a.maximum_value << " }";
58+
os << "{ " << a.element << " <= " << a.maximum_value << " }";
5959
return os;
6060
}
6161
Limb(const Limb& other) = default;
@@ -326,29 +326,25 @@ template <typename Builder, typename T> class bigfield {
326326
static constexpr size_t MAXIMUM_SUMMAND_COUNT_LOG = 4;
327327
static constexpr size_t MAXIMUM_SUMMAND_COUNT = 1 << MAXIMUM_SUMMAND_COUNT_LOG;
328328

329-
static constexpr uint256_t prime_basis_maximum_limb =
330-
modulus_u512.slice(NUM_LIMB_BITS * (NUM_LIMBS - 1), NUM_LIMB_BITS* NUM_LIMBS).lo;
331329
static constexpr Basis prime_basis{ uint512_t(bb::fr::modulus), bb::fr::modulus.get_msb() + 1 };
332330
static constexpr Basis binary_basis{ uint512_t(1) << LOG2_BINARY_MODULUS, LOG2_BINARY_MODULUS };
333331
static constexpr Basis target_basis{ modulus_u512, static_cast<size_t>(modulus_u512.get_msb() + 1) };
334332
static constexpr bb::fr shift_1 = bb::fr(uint256_t(1) << NUM_LIMB_BITS);
335333
static constexpr bb::fr shift_2 = bb::fr(uint256_t(1) << (NUM_LIMB_BITS * 2));
336334
static constexpr bb::fr shift_3 = bb::fr(uint256_t(1) << (NUM_LIMB_BITS * 3));
337-
static constexpr bb::fr shift_right_1 = bb::fr(1) / shift_1;
338-
static constexpr bb::fr shift_right_2 = bb::fr(1) / shift_2;
339-
static constexpr bb::fr negative_prime_modulus_mod_binary_basis = -bb::fr(uint256_t(modulus_u512));
340-
static constexpr uint512_t negative_prime_modulus = binary_basis.modulus - target_basis.modulus;
341-
static constexpr std::array<uint256_t, NUM_LIMBS> neg_modulus_limbs_u256{
342-
negative_prime_modulus.slice(0, NUM_LIMB_BITS).lo,
343-
negative_prime_modulus.slice(NUM_LIMB_BITS, NUM_LIMB_BITS * 2).lo,
344-
negative_prime_modulus.slice(NUM_LIMB_BITS * 2, NUM_LIMB_BITS * 3).lo,
345-
negative_prime_modulus.slice(NUM_LIMB_BITS * 3, NUM_LIMB_BITS * 4).lo,
335+
static constexpr bb::fr negative_prime_modulus_mod_native_basis = -bb::fr(uint256_t(target_basis.modulus));
336+
static constexpr uint512_t negative_prime_modulus_mod_binary_basis = binary_basis.modulus - target_basis.modulus;
337+
static constexpr std::array<uint256_t, NUM_LIMBS> neg_modulus_mod_binary_basis_limbs_u256{
338+
negative_prime_modulus_mod_binary_basis.slice(0, NUM_LIMB_BITS).lo,
339+
negative_prime_modulus_mod_binary_basis.slice(NUM_LIMB_BITS, NUM_LIMB_BITS * 2).lo,
340+
negative_prime_modulus_mod_binary_basis.slice(NUM_LIMB_BITS * 2, NUM_LIMB_BITS * 3).lo,
341+
negative_prime_modulus_mod_binary_basis.slice(NUM_LIMB_BITS * 3, NUM_LIMB_BITS * 4).lo,
346342
};
347-
static constexpr std::array<bb::fr, NUM_LIMBS> neg_modulus_limbs{
348-
bb::fr(negative_prime_modulus.slice(0, NUM_LIMB_BITS).lo),
349-
bb::fr(negative_prime_modulus.slice(NUM_LIMB_BITS, NUM_LIMB_BITS * 2).lo),
350-
bb::fr(negative_prime_modulus.slice(NUM_LIMB_BITS * 2, NUM_LIMB_BITS * 3).lo),
351-
bb::fr(negative_prime_modulus.slice(NUM_LIMB_BITS * 3, NUM_LIMB_BITS * 4).lo),
343+
static constexpr std::array<bb::fr, NUM_LIMBS> neg_modulus_mod_binary_basis_limbs{
344+
bb::fr(negative_prime_modulus_mod_binary_basis.slice(0, NUM_LIMB_BITS).lo),
345+
bb::fr(negative_prime_modulus_mod_binary_basis.slice(NUM_LIMB_BITS, NUM_LIMB_BITS * 2).lo),
346+
bb::fr(negative_prime_modulus_mod_binary_basis.slice(NUM_LIMB_BITS * 2, NUM_LIMB_BITS * 3).lo),
347+
bb::fr(negative_prime_modulus_mod_binary_basis.slice(NUM_LIMB_BITS * 3, NUM_LIMB_BITS * 4).lo),
352348
};
353349

354350
/**
@@ -568,7 +564,7 @@ template <typename Builder, typename T> class bigfield {
568564
const std::vector<bigfield>& mul_right,
569565
const bigfield& divisor,
570566
const std::vector<bigfield>& to_sub,
571-
bool enable_divisor_nz_check = false);
567+
bool enable_divisor_nz_check = true);
572568

573569
static bigfield sum(const std::vector<bigfield>& terms);
574570
static bigfield internal_div(const std::vector<bigfield>& numerators,
@@ -770,7 +766,7 @@ template <typename Builder, typename T> class bigfield {
770766
//
771767
// a * b = q * p + r
772768
//
773-
// where p is the quotient, r is the remainder, and p is the size of the non-native field.
769+
// where q is the quotient, r is the remainder, and p is the size of the non-native field.
774770
// The CRT requires that we check that the equation:
775771
// (a) holds modulo the size of the native field n,
776772
// (b) holds modulo the size of the bigger ring 2^t,
@@ -787,7 +783,7 @@ template <typename Builder, typename T> class bigfield {
787783
// Note: We use a further safer bound of 2^((t + m - 1) / 2). We use -1 to stay safer,
788784
// because it provides additional space to avoid the overflow, but get_msb() by itself should be enough.
789785
uint64_t maximum_product_bits = maximum_product.get_msb() - 1;
790-
return (uint512_t(1) << (maximum_product_bits >> 1));
786+
return (uint512_t(1) << (maximum_product_bits >> 1)) - uint512_t(1);
791787
}
792788

793789
// If we encounter this maximum value of a bigfield we stop execution
@@ -915,24 +911,27 @@ template <typename Builder, typename T> class bigfield {
915911
// = 2^k * (3 * 2^2Q) + 2^k * 2^L * (4 * 2^2Q)
916912
// < 2^k * (2^L + 1) * (4 * 2^2Q)
917913
// < n
918-
// ==> 2^k * 2^L * 2^(2Q + 2) < n
919-
// ==> 2Q + 2 < (log(n) - k - L)
920-
// ==> Q < ((log(n) - k - L) - 2) / 2
914+
// ==> 2^k * 2^L * 2^(2Q + 3) < n
915+
// ==> 2Q + 3 < (log(n) - k - L)
916+
// ==> Q < ((log(n) - k - L) - 3) / 2
921917
//
922918
static constexpr uint64_t MAXIMUM_LIMB_SIZE_THAT_WOULDNT_OVERFLOW =
923-
(bb::fr::modulus.get_msb() - MAX_ADDITION_LOG - NUM_LIMB_BITS - 2) / 2;
919+
(bb::fr::modulus.get_msb() - MAX_ADDITION_LOG - NUM_LIMB_BITS - 3) / 2;
924920

925921
// If the logarithm of the maximum value of a limb is more than this, we need to reduce.
926922
// We allow an element to be added to itself 10 times, so we allow the limb to grow by 10 bits.
927923
// Number 10 is arbitrary, there's no actual usecase for adding 1024 elements together.
928-
static constexpr uint64_t MAX_UNREDUCED_LIMB_BITS = NUM_LIMB_BITS + 10;
924+
static constexpr uint64_t MAX_UNREDUCED_LIMB_BITS = NUM_LIMB_BITS + MAX_ADDITION_LOG;
929925

930926
// If we reach this size of a limb, we stop execution (as safety measure). This should never reach during addition
931927
// as we would reduce the limbs before they reach this size.
932928
static constexpr uint64_t PROHIBITED_LIMB_BITS = MAX_UNREDUCED_LIMB_BITS + 5;
933929

934930
// If we encounter this maximum value of a bigfield we need to reduce it.
935-
static constexpr uint256_t get_maximum_unreduced_limb_value() { return uint256_t(1) << MAX_UNREDUCED_LIMB_BITS; }
931+
static constexpr uint256_t get_maximum_unreduced_limb_value()
932+
{
933+
return ((uint256_t(1) << MAX_UNREDUCED_LIMB_BITS) - uint256_t(1));
934+
}
936935

937936
// If we encounter this maximum value of a limb we stop execution
938937
static constexpr uint256_t get_prohibited_limb_value() { return uint256_t(1) << PROHIBITED_LIMB_BITS; }

barretenberg/cpp/src/barretenberg/stdlib/primitives/bigfield/bigfield_edge_cases.test.cpp

Lines changed: 97 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ template <typename BigField> class stdlib_bigfield_edge_cases : public testing::
8080
uint512_t(fq_ct::modulus), // p
8181
};
8282

83-
inline static const std::array<uint512_t, 9> values_larger_than_bigfield = {
83+
inline static const std::array<uint512_t, 10> values_larger_than_bigfield = {
8484
uint512_t(fq_ct::modulus) + uint512_t(1),
8585
uint512_t(fq_ct::modulus) + uint512_t(fr::modulus),
8686
(uint512_t(1) << 256) - 1,
@@ -89,6 +89,7 @@ template <typename BigField> class stdlib_bigfield_edge_cases : public testing::
8989
uint512_t(fq_ct::get_maximum_unreduced_value()) - 1,
9090
uint512_t(fq_ct::get_maximum_unreduced_value()),
9191
uint512_t(fq_ct::get_maximum_unreduced_value()) + 1,
92+
uint512_t(fq_ct::get_maximum_unreduced_value()) + 2,
9293
(uint512_t(1) << (stdlib::NUM_LIMB_BITS_IN_FIELD_SIMULATION * 4)) - 1,
9394
};
9495

@@ -126,16 +127,16 @@ template <typename BigField> class stdlib_bigfield_edge_cases : public testing::
126127
fq_ct combined_a = fq_ct::unsafe_construct_from_limbs(limb_0, limb_1, limb_2, limb_3, true);
127128
combined_a.binary_basis_limbs[3].maximum_value = other_mask;
128129

129-
// Check that individual limbs are less than maximum unreduced limb value
130-
const bool limbs_less_than_max = (limb_0_native < fq_ct::get_maximum_unreduced_limb_value()) &&
131-
(limb_1_native < fq_ct::get_maximum_unreduced_limb_value()) &&
132-
(limb_2_native < fq_ct::get_maximum_unreduced_limb_value()) &&
133-
(limb_3_native < fq_ct::get_maximum_unreduced_limb_value());
130+
// Check that individual limbs are than maximum unreduced limb value
131+
const bool limbs_less_than_max = (limb_0_native <= fq_ct::get_maximum_unreduced_limb_value()) &&
132+
(limb_1_native <= fq_ct::get_maximum_unreduced_limb_value()) &&
133+
(limb_2_native <= fq_ct::get_maximum_unreduced_limb_value()) &&
134+
(limb_3_native <= fq_ct::get_maximum_unreduced_limb_value());
134135
EXPECT_EQ(limbs_less_than_max, true);
135136

136137
// Check that the combined max value is greater than the maximum unreduced bigfield value
137138
EXPECT_GT(combined_a.get_maximum_value(), // 2^68 * 2^68 * 2^68 * 2^61 = 2^265
138-
fq_ct::get_maximum_unreduced_value() // sqrt(2^272 * |Fr|) ≈ 2^(263) or 2^(264)
139+
fq_ct::get_maximum_unreduced_value() // sqrt(2^272 * |Fr|) ≈ (2^(263) - 1) or (2^(264) - 1)
139140
);
140141

141142
// Squaring op must perform self-reduction
@@ -144,7 +145,7 @@ template <typename BigField> class stdlib_bigfield_edge_cases : public testing::
144145

145146
// Check that original combined value is now reduced
146147
EXPECT_EQ(combined_a.get_value() < reduction_upper_bound, true); // reduced value < 2^s
147-
EXPECT_EQ(combined_a.get_maximum_value() < fq_ct::get_maximum_unreduced_value(), true);
148+
EXPECT_EQ(combined_a.get_maximum_value() <= fq_ct::get_maximum_unreduced_value(), true);
148149

149150
bool result = CircuitChecker::check(builder);
150151
EXPECT_EQ(result, true);
@@ -176,14 +177,14 @@ template <typename BigField> class stdlib_bigfield_edge_cases : public testing::
176177
(uint256_t(1) << fq_ct::MAX_UNREDUCED_LIMB_BITS) + 1000; // > 2^78
177178

178179
// Check that the combined max value is less than the maximum unreduced bigfield value
179-
EXPECT_EQ(combined_a.get_maximum_value() < fq_ct::get_maximum_unreduced_value(), true);
180+
EXPECT_EQ(combined_a.get_maximum_value() <= fq_ct::get_maximum_unreduced_value(), true);
180181

181182
// Perform a squaring which should trigger reduction
182183
combined_a.sqr();
183184

184185
// Check that the original combined value is now reduced
185186
EXPECT_EQ(combined_a.get_value() < reduction_upper_bound, true);
186-
EXPECT_EQ(combined_a.get_maximum_value() < fq_ct::get_maximum_unreduced_value(), true);
187+
EXPECT_EQ(combined_a.get_maximum_value() <= fq_ct::get_maximum_unreduced_value(), true);
187188

188189
// Check the circuit is valid
189190
bool result = CircuitChecker::check(builder);
@@ -444,6 +445,87 @@ template <typename BigField> class stdlib_bigfield_edge_cases : public testing::
444445
bool result = CircuitChecker::check(builder);
445446
EXPECT_EQ(result, true);
446447
}
448+
449+
static void test_divide_by_zero_fails()
450+
{
451+
Builder builder = Builder();
452+
{
453+
// numerator and denominator both are witnesses
454+
auto [a_native, a_ct] = get_random_witness(&builder, false);
455+
fq_ct zero = fq_ct::create_from_u512_as_witness(&builder, uint512_t(0), true);
456+
fq_ct zero_modulus = fq_ct::create_from_u512_as_witness(&builder, uint512_t(fq_ct::modulus), true);
457+
458+
// Division by zero should trigger an assertion failure
459+
fq_ct output = a_ct / zero;
460+
fq_ct output_modulus = a_ct / zero_modulus;
461+
462+
// outputs are irrelevant
463+
EXPECT_EQ(output.get_value(), 0);
464+
EXPECT_EQ(output_modulus.get_value(), 0);
465+
466+
bool result = CircuitChecker::check(builder);
467+
EXPECT_EQ(result, false);
468+
EXPECT_EQ(builder.err(), "bigfield: prime limb identity failed");
469+
}
470+
{
471+
// numerator is constant, denominator is witness
472+
fq_native a_native = fq_native::random_element();
473+
fq_ct a_ct(&builder, uint256_t(a_native));
474+
fq_ct zero = fq_ct::create_from_u512_as_witness(&builder, uint512_t(0), true);
475+
476+
// Division by zero should trigger an assertion failure
477+
fq_ct output = a_ct / zero;
478+
479+
// outputs are irrelevant
480+
EXPECT_EQ(output.get_value(), 0);
481+
482+
bool result = CircuitChecker::check(builder);
483+
EXPECT_EQ(result, false);
484+
EXPECT_EQ(builder.err(), "bigfield: prime limb identity failed");
485+
}
486+
{
487+
// numerator is empty, denominator is witness
488+
fq_ct zero = fq_ct::create_from_u512_as_witness(&builder, uint512_t(0), true);
489+
490+
// Division by zero should trigger an assertion failure
491+
fq_ct output = fq_ct::div_check_denominator_nonzero({}, zero);
492+
493+
// outputs are irrelevant
494+
EXPECT_EQ(output.get_value(), 0);
495+
496+
bool result = CircuitChecker::check(builder);
497+
EXPECT_EQ(result, false);
498+
EXPECT_EQ(builder.err(), "bigfield: prime limb diff is zero, but expected non-zero");
499+
}
500+
{
501+
// numerator is witness, denominator is constant
502+
[[maybe_unused]] auto [a_native, a_ct] = get_random_witness(&builder, false);
503+
fq_ct constant_zero = fq_ct(&builder, uint256_t(0));
504+
505+
#ifndef NDEBUG
506+
// In debug mode, we should hit an assertion failure
507+
EXPECT_THROW_OR_ABORT(a_ct / constant_zero, "bigfield: prime limb diff is zero, but expected non-zero");
508+
#endif
509+
}
510+
{
511+
// numerator and denominator both are constant
512+
fq_native a_native = fq_native::random_element();
513+
fq_ct a_ct(&builder, uint256_t(a_native));
514+
515+
#ifndef NDEBUG
516+
fq_ct constant_zero = fq_ct(&builder, uint256_t(0));
517+
EXPECT_THROW_OR_ABORT(a_ct / constant_zero, "bigfield: division by zero in constant division");
518+
#endif
519+
}
520+
{
521+
// numerator is empty, denominator is constant
522+
#ifndef NDEBUG
523+
fq_ct constant_zero = fq_ct(&builder, uint256_t(0));
524+
EXPECT_THROW_OR_ABORT(fq_ct::div_check_denominator_nonzero({}, constant_zero),
525+
"bigfield: prime limb diff is zero, but expected non-zero");
526+
#endif
527+
}
528+
}
447529
};
448530

449531
// Define types for which the above tests will be constructed.
@@ -512,3 +594,8 @@ TYPED_TEST(stdlib_bigfield_edge_cases, assert_equal_edge_case)
512594
{
513595
TestFixture::test_assert_equal_edge_case();
514596
}
597+
598+
TYPED_TEST(stdlib_bigfield_edge_cases, divide_by_zero_fails)
599+
{
600+
TestFixture::test_divide_by_zero_fails();
601+
}

0 commit comments

Comments
 (0)