diff --git a/libs/math/docs/polynomial_arithmetic.md b/libs/math/docs/polynomial_arithmetic.md index e581f5d60..a6b452b77 100644 --- a/libs/math/docs/polynomial_arithmetic.md +++ b/libs/math/docs/polynomial_arithmetic.md @@ -187,6 +187,7 @@ Separate contexts are required for concurrent calls when the backend owns mutabl | `half_gcd_basecase_cutoff` | 30 | At or below this coefficient count, recursive half-GCD constructs its transformation iteratively; zero disables this base case. | | `gcd_half_gcd_cutoff` | 0 | Use half-GCD when the smaller operand reaches this size; zero disables half-GCD. | | `modular_composition_cached_power_limit` | maximum `size_t` | Cap the number of Brent-Kung baby powers; constructing a composition cache requires a positive limit. | +| `modular_composition_block_size` | 0 | Request a Brent-Kung block size for composition caches; zero selects the square-root heuristic. A positive request is capped by the cached-power limit and maximum outer coefficient count. | | `distinct_degree_screening_limit` | 0 | Screen initial irreducible-factor degrees before the full Kaltofen-Shoup baby table; zero disables screening. | | `modular_composition_prepared_byte_limit` | 0 | Bound the additional prepared Fp12 coefficient payload per composition cache; zero disables preparation. | | `modular_composition_simd_lanes` | 8 | Select four or eight IFMA lanes when supported; zero selects scalar prepared dots. The byte budget still controls whether preparation is enabled. | @@ -374,14 +375,27 @@ The faster `compose_mod` overload uses blocked Brent-Kung composition. For block 1, inner, ..., inner^(k - 1) mod B and the giant step `inner^k mod B`. Each `Fi(inner)` is a linear combination of the baby powers, and the block values -are combined by Horner's rule in the giant step. The default `k = ceil(sqrt(L))`, where `L` is the maximum outer -coefficient count, balances precomputation against giant-step multiplications. +are combined by Horner's rule in the giant step. Let `L` be the positive maximum outer coefficient count and `C` the +positive `modular_composition_cached_power_limit`. With the default `modular_composition_block_size == 0`, the cache +selects `k = min(ceil(sqrt(L)), C)`. A positive request `r` instead selects `k = min(r, L, C)`. Construct and reuse the precomputation when composing several outer polynomials with the same inner polynomial and divisor. The one-off overload constructs it internally. `modular_composition_cached_power_limit` caps `k`, reducing memory from cached powers at the cost of more giant-step multiplications. Zero is rejected by the Brent-Kung APIs; it does not select Horner composition automatically. `compose_mod_reference` remains available explicitly. +Set `modular_composition_block_size` on the arithmetic options before constructing the context and its caches. +For example, requesting 246 requires both `L >= 246` and a cached-power limit of at least 246 to obtain that width. +The request changes the cache layout while preserving the declared maximum outer coefficient count: an outer +polynomial exceeding `L` is still rejected. The option applies to each newly constructed composition cache, including +those created inside Frobenius and recovery operations; it does not detect how often a particular cache will be used. + +For a nonconstant modulus and an outer polynomial with `m` coefficients, blocked evaluation uses +`ceil(m / k) - 1` giant-step modular multiplications. Increasing `k` reduces that count but builds and retains more +baby powers and may require a larger prepared coefficient payload. Choose a block size using complete workloads +that include cache construction, the intended number of reuses, and peak memory. A wider cache can cost more than it +saves when reused only a few times. Its prepared payload remains subject to the separate per-cache byte budget below. + ### Prepared coefficient combinations Supported Fp12 coefficient types can additionally prepare the linear forms used by the block dot products. Set @@ -494,7 +508,12 @@ coefficient-field order. These are rough bounds for the current implementation: | One cached Frobenius map | `O(sqrt(n) * M(n) + n^2)` | | Frobenius product or weighted sum of `t` terms | `O(log t)` compositions, composition-cache preparations, and modular products for `t > 1` | -The `n^2` term in Brent-Kung and Frobenius maps is the block-formation phase, including when prepared dots are enabled. +The composition and Frobenius bounds above assume automatic square-root blocking and a cached-power limit large +enough to permit it. +For comparable outer and modulus sizes, a chosen block size `k` instead requires `O(k * M(n))` to construct the +cached powers and `O(ceil(n / k) * M(n) + n^2)` per cached composition, with `O(k * n)` coefficients in the baby powers. +Prepared coefficient payloads and scratch require additional work and storage. The `n^2` term in Brent-Kung and +Frobenius maps is the block-formation phase, including when prepared dots are enabled. Both FFT backends' `multiply_low` operations truncate their inputs but still transform their complete prefix product, so they have the same rough asymptotic cost as multiplying those prefixes in full. They do not currently use truncated transforms. The transform-only cost for the small-prime backend is not a complete product-cost estimate: conversion diff --git a/libs/math/docs/polynomial_factorization.md b/libs/math/docs/polynomial_factorization.md index ed4163b46..e1644ba94 100644 --- a/libs/math/docs/polynomial_factorization.md +++ b/libs/math/docs/polynomial_factorization.md @@ -105,12 +105,17 @@ Several independent options affect the work in this pipeline: | `gcd_half_gcd_cutoff` | Recursive half-GCD during square-free decomposition, screening, and splitting | 0: disabled | | `distinct_degree_screening_limit` | Early degree checks before the full baby table | 0: disabled | | `modular_composition_cached_power_limit` | Baby powers retained by each Brent-Kung composition cache | Maximum `size_t`: permits the usual square-root block size | +| `modular_composition_block_size` | Explicit Brent-Kung block size, capped by the cached-power limit and maximum outer coefficient count | 0: automatic square-root heuristic | | `modular_composition_prepared_byte_limit` | Additional prepared Fp12 coefficient combinations | 0: disabled | | `modular_composition_simd_lanes` | Four- or eight-lane IFMA for eligible prepared combinations | 8; still requires a positive byte budget and build support | Selecting the small-prime backend does not enable half-GCD or prepared coefficient combinations automatically. Choose their cutoffs and budgets from representative complete operations, including preparation and memory costs. Composition budgets apply to each cache, and simultaneous workers need separate arithmetic contexts and generators. +The explicit composition block size controls each Brent-Kung cache, independently of the Kaltofen-Shoup degree-block +size. Larger composition blocks exchange additional preparation and storage for fewer modular products per reuse; +they can make short-lived caches slower. See [composition block selection](@ref math_polynomial_arithmetic) for the +selection rules and outer-size contract. ## Staged factorization @@ -139,6 +144,13 @@ is shared with callbacks, so their sampling affects its state for subsequent spl exceptions propagate to the caller. Reproducibility assumes the same implementation, configuration, and callback behavior; changing a recovery algorithm can change sampling even with the same initial seed. +At the internal boundary from distinct-degree discovery to complete factorization, a group whose polynomial degree +equals its reported irreducible-factor degree already contains exactly one factor under the distinct-degree +precondition. After the structural and characteristic checks, it is emitted directly without constructing the +Cantor-Zassenhaus splitting exponent or consuming random elements in the dispatch itself. Its callback is invoked +once, its stop/continue result is honored, and exceptions propagate. This shortcut relies on the preceding discovery +stage; it does not establish irreducibility for an arbitrary polynomial supplied as a degree group. + The public factorization APIs collect the factors they report. Internally, complete factorization releases each distinct-degree group after splitting and any required exact division instead of retaining all processed groups. Its internal callback path also allows norm recovery to consume irreducible factors without collecting a second @@ -164,14 +176,21 @@ Screening reuses the initial `X^Q mod B` and subsequent Frobenius powers, all co 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. +Once the table is complete, the blocked search retains the original divisor context and releases the initial +Frobenius context, including its composition cache, before constructing the giant-step composition cache. The baby +table owns its residues, so later steps do not depend on the released cache. The divisor and all powers still refer +to the original square-free part B; this lifetime change does not shrink the modulus or rebuild the baby table. 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 original -polynomial after the callback returns. No default screening bound is selected from the behavior of one candidate. +Splitting and callback work run while the current discovery phase's precomputation remains alive: the initial +Frobenius cache during screening, or the completed baby table and giant-step cache during the blocked search. Their +storage can therefore overlap a callback's own recovery caches even though the two discovery composition caches no +longer overlap each other during the transition. Splitting copies the emitted group because distinct-degree +refinement or screening still needs its original polynomial after the callback returns. No default screening bound +is selected from the behavior of one candidate. ## Preconditions and current restrictions diff --git a/libs/math/docs/polynomial_recovery.md b/libs/math/docs/polynomial_recovery.md index 6272d51f9..dfa03bc63 100644 --- a/libs/math/docs/polynomial_recovery.md +++ b/libs/math/docs/polynomial_recovery.md @@ -431,6 +431,20 @@ phases. Optional prepared coefficient payloads obey `modular_composition_prepare cached powers, backend scratch, and concurrent operations require additional memory; this option is not a total memory limit. Measure seed construction, degree-dependent work, and complete recovery together when assessing speed. +`modular_composition_block_size` applies to the composition caches constructed by these recovery paths as well as +those used in factor discovery. Its default of zero keeps the automatic square-root heuristic. A positive value +requests a width capped by the cached-power limit and each cache's maximum outer coefficient count, as described in +the [composition guide](@ref math_polynomial_arithmetic). A larger width spends more time and memory on preparation +to reduce giant-step products during composition. Recovery chains can have different degrees and reuse counts from +discovery, so a width that helps a long discovery chain can slow a short recovery chain. Assess the complete recovery +with its actual factor pattern, including preparation and peak memory, before selecting a width. + +During composite norm recovery, factor discovery releases its initial Frobenius cache after the baby table is +complete and before allocating its giant-step cache. This reduces overlapping discovery storage, but the active +discovery precomputation still remains alive while the factor callback performs norm recovery. Its storage must be +counted alongside the recovery caches when estimating peak memory. See +[staged factorization](@ref math_polynomial_factorization) for screening and callback lifetimes. + Let `Q = p^e` be the coefficient-field order, `d` the irreducible divisor degree, and `s` the two-adicity of `Q^d - 1`. The current implementations have the rough bounds shown below. 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 2884eb897..253b0f727 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 @@ -67,6 +67,11 @@ namespace nil::crypto3::math::polynomial_arithmetic { // Prepared BN254 combinations may use four or eight IFMA lanes when the build supports them. // Zero retains the scalar prepared kernel. The byte budget above remains the opt-in switch. std::size_t modular_composition_simd_lanes = 8; + + // Explicit Brent-Kung block size for reusable composition caches. Zero selects the square-root heuristic. + // A positive request is capped by the cached-power limit and maximum outer coefficient count. Larger blocks + // trade more cache construction and storage for fewer giant-step products when the cache is reused. + std::size_t modular_composition_block_size = 0; }; /** diff --git a/libs/math/include/nil/crypto3/math/polynomial/factorization/equal_degree_factorization.hpp b/libs/math/include/nil/crypto3/math/polynomial/factorization/equal_degree_factorization.hpp index c2651cbf9..382f29480 100644 --- a/libs/math/include/nil/crypto3/math/polynomial/factorization/equal_degree_factorization.hpp +++ b/libs/math/include/nil/crypto3/math/polynomial/factorization/equal_degree_factorization.hpp @@ -286,6 +286,16 @@ namespace nil::crypto3::math { "equal-degree factorization requires the factor degree to divide the polynomial degree"); } + // A group of exactly one irreducible factor needs no splitting exponent or random trials. + // Preserve the characteristic restriction of the splitting context before emitting it. + if (group.polynomial.size() - 1 == group.irreducible_factor_degree) { + if (algebra::fields::field_characteristic() == 2) { + throw std::invalid_argument( + "Cantor-Zassenhaus splitting for characteristic two is not implemented"); + } + return factor_callback(std::move(group.polynomial)); + } + 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)); 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 ed9950dfa..245533b06 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 @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -92,13 +93,22 @@ namespace nil::crypto3::math::detail { std::size_t block_size, const polynomial_frobenius_context &frobenius_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))), + kaltofen_shoup_frobenius_precomputation( + make_baby_steps(block_size, frobenius_context, arithmetic_context, std::move(initial_baby_steps)), + frobenius_context.divisor_context(), arithmetic_context) { + } + + // A completed table permits destroying the initial Frobenius cache before allocating the giant-step cache. + // The table must contain h[0], ..., h[b], b >= 1, for the supplied divisor. + kaltofen_shoup_frobenius_precomputation( + std::vector baby_steps, const polynomial_divisor_context &divisor_context, + polynomial_arithmetic::polynomial_context &arithmetic_context) : + block_size_(completed_block_size(baby_steps)), baby_steps_(std::move(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. - std::max(1, frobenius_context.divisor_context().degree()), - frobenius_context.divisor_context(), arithmetic_context) { + std::max(1, divisor_context.degree()), divisor_context, + arithmetic_context) { } std::size_t block_size() const { @@ -114,16 +124,15 @@ namespace nil::crypto3::math::detail { * Apply b Frobenius iterations with one modular composition. Output may alias input. * * @pre input is reduced modulo B. - * @pre frobenius_context represents the same B used to construct this precomputation. + * @pre divisor_context represents the same B used to construct this precomputation. */ void apply_block_frobenius(polynomial_type &output, const polynomial_type &input, - const polynomial_frobenius_context &frobenius_context, + const polynomial_divisor_context &divisor_context, polynomial_arithmetic::polynomial_context &arithmetic_context) const { - compose_mod(output, input, block_frobenius_precomputation_, frobenius_context.divisor_context(), - arithmetic_context); + compose_mod(output, input, block_frobenius_precomputation_, divisor_context, arithmetic_context); } - private: + // Complete the table while the initial Frobenius cache is alive; the returned residues own their data. static std::vector make_baby_steps(std::size_t block_size, const polynomial_frobenius_context &frobenius_context, polynomial_arithmetic::polynomial_context &arithmetic_context, @@ -154,6 +163,14 @@ namespace nil::crypto3::math::detail { return baby_steps; } + private: + static std::size_t completed_block_size(const std::vector &baby_steps) { + if (baby_steps.size() < 2) { + throw std::invalid_argument("a completed Frobenius table must contain at least h[0] and h[1]"); + } + return baby_steps.size() - 1; + } + std::size_t block_size_; std::vector baby_steps_; polynomial_composition_precomputation block_frobenius_precomputation_; @@ -161,7 +178,7 @@ namespace nil::crypto3::math::detail { /** * Extract one coarse Kaltofen-Shoup degree block. Let the coefficient field contain Q elements, let B be the - * modulus in frobenius_context, and let precomputation store h[i] = X^(Q^i) mod B for a block of size b. For + * modulus in divisor_context, and let precomputation store h[i] = X^(Q^i) mod B for a block of size b. For * zero-based block j, the giant-step exponent is e = (j + 1) * b. The number c = degree_count satisfies * 1 <= c <= b and allows the final block to process fewer than b degrees. Let R denote the polynomial passed as * remaining. If giant_step = X^(Q^e) mod B, form @@ -178,7 +195,7 @@ namespace nil::crypto3::math::detail { * @throws std::invalid_argument if degree_count is zero or greater than the block size. * @pre remaining is square-free, divides B, and has all earlier degree factors removed. * @pre giant_step is reduced modulo B. - * @pre frobenius_context represents the same B used to construct precomputation. + * @pre divisor_context represents the same B used to construct precomputation. */ template void kaltofen_shoup_coarse_block_factor(typename Backend::polynomial_type &output, @@ -186,7 +203,7 @@ namespace nil::crypto3::math::detail { const typename Backend::polynomial_type &giant_step, std::size_t degree_count, const kaltofen_shoup_frobenius_precomputation &precomputation, - const polynomial_frobenius_context &frobenius_context, + const polynomial_divisor_context &divisor_context, polynomial_arithmetic::polynomial_context &arithmetic_context) { using polynomial_type = typename Backend::polynomial_type; using value_type = typename polynomial_type::value_type; @@ -200,8 +217,7 @@ namespace nil::crypto3::math::detail { for (std::size_t offset = 0; offset < degree_count; ++offset) { const std::size_t baby_step_index = precomputation.block_size() - 1 - offset; subtraction(difference, giant_step, precomputation.baby_step(baby_step_index)); - mulmod(interval_product, interval_product, difference, frobenius_context.divisor_context(), - arithmetic_context); + mulmod(interval_product, interval_product, difference, divisor_context, arithmetic_context); } gcd(output, remaining, interval_product, arithmetic_context); } @@ -315,7 +331,9 @@ namespace nil::crypto3::math::detail { return factor_callback(reported); } - polynomial_frobenius_context frobenius_context(input, arithmetic_context); + std::optional> initial_frobenius(std::in_place, input, + arithmetic_context); + const auto &frobenius_context = *initial_frobenius; 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); @@ -359,8 +377,14 @@ namespace nil::crypto3::math::detail { } } - kaltofen_shoup_frobenius_precomputation precomputation( - block_size, frobenius_context, arithmetic_context, std::move(initial_baby_steps)); + using precomputation_type = kaltofen_shoup_frobenius_precomputation; + auto baby_steps = precomputation_type::make_baby_steps(block_size, frobenius_context, arithmetic_context, + std::move(initial_baby_steps)); + // Keep the original modulus and reciprocal, but release the much larger initial composition cache before + // constructing the giant-step cache. No single-step Frobenius maps are needed after the baby table. + auto divisor_context = frobenius_context.divisor_context(); + initial_frobenius.reset(); + precomputation_type precomputation(std::move(baby_steps), divisor_context, arithmetic_context); 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 @@ -372,7 +396,7 @@ namespace nil::crypto3::math::detail { polynomial_type coarse_block; kaltofen_shoup_coarse_block_factor(coarse_block, unclassified, giant_step, degree_count, precomputation, - frobenius_context, arithmetic_context); + divisor_context, arithmetic_context); if (coarse_block.size() > 1) { polynomial_type quotient; factorization_exact_quotient(quotient, unclassified, coarse_block, arithmetic_context); @@ -399,7 +423,7 @@ namespace nil::crypto3::math::detail { // shorter than block_size. first_factor_degree += block_size; if (first_factor_degree <= (unclassified.size() - 1) / 2) { - precomputation.apply_block_frobenius(giant_step, giant_step, frobenius_context, arithmetic_context); + precomputation.apply_block_frobenius(giant_step, giant_step, divisor_context, arithmetic_context); } } diff --git a/libs/math/include/nil/crypto3/math/polynomial/quotient_ring/polynomial_composition.hpp b/libs/math/include/nil/crypto3/math/polynomial/quotient_ring/polynomial_composition.hpp index ca110b07f..b56ed7d6e 100644 --- a/libs/math/include/nil/crypto3/math/polynomial/quotient_ring/polynomial_composition.hpp +++ b/libs/math/include/nil/crypto3/math/polynomial/quotient_ring/polynomial_composition.hpp @@ -47,6 +47,8 @@ namespace nil::crypto3::math { * and the giant step inner^k mod B. It can be reused to compose multiple outer polynomials containing at most * maximum_outer_coefficient_count coefficients. A divisor context used for later compositions must represent the * same polynomial B and contain sufficient inverse precision. + * The default block size is ceil(sqrt(maximum_outer_coefficient_count)). An explicit context option may select + * a larger block for repeated compositions, subject to the cached-power limit and maximum outer size. * * Supported Fp12 coefficients may additionally cache prepared base-field forms (23 for BN254 SIMD, * 54 for the scalar tower kernel). The context's @@ -79,7 +81,11 @@ namespace nil::crypto3::math { if (cached_power_limit == 0) { throw std::invalid_argument("the modular-composition cached-power limit must be positive"); } - block_size_ = std::min(detail::ceil_sqrt(maximum_outer_coefficient_count_), cached_power_limit); + const std::size_t requested_block_size = arithmetic_context.options().modular_composition_block_size; + const std::size_t selected_block_size = + requested_block_size == 0 ? detail::ceil_sqrt(maximum_outer_coefficient_count_) : + std::min(requested_block_size, maximum_outer_coefficient_count_); + block_size_ = std::min(selected_block_size, cached_power_limit); if (divisor_context.degree() == 0) { return; diff --git a/libs/math/test/distinct_degree_factorization.cpp b/libs/math/test/distinct_degree_factorization.cpp index 39e702913..df04cb24d 100644 --- a/libs/math/test/distinct_degree_factorization.cpp +++ b/libs/math/test/distinct_degree_factorization.cpp @@ -25,6 +25,7 @@ #define BOOST_TEST_MODULE distinct_degree_factorization_test #include +#include #include #include @@ -290,11 +291,40 @@ BOOST_AUTO_TEST_CASE(kaltofen_shoup_precomputation_builds_baby_and_giant_frobeni arithmetic_context); BOOST_CHECK(giant_step == expected_giant_step); if (block != 3) { - precomputation.apply_block_frobenius(giant_step, giant_step, frobenius_context, arithmetic_context); + precomputation.apply_block_frobenius(giant_step, giant_step, frobenius_context.divisor_context(), + arithmetic_context); } } } +BOOST_AUTO_TEST_CASE(kaltofen_shoup_giant_steps_outlive_the_initial_prepared_cache) { + using backend_type = polynomial_arithmetic::schoolbook_backend; + using polynomial_type = typename backend_type::polynomial_type; + using precomputation_type = math::detail::kaltofen_shoup_frobenius_precomputation; + polynomial_arithmetic::polynomial_context_options options; + options.modular_composition_prepared_byte_limit = 1024 * 1024; + polynomial_arithmetic::polynomial_context context(backend_type {}, options); + const polynomial_type modulus = {fq12_value(2), fq12_value(3), fq12_value(4), fq12_value_type::zero(), + fq12_value_type::one()}; + std::optional> divisor; + std::vector babies; + polynomial_type expected; + { + math::polynomial_frobenius_context initial(modulus, context); + BOOST_REQUIRE_GT(initial.composition_precomputation().prepared_coefficient_bytes(), 0); + babies = precomputation_type::make_baby_steps(3, initial, context, {}); + math::frobenius_map(expected, babies.back(), 3, initial, context); + divisor.emplace(initial.divisor_context()); + } + const precomputation_type precomputation(std::move(babies), *divisor, context); + polynomial_type giant = precomputation.baby_step(3); + precomputation.apply_block_frobenius(giant, giant, *divisor, context); + BOOST_CHECK(giant == expected); + BOOST_CHECK_THROW(precomputation_type(std::vector {}, *divisor, context), std::invalid_argument); + BOOST_CHECK_THROW(precomputation_type(std::vector {{fq12_value_type::one()}}, *divisor, context), + std::invalid_argument); +} + BOOST_AUTO_TEST_CASE(kaltofen_shoup_precomputation_rejects_a_zero_block_size) { using backend_type = polynomial_arithmetic::schoolbook_backend; using polynomial_type = typename backend_type::polynomial_type; @@ -352,12 +382,13 @@ BOOST_AUTO_TEST_CASE(kaltofen_shoup_coarse_blocks_extract_degree_intervals) { polynomial_type giant_step = precomputation.baby_step(block_size); polynomial_type factor; math::detail::kaltofen_shoup_coarse_block_factor(factor, input, giant_step, block_size, precomputation, - frobenius_context, arithmetic_context); + frobenius_context.divisor_context(), arithmetic_context); BOOST_CHECK(factor == first_block); - precomputation.apply_block_frobenius(giant_step, giant_step, frobenius_context, arithmetic_context); + precomputation.apply_block_frobenius(giant_step, giant_step, frobenius_context.divisor_context(), + arithmetic_context); math::detail::kaltofen_shoup_coarse_block_factor(factor, second_block, giant_step, 1, precomputation, - frobenius_context, arithmetic_context); + frobenius_context.divisor_context(), arithmetic_context); BOOST_CHECK(factor == second_block); } @@ -374,10 +405,12 @@ BOOST_AUTO_TEST_CASE(kaltofen_shoup_coarse_block_rejects_an_invalid_degree_count polynomial_type output; BOOST_CHECK_THROW(math::detail::kaltofen_shoup_coarse_block_factor(output, divisor, giant_step, 0, precomputation, - frobenius_context, arithmetic_context), + frobenius_context.divisor_context(), + arithmetic_context), std::invalid_argument); BOOST_CHECK_THROW(math::detail::kaltofen_shoup_coarse_block_factor(output, divisor, giant_step, 3, precomputation, - frobenius_context, arithmetic_context), + frobenius_context.divisor_context(), + arithmetic_context), std::invalid_argument); } @@ -417,7 +450,8 @@ BOOST_AUTO_TEST_CASE(kaltofen_shoup_fine_splitting_separates_exact_degrees) { precomputation, arithmetic_context, record_factor) == math::factorization_control::continue_factorization); - precomputation.apply_block_frobenius(giant_step, giant_step, frobenius_context, arithmetic_context); + precomputation.apply_block_frobenius(giant_step, giant_step, frobenius_context.divisor_context(), + arithmetic_context); BOOST_CHECK(math::detail::kaltofen_shoup_split_coarse_block(&factors, second_block, giant_step, 3, 1, precomputation, arithmetic_context, record_factor) == math::factorization_control::continue_factorization); diff --git a/libs/math/test/equal_degree_factorization.cpp b/libs/math/test/equal_degree_factorization.cpp index c5d09c910..b16f65359 100644 --- a/libs/math/test/equal_degree_factorization.cpp +++ b/libs/math/test/equal_degree_factorization.cpp @@ -248,6 +248,63 @@ BOOST_AUTO_TEST_CASE(cantor_zassenhaus_does_not_sample_an_already_irreducible_gr BOOST_CHECK_EQUAL(generated_coefficient_count, 0); } +BOOST_AUTO_TEST_CASE(single_irreducible_degree_group_preserves_callback_control_without_sampling) { + const polynomial_type group = {-value_type(3), value_type::zero(), value_type::one()}; + polynomial_arithmetic::polynomial_context context; + std::size_t draws = 0; + auto generator = [&] { + ++draws; + return value_type::one(); + }; + for (auto requested : + {math::factorization_control::continue_factorization, math::factorization_control::stop_factorization}) { + std::size_t callbacks = 0; + polynomial_type emitted; + const auto control = math::detail::factor_distinct_degree_group({group, 2}, context, generator, + [&](polynomial_type &&factor) { + emitted = std::move(factor); + ++callbacks; + return requested; + }); + BOOST_CHECK(control == requested); + BOOST_CHECK(emitted == group); + BOOST_CHECK_EQUAL(callbacks, 1); + BOOST_CHECK_EQUAL(draws, 0); + } + BOOST_CHECK_THROW( + math::detail::factor_distinct_degree_group( + {group, 2}, context, generator, + [](polynomial_type &&) -> math::factorization_control { throw std::runtime_error("consumer failed"); }), + std::runtime_error); + BOOST_CHECK_EQUAL(draws, 0); +} + +BOOST_AUTO_TEST_CASE(degree_group_checks_structure_before_emitting_a_single_factor) { + polynomial_arithmetic::polynomial_context context; + std::size_t draws = 0, callbacks = 0; + auto generator = [&] { + ++draws; + return value_type::one(); + }; + auto callback = [&](polynomial_type &&) { + ++callbacks; + return math::factorization_control::continue_factorization; + }; + const polynomial_type quadratic = {-value_type(3), value_type::zero(), value_type::one()}; + const std::array, 4> invalid = { + math::distinct_degree_factor {quadratic, 0}, + {{value_type::one()}, 1}, + {{value_type::one(), value_type(2)}, 1}, + {quadratic, 3}, + }; + for (const auto &group : invalid) { + BOOST_CHECK_THROW(math::detail::factor_distinct_degree_group(group, context, generator, callback), + std::invalid_argument); + } + BOOST_CHECK_EQUAL(draws, 0); + BOOST_CHECK_EQUAL(callbacks, 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()}; diff --git a/libs/math/test/polynomial_composition.cpp b/libs/math/test/polynomial_composition.cpp index b45c71c19..56bcf09f1 100644 --- a/libs/math/test/polynomial_composition.cpp +++ b/libs/math/test/polynomial_composition.cpp @@ -321,6 +321,59 @@ BOOST_AUTO_TEST_CASE(composition_matches_between_schoolbook_and_mixed_radix_back BOOST_CHECK(result == expected); } +BOOST_AUTO_TEST_CASE(explicit_composition_blocks_preserve_the_outer_contract_and_match_horner) { + using backend = polynomial_arithmetic::schoolbook_backend; + using polynomial = backend::polynomial_type; + polynomial modulus(12), inner(11), outer(17); + for (std::size_t i = 0; i < modulus.size(); ++i) + modulus[i] = fq12_value(13 * i + 2); + modulus.back() = fq12_value_type::one(); + for (std::size_t i = 0; i < inner.size(); ++i) + inner[i] = fq12_value(17 * i + 3); + for (std::size_t i = 0; i < outer.size(); ++i) + outer[i] = fq12_value(19 * i + 5); + + for (const std::size_t prepared_bytes : {0U, 1024U * 1024U}) { + for (const std::size_t power_limit : {1U, 7U, 20U}) { + polynomial_arithmetic::polynomial_context_options options; + options.modular_composition_cached_power_limit = power_limit; + options.modular_composition_prepared_byte_limit = prepared_bytes; + options.modular_composition_block_size = 9; // Larger than ceil(sqrt(17)) = 5. + polynomial_arithmetic::polynomial_context context(backend {}, options); + const math::polynomial_divisor_context divisor(modulus, 10, context); + const math::polynomial_composition_precomputation cache(inner, outer.size(), divisor, context); + BOOST_CHECK_EQUAL(cache.block_size(), std::min(9, power_limit)); + BOOST_CHECK_EQUAL(cache.maximum_outer_coefficient_count(), outer.size()); + if (power_limit > 1 && prepared_bytes != 0) + BOOST_CHECK_GT(cache.prepared_coefficient_bytes(), 0); + else + BOOST_CHECK_EQUAL(cache.prepared_coefficient_bytes(), 0); + polynomial expected, result = outer; + math::compose_mod_reference(expected, outer, inner, divisor, context); + math::compose_mod(result, result, cache, divisor, context); + BOOST_CHECK(result == expected); + math::compose_mod_reference(expected, expected, inner, divisor, context); + math::compose_mod(result, result, cache, divisor, context); + BOOST_CHECK(result == expected); + + const math::polynomial_composition_precomputation short_cache(inner, 3, divisor, context); + BOOST_CHECK_EQUAL(short_cache.block_size(), std::min(3, power_limit)); + BOOST_CHECK_THROW(math::compose_mod(result, outer, short_cache, divisor, context), std::invalid_argument); + } + } + polynomial_arithmetic::polynomial_context automatic; + const math::polynomial_divisor_context divisor(modulus, 10, automatic); + const math::polynomial_composition_precomputation automatic_cache(inner, outer.size(), divisor, automatic); + BOOST_CHECK_EQUAL(automatic_cache.block_size(), 5); + polynomial_arithmetic::polynomial_context_options invalid_options; + invalid_options.modular_composition_cached_power_limit = 0; + invalid_options.modular_composition_block_size = 9; + polynomial_arithmetic::polynomial_context invalid(backend {}, invalid_options); + polynomial result = outer; + BOOST_CHECK_THROW(math::compose_mod(result, outer, inner, divisor, invalid), std::invalid_argument); + BOOST_CHECK(result == outer); +} + BOOST_AUTO_TEST_CASE(prepared_composition_cache_obeys_its_budget_and_pads_short_powers) { using backend_type = polynomial_arithmetic::schoolbook_backend; using polynomial_type = typename backend_type::polynomial_type;