diff --git a/libs/math/include/nil/crypto3/math/polynomial/complete_factorization.hpp b/libs/math/include/nil/crypto3/math/polynomial/complete_factorization.hpp new file mode 100644 index 000000000..827c3b641 --- /dev/null +++ b/libs/math/include/nil/crypto3/math/polynomial/complete_factorization.hpp @@ -0,0 +1,115 @@ +//---------------------------------------------------------------------------// +// 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_COMPLETE_FACTORIZATION_HPP +#define CRYPTO3_MATH_COMPLETE_FACTORIZATION_HPP + +#include +#include +#include + +#include +#include +#include + +namespace nil::crypto3::math { + + /** + * Factor a polynomial into monic irreducible factors by composing the three finite-field factorization stages: + * + * 1. Square-free factorization separates factors by their multiplicity in the input. + * 2. Kaltofen-Shoup distinct-degree factorization separates each square-free part into groups whose irreducible + * factors all have the same degree. + * 3. Cantor-Zassenhaus equal-degree factorization splits each group into individual irreducible factors. + * + * Thus, a complete result satisfies + * + * input = leading_coefficient * product(factor.polynomial ^ factor.multiplicity). + * + * The multiplicity attached to each irreducible factor is inherited from its square-free part. The caller-owned + * generator is shared by all Cantor-Zassenhaus groups and must return independent uniformly distributed + * coefficient-field elements. + * + * After each irreducible factor is appended to the result, factor_callback may request an early stop. A stopped + * result includes that factor and has complete set to false. Zero and constant inputs produce no factors and + * preserve their scalar value as leading_coefficient. + * + * @throws std::invalid_argument under the restrictions documented by the component stages, including when the + * coefficient-field characteristic is not greater than the input degree or is two. + * @pre input is a nonempty coefficient polynomial. + */ + template + requires detail::PolynomialFactorCallback + polynomial_factorization_result + complete_factorization(const typename Backend::polynomial_type &input, + polynomial_arithmetic::polynomial_context &arithmetic_context, + Generator &generator, FactorCallback &&factor_callback) { + using polynomial_type = typename Backend::polynomial_type; + using result_type = polynomial_factorization_result; + + auto square_free_result = square_free_factorization(input, arithmetic_context); + + result_type result; + result.leading_coefficient = square_free_result.leading_coefficient; + for (auto &square_free_factor : square_free_result.factors) { + const std::size_t multiplicity = square_free_factor.multiplicity; + const std::size_t block_size = detail::kaltofen_shoup_block_size(square_free_factor.polynomial.size() - 1); + + std::vector> degree_groups; + detail::kaltofen_shoup_factor_monic_square_free(degree_groups, std::move(square_free_factor.polynomial), + block_size, arithmetic_context, + [](const distinct_degree_factor &) { + return factorization_control::continue_factorization; + }); + + for (auto °ree_group : degree_groups) { + const factorization_control control = detail::factor_distinct_degree_group( + std::move(degree_group), arithmetic_context, generator, [&](polynomial_type &&factor) { + result.factors.push_back({std::move(factor), multiplicity}); + return factor_callback(result.factors.back()); + }); + if (control == factorization_control::stop_factorization) { + result.complete = false; + return result; + } + } + } + return result; + } + + /** Compute the complete irreducible factorization without a staged callback. */ + template + polynomial_factorization_result + complete_factorization(const typename Backend::polynomial_type &input, + polynomial_arithmetic::polynomial_context &arithmetic_context, + Generator &generator) { + using factor_type = polynomial_factor; + return complete_factorization(input, arithmetic_context, generator, [](const factor_type &) { + return factorization_control::continue_factorization; + }); + } + +} // namespace nil::crypto3::math + +#endif // CRYPTO3_MATH_COMPLETE_FACTORIZATION_HPP 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 c474e7cf5..55fdf39c0 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 @@ -42,14 +42,6 @@ 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. @@ -367,7 +359,7 @@ namespace nil::crypto3::math { * irreducible_factor_degree. */ template - requires detail::EqualDegreeFactorCallback + requires detail::PolynomialFactorCallback polynomial_factorization_result equal_degree_factorization(const typename Backend::polynomial_type &input, std::size_t irreducible_factor_degree, 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 c2d12443e..b7513ab7f 100644 --- a/libs/math/include/nil/crypto3/math/polynomial/polynomial_factorization.hpp +++ b/libs/math/include/nil/crypto3/math/polynomial/polynomial_factorization.hpp @@ -25,6 +25,7 @@ #ifndef CRYPTO3_MATH_POLYNOMIAL_FACTORIZATION_HPP #define CRYPTO3_MATH_POLYNOMIAL_FACTORIZATION_HPP +#include #include #include #include @@ -104,6 +105,14 @@ namespace nil::crypto3::math { namespace detail { + /** A callback that controls staged factorization after receiving one polynomial factor. */ + template + concept PolynomialFactorCallback = + CoefficientPolynomial && + requires(FactorCallback &callback, const polynomial_factor &factor) { + { callback(factor) } -> std::same_as; + }; + /** * 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 diff --git a/libs/math/include/nil/crypto3/math/polynomial/square_free_factorization.hpp b/libs/math/include/nil/crypto3/math/polynomial/square_free_factorization.hpp index 72f8c45b2..d60f98586 100644 --- a/libs/math/include/nil/crypto3/math/polynomial/square_free_factorization.hpp +++ b/libs/math/include/nil/crypto3/math/polynomial/square_free_factorization.hpp @@ -79,10 +79,7 @@ namespace nil::crypto3::math { * @throws std::invalid_argument if the coefficient-field characteristic is not greater than the input degree. */ template - requires requires(FactorCallback &callback, - const polynomial_factor &factor) { - { callback(factor) } -> std::same_as; - } + requires detail::PolynomialFactorCallback polynomial_factorization_result square_free_factorization(const typename Backend::polynomial_type &input, polynomial_arithmetic::polynomial_context &arithmetic_context, diff --git a/libs/math/test/CMakeLists.txt b/libs/math/test/CMakeLists.txt index a078ba5d8..ed09e60b2 100644 --- a/libs/math/test/CMakeLists.txt +++ b/libs/math/test/CMakeLists.txt @@ -40,6 +40,7 @@ endmacro() set(TESTS_NAMES "batch_inverse" + "complete_factorization" "distinct_degree_factorization" "equal_degree_factorization" "evaluation_domain" diff --git a/libs/math/test/complete_factorization.cpp b/libs/math/test/complete_factorization.cpp new file mode 100644 index 000000000..dee55fd9d --- /dev/null +++ b/libs/math/test/complete_factorization.cpp @@ -0,0 +1,229 @@ +//---------------------------------------------------------------------------// +// 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 complete_factorization_test + +#include +#include +#include + +#include + +#include +#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 fq_field_type = fields::alt_bn128_base_field<254>; + using value_type = fq_field_type::value_type; + using fq12_field_type = fields::fp12_2over3over2>; + using fq12_value_type = fq12_field_type::value_type; + using backend_type = polynomial_arithmetic::schoolbook_backend; + using polynomial_type = typename backend_type::polynomial_type; + + fq12_value_type fq12_value(std::size_t first_coordinate) { + fq12_value_type value = fq12_value_type::zero(); + for (std::size_t coordinate = 0; coordinate < fq12_field_type::arity; ++coordinate) { + value.coordinate(coordinate) = value_type(first_coordinate + coordinate); + } + return value; + } + + template + typename Backend::polynomial_type multiply(Backend &backend, const typename Backend::polynomial_type &left, + const typename Backend::polynomial_type &right) { + typename Backend::polynomial_type result; + backend.multiply(result, left, right); + return result; + } + + template + typename Backend::polynomial_type + reconstruct(Backend &backend, + const math::polynomial_factorization_result &factorization) { + typename Backend::polynomial_type result = {factorization.leading_coefficient}; + for (const auto &factor : factorization.factors) { + for (std::size_t copy = 0; copy < factor.multiplicity; ++copy) { + result = multiply(backend, result, factor.polynomial); + } + } + return result; + } +} // namespace + +BOOST_AUTO_TEST_SUITE(complete_factorization_test_suite) + +BOOST_AUTO_TEST_CASE(complete_factorization_recovers_irreducible_factors_and_multiplicities) { + backend_type backend; + const polynomial_type first_linear_factor = {value_type(1), value_type::one()}; + const polynomial_type second_linear_factor = {value_type(2), value_type::one()}; + // Three is a multiplicative generator of BN254 Fq and is therefore not a square, so X^2 - 3 is irreducible. + const polynomial_type quadratic_factor = {-value_type(3), value_type::zero(), value_type::one()}; + + polynomial_type input = multiply(backend, first_linear_factor, second_linear_factor); + input = multiply(backend, input, quadratic_factor); + input = multiply(backend, input, quadratic_factor); + const value_type leading_coefficient(11); + for (value_type &coefficient : input) { + coefficient = coefficient * leading_coefficient; + } + + polynomial_arithmetic::polynomial_context arithmetic_context; + nil::crypto3::random::algebraic_engine generator(17); + const auto result = math::complete_factorization(input, arithmetic_context, generator); + + BOOST_CHECK(result.complete); + BOOST_CHECK(result.leading_coefficient == leading_coefficient); + BOOST_REQUIRE_EQUAL(result.factors.size(), 3); + BOOST_CHECK(reconstruct(backend, result) == input); + + std::size_t matched_factors = 0; + for (const auto &factor : result.factors) { + if (factor.polynomial == first_linear_factor || factor.polynomial == second_linear_factor) { + BOOST_CHECK_EQUAL(factor.multiplicity, 1); + ++matched_factors; + } else if (factor.polynomial == quadratic_factor) { + BOOST_CHECK_EQUAL(factor.multiplicity, 2); + ++matched_factors; + } + } + BOOST_CHECK_EQUAL(matched_factors, 3); +} + +BOOST_AUTO_TEST_CASE(staged_complete_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 input = multiply(backend, first_factor, second_factor); + input = multiply(backend, input, third_factor); + + 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; + nil::crypto3::random::algebraic_engine generator(29); + const auto result = math::complete_factorization(input, arithmetic_context, generator, callback); + + BOOST_CHECK(!result.complete); + BOOST_CHECK_EQUAL(callback_count, 1); + 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_AUTO_TEST_CASE(zero_and_constant_inputs_preserve_their_scalar_values) { + polynomial_arithmetic::polynomial_context arithmetic_context; + nil::crypto3::random::algebraic_engine generator(31); + + const auto zero = + math::complete_factorization(polynomial_type {value_type::zero()}, arithmetic_context, generator); + BOOST_CHECK(zero.complete); + BOOST_CHECK(zero.leading_coefficient == value_type::zero()); + BOOST_CHECK(zero.factors.empty()); + + const auto constant = + math::complete_factorization(polynomial_type {value_type(13)}, arithmetic_context, generator); + BOOST_CHECK(constant.complete); + BOOST_CHECK(constant.leading_coefficient == value_type(13)); + BOOST_CHECK(constant.factors.empty()); +} + +BOOST_AUTO_TEST_CASE(complete_factorization_is_deterministic_for_a_seeded_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 input = multiply(backend, first_factor, second_factor); + input = multiply(backend, input, third_factor); + + auto factor_with_seed = [&input](std::uint32_t seed) { + polynomial_arithmetic::polynomial_context arithmetic_context; + nil::crypto3::random::algebraic_engine generator(seed); + return math::complete_factorization(input, arithmetic_context, generator); + }; + + const auto first_result = factor_with_seed(37); + const auto second_result = factor_with_seed(37); + BOOST_CHECK(first_result == second_result); +} + +BOOST_AUTO_TEST_CASE(fq12_factorization_reconstructs_input_and_matches_the_mixed_radix_backend) { + using extension_backend_type = polynomial_arithmetic::schoolbook_backend; + using mixed_radix_backend_type = polynomial_arithmetic::mixed_radix_backend; + using extension_polynomial_type = typename extension_backend_type::polynomial_type; + + extension_backend_type backend; + const extension_polynomial_type first_factor = {-fq12_value(1), fq12_value_type::one()}; + const extension_polynomial_type second_factor = {-fq12_value(20), fq12_value_type::one()}; + const extension_polynomial_type repeated_factor = {-fq12_value(40), fq12_value_type::one()}; + extension_polynomial_type input = multiply(backend, first_factor, second_factor); + input = multiply(backend, input, repeated_factor); + input = multiply(backend, input, repeated_factor); + const fq12_value_type leading_coefficient = fq12_value(60); + math::scalar_multiplication(input, input, leading_coefficient); + + // These coefficients make the sampled polynomial equal first_factor. Cantor-Zassenhaus therefore finds a proper + // factor in its initial GCD, which exercises generator injection and splitting without making this Fq12 unit test + // pay for a full modular exponentiation. + const std::array random_coefficients = {-fq12_value(1), fq12_value_type::one()}; + std::size_t next_coefficient = 0; + auto generator = [&] { return random_coefficients[next_coefficient++]; }; + polynomial_arithmetic::polynomial_context arithmetic_context; + const auto result = math::complete_factorization(input, arithmetic_context, generator); + + BOOST_CHECK(result.complete); + BOOST_CHECK(result.leading_coefficient == leading_coefficient); + BOOST_REQUIRE_EQUAL(result.factors.size(), 3); + BOOST_CHECK(reconstruct(backend, result) == input); + BOOST_CHECK_EQUAL(next_coefficient, random_coefficients.size()); + + polynomial_arithmetic::polynomial_context_options options; + // Force fast division so the comparison exercises mixed-radix arithmetic throughout the factorization pipeline. + options.basecase_divisor_coefficient_cutoff = 0; + options.basecase_quotient_coefficient_cutoff = 0; + polynomial_arithmetic::polynomial_context mixed_radix_context( + mixed_radix_backend_type(18), options); + std::size_t next_mixed_radix_coefficient = 0; + auto mixed_radix_generator = [&] { return random_coefficients[next_mixed_radix_coefficient++]; }; + const auto mixed_radix_result = + math::complete_factorization(input, mixed_radix_context, mixed_radix_generator); + + BOOST_CHECK(mixed_radix_result == result); + BOOST_CHECK_EQUAL(next_mixed_radix_coefficient, random_coefficients.size()); +} + +BOOST_AUTO_TEST_SUITE_END()