From 0cf4bdc853d864fd4cf73d5af7e261ee2515c0d0 Mon Sep 17 00:00:00 2001 From: Lucas Xia Date: Fri, 29 Sep 2023 12:16:49 -0400 Subject: [PATCH] docs: fixed original minus underflow test (#2472) Motivated by AztecProtocol/barretenberg#669. This PR updates the safe_uint tests, and adds more documentation to the tests as well as the subtract and operator- class methods. In more detail: - it fixes TestMinusOperatorUnderflowFails (now named TestMinusUnderflowGeneral1) to check for a failing constraint through `EXPECT_FALSE(composer.check_circuit())` instead of executing without checking anything. - it adds new tests to test edge cases and different failure branches where overflow/underflow occur in the operator+, operator*, subtract(), and operator- methods. - it adds detailed comments to the subtract() and operator- functions, and the corresponding tests. - it moves the subtract, operator-, divide, and operator/ implementations from the header file to the source file. --------- Co-authored-by: maramihali --- .../stdlib/primitives/safe_uint/safe_uint.cpp | 144 ++++++++++++ .../stdlib/primitives/safe_uint/safe_uint.hpp | 79 +------ .../primitives/safe_uint/safe_uint.test.cpp | 216 +++++++++++++++--- 3 files changed, 330 insertions(+), 109 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/stdlib/primitives/safe_uint/safe_uint.cpp b/barretenberg/cpp/src/barretenberg/stdlib/primitives/safe_uint/safe_uint.cpp index da119ef1712..3ff394271d9 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/primitives/safe_uint/safe_uint.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/primitives/safe_uint/safe_uint.cpp @@ -21,6 +21,150 @@ template safe_uint_t safe_uint_t::operator* return safe_uint_t((value * other.value), new_max.lo, IS_UNSAFE); } +/** + * @brief Subtraction when you have a pre-determined bound on the difference size + * @details Same as operator- except with this pre-determined bound `difference_bit_size`. + * + * @tparam Builder + * @param other + * @param difference_bit_size + * @param description + * @return safe_uint_t + */ +template +safe_uint_t safe_uint_t::subtract(const safe_uint_t& other, + const size_t difference_bit_size, + std::string const& description) const +{ + ASSERT(difference_bit_size <= MAX_BIT_NUM); + ASSERT(!(this->value.is_constant() && other.value.is_constant())); + field_ct difference_val = this->value - other.value; + // Creates the range constraint that difference_val is in [0, (1< difference(difference_val, difference_bit_size, format("subtract: ", description)); + // It is possible for underflow to happen and the range constraint to not catch it. + // This is when (a - b) + modulus <= (1<= modulus will ensure that underflow is caught in all + // cases + if (difference.current_max + other.current_max > MAX_VALUE) + throw_or_abort("maximum value exceeded in safe_uint subtract"); + return difference; +} + +/** + * @brief Subtraction on two safe_uint_t objects + * @details The function first checks the case when both operands are constants and there is underflow. + * Then, it computes the difference and create a safe_uint_t from its value + * with the same max value as `current_max`. + * Constructing the `difference` safe_uint_t will create a range constraint, + * which catches underflow as long as the difference value does not end up in the range [0, + * current_max]. The only case where it is possible that the difference value can end up in this range, is when + * `current_max` + `other.current_max` exceeds MAX_VALUE (the modulus - 1), so we throw an error in this case. + * + * @tparam Builder + * @param other + * @return safe_uint_t + */ +template safe_uint_t safe_uint_t::operator-(const safe_uint_t& other) const +{ + // If both are constants and the operation is an underflow, throw an error since circuit itself underflows + ASSERT(!(this->value.is_constant() && other.value.is_constant() && + static_cast(value.get_value()) < static_cast(other.value.get_value()))); + field_ct difference_val = this->value - other.value; + + // safe_uint_t constructor creates a range constraint which checks that `difference_val` is within [0, + // current_max]. + safe_uint_t difference(difference_val, (size_t)(current_max.get_msb() + 1), "- operator"); + + // Call the two operands a and b. If this operations is underflow and the range constraint fails to catch it, + // this means that (a-b) + modulus is IN the range [0, a.current_max]. + // This is equivalent to the condition that (a - b) + modulus <= a.current_max. + // IF b.current_max >= modulus - a.current_max, then it is possible for this condition to be true + // because we can let a be 0, and b be b.current_max -> (0 - b.current_max) + modulus <= a.current_max is true. + // IF b.current_max < modulus - a.current_max, it is impossible for underflow to happen, no matter how you set a and + // b. Therefore, we check that b.current_max >= modulus - a.current_max, which is equivalent to + // difference.current_max + other.current_max > MAX_VALUE Note that we will throw an error sometimes even if a-b is + // not an underflow but we cannot distinguish it from a case that underflows, so we must throw an error. + if (difference.current_max + other.current_max > MAX_VALUE) + throw_or_abort("maximum value exceeded in safe_uint minus operator"); + return difference; +} + +/** + * @brief division when you have a pre-determined bound on the sizes of the quotient and remainder + * + * @tparam Builder + * @param other + * @param quotient_bit_size + * @param remainder_bit_size + * @param description + * @param get_quotient + * @return safe_uint_t + */ +template +safe_uint_t safe_uint_t::divide( + const safe_uint_t& other, + const size_t quotient_bit_size, + const size_t remainder_bit_size, + std::string const& description, + const std::function(uint256_t, uint256_t)>& get_quotient) const +{ + ASSERT(this->value.is_constant() == false); + ASSERT(quotient_bit_size <= MAX_BIT_NUM); + ASSERT(remainder_bit_size <= MAX_BIT_NUM); + uint256_t val = this->value.get_value(); + auto [quotient_val, remainder_val] = get_quotient(val, (uint256_t)other.value.get_value()); + field_ct quotient_field(witness_t(value.context, quotient_val)); + field_ct remainder_field(witness_t(value.context, remainder_val)); + safe_uint_t quotient(quotient_field, quotient_bit_size, format("divide method quotient: ", description)); + safe_uint_t remainder( + remainder_field, remainder_bit_size, format("divide method remainder: ", description)); + + // This line implicitly checks we are not overflowing + safe_uint_t int_val = quotient * other + remainder; + + // We constrain divisor - remainder - 1 to be non-negative to ensure that remainder < divisor. + // Define remainder_plus_one to avoid multiple subtractions + const safe_uint_t remainder_plus_one = remainder + 1; + // Subtraction of safe_uint_t's imposes the desired range constraint + other - remainder_plus_one; + + this->assert_equal(int_val, "divide method quotient and/or remainder incorrect"); + + return quotient; +} + +/** + * @brief Potentially less efficient than divide function - bounds remainder and quotient by max of this + * + * @tparam Builder + * @param other + * @return safe_uint_t + */ +template safe_uint_t safe_uint_t::operator/(const safe_uint_t& other) const +{ + ASSERT(this->value.is_constant() == false); + uint256_t val = this->value.get_value(); + auto [quotient_val, remainder_val] = val.divmod((uint256_t)other.value.get_value()); + field_ct quotient_field(witness_t(value.context, quotient_val)); + field_ct remainder_field(witness_t(value.context, remainder_val)); + safe_uint_t quotient(quotient_field, (size_t)(current_max.get_msb() + 1), format("/ operator quotient")); + safe_uint_t remainder( + remainder_field, (size_t)(other.current_max.get_msb() + 1), format("/ operator remainder")); + + // This line implicitly checks we are not overflowing + safe_uint_t int_val = quotient * other + remainder; + + // We constrain divisor - remainder - 1 to be non-negative to ensure that remainder < divisor. + // // define remainder_plus_one to avoid multiple subtractions + const safe_uint_t remainder_plus_one = remainder + 1; + // // subtraction of safe_uint_t's imposes the desired range constraint + other - remainder_plus_one; + + this->assert_equal(int_val, "/ operator quotient and/or remainder incorrect"); + + return quotient; +} + template safe_uint_t safe_uint_t::normalize() const { auto norm_value = value.normalize(); diff --git a/barretenberg/cpp/src/barretenberg/stdlib/primitives/safe_uint/safe_uint.hpp b/barretenberg/cpp/src/barretenberg/stdlib/primitives/safe_uint/safe_uint.hpp index b022892b62b..6f5d225293b 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/primitives/safe_uint/safe_uint.hpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/primitives/safe_uint/safe_uint.hpp @@ -98,30 +98,9 @@ template class safe_uint_t { // Subtraction when you have a pre-determined bound on the difference size safe_uint_t subtract(const safe_uint_t& other, const size_t difference_bit_size, - std::string const& description = "") const - { - ASSERT(difference_bit_size <= MAX_BIT_NUM); - ASSERT(!(this->value.is_constant() && other.value.is_constant())); - field_ct difference_val = this->value - other.value; - safe_uint_t difference(difference_val, difference_bit_size, format("subtract: ", description)); - // This checks the subtraction is correct for integers without any wraps - if (difference.current_max + other.current_max > MAX_VALUE) - throw_or_abort("maximum value exceeded in safe_uint subtract"); - return difference; - } + std::string const& description = "") const; - safe_uint_t operator-(const safe_uint_t& other) const - { - // We could get a constant underflow - ASSERT(!(this->value.is_constant() && other.value.is_constant() && - static_cast(value.get_value()) < static_cast(other.value.get_value()))); - field_ct difference_val = this->value - other.value; - safe_uint_t difference(difference_val, (size_t)(current_max.get_msb() + 1), "- operator"); - // This checks the subtraction is correct for integers without any wraps - if (difference.current_max + other.current_max > MAX_VALUE) - throw_or_abort("maximum value exceeded in safe_uint minus operator"); - return difference; - } + safe_uint_t operator-(const safe_uint_t& other) const; // division when you have a pre-determined bound on the sizes of the quotient and remainder safe_uint_t divide( @@ -132,60 +111,10 @@ template class safe_uint_t { const std::function(uint256_t, uint256_t)>& get_quotient = [](uint256_t val, uint256_t divisor) { return std::make_pair((uint256_t)(val / (uint256_t)divisor), (uint256_t)(val % (uint256_t)divisor)); - }) - { - ASSERT(this->value.is_constant() == false); - ASSERT(quotient_bit_size <= MAX_BIT_NUM); - ASSERT(remainder_bit_size <= MAX_BIT_NUM); - uint256_t val = this->value.get_value(); - auto [quotient_val, remainder_val] = get_quotient(val, (uint256_t)other.value.get_value()); - field_ct quotient_field(witness_t(value.context, quotient_val)); - field_ct remainder_field(witness_t(value.context, remainder_val)); - safe_uint_t quotient( - quotient_field, quotient_bit_size, format("divide method quotient: ", description)); - safe_uint_t remainder( - remainder_field, remainder_bit_size, format("divide method remainder: ", description)); - - // This line implicitly checks we are not overflowing - safe_uint_t int_val = quotient * other + remainder; - - // We constrain divisor - remainder - 1 to be non-negative to ensure that remainder < divisor. - // Define remainder_plus_one to avoid multiple subtractions - const safe_uint_t remainder_plus_one = remainder + 1; - // Subtraction of safe_uint_t's imposes the desired range constraint - other - remainder_plus_one; - - this->assert_equal(int_val, "divide method quotient and/or remainder incorrect"); - - return quotient; - } + }) const; // Potentially less efficient than divide function - bounds remainder and quotient by max of this - safe_uint_t operator/(const safe_uint_t& other) const - { - ASSERT(this->value.is_constant() == false); - uint256_t val = this->value.get_value(); - auto [quotient_val, remainder_val] = val.divmod((uint256_t)other.value.get_value()); - field_ct quotient_field(witness_t(value.context, quotient_val)); - field_ct remainder_field(witness_t(value.context, remainder_val)); - safe_uint_t quotient( - quotient_field, (size_t)(current_max.get_msb() + 1), format("/ operator quotient")); - safe_uint_t remainder( - remainder_field, (size_t)(other.current_max.get_msb() + 1), format("/ operator remainder")); - - // This line implicitly checks we are not overflowing - safe_uint_t int_val = quotient * other + remainder; - - // We constrain divisor - remainder - 1 to be non-negative to ensure that remainder < divisor. - // // define remainder_plus_one to avoid multiple subtractions - const safe_uint_t remainder_plus_one = remainder + 1; - // // subtraction of safe_uint_t's imposes the desired range constraint - other - remainder_plus_one; - - this->assert_equal(int_val, "/ operator quotient and/or remainder incorrect"); - - return quotient; - } + safe_uint_t operator/(const safe_uint_t& other) const; safe_uint_t add_two(const safe_uint_t& add_a, const safe_uint_t& add_b) const { diff --git a/barretenberg/cpp/src/barretenberg/stdlib/primitives/safe_uint/safe_uint.test.cpp b/barretenberg/cpp/src/barretenberg/stdlib/primitives/safe_uint/safe_uint.test.cpp index 33a8a374375..cf54c1182f8 100644 --- a/barretenberg/cpp/src/barretenberg/stdlib/primitives/safe_uint/safe_uint.test.cpp +++ b/barretenberg/cpp/src/barretenberg/stdlib/primitives/safe_uint/safe_uint.test.cpp @@ -61,6 +61,9 @@ TYPED_TEST(SafeUintTest, TestConstructorWithValueInRange) // * OPERATOR +/** + * @brief Test that we overflow correctly on the border of 3**160 and 3**161. + */ #if !defined(__wasm__) TYPED_TEST(SafeUintTest, TestMultiplyOperationOutOfRangeFails) { @@ -69,22 +72,29 @@ TYPED_TEST(SafeUintTest, TestMultiplyOperationOutOfRangeFails) // Since max is initally set to (1 << 2) - 1 = 3 (as bit range checks are easier than generic integer bounds), // should allow largest power of 3 smaller than r iterations, which is 159. Hence below we should exceed r, and // expect a throw + field_ct a(witness_ct(&builder, 2)); + suint_ct c(a, 2); + suint_ct d(a, 2); + // should not fail on 159 iterations, since 3**160 < r < 3**161 + for (auto i = 0; i < 159; i++) { + c = c * d; + } + EXPECT_TRUE(builder.check_circuit()); try { - - field_ct a(witness_ct(&builder, 2)); - suint_ct c(a, 2); - suint_ct d(a, 2); - for (auto i = 0; i < 160; i++) { - c = c * d; - } + // should throw an overflow error on the 160th iteration + c = c * d; FAIL() << "Expected out of range error"; } catch (std::runtime_error const& err) { + EXPECT_TRUE(builder.check_circuit()); // no failing constraints should be created from multiply EXPECT_EQ(err.what(), std::string("exceeded modulus in safe_uint class")); } catch (...) { FAIL() << "Expected std::runtime_error modulus in safe_uint class"; } } +/** + * @brief Test that we correctly overflow multiplying by a constant on the border of 2**253 and 2**254. + */ TYPED_TEST(SafeUintTest, TestMultiplyOperationOnConstantsOutOfRangeFails) { STDLIB_TYPE_ALIASES @@ -96,21 +106,19 @@ TYPED_TEST(SafeUintTest, TestMultiplyOperationOnConstantsOutOfRangeFails) suint_ct c(a, 2); suint_ct d(fr(2)); + // should not fail on 252 iterations for (auto i = 0; i < 252; i++) { c = c * d; } + EXPECT_TRUE(builder.check_circuit()); // Below we should exceed r, and expect a throw try { - - field_ct a(witness_ct(&builder, 2)); - suint_ct c(a, 2); - suint_ct d(fr(2)); - for (auto i = 0; i < 253; i++) { - c = c * d; - } + // should fail on the 253rd iteration + c = c * d; FAIL() << "Expected out of range error"; } catch (std::runtime_error const& err) { + EXPECT_TRUE(builder.check_circuit()); // no failing constraint from multiply EXPECT_EQ(err.what(), std::string("exceeded modulus in safe_uint class")); } catch (...) { FAIL() << "Expected std::runtime_error modulus in safe_uint class"; @@ -118,31 +126,41 @@ TYPED_TEST(SafeUintTest, TestMultiplyOperationOnConstantsOutOfRangeFails) } // + OPERATOR +/** + * @brief Test that we correctly overflow on addition on the border of 3**160 and 2 * 3**160. + */ TYPED_TEST(SafeUintTest, TestAddOperationOutOfRangeFails) { STDLIB_TYPE_ALIASES auto builder = Builder(); // Here we test the addition operator also causes a throw when exceeding r + field_ct a(witness_ct(&builder, 2)); + suint_ct c(a, 2); + suint_ct d(a, 2); + // should not fail on the initial setup + for (auto i = 0; i < 159; i++) { + c = c * d; + } + EXPECT_TRUE(builder.check_circuit()); try { - - field_ct a(witness_ct(&builder, 2)); - suint_ct c(a, 2); - suint_ct d(a, 2); - for (auto i = 0; i < 159; i++) { - c = c * d; - } - c = c + c + c; + // should fail when we add and exceed the modulus + c = c + c; FAIL() << "Expected out of range error"; } catch (std::runtime_error const& err) { + EXPECT_TRUE(builder.check_circuit()); // no failing constraints from add or multiply EXPECT_EQ(err.what(), std::string("exceeded modulus in safe_uint class")); } catch (...) { FAIL() << "Expected std::runtime_error modulus in safe_uint class"; } } #endif + // SUBTRACT METHOD -TYPED_TEST(SafeUintTest, TestSubtractMethod) +/** + * @brief Test that we can subtract without underflow successfully. + */ +TYPED_TEST(SafeUintTest, TestSubtract) { STDLIB_TYPE_ALIASES auto builder = Builder(); @@ -151,12 +169,16 @@ TYPED_TEST(SafeUintTest, TestSubtractMethod) field_ct b(witness_ct(&builder, 9)); suint_ct c(a, 2); suint_ct d(b, 4); - c = d.subtract(c, 3); + c = d.subtract(c, 3); // result is 7, which fits in 3 bits and does not fail the range constraint EXPECT_TRUE(builder.check_circuit()); } -TYPED_TEST(SafeUintTest, TestSubtractMethodMinuedGtLhsFails) +/** + * @brief Test that range constraint fails if the value exceeds the bit limit. + * @details difference is 7, which exceeds 2 bits, and causes the circuit to fail. + */ +TYPED_TEST(SafeUintTest, TestSubtractResultOutOfRange) { STDLIB_TYPE_ALIASES auto builder = Builder(); @@ -171,8 +193,32 @@ TYPED_TEST(SafeUintTest, TestSubtractMethodMinuedGtLhsFails) EXPECT_FALSE(builder.check_circuit()); } +/** + * @brief Test that underflow is caught in general case. + * @details General case refers to when difference.current_max + other.current_max does not exceed MAX_VALUE + * and underflow is caught by range constraint. + */ +#if !defined(__wasm__) +TYPED_TEST(SafeUintTest, TestSubtractUnderflowGeneral) +{ + STDLIB_TYPE_ALIASES + auto builder = Builder(); + + field_ct a(witness_ct(&builder, 0)); + field_ct b(witness_ct(&builder, 1)); + suint_ct c(a, 0); + suint_ct d(b, 1); + c = c.subtract(d, suint_ct::MAX_BIT_NUM); + EXPECT_FALSE(builder.check_circuit()); +} +#endif + +/** + * @brief Test that underflow is caught in the special case. + * @details Should throw an error because difference.current_max + other.current_max exceeds the MAX_VALUE. + */ #if !defined(__wasm__) -TYPED_TEST(SafeUintTest, TestSubtractMethodUnderflowFails) +TYPED_TEST(SafeUintTest, TestSubtractUnderflowSpecial) { STDLIB_TYPE_ALIASES auto builder = Builder(); @@ -185,30 +231,57 @@ TYPED_TEST(SafeUintTest, TestSubtractMethodUnderflowFails) c = c.subtract(d, suint_ct::MAX_BIT_NUM); FAIL() << "Expected out of range error"; } catch (std::runtime_error const& err) { + EXPECT_TRUE(builder.check_circuit()); EXPECT_EQ(err.what(), std::string("maximum value exceeded in safe_uint subtract")); } catch (...) { FAIL() << "Expected std::runtime_error modulus in safe_uint class"; } } #endif + // - OPERATOR +/** + * @brief Test that valid minus operation works. + */ TYPED_TEST(SafeUintTest, TestMinusOperator) { STDLIB_TYPE_ALIASES auto builder = Builder(); + field_ct a(witness_ct(&builder, 9)); + field_ct b(witness_ct(&builder, 2)); + suint_ct c(a, 4); + suint_ct d(b, 2); + c = c - d; // 9 - 2 = 7 should not underflow + + EXPECT_TRUE(builder.check_circuit()); +} + +/** + * @brief Test that valid minus operation works on 0. + */ +#if !defined(__wasm__) +TYPED_TEST(SafeUintTest, TestMinusOperatorValidOnZero) +{ + STDLIB_TYPE_ALIASES + auto builder = Builder(); + field_ct a(witness_ct(&builder, 2)); - field_ct b(witness_ct(&builder, 9)); + field_ct b(witness_ct(&builder, 2)); suint_ct c(a, 2); - suint_ct d(b, 4); - c = d - c; - + suint_ct d(b, 3); + c = c - d; // 2 - 2 = 0 should not overflow, even if d has more bits than c. EXPECT_TRUE(builder.check_circuit()); } +#endif +/** + * @brief Test that checks that minus operator underflow is caught in the general case. + * @details General case means that the special case does not happen. + */ #if !defined(__wasm__) -TYPED_TEST(SafeUintTest, TestMinusOperatorUnderflowFails) +TYPED_TEST(SafeUintTest, TestMinusUnderflowGeneral1) { STDLIB_TYPE_ALIASES auto builder = Builder(); @@ -217,12 +290,87 @@ TYPED_TEST(SafeUintTest, TestMinusOperatorUnderflowFails) field_ct b(witness_ct(&builder, field_ct::modulus / 2)); suint_ct c(a, 2); suint_ct d(b, suint_ct::MAX_BIT_NUM); + c = c - d; // generates range constraint that the difference is in [0, 3], which it is not with these witness values + EXPECT_FALSE(builder.check_circuit()); +} +#endif + +/** + * @brief Test that checks that minus operator underflow is caught in the general case. + * @details Testing -1 is an underflow. + */ +#if !defined(__wasm__) +TYPED_TEST(SafeUintTest, TestMinusUnderflowGeneral2) +{ + STDLIB_TYPE_ALIASES + auto builder = Builder(); + + field_ct a(witness_ct(&builder, 2)); + field_ct b(witness_ct(&builder, 3)); + suint_ct c(a, 2); + suint_ct d(b, 3); + c = c - d; + EXPECT_FALSE(builder.check_circuit()); // underflow should cause range constraint to fail +} +#endif + +/** + * @brief Test that checks that minus operator underflow is caught from special case. + * @details Special case refers to the check that current_max + other.current_max > MAX_VALUE, which is a potential + * underflow case, that escapes the general check through the range constraint. Throws an error even if it is not an + * underflow in some instantiations of the witness values. + */ +#if !defined(__wasm__) +TYPED_TEST(SafeUintTest, TestMinusUnderflowSpecial1) +{ + STDLIB_TYPE_ALIASES + auto builder = Builder(); + + field_ct a(witness_ct(&builder, 1)); + field_ct b(witness_ct(&builder, 0)); + suint_ct c(a, suint_ct::MAX_BIT_NUM); + suint_ct d(b, suint_ct::MAX_BIT_NUM); try { - c = c - d; + c = c - d; // even though this is not an underflow, we cannot distinguish it from an actual underflow because + // the sum of maxes exceeds MAX_VALUE so we must throw an error + FAIL() << "Expected error to be thrown"; } catch (std::runtime_error const& err) { - EXPECT_EQ(err.what(), std::string("maximum value exceeded in safe_uint minus operator")); + EXPECT_TRUE(builder.check_circuit()); // no incorrect constraints + EXPECT_EQ(err.what(), + std::string("maximum value exceeded in safe_uint minus operator")); // possible underflow is detected + // with check on maxes } catch (...) { - FAIL() << "Expected std::runtime_error modulus in safe_uint class"; + FAIL() << "Expected no error, got other error"; + } +} +#endif + +/** + * @brief Test that checks that minus operator underflow is caught from special case. + * @details Special case refers to the check that current_max + other.current_max > MAX_VALUE, which is a potential + * underflow case, that escapes the general check through the range constraint. Also, underflow can actually be detected + * from range constraint. + */ +#if !defined(__wasm__) +TYPED_TEST(SafeUintTest, TestMinusUnderflowSpecial2) +{ + STDLIB_TYPE_ALIASES + auto builder = Builder(); + + field_ct a(witness_ct(&builder, 0)); + field_ct b(witness_ct(&builder, 1)); + suint_ct c(a, suint_ct::MAX_BIT_NUM); + suint_ct d(b, suint_ct::MAX_BIT_NUM); + try { + c = c - d; // underflow and error should be thrown + FAIL() << "Expected error to be thrown"; + } catch (std::runtime_error const& err) { + EXPECT_FALSE(builder.check_circuit()); // underflow causes failing constraint + EXPECT_EQ(err.what(), + std::string("maximum value exceeded in safe_uint minus operator")); // possible underflow is detected + // with check on maxes + } catch (...) { + FAIL() << "Expected no error, got other error"; } } #endif