From fc9395574fcf5d54744cb85ee2294ca256dcaf25 Mon Sep 17 00:00:00 2001 From: Riccardo Abbate Date: Tue, 25 Aug 2026 16:01:22 -0400 Subject: [PATCH 1/5] is_square mod --- .../detail/element/fp12_2over3over2.hpp | 26 +-- .../crypto3/algebra/fields/field_order.hpp | 48 ++++ libs/algebra/test/field_order.cpp | 30 +++ libs/algebra/test/fp12.cpp | 18 +- .../polynomial/equal_degree_factorization.hpp | 7 +- .../polynomial/polynomial_square_root.hpp | 207 ++++++++++++++++++ libs/math/test/CMakeLists.txt | 1 + libs/math/test/polynomial_square_root.cpp | 156 +++++++++++++ 8 files changed, 458 insertions(+), 35 deletions(-) create mode 100644 libs/math/include/nil/crypto3/math/polynomial/polynomial_square_root.hpp create mode 100644 libs/math/test/polynomial_square_root.cpp diff --git a/libs/algebra/include/nil/crypto3/algebra/fields/detail/element/fp12_2over3over2.hpp b/libs/algebra/include/nil/crypto3/algebra/fields/detail/element/fp12_2over3over2.hpp index f3c6301a1..db72873ea 100644 --- a/libs/algebra/include/nil/crypto3/algebra/fields/detail/element/fp12_2over3over2.hpp +++ b/libs/algebra/include/nil/crypto3/algebra/fields/detail/element/fp12_2over3over2.hpp @@ -218,20 +218,12 @@ namespace nil { } using boost::multiprecision::cpp_int; - static const cpp_int order_minus_one = field_order() - 1; - static const auto parameters = []() { - cpp_int odd_part = order_minus_one; - std::size_t power_of_two = 0; - while ((odd_part & 1) == 0) { - odd_part >>= 1; - ++power_of_two; - } - return std::pair(odd_part, power_of_two); - }(); + static const auto parameters = field_multiplicative_group_decomposition(); + static const cpp_int order_minus_one = parameters.odd_order << parameters.two_adicity; - const cpp_int &odd_part = parameters.first; - const std::size_t power_of_two = parameters.second; - if (power_of_two == 1) { + const cpp_int &odd_order = parameters.odd_order; + const std::size_t two_adicity = parameters.two_adicity; + if (two_adicity == 1) { return pow((order_minus_one + 2) >> 2); } @@ -241,7 +233,7 @@ namespace nil { const element_fp12_2over3over2 value(underlying_type::zero(), underlying_type(base_type(candidate))); if (value.pow(order_minus_one >> 1) == -one()) { - return value.pow(parameters.first); + return value.pow(parameters.odd_order); } } assert(false && "failed to find an Fp12 quadratic non-residue"); @@ -249,9 +241,9 @@ namespace nil { }(); element_fp12_2over3over2 c = non_residue_to_odd_part; - element_fp12_2over3over2 t = pow(odd_part); - element_fp12_2over3over2 root = pow((odd_part + 1) >> 1); - std::size_t remaining_power = power_of_two; + element_fp12_2over3over2 t = pow(odd_order); + element_fp12_2over3over2 root = pow((odd_order + 1) >> 1); + std::size_t remaining_power = two_adicity; while (t != one()) { element_fp12_2over3over2 t_squared = t; diff --git a/libs/algebra/include/nil/crypto3/algebra/fields/field_order.hpp b/libs/algebra/include/nil/crypto3/algebra/fields/field_order.hpp index d1d1e5707..40629674d 100644 --- a/libs/algebra/include/nil/crypto3/algebra/fields/field_order.hpp +++ b/libs/algebra/include/nil/crypto3/algebra/fields/field_order.hpp @@ -26,6 +26,8 @@ #define CRYPTO3_ALGEBRA_FIELDS_FIELD_ORDER_HPP #include +#include +#include #include @@ -33,6 +35,25 @@ namespace nil::crypto3::algebra::fields { + struct multiplicative_group_decomposition { + boost::multiprecision::cpp_int odd_order; + std::size_t two_adicity = 0; + }; + + /** Decompose a positive multiplicative-group order into odd_order * 2^two_adicity. */ + inline multiplicative_group_decomposition + decompose_multiplicative_group_order(boost::multiprecision::cpp_int group_order) { + if (group_order <= 0) { + throw std::invalid_argument("a multiplicative-group order must be positive"); + } + std::size_t two_adicity = 0; + while ((group_order & 1) == 0) { + group_order >>= 1; + ++two_adicity; + } + return {std::move(group_order), two_adicity}; + } + template> struct field_type { using type = FieldOrValue; @@ -68,6 +89,33 @@ namespace nil::crypto3::algebra::fields { return order; } + /** Return the number of elements in a positive-degree extension of FieldType. */ + template + boost::multiprecision::cpp_int extension_field_order(std::size_t extension_degree) { + if (extension_degree == 0) { + throw std::invalid_argument("a field extension must have positive degree"); + } + const boost::multiprecision::cpp_int base_field_order = field_order(); + boost::multiprecision::cpp_int order = 1; + for (std::size_t i = 0; i < extension_degree; ++i) { + order *= base_field_order; + } + return order; + } + + /** Decompose the multiplicative-group order of FieldType into odd_order * 2^two_adicity. */ + template + multiplicative_group_decomposition field_multiplicative_group_decomposition() { + return decompose_multiplicative_group_order(field_order() - 1); + } + + /** Decompose the multiplicative-group order of a positive-degree extension of FieldType. */ + template + multiplicative_group_decomposition + extension_field_multiplicative_group_decomposition(std::size_t extension_degree) { + return decompose_multiplicative_group_order(extension_field_order(extension_degree) - 1); + } + } // namespace nil::crypto3::algebra::fields #endif // CRYPTO3_ALGEBRA_FIELDS_FIELD_ORDER_HPP diff --git a/libs/algebra/test/field_order.cpp b/libs/algebra/test/field_order.cpp index 8f2d04e38..56bf74b2f 100644 --- a/libs/algebra/test/field_order.cpp +++ b/libs/algebra/test/field_order.cpp @@ -71,4 +71,34 @@ BOOST_AUTO_TEST_CASE(extension_field_order_is_characteristic_to_the_extension_de BOOST_CHECK(fields::field_characteristic() == characteristic); } +BOOST_AUTO_TEST_CASE(runtime_extension_field_order_is_base_field_order_to_the_requested_degree) { + const boost::multiprecision::cpp_int fq_order = fields::field_order(); + const boost::multiprecision::cpp_int fq12_order = fields::field_order(); + + BOOST_CHECK(fields::extension_field_order(1) == fq_order); + BOOST_CHECK(fields::extension_field_order(2) == fq_order * fq_order); + BOOST_CHECK(fields::extension_field_order(2) == fq12_order * fq12_order); +} + +BOOST_AUTO_TEST_CASE(runtime_extension_field_order_rejects_degree_zero) { + BOOST_CHECK_THROW(fields::extension_field_order(0), std::invalid_argument); +} + +BOOST_AUTO_TEST_CASE(multiplicative_group_order_decomposition_separates_the_two_primary_part) { + const auto fq12_decomposition = fields::field_multiplicative_group_decomposition(); + BOOST_CHECK((fq12_decomposition.odd_order & 1) == 1); + BOOST_CHECK((fq12_decomposition.odd_order << fq12_decomposition.two_adicity) == + fields::field_order() - 1); + + const auto quadratic_decomposition = fields::extension_field_multiplicative_group_decomposition(2); + BOOST_CHECK((quadratic_decomposition.odd_order & 1) == 1); + BOOST_CHECK((quadratic_decomposition.odd_order << quadratic_decomposition.two_adicity) == + fields::extension_field_order(2) - 1); +} + +BOOST_AUTO_TEST_CASE(multiplicative_group_order_decomposition_rejects_nonpositive_orders) { + BOOST_CHECK_THROW(fields::decompose_multiplicative_group_order(0), std::invalid_argument); + BOOST_CHECK_THROW(fields::decompose_multiplicative_group_order(-1), std::invalid_argument); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/libs/algebra/test/fp12.cpp b/libs/algebra/test/fp12.cpp index 650575593..8a0d13ba7 100644 --- a/libs/algebra/test/fp12.cpp +++ b/libs/algebra/test/fp12.cpp @@ -219,12 +219,9 @@ BOOST_AUTO_TEST_CASE(bn254_subgroup_algorithms_match_direct_exponentiation) { using value_type = typename field_type::value_type; using boost::multiprecision::cpp_int; - cpp_int odd_order = fields::field_order() - 1; - std::size_t two_adicity = 0; - while ((odd_order & 1) == 0) { - odd_order >>= 1; - ++two_adicity; - } + const auto order_decomposition = fields::field_multiplicative_group_decomposition(); + const cpp_int &odd_order = order_decomposition.odd_order; + const std::size_t two_adicity = order_decomposition.two_adicity; boost::random::mt19937 rng(0x2a11); for (std::size_t i = 0; i < 2; ++i) { @@ -274,12 +271,9 @@ BOOST_AUTO_TEST_CASE(generic_subgroup_algorithms_match_direct_formulas) { using value_type = typename field_type::value_type; using boost::multiprecision::cpp_int; - cpp_int odd_order = fields::field_order() - 1; - std::size_t two_adicity = 0; - while ((odd_order & 1) == 0) { - odd_order >>= 1; - ++two_adicity; - } + const auto order_decomposition = fields::field_multiplicative_group_decomposition(); + const cpp_int &odd_order = order_decomposition.odd_order; + const std::size_t two_adicity = order_decomposition.two_adicity; boost::random::mt19937 rng(0x3812); const value_type x = random_fp12(rng); BOOST_CHECK_EQUAL(fields::two_primary_component(x, odd_order, two_adicity), x.pow(odd_order)); diff --git a/libs/math/include/nil/crypto3/math/polynomial/equal_degree_factorization.hpp b/libs/math/include/nil/crypto3/math/polynomial/equal_degree_factorization.hpp index 55fdf39c0..594353f2a 100644 --- a/libs/math/include/nil/crypto3/math/polynomial/equal_degree_factorization.hpp +++ b/libs/math/include/nil/crypto3/math/polynomial/equal_degree_factorization.hpp @@ -90,12 +90,7 @@ namespace nil::crypto3::math { "Cantor-Zassenhaus splitting for characteristic two is not implemented"); } - const boost::multiprecision::cpp_int field_order = algebra::fields::field_order(); - exponent_ = 1; - for (std::size_t i = 0; i < irreducible_factor_degree_; ++i) { - exponent_ *= field_order; - } - exponent_ = (exponent_ - 1) / 2; + exponent_ = (algebra::fields::extension_field_order(irreducible_factor_degree_) - 1) / 2; } std::size_t irreducible_factor_degree() const { diff --git a/libs/math/include/nil/crypto3/math/polynomial/polynomial_square_root.hpp b/libs/math/include/nil/crypto3/math/polynomial/polynomial_square_root.hpp new file mode 100644 index 000000000..3f708a0d6 --- /dev/null +++ b/libs/math/include/nil/crypto3/math/polynomial/polynomial_square_root.hpp @@ -0,0 +1,207 @@ +//---------------------------------------------------------------------------// +// Copyright (c) 2026 +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +//---------------------------------------------------------------------------// + +#ifndef CRYPTO3_MATH_POLYNOMIAL_SQUARE_ROOT_HPP +#define CRYPTO3_MATH_POLYNOMIAL_SQUARE_ROOT_HPP + +#include +#include +#include + +#include + +#include +#include + +#include + +namespace nil::crypto3::math { + + namespace detail { + + template + requires algebra::FieldValue + bool is_square_mod_impl(const typename Backend::polynomial_type &input, + const polynomial_divisor_context &divisor_context, + polynomial_arithmetic::polynomial_context &arithmetic_context, + const algebra::fields::multiplicative_group_decomposition *order_decomposition); + + } // namespace detail + + /** + * Immutable cache of multiplicative-order parameters for square roots in the quotient field K[X]/(B). If K has Q + * elements and d = degree(B), the shared field-order utility decomposes + * + * Q^d - 1 = odd_order * 2^two_adicity. + * + * Given a quadratic nonresidue z in that quotient field, the context also stores z^odd_order. Tonelli-Shanks reuses + * the order decomposition and this cached nonresidue power for every square root modulo B. Irreducibility is a + * caller precondition and is not tested. + * + * @throws std::invalid_argument if B is constant, K has characteristic two, or quadratic_non_residue is not a + * reduced nonsquare residue modulo B. + * @pre B is irreducible and quadratic_non_residue is a nonempty canonical coefficient polynomial. + */ + template + requires algebra::FieldValue + class polynomial_square_root_context { + public: + using polynomial_type = typename Backend::polynomial_type; + using value_type = typename polynomial_type::value_type; + using field_type = typename value_type::field_type; + + polynomial_square_root_context(const polynomial_type &quadratic_non_residue, + const polynomial_divisor_context &divisor_context, + polynomial_arithmetic::polynomial_context &arithmetic_context) { + const std::size_t extension_degree = divisor_context.degree(); + if (extension_degree == 0) { + throw std::invalid_argument("polynomial square roots require a nonconstant divisor"); + } + if (algebra::fields::field_characteristic() == 2) { + throw std::invalid_argument("polynomial square roots in characteristic two are not implemented"); + } + if (quadratic_non_residue.size() > extension_degree) { + throw std::invalid_argument("a polynomial square-root nonresidue must be reduced modulo the divisor"); + } + + order_decomposition_ = + algebra::fields::extension_field_multiplicative_group_decomposition(extension_degree); + if (detail::is_square_mod_impl(quadratic_non_residue, divisor_context, arithmetic_context, + &order_decomposition_)) { + throw std::invalid_argument("a polynomial square-root context requires a quadratic nonresidue"); + } + powmod(non_residue_to_odd_order_, quadratic_non_residue, odd_order(), divisor_context, arithmetic_context); + } + + const boost::multiprecision::cpp_int &odd_order() const { + return order_decomposition_.odd_order; + } + + std::size_t two_adicity() const { + return order_decomposition_.two_adicity; + } + + const polynomial_type &non_residue_to_odd_order() const { + return non_residue_to_odd_order_; + } + + private: + algebra::fields::multiplicative_group_decomposition order_decomposition_; + polynomial_type non_residue_to_odd_order_; + }; + + /** + * Return whether input is a square in K[X]/(B), where K is a finite field and B is the irreducible polynomial + * stored in divisor_context. If Q is the order of K and d = degree(B), the quotient is a field of order Q^d. + * For a nonzero residue a in odd characteristic, Euler's criterion gives + * + * a^((Q^d - 1) / 2) = 1 + * + * exactly when a is a square. Zero is a square. In characteristic two, squaring is an automorphism of every + * finite field, so every residue is a square. + * + * The canonical indeterminate X uses the closed-form field norm + * + * Norm(X) = (-1)^d B(0) / leading_coefficient(B). + * + * An element of a finite extension of an odd-order field is a square exactly when its norm is a square in the + * coefficient field. When the coefficient value type provides is_square(), this avoids quotient-ring + * exponentiation for X. Other inputs use Euler's criterion as a reference implementation. + * + * The irreducibility precondition is essential: for a reducible B the quotient has zero divisors and Euler's + * criterion does not characterize its squares. The function does not repeat an irreducibility test. + * + * @throws std::invalid_argument if B is constant or input is not a reduced quotient-field representative. + * @pre B is irreducible and input is a nonempty canonical coefficient polynomial. + */ + namespace detail { + + template + requires algebra::FieldValue + bool is_square_mod_impl(const typename Backend::polynomial_type &input, + const polynomial_divisor_context &divisor_context, + polynomial_arithmetic::polynomial_context &arithmetic_context, + const algebra::fields::multiplicative_group_decomposition *order_decomposition) { + using polynomial_type = typename Backend::polynomial_type; + using value_type = typename polynomial_type::value_type; + using field_type = typename value_type::field_type; + + const std::size_t extension_degree = divisor_context.degree(); + if (extension_degree == 0) { + throw std::invalid_argument("square testing modulo a polynomial requires a nonconstant divisor"); + } + if (input.size() > extension_degree) { + throw std::invalid_argument("square testing requires a reduced quotient-field representative"); + } + if (input.size() == 1 && input[0] == value_type::zero()) { + return true; + } + if (algebra::fields::field_characteristic() == 2) { + return true; + } + + // Use the norm shortcut when the coefficient field provides its own square test. Other field-value types + // continue to the generic Euler test below. + if constexpr (requires(const value_type &value) { + { value.is_square() } -> std::convertible_to; + }) { + if (input.size() == 2 && input[0] == value_type::zero() && input[1] == value_type::one()) { + const polynomial_type &divisor = divisor_context.divisor(); + // The constant coefficient of the inverse reversed divisor is the cached inverse of the leading + // coefficient of the divisor. + value_type norm = divisor[0] * divisor_context.reversed_divisor_inverse()[0]; + if (extension_degree % 2 != 0) { + norm = value_type::zero() - norm; + } + return norm.is_square(); + } + } + + boost::multiprecision::cpp_int exponent; + if (order_decomposition != nullptr) { + // In odd characteristic the two-adicity is positive, so half the group order is + // odd_order * 2^(two_adicity - 1). + exponent = order_decomposition->odd_order; + exponent <<= order_decomposition->two_adicity - 1; + } else { + exponent = (algebra::fields::extension_field_order(extension_degree) - 1) >> 1; + } + polynomial_type quadratic_character; + powmod(quadratic_character, input, exponent, divisor_context, arithmetic_context); + return quadratic_character.size() == 1 && quadratic_character[0] == value_type::one(); + } + + } // namespace detail + + template + requires algebra::FieldValue + bool is_square_mod(const typename Backend::polynomial_type &input, + const polynomial_divisor_context &divisor_context, + polynomial_arithmetic::polynomial_context &arithmetic_context) { + return detail::is_square_mod_impl(input, divisor_context, arithmetic_context, nullptr); + } + +} // namespace nil::crypto3::math + +#endif // CRYPTO3_MATH_POLYNOMIAL_SQUARE_ROOT_HPP diff --git a/libs/math/test/CMakeLists.txt b/libs/math/test/CMakeLists.txt index ed09e60b2..c10943425 100644 --- a/libs/math/test/CMakeLists.txt +++ b/libs/math/test/CMakeLists.txt @@ -53,6 +53,7 @@ set(TESTS_NAMES "polynomial_backend" "polynomial_division" "polynomial_exponentiation" + "polynomial_square_root" "polynomial_composition" "polynomial_frobenius" "square_free_factorization" diff --git a/libs/math/test/polynomial_square_root.cpp b/libs/math/test/polynomial_square_root.cpp new file mode 100644 index 000000000..0c41830e7 --- /dev/null +++ b/libs/math/test/polynomial_square_root.cpp @@ -0,0 +1,156 @@ +//---------------------------------------------------------------------------// +// Copyright (c) 2026 +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +//---------------------------------------------------------------------------// + +#define BOOST_TEST_MODULE polynomial_square_root_test + +#include +#include + +#include + +#include + +#include +#include + +namespace { + namespace math = nil::crypto3::math; + namespace fields = nil::crypto3::algebra::fields; + namespace polynomial_arithmetic = math::polynomial_arithmetic; + + using field_type = fields::babybear; + using value_type = field_type::value_type; + using backend_type = polynomial_arithmetic::schoolbook_backend; + using polynomial_type = typename backend_type::polynomial_type; + + value_type first_quadratic_non_residue() { + value_type candidate(2); + while (candidate.is_square()) { + candidate = candidate + value_type::one(); + } + return candidate; + } + + value_type first_cubic_non_residue() { + const field_type::integral_type cubic_character_exponent = (field_type::modulus - 1u) / 3u; + value_type candidate(2); + while (candidate.pow(cubic_character_exponent) == value_type::one()) { + candidate = candidate + value_type::one(); + } + return candidate; + } +} // namespace + +BOOST_AUTO_TEST_SUITE(polynomial_square_root_test_suite) + +BOOST_AUTO_TEST_CASE(square_root_context_validates_and_caches_tonelli_shanks_parameters) { + const value_type base_non_residue = first_quadratic_non_residue(); + const polynomial_type irreducible_divisor = {-base_non_residue, value_type::zero(), value_type::one()}; + // BabyBear has order 1 modulo 4, so the square root X of a base-field nonsquare remains a nonsquare in this + // quadratic extension. + const polynomial_type quotient_non_residue = {value_type::zero(), value_type::one()}; + polynomial_arithmetic::polynomial_context arithmetic_context; + math::polynomial_divisor_context divisor_context(irreducible_divisor, 1, arithmetic_context); + const math::polynomial_square_root_context square_root_context(quotient_non_residue, divisor_context, + arithmetic_context); + + const boost::multiprecision::cpp_int quotient_order = fields::extension_field_order(2); + BOOST_CHECK((square_root_context.odd_order() & 1) == 1); + BOOST_CHECK((square_root_context.odd_order() << square_root_context.two_adicity()) == quotient_order - 1); + + polynomial_type expected_non_residue_to_odd_order; + math::powmod(expected_non_residue_to_odd_order, quotient_non_residue, square_root_context.odd_order(), + divisor_context, arithmetic_context); + BOOST_CHECK(square_root_context.non_residue_to_odd_order() == expected_non_residue_to_odd_order); + + BOOST_CHECK_THROW((math::polynomial_square_root_context(polynomial_type {value_type::one()}, + divisor_context, arithmetic_context)), + std::invalid_argument); + BOOST_CHECK_THROW((math::polynomial_square_root_context( + polynomial_type {value_type::zero(), value_type::zero(), value_type::one()}, divisor_context, + arithmetic_context)), + std::invalid_argument); +} + +BOOST_AUTO_TEST_CASE(euler_criterion_classifies_squares_in_a_quadratic_quotient_field) { + const value_type non_residue = first_quadratic_non_residue(); + const polynomial_type irreducible_divisor = {-non_residue, value_type::zero(), value_type::one()}; + polynomial_arithmetic::polynomial_context arithmetic_context; + math::polynomial_divisor_context divisor_context(irreducible_divisor, 1, arithmetic_context); + + const polynomial_type value = {value_type(3), value_type(5)}; + polynomial_type square; + math::squaremod(square, value, divisor_context, arithmetic_context); + + BOOST_CHECK(math::is_square_mod(square, divisor_context, arithmetic_context)); + BOOST_CHECK(math::is_square_mod(polynomial_type {value_type::zero()}, divisor_context, arithmetic_context)); + BOOST_CHECK(math::is_square_mod(polynomial_type {value_type::one()}, divisor_context, arithmetic_context)); + + // BabyBear has order 1 modulo 4. In a quadratic extension of such a field, the square root X of a base-field + // nonsquare remains a nonsquare in the extension. + const polynomial_type outer_generator = {value_type::zero(), value_type::one()}; + BOOST_CHECK(!math::is_square_mod(outer_generator, divisor_context, arithmetic_context)); +} + +BOOST_AUTO_TEST_CASE(indeterminate_square_test_uses_its_norm_for_monic_and_nonmonic_divisors) { + const value_type non_residue = first_quadratic_non_residue(); + const polynomial_type indeterminate = {value_type::zero(), value_type::one()}; + polynomial_arithmetic::polynomial_context arithmetic_context; + + const polynomial_type monic_quadratic = {-non_residue, value_type::zero(), value_type::one()}; + math::polynomial_divisor_context monic_context(monic_quadratic, 1, arithmetic_context); + BOOST_CHECK(!math::is_square_mod(indeterminate, monic_context, arithmetic_context)); + + const value_type scale(7); + const polynomial_type nonmonic_quadratic = {-scale * non_residue, value_type::zero(), scale}; + math::polynomial_divisor_context nonmonic_context(nonmonic_quadratic, 1, arithmetic_context); + BOOST_CHECK(!math::is_square_mod(indeterminate, nonmonic_context, arithmetic_context)); + + // BabyBear contains the cube roots of unity, so X^3 - c is irreducible when c is not a cube. For this odd-degree + // divisor, Norm(X) = c, which also exercises the sign in the constant-term formula. + const value_type cubic_non_residue = first_cubic_non_residue(); + const polynomial_type monic_cubic = {-cubic_non_residue, value_type::zero(), value_type::zero(), value_type::one()}; + math::polynomial_divisor_context cubic_context(monic_cubic, 2, arithmetic_context); + BOOST_CHECK_EQUAL(math::is_square_mod(indeterminate, cubic_context, arithmetic_context), + cubic_non_residue.is_square()); +} + +BOOST_AUTO_TEST_CASE(square_testing_rejects_inputs_outside_the_quotient_field_contract) { + polynomial_arithmetic::polynomial_context arithmetic_context; + const polynomial_type linear_divisor = {value_type::one(), value_type::one()}; + math::polynomial_divisor_context linear_context(linear_divisor, 1, arithmetic_context); + BOOST_CHECK_THROW( + math::is_square_mod(polynomial_type {value_type::one(), value_type::one()}, linear_context, arithmetic_context), + std::invalid_argument); + + const polynomial_type constant_divisor = {value_type::one()}; + math::polynomial_divisor_context constant_context(constant_divisor, 1, arithmetic_context); + BOOST_CHECK_THROW((math::polynomial_square_root_context(polynomial_type {value_type::one()}, + constant_context, arithmetic_context)), + std::invalid_argument); + BOOST_CHECK_THROW(math::is_square_mod(polynomial_type {value_type::one()}, constant_context, arithmetic_context), + std::invalid_argument); +} + +BOOST_AUTO_TEST_SUITE_END() From 44181b8a98b9b78aab9b9c86c0e96d503f851d5a Mon Sep 17 00:00:00 2001 From: Riccardo Abbate Date: Tue, 25 Aug 2026 16:35:12 -0400 Subject: [PATCH 2/5] =?UTF-8?q?caller-supplied=20Tonelli=E2=80=93Shanks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../polynomial/polynomial_square_root.hpp | 102 +++++++++++++++++- libs/math/test/polynomial_square_root.cpp | 35 ++++++ 2 files changed, 135 insertions(+), 2 deletions(-) diff --git a/libs/math/include/nil/crypto3/math/polynomial/polynomial_square_root.hpp b/libs/math/include/nil/crypto3/math/polynomial/polynomial_square_root.hpp index 3f708a0d6..34b6e1ed7 100644 --- a/libs/math/include/nil/crypto3/math/polynomial/polynomial_square_root.hpp +++ b/libs/math/include/nil/crypto3/math/polynomial/polynomial_square_root.hpp @@ -28,6 +28,7 @@ #include #include #include +#include #include @@ -57,7 +58,7 @@ namespace nil::crypto3::math { * * Given a quadratic nonresidue z in that quotient field, the context also stores z^odd_order. Tonelli-Shanks reuses * the order decomposition and this cached nonresidue power for every square root modulo B. Irreducibility is a - * caller precondition and is not tested. + * caller precondition and is not tested. The referenced divisor context must outlive this context. * * @throws std::invalid_argument if B is constant, K has characteristic two, or quadratic_non_residue is not a * reduced nonsquare residue modulo B. @@ -73,7 +74,8 @@ namespace nil::crypto3::math { polynomial_square_root_context(const polynomial_type &quadratic_non_residue, const polynomial_divisor_context &divisor_context, - polynomial_arithmetic::polynomial_context &arithmetic_context) { + polynomial_arithmetic::polynomial_context &arithmetic_context) : + divisor_context_(&divisor_context) { const std::size_t extension_degree = divisor_context.degree(); if (extension_degree == 0) { throw std::invalid_argument("polynomial square roots require a nonconstant divisor"); @@ -106,7 +108,12 @@ namespace nil::crypto3::math { return non_residue_to_odd_order_; } + const polynomial_divisor_context &divisor_context() const { + return *divisor_context_; + } + private: + const polynomial_divisor_context *divisor_context_; algebra::fields::multiplicative_group_decomposition order_decomposition_; polynomial_type non_residue_to_odd_order_; }; @@ -202,6 +209,97 @@ namespace nil::crypto3::math { return detail::is_square_mod_impl(input, divisor_context, arithmetic_context, nullptr); } + /** + * Compute a square root of input in K[X]/(B) with Tonelli-Shanks, using the multiplicative-group decomposition and + * quadratic nonresidue cached in square_root_context. The irreducible polynomial B is stored in the divisor context + * referenced by square_root_context. The result is canonical and may alias input. + * + * The function returns false when input is not a square. In that case output is set to the zero polynomial. Zero + * is its own square root. + * + * @throws std::invalid_argument if input is not a reduced quotient-field representative. + * @pre B is irreducible and input is a nonempty canonical coefficient polynomial. + */ + template + requires algebra::FieldValue + bool square_root_mod(typename Backend::polynomial_type &output, const typename Backend::polynomial_type &input, + const polynomial_square_root_context &square_root_context, + polynomial_arithmetic::polynomial_context &arithmetic_context) { + using polynomial_type = typename Backend::polynomial_type; + using value_type = typename polynomial_type::value_type; + const polynomial_divisor_context &divisor_context = square_root_context.divisor_context(); + + if (input.size() > divisor_context.degree()) { + throw std::invalid_argument("a polynomial square root requires a reduced quotient-field representative"); + } + if (input.size() == 1 && input[0] == value_type::zero()) { + output.assign(1, value_type::zero()); + return true; + } + + polynomial_type root; + polynomial_type input_to_odd_order; + polynomial_type non_residue_power = square_root_context.non_residue_to_odd_order(); + + // Write the quotient field's multiplicative-group order as odd_order * 2^S. For input a and the cached + // nonresidue z, Tonelli-Shanks starts with + // + // input_to_odd_order = a^odd_order, + // root = a^((odd_order + 1) / 2), + // non_residue_power = z^odd_order. + // + // The first two values satisfy root^2 = a * input_to_odd_order. + powmod(input_to_odd_order, input, square_root_context.odd_order(), divisor_context, arithmetic_context); + boost::multiprecision::cpp_int root_exponent = square_root_context.odd_order() + 1; + root_exponent >>= 1; + powmod(root, input, root_exponent, divisor_context, arithmetic_context); + + std::size_t remaining_two_adicity = square_root_context.two_adicity(); + const polynomial_type one = {value_type::one()}; + while (input_to_odd_order != one) { + // The odd-order exponent places this value in the power-of-two subgroup. Find the smallest i for which + // + // input_to_odd_order^(2^i) = 1. + // + // A square must reach one for some i < M, where M = remaining_two_adicity and its current order divides + // 2^M. + polynomial_type power = input_to_odd_order; + std::size_t first_one_power = 0; + do { + squaremod(power, power, divisor_context, arithmetic_context); + ++first_one_power; + } while (first_one_power < remaining_two_adicity && power != one); + + if (first_one_power == remaining_two_adicity) { + output.assign(1, value_type::zero()); + return false; + } + + // Set correction = non_residue_power^(2^(M - i - 1)), then update + // + // non_residue_power = correction^2, + // input_to_odd_order = input_to_odd_order * correction^2, + // root = root * correction, + // M = i. + // + // This preserves root^2 = input * input_to_odd_order while reducing input_to_odd_order's two-power order + // from 2^M to 2^i. + // The previous nonresidue power is no longer needed, so move its storage into the correction. + polynomial_type correction = std::move(non_residue_power); + for (std::size_t i = 0; i < remaining_two_adicity - first_one_power - 1; ++i) { + squaremod(correction, correction, divisor_context, arithmetic_context); + } + + squaremod(non_residue_power, correction, divisor_context, arithmetic_context); + mulmod(input_to_odd_order, input_to_odd_order, non_residue_power, divisor_context, arithmetic_context); + mulmod(root, root, correction, divisor_context, arithmetic_context); + remaining_two_adicity = first_one_power; + } + + output = std::move(root); + return true; + } + } // namespace nil::crypto3::math #endif // CRYPTO3_MATH_POLYNOMIAL_SQUARE_ROOT_HPP diff --git a/libs/math/test/polynomial_square_root.cpp b/libs/math/test/polynomial_square_root.cpp index 0c41830e7..0c2d4a47c 100644 --- a/libs/math/test/polynomial_square_root.cpp +++ b/libs/math/test/polynomial_square_root.cpp @@ -153,4 +153,39 @@ BOOST_AUTO_TEST_CASE(square_testing_rejects_inputs_outside_the_quotient_field_co std::invalid_argument); } +BOOST_AUTO_TEST_CASE(tonelli_shanks_recovers_square_roots_in_a_quotient_field) { + const value_type base_non_residue = first_quadratic_non_residue(); + const polynomial_type irreducible_divisor = {-base_non_residue, value_type::zero(), value_type::one()}; + const polynomial_type quotient_non_residue = {value_type::zero(), value_type::one()}; + polynomial_arithmetic::polynomial_context arithmetic_context; + math::polynomial_divisor_context divisor_context(irreducible_divisor, 1, arithmetic_context); + const math::polynomial_square_root_context square_root_context(quotient_non_residue, divisor_context, + arithmetic_context); + + const polynomial_type value = {value_type(3), value_type(5)}; + polynomial_type square; + math::squaremod(square, value, divisor_context, arithmetic_context); + + polynomial_type root; + BOOST_REQUIRE(math::square_root_mod(root, square, square_root_context, arithmetic_context)); + polynomial_type recovered_square; + math::squaremod(recovered_square, root, divisor_context, arithmetic_context); + BOOST_CHECK(recovered_square == square); + + polynomial_type aliased_root = square; + BOOST_REQUIRE(math::square_root_mod(aliased_root, aliased_root, square_root_context, arithmetic_context)); + math::squaremod(recovered_square, aliased_root, divisor_context, arithmetic_context); + BOOST_CHECK(recovered_square == square); + + const polynomial_type zero = {value_type::zero()}; + BOOST_REQUIRE(math::square_root_mod(root, zero, square_root_context, arithmetic_context)); + BOOST_CHECK(root == zero); + + BOOST_CHECK(!math::square_root_mod(root, quotient_non_residue, square_root_context, arithmetic_context)); + BOOST_CHECK(root == zero); + + BOOST_CHECK_THROW(math::square_root_mod(root, irreducible_divisor, square_root_context, arithmetic_context), + std::invalid_argument); +} + BOOST_AUTO_TEST_SUITE_END() From c82964f0837f5d7af57b46e662a318eefb5cb572 Mon Sep 17 00:00:00 2001 From: Riccardo Abbate Date: Tue, 25 Aug 2026 17:01:38 -0400 Subject: [PATCH 3/5] nonresidue discovery path --- .../polynomial/polynomial_square_root.hpp | 73 ++++++++++++++----- libs/math/test/polynomial_square_root.cpp | 62 ++++++++++++++++ 2 files changed, 118 insertions(+), 17 deletions(-) diff --git a/libs/math/include/nil/crypto3/math/polynomial/polynomial_square_root.hpp b/libs/math/include/nil/crypto3/math/polynomial/polynomial_square_root.hpp index 34b6e1ed7..93b4c5b4f 100644 --- a/libs/math/include/nil/crypto3/math/polynomial/polynomial_square_root.hpp +++ b/libs/math/include/nil/crypto3/math/polynomial/polynomial_square_root.hpp @@ -60,9 +60,15 @@ namespace nil::crypto3::math { * the order decomposition and this cached nonresidue power for every square root modulo B. Irreducibility is a * caller precondition and is not tested. The referenced divisor context must outlive this context. * - * @throws std::invalid_argument if B is constant, K has characteristic two, or quadratic_non_residue is not a - * reduced nonsquare residue modulo B. - * @pre B is irreducible and quadratic_non_residue is a nonempty canonical coefficient polynomial. + * Tonelli-Shanks needs a nonsquare z. Whether a polynomial is a square depends on B, so the second constructor asks + * a caller-owned generator for candidates until one is nonsquare modulo B. Each candidate polynomial represents + * one element of K[X]/(B). The caller owns the generator and controls its random source and seed. The first + * constructor can be used when the caller already knows a suitable z. + * + * @throws std::invalid_argument if B is constant, K has characteristic two, the explicitly supplied nonresidue is + * square, or a supplied or generated representative is empty, noncanonical, or not reduced modulo B. + * @pre B is irreducible. An explicitly supplied nonresidue is canonical. A generator returns canonical reduced + * representatives and eventually returns a nonsquare. */ template requires algebra::FieldValue @@ -76,24 +82,30 @@ namespace nil::crypto3::math { const polynomial_divisor_context &divisor_context, polynomial_arithmetic::polynomial_context &arithmetic_context) : divisor_context_(&divisor_context) { - const std::size_t extension_degree = divisor_context.degree(); - if (extension_degree == 0) { - throw std::invalid_argument("polynomial square roots require a nonconstant divisor"); - } - if (algebra::fields::field_characteristic() == 2) { - throw std::invalid_argument("polynomial square roots in characteristic two are not implemented"); - } - if (quadratic_non_residue.size() > extension_degree) { - throw std::invalid_argument("a polynomial square-root nonresidue must be reduced modulo the divisor"); - } - - order_decomposition_ = - algebra::fields::extension_field_multiplicative_group_decomposition(extension_degree); + initialize_order_decomposition(); + require_canonical_reduced(quadratic_non_residue); if (detail::is_square_mod_impl(quadratic_non_residue, divisor_context, arithmetic_context, &order_decomposition_)) { throw std::invalid_argument("a polynomial square-root context requires a quadratic nonresidue"); } - powmod(non_residue_to_odd_order_, quadratic_non_residue, odd_order(), divisor_context, arithmetic_context); + cache_non_residue_power(quadratic_non_residue, arithmetic_context); + } + + template + requires requires(Generator &generator) { + { generator() } -> std::convertible_to; + } + polynomial_square_root_context(const polynomial_divisor_context &divisor_context, + polynomial_arithmetic::polynomial_context &arithmetic_context, + Generator &generator) : divisor_context_(&divisor_context) { + initialize_order_decomposition(); + + polynomial_type candidate; + do { + candidate = generator(); + require_canonical_reduced(candidate); + } while (detail::is_square_mod_impl(candidate, divisor_context, arithmetic_context, &order_decomposition_)); + cache_non_residue_power(candidate, arithmetic_context); } const boost::multiprecision::cpp_int &odd_order() const { @@ -113,6 +125,33 @@ namespace nil::crypto3::math { } private: + void initialize_order_decomposition() { + const std::size_t extension_degree = divisor_context().degree(); + if (extension_degree == 0) { + throw std::invalid_argument("polynomial square roots require a nonconstant divisor"); + } + if (algebra::fields::field_characteristic() == 2) { + throw std::invalid_argument("polynomial square roots in characteristic two are not implemented"); + } + order_decomposition_ = + algebra::fields::extension_field_multiplicative_group_decomposition(extension_degree); + } + + void require_canonical_reduced(const polynomial_type &candidate) const { + if (candidate.empty() || candidate.size() > divisor_context().degree() || + (candidate.size() > 1 && candidate.back() == value_type::zero())) { + throw std::invalid_argument( + "a polynomial square-root nonresidue candidate must be a nonempty canonical reduced " + "representative"); + } + } + + void cache_non_residue_power(const polynomial_type &quadratic_non_residue, + polynomial_arithmetic::polynomial_context &arithmetic_context) { + powmod(non_residue_to_odd_order_, quadratic_non_residue, odd_order(), divisor_context(), + arithmetic_context); + } + const polynomial_divisor_context *divisor_context_; algebra::fields::multiplicative_group_decomposition order_decomposition_; polynomial_type non_residue_to_odd_order_; diff --git a/libs/math/test/polynomial_square_root.cpp b/libs/math/test/polynomial_square_root.cpp index 0c2d4a47c..f9bfcfedc 100644 --- a/libs/math/test/polynomial_square_root.cpp +++ b/libs/math/test/polynomial_square_root.cpp @@ -93,6 +93,68 @@ BOOST_AUTO_TEST_CASE(square_root_context_validates_and_caches_tonelli_shanks_par std::invalid_argument); } +BOOST_AUTO_TEST_CASE(square_root_context_discovers_a_nonresidue_with_injected_sampling) { + const value_type base_non_residue = first_quadratic_non_residue(); + const polynomial_type irreducible_divisor = {-base_non_residue, value_type::zero(), value_type::one()}; + const polynomial_type quotient_non_residue = {value_type::zero(), value_type::one()}; + polynomial_arithmetic::polynomial_context arithmetic_context; + math::polynomial_divisor_context divisor_context(irreducible_divisor, 1, arithmetic_context); + + std::size_t sample_index = 0; + auto generator = [&] { + ++sample_index; + return sample_index == 1 ? polynomial_type {value_type::one()} : quotient_non_residue; + }; + const math::polynomial_square_root_context square_root_context(divisor_context, arithmetic_context, + generator); + + BOOST_CHECK_EQUAL(sample_index, 2); + polynomial_type expected_non_residue_to_odd_order; + math::powmod(expected_non_residue_to_odd_order, quotient_non_residue, square_root_context.odd_order(), + divisor_context, arithmetic_context); + BOOST_CHECK(square_root_context.non_residue_to_odd_order() == expected_non_residue_to_odd_order); + + const polynomial_type value = {value_type(3), value_type(5)}; + polynomial_type square; + math::squaremod(square, value, divisor_context, arithmetic_context); + polynomial_type root; + BOOST_REQUIRE(math::square_root_mod(root, square, square_root_context, arithmetic_context)); + polynomial_type recovered_square; + math::squaremod(recovered_square, root, divisor_context, arithmetic_context); + BOOST_CHECK(recovered_square == square); +} + +BOOST_AUTO_TEST_CASE(square_root_context_rejects_invalid_generated_representatives) { + const value_type base_non_residue = first_quadratic_non_residue(); + const polynomial_type irreducible_divisor = {-base_non_residue, value_type::zero(), value_type::one()}; + polynomial_arithmetic::polynomial_context arithmetic_context; + math::polynomial_divisor_context divisor_context(irreducible_divisor, 1, arithmetic_context); + + auto empty_generator = [] { + polynomial_type result; + result.get_storage().clear(); + return result; + }; + BOOST_CHECK_THROW( + (math::polynomial_square_root_context(divisor_context, arithmetic_context, empty_generator)), + std::invalid_argument); + + auto noncanonical_generator = [] { + polynomial_type result(2); + result[0] = value_type::one(); + result[1] = value_type::zero(); + return result; + }; + BOOST_CHECK_THROW((math::polynomial_square_root_context(divisor_context, arithmetic_context, + noncanonical_generator)), + std::invalid_argument); + + auto unreduced_generator = [&] { return irreducible_divisor; }; + BOOST_CHECK_THROW( + (math::polynomial_square_root_context(divisor_context, arithmetic_context, unreduced_generator)), + std::invalid_argument); +} + BOOST_AUTO_TEST_CASE(euler_criterion_classifies_squares_in_a_quadratic_quotient_field) { const value_type non_residue = first_quadratic_non_residue(); const polynomial_type irreducible_divisor = {-non_residue, value_type::zero(), value_type::one()}; From c0dcbcf2aa8c06e91bd23e91cdfed656d3b83af8 Mon Sep 17 00:00:00 2001 From: Riccardo Abbate Date: Tue, 25 Aug 2026 17:33:47 -0400 Subject: [PATCH 4/5] Fq and Fq12 roots tests --- libs/math/test/polynomial_square_root.cpp | 94 +++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/libs/math/test/polynomial_square_root.cpp b/libs/math/test/polynomial_square_root.cpp index f9bfcfedc..7a48d4419 100644 --- a/libs/math/test/polynomial_square_root.cpp +++ b/libs/math/test/polynomial_square_root.cpp @@ -27,9 +27,14 @@ #include #include +#include #include +#include +#include #include +#include +#include #include #include @@ -250,4 +255,93 @@ BOOST_AUTO_TEST_CASE(tonelli_shanks_recovers_square_roots_in_a_quotient_field) { std::invalid_argument); } +BOOST_AUTO_TEST_CASE(tonelli_shanks_recovers_a_bn254_fq_square) { + using fq_field_type = fields::alt_bn128_base_field<254>; + using fq_value_type = fq_field_type::value_type; + using fq_backend_type = polynomial_arithmetic::schoolbook_backend; + using fq_polynomial_type = fq_backend_type::polynomial_type; + + fq_value_type non_residue(2); + while (non_residue.is_square()) { + non_residue = non_residue + fq_value_type::one(); + } + + polynomial_arithmetic::polynomial_context arithmetic_context; + // Modulo the linear irreducible B = X, the quotient is Fq itself. This isolates support for Fq coefficients; the + // earlier BabyBear test covers a nontrivial quotient extension. + const fq_polynomial_type divisor = {fq_value_type::zero(), fq_value_type::one()}; + math::polynomial_divisor_context divisor_context(divisor, 1, arithmetic_context); + const math::polynomial_square_root_context square_root_context( + fq_polynomial_type {non_residue}, divisor_context, arithmetic_context); + + const fq_polynomial_type value = {fq_value_type(7)}; + fq_polynomial_type square; + math::squaremod(square, value, divisor_context, arithmetic_context); + fq_polynomial_type root; + BOOST_REQUIRE(math::square_root_mod(root, square, square_root_context, arithmetic_context)); + fq_polynomial_type recovered_square; + math::squaremod(recovered_square, root, divisor_context, arithmetic_context); + BOOST_CHECK(recovered_square == square); +} + +BOOST_AUTO_TEST_CASE(tonelli_shanks_recovers_a_bn254_fq12_square) { + using fq12_field_type = fields::fp12_2over3over2>; + using fq12_value_type = fq12_field_type::value_type; + using fq12_backend_type = polynomial_arithmetic::schoolbook_backend; + using fq12_polynomial_type = fq12_backend_type::polynomial_type; + + polynomial_arithmetic::polynomial_context arithmetic_context; + // Modulo the linear irreducible B = X, the quotient is Fq12 itself. This isolates support for Fq12 coefficients; + // the following test combines them with a nontrivial quotient extension. + const fq12_polynomial_type divisor = {fq12_value_type::zero(), fq12_value_type::one()}; + math::polynomial_divisor_context divisor_context(divisor, 1, arithmetic_context); + + boost::random::mt19937 rng(0xF012); + auto generator = [&] { return fq12_polynomial_type {nil::crypto3::algebra::random_element(rng)}; }; + const math::polynomial_square_root_context square_root_context(divisor_context, + arithmetic_context, generator); + + const fq12_polynomial_type value = {nil::crypto3::algebra::random_element(rng)}; + fq12_polynomial_type square; + math::squaremod(square, value, divisor_context, arithmetic_context); + fq12_polynomial_type root; + BOOST_REQUIRE(math::square_root_mod(root, square, square_root_context, arithmetic_context)); + fq12_polynomial_type recovered_square; + math::squaremod(recovered_square, root, divisor_context, arithmetic_context); + BOOST_CHECK(recovered_square == square); +} + +BOOST_AUTO_TEST_CASE(tonelli_shanks_recovers_a_square_in_a_quadratic_extension_of_bn254_fq12) { + using fq12_field_type = fields::fp12_2over3over2>; + using fq12_value_type = fq12_field_type::value_type; + using fq12_backend_type = polynomial_arithmetic::schoolbook_backend; + using fq12_polynomial_type = fq12_backend_type::polynomial_type; + + boost::random::mt19937 rng(0xF01202); + fq12_value_type base_non_residue = fq12_value_type::one(); + for (std::size_t sample = 0; sample < 64 && base_non_residue.is_square(); ++sample) { + base_non_residue = nil::crypto3::algebra::random_element(rng); + } + BOOST_REQUIRE(!base_non_residue.is_square()); + + polynomial_arithmetic::polynomial_context arithmetic_context; + // X^2 - base_non_residue is irreducible because base_non_residue is not a square in Fq12. + const fq12_polynomial_type divisor = {-base_non_residue, fq12_value_type::zero(), fq12_value_type::one()}; + math::polynomial_divisor_context divisor_context(divisor, 1, arithmetic_context); + // Norm(X) = -base_non_residue. Since -1 is square in Fq12, this norm and therefore X are nonsquares. + const fq12_polynomial_type quotient_non_residue = {fq12_value_type::zero(), fq12_value_type::one()}; + const math::polynomial_square_root_context square_root_context( + quotient_non_residue, divisor_context, arithmetic_context); + + const fq12_polynomial_type value = {nil::crypto3::algebra::random_element(rng), + nil::crypto3::algebra::random_element(rng)}; + fq12_polynomial_type square; + math::squaremod(square, value, divisor_context, arithmetic_context); + fq12_polynomial_type root; + BOOST_REQUIRE(math::square_root_mod(root, square, square_root_context, arithmetic_context)); + fq12_polynomial_type recovered_square; + math::squaremod(recovered_square, root, divisor_context, arithmetic_context); + BOOST_CHECK(recovered_square == square); +} + BOOST_AUTO_TEST_SUITE_END() From 80694304616916a12f0321266143a177f5524132 Mon Sep 17 00:00:00 2001 From: Riccardo Abbate Date: Tue, 25 Aug 2026 19:06:28 -0400 Subject: [PATCH 5/5] rational poly reconstruction --- .../polynomial_rational_reconstruction.hpp | 148 +++++++++++++ libs/math/test/CMakeLists.txt | 1 + .../polynomial_rational_reconstruction.cpp | 198 ++++++++++++++++++ 3 files changed, 347 insertions(+) create mode 100644 libs/math/include/nil/crypto3/math/polynomial/polynomial_rational_reconstruction.hpp create mode 100644 libs/math/test/polynomial_rational_reconstruction.cpp diff --git a/libs/math/include/nil/crypto3/math/polynomial/polynomial_rational_reconstruction.hpp b/libs/math/include/nil/crypto3/math/polynomial/polynomial_rational_reconstruction.hpp new file mode 100644 index 000000000..1d635f464 --- /dev/null +++ b/libs/math/include/nil/crypto3/math/polynomial/polynomial_rational_reconstruction.hpp @@ -0,0 +1,148 @@ +//---------------------------------------------------------------------------// +// Copyright (c) 2026 +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +//---------------------------------------------------------------------------// + +#ifndef CRYPTO3_MATH_POLYNOMIAL_RATIONAL_RECONSTRUCTION_HPP +#define CRYPTO3_MATH_POLYNOMIAL_RATIONAL_RECONSTRUCTION_HPP + +#include +#include +#include +#include + +#include + +namespace nil::crypto3::math { + + /** + * Reconstruct polynomials numerator P and denominator Q such that + * + * P = residue * Q mod modulus, + * + * with degree(P) at most maximum_numerator_degree and degree(Q) at most + * maximum_denominator_degree. The denominator is normalized to monic form, and the numerator is scaled by the + * same field element. Outputs are replaced only when reconstruction succeeds and may alias either input, but must + * be distinct from each other. + * + * Starting with consecutive Euclidean remainders R0 = modulus and R1 = residue, write + * + * Ri = Ui * modulus + Qi * residue. + * + * Here Ui is the coefficient of modulus and Qi is the coefficient of residue. Initially U0 = 1, Q0 = 0 and + * U1 = 0, Q1 = 1. The Ui terms vanish modulo modulus, so the algorithm stores only Q0 and Q1. + * + * A Euclidean quotient q replaces the pairs by + * + * (R0, R1) <- (R1, R0 - q * R1), + * (Q0, Q1) <- (Q1, Q0 - q * Q1). + * + * Repeat this step while R1 is nonzero and degree(R1) exceeds maximum_numerator_degree. The loop normally stops + * before the Euclidean sequence reaches zero. At that point R1 is the first remainder within the numerator bound, + * so R1 is a valid numerator and Q1 is its denominator. Reconstruction fails if Q1 exceeds the denominator bound. + * + * The strict condition + * + * maximum_numerator_degree + maximum_denominator_degree < degree(modulus) + * + * ensures that at most one rational function can satisfy the requested bounds. + * + * @return true on success; false if the Euclidean candidate does not satisfy the denominator bound. + * @throws std::invalid_argument if the outputs are the same object; an input is empty or noncanonical; modulus is + * constant; residue is not reduced modulo modulus; or the degree bounds do not satisfy the strict + * uniqueness condition. + */ + template + bool rational_reconstruct(typename Backend::polynomial_type &numerator, + typename Backend::polynomial_type &denominator, + const typename Backend::polynomial_type &residue, + const typename Backend::polynomial_type &modulus, + std::size_t maximum_numerator_degree, + std::size_t maximum_denominator_degree, + polynomial_arithmetic::polynomial_context &arithmetic_context) { + using polynomial_type = typename Backend::polynomial_type; + using value_type = typename polynomial_type::value_type; + + if (std::addressof(numerator) == std::addressof(denominator)) { + throw std::invalid_argument("rational-reconstruction outputs must be distinct objects"); + } + const auto is_canonical = [](const polynomial_type &polynomial) { + return !polynomial.empty() && + (polynomial.size() == 1 || polynomial[polynomial.size() - 1] != value_type {}); + }; + if (!is_canonical(residue) || !is_canonical(modulus)) { + throw std::invalid_argument("rational reconstruction requires canonical nonempty inputs"); + } + if (modulus.size() == 1) { + throw std::invalid_argument("rational reconstruction requires a nonconstant modulus"); + } + + const std::size_t modulus_degree = modulus.size() - 1; + if (residue.size() > modulus_degree) { + throw std::invalid_argument("rational reconstruction requires a reduced residue"); + } + // Express the strict sum bound without a potentially overflowing addition. + if (maximum_numerator_degree >= modulus_degree || + maximum_denominator_degree >= modulus_degree - maximum_numerator_degree) { + throw std::invalid_argument("rational-reconstruction degree bounds do not ensure uniqueness"); + } + + polynomial_type previous_remainder(modulus); + polynomial_type current_remainder(residue); + polynomial_type previous_denominator = {value_type {}}; + polynomial_type current_denominator = {value_type::one()}; + polynomial_type quotient; + polynomial_type next_remainder; + polynomial_type quotient_times_denominator; + polynomial_type next_denominator; + + while (!is_zero(current_remainder) && current_remainder.size() - 1 > maximum_numerator_degree) { + detail::gcd_divrem_step(quotient, next_remainder, previous_remainder, current_remainder, + arithmetic_context); + arithmetic_context.multiply(quotient_times_denominator, quotient, current_denominator); + subtraction(next_denominator, previous_denominator, quotient_times_denominator); + + previous_remainder = std::move(current_remainder); + current_remainder = std::move(next_remainder); + previous_denominator = std::move(current_denominator); + current_denominator = std::move(next_denominator); + } + + if (is_zero(current_denominator) || current_denominator.size() - 1 > maximum_denominator_degree) { + return false; + } + + const value_type denominator_leading_coefficient = current_denominator[current_denominator.size() - 1]; + if (denominator_leading_coefficient != value_type::one()) { + const value_type normalization = denominator_leading_coefficient.inversed(); + scalar_multiplication(current_remainder, current_remainder, normalization); + scalar_multiplication(current_denominator, current_denominator, normalization); + } + + numerator = std::move(current_remainder); + denominator = std::move(current_denominator); + return true; + } + +} // namespace nil::crypto3::math + +#endif // CRYPTO3_MATH_POLYNOMIAL_RATIONAL_RECONSTRUCTION_HPP diff --git a/libs/math/test/CMakeLists.txt b/libs/math/test/CMakeLists.txt index c10943425..21c5b09f3 100644 --- a/libs/math/test/CMakeLists.txt +++ b/libs/math/test/CMakeLists.txt @@ -53,6 +53,7 @@ set(TESTS_NAMES "polynomial_backend" "polynomial_division" "polynomial_exponentiation" + "polynomial_rational_reconstruction" "polynomial_square_root" "polynomial_composition" "polynomial_frobenius" diff --git a/libs/math/test/polynomial_rational_reconstruction.cpp b/libs/math/test/polynomial_rational_reconstruction.cpp new file mode 100644 index 000000000..c9b64ab10 --- /dev/null +++ b/libs/math/test/polynomial_rational_reconstruction.cpp @@ -0,0 +1,198 @@ +//---------------------------------------------------------------------------// +// Copyright (c) 2026 +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +//---------------------------------------------------------------------------// + +#define BOOST_TEST_MODULE polynomial_rational_reconstruction_test + +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include + +namespace { + namespace fields = nil::crypto3::algebra::fields; + namespace math = nil::crypto3::math; + namespace polynomial_arithmetic = math::polynomial_arithmetic; + + using value_type = fields::babybear::value_type; + using backend_type = polynomial_arithmetic::schoolbook_backend; + using polynomial_type = typename backend_type::polynomial_type; + + using fq_value_type = fields::alt_bn128_base_field<254>::value_type; + using fq12_field_type = fields::fp12_2over3over2>; + using fq12_value_type = fq12_field_type::value_type; + using fq12_backend_type = polynomial_arithmetic::schoolbook_backend; + using fq12_polynomial_type = typename fq12_backend_type::polynomial_type; + + fq12_value_type fq12_value(std::size_t first_coordinate) { + fq12_value_type result = fq12_value_type::zero(); + for (std::size_t i = 0; i < fq12_field_type::arity; ++i) { + result.coordinate(i) = fq_value_type(first_coordinate + i); + } + return result; + } + + const polynomial_type modulus = {value_type(3), value_type(3), value_type(3), value_type(2), value_type::one()}; + const polynomial_type residue = {value_type(2), value_type::one(), value_type::one(), value_type::one()}; + const polynomial_type expected_numerator = {value_type::one()}; + const polynomial_type expected_denominator = {value_type(2), value_type(2), value_type::one()}; +} // namespace + +BOOST_AUTO_TEST_SUITE(polynomial_rational_reconstruction_test_suite) + +BOOST_AUTO_TEST_CASE(reconstruction_stops_at_the_degree_bound_and_normalizes_the_denominator) { + polynomial_arithmetic::polynomial_context arithmetic_context; + polynomial_type numerator; + polynomial_type denominator; + + BOOST_REQUIRE(math::rational_reconstruct(numerator, denominator, residue, modulus, 0, 2, arithmetic_context)); + BOOST_CHECK(numerator == expected_numerator); + BOOST_CHECK(denominator == expected_denominator); + + polynomial_type residue_times_denominator; + arithmetic_context.multiply(residue_times_denominator, residue, denominator); + polynomial_type divisor_quotient; + polynomial_type congruence_remainder; + math::division(divisor_quotient, congruence_remainder, residue_times_denominator, modulus); + BOOST_CHECK(congruence_remainder == numerator); +} + +BOOST_AUTO_TEST_CASE(reconstruction_failure_preserves_the_outputs) { + polynomial_arithmetic::polynomial_context arithmetic_context; + polynomial_type numerator = {value_type(7)}; + polynomial_type denominator = {value_type(11)}; + + BOOST_CHECK(!math::rational_reconstruct(numerator, denominator, residue, modulus, 0, 1, arithmetic_context)); + BOOST_CHECK(numerator == polynomial_type({value_type(7)})); + BOOST_CHECK(denominator == polynomial_type({value_type(11)})); +} + +BOOST_AUTO_TEST_CASE(reconstruction_supports_bn254_fq12_coefficients) { + polynomial_arithmetic::polynomial_context arithmetic_context; + + const fq12_polynomial_type first_quotient = {fq12_value(1), fq12_value(13)}; + const fq12_polynomial_type second_quotient = {fq12_value(25), fq12_value(37)}; + const fq12_polynomial_type second_remainder = {fq12_value(49), fq12_value(61), fq12_value(73)}; + const fq12_polynomial_type expected_numerator_before_normalization = {fq12_value(85)}; + + // Construct two Euclidean steps backwards: + // + // residue = second_quotient * second_remainder + expected_numerator, + // modulus = first_quotient * residue + second_remainder. + fq12_polynomial_type product; + fq12_polynomial_type extension_residue; + arithmetic_context.multiply(product, second_quotient, second_remainder); + math::addition(extension_residue, product, expected_numerator_before_normalization); + fq12_polynomial_type extension_modulus; + arithmetic_context.multiply(product, first_quotient, extension_residue); + math::addition(extension_modulus, product, second_remainder); + + // The matching residue coefficient is 1 + second_quotient * first_quotient. Normalize it and the numerator by + // the same Fq12 scalar to obtain the function's canonical result. + fq12_polynomial_type extension_expected_denominator; + arithmetic_context.multiply(extension_expected_denominator, second_quotient, first_quotient); + math::addition(extension_expected_denominator, extension_expected_denominator, + fq12_polynomial_type {fq12_value_type::one()}); + const fq12_value_type normalization = extension_expected_denominator.back().inversed(); + fq12_polynomial_type extension_expected_numerator; + math::scalar_multiplication(extension_expected_numerator, expected_numerator_before_normalization, normalization); + math::scalar_multiplication(extension_expected_denominator, extension_expected_denominator, normalization); + + fq12_polynomial_type numerator; + fq12_polynomial_type denominator; + BOOST_REQUIRE(math::rational_reconstruct(numerator, denominator, extension_residue, extension_modulus, 0, 2, + arithmetic_context)); + BOOST_CHECK(numerator == extension_expected_numerator); + BOOST_CHECK(denominator == extension_expected_denominator); +} + +BOOST_AUTO_TEST_CASE(reconstruction_handles_zero_and_input_aliasing) { + polynomial_arithmetic::polynomial_context arithmetic_context; + polynomial_type numerator; + polynomial_type denominator; + const polynomial_type zero = {value_type::zero()}; + + BOOST_REQUIRE(math::rational_reconstruct(numerator, denominator, zero, modulus, 0, 0, arithmetic_context)); + BOOST_CHECK(numerator == zero); + BOOST_CHECK(denominator == polynomial_type({value_type::one()})); + + polynomial_type residue_alias = residue; + polynomial_type modulus_alias = modulus; + BOOST_REQUIRE(math::rational_reconstruct(residue_alias, modulus_alias, residue_alias, modulus_alias, 0, 2, + arithmetic_context)); + BOOST_CHECK(residue_alias == expected_numerator); + BOOST_CHECK(modulus_alias == expected_denominator); +} + +BOOST_AUTO_TEST_CASE(reconstruction_rejects_invalid_inputs_and_bounds) { + polynomial_arithmetic::polynomial_context arithmetic_context; + polynomial_type numerator; + polynomial_type denominator; + + BOOST_CHECK_THROW(math::rational_reconstruct(numerator, numerator, residue, modulus, 0, 2, arithmetic_context), + std::invalid_argument); + + polynomial_type empty; + empty.get_storage().clear(); + BOOST_CHECK_THROW(math::rational_reconstruct(numerator, denominator, empty, modulus, 0, 2, arithmetic_context), + std::invalid_argument); + + polynomial_type noncanonical(2); + noncanonical[0] = value_type::one(); + noncanonical[1] = value_type::zero(); + BOOST_CHECK_THROW( + math::rational_reconstruct(numerator, denominator, noncanonical, modulus, 0, 2, arithmetic_context), + std::invalid_argument); + + const polynomial_type constant_modulus = {value_type::one()}; + BOOST_CHECK_THROW( + math::rational_reconstruct(numerator, denominator, residue, constant_modulus, 0, 0, arithmetic_context), + std::invalid_argument); + + const polynomial_type quadratic_modulus = {value_type::one(), value_type::zero(), value_type::one()}; + const polynomial_type unreduced_residue = {value_type::one(), value_type::one(), value_type::one()}; + BOOST_CHECK_THROW(math::rational_reconstruct(numerator, denominator, unreduced_residue, quadratic_modulus, 0, 0, + arithmetic_context), + std::invalid_argument); + + BOOST_CHECK_THROW(math::rational_reconstruct(numerator, denominator, residue, modulus, 2, 2, arithmetic_context), + std::invalid_argument); + + const std::size_t maximum_size = std::numeric_limits::max(); + BOOST_CHECK_THROW( + math::rational_reconstruct(numerator, denominator, residue, modulus, maximum_size, 0, arithmetic_context), + std::invalid_argument); + BOOST_CHECK_THROW( + math::rational_reconstruct(numerator, denominator, residue, modulus, 0, maximum_size, arithmetic_context), + std::invalid_argument); +} + +BOOST_AUTO_TEST_SUITE_END()