/// @file grotto/offset_jet.hpp /// @brief Binomial-basis jet after the public offset is opened. /// @details The dealer keys one incremental comparison whose payload is the /// vector of \f$\binom{\mathrm{center}}{k}\f$ in \f$\mathbb{Z}/2^{64}\f$, /// for \f$k = 0,\ldots,d\f$. The seed spine is stored once. After `eta` opens, the same knot shift and /// carry cut as offset poly refine the pieces. A public /// Chu–Vandermonde convolution by the piece's carry `kappa` /// \f$\binom{c+\kappa}{k} =\sum_j\binom{c}{j}\binom{\kappa}{k-j}\f$ /// yields additive shares of the jet at the wrapped input. Public /// dots against that jet are free: /// - value of \f$\sum a_k\binom{x}{k}\f$; /// - forward difference, by Pascal's /// \f$\binom{x+1}{k}-\binom{x}{k}=\binom{x}{k-1}\f$; /// - hockey-stick prefix \f$\sum_{i #include #include #include #include namespace grotto { inline constexpr std::size_t offset_jet_max_degree = offset_poly_max_degree; template struct offset_jet_keys { static_assert(std::is_integral_v, "offset jet domain must be an integer group"); using input_type = InputT; std::size_t degree = 0; bool verifiable = false; /// @brief One `idcf(gt)` of `vec`. offset_horner_detail::lane_keys keys{}; offset_horner_detail::lane_keys keys_v{}; std::vector> wrap_share; }; namespace offset_jet_detail { inline void check_degree(std::size_t degree) { if (degree > offset_jet_max_degree) throw std::invalid_argument("offset jet: degree exceeds 16"); } /// @brief \f$v_2(k!)\f$ for \f$k \le 16\f$. inline unsigned val2_factorial(unsigned k) noexcept { unsigned v = 0; for (unsigned p = 2; p <= k; p <<= 1) v += k / p; return v; } /// @brief Odd part of \f$k!\f$ and \f$v_2(k!)\f$. inline uint64_t odd_factorial(unsigned k, unsigned & val2) noexcept { val2 = 0; uint64_t odd = 1; for (unsigned i = 1; i <= k; ++i) { unsigned x = i; while ((x & 1u) == 0u) { x >>= 1u; ++val2; } odd *= x; } return odd; } /// @brief Modular inverse of an odd unit modulo \f$2^{64}\f$. inline uint64_t inv_odd_u64(uint64_t a) noexcept { uint64_t x = a; x *= 2 - a * x; x *= 2 - a * x; x *= 2 - a * x; x *= 2 - a * x; x *= 2 - a * x; return x; } /// @brief \f$\binom{n}{k} \bmod 2^{64}\f$ for a nonnegative upper index. HEDLEY_CONST HEDLEY_NO_THROW inline uint64_t binom_u64(uint64_t n, unsigned k) noexcept { if (k == 0) return 1; if (k > offset_jet_max_degree) return 0; unsigned val2 = 0; const uint64_t odd = odd_factorial(k, val2); const unsigned bits = 64u + val2; const unsigned __int128 mask = (bits >= 128) ? ~static_cast(0) : (static_cast(1) << bits) - 1u; unsigned __int128 falling = 1; for (unsigned i = 0; i < k; ++i) { const unsigned __int128 term = static_cast(n - i) & mask; falling = (falling * term) & mask; } falling = (falling * inv_odd_u64(odd)) & mask; falling >>= val2; return static_cast(falling); } /// @brief \f$\binom{n}{k} \bmod 2^{64}\f$ for a signed upper index (carry `kappa`). HEDLEY_CONST HEDLEY_NO_THROW inline uint64_t binom_i64(std::int64_t n, unsigned k) noexcept { if (k == 0) return 1; if (k > offset_jet_max_degree) return 0; if (n >= 0) return binom_u64(static_cast(n), k); // \f$\binom{-m}{k}=(-1)^k\binom{m+k-1}{k}\f$ with \f$m=-n>0\f$. const uint64_t m = static_cast(-n); const uint64_t mag = binom_u64(m + k - 1u, k); return (k & 1u) ? static_cast(0) - mag : mag; } inline std::vector chu_vandermonde( const std::vector & center_jet, std::int64_t kappa) { const std::size_t degree = center_jet.empty() ? 0 : center_jet.size() - 1; std::vector out(degree + 1, 0); for (std::size_t k = 0; k <= degree; ++k) { uint64_t acc = 0; for (std::size_t j = 0; j <= k; ++j) acc += center_jet[j] * binom_i64(kappa, static_cast(k - j)); out[k] = acc; } return out; } } // namespace offset_jet_detail /// @brief \f$\binom{n}{k} \bmod 2^{64}\f$ for nonnegative \f$n\f$. /// \complexity The product for `k ≤ 16` runs `k` steps, widening by `v_2(k!)` (15 when `k` is 16) and multiplying by the odd inverse. `Θ(k)`, extra space `Θ(1)`. /// @warning Not division by `k!` inside `Z/2^64`. The falling factorial uses the extra `v_2(k!)` bits, then the inverse of the odd part of `k!`. /// @see grotto::offset_jet_shares /// @param n upper index /// @param k lower index, at most 16 in the keyed jet /// @return `binom(n, k)` modulo `2^64` HEDLEY_CONST HEDLEY_NO_THROW inline uint64_t offset_jet_binom(uint64_t n, unsigned k) noexcept { return offset_jet_detail::binom_u64(n, k); } /// @brief \f$\binom{n}{k} \bmod 2^{64}\f$ for a signed upper index. /// \complexity The product for `k ≤ 16` runs `k` steps, widening by `v_2(k!)` (15 when `k` is 16) and multiplying by the odd inverse. `Θ(k)`, extra space `Θ(1)`. /// @warning Not division by `k!` inside `Z/2^64`. The falling factorial uses the extra `v_2(k!)` bits, then the inverse of the odd part of `k!`. /// @see grotto::offset_jet_shares /// @param n upper index /// @param k lower index, at most 16 in the keyed jet /// @return `binom(n, k)` modulo `2^64` HEDLEY_CONST HEDLEY_NO_THROW inline uint64_t offset_jet_binom(std::int64_t n, unsigned k) noexcept { return offset_jet_detail::binom_i64(n, k); } /// \complexity One `dpf::make_dpf` of `idcf(gt)` on a `degree + 1` lane vector of binomials. `check_degree` rejects `degree > offset_jet_max_degree` (16, the offset-poly cap). /// Each binomial is one call to `binom_i64`. The seed spine is one key. Value words grow with `degree`. /// \rounds No party interaction. /// \communication None inside this function. /// \preprocessing One comparison key and `degree + 1` additive splits of those binomials. /// @warning `binom_i64` is a falling factorial modulo `2^{64+v_2(k!)}`, then a multiply by the inverse of the odd part of `k!`. Dividing by `k!` in `Z/2^64` alone is not exact. `v_2(16!)` is 15. /// @see grotto::offset_poly_eval /// @see grotto::offset_horner_eval /// @see [Binomial jet](@ref offset_jet) template offset_jet_keys make_offset_jet_keys(InputT center, std::size_t degree) { using namespace offset_horner_detail; offset_jet_detail::check_degree(degree); offset_jet_keys mat; mat.degree = degree; mat.verifiable = false; std::vector payload(degree + 1); mat.wrap_share.reserve(degree + 1); // Signed representative: lift(center) is wrong for degree >= 2 when center < 0. const std::int64_t base = math_lift(center); for (std::size_t k = 0; k <= degree; ++k) { payload[k] = offset_jet_detail::binom_i64(base, static_cast(k)); const uint64_t blind = dpf::uniform_sample(); mat.wrap_share.push_back({blind, payload[k] - blind}); } mat.keys = offset_horner_detail::make_lane_keys( center, payload.data(), payload.size()); return mat; } /// \complexity One `dpf::make_dpf` of `idcf(gt)` on a `degree + 1` lane vector of binomials. `check_degree` rejects `degree > offset_jet_max_degree` (16, the offset-poly cap). /// Each binomial is one call to `binom_i64`. The seed spine is one key. Value words grow with `degree`. /// \rounds No party interaction. /// \communication None inside this function. /// \preprocessing One comparison key and `degree + 1` additive splits of those binomials. /// @warning `binom_i64` is a falling factorial modulo `2^{64+v_2(k!)}`, then a multiply by the inverse of the odd part of `k!`. Dividing by `k!` in `Z/2^64` alone is not exact. `v_2(16!)` is 15. /// @see grotto::offset_poly_eval /// @see grotto::offset_horner_eval /// @see [Binomial jet](@ref offset_jet) template offset_jet_keys make_offset_jet_keys(InputT center, std::size_t degree, dpf::verifiable) { using namespace offset_horner_detail; offset_jet_detail::check_degree(degree); offset_jet_keys mat; mat.degree = degree; mat.verifiable = true; std::vector payload(degree + 1); mat.wrap_share.reserve(degree + 1); const std::int64_t base = math_lift(center); for (std::size_t k = 0; k <= degree; ++k) { payload[k] = offset_jet_detail::binom_i64(base, static_cast(k)); const uint64_t blind = dpf::uniform_sample(); mat.wrap_share.push_back({blind, payload[k] - blind}); } mat.keys_v = offset_horner_detail::make_lane_keys( center, payload.data(), payload.size()); return mat; } /// @brief Coefficient vector of the forward difference in the binomial basis. /// @details \f$\Delta f(x)=\sum a_k\binom{x}{k-1}\f$ for \f$k\ge 1\f$, so the /// difference coefficients are \f$(a_1,a_2,\ldots,a_d,0)\f$. /// \complexity One copy of `coeff`, shifted down by one index. `Θ(degree)`. /// @see grotto::offset_jet_dot /// @see grotto::offset_jet_prefix_coeff inline std::vector offset_jet_difference_coeff( const std::vector & coeff) { if (coeff.empty()) return {}; std::vector out(coeff.size(), 0); for (std::size_t k = 1; k < coeff.size(); ++k) out[k - 1] = coeff[k]; return out; } /// @brief Coefficient vector of the hockey-stick prefix sum. /// @details \f$\sum_{i offset_jet_prefix_coeff( const std::vector & coeff) { std::vector out(coeff.size() + 1, 0); for (std::size_t k = 0; k < coeff.size(); ++k) out[k + 1] = coeff[k]; return out; } /// @brief Public carry `kappa` of each refined piece, in knot order. /// \complexity Forwards to `offset_poly_kappas`: one piece preparation, `Θ(P log P)`. /// @see grotto::offset_poly_kappas template std::vector offset_jet_kappas( const std::vector & knots, std::size_t degree, InputT eta) { return offset_poly_kappas(knots, degree, eta); } /// @brief Dot of a public coefficient vector with a jet share. /// \complexity One multiply-add per coefficient. `Θ(degree)`, extra space `Θ(1)`. /// @see grotto::offset_jet_shares inline uint64_t offset_jet_dot( const std::vector & coeff, const std::vector & jet) { if (coeff.size() != jet.size()) throw std::invalid_argument("offset jet: coefficient and jet lengths differ"); uint64_t acc = 0; for (std::size_t k = 0; k < coeff.size(); ++k) acc += coeff[k] * jet[k]; return acc; } /// @brief Shares of the shifted binomial jet on the hot piece, for one party. /// \complexity One segment walk of the `degree + 1` lane payload, then a Chu–Vandermonde update. /// The update loops `m`, then pieces `i`, then `k ≥ m`, so the arithmetic after the walks is `Θ(P · degree²)`. /// Refined pieces: the knot vector, plus the cuts `prepare` / `prepare_pieces` inserts (domain minimum, and the carry threshold when the input width is at most 62 bits). Call that count `P`. `degree ≤ 16`. /// Extra space is the jet (`degree + 1` words) and the per-piece binomial table `Θ(P · degree)`. /// \rounds None. `eta` is an argument. /// \communication None. /// \preprocessing None created here. Uses the keys from `make_offset_jet_keys`. /// @warning The binomials in that update are `offset_jet_binom`: not division by `k!` inside `Z/2^64`. /// @see grotto::offset_poly_eval /// @see grotto::offset_horner_eval /// @see [Binomial jet](@ref offset_jet) template std::vector offset_jet_shares( const offset_jet_keys & mat, const std::vector & knots, InputT eta, dpf::proof_token * tokens = nullptr) { static_assert(Party < 2, "offset jet party is 0 or 1"); using namespace offset_horner_detail; using namespace offset_poly_detail; const std::size_t lanes = mat.verifiable ? mat.keys_v.index() : mat.keys.index(); if (lanes != mat.degree + 1) throw std::invalid_argument("offset jet: comparison payload width differs from the degree"); std::vector> dummy(knots.size(), std::vector(mat.degree + 1, 0)); const auto pieces = prepare(knots, dummy, eta); std::vector shifted; shifted.reserve(pieces.size()); for (const auto & piece : pieces) shifted.push_back(piece.knot); std::vector jet(mat.degree + 1, 0); // Public kappa table and Chu factors are independent of the power index. std::vector> kappa_binom(pieces.size()); for (std::size_t i = 0; i < pieces.size(); ++i) { kappa_binom[i].resize(mat.degree + 1); for (std::size_t t = 0; t <= mat.degree; ++t) kappa_binom[i][t] = offset_jet_detail::binom_i64( pieces[i].kappa, static_cast(t)); } dpf::proof_token * pi = (mat.verifiable && tokens != nullptr) ? &tokens[0] : nullptr; const auto table = mat.verifiable ? lane_segments(mat.keys_v, shifted, mat.wrap_share, pi) : lane_segments(mat.keys, shifted, mat.wrap_share, nullptr); if (mat.verifiable) replicate_proof(tokens, mat.degree + 1); for (std::size_t m = 0; m <= mat.degree; ++m) { for (std::size_t i = 0; i < pieces.size(); ++i) { // Chu–Vandermonde: only column m of the center jet is nonzero. for (std::size_t k = m; k <= mat.degree; ++k) jet[k] += table[i][m] * kappa_binom[i][k - m]; } } return jet; } /// @brief One party's share of \f$\sum a_k\binom{x}{k}\f$ at the wrapped point. /// \complexity One segment walk of the `degree + 1` lane payload, then a Chu–Vandermonde update. /// The update loops `m`, then pieces `i`, then `k ≥ m`, so the arithmetic after the walks is `Θ(P · degree²)`. /// Refined pieces: the knot vector, plus the cuts `prepare` / `prepare_pieces` inserts (domain minimum, and the carry threshold when the input width is at most 62 bits). Call that count `P`. `degree ≤ 16`. /// Extra space is the jet (`degree + 1` words) and the per-piece binomial table `Θ(P · degree)`. /// \rounds None. `eta` is an argument. /// \communication None. /// \preprocessing None created here. Uses the keys from `make_offset_jet_keys`. /// @warning The binomials in that update are `offset_jet_binom`: not division by `k!` inside `Z/2^64`. /// @see grotto::offset_poly_eval /// @see grotto::offset_horner_eval /// @see [Binomial jet](@ref offset_jet) template uint64_t offset_jet_eval( const offset_jet_keys & mat, const std::vector & knots, const std::vector & coeff, InputT eta, dpf::proof_token * tokens = nullptr) { if (coeff.size() != mat.degree + 1) throw std::invalid_argument("offset jet: coefficient length must be degree+1"); const auto jet = offset_jet_shares(mat, knots, eta, tokens); return offset_jet_dot(coeff, jet); } /// @brief Cleartext value of \f$\sum a_k\binom{\mathrm{center}+\kappa}{k}\f$. /// \complexity Builds the center jet with `degree + 1` binomials and one Chu–Vandermonde (`Θ(degree²)`), after the piece sort. No keys. /// @see grotto::offset_jet_eval template uint64_t offset_jet_clear( InputT center, const std::vector & knots, const std::vector & coeff, InputT eta) { using namespace offset_poly_detail; if (coeff.empty()) return 0; const std::size_t degree = coeff.size() - 1; std::vector> dummy(knots.size(), std::vector(degree + 1, 0)); const auto pieces = prepare(knots, dummy, eta); std::vector cuts; for (const auto & piece : pieces) cuts.push_back(piece.knot); std::size_t hot = pieces.size() - 1; for (std::size_t i = 0; i + 1 < cuts.size(); ++i) { if (center >= cuts[i] && center < cuts[i + 1]) { hot = i; break; } } const std::int64_t base = offset_horner_detail::math_lift(center); std::vector center_jet(degree + 1); for (std::size_t k = 0; k <= degree; ++k) center_jet[k] = offset_jet_detail::binom_i64(base, static_cast(k)); const auto jet = offset_jet_detail::chu_vandermonde(center_jet, pieces[hot].kappa); return offset_jet_dot(coeff, jet); } } // namespace grotto #endif // LIBDPF_INCLUDE_GROTTO_OFFSET_JET_HPP__