/// @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` and sorts them. The /// sign-respecting segment walk then returns additive shares of /// `center^m` on the piece that contains the wrapped sum /// `center + eta`, and shares of 0 on the other pieces. A public /// binomial combination of those shares is a share of the coefficients /// of that piece as a polynomial in `eta`. Horner at the public `eta` /// needs no further round. /// /// The opened value is that polynomial at `lift(center) + lift(eta)` /// in Z/2^64. `lift` sign-extends a signed domain element and /// zero-extends an unsigned one. This equals the polynomial at the /// wrapped group element only when the domain addition does not /// overflow. Piece selection still follows the wrapped element. /// /// `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 #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 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 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))); } /// `eta = x - r` and `center = 2r`, both in the input group. template struct offset_horner_x_plus_r { T eta{}; T center{}; }; template 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 uint64_t lift(T v) noexcept { if constexpr (std::is_signed_v) return static_cast(static_cast(v)); else return static_cast(v); } template 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 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; } template std::vector segments_of(const Key & key, const std::vector & knots, uint64_t wrap_share) { using namespace dpf::detail::dcf_impl; const std::size_t n = knots.size(); const uint64_t mask = key.cmp().mask; if (n == 1) return std::vector{wrap_share & mask}; std::vector prefix(n); signed_prefix_parities_into(key, knots.data(), n, prefix.data()); 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 void accumulate(std::array & out, const std::array, Degree + 1> & seg, const std::vector> & coeff) { const std::size_t n = coeff.size(); for (std::size_t i = 0; i < n; ++i) { for (std::size_t m = 0; m <= Degree; ++m) { for (std::size_t k = 0; k <= m; ++k) { // seg[m-k] opens to center^{m-k} on this piece. const uint64_t weight = seg[m - k][i]; out[k] += weight * coeff[i][m] * binom[m][k]; } } } } 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; } } // namespace offset_horner_detail /// Both parties' comparison keys and wrap-piece shares for one center. 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{}; /// `keys[m]` is `gt(center^m)` keyed at `center`. `.first` is party 0. std::array keys; /// 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; } /// Cleartext coefficients of the selected piece, shifted to `center`, in Z/2^64. 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 rows = shift_and_sort(knots, coeff, eta); std::vector shifted(rows.size()); for (std::size_t i = 0; i < rows.size(); ++i) shifted[i] = rows[i].knot; const int hot = piece_index(center, shifted); return binomial_coefficients(rows[static_cast(hot)].coeff, lift(center)); } /// Cleartext value: selected piece at `lift(center) + lift(eta)` in Z/2^64. 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(eta)); } /// One party's coefficient shares. `Party` is 0 or 1. template std::array offset_horner_coefficient_share( const offset_horner_keys & mat, 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()); const auto rows = shift_and_sort(knots, coeff, eta); std::vector shifted(rows.size()); std::vector> ordered(rows.size()); for (std::size_t i = 0; i < rows.size(); ++i) { shifted[i] = rows[i].knot; ordered[i] = rows[i].coeff; } std::array, Degree + 1> seg; for (std::size_t m = 0; m <= Degree; ++m) { seg[m] = segments_of(std::get(mat.keys[m]), shifted, mat.wrap_share[m][Party]); } std::array out{}; accumulate(out, seg, ordered); return out; } /// One party's share of the cubic at `lift(center) + lift(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); return offset_horner_detail::horner_at(shares, offset_horner_detail::lift(eta)); } } // namespace grotto #endif // LIBDPF_INCLUDE_GROTTO_OFFSET_HORNER_HPP__