From 53373c4539b7406dd9c0314fb4954a051cec614d Mon Sep 17 00:00:00 2001 From: Riccardo Abbate Date: Mon, 24 Aug 2026 11:35:24 -0400 Subject: [PATCH 1/5] equal-degree api --- .../distinct_degree_factorization.hpp | 36 +----- .../polynomial/equal_degree_factorization.hpp | 82 +++++++++++++ ...en_shoup_distinct_degree_factorization.hpp | 4 +- .../polynomial/polynomial_factorization.hpp | 34 ++++++ libs/math/test/CMakeLists.txt | 1 + libs/math/test/equal_degree_factorization.cpp | 113 ++++++++++++++++++ 6 files changed, 234 insertions(+), 36 deletions(-) create mode 100644 libs/math/include/nil/crypto3/math/polynomial/equal_degree_factorization.hpp create mode 100644 libs/math/test/equal_degree_factorization.cpp diff --git a/libs/math/include/nil/crypto3/math/polynomial/distinct_degree_factorization.hpp b/libs/math/include/nil/crypto3/math/polynomial/distinct_degree_factorization.hpp index 035d039d4..428049a7d 100644 --- a/libs/math/include/nil/crypto3/math/polynomial/distinct_degree_factorization.hpp +++ b/libs/math/include/nil/crypto3/math/polynomial/distinct_degree_factorization.hpp @@ -46,38 +46,6 @@ namespace nil::crypto3::math { { callback(factor) } -> std::same_as; }; - /** - * Normalize a distinct-degree factorization input and verify that every irreducible factor has multiplicity - * one. The original leading coefficient is returned separately. Constant inputs return false; nonconstant - * square-free inputs return true and monic_input receives their monic form. - * - * @throws std::invalid_argument if a nonconstant input is not square-free. - * @pre input is a nonempty coefficient polynomial. - */ - template - bool prepare_distinct_degree_factorization_input( - typename Backend::polynomial_type &monic_input, - typename Backend::polynomial_type::value_type &leading_coefficient, - const typename Backend::polynomial_type &input, - polynomial_arithmetic::polynomial_context &arithmetic_context) { - monic_input = input; - condense(monic_input); - leading_coefficient = monic_input.back(); - if (monic_input.size() == 1) { - return false; - } - make_monic(monic_input, monic_input); - - typename Backend::polynomial_type input_derivative; - derivative(input_derivative, monic_input); - typename Backend::polynomial_type repeated_factor; - gcd(repeated_factor, monic_input, input_derivative, arithmetic_context); - if (repeated_factor.size() > 1) { - throw std::invalid_argument("distinct-degree factorization requires a square-free polynomial"); - } - return true; - } - } // namespace detail /** @@ -122,8 +90,8 @@ namespace nil::crypto3::math { result_type result; polynomial_type monic_input; - if (!detail::prepare_distinct_degree_factorization_input(monic_input, result.leading_coefficient, - input, arithmetic_context)) { + if (!detail::prepare_square_free_factorization_input(monic_input, result.leading_coefficient, input, + arithmetic_context)) { return result; } 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 new file mode 100644 index 000000000..ad5b995b9 --- /dev/null +++ b/libs/math/include/nil/crypto3/math/polynomial/equal_degree_factorization.hpp @@ -0,0 +1,82 @@ +//---------------------------------------------------------------------------// +// 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_EQUAL_DEGREE_FACTORIZATION_HPP +#define CRYPTO3_MATH_EQUAL_DEGREE_FACTORIZATION_HPP + +#include +#include + +#include + +namespace nil::crypto3::math { + + namespace detail { + + /** + * Prepare one group produced by distinct-degree factorization for equal-degree splitting. The input is + * normalized to monic form and its original leading coefficient is returned separately. Constant inputs + * return false because they contain no factors. + * + * Equal-degree factorization requires a square-free input whose irreducible factors all have + * irreducible_factor_degree. This function checks square-freeness and the necessary divisibility of the total + * degree. The caller must supply a group produced by distinct-degree factorization. Verifying the degree of + * every irreducible factor here would require repeating that factorization, so this function does not perform + * that check. + * + * For example, suppose distinct-degree factorization produces the group G = Q1 * Q2 * Q3, where Q1, Q2, + * and Q3 are distinct irreducible quadratic polynomials. Equal-degree factorization of G with + * irreducible_factor_degree = 2 separates that group into Q1, Q2, and Q3. Splitting stops when a resulting + * piece has degree two: under the equal-degree precondition, that piece is one of Q1, Q2, or Q3 and is already + * irreducible. + * + * @throws std::invalid_argument if irreducible_factor_degree is zero, the input is not square-free, or its + * total degree is not divisible by irreducible_factor_degree. + * @pre input is a nonempty coefficient polynomial. + */ + template + bool prepare_equal_degree_factorization_input( + typename Backend::polynomial_type &monic_input, + typename Backend::polynomial_type::value_type &leading_coefficient, + const typename Backend::polynomial_type &input, std::size_t irreducible_factor_degree, + polynomial_arithmetic::polynomial_context &arithmetic_context) { + if (irreducible_factor_degree == 0) { + throw std::invalid_argument("equal-degree factorization requires a positive factor degree"); + } + if (!prepare_square_free_factorization_input(monic_input, leading_coefficient, input, + arithmetic_context)) { + return false; + } + if ((monic_input.size() - 1) % irreducible_factor_degree != 0) { + throw std::invalid_argument( + "equal-degree factorization requires the factor degree to divide the polynomial degree"); + } + return true; + } + + } // namespace detail + +} // namespace nil::crypto3::math + +#endif // CRYPTO3_MATH_EQUAL_DEGREE_FACTORIZATION_HPP diff --git a/libs/math/include/nil/crypto3/math/polynomial/kaltofen_shoup_distinct_degree_factorization.hpp b/libs/math/include/nil/crypto3/math/polynomial/kaltofen_shoup_distinct_degree_factorization.hpp index 41c112a69..460563e61 100644 --- a/libs/math/include/nil/crypto3/math/polynomial/kaltofen_shoup_distinct_degree_factorization.hpp +++ b/libs/math/include/nil/crypto3/math/polynomial/kaltofen_shoup_distinct_degree_factorization.hpp @@ -378,8 +378,8 @@ namespace nil::crypto3::math { result_type result; polynomial_type monic_input; - if (!detail::prepare_distinct_degree_factorization_input(monic_input, result.leading_coefficient, - input, arithmetic_context)) { + if (!detail::prepare_square_free_factorization_input(monic_input, result.leading_coefficient, input, + arithmetic_context)) { return result; } diff --git a/libs/math/include/nil/crypto3/math/polynomial/polynomial_factorization.hpp b/libs/math/include/nil/crypto3/math/polynomial/polynomial_factorization.hpp index 74a212ec3..c2d12443e 100644 --- a/libs/math/include/nil/crypto3/math/polynomial/polynomial_factorization.hpp +++ b/libs/math/include/nil/crypto3/math/polynomial/polynomial_factorization.hpp @@ -26,9 +26,11 @@ #define CRYPTO3_MATH_POLYNOMIAL_FACTORIZATION_HPP #include +#include #include #include +#include #include namespace nil::crypto3::math { @@ -102,6 +104,38 @@ namespace nil::crypto3::math { namespace detail { + /** + * Normalize a factorization input and verify that every irreducible factor has multiplicity one. The + * original leading coefficient is returned separately. Constant inputs return false; nonconstant + * square-free inputs return true and monic_input receives their monic form. + * + * @throws std::invalid_argument if a nonconstant input is not square-free. + * @pre input is a nonempty coefficient polynomial. + */ + template + bool prepare_square_free_factorization_input( + typename Backend::polynomial_type &monic_input, + typename Backend::polynomial_type::value_type &leading_coefficient, + const typename Backend::polynomial_type &input, + polynomial_arithmetic::polynomial_context &arithmetic_context) { + monic_input = input; + condense(monic_input); + leading_coefficient = monic_input.back(); + if (monic_input.size() == 1) { + return false; + } + make_monic(monic_input, monic_input); + + typename Backend::polynomial_type input_derivative; + derivative(input_derivative, monic_input); + typename Backend::polynomial_type repeated_factor; + gcd(repeated_factor, monic_input, input_derivative, arithmetic_context); + if (repeated_factor.size() > 1) { + throw std::invalid_argument("factorization requires a square-free polynomial"); + } + return true; + } + template void factorization_exact_quotient(typename Backend::polynomial_type &output, const typename Backend::polynomial_type ÷nd, diff --git a/libs/math/test/CMakeLists.txt b/libs/math/test/CMakeLists.txt index fc36457a8..a078ba5d8 100644 --- a/libs/math/test/CMakeLists.txt +++ b/libs/math/test/CMakeLists.txt @@ -41,6 +41,7 @@ endmacro() set(TESTS_NAMES "batch_inverse" "distinct_degree_factorization" + "equal_degree_factorization" "evaluation_domain" "geometric_sequence_domain" "integer_sqrt" diff --git a/libs/math/test/equal_degree_factorization.cpp b/libs/math/test/equal_degree_factorization.cpp new file mode 100644 index 000000000..576ab6413 --- /dev/null +++ b/libs/math/test/equal_degree_factorization.cpp @@ -0,0 +1,113 @@ +//---------------------------------------------------------------------------// +// 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 equal_degree_factorization_test + +#include + +#include + +#include +#include + +#include +#include + +namespace { + namespace math = nil::crypto3::math; + namespace polynomial_arithmetic = math::polynomial_arithmetic; + namespace fields = nil::crypto3::algebra::fields; + + using field_type = fields::alt_bn128_base_field<254>; + using value_type = field_type::value_type; + using backend_type = polynomial_arithmetic::schoolbook_backend; + using polynomial_type = typename backend_type::polynomial_type; +} // namespace + +BOOST_AUTO_TEST_SUITE(equal_degree_factorization_test_suite) + +BOOST_AUTO_TEST_CASE(preparation_normalizes_a_square_free_equal_degree_input) { + backend_type backend; + const polynomial_type first_factor = {-value_type(3), value_type::zero(), value_type::one()}; + // Three is a nonsquare in BN254 Fq. Multiplying it by the nonzero square four preserves that property, so both + // quadratics are irreducible. + const polynomial_type second_factor = {-value_type(12), value_type::zero(), value_type::one()}; + polynomial_type input; + backend.multiply(input, first_factor, second_factor); + + const value_type leading_coefficient(11); + for (value_type &coefficient : input) { + coefficient *= leading_coefficient; + } + + polynomial_arithmetic::polynomial_context arithmetic_context; + polynomial_type monic_input; + value_type recovered_leading_coefficient; + const bool has_factors = math::detail::prepare_equal_degree_factorization_input( + monic_input, recovered_leading_coefficient, input, 2, arithmetic_context); + + polynomial_type expected; + backend.multiply(expected, first_factor, second_factor); + BOOST_CHECK(has_factors); + BOOST_CHECK(recovered_leading_coefficient == leading_coefficient); + BOOST_CHECK(monic_input == expected); +} + +BOOST_AUTO_TEST_CASE(preparation_rejects_invalid_equal_degree_inputs) { + backend_type backend; + const polynomial_type linear_factor = {value_type(1), value_type::one()}; + const polynomial_type quadratic_factor = {-value_type(3), value_type::zero(), value_type::one()}; + polynomial_type mixed_degree_input; + backend.multiply(mixed_degree_input, linear_factor, quadratic_factor); + + polynomial_arithmetic::polynomial_context arithmetic_context; + polynomial_type monic_input; + value_type leading_coefficient; + BOOST_CHECK_THROW(math::detail::prepare_equal_degree_factorization_input( + monic_input, leading_coefficient, mixed_degree_input, 0, arithmetic_context), + std::invalid_argument); + BOOST_CHECK_THROW(math::detail::prepare_equal_degree_factorization_input( + monic_input, leading_coefficient, mixed_degree_input, 2, arithmetic_context), + std::invalid_argument); + + polynomial_type repeated_input; + backend.multiply(repeated_input, linear_factor, linear_factor); + BOOST_CHECK_THROW(math::detail::prepare_equal_degree_factorization_input( + monic_input, leading_coefficient, repeated_input, 1, arithmetic_context), + std::invalid_argument); +} + +BOOST_AUTO_TEST_CASE(preparation_reports_that_constants_have_no_factors) { + polynomial_arithmetic::polynomial_context arithmetic_context; + polynomial_type monic_input; + value_type leading_coefficient; + const bool has_factors = math::detail::prepare_equal_degree_factorization_input( + monic_input, leading_coefficient, polynomial_type {value_type(13)}, 1, arithmetic_context); + + BOOST_CHECK(!has_factors); + BOOST_CHECK(leading_coefficient == value_type(13)); + BOOST_CHECK(monic_input == polynomial_type({value_type(13)})); +} + +BOOST_AUTO_TEST_SUITE_END() From 7bbfe5d6966aa57975f5ce111c7074bc3103d37a Mon Sep 17 00:00:00 2001 From: Riccardo Abbate Date: Mon, 24 Aug 2026 14:00:02 -0400 Subject: [PATCH 2/5] one non trivial Cantor-Zassenhaus split --- .../polynomial/equal_degree_factorization.hpp | 138 ++++++++++++++++++ libs/math/test/equal_degree_factorization.cpp | 127 ++++++++++++++++ 2 files changed, 265 insertions(+) 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 ad5b995b9..7cb1692f7 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 @@ -25,15 +25,153 @@ #ifndef CRYPTO3_MATH_EQUAL_DEGREE_FACTORIZATION_HPP #define CRYPTO3_MATH_EQUAL_DEGREE_FACTORIZATION_HPP +#include #include #include +#include + +#include + #include +#include namespace nil::crypto3::math { namespace detail { + /** + * Sample a canonical polynomial whose degree is less than coefficient_count. Each coefficient is obtained + * directly from the caller-owned generator, so the caller controls both the random source and its seed. + * + * Cantor-Zassenhaus splits an equal-degree group G by sampling an element of the quotient ring F[X]/(G). + * Every quotient-ring element has a unique polynomial representative of degree below degree(G), which is why + * the algorithm samples this many coefficients. A random scalar is not sufficient because it has the same + * residue modulo every irreducible factor of G and therefore cannot generally separate those factors. + * The highest sampled coefficient may be zero: lower-degree and constant representatives are still valid + * quotient-ring elements. The splitting loop retries whenever a sampled element does not produce a nontrivial + * factor. + * + * @throws std::invalid_argument if coefficient_count is zero. + */ + template + requires std::constructible_from && + requires(Polynomial &polynomial, Generator &generator) { polynomial[0] = generator(); } + Polynomial sample_random_polynomial(std::size_t coefficient_count, Generator &generator) { + if (coefficient_count == 0) { + throw std::invalid_argument("random polynomial sampling requires a positive coefficient count"); + } + + Polynomial result(coefficient_count); + for (std::size_t index = 0; index < coefficient_count; ++index) { + result[index] = generator(); + } + condense(result); + return result; + } + + /** Precompute the exponent shared by all Cantor-Zassenhaus trials for one irreducible-factor degree. */ + template + class cantor_zassenhaus_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; + + explicit cantor_zassenhaus_context(std::size_t irreducible_factor_degree) : + irreducible_factor_degree_(irreducible_factor_degree) { + if (irreducible_factor_degree_ == 0) { + throw std::invalid_argument("Cantor-Zassenhaus splitting requires a positive factor degree"); + } + if (algebra::fields::field_characteristic() == 2) { + throw std::invalid_argument( + "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; + } + + std::size_t irreducible_factor_degree() const { + return irreducible_factor_degree_; + } + + const boost::multiprecision::cpp_int &exponent() const { + return exponent_; + } + + private: + std::size_t irreducible_factor_degree_; + boost::multiprecision::cpp_int exponent_; + }; + + /** + * Try once to split a product G of distinct irreducible polynomials that all have the same degree, + * irreducible_factor_degree, using the odd-characteristic Cantor-Zassenhaus algorithm. One random polynomial a + * is sampled with degree below degree(G). If gcd(a, G) is already a proper factor, it is returned immediately. + * Otherwise compute + * + * quadratic_character = a^((Q^irreducible_factor_degree - 1) / 2) mod G, + * + * where Q is the coefficient field's order and d = irreducible_factor_degree. For every irreducible degree-d + * factor Qi, the quotient F[X]/(Qi) is a field with Q^d elements, and its nonzero multiplicative group has + * order Q^d - 1. Raising a nonzero residue to half that order gives a value whose square is 1, so in odd + * characteristic it is either 1 or -1. Thus, modulo each irreducible factor of G, quadratic_character is + * either 1 or -1, and gcd(quadratic_character - 1, G) selects the factors on which it is 1. The trial succeeds + * when this GCD is neither 1 nor G. A failed trial sets factor to zero and returns false; it does not sample + * again. + * + * cantor_zassenhaus_context supplies the factor degree and precomputed exponent shared by every trial. + * divisor_context is supplied separately so its polynomial inverse can be reused by subsequent trials against + * this particular G. Its divisor must be monic and square-free, all its irreducible factors must have the + * context's irreducible_factor_degree, and it must contain at least two such factors. + * + * @throws std::invalid_argument if the divisor degree is inconsistent with the requested factor degree or the + * divisor is not monic. + */ + template + bool try_cantor_zassenhaus_split(typename Backend::polynomial_type &factor, + const cantor_zassenhaus_context &split_context, + const polynomial_divisor_context &divisor_context, + polynomial_arithmetic::polynomial_context &arithmetic_context, + Generator &generator) { + using polynomial_type = typename Backend::polynomial_type; + using value_type = typename polynomial_type::value_type; + + const std::size_t irreducible_factor_degree = split_context.irreducible_factor_degree(); + const std::size_t group_degree = divisor_context.degree(); + if (group_degree <= irreducible_factor_degree || group_degree % irreducible_factor_degree != 0) { + throw std::invalid_argument( + "Cantor-Zassenhaus splitting requires at least two factors of the requested degree"); + } + const polynomial_type &group = divisor_context.divisor(); + if (group.back() != value_type::one()) { + throw std::invalid_argument("Cantor-Zassenhaus splitting requires a monic polynomial"); + } + polynomial_type random_polynomial = sample_random_polynomial(group_degree, generator); + gcd(factor, random_polynomial, group, arithmetic_context); + if (factor.size() > 1 && factor.size() < group.size()) { + return true; + } + + polynomial_type quadratic_character; + powmod(quadratic_character, random_polynomial, split_context.exponent(), divisor_context, + arithmetic_context); + quadratic_character[0] = quadratic_character[0] - value_type::one(); + condense(quadratic_character); + gcd(factor, quadratic_character, group, arithmetic_context); + if (factor.size() > 1 && factor.size() < group.size()) { + return true; + } + + factor.assign(1, value_type::zero()); + return false; + } + /** * Prepare one group produced by distinct-degree factorization for equal-degree splitting. The input is * normalized to monic form and its original leading coefficient is returned separately. Constant inputs diff --git a/libs/math/test/equal_degree_factorization.cpp b/libs/math/test/equal_degree_factorization.cpp index 576ab6413..018e03184 100644 --- a/libs/math/test/equal_degree_factorization.cpp +++ b/libs/math/test/equal_degree_factorization.cpp @@ -24,15 +24,18 @@ #define BOOST_TEST_MODULE equal_degree_factorization_test +#include #include #include #include #include +#include #include #include +#include namespace { namespace math = nil::crypto3::math; @@ -41,12 +44,136 @@ namespace { using field_type = fields::alt_bn128_base_field<254>; using value_type = field_type::value_type; + using extension_field_type = fields::fp12_2over3over2>; + using extension_value_type = extension_field_type::value_type; using backend_type = polynomial_arithmetic::schoolbook_backend; using polynomial_type = typename backend_type::polynomial_type; } // namespace BOOST_AUTO_TEST_SUITE(equal_degree_factorization_test_suite) +BOOST_AUTO_TEST_CASE(random_polynomial_sampling_is_seeded) { + nil::crypto3::random::algebraic_engine first_generator(17); + nil::crypto3::random::algebraic_engine second_generator(17); + + const polynomial_type first = math::detail::sample_random_polynomial(8, first_generator); + const polynomial_type second = math::detail::sample_random_polynomial(8, second_generator); + + BOOST_CHECK(first == second); + BOOST_CHECK_LE(first.size(), 8); + BOOST_CHECK_THROW(math::detail::sample_random_polynomial(0, first_generator), + std::invalid_argument); +} + +BOOST_AUTO_TEST_CASE(random_polynomial_sampling_returns_canonical_polynomials) { + const std::array coefficients = {value_type(2), value_type(3), value_type::zero(), value_type::zero()}; + std::size_t next_coefficient = 0; + auto generator = [&] { return coefficients[next_coefficient++]; }; + + const polynomial_type sampled = math::detail::sample_random_polynomial(4, generator); + + BOOST_CHECK(sampled == polynomial_type({value_type(2), value_type(3)})); + BOOST_CHECK_EQUAL(next_coefficient, coefficients.size()); +} + +BOOST_AUTO_TEST_CASE(random_polynomial_sampling_supports_extension_field_coefficients) { + using extension_polynomial_type = math::polynomial; + + nil::crypto3::random::algebraic_engine first_generator(29); + nil::crypto3::random::algebraic_engine second_generator(29); + const extension_polynomial_type first = + math::detail::sample_random_polynomial(5, first_generator); + const extension_polynomial_type second = + math::detail::sample_random_polynomial(5, second_generator); + + BOOST_CHECK(first == second); + BOOST_CHECK_LE(first.size(), 5); +} + +BOOST_AUTO_TEST_CASE(one_cantor_zassenhaus_trial_finds_a_proper_factor) { + backend_type backend; + const polynomial_type first_factor = {-value_type::one(), value_type::one()}; + const polynomial_type second_factor = {value_type::one(), value_type::one()}; + polynomial_type group; + backend.multiply(group, first_factor, second_factor); + + polynomial_arithmetic::polynomial_context arithmetic_context; + math::polynomial_divisor_context divisor_context(group, group.size() - 1, arithmetic_context); + math::detail::cantor_zassenhaus_context split_context(1); + + const std::array coefficients = {value_type::zero(), value_type::one()}; + std::size_t next_coefficient = 0; + auto generator = [&] { return coefficients[next_coefficient++]; }; + polynomial_type factor; + const bool split = math::detail::try_cantor_zassenhaus_split(factor, split_context, divisor_context, + arithmetic_context, generator); + + BOOST_CHECK(split); + BOOST_CHECK(factor == first_factor || factor == second_factor); + BOOST_CHECK_EQUAL(next_coefficient, coefficients.size()); +} + +BOOST_AUTO_TEST_CASE(one_cantor_zassenhaus_trial_reports_an_unsuccessful_sample) { + backend_type backend; + const polynomial_type first_factor = {-value_type::one(), value_type::one()}; + const polynomial_type second_factor = {value_type::one(), value_type::one()}; + polynomial_type group; + backend.multiply(group, first_factor, second_factor); + + polynomial_arithmetic::polynomial_context arithmetic_context; + math::polynomial_divisor_context divisor_context(group, group.size() - 1, arithmetic_context); + math::detail::cantor_zassenhaus_context split_context(1); + + const std::array coefficients = {value_type::one(), value_type::zero()}; + std::size_t next_coefficient = 0; + auto generator = [&] { return coefficients[next_coefficient++]; }; + polynomial_type factor; + const bool split = math::detail::try_cantor_zassenhaus_split(factor, split_context, divisor_context, + arithmetic_context, generator); + + BOOST_CHECK(!split); + BOOST_CHECK(factor == polynomial_type({value_type::zero()})); +} + +BOOST_AUTO_TEST_CASE(cantor_zassenhaus_trial_rejects_invalid_inputs_before_sampling) { + BOOST_CHECK_THROW(static_cast(math::detail::cantor_zassenhaus_context(0)), + std::invalid_argument); + + polynomial_arithmetic::polynomial_context arithmetic_context; + polynomial_type factor; + std::size_t generated_coefficient_count = 0; + auto generator = [&] { + ++generated_coefficient_count; + return value_type::one(); + }; + + const polynomial_type degree_two_group = {-value_type::one(), value_type::zero(), value_type::one()}; + math::polynomial_divisor_context degree_two_divisor(degree_two_group, degree_two_group.size() - 1, + arithmetic_context); + math::detail::cantor_zassenhaus_context degree_two_split_context(2); + BOOST_CHECK_THROW(math::detail::try_cantor_zassenhaus_split( + factor, degree_two_split_context, degree_two_divisor, arithmetic_context, generator), + std::invalid_argument); + + const polynomial_type degree_three_group = {value_type::one(), value_type::zero(), value_type::zero(), + value_type::one()}; + math::polynomial_divisor_context degree_three_divisor( + degree_three_group, degree_three_group.size() - 1, arithmetic_context); + BOOST_CHECK_THROW(math::detail::try_cantor_zassenhaus_split( + factor, degree_two_split_context, degree_three_divisor, arithmetic_context, generator), + std::invalid_argument); + + const polynomial_type nonmonic_group = {-value_type(2), value_type::zero(), value_type(2)}; + math::polynomial_divisor_context nonmonic_divisor(nonmonic_group, nonmonic_group.size() - 1, + arithmetic_context); + math::detail::cantor_zassenhaus_context degree_one_split_context(1); + BOOST_CHECK_THROW(math::detail::try_cantor_zassenhaus_split( + factor, degree_one_split_context, nonmonic_divisor, arithmetic_context, generator), + std::invalid_argument); + + BOOST_CHECK_EQUAL(generated_coefficient_count, 0); +} + BOOST_AUTO_TEST_CASE(preparation_normalizes_a_square_free_equal_degree_input) { backend_type backend; const polynomial_type first_factor = {-value_type(3), value_type::zero(), value_type::one()}; From 6ab55ecb853ba166559e960b07c8f43804b66227 Mon Sep 17 00:00:00 2001 From: Riccardo Abbate Date: Mon, 24 Aug 2026 14:08:26 -0400 Subject: [PATCH 3/5] Repeatedly split until every factor has degree d. --- .../polynomial/equal_degree_factorization.hpp | 56 +++++++++++++++++ libs/math/test/equal_degree_factorization.cpp | 61 +++++++++++++++++++ 2 files changed, 117 insertions(+) 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 7cb1692f7..69deb1f4f 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 @@ -28,6 +28,7 @@ #include #include #include +#include #include @@ -172,6 +173,61 @@ namespace nil::crypto3::math { return false; } + /** + * Repeatedly apply Cantor-Zassenhaus splitting until every returned factor has + * split_context.irreducible_factor_degree(). An explicit worklist avoids recursion depth proportional to the + * number of factors. Each composite subgroup gets one divisor context that is reused across all failed trials + * against that subgroup. + * + * A successful trial divides the current subgroup into the returned proper factor and its exact quotient, then + * places both pieces back on the worklist. A piece whose degree equals the requested irreducible-factor degree + * is already irreducible under the equal-degree input precondition and is moved to the result. + * + * @pre group is monic and square-free, all its irreducible factors have the context's factor degree, and + * generator supplies independent uniformly distributed coefficient-field elements. + */ + template + std::vector cantor_zassenhaus_split_all( + typename Backend::polynomial_type group, const cantor_zassenhaus_context &split_context, + polynomial_arithmetic::polynomial_context &arithmetic_context, Generator &generator) { + using polynomial_type = typename Backend::polynomial_type; + + std::vector factors; + std::vector pending; + pending.push_back(std::move(group)); + + while (!pending.empty()) { + polynomial_type current = std::move(pending.back()); + pending.pop_back(); + + const std::size_t current_degree = current.size() - 1; + if (current_degree < split_context.irreducible_factor_degree() || + current_degree % split_context.irreducible_factor_degree() != 0) { + throw std::invalid_argument( + "Cantor-Zassenhaus splitting requires the factor degree to divide every pending degree"); + } + if (current_degree == split_context.irreducible_factor_degree()) { + factors.push_back(std::move(current)); + continue; + } + + polynomial_divisor_context divisor_context(current, current_degree - 1, arithmetic_context); + polynomial_type factor; + // The condition performs one complete random trial. Failure requires no state update other than the + // generator advancing, so the loop body is intentionally empty and the next condition retries. + while (!try_cantor_zassenhaus_split(factor, split_context, divisor_context, arithmetic_context, + generator)) { + } + + polynomial_type quotient; + factorization_exact_quotient(quotient, current, factor, arithmetic_context); + pending.push_back(std::move(factor)); + pending.push_back(std::move(quotient)); + } + + return factors; + } + /** * Prepare one group produced by distinct-degree factorization for equal-degree splitting. The input is * normalized to monic form and its original leading coefficient is returned separately. Constant inputs diff --git a/libs/math/test/equal_degree_factorization.cpp b/libs/math/test/equal_degree_factorization.cpp index 018e03184..3793bff36 100644 --- a/libs/math/test/equal_degree_factorization.cpp +++ b/libs/math/test/equal_degree_factorization.cpp @@ -26,6 +26,8 @@ #include #include +#include +#include #include @@ -174,6 +176,65 @@ BOOST_AUTO_TEST_CASE(cantor_zassenhaus_trial_rejects_invalid_inputs_before_sampl BOOST_CHECK_EQUAL(generated_coefficient_count, 0); } +BOOST_AUTO_TEST_CASE(cantor_zassenhaus_retries_and_splits_until_every_factor_has_the_requested_degree) { + backend_type backend; + const polynomial_type first_factor = {-value_type(1), value_type::one()}; + const polynomial_type second_factor = {-value_type(2), value_type::one()}; + const polynomial_type third_factor = {-value_type(3), value_type::one()}; + polynomial_type group; + backend.multiply(group, first_factor, second_factor); + polynomial_type complete_group; + backend.multiply(complete_group, group, third_factor); + group = std::move(complete_group); + + // The constant samples 1 fail. The following X - 1 and X - 2 samples share a proper factor with the current + // subgroup, so their early GCDs split it without exponentiation. + const std::array coefficients = { + value_type::one(), value_type::zero(), value_type::zero(), -value_type::one(), value_type::one(), + value_type::zero(), value_type::one(), value_type::zero(), -value_type(2), value_type::one(), + }; + std::size_t next_coefficient = 0; + auto generator = [&] { + if (next_coefficient == coefficients.size()) { + throw std::runtime_error("Cantor-Zassenhaus consumed more test coefficients than expected"); + } + return coefficients[next_coefficient++]; + }; + + polynomial_arithmetic::polynomial_context arithmetic_context; + math::detail::cantor_zassenhaus_context split_context(1); + const std::vector factors = + math::detail::cantor_zassenhaus_split_all(group, split_context, arithmetic_context, generator); + + BOOST_REQUIRE_EQUAL(factors.size(), 3); + polynomial_type reconstructed = {value_type::one()}; + for (const polynomial_type &factor : factors) { + BOOST_CHECK_EQUAL(factor.degree(), 1); + polynomial_type product; + backend.multiply(product, reconstructed, factor); + reconstructed = std::move(product); + } + BOOST_CHECK(reconstructed == group); + BOOST_CHECK_EQUAL(next_coefficient, coefficients.size()); +} + +BOOST_AUTO_TEST_CASE(cantor_zassenhaus_does_not_sample_an_already_irreducible_group) { + const polynomial_type group = {-value_type(3), value_type::zero(), value_type::one()}; + std::size_t generated_coefficient_count = 0; + auto generator = [&] { + ++generated_coefficient_count; + return value_type::one(); + }; + + polynomial_arithmetic::polynomial_context arithmetic_context; + math::detail::cantor_zassenhaus_context split_context(2); + const std::vector factors = + math::detail::cantor_zassenhaus_split_all(group, split_context, arithmetic_context, generator); + + BOOST_CHECK(factors == std::vector({group})); + BOOST_CHECK_EQUAL(generated_coefficient_count, 0); +} + BOOST_AUTO_TEST_CASE(preparation_normalizes_a_square_free_equal_degree_input) { backend_type backend; const polynomial_type first_factor = {-value_type(3), value_type::zero(), value_type::one()}; From 121ae9bdc42ab9c0974c8d31f9539c1f5ace5634 Mon Sep 17 00:00:00 2001 From: Riccardo Abbate Date: Mon, 24 Aug 2026 14:23:00 -0400 Subject: [PATCH 4/5] public api --- .../polynomial/equal_degree_factorization.hpp | 98 +++++++++++++- libs/math/test/equal_degree_factorization.cpp | 120 ++++++++++++++++++ 2 files changed, 212 insertions(+), 6 deletions(-) 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 69deb1f4f..0ac0054b9 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 @@ -41,6 +41,14 @@ namespace nil::crypto3::math { namespace detail { + /** A callback that controls staged equal-degree factorization after receiving one irreducible factor. */ + template + concept EqualDegreeFactorCallback = + CoefficientPolynomial && + requires(FactorCallback &callback, const polynomial_factor &factor) { + { callback(factor) } -> std::same_as; + }; + /** * Sample a canonical polynomial whose degree is less than coefficient_count. Each coefficient is obtained * directly from the caller-owned generator, so the caller controls both the random source and its seed. @@ -186,13 +194,14 @@ namespace nil::crypto3::math { * @pre group is monic and square-free, all its irreducible factors have the context's factor degree, and * generator supplies independent uniformly distributed coefficient-field elements. */ - template - std::vector cantor_zassenhaus_split_all( - typename Backend::polynomial_type group, const cantor_zassenhaus_context &split_context, - polynomial_arithmetic::polynomial_context &arithmetic_context, Generator &generator) { + template + factorization_control + cantor_zassenhaus_split_all(typename Backend::polynomial_type group, + const cantor_zassenhaus_context &split_context, + polynomial_arithmetic::polynomial_context &arithmetic_context, + Generator &generator, FactorCallback &&factor_callback) { using polynomial_type = typename Backend::polynomial_type; - std::vector factors; std::vector pending; pending.push_back(std::move(group)); @@ -207,7 +216,9 @@ namespace nil::crypto3::math { "Cantor-Zassenhaus splitting requires the factor degree to divide every pending degree"); } if (current_degree == split_context.irreducible_factor_degree()) { - factors.push_back(std::move(current)); + if (factor_callback(std::move(current)) == factorization_control::stop_factorization) { + return factorization_control::stop_factorization; + } continue; } @@ -225,6 +236,22 @@ namespace nil::crypto3::math { pending.push_back(std::move(quotient)); } + return factorization_control::continue_factorization; + } + + /** Split an entire equal-degree group and collect every irreducible factor. */ + template + std::vector cantor_zassenhaus_split_all( + typename Backend::polynomial_type group, const cantor_zassenhaus_context &split_context, + polynomial_arithmetic::polynomial_context &arithmetic_context, Generator &generator) { + using polynomial_type = typename Backend::polynomial_type; + + std::vector factors; + cantor_zassenhaus_split_all(std::move(group), split_context, arithmetic_context, generator, + [&](polynomial_type &&factor) { + factors.push_back(std::move(factor)); + return factorization_control::continue_factorization; + }); return factors; } @@ -271,6 +298,65 @@ namespace nil::crypto3::math { } // namespace detail + /** + * Split one square-free distinct-degree group into its monic irreducible factors using the odd-characteristic + * Cantor-Zassenhaus algorithm. Every irreducible input factor must have irreducible_factor_degree; callers normally + * obtain this pair from distinct-degree factorization. The necessary total-degree divisibility is checked, but the + * function does not repeat distinct-degree factorization to verify this precondition. + * + * The input is normalized to monic form and its original leading coefficient is preserved in the result. Zero and + * constant inputs return that coefficient and no factors. Complete results contain every irreducible factor with + * multiplicity one and reconstruct the input using polynomial_factorization_result's usual convention. + * + * generator must return independent uniformly distributed coefficient-field elements. After each factor is added + * to the result, factor_callback may request an early stop. A stopped result contains that factor, has complete set + * to false, and does not continue splitting pending subgroups. + * + * @throws std::invalid_argument if the factor degree is zero, a nonconstant input is not square-free, its degree is + * not divisible by the factor degree, or the coefficient field has characteristic two. + * @pre input is a nonempty coefficient polynomial whose irreducible factors all have + * irreducible_factor_degree. + */ + template + requires detail::EqualDegreeFactorCallback + polynomial_factorization_result + equal_degree_factorization(const typename Backend::polynomial_type &input, + std::size_t irreducible_factor_degree, + polynomial_arithmetic::polynomial_context &arithmetic_context, + Generator &generator, FactorCallback &&factor_callback) { + using polynomial_type = typename Backend::polynomial_type; + using result_type = polynomial_factorization_result; + + result_type result; + polynomial_type monic_input; + if (!detail::prepare_equal_degree_factorization_input(monic_input, result.leading_coefficient, input, + irreducible_factor_degree, arithmetic_context)) { + return result; + } + + detail::cantor_zassenhaus_context split_context(irreducible_factor_degree); + const factorization_control control = detail::cantor_zassenhaus_split_all( + std::move(monic_input), split_context, arithmetic_context, generator, [&](polynomial_type &&factor) { + result.factors.push_back({std::move(factor), 1}); + return factor_callback(result.factors.back()); + }); + if (control == factorization_control::stop_factorization) { + result.complete = false; + } + return result; + } + + /** Compute the complete equal-degree factorization without a staged callback. */ + template + polynomial_factorization_result equal_degree_factorization( + const typename Backend::polynomial_type &input, std::size_t irreducible_factor_degree, + polynomial_arithmetic::polynomial_context &arithmetic_context, Generator &generator) { + using factor_type = polynomial_factor; + return equal_degree_factorization( + input, irreducible_factor_degree, arithmetic_context, generator, + [](const factor_type &) { return factorization_control::continue_factorization; }); + } + } // namespace nil::crypto3::math #endif // CRYPTO3_MATH_EQUAL_DEGREE_FACTORIZATION_HPP diff --git a/libs/math/test/equal_degree_factorization.cpp b/libs/math/test/equal_degree_factorization.cpp index 3793bff36..5334a6462 100644 --- a/libs/math/test/equal_degree_factorization.cpp +++ b/libs/math/test/equal_degree_factorization.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include @@ -135,6 +136,7 @@ BOOST_AUTO_TEST_CASE(one_cantor_zassenhaus_trial_reports_an_unsuccessful_sample) BOOST_CHECK(!split); BOOST_CHECK(factor == polynomial_type({value_type::zero()})); + BOOST_CHECK_EQUAL(next_coefficient, coefficients.size()); } BOOST_AUTO_TEST_CASE(cantor_zassenhaus_trial_rejects_invalid_inputs_before_sampling) { @@ -173,6 +175,10 @@ BOOST_AUTO_TEST_CASE(cantor_zassenhaus_trial_rejects_invalid_inputs_before_sampl factor, degree_one_split_context, nonmonic_divisor, arithmetic_context, generator), std::invalid_argument); + BOOST_CHECK_THROW(math::detail::cantor_zassenhaus_split_all( + polynomial_type {value_type::one()}, degree_one_split_context, arithmetic_context, generator), + std::invalid_argument); + BOOST_CHECK_EQUAL(generated_coefficient_count, 0); } @@ -235,6 +241,120 @@ BOOST_AUTO_TEST_CASE(cantor_zassenhaus_does_not_sample_an_already_irreducible_gr BOOST_CHECK_EQUAL(generated_coefficient_count, 0); } +BOOST_AUTO_TEST_CASE(cantor_zassenhaus_is_deterministic_for_a_seeded_fq_generator) { + backend_type backend; + const polynomial_type first_factor = {-value_type(1), value_type::one()}; + const polynomial_type second_factor = {-value_type(2), value_type::one()}; + const polynomial_type third_factor = {-value_type(3), value_type::one()}; + polynomial_type first_product; + backend.multiply(first_product, first_factor, second_factor); + polynomial_type group; + backend.multiply(group, first_product, third_factor); + const value_type leading_coefficient(7); + polynomial_type input(group); + for (value_type &coefficient : input) { + coefficient *= leading_coefficient; + } + + auto factor_with_seed = [&input](std::uint32_t seed) { + nil::crypto3::random::algebraic_engine generator(seed); + polynomial_arithmetic::polynomial_context arithmetic_context; + return math::equal_degree_factorization(input, 1, arithmetic_context, generator); + }; + + const math::polynomial_factorization_result first = factor_with_seed(41); + const math::polynomial_factorization_result second = factor_with_seed(41); + BOOST_CHECK(first == second); + BOOST_CHECK(first.complete); + BOOST_CHECK(first.leading_coefficient == leading_coefficient); + BOOST_REQUIRE_EQUAL(first.factors.size(), 3); + + polynomial_type reconstructed = {first.leading_coefficient}; + for (const auto &factor : first.factors) { + BOOST_CHECK_EQUAL(factor.polynomial.degree(), 1); + BOOST_CHECK_EQUAL(factor.multiplicity, 1); + polynomial_type product; + backend.multiply(product, reconstructed, factor.polynomial); + reconstructed = std::move(product); + } + BOOST_CHECK(reconstructed == input); +} + +BOOST_AUTO_TEST_CASE(equal_degree_factorization_stops_after_the_reported_factor) { + backend_type backend; + const polynomial_type first_factor = {-value_type(1), value_type::one()}; + const polynomial_type second_factor = {-value_type(2), value_type::one()}; + const polynomial_type third_factor = {-value_type(3), value_type::one()}; + polynomial_type first_product; + backend.multiply(first_product, first_factor, second_factor); + polynomial_type group; + backend.multiply(group, first_product, third_factor); + + const std::array coefficients = {-value_type::one(), value_type::one(), value_type::zero(), -value_type(2), + value_type::one()}; + std::size_t next_coefficient = 0; + auto generator = [&] { return coefficients[next_coefficient++]; }; + std::size_t callback_count = 0; + auto callback = [&callback_count](const math::polynomial_factor &) { + ++callback_count; + return math::factorization_control::stop_factorization; + }; + + polynomial_arithmetic::polynomial_context arithmetic_context; + const auto result = + math::equal_degree_factorization(group, 1, arithmetic_context, generator, callback); + + BOOST_CHECK(!result.complete); + BOOST_REQUIRE_EQUAL(result.factors.size(), 1); + BOOST_CHECK_EQUAL(result.factors.front().polynomial.degree(), 1); + BOOST_CHECK_EQUAL(result.factors.front().multiplicity, 1); + BOOST_CHECK_EQUAL(callback_count, 1); + BOOST_CHECK_EQUAL(next_coefficient, coefficients.size()); +} + +BOOST_AUTO_TEST_CASE(cantor_zassenhaus_reconstructs_native_fq12_input_deterministically) { + using extension_backend_type = polynomial_arithmetic::schoolbook_backend; + using extension_polynomial_type = typename extension_backend_type::polynomial_type; + + const extension_value_type one = extension_value_type::one(); + const extension_value_type two = one + one; + const extension_value_type three = two + one; + const extension_polynomial_type first_factor = {-one, one}; + const extension_polynomial_type second_factor = {-two, one}; + const extension_polynomial_type third_factor = {-three, one}; + extension_backend_type backend; + extension_polynomial_type first_product; + backend.multiply(first_product, first_factor, second_factor); + extension_polynomial_type group; + backend.multiply(group, first_product, third_factor); + + const std::array coefficients = {-one, one, extension_value_type::zero(), -two, one}; + auto factor_group = [&] { + std::size_t next_coefficient = 0; + auto generator = [&] { return coefficients[next_coefficient++]; }; + polynomial_arithmetic::polynomial_context arithmetic_context; + auto result = math::equal_degree_factorization(group, 1, arithmetic_context, generator); + BOOST_CHECK_EQUAL(next_coefficient, coefficients.size()); + return result; + }; + + const math::polynomial_factorization_result first = factor_group(); + const math::polynomial_factorization_result second = factor_group(); + BOOST_CHECK(first == second); + BOOST_CHECK(first.complete); + BOOST_REQUIRE_EQUAL(first.factors.size(), 3); + + extension_polynomial_type reconstructed = {first.leading_coefficient}; + for (const auto &factor : first.factors) { + BOOST_CHECK_EQUAL(factor.polynomial.degree(), 1); + BOOST_CHECK_EQUAL(factor.multiplicity, 1); + extension_polynomial_type product; + backend.multiply(product, reconstructed, factor.polynomial); + reconstructed = std::move(product); + } + BOOST_CHECK(reconstructed == group); +} + BOOST_AUTO_TEST_CASE(preparation_normalizes_a_square_free_equal_degree_input) { backend_type backend; const polynomial_type first_factor = {-value_type(3), value_type::zero(), value_type::one()}; From ee08a05f49dec105ee3247b0706e53723dc5e938 Mon Sep 17 00:00:00 2001 From: Riccardo Abbate Date: Mon, 24 Aug 2026 15:45:04 -0400 Subject: [PATCH 5/5] public api useful for full factorization and simplifying some corner cases --- .../polynomial/equal_degree_factorization.hpp | 61 +++++++++++++++++-- libs/math/test/equal_degree_factorization.cpp | 23 ++++--- 2 files changed, 70 insertions(+), 14 deletions(-) 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 0ac0054b9..c474e7cf5 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 @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -58,8 +59,8 @@ namespace nil::crypto3::math { * the algorithm samples this many coefficients. A random scalar is not sufficient because it has the same * residue modulo every irreducible factor of G and therefore cannot generally separate those factors. * The highest sampled coefficient may be zero: lower-degree and constant representatives are still valid - * quotient-ring elements. The splitting loop retries whenever a sampled element does not produce a nontrivial - * factor. + * quotient-ring elements. Cantor-Zassenhaus excludes zero and constant representatives before starting a trial + * because they cannot separate the irreducible factors of G. * * @throws std::invalid_argument if coefficient_count is zero. */ @@ -120,9 +121,10 @@ namespace nil::crypto3::math { /** * Try once to split a product G of distinct irreducible polynomials that all have the same degree, - * irreducible_factor_degree, using the odd-characteristic Cantor-Zassenhaus algorithm. One random polynomial a - * is sampled with degree below degree(G). If gcd(a, G) is already a proper factor, it is returned immediately. - * Otherwise compute + * irreducible_factor_degree, using the odd-characteristic Cantor-Zassenhaus algorithm. Zero and constant random + * candidates are discarded because they cannot separate the factors of G. The trial uses the first remaining + * polynomial a, whose degree is between one and degree(G) - 1. If gcd(a, G) is already a proper factor, it is + * returned immediately. Otherwise compute * * quadratic_character = a^((Q^irreducible_factor_degree - 1) / 2) mod G, * @@ -161,7 +163,12 @@ namespace nil::crypto3::math { if (group.back() != value_type::one()) { throw std::invalid_argument("Cantor-Zassenhaus splitting requires a monic polynomial"); } - polynomial_type random_polynomial = sample_random_polynomial(group_degree, generator); + polynomial_type random_polynomial; + // Zero and constant residues have the same value modulo every irreducible factor, so they cannot split G. + // Resample them before paying for either GCD or modular exponentiation. + do { + random_polynomial = sample_random_polynomial(group_degree, generator); + } while (random_polynomial.size() <= 1); gcd(factor, random_polynomial, group, arithmetic_context); if (factor.size() > 1 && factor.size() < group.size()) { return true; @@ -255,6 +262,48 @@ namespace nil::crypto3::math { return factors; } + /** + * Factor one group produced by distinct-degree factorization and emit its monic irreducible factors directly to + * factor_callback. This is the composition boundary used by full factorization: it avoids repeating the + * normalization and square-free GCD performed by the preceding stages, and it does not build an intermediate + * factorization result. The full-factorization caller can therefore attach the multiplicity inherited from the + * square-free stage as each irreducible factor is emitted. + * + * The inexpensive structural properties are still checked. Square-freeness and the stated common irreducible + * degree are trusted because verifying them would repeat the preceding factorization stages. + * + * @return stop_factorization if factor_callback requests an early stop; continue_factorization otherwise. + * @throws std::invalid_argument if the group is constant, nonmonic, has a zero factor degree, or its factor + * degree does not divide its polynomial degree. + * @pre group.polynomial is canonical and square-free, and all its irreducible factors have + * group.irreducible_factor_degree. + */ + template + factorization_control + factor_distinct_degree_group(distinct_degree_factor group, + polynomial_arithmetic::polynomial_context &arithmetic_context, + Generator &generator, FactorCallback &&factor_callback) { + using value_type = typename Backend::polynomial_type::value_type; + + if (group.irreducible_factor_degree == 0) { + throw std::invalid_argument("equal-degree factorization requires a positive factor degree"); + } + if (group.polynomial.size() <= 1) { + throw std::invalid_argument("equal-degree factorization requires a nonconstant degree group"); + } + if (group.polynomial.back() != value_type::one()) { + throw std::invalid_argument("equal-degree factorization requires a monic degree group"); + } + if ((group.polynomial.size() - 1) % group.irreducible_factor_degree != 0) { + throw std::invalid_argument( + "equal-degree factorization requires the factor degree to divide the polynomial degree"); + } + + cantor_zassenhaus_context split_context(group.irreducible_factor_degree); + return cantor_zassenhaus_split_all(std::move(group.polynomial), split_context, arithmetic_context, + generator, std::forward(factor_callback)); + } + /** * Prepare one group produced by distinct-degree factorization for equal-degree splitting. The input is * normalized to monic form and its original leading coefficient is returned separately. Constant inputs diff --git a/libs/math/test/equal_degree_factorization.cpp b/libs/math/test/equal_degree_factorization.cpp index 5334a6462..bb438080f 100644 --- a/libs/math/test/equal_degree_factorization.cpp +++ b/libs/math/test/equal_degree_factorization.cpp @@ -116,7 +116,7 @@ BOOST_AUTO_TEST_CASE(one_cantor_zassenhaus_trial_finds_a_proper_factor) { BOOST_CHECK_EQUAL(next_coefficient, coefficients.size()); } -BOOST_AUTO_TEST_CASE(one_cantor_zassenhaus_trial_reports_an_unsuccessful_sample) { +BOOST_AUTO_TEST_CASE(one_cantor_zassenhaus_trial_rejects_zero_and_constant_samples) { backend_type backend; const polynomial_type first_factor = {-value_type::one(), value_type::one()}; const polynomial_type second_factor = {value_type::one(), value_type::one()}; @@ -127,7 +127,10 @@ BOOST_AUTO_TEST_CASE(one_cantor_zassenhaus_trial_reports_an_unsuccessful_sample) math::polynomial_divisor_context divisor_context(group, group.size() - 1, arithmetic_context); math::detail::cantor_zassenhaus_context split_context(1); - const std::array coefficients = {value_type::one(), value_type::zero()}; + // Zero and one are rejected before the trial. The accepted sample X + 3 is nonzero at both roots of X^2 - 1, + // and both residues are squares, so its quadratic character is one modulo the entire group and the trial fails. + const std::array coefficients = {value_type::zero(), value_type::zero(), value_type::one(), + value_type::zero(), value_type(3), value_type::one()}; std::size_t next_coefficient = 0; auto generator = [&] { return coefficients[next_coefficient++]; }; polynomial_type factor; @@ -193,8 +196,8 @@ BOOST_AUTO_TEST_CASE(cantor_zassenhaus_retries_and_splits_until_every_factor_has backend.multiply(complete_group, group, third_factor); group = std::move(complete_group); - // The constant samples 1 fail. The following X - 1 and X - 2 samples share a proper factor with the current - // subgroup, so their early GCDs split it without exponentiation. + // The constant samples 1 are rejected and resampled before starting a trial. The following X - 1 and X - 2 + // samples share a proper factor with the current subgroup, so their early GCDs split it without exponentiation. const std::array coefficients = { value_type::one(), value_type::zero(), value_type::zero(), -value_type::one(), value_type::one(), value_type::zero(), value_type::one(), value_type::zero(), -value_type(2), value_type::one(), @@ -208,10 +211,14 @@ BOOST_AUTO_TEST_CASE(cantor_zassenhaus_retries_and_splits_until_every_factor_has }; polynomial_arithmetic::polynomial_context arithmetic_context; - math::detail::cantor_zassenhaus_context split_context(1); - const std::vector factors = - math::detail::cantor_zassenhaus_split_all(group, split_context, arithmetic_context, generator); - + std::vector factors; + const auto control = math::detail::factor_distinct_degree_group( + {group, 1}, arithmetic_context, generator, [&factors](polynomial_type &&factor) { + factors.push_back(std::move(factor)); + return math::factorization_control::continue_factorization; + }); + + BOOST_CHECK(control == math::factorization_control::continue_factorization); BOOST_REQUIRE_EQUAL(factors.size(), 3); polynomial_type reconstructed = {value_type::one()}; for (const polynomial_type &factor : factors) {