diff --git a/libs/math/docs/polynomial_factorization.md b/libs/math/docs/polynomial_factorization.md index 510e83264..bd5e3f029 100644 --- a/libs/math/docs/polynomial_factorization.md +++ b/libs/math/docs/polynomial_factorization.md @@ -114,6 +114,40 @@ The factor that triggers the stop is included in `result.factors`, and `result.c `complete == true`. Square-free and distinct-degree factorization also expose staged callbacks, allowing a caller to stop at the earliest useful stage rather than computing a complete irreducible factorization. +Complete factorization splits each distinct-degree group and invokes its irreducible-factor callbacks as soon as the +group is discovered. A callback stop also stops distinct-degree discovery, so later groups and square-free parts are +not processed. Factors retain their square-free multiplicities and the same group/splitting order. The shared +generator is consumed in the same order, including when callbacks use it, and callback or generator exceptions +propagate to the caller. + +By default, each nonlinear square-free part prepares the initial Frobenius power and full baby-step table before +discovering groups. To screen small degrees first, set a numeric limit on the existing arithmetic options: + +```cpp +math::polynomial_arithmetic::polynomial_context_options options; +options.distinct_degree_screening_limit = 1; // Screen linear factors before building the full baby table. +math::polynomial_arithmetic::polynomial_context arithmetic_context(backend_type{}, options); +``` + +The default limit is zero, preserving the unscreened baseline. Positive limits are capped at the Kaltofen-Shoup +degree-block size. Screening computes `gcd(remaining, X^(Q^d) - X)` in increasing degrees `d`, removing each emitted +group before advancing. These are equal-degree groups; their combined norm must not be used to classify every +constituent factor. Complete factorization splits them individually and preserves square-free multiplicities, so +norm recovery still handles even powers without rejecting their individually unsuitable factors. + +Screening reuses the initial `X^Q mod B` and subsequent Frobenius powers, all computed modulo the original +square-free part B. A callback stop avoids constructing the remaining baby table and its block-composition cache. +It still pays for square-free decomposition, the initial power, and the initial Frobenius composition cache. If +screening continues, its powers become the prefix of the full baby table and only missing steps are computed. +The blocked search retains its original alignment; its first block may revisit screened degrees, whose factors +have already been removed. This adds GCD/exact-division work relative to the baseline and should be measured on +both rejected and suitable inputs before choosing a limit. Passing a bounded screen does not establish that norm +recovery will succeed. + +Splitting and callback work run while the active Frobenius precomputation remains alive, which can increase peak +memory. Splitting copies the emitted group because distinct-degree refinement or screening still needs its stored +polynomial after the callback returns. No default screening bound is selected from the behavior of one candidate. + ## Preconditions and current restrictions * Inputs are nonempty coefficient polynomials. Algorithms canonicalize trailing zero coefficients and return monic diff --git a/libs/math/include/nil/crypto3/math/polynomial/backends/polynomial_backend.hpp b/libs/math/include/nil/crypto3/math/polynomial/backends/polynomial_backend.hpp index 0d9fc2b9b..d57a83a5e 100644 --- a/libs/math/include/nil/crypto3/math/polynomial/backends/polynomial_backend.hpp +++ b/libs/math/include/nil/crypto3/math/polynomial/backends/polynomial_backend.hpp @@ -52,6 +52,11 @@ namespace nil::crypto3::math::polynomial_arithmetic { // has at most degree(divisor) coefficients. The limit must be positive; the default permits the algorithm's // square-root block size. std::size_t modular_composition_cached_power_limit = std::numeric_limits::max(); + + // Examine initial irreducible-factor degrees before constructing the full Kaltofen-Shoup baby table. + // Zero keeps the unscreened path. The limit is capped at the degree-block size so the computed Frobenius + // powers can be reused as the start of that table if factorization continues. + std::size_t distinct_degree_screening_limit = 0; }; /** diff --git a/libs/math/include/nil/crypto3/math/polynomial/factorization/complete_factorization.hpp b/libs/math/include/nil/crypto3/math/polynomial/factorization/complete_factorization.hpp index 0c4fada6a..d122279e2 100644 --- a/libs/math/include/nil/crypto3/math/polynomial/factorization/complete_factorization.hpp +++ b/libs/math/include/nil/crypto3/math/polynomial/factorization/complete_factorization.hpp @@ -51,8 +51,11 @@ namespace nil::crypto3::math { * 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 + * Each distinct-degree group is split as soon as it is discovered, before searching for later groups. After each + * irreducible factor is appended to the result, factor_callback may request an early stop. This stops both group + * splitting and distinct-degree discovery; the result includes that factor and has complete set to false. The + * default path constructs the Kaltofen-Shoup baby table before discovering groups. Configured small-degree + * screening can deliver factors before that table is built. 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 @@ -77,22 +80,23 @@ namespace nil::crypto3::math { 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; - } + const auto control = detail::kaltofen_shoup_factor_monic_square_free( + degree_groups, std::move(square_free_factor.polynomial), block_size, arithmetic_context, + [&](const distinct_degree_factor °ree_group) { + // Split each newly discovered degree group now and report its irreducible factors immediately. + // This lets the caller stop before Kaltofen-Shoup searches for later degree groups. + // The splitter takes its group by value, so passing degree_group intentionally copies it. + // Splitting consumes that copy; Kaltofen-Shoup still needs the original polynomial for exact + // division if this callback continues. + return detail::factor_distinct_degree_group( + 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; diff --git a/libs/math/include/nil/crypto3/math/polynomial/factorization/kaltofen_shoup_distinct_degree_factorization.hpp b/libs/math/include/nil/crypto3/math/polynomial/factorization/kaltofen_shoup_distinct_degree_factorization.hpp index 1e238eee9..309a221f5 100644 --- a/libs/math/include/nil/crypto3/math/polynomial/factorization/kaltofen_shoup_distinct_degree_factorization.hpp +++ b/libs/math/include/nil/crypto3/math/polynomial/factorization/kaltofen_shoup_distinct_degree_factorization.hpp @@ -74,10 +74,12 @@ namespace nil::crypto3::math::detail { * Reusing that composition advances consecutive giant steps with one modular composition per block. * * Constructing the baby-step table performs b Frobenius maps and stores b + 1 residues, using - * O(b * degree(B)) field elements. Construction also builds the existing modular-composition precomputation for - * h[b]. + * O(b * degree(B)) field elements. A prefix already computed by small-degree screening can be moved into the + * table; only missing steps are computed. Construction also builds the existing modular-composition + * precomputation for h[b]. * - * @throws std::invalid_argument if block_size is zero. + * @throws std::invalid_argument if block_size is zero or the supplied prefix extends beyond h[b]. + * @pre a nonempty initial_baby_steps contains h[0], ..., h[s] for this same frobenius_context, with s <= b. */ template class kaltofen_shoup_frobenius_precomputation { @@ -88,8 +90,10 @@ namespace nil::crypto3::math::detail { kaltofen_shoup_frobenius_precomputation( std::size_t block_size, const polynomial_frobenius_context &frobenius_context, - polynomial_arithmetic::polynomial_context &arithmetic_context) : - block_size_(block_size), baby_steps_(make_baby_steps(block_size_, frobenius_context, arithmetic_context)), + polynomial_arithmetic::polynomial_context &arithmetic_context, + std::vector initial_baby_steps = {}) : + block_size_(block_size), baby_steps_(make_baby_steps(block_size_, frobenius_context, arithmetic_context, + std::move(initial_baby_steps))), block_frobenius_precomputation_(baby_steps_.back(), // A residue modulo degree-d polynomial B has at most d coefficients. The // composition precomputation requires a positive limit for constant B. @@ -122,22 +126,27 @@ namespace nil::crypto3::math::detail { private: static std::vector make_baby_steps(std::size_t block_size, const polynomial_frobenius_context &frobenius_context, - polynomial_arithmetic::polynomial_context &arithmetic_context) { + polynomial_arithmetic::polynomial_context &arithmetic_context, + std::vector baby_steps) { if (block_size == 0) { throw std::invalid_argument("the Kaltofen-Shoup block size must be positive"); } - std::vector baby_steps; + if (!baby_steps.empty() && baby_steps.size() - 1 > block_size) { + throw std::invalid_argument("the supplied Frobenius prefix exceeds the Kaltofen-Shoup block size"); + } baby_steps.reserve(block_size + 1); - polynomial_type reduced_x = {value_type {}, value_type::one()}; - const auto &divisor_context = frobenius_context.divisor_context(); - if (divisor_context.degree() == 0) { - reduced_x.assign(1, value_type {}); - } else if (reduced_x.size() >= divisor_context.divisor().size()) { - remainder(reduced_x, reduced_x, divisor_context, arithmetic_context); + if (baby_steps.empty()) { + polynomial_type reduced_x = {value_type {}, value_type::one()}; + const auto &divisor_context = frobenius_context.divisor_context(); + if (divisor_context.degree() == 0) { + reduced_x.assign(1, value_type {}); + } else if (reduced_x.size() >= divisor_context.divisor().size()) { + remainder(reduced_x, reduced_x, divisor_context, arithmetic_context); + } + baby_steps.emplace_back(std::move(reduced_x)); } - baby_steps.emplace_back(std::move(reduced_x)); - for (std::size_t index = 1; index <= block_size; ++index) { + for (std::size_t index = baby_steps.size(); index <= block_size; ++index) { polynomial_type next_step; frobenius_map(next_step, baby_steps.back(), frobenius_context, arithmetic_context); baby_steps.emplace_back(std::move(next_step)); @@ -270,6 +279,8 @@ namespace nil::crypto3::math::detail { * For each consecutive degree block, the algorithm computes one coarse GCD, removes that block from the * unclassified polynomial, and immediately fine-splits it. Interleaving the coarse and fine phases avoids storing * a giant-step polynomial for every block while preserving increasing degree order and immediate callback stops. + * Optional small-degree screening uses successive Frobenius differences before building the full baby table. + * The powers and divisor context remain relative to the original input even after screened factors are removed. * * Once the smallest unprocessed factor degree is greater than half the degree of the unclassified polynomial, * that polynomial is either constant or irreducible. Otherwise it would contain at least two factors of at least @@ -287,6 +298,7 @@ namespace nil::crypto3::math::detail { typename Backend::polynomial_type input, std::size_t block_size, polynomial_arithmetic::polynomial_context &arithmetic_context, FactorCallback &&factor_callback) { using polynomial_type = typename Backend::polynomial_type; + using value_type = typename polynomial_type::value_type; if (block_size == 0) { throw std::invalid_argument("the Kaltofen-Shoup block size must be positive"); @@ -298,11 +310,53 @@ namespace nil::crypto3::math::detail { } polynomial_frobenius_context frobenius_context(input, arithmetic_context); - kaltofen_shoup_frobenius_precomputation precomputation(block_size, frobenius_context, - arithmetic_context); - polynomial_type giant_step = precomputation.baby_step(block_size); polynomial_type unclassified(std::move(input)); + std::vector initial_baby_steps; + const auto screening_limit = std::min(arithmetic_context.options().distinct_degree_screening_limit, block_size); + if (screening_limit != 0) { + const polynomial_type x = {value_type {}, value_type::one()}; + initial_baby_steps.push_back(x); + std::size_t degree = 1; + for (; degree <= screening_limit && degree <= (unclassified.size() - 1) / 2; ++degree) { + if (degree == 1) { + // The initial X^Q mod B is already available; do not repeat its exponentiation or composition. + initial_baby_steps.push_back(frobenius_context.x_to_field_order()); + } else { + polynomial_type next_power; + frobenius_map(next_power, initial_baby_steps.back(), frobenius_context, arithmetic_context); + initial_baby_steps.push_back(std::move(next_power)); + } + polynomial_type difference, factor; + subtraction(difference, initial_baby_steps.back(), x); + gcd(factor, unclassified, difference, arithmetic_context); + if (factor.size() > 1) { + // All lower degrees have been removed, so this is an exact-degree group, not necessarily one + // irreducible factor. Let the existing callback split it and apply multiplicity-aware recovery. + output.push_back({std::move(factor), degree}); + if (factor_callback(output.back()) == factorization_control::stop_factorization) + return factorization_control::stop_factorization; + polynomial_type quotient; + factorization_exact_quotient(quotient, unclassified, output.back().polynomial, arithmetic_context); + unclassified = std::move(quotient); + } + } + // Once the remaining degree is less than twice the next untested degree, at most one factor remains. + if (degree > (unclassified.size() - 1) / 2) { + if (unclassified.size() > 1) { + const auto remaining_degree = unclassified.size() - 1; + output.push_back({std::move(unclassified), remaining_degree}); + return factor_callback(output.back()); + } + return factorization_control::continue_factorization; + } + } + + kaltofen_shoup_frobenius_precomputation precomputation( + block_size, frobenius_context, arithmetic_context, std::move(initial_baby_steps)); + polynomial_type giant_step = precomputation.baby_step(block_size); + // Keep the original degree-block alignment. The first block may revisit screened degrees, but their factors + // have been removed from unclassified and therefore cannot be emitted twice. std::size_t first_factor_degree = 1; while (first_factor_degree <= (unclassified.size() - 1) / 2) { const std::size_t maximum_degree_to_test = (unclassified.size() - 1) / 2; diff --git a/libs/math/test/complete_factorization.cpp b/libs/math/test/complete_factorization.cpp index 8ee4289e0..73901245d 100644 --- a/libs/math/test/complete_factorization.cpp +++ b/libs/math/test/complete_factorization.cpp @@ -27,6 +27,9 @@ #include #include #include +#include +#include +#include #include @@ -51,6 +54,30 @@ namespace { using backend_type = polynomial_arithmetic::schoolbook_backend; using polynomial_type = typename backend_type::polynomial_type; + // Count backend calls without replacing any arithmetic, as in the exponentiation tests. + struct counting_schoolbook_backend : backend_type { + explicit counting_schoolbook_backend(std::size_t &calls) : calls(calls) { + } + + void multiply(polynomial_type &output, const polynomial_type &left, const polynomial_type &right) const { + ++calls; + backend_type::multiply(output, left, right); + } + + void square(polynomial_type &output, const polynomial_type &input) const { + ++calls; + backend_type::square(output, input); + } + + void multiply_low(polynomial_type &output, const polynomial_type &left, const polynomial_type &right, + std::size_t coefficient_count) const { + ++calls; + backend_type::multiply_low(output, left, right, coefficient_count); + } + + std::size_t &calls; + }; + 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) { @@ -145,6 +172,223 @@ BOOST_AUTO_TEST_CASE(staged_complete_factorization_stops_after_the_reported_fact BOOST_CHECK_EQUAL(result.factors.front().multiplicity, 1); } +BOOST_AUTO_TEST_CASE(irreducible_callback_runs_before_later_degree_group_work) { + backend_type backend; + const polynomial_type linear = {value_type(1), value_type::one()}; + // Three is neither a square nor a cube in BN254 Fq, so these have degrees two and three irreducibly. + const polynomial_type quadratic = {-value_type(3), value_type::zero(), value_type::one()}; + const polynomial_type cubic = {-value_type(3), value_type::zero(), value_type::zero(), value_type::one()}; + const auto input = multiply(backend, multiply(backend, linear, quadratic), cubic); + + polynomial_arithmetic::polynomial_context_options options; + // Include division work in the backend count, even for this small fixture. + options.basecase_divisor_coefficient_cutoff = 0; + options.basecase_quotient_coefficient_cutoff = 0; + std::size_t prefix_calls = 0; + polynomial_arithmetic::polynomial_context prefix_context( + counting_schoolbook_backend(prefix_calls), options); + auto square_free = math::square_free_factorization(input, prefix_context); + BOOST_REQUIRE_EQUAL(square_free.factors.size(), 1); + std::vector> groups; + const auto control = math::detail::kaltofen_shoup_factor_monic_square_free( + groups, std::move(square_free.factors.front().polynomial), + math::detail::kaltofen_shoup_block_size(input.degree()), prefix_context, [&](const auto &group) { + BOOST_CHECK(group.polynomial == linear); + return math::factorization_control::stop_factorization; + }); + BOOST_REQUIRE(control == math::factorization_control::stop_factorization); + BOOST_REQUIRE_EQUAL(groups.size(), 1); + BOOST_REQUIRE_GT(prefix_calls, 0); + + // This first degree group is already irreducible: delivering its factor requires no further backend calls + // or random coefficients. Returning or throwing from the callback must both avoid all later degree-group work. + for (bool throw_from_callback : {false, true}) { + std::size_t calls = 0, callback_count = 0; + polynomial_arithmetic::polynomial_context context( + counting_schoolbook_backend(calls), options); + auto generator = []() -> value_type { throw std::logic_error("unexpected random coefficient request"); }; + auto callback = [&](const math::polynomial_factor &factor) { + ++callback_count; + BOOST_CHECK(factor.polynomial == linear); + BOOST_CHECK_EQUAL(factor.multiplicity, 1); + BOOST_CHECK_EQUAL(calls, prefix_calls); + if (throw_from_callback) + throw std::runtime_error("factor callback failure"); + return math::factorization_control::stop_factorization; + }; + if (throw_from_callback) { + BOOST_CHECK_EXCEPTION( + math::complete_factorization(input, context, generator, callback), + std::runtime_error, + [](const auto &error) { return std::string(error.what()) == "factor callback failure"; }); + } else { + const auto result = + math::complete_factorization(input, context, generator, callback); + BOOST_CHECK(!result.complete); + BOOST_REQUIRE_EQUAL(result.factors.size(), 1); + BOOST_CHECK(result.factors.front().polynomial == linear); + } + BOOST_CHECK_EQUAL(callback_count, 1); + BOOST_CHECK_EQUAL(calls, prefix_calls); + } +} + +BOOST_AUTO_TEST_CASE(incremental_groups_preserve_factor_order_multiplicities_and_shared_generator_order) { + backend_type backend; + const polynomial_type first_linear = {value_type(1), value_type::one()}; + const polynomial_type second_linear = {value_type(2), value_type::one()}; + const polynomial_type first_quadratic = {-value_type(3), value_type::zero(), value_type::one()}; + // Multiplying the nonsquare 3 by the square 4 gives another nonsquare, so X^2 - 12 is irreducible too. + const polynomial_type second_quadratic = {-value_type(12), value_type::zero(), value_type::one()}; + const polynomial_type cubic = {-value_type(3), value_type::zero(), value_type::zero(), value_type::one()}; + const polynomial_type repeated_linear = {value_type(4), value_type::one()}; + const std::vector> expected = { + {second_linear, 1}, {first_linear, 1}, {second_quadratic, 1}, + {first_quadratic, 1}, {cubic, 1}, {repeated_linear, 3}}; + math::polynomial_factorization_result expected_result; + expected_result.leading_coefficient = value_type(11); + expected_result.factors = expected; + const auto input = reconstruct(backend, expected_result); + + // Feed each splitter one exact proper factor, padded to the sampled coefficient count. The existing worklist + // emits the complementary factor first. Interspersed 101..106 values are consumed by callbacks, modeling callers + // such as norm recovery that share their generator with factorization. + const std::array coefficients = {value_type(1), value_type(1), value_type(101), value_type(102), + -value_type(3), value_type(0), value_type(1), value_type(0), + value_type(103), value_type(104), value_type(105), value_type(106)}; + const std::array consumed_at_factor = {3, 4, 9, 10, 11, 12}; + for (std::size_t screening_limit : {0, 1, 2, 100}) { + polynomial_arithmetic::polynomial_context_options options; + options.distinct_degree_screening_limit = screening_limit; + for (std::size_t stop_after = 0; stop_after <= expected.size(); ++stop_after) { + polynomial_arithmetic::polynomial_context context(backend_type {}, options); + std::size_t next_coefficient = 0; + auto generator = [&] { return coefficients.at(next_coefficient++); }; + std::vector> reported; + const auto result = + math::complete_factorization(input, context, generator, [&](const auto &factor) { + reported.push_back(factor); + BOOST_CHECK(generator() == value_type(100 + reported.size())); + return reported.size() == stop_after ? math::factorization_control::stop_factorization : + math::factorization_control::continue_factorization; + }); + const auto count = stop_after ? stop_after : expected.size(); + BOOST_REQUIRE_EQUAL(result.factors.size(), count); + BOOST_CHECK(result.factors == reported); + for (std::size_t i = 0; i < count; ++i) + BOOST_CHECK(result.factors[i] == expected[i]); + BOOST_CHECK(result.leading_coefficient == expected_result.leading_coefficient); + BOOST_CHECK_EQUAL(result.complete, stop_after == 0); + BOOST_CHECK_EQUAL(next_coefficient, consumed_at_factor[count - 1]); + if (result.complete) + BOOST_CHECK(reconstruct(backend, result) == input); + } + } + + polynomial_arithmetic::polynomial_context context; + auto throwing_generator = []() -> value_type { throw std::runtime_error("coefficient generator failure"); }; + std::size_t callback_count = 0; + BOOST_CHECK_EXCEPTION( + math::complete_factorization(input, context, throwing_generator, + [&](const auto &) { + ++callback_count; + return math::factorization_control::continue_factorization; + }), + std::runtime_error, + [](const auto &error) { return std::string(error.what()) == "coefficient generator failure"; }); + BOOST_CHECK_EQUAL(callback_count, 0); +} + +BOOST_AUTO_TEST_CASE(screening_delivers_a_linear_factor_before_building_the_baby_table) { + backend_type backend; + const polynomial_type linear = {value_type(1), value_type::one()}; + const polynomial_type quadratic = {-value_type(3), value_type::zero(), value_type::one()}; + const polynomial_type cubic = {-value_type(3), value_type::zero(), value_type::zero(), value_type::one()}; + const auto input = multiply(backend, multiply(backend, linear, quadratic), cubic); + polynomial_arithmetic::polynomial_context_options options; + options.basecase_divisor_coefficient_cutoff = 0; + options.basecase_quotient_coefficient_cutoff = 0; + options.distinct_degree_screening_limit = 1; + + std::size_t prefix_calls = 0; + polynomial_arithmetic::polynomial_context prefix_context( + counting_schoolbook_backend(prefix_calls), options); + const auto square_free = math::square_free_factorization(input, prefix_context); + BOOST_REQUIRE_EQUAL(square_free.factors.size(), 1); + const math::polynomial_frobenius_context frobenius(input, prefix_context); + polynomial_type difference, factor; + math::subtraction(difference, frobenius.x_to_field_order(), + polynomial_type {value_type::zero(), value_type::one()}); + math::gcd(factor, input, difference, prefix_context); + BOOST_REQUIRE(factor == linear); + + for (bool throw_from_callback : {false, true}) { + std::size_t calls = 0, callbacks = 0; + polynomial_arithmetic::polynomial_context context( + counting_schoolbook_backend(calls), options); + auto generator = []() -> value_type { throw std::logic_error("unexpected random coefficient request"); }; + auto callback = [&](const auto &reported) { + ++callbacks; + BOOST_CHECK(reported.polynomial == linear); + // Only square-free preparation, the initial Frobenius context, and this one GCD are permitted here. + BOOST_CHECK_EQUAL(calls, prefix_calls); + if (throw_from_callback) + throw std::runtime_error("screening callback failure"); + return math::factorization_control::stop_factorization; + }; + if (throw_from_callback) { + BOOST_CHECK_EXCEPTION( + math::complete_factorization(input, context, generator, callback), + std::runtime_error, + [](const auto &error) { return std::string(error.what()) == "screening callback failure"; }); + } else { + const auto result = + math::complete_factorization(input, context, generator, callback); + BOOST_CHECK(!result.complete); + BOOST_REQUIRE_EQUAL(result.factors.size(), 1); + BOOST_CHECK_EQUAL(result.factors.front().multiplicity, 1); + } + BOOST_CHECK_EQUAL(callbacks, 1); + BOOST_CHECK_EQUAL(calls, prefix_calls); + } +} + +BOOST_AUTO_TEST_CASE(screening_prefix_is_reused_when_completing_the_baby_table) { + const polynomial_type input = {value_type(2), value_type(3), value_type(5), + value_type(7), value_type(11), value_type::one()}; + std::size_t calls = 0; + polynomial_arithmetic::polynomial_context context {counting_schoolbook_backend(calls)}; + const math::polynomial_frobenius_context frobenius(input, context); + std::vector powers = {{value_type::zero(), value_type::one()}, frobenius.x_to_field_order()}; + for (std::size_t degree = 2; degree <= 3; ++degree) { + polynomial_type next; + math::frobenius_map(next, powers.back(), frobenius, context); + powers.push_back(std::move(next)); + } + // Compare against constructing just the still-required block-composition cache. Completing a full prefix must + // not perform any extra Frobenius work; a partial prefix must compute only its missing last step. + for (std::size_t prefix_size : {3, 4}) { + calls = 0; + if (prefix_size == 3) { + polynomial_type next; + math::frobenius_map(next, powers[2], frobenius, context); + BOOST_CHECK(next == powers[3]); + } + const math::polynomial_composition_precomputation block_cache( + powers[3], input.degree(), frobenius.divisor_context(), context); + const auto expected_calls = calls; + calls = 0; + const math::detail::kaltofen_shoup_frobenius_precomputation table( + 3, frobenius, context, std::vector(powers.begin(), powers.begin() + prefix_size)); + BOOST_CHECK_EQUAL(calls, expected_calls); + for (std::size_t i = 0; i < powers.size(); ++i) + BOOST_CHECK(table.baby_step(i) == powers[i]); + } + BOOST_CHECK_THROW((math::detail::kaltofen_shoup_frobenius_precomputation( + 1, frobenius, context, powers)), + std::invalid_argument); +} + 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); diff --git a/libs/math/test/distinct_degree_factorization.cpp b/libs/math/test/distinct_degree_factorization.cpp index 8cac7e1e6..2c48600b1 100644 --- a/libs/math/test/distinct_degree_factorization.cpp +++ b/libs/math/test/distinct_degree_factorization.cpp @@ -587,4 +587,87 @@ BOOST_AUTO_TEST_CASE(kaltofen_shoup_internal_driver_propagates_an_early_stop) { BOOST_CHECK(factors[1] == factor_type({quadratic_factor, 2})); } +BOOST_AUTO_TEST_CASE(screening_continuation_matches_reference_and_unscreened_blocks) { + using backend_type = polynomial_arithmetic::schoolbook_backend; + using mixed_backend = polynomial_arithmetic::mixed_radix_backend; + using polynomial_type = typename backend_type::polynomial_type; + backend_type backend; + const polynomial_type linear = {fq_value_type::one(), fq_value_type::one()}; + const polynomial_type quadratic = {-fq_value_type(3), fq_value_type::zero(), fq_value_type::one()}; + const polynomial_type cubic = {-fq_value_type(3), fq_value_type::zero(), fq_value_type::zero(), + fq_value_type::one()}; + const polynomial_type second_cubic = {-fq_value_type(9), fq_value_type::zero(), fq_value_type::zero(), + fq_value_type::one()}; + const auto higher_degrees = multiply(backend, quadratic, multiply(backend, cubic, second_cubic)); + const auto mixed_degrees = multiply(backend, linear, higher_degrees); + auto nonmonic = mixed_degrees; + math::scalar_multiplication(nonmonic, nonmonic, fq_value_type(7)); + const std::vector inputs = { + {fq_value_type::zero()}, + {fq_value_type(7)}, + linear, + quadratic, + cubic, + multiply(backend, linear, polynomial_type {fq_value_type::zero(), fq_value_type::one()}), + higher_degrees, + mixed_degrees, + nonmonic}; + for (const auto &input : inputs) { + polynomial_arithmetic::polynomial_context reference_context; + const auto reference = math::distinct_degree_factorization_reference(input, reference_context); + for (std::size_t limit : {0, 1, 2, 100}) { + polynomial_arithmetic::polynomial_context_options options; + options.distinct_degree_screening_limit = limit; + options.basecase_divisor_coefficient_cutoff = 0; + options.basecase_quotient_coefficient_cutoff = 0; + polynomial_arithmetic::polynomial_context school(backend_type {}, options); + polynomial_arithmetic::polynomial_context mixed(mixed_backend(26), options); + BOOST_CHECK(math::distinct_degree_factorization_kaltofen_shoup(input, school) == reference); + BOOST_CHECK(math::distinct_degree_factorization_kaltofen_shoup(input, mixed) == reference); + } + } + // Smaller explicit block sizes force continuation across several degree blocks after the linear screen. + for (std::size_t block_size : {1, 2}) { + polynomial_arithmetic::polynomial_context_options options; + options.distinct_degree_screening_limit = 1; + polynomial_arithmetic::polynomial_context context(backend_type {}, options); + const auto reference = math::distinct_degree_factorization_reference(mixed_degrees, context); + std::vector> groups; + const auto control = math::detail::kaltofen_shoup_factor_monic_square_free( + groups, mixed_degrees, block_size, context, + [](const auto &) { return math::factorization_control::continue_factorization; }); + BOOST_CHECK(control == math::factorization_control::continue_factorization); + BOOST_CHECK(groups == reference.factors); + } +} + +BOOST_AUTO_TEST_CASE(screening_preserves_full_fq12_groups_and_early_stops) { + using school_backend = polynomial_arithmetic::schoolbook_backend; + using mixed_backend = polynomial_arithmetic::mixed_radix_backend; + using polynomial_type = typename school_backend::polynomial_type; + school_backend backend; + const polynomial_type first = {-fq12_value(1), fq12_value_type::one()}; + const polynomial_type second = {-fq12_value(20), fq12_value_type::one()}; + const auto input = multiply(backend, first, second); + polynomial_arithmetic::polynomial_context_options options; + options.distinct_degree_screening_limit = 1; + options.basecase_divisor_coefficient_cutoff = 0; + options.basecase_quotient_coefficient_cutoff = 0; + polynomial_arithmetic::polynomial_context school(school_backend {}, options); + polynomial_arithmetic::polynomial_context mixed(mixed_backend(18), options); + std::size_t callbacks = 0; + const auto callback = [&](const auto &group) { + ++callbacks; + // Screening emits a reducible degree-one group; irreducible splitting remains the next stage's job. + BOOST_CHECK_EQUAL(group.irreducible_factor_degree, 1); + BOOST_CHECK(group.polynomial == input); + return math::factorization_control::stop_factorization; + }; + const auto reference = math::distinct_degree_factorization_kaltofen_shoup(input, school, callback); + const auto result = math::distinct_degree_factorization_kaltofen_shoup(input, mixed, callback); + BOOST_CHECK(!result.complete); + BOOST_CHECK(result == reference); + BOOST_CHECK_EQUAL(callbacks, 2); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/libs/math/test/polynomial_x_norm_reconstruction.cpp b/libs/math/test/polynomial_x_norm_reconstruction.cpp index 5f45eed9b..7c0ff74b6 100644 --- a/libs/math/test/polynomial_x_norm_reconstruction.cpp +++ b/libs/math/test/polynomial_x_norm_reconstruction.cpp @@ -444,4 +444,39 @@ BOOST_AUTO_TEST_CASE(high_level_recovery_supports_bn254_fq12_with_the_mixed_radi check_bn254_fq12_high_level_recovery(arithmetic_context, 0xF0125102); } +BOOST_AUTO_TEST_CASE(screening_preserves_even_multiplicities_and_rejects_odd_obstructions) { + polynomial_arithmetic::polynomial_context preparation; + const auto non_residue = first_quadratic_non_residue(); + const auto other_non_residue = next_quadratic_non_residue(non_residue); + const polynomial_type first = {-non_residue, value_type::one()}; + const polynomial_type second = {-other_non_residue, value_type::one()}; + const auto unsuitable_group = context_multiply(first, second, preparation); + const auto good_quadratic = representable_irreducible_quadratic(); + const auto rejected_input = context_multiply(unsuitable_group, good_quadratic, preparation); + polynomial_type even_power; + preparation.square(even_power, unsuitable_group); + auto suitable_input = context_multiply(even_power, good_quadratic, preparation); + suitable_input = context_multiply( + suitable_input, polynomial_type {value_type::zero(), value_type::one()}, preparation); + + // The product of two unsuitable linear factors passes the scalar filters. They must be classified individually; + // their even powers remain admissible, including when combined with the zero-root factor X. + BOOST_REQUIRE(rejected_input[0].is_square()); + BOOST_REQUIRE(rejected_input.back().is_square()); + for (std::size_t limit : {0, 1, 2, 100}) { + polynomial_arithmetic::polynomial_context_options options; + options.distinct_degree_screening_limit = limit; + polynomial_arithmetic::polynomial_context context(backend_type {}, options); + boost::random::mt19937 rng(0x584E3001); + auto generator = [&] { return nil::crypto3::algebra::random_element(rng); }; + const auto rejected = + math::recover_polynomial_x_norm_representation(rejected_input, context, generator); + BOOST_CHECK(!rejected.has_value()); + const auto recovered = + math::recover_polynomial_x_norm_representation(suitable_input, context, generator); + BOOST_REQUIRE(recovered.has_value()); + BOOST_CHECK(math::evaluate_polynomial_x_norm(*recovered, context) == suitable_input); + } +} + BOOST_AUTO_TEST_SUITE_END()