diff --git a/libs/math/docs/polynomial_recovery.md b/libs/math/docs/polynomial_recovery.md index 4df8901ac..3cf7d4b0b 100644 --- a/libs/math/docs/polynomial_recovery.md +++ b/libs/math/docs/polynomial_recovery.md @@ -2,17 +2,18 @@ @tableofcontents -Crypto3.Math provides two complementary recovery primitives: +Crypto3.Math provides three complementary recovery facilities: -* square testing and square roots in a finite polynomial quotient field; and -* bounded rational reconstruction from a residue modulo a polynomial. +* square testing and square roots in a finite polynomial quotient field; +* bounded rational reconstruction from a residue modulo a polynomial; and +* recovery of the fixed `X`-norm representation of an irreducible polynomial. Together they can recover representations of irreducible factors by the polynomial norm form P(X)^2 - X * Q(X)^2. -The library exposes the generic arithmetic operations. Applications remain responsible for selecting factors, degree -bounds, random sources, and any policy for combining or rejecting recovered representations. +The recovery operation owns the degree bounds and exact normalization for this norm equation. The caller supplies the +polynomial arithmetic context and the coefficient-field generator. ### Header map @@ -22,6 +23,7 @@ bounds, random sources, and any policy for combining or rejecting recovered repr | Coefficient-field square-root helpers | `` | | Quotient-field square testing and roots | `` | | Bounded rational reconstruction | `` | +| `X`-norm reconstruction | `` | ## Field orders and coefficient square roots @@ -64,9 +66,9 @@ characterize squares if the quotient has zero divisors. `square_root_mod` uses Tonelli-Shanks in `K[X]/(B)`. Repeated calls should share two immutable precomputations: -1. `polynomial_divisor_context` caches the divisor and the inverse needed for modular reduction. -2. `polynomial_square_root_context` caches the decomposition - `order(K)^d - 1 = odd_order * 2^two_adicity` and a suitable quadratic nonresidue raised to `odd_order`. +* `polynomial_divisor_context` caches the divisor and the inverse needed for modular reduction. +* `polynomial_square_root_context` caches the decomposition + `order(K)^d - 1 = odd_order * 2^two_adicity` and a suitable quadratic nonresidue raised to `odd_order`. ```cpp #include @@ -146,34 +148,71 @@ reconstructs with bounds `degree(P) <= 0` and `degree(Q) <= 2` as P = 1, Q = 2 + 2X + X^2. -## Recovering a polynomial norm representation +## `X`-norm reconstruction -The square-root and reconstruction APIs fit together as follows. For one irreducible factor `B` of degree `d`: +`recover_irreducible_polynomial_x_norm_representation` composes quotient-field square roots, bounded rational +reconstruction, and coefficient-field normalization to recover polynomials `P` and `Q` satisfying -1. Represent the indeterminate by `x = {0, 1}` and test whether it is a square modulo `B`. -2. If it is square, compute `R` such that `R^2 = X mod B`. -3. Rationally reconstruct `R` with + P^2 - X * Q^2 = g. - maximum_numerator_degree = floor(d / 2), - maximum_denominator_degree = floor((d - 1) / 2). +The name explicitly identifies `X` as the quadratic element: the represented element is `P + Q * sqrt(X)`, whose norm +is `P^2 - X * Q^2`. The input `g` must be a canonical nonconstant irreducible polynomial. Irreducibility is a caller +precondition and is not tested. - This produces `P = R * Q mod B`. -4. Squaring the congruence gives +```cpp +#include + +// arithmetic_context and coefficient_generator are caller-owned. +auto representation = math::recover_irreducible_polynomial_x_norm_representation( + g, arithmetic_context, coefficient_generator); + +if (representation) { + polynomial_type exact_norm = math::evaluate_polynomial_x_norm( + *representation, arithmetic_context); + // exact_norm == g +} +``` + +The coefficient generator returns coefficient-field values. The recovery operation adapts those values into canonical +degree-below-`degree(g)` representatives of `K[X]/(g)`. The generator remains caller-owned and must eventually supply +coefficients forming a quotient-field nonsquare. + +On success, the optional contains `polynomial_x_norm_representation`. Its `p` and `q` members hold the +two recovered coefficient polynomials. - P^2 - X * Q^2 = 0 mod B. +The function reduces `{0, 1}` modulo `g`, including when `g` is linear, and recovers `R` with `R^2 = X mod g`. It then +reconstructs `P = R * Q mod g` with - The degree bounds make the left side have degree at most `d`, so it is a scalar multiple of `B`. + degree(P) <= floor(degree(g) / 2), + degree(Q) <= floor((degree(g) - 1) / 2). -This is the local representation of `B` by the norm from adjoining a square root of `X`. Representations compose -multiplicatively: +These bounds imply + + P^2 - X * Q^2 = lambda * g + +for a coefficient-field scalar `lambda`. A nonzero square `lambda` is removed by scaling both outputs by +`sqrt(lambda^-1)`. The normalized norm is evaluated again and compared exactly with `g` before success is returned. +Both polynomial squares use the caller's arithmetic context; multiplication by `X` is a coefficient shift. + +| Outcome | Contract | +|---|---| +| Representation returned | The degree bounds hold and `evaluate_polynomial_x_norm(result, context) == g`. | +| No value returned | `X` is nonsquare modulo `g`, bounded reconstruction fails, or `lambda` is zero or nonsquare. | +| `std::invalid_argument` | The input is empty, noncanonical, zero, or constant, or a composed API contract is violated. | +| `std::logic_error` | An operation reported success but a required modular, scalar-multiple, or final exact identity is inconsistent. | + +This is the local representation of `g` by the norm from adjoining a square root of `X`. +`multiply_polynomial_x_norm_representations(left, right, context)` composes two representations using (P1^2 - X Q1^2) * (P2^2 - X Q2^2) = (P1 P2 + X Q1 Q2)^2 - X * (P1 Q2 + P2 Q1)^2. +All four polynomial products use the supplied arithmetic context, while multiplication by `X` is a coefficient shift. + Consequently, a caller can factor a target polynomial, recover eligible irreducible factors independently, account for multiplicities and the scalar leading coefficient, and combine the local representations. Crypto3.Math deliberately -keeps that application-level policy separate from the generic factorization, quotient-field square-root, and bounded -rational-reconstruction primitives. +keeps that application-level policy separate from the generic factorization, quotient-field square-root, bounded +rational-reconstruction, and `X`-norm reconstruction facilities. ### Worked example over F7 @@ -211,16 +250,16 @@ Then the scalar is corrected exactly: P'^2 - X Q'^2 = H. -This example performs the same local steps required for a higher-degree factor: factor, test whether `X` is square in -the quotient, compute its root, reconstruct bounded `P` and `Q`, and normalize the remaining coefficient-field -scalar. Combining several eligible factors then uses the multiplicative identity above. +The recovery API performs this square test, quotient-field square root, bounded reconstruction, scalar normalization, +and final exact verification using the supplied polynomial context and coefficient generator. Combining several +eligible factors then uses the multiplicative identity above. ## Reuse and performance -The [polynomial arithmetic infrastructure](@ref math_polynomial_arithmetic) describes these contexts in detail. Reuse -one polynomial arithmetic context throughout a recovery, one divisor context for every operation modulo the same `B`, -and one square-root context for repeated roots modulo that divisor. This avoids rebuilding divisor inverses, -multiplicative-group decompositions, nonresidue powers, backend plans, and scratch storage. +The [polynomial arithmetic infrastructure](@ref math_polynomial_arithmetic) describes these contexts in detail. The +recovery API reuses the caller's polynomial arithmetic context throughout recovery. It constructs one divisor context +and one square-root context and reuses them for all operations within that call. Direct users of the lower-level APIs +can retain those contexts across repeated operations modulo the same divisor. Let `Q` 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/reconstruction/polynomial_x_norm_reconstruction.hpp b/libs/math/include/nil/crypto3/math/polynomial/reconstruction/polynomial_x_norm_reconstruction.hpp new file mode 100644 index 000000000..77ebf2472 --- /dev/null +++ b/libs/math/include/nil/crypto3/math/polynomial/reconstruction/polynomial_x_norm_reconstruction.hpp @@ -0,0 +1,249 @@ +//---------------------------------------------------------------------------// +// Copyright (c) 2026 +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +//---------------------------------------------------------------------------// + +#ifndef CRYPTO3_MATH_POLYNOMIAL_X_NORM_RECONSTRUCTION_HPP +#define CRYPTO3_MATH_POLYNOMIAL_X_NORM_RECONSTRUCTION_HPP + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace nil::crypto3::math { + + /** Coefficients of P + Q * sqrt(X), represented by the polynomials P and Q. */ + template + struct polynomial_x_norm_representation { + Polynomial p; + Polynomial q; + }; + + namespace detail { + + template + bool is_canonical_polynomial_x_norm_representation( + const polynomial_x_norm_representation &representation) { + using value_type = typename Polynomial::value_type; + const auto is_canonical = [](const Polynomial &polynomial) { + return !polynomial.empty() && + (polynomial.size() == 1 || polynomial[polynomial.size() - 1] != value_type {}); + }; + return is_canonical(representation.p) && is_canonical(representation.q); + } + + } // namespace detail + + /** + * Evaluate the polynomial norm of P + Q * sqrt(X): + * + * (P + Q * sqrt(X)) * (P - Q * sqrt(X)) = P^2 - X * Q^2. + * + * Both squares use the caller-owned arithmetic context. Multiplication by X is a coefficient shift and therefore + * does not require a polynomial product. + * + * @throws std::invalid_argument if P or Q is empty or noncanonical. + */ + template + typename Backend::polynomial_type evaluate_polynomial_x_norm( + const polynomial_x_norm_representation &representation, + polynomial_arithmetic::polynomial_context &arithmetic_context) { + using polynomial_type = typename Backend::polynomial_type; + if (!detail::is_canonical_polynomial_x_norm_representation(representation)) { + throw std::invalid_argument("polynomial X-norm evaluation requires canonical nonempty inputs"); + } + + polynomial_type p_squared; + polynomial_type q_squared; + polynomial_type x_q_squared; + polynomial_type norm; + arithmetic_context.square(p_squared, representation.p); + arithmetic_context.square(q_squared, representation.q); + shift_left(x_q_squared, q_squared, 1); + subtraction(norm, p_squared, x_q_squared); + return norm; + } + + /** + * Multiply two polynomial X-norm representations using + * + * P = P1 * P2 + X * Q1 * Q2, + * Q = P1 * Q2 + Q1 * P2. + * + * The returned representation has norm equal to the product of the input norms. Every polynomial product uses the + * caller-owned arithmetic context; multiplication by X is a coefficient shift. + * + * @throws std::invalid_argument if an input polynomial is empty or noncanonical. + */ + template + polynomial_x_norm_representation multiply_polynomial_x_norm_representations( + const polynomial_x_norm_representation &left, + const polynomial_x_norm_representation &right, + polynomial_arithmetic::polynomial_context &arithmetic_context) { + using polynomial_type = typename Backend::polynomial_type; + using representation_type = polynomial_x_norm_representation; + + if (!detail::is_canonical_polynomial_x_norm_representation(left) || + !detail::is_canonical_polynomial_x_norm_representation(right)) { + throw std::invalid_argument("polynomial X-norm multiplication requires canonical nonempty inputs"); + } + + polynomial_type p_product; + polynomial_type q_product; + polynomial_type shifted_q_product; + polynomial_type result_p; + arithmetic_context.multiply(p_product, left.p, right.p); + arithmetic_context.multiply(q_product, left.q, right.q); + shift_left(shifted_q_product, q_product, 1); + addition(result_p, p_product, shifted_q_product); + + polynomial_type left_p_right_q; + polynomial_type left_q_right_p; + polynomial_type result_q; + arithmetic_context.multiply(left_p_right_q, left.p, right.q); + arithmetic_context.multiply(left_q_right_p, left.q, right.p); + addition(result_q, left_p_right_q, left_q_right_p); + + return representation_type {std::move(result_p), std::move(result_q)}; + } + + /** + * Recover P and Q satisfying + * + * P^2 - X * Q^2 = g, + * + * with degree(P) at most floor(degree(g) / 2) and degree(Q) at most + * floor((degree(g) - 1) / 2). The coefficient generator supplies field elements used to find a quadratic + * nonresidue in K[X]/(g); it remains owned by the caller. + * + * Irreducibility of g is a caller precondition and is not tested. The coefficient generator must eventually produce + * coefficients forming a nonsquare canonical representative of degree below degree(g). + * + * @return a normalized representation whose evaluated norm is exactly g; no value if X is nonsquare modulo g, + * bounded rational reconstruction fails, or the resulting nonzero scalar multiple of g cannot be + * normalized. + * @throws std::invalid_argument if g is empty, noncanonical, zero, or constant, or another documented precondition + * of a composed polynomial operation is violated. + * @throws std::logic_error if a composed operation reports success but its resulting identities are inconsistent. + */ + template + requires algebra::FieldValue && + std::constructible_from && + requires(typename Backend::polynomial_type &polynomial, Generator &generator, + const typename Backend::polynomial_type::value_type &value) { + polynomial[0] = generator(); + { value.is_square() } -> std::convertible_to; + } + std::optional> + recover_irreducible_polynomial_x_norm_representation( + const typename Backend::polynomial_type &g, + polynomial_arithmetic::polynomial_context &arithmetic_context, + Generator &coefficient_generator) { + using polynomial_type = typename Backend::polynomial_type; + using value_type = typename polynomial_type::value_type; + using representation_type = polynomial_x_norm_representation; + + if (g.empty() || (g.size() > 1 && g[g.size() - 1] == value_type {})) { + throw std::invalid_argument("polynomial X-norm recovery requires a canonical nonempty polynomial"); + } + if (g.size() == 1) { + throw std::invalid_argument("polynomial X-norm recovery requires a nonconstant polynomial"); + } + + const std::size_t degree = g.size() - 1; + const std::size_t inverse_precision = std::max(1, degree - 1); + polynomial_divisor_context divisor_context(g, inverse_precision, arithmetic_context); + + // Work with the canonical representative of X in K[X]/(g). Reduction is necessary when g is linear. + const polynomial_type x = {value_type::zero(), value_type::one()}; + polynomial_type x_mod_g; + remainder(x_mod_g, x, divisor_context, arithmetic_context); + if (!is_square_mod(x_mod_g, divisor_context, arithmetic_context)) { + return std::nullopt; + } + + // Adapt caller-generated field coefficients into canonical representatives of K[X]/(g). No random source or + // polynomial backend is constructed here. + auto quotient_representative_generator = [&]() { + polynomial_type representative(degree); + for (std::size_t index = 0; index < degree; ++index) { + representative[index] = coefficient_generator(); + } + condense(representative); + return representative; + }; + polynomial_square_root_context square_root_context(divisor_context, arithmetic_context, + quotient_representative_generator); + + // Recover R with R^2 = X mod g. A failure here contradicts the successful square test above. + polynomial_type root; + if (!square_root_mod(root, x_mod_g, square_root_context, arithmetic_context)) { + throw std::logic_error("polynomial X-norm recovery failed after X was reported square modulo g"); + } + + // Reconstruct P = R * Q mod g with the unique degree bounds required by the norm equation. + polynomial_type p; + polynomial_type q; + if (!rational_reconstruct(p, q, root, g, degree / 2, (degree - 1) / 2, arithmetic_context)) { + return std::nullopt; + } + + // The two modular identities imply P^2 - X * Q^2 = lambda * g. The degree bounds ensure that lambda is a + // scalar. A nonzero remainder or nonconstant quotient would contradict those successful operations. + representation_type representation {std::move(p), std::move(q)}; + const polynomial_type norm = evaluate_polynomial_x_norm(representation, arithmetic_context); + polynomial_type scalar_quotient; + polynomial_type scalar_remainder; + divrem(scalar_quotient, scalar_remainder, norm, divisor_context, arithmetic_context); + if (!is_zero(scalar_remainder) || scalar_quotient.size() != 1) { + throw std::logic_error("polynomial X-norm reconstruction produced an inconsistent scalar multiple"); + } + + const value_type lambda = scalar_quotient[0]; + if (lambda.is_zero() || !lambda.is_square()) { + return std::nullopt; + } + + // Multiplying P and Q by sqrt(lambda^-1) changes their norm from lambda * g to exactly g. + const value_type normalization = algebra::fields::sqrt_known_square(lambda.inversed()); + scalar_multiplication(representation.p, representation.p, normalization); + scalar_multiplication(representation.q, representation.q, normalization); + + if (evaluate_polynomial_x_norm(representation, arithmetic_context) != g) { + throw std::logic_error("normalized polynomial X-norm representation failed exact verification"); + } + return std::move(representation); + } + +} // namespace nil::crypto3::math + +#endif // CRYPTO3_MATH_POLYNOMIAL_X_NORM_RECONSTRUCTION_HPP diff --git a/libs/math/test/CMakeLists.txt b/libs/math/test/CMakeLists.txt index 21c5b09f3..94c805953 100644 --- a/libs/math/test/CMakeLists.txt +++ b/libs/math/test/CMakeLists.txt @@ -55,6 +55,7 @@ set(TESTS_NAMES "polynomial_exponentiation" "polynomial_rational_reconstruction" "polynomial_square_root" + "polynomial_x_norm_reconstruction" "polynomial_composition" "polynomial_frobenius" "square_free_factorization" diff --git a/libs/math/test/polynomial_x_norm_reconstruction.cpp b/libs/math/test/polynomial_x_norm_reconstruction.cpp new file mode 100644 index 000000000..89d6674e2 --- /dev/null +++ b/libs/math/test/polynomial_x_norm_reconstruction.cpp @@ -0,0 +1,227 @@ +//---------------------------------------------------------------------------// +// Copyright (c) 2026 +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +//---------------------------------------------------------------------------// + +#define BOOST_TEST_MODULE polynomial_x_norm_reconstruction_test + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + namespace fields = nil::crypto3::algebra::fields; + namespace math = nil::crypto3::math; + namespace polynomial_arithmetic = math::polynomial_arithmetic; + + using field_type = fields::babybear; + using value_type = field_type::value_type; + using backend_type = polynomial_arithmetic::schoolbook_backend; + using polynomial_type = typename backend_type::polynomial_type; + + using fq_field_type = fields::alt_bn128_base_field<254>; + using fq_value_type = fq_field_type::value_type; + using fq12_field_type = fields::fp12_2over3over2>; + using fq12_value_type = fq12_field_type::value_type; + using fq12_schoolbook_backend = polynomial_arithmetic::schoolbook_backend; + using fq12_mixed_radix_backend = polynomial_arithmetic::mixed_radix_backend; + + value_type first_quadratic_non_residue() { + value_type candidate(2); + while (candidate.is_square()) { + candidate = candidate + value_type::one(); + } + return candidate; + } + + fq12_value_type fq12_scalar(std::size_t value) { + fq12_value_type result = fq12_value_type::zero(); + result.coordinate(0) = fq_value_type(value); + return result; + } + + template + void check_bn254_fq12_linear_recovery(polynomial_arithmetic::polynomial_context &arithmetic_context, + std::size_t seed) { + using extension_polynomial_type = typename Backend::polynomial_type; + + const extension_polynomial_type g = {fq12_scalar(9), -fq12_scalar(4)}; + boost::random::mt19937 rng(seed); + auto coefficient_generator = [&] { return nil::crypto3::algebra::random_element(rng); }; + + const auto result = math::recover_irreducible_polynomial_x_norm_representation(g, arithmetic_context, + coefficient_generator); + BOOST_REQUIRE(result.has_value()); + BOOST_CHECK_EQUAL(result->p.size(), 1); + BOOST_CHECK_EQUAL(result->q.size(), 1); + BOOST_CHECK(result->p[0] * result->p[0] == fq12_scalar(9)); + BOOST_CHECK(result->q[0] * result->q[0] == fq12_scalar(4)); + BOOST_CHECK(math::evaluate_polynomial_x_norm(*result, arithmetic_context) == g); + } +} // namespace + +BOOST_AUTO_TEST_SUITE(polynomial_x_norm_reconstruction_test_suite) + +BOOST_AUTO_TEST_CASE(multiplies_x_norm_representations_and_preserves_the_exact_norm_product) { + polynomial_arithmetic::polynomial_context arithmetic_context; + const math::polynomial_x_norm_representation left = { + polynomial_type {value_type::one(), value_type(2)}, polynomial_type {value_type(3)}}; + const math::polynomial_x_norm_representation right = { + polynomial_type {value_type(4), value_type(5)}, polynomial_type {value_type(6), value_type(7)}}; + + const auto product = + math::multiply_polynomial_x_norm_representations(left, right, arithmetic_context); + BOOST_CHECK(product.p == polynomial_type({value_type(4), value_type(31), value_type(31)})); + BOOST_CHECK(product.q == polynomial_type({value_type(18), value_type(34), value_type(14)})); + + const polynomial_type left_norm = math::evaluate_polynomial_x_norm(left, arithmetic_context); + const polynomial_type right_norm = math::evaluate_polynomial_x_norm(right, arithmetic_context); + polynomial_type expected_norm_product; + arithmetic_context.multiply(expected_norm_product, left_norm, right_norm); + BOOST_CHECK(math::evaluate_polynomial_x_norm(product, arithmetic_context) == expected_norm_product); + + const math::polynomial_x_norm_representation zero = {polynomial_type {value_type::zero()}, + polynomial_type {value_type::zero()}}; + const auto zero_product = + math::multiply_polynomial_x_norm_representations(zero, right, arithmetic_context); + BOOST_CHECK(zero_product.p == polynomial_type({value_type::zero()})); + BOOST_CHECK(zero_product.q == polynomial_type({value_type::zero()})); +} + +BOOST_AUTO_TEST_CASE(x_norm_representation_multiplication_rejects_malformed_inputs) { + polynomial_arithmetic::polynomial_context arithmetic_context; + const math::polynomial_x_norm_representation valid = {polynomial_type {value_type::one()}, + polynomial_type {value_type::zero()}}; + math::polynomial_x_norm_representation malformed = valid; + malformed.q.get_storage().clear(); + + BOOST_CHECK_THROW( + math::multiply_polynomial_x_norm_representations(valid, malformed, arithmetic_context), + std::invalid_argument); +} + +BOOST_AUTO_TEST_CASE(recovers_and_normalizes_an_irreducible_quadratic_with_the_stated_degree_bounds) { + polynomial_arithmetic::polynomial_context arithmetic_context; + const math::polynomial_x_norm_representation original = { + polynomial_type {value_type::one(), value_type::one()}, polynomial_type {value_type(9)}}; + const polynomial_type g = math::evaluate_polynomial_x_norm(original, arithmetic_context); + + // The discriminant of g is 81 * 77. Since 81 is square and 77 is nonsquare, g is irreducible. + BOOST_REQUIRE(!value_type(77).is_square()); + BOOST_REQUIRE(g == polynomial_type({value_type::one(), value_type::zero() - value_type(79), value_type::one()})); + + boost::random::mt19937 rng(0x584E4F52); + auto coefficient_generator = [&] { return nil::crypto3::algebra::random_element(rng); }; + const auto result = math::recover_irreducible_polynomial_x_norm_representation(g, arithmetic_context, + coefficient_generator); + + BOOST_REQUIRE(result.has_value()); + BOOST_CHECK_LE(result->p.size() - 1, 1); + BOOST_CHECK_LE(result->q.size() - 1, 0); + // Rational reconstruction makes Q monic, initially producing lambda = 1/81. Exact normalization restores Q^2 = 81. + BOOST_CHECK(result->q[0] != value_type::one()); + BOOST_CHECK(result->q[0] * result->q[0] == value_type(81)); + BOOST_CHECK(math::evaluate_polynomial_x_norm(*result, arithmetic_context) == g); +} + +BOOST_AUTO_TEST_CASE(returns_no_value_when_x_is_nonsquare_modulo_the_irreducible_polynomial) { + const value_type non_residue = first_quadratic_non_residue(); + BOOST_REQUIRE((value_type::zero() - value_type::one()).is_square()); + const polynomial_type g = {value_type::zero() - non_residue, value_type::zero(), value_type::one()}; + polynomial_arithmetic::polynomial_context arithmetic_context; + std::size_t generator_calls = 0; + auto coefficient_generator = [&] { + ++generator_calls; + return value_type::zero(); + }; + + const auto result = math::recover_irreducible_polynomial_x_norm_representation(g, arithmetic_context, + coefficient_generator); + BOOST_CHECK(!result.has_value()); + BOOST_CHECK_EQUAL(generator_calls, 0); +} + +BOOST_AUTO_TEST_CASE(returns_no_value_when_the_scalar_multiple_cannot_be_normalized) { + const value_type non_residue = first_quadratic_non_residue(); + const value_type minus_one = value_type::zero() - value_type::one(); + BOOST_REQUIRE(minus_one.is_square()); + + // X is one modulo non_residue * (X - 1), but the reconstructed norm is multiplied by + // lambda = -non_residue^-1. This lambda is nonsquare and cannot be removed by scaling P and Q. + const polynomial_type g = {value_type::zero() - non_residue, non_residue}; + polynomial_arithmetic::polynomial_context arithmetic_context; + auto coefficient_generator = [&] { return non_residue; }; + + const auto result = math::recover_irreducible_polynomial_x_norm_representation(g, arithmetic_context, + coefficient_generator); + BOOST_CHECK(!result.has_value()); +} + +BOOST_AUTO_TEST_CASE(recovers_a_linear_bn254_fq12_norm_with_the_schoolbook_backend) { + polynomial_arithmetic::polynomial_context arithmetic_context; + check_bn254_fq12_linear_recovery(arithmetic_context, 0xF0125001); +} + +BOOST_AUTO_TEST_CASE(recovers_a_linear_bn254_fq12_norm_with_the_mixed_radix_backend) { + polynomial_arithmetic::polynomial_context arithmetic_context { + fq12_mixed_radix_backend(3)}; + check_bn254_fq12_linear_recovery(arithmetic_context, 0xF0125002); +} + +BOOST_AUTO_TEST_CASE(rejects_malformed_zero_and_constant_inputs) { + polynomial_arithmetic::polynomial_context arithmetic_context; + auto coefficient_generator = [] { return value_type::one(); }; + + polynomial_type empty; + empty.get_storage().clear(); + BOOST_CHECK_THROW(math::recover_irreducible_polynomial_x_norm_representation( + empty, arithmetic_context, coefficient_generator), + std::invalid_argument); + + polynomial_type noncanonical(2); + noncanonical[0] = value_type::one(); + noncanonical[1] = value_type::zero(); + BOOST_CHECK_THROW(math::recover_irreducible_polynomial_x_norm_representation( + noncanonical, arithmetic_context, coefficient_generator), + std::invalid_argument); + + BOOST_CHECK_THROW(math::recover_irreducible_polynomial_x_norm_representation( + polynomial_type {value_type::zero()}, arithmetic_context, coefficient_generator), + std::invalid_argument); + BOOST_CHECK_THROW(math::recover_irreducible_polynomial_x_norm_representation( + polynomial_type {value_type::one()}, arithmetic_context, coefficient_generator), + std::invalid_argument); +} + +BOOST_AUTO_TEST_SUITE_END()