From de70899483d8f131273ab46b4971fba1d63c8414 Mon Sep 17 00:00:00 2001 From: Riccardo Abbate Date: Wed, 21 Oct 2026 15:23:51 -0400 Subject: [PATCH 1/2] Accelerate BN254 input coefficient conversion --- .../math/detail/bn254_small_prime_fft.hpp | 23 +++++++++++++++---- libs/math/test/small_prime_fft_conversion.cpp | 12 ++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/libs/math/include/nil/crypto3/math/detail/bn254_small_prime_fft.hpp b/libs/math/include/nil/crypto3/math/detail/bn254_small_prime_fft.hpp index 7507bd000..a73ef3195 100644 --- a/libs/math/include/nil/crypto3/math/detail/bn254_small_prime_fft.hpp +++ b/libs/math/include/nil/crypto3/math/detail/bn254_small_prime_fft.hpp @@ -128,7 +128,7 @@ namespace nil::crypto3::math::detail { } private: - template + template void convert_impl(std::span output, std::span coefficients, std::size_t stride) const { for (std::size_t channel = 0; channel < field_type::arity * primes_.size(); ++channel) { @@ -142,9 +142,24 @@ namespace nil::crypto3::math::detail { const std::size_t count = std::min(block_size, coefficients.size() - offset); for (std::size_t i = 0; i < count; ++i) { std::array words {}; - // Use the canonical value, not the field's internal Montgomery representation. - const auto canonical = coefficients[offset + i].coordinate(coordinate).to_integral(); - boost::multiprecision::export_bits(canonical, words.begin(), chunk_bits, false); + using base_backend_type = typename Params::base_field_type::integral_type::backend_type; + if constexpr (base_backend_type::limb_bits == 64) { + namespace fp12_fast = algebra::fields::detail::fp12_fast; + // The stored value is a*R mod p, with R = 2^256. Zero-extend it + // into the low half of montgomery; one reduction recovers a in [0,p). + typename Params::limb_array montgomery {}; + const auto stored = fp12_fast::load_limbs( + coefficients[offset + i].coordinate(coordinate).data.backend().base_data()); + std::copy(stored.begin(), stored.end(), montgomery.begin()); + std::array canonical; + fp12_fast::montgomery_reduce(canonical.data(), montgomery); + // pack only extracts five 52-bit chunks; it does not rescale them. + // Packing the canonical value preserves the lift of field -1 to p-1. + words = fp12_fast::montgomery_radix52::pack(canonical.data()); + } else { + const auto canonical = coefficients[offset + i].coordinate(coordinate).to_integral(); + boost::multiprecision::export_bits(canonical, words.begin(), chunk_bits, false); + } for (std::size_t j = 0; j < chunk_count; ++j) { chunks[i][j] = static_cast(words[j]); } diff --git a/libs/math/test/small_prime_fft_conversion.cpp b/libs/math/test/small_prime_fft_conversion.cpp index fe6fcab70..91a81c82a 100644 --- a/libs/math/test/small_prime_fft_conversion.cpp +++ b/libs/math/test/small_prime_fft_conversion.cpp @@ -154,6 +154,18 @@ BOOST_AUTO_TEST_CASE(canonical_values_and_chunk_boundaries_match_integer_oracle) values.push_back(power); values.push_back(power + 1); } + // Also cross limb boundaries in the stored Montgomery value a*R mod p. + // Multiplying by R^-1 constructs canonical inputs independently of the decoder. + using base_backend_type = fq::integral_type::backend_type; + const cpp_int radix = cpp_int(1) << (base_backend_type::limb_bits * base_backend_type::internal_limb_count); + const cpp_int inverse_radix = boost::multiprecision::powm(radix, cpp_int(characteristic - 2), characteristic); + values.push_back(inverse_radix); + values.push_back(((characteristic - 1) * inverse_radix) % characteristic); + for (unsigned bit = base_backend_type::limb_bits; bit < 254; bit += base_backend_type::limb_bits) { + for (int delta : {-1, 0, 1}) { + values.push_back((((cpp_int(1) << bit) + delta) * inverse_radix) % characteristic); + } + } for (const auto &prime : detail::small_prime_fft_primes) { for (int delta : {-1, 0, 1}) { values.push_back(cpp_int(prime.modulus) + delta); From ff7da5311697710ad031cecd06755d4d0cb91739 Mon Sep 17 00:00:00 2001 From: Riccardo Abbate Date: Wed, 21 Oct 2026 15:39:09 -0400 Subject: [PATCH 2/2] fix --- .../math/detail/bn254_small_prime_fft.hpp | 2 +- libs/math/test/CMakeLists.txt | 1 + .../test/small_prime_fft_conversion_32bit.cpp | 43 +++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 libs/math/test/small_prime_fft_conversion_32bit.cpp diff --git a/libs/math/include/nil/crypto3/math/detail/bn254_small_prime_fft.hpp b/libs/math/include/nil/crypto3/math/detail/bn254_small_prime_fft.hpp index a73ef3195..03f0bc146 100644 --- a/libs/math/include/nil/crypto3/math/detail/bn254_small_prime_fft.hpp +++ b/libs/math/include/nil/crypto3/math/detail/bn254_small_prime_fft.hpp @@ -148,7 +148,7 @@ namespace nil::crypto3::math::detail { // The stored value is a*R mod p, with R = 2^256. Zero-extend it // into the low half of montgomery; one reduction recovers a in [0,p). typename Params::limb_array montgomery {}; - const auto stored = fp12_fast::load_limbs( + const auto stored = fp12_fast::load_limbs( coefficients[offset + i].coordinate(coordinate).data.backend().base_data()); std::copy(stored.begin(), stored.end(), montgomery.begin()); std::array canonical; diff --git a/libs/math/test/CMakeLists.txt b/libs/math/test/CMakeLists.txt index 197d3f59a..5fd2f07ec 100644 --- a/libs/math/test/CMakeLists.txt +++ b/libs/math/test/CMakeLists.txt @@ -83,6 +83,7 @@ endforeach() set(COMPILE_TIME_TESTS_NAMES "polynomial_algorithm_constraints" "polynomial_concepts" + "small_prime_fft_conversion_32bit" "static_matrix") foreach(TEST_NAME ${COMPILE_TIME_TESTS_NAMES}) diff --git a/libs/math/test/small_prime_fft_conversion_32bit.cpp b/libs/math/test/small_prime_fft_conversion_32bit.cpp new file mode 100644 index 000000000..d04383eb1 --- /dev/null +++ b/libs/math/test/small_prime_fft_conversion_32bit.cpp @@ -0,0 +1,43 @@ +//---------------------------------------------------------------------------// +// 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. +//---------------------------------------------------------------------------// + +#include +// Exercise the fallback with 32-bit Boost limbs even on a 64-bit host. Configure +// Boost first so subsequent includes cannot re-enable its 128-bit integer support. +#undef BOOST_HAS_INT128 + +#include + +#include + +using converter_type = nil::crypto3::math::detail::bn254_small_prime_fft_converter; +using base_field_type = nil::crypto3::algebra::fields::alt_bn128_base_field<254>; +static_assert(base_field_type::integral_type::backend_type::limb_bits == 32); + +void instantiate_32bit_conversion() { + // This call must discard the 64-bit Montgomery helper rather than instantiate it. + std::array input = {converter_type::value_type::zero()}; + std::array output {}; + converter_type(1).convert(output, input, 1); +}