/// @file grotto/offset_horner.hpp /// @brief Noninteractive cubic evaluation after the public offset is opened. /// @details The dealer keys one comparison at `center` per power /// `1, center, center^2, center^3` in Z/2^64. After the parties open /// `eta`, each party shifts the knots by `eta`, inserts the domain /// minimum and the public carry threshold, and sorts. The /// sign-respecting segment walk then returns additive shares of /// `center^m` on the refined piece that contains `center`. On each /// refined piece the wrapped input is `center + kappa` for a public /// `kappa`: `eta` on the side that does not overflow, and /// `eta ∓ 2^n` on the side that does. A public binomial shift by that /// `kappa`, dotted with the segment shares, is a share of the /// polynomial at the wrapped group element. No further round. /// /// Domains of 63 bits or more are already the ring Z/2^64, so the /// carry adjustment is the identity there. `lift` sign-extends a /// signed domain element and zero-extends an unsigned one. /// /// `offset_horner_at_x_plus_r` is the wiring from the reconstruction /// the parties already do: `eta = x - r` and `center = 2r`. #ifndef LIBDPF_INCLUDE_GROTTO_OFFSET_HORNER_HPP__ #define LIBDPF_INCLUDE_GROTTO_OFFSET_HORNER_HPP__ #include "hedley/hedley.h" #include #include #include #include #include #include #include #include #include #include #include #include "dpf.hpp" #include "grotto/prefix_parity.hpp" namespace grotto { inline constexpr std::size_t offset_horner_max_degree = 3; template HEDLEY_NO_THROW T offset_horner_group_add(T a, T b) noexcept { using u = std::make_unsigned_t; return static_cast(static_cast(static_cast(a) + static_cast(b))); } template HEDLEY_NO_THROW T offset_horner_group_sub(T a, T b) noexcept { using u = std::make_unsigned_t; return static_cast(static_cast(static_cast(a) - static_cast(b))); } /// @brief `eta = x - r` and `center = 2r`, both in the input group. /// @tparam T value type template struct offset_horner_x_plus_r { T eta{}; T center{}; }; template HEDLEY_NO_THROW offset_horner_x_plus_r offset_horner_at_x_plus_r(T x, T r) noexcept { return offset_horner_x_plus_r{ offset_horner_group_sub(x, r), offset_horner_group_add(r, r)}; } template struct offset_horner_keys; namespace offset_horner_detail { inline constexpr uint64_t binom[4][4] = { {1, 0, 0, 0}, {1, 1, 0, 0}, {1, 2, 1, 0}, {1, 3, 3, 1}, }; template HEDLEY_NO_THROW uint64_t lift(T v) noexcept { if constexpr (std::is_signed_v) return static_cast(static_cast(v)); else return static_cast(v); } template HEDLEY_NO_THROW uint64_t horner_at(const std::array & coeff, uint64_t point) noexcept { uint64_t acc = coeff[Degree]; for (std::size_t k = Degree; k-- > 0; ) acc = acc * point + coeff[k]; return acc; } template HEDLEY_NO_THROW void fill_payloads(uint64_t base, uint64_t (&payload)[Degree + 1]) noexcept { uint64_t pow = 1; for (std::size_t m = 0; m <= Degree; ++m) { payload[m] = pow; pow *= base; } } template auto make_key_array(InputT center, const uint64_t (&payload)[Degree + 1], std::index_sequence) { using pair = typename offset_horner_keys::key_pair; return std::array{ dpf::make_dpf(center, dpf::gt(payload[M]))...}; } template void check_knots(const std::vector & knots, std::size_t coeff_rows) { if (knots.empty() || knots.size() != coeff_rows) throw std::invalid_argument("offset horner: knots and coefficient rows differ"); for (std::size_t i = 1; i < knots.size(); ++i) { if (!(knots[i - 1] < knots[i])) throw std::invalid_argument("offset horner: knots must be strictly increasing"); } } template struct shifted_piece { InputT knot{}; std::array coeff{}; }; template std::vector> shift_and_sort( const std::vector & knots, const std::vector> & coeff, InputT eta) { std::vector> rows(knots.size()); for (std::size_t i = 0; i < knots.size(); ++i) { rows[i].knot = offset_horner_group_sub(knots[i], eta); rows[i].coeff = coeff[i]; } std::sort(rows.begin(), rows.end(), [](const shifted_piece & a, const shifted_piece & b) { return a.knot < b.knot; }); return rows; } inline std::vector segments_from_prefixes( const std::vector & prefix, uint64_t wrap_share, uint64_t mask) { using namespace dpf::detail::dcf_impl; const std::size_t n = prefix.size(); if (n == 1) return std::vector{wrap_share & mask}; std::vector seg(n); for (std::size_t i = 0; i < n; ++i) { const uint64_t nxt = prefix[(i + 1) % n]; seg[i] = (nxt + neg_m(prefix[i], mask)) & mask; } seg[n - 1] = (seg[n - 1] + wrap_share) & mask; return seg; } template std::vector segments_of(const Key & key, const std::vector & knots, uint64_t wrap_share) { const std::size_t n = knots.size(); if (n == 1) return std::vector{wrap_share & key.cmp().mask}; std::vector prefix(n); signed_prefix_parities_into(key, knots.data(), n, prefix.data()); return segments_from_prefixes(prefix, wrap_share, key.cmp().mask); } template HEDLEY_NO_THROW int64_t math_lift(T value) noexcept { if constexpr (std::is_signed_v) return static_cast(value); else return static_cast(lift(value)); } template HEDLEY_NO_THROW T domain_min() noexcept { if constexpr (std::is_signed_v) return std::numeric_limits::min(); else return T{0}; } /// @brief Public center-space cut where `center + eta` crosses the domain end. /// @details Empty when that cut is outside the domain, including `eta == 0`. /// @tparam T value type /// @param eta the `eta` /// @return Public center-space cut where `center + eta` crosses the domain end template HEDLEY_NO_THROW std::optional carry_threshold(T eta) noexcept { constexpr unsigned bits = dpf::utils::bitlength_of_v; if (bits > 62) return std::nullopt; const int64_t mod = int64_t{1} << bits; const int64_t half = mod >> 1; const int64_t ez = math_lift(eta); if constexpr (std::is_signed_v) { if (ez > 0) return static_cast(half - ez); if (ez < 0) return static_cast(-half - ez); return std::nullopt; } else { if (ez == 0) return std::nullopt; return static_cast(mod - ez); } } /// @brief `center + kappa` is the wrapped representative, as a mathematical integer. /// @tparam T value type /// @param left the `left` /// @param eta the `eta` /// @return `center + kappa` is the wrapped representative, as a mathematical integer template HEDLEY_NO_THROW int64_t kappa_for(T left, T eta) noexcept { constexpr unsigned bits = dpf::utils::bitlength_of_v; const int64_t ez = math_lift(eta); if (bits > 62) return ez; const int64_t mod = int64_t{1} << bits; const int64_t left_i = math_lift(left); if constexpr (std::is_signed_v) { const int64_t half = mod >> 1; if (ez > 0 && left_i >= half - ez) return ez - mod; if (ez < 0 && left_i < -half - ez) return ez + mod; return ez; } else { if (ez != 0 && left_i >= mod - ez) return ez - mod; return ez; } } template int piece_index(InputT point, const std::vector & sorted_knots) { const std::size_t n = sorted_knots.size(); if (n <= 1) return 0; for (std::size_t i = 0; i + 1 < n; ++i) { if (point >= sorted_knots[i] && point < sorted_knots[i + 1]) return static_cast(i); } return static_cast(n - 1); } template std::array binomial_coefficients( const std::array & a, uint64_t center_limb) { std::array c{}; uint64_t center_pow[Degree + 1]; center_pow[0] = 1; for (std::size_t m = 1; m <= Degree; ++m) center_pow[m] = center_pow[m - 1] * center_limb; for (std::size_t m = 0; m <= Degree; ++m) { for (std::size_t k = 0; k <= m; ++k) c[k] += a[m] * binom[m][k] * center_pow[m - k]; } return c; } template struct prepared_piece { InputT knot{}; std::array coeff{}; int64_t kappa = 0; }; template void insert_cut(std::vector> & rows, InputT point) { for (const auto & row : rows) { if (row.knot == point) return; } std::vector knots; knots.reserve(rows.size()); for (const auto & row : rows) knots.push_back(row.knot); const int hot = piece_index(point, knots); shifted_piece extra; extra.knot = point; extra.coeff = rows[static_cast(hot)].coeff; rows.push_back(std::move(extra)); std::sort(rows.begin(), rows.end(), [](const shifted_piece & a, const shifted_piece & b) { return a.knot < b.knot; }); } template std::vector> prepare_pieces( const std::vector & knots, const std::vector> & coeff, InputT eta) { auto rows = shift_and_sort(knots, coeff, eta); constexpr unsigned bits = dpf::utils::bitlength_of_v; if (bits <= 62) { insert_cut(rows, domain_min()); if (const auto cut = carry_threshold(eta)) insert_cut(rows, *cut); } std::vector> out; out.reserve(rows.size()); for (const auto & row : rows) { prepared_piece piece; piece.knot = row.knot; piece.coeff = row.coeff; piece.kappa = kappa_for(row.knot, eta); out.push_back(std::move(piece)); } return out; } /// @brief `out[k]` sums to the polynomial at the wrapped input. It is /// `center^k` times the public binomial coefficient of `kappa`, not a /// coefficient you Horner-evaluate at `eta`. /// @tparam Degree degree /// @param seg the `seg` /// @param coeff the public coefficient /// @param kappa the `kappa` /// @return `out[k]` sums to the polynomial at the wrapped input template std::array contributions( const std::array, Degree + 1> & seg, const std::vector> & coeff, const std::vector & kappa) { std::array out{}; const std::size_t n = coeff.size(); for (std::size_t i = 0; i < n; ++i) { const auto q = binomial_coefficients( coeff[i], static_cast(kappa[i])); for (std::size_t k = 0; k <= Degree; ++k) out[k] += seg[k][i] * q[k]; } return out; } } // namespace offset_horner_detail /// @brief Both parties' comparison keys and wrap-piece shares for one center. /// @tparam InputT input domain type /// @tparam Degree degree template struct offset_horner_keys { static_assert(Degree <= offset_horner_max_degree, "offset horner degree is at most 3"); static_assert(std::is_integral_v, "offset horner domain must be an integer group"); static constexpr std::size_t degree = Degree; using input_type = InputT; using key_pair = decltype(dpf::make_dpf(std::declval(), dpf::gt(uint64_t{0}))); InputT center{}; /// @brief `keys[m]` is `gt(center^m)` keyed at `center`. `.first` is party 0. std::array keys; /// @brief Random additive split of `center^m`, indexed `[power][party]`. std::array, Degree + 1> wrap_share{}; }; template offset_horner_keys make_offset_horner_keys(InputT center) { using namespace offset_horner_detail; uint64_t payload[Degree + 1]; fill_payloads(lift(center), payload); offset_horner_keys mat{ center, make_key_array(center, payload, std::make_index_sequence{}), {}}; for (std::size_t m = 0; m <= Degree; ++m) { const uint64_t blind = dpf::uniform_sample(); mat.wrap_share[m][0] = blind; mat.wrap_share[m][1] = payload[m] - blind; } return mat; } /// @brief Cleartext binomial coefficients of the selected refined piece in the /// variable `center`: Horner at `lift(center)` is the polynomial at the /// wrapped input. /// @tparam Degree degree /// @tparam InputT input domain type /// @param center the `center` /// @param knots the `knots` /// @param coeff the public coefficient /// @param eta the `eta` /// @return Cleartext binomial coefficients of the selected refined piece in the variable `center`: /// Horner at `lift(center)` is the polynomial at the wrapped input template std::array offset_horner_clear_coefficients( InputT center, const std::vector & knots, const std::vector> & coeff, InputT eta) { using namespace offset_horner_detail; check_knots(knots, coeff.size()); const auto pieces = prepare_pieces(knots, coeff, eta); std::vector cuts; cuts.reserve(pieces.size()); for (const auto & piece : pieces) cuts.push_back(piece.knot); const int hot = piece_index(center, cuts); const auto & piece = pieces[static_cast(hot)]; return binomial_coefficients( piece.coeff, static_cast(piece.kappa)); } /// @brief Cleartext value of the selected piece at the wrapped `center + eta`. /// @tparam Degree degree /// @tparam InputT input domain type /// @param center the `center` /// @param knots the `knots` /// @param coeff the public coefficient /// @param eta the `eta` /// @return Cleartext value of the selected piece at the wrapped `center + eta` template uint64_t offset_horner_clear( InputT center, const std::vector & knots, const std::vector> & coeff, InputT eta) { const auto c = offset_horner_clear_coefficients(center, knots, coeff, eta); return offset_horner_detail::horner_at(c, offset_horner_detail::lift(center)); } /// @brief `Party` selects `.first` or `.second` of each key pair. /// @tparam Party party index, `0` or `1` /// @tparam Degree degree /// @tparam InputT input domain type /// @tparam KeyPair key pair /// @param keys the party keys /// @param wrap_share the `wrap_share` /// @param knots the `knots` /// @param coeff the public coefficient /// @param eta the `eta` /// @return `Party` selects `.first` or `.second` of each key pair /// @throws std::invalid_argument if `one comparison key per power` template std::array offset_horner_coefficient_share( const std::vector & keys, const std::array, Degree + 1> & wrap_share, const std::vector & knots, const std::vector> & coeff, InputT eta) { static_assert(Party < 2, "offset horner party is 0 or 1"); using namespace offset_horner_detail; check_knots(knots, coeff.size()); if (keys.size() != Degree + 1) throw std::invalid_argument("offset horner: one comparison key per power"); const auto pieces = prepare_pieces(knots, coeff, eta); std::vector shifted(pieces.size()); std::vector> ordered(pieces.size()); std::vector kappa(pieces.size()); for (std::size_t i = 0; i < pieces.size(); ++i) { shifted[i] = pieces[i].knot; ordered[i] = pieces[i].coeff; kappa[i] = pieces[i].kappa; } std::array, Degree + 1> seg; for (std::size_t m = 0; m <= Degree; ++m) { seg[m] = segments_of(std::get(keys[m]), shifted, wrap_share[m][Party]); } return contributions(seg, ordered, kappa); } /// @brief One party's coefficient shares. `Party` is 0 or 1. /// @tparam Party party index, `0` or `1` /// @tparam Degree degree /// @tparam InputT input domain type /// @param mat the `mat` /// @param knots the `knots` /// @param coeff the public coefficient /// @param eta the `eta` /// @return One party's coefficient shares template std::array offset_horner_coefficient_share( const offset_horner_keys & mat, const std::vector & knots, const std::vector> & coeff, InputT eta) { std::vector::key_pair> keys( mat.keys.begin(), mat.keys.end()); return offset_horner_coefficient_share( keys, mat.wrap_share, knots, coeff, eta); } /// @brief Both parties' Horner shares from one joint Doerner–Shelat generation. /// @details `center0 XOR center1` is the comparison point, in geneval's share /// convention (the signed MSB of `center0` is flipped before the XOR, and /// flipped back here). `eta` is already public. /// @tparam Degree degree /// @tparam InputT input domain type template struct geneval_offset_horner_result { static_assert(Degree <= offset_horner_max_degree, "offset horner degree is at most 3"); InputT center{}; InputT eta{}; std::array coeff0{}; std::array coeff1{}; uint64_t value0 = 0; uint64_t value1 = 0; }; /// @brief Logical comparison point for geneval's XOR shares. Matches `make_dpf(P)` /// when `center1 = P XOR center0` or when `center0 = P` and `center1 = 0`. /// @tparam InputT input domain type /// @param center0 the `center0` /// @param center1 the `center1` /// @return Logical comparison point for geneval's XOR shares template InputT geneval_offset_horner_center(InputT center0, InputT center1) { InputT flipped0 = center0; dpf::utils::flip_msb_if_signed_integral(flipped0); InputT mixed = dpf::utils::xor_input_shares(flipped0, center1); dpf::utils::flip_msb_if_signed_integral(mixed); return mixed; } namespace offset_horner_detail { template geneval_offset_horner_result geneval_at( bool arith, InputT center0, InputT center1, InputT center, InputT eta, const std::vector & knots, const std::vector> & coeff, Rng rng) { check_knots(knots, coeff.size()); const auto pieces = prepare_pieces(knots, coeff, eta); std::vector shifted(pieces.size()); std::vector> ordered(pieces.size()); std::vector kappa(pieces.size()); for (std::size_t i = 0; i < pieces.size(); ++i) { shifted[i] = pieces[i].knot; ordered[i] = pieces[i].coeff; kappa[i] = pieces[i].kappa; } uint64_t payload[Degree + 1]; fill_payloads(lift(center), payload); std::array, Degree + 1> wrap{}; std::array, Degree + 1> seg0; std::array, Degree + 1> seg1; for (std::size_t m = 0; m <= Degree; ++m) { const auto opened = arith ? dpf::geneval_cmp(dpf::arith_input, center0, center1, shifted.begin(), shifted.end(), rng, payload[m]) : dpf::geneval_cmp(center0, center1, shifted.begin(), shifted.end(), rng, payload[m]); const uint64_t blind = dpf::uniform_sample(); wrap[m][0] = blind; wrap[m][1] = payload[m] - blind; seg0[m] = segments_from_prefixes(opened.party0, wrap[m][0], opened.mask); seg1[m] = segments_from_prefixes(opened.party1, wrap[m][1], opened.mask); } geneval_offset_horner_result out; out.center = center; out.eta = eta; out.coeff0 = contributions(seg0, ordered, kappa); out.coeff1 = contributions(seg1, ordered, kappa); for (uint64_t term : out.coeff0) out.value0 += term; for (uint64_t term : out.coeff1) out.value1 += term; return out; } template geneval_offset_horner_result geneval_at( InputT center0, InputT center1, InputT center, InputT eta, const std::vector & knots, const std::vector> & coeff, Rng rng) { return geneval_at(false, center0, center1, center, eta, knots, coeff, std::move(rng)); } } // namespace offset_horner_detail /// @brief Geneval-style offset Horner. The center is XOR-shared as in `geneval_point`. /// @details Comparison keys are opened with the same local Doerner–Shelat protocol /// geneval uses for its correction words. The value dot uses the per-piece /// carry shift and is local. /// A value-correction word is required on every level of the secret path, so /// this does not stop early the way a leaf trie does. /// @tparam Degree degree /// @tparam InputT input domain type /// @tparam Rng rng /// @param center0 the `center0` /// @param center1 the `center1` /// @param eta the `eta` /// @param knots the `knots` /// @param coeff the public coefficient /// @param rng the Doerner–Shelat randomness tapes /// @return Geneval-style offset Horner template geneval_offset_horner_result geneval_offset_horner( InputT center0, InputT center1, InputT eta, const std::vector & knots, const std::vector> & coeff, Rng rng) { const InputT center = geneval_offset_horner_center(center0, center1); return offset_horner_detail::geneval_at( center0, center1, center, eta, knots, coeff, std::move(rng)); } template geneval_offset_horner_result geneval_offset_horner( InputT center0, InputT center1, InputT eta, const std::vector & knots, const std::vector> & coeff) { using block = typename dpf::prg::aes128::block_type; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") dpf::ds_randomness rng{ dpf::uniform_sample, {}}; HEDLEY_PRAGMA(GCC diagnostic pop) return geneval_offset_horner( center0, center1, eta, knots, coeff, std::move(rng)); } /// @brief Additive shares of the center: `center0 + center1` is the comparison point. /// @tparam Degree degree /// @tparam InputT input domain type /// @tparam Rng rng /// @param arith_input_t the `arith_input_t` /// @param center0 the `center0` /// @param center1 the `center1` /// @param eta the `eta` /// @param knots the `knots` /// @param coeff the public coefficient /// @param rng the Doerner–Shelat randomness tapes /// @return Additive shares of the center: `center0 + center1` is the comparison point template geneval_offset_horner_result geneval_offset_horner( dpf::arith_input_t, InputT center0, InputT center1, InputT eta, const std::vector & knots, const std::vector> & coeff, Rng rng) { const InputT center = offset_horner_group_add(center0, center1); return offset_horner_detail::geneval_at( true, center0, center1, center, eta, knots, coeff, std::move(rng)); } /// @brief Additive shares of the input `x` and the mask `r`. Reconstructs /// `eta = x - r` and passes additive shares of `center = 2r` (`2·r0`, `2·r1`) /// to arithmetic `geneval_cmp`. Returns both parties' Horner shares of the /// cubic at `x + r` (the group element `x + r`). /// @tparam Degree degree /// @tparam InputT input domain type /// @tparam Rng rng /// @param x0 the `x0` /// @param x1 the `x1` /// @param r0 the party 0's share of the mask /// @param r1 the party 1's share of the mask /// @param knots the `knots` /// @param coeff the public coefficient /// @param rng the Doerner–Shelat randomness tapes /// @return Additive shares of the input `x` and the mask `r` template geneval_offset_horner_result geneval_offset_horner( InputT x0, InputT x1, InputT r0, InputT r1, const std::vector & knots, const std::vector> & coeff, Rng rng) { const InputT x = offset_horner_group_add(x0, x1); const InputT r = offset_horner_group_add(r0, r1); const InputT eta = offset_horner_group_sub(x, r); const InputT center0 = offset_horner_group_add(r0, r0); const InputT center1 = offset_horner_group_add(r1, r1); const InputT center = offset_horner_group_add(center0, center1); return offset_horner_detail::geneval_at( true, center0, center1, center, eta, knots, coeff, std::move(rng)); } template geneval_offset_horner_result geneval_offset_horner( InputT x0, InputT x1, InputT r0, InputT r1, const std::vector & knots, const std::vector> & coeff) { using block = typename dpf::prg::aes128::block_type; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") dpf::ds_randomness rng{ dpf::uniform_sample, {}}; HEDLEY_PRAGMA(GCC diagnostic pop) return geneval_offset_horner( x0, x1, r0, r1, knots, coeff, std::move(rng)); } /// @brief One party's share of the cubic at the wrapped `center + eta`. /// @details Sum the coefficient shares; they are already scaled by `center^k`. /// @tparam Party party index, `0` or `1` /// @tparam Degree degree /// @tparam InputT input domain type /// @param mat the `mat` /// @param knots the `knots` /// @param coeff the public coefficient /// @param eta the `eta` /// @return One party's share of the cubic at the wrapped `center + eta` template uint64_t offset_horner_eval( const offset_horner_keys & mat, const std::vector & knots, const std::vector> & coeff, InputT eta) { const auto shares = offset_horner_coefficient_share(mat, knots, coeff, eta); uint64_t value = 0; for (uint64_t term : shares) value += term; return value; } } // namespace grotto #endif // LIBDPF_INCLUDE_GROTTO_OFFSET_HORNER_HPP__