libdpf/include/grotto/offset_horner.hpp
Ryan Henry 875f09fec1 Record Grotto half-ulp tables and comparison geneval, and factor shared beaver terms before the quotient.
Horner and window evaluation need those tables in the tree. Comparison geneval opens the same value words as a Doerner–Shelat key. A factor common to every polynomial term is multiplied first so that preprocessing stays smaller.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-24 15:16:21 -06:00

665 lines
23 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// @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 <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <optional>
#include <stdexcept>
#include <tuple>
#include <type_traits>
#include <utility>
#include <vector>
#include "dpf.hpp"
#include "grotto/prefix_parity.hpp"
namespace grotto
{
inline constexpr std::size_t offset_horner_max_degree = 3;
template <typename T>
T offset_horner_group_add(T a, T b) noexcept
{
using u = std::make_unsigned_t<T>;
return static_cast<T>(static_cast<u>(static_cast<u>(a) + static_cast<u>(b)));
}
template <typename T>
T offset_horner_group_sub(T a, T b) noexcept
{
using u = std::make_unsigned_t<T>;
return static_cast<T>(static_cast<u>(static_cast<u>(a) - static_cast<u>(b)));
}
/// `eta = x - r` and `center = 2r`, both in the input group.
template <typename T>
struct offset_horner_x_plus_r
{
T eta{};
T center{};
};
template <typename T>
offset_horner_x_plus_r<T> offset_horner_at_x_plus_r(T x, T r) noexcept
{
return offset_horner_x_plus_r<T>{
offset_horner_group_sub(x, r),
offset_horner_group_add(r, r)};
}
template <typename InputT, std::size_t Degree>
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 <typename T>
uint64_t lift(T v) noexcept
{
if constexpr (std::is_signed_v<T>)
return static_cast<uint64_t>(static_cast<std::int64_t>(v));
else
return static_cast<uint64_t>(v);
}
template <std::size_t Degree>
uint64_t horner_at(const std::array<uint64_t, Degree + 1> & 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 <std::size_t Degree>
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 <typename InputT, std::size_t Degree, std::size_t... M>
auto make_key_array(InputT center, const uint64_t (&payload)[Degree + 1],
std::index_sequence<M...>)
{
using pair = typename offset_horner_keys<InputT, Degree>::key_pair;
return std::array<pair, Degree + 1>{
dpf::make_dpf(center, dpf::gt(payload[M]))...};
}
template <typename InputT>
void check_knots(const std::vector<InputT> & 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 <std::size_t Degree, typename InputT>
struct shifted_piece
{
InputT knot{};
std::array<uint64_t, Degree + 1> coeff{};
};
template <std::size_t Degree, typename InputT>
std::vector<shifted_piece<Degree, InputT>> shift_and_sort(
const std::vector<InputT> & knots,
const std::vector<std::array<uint64_t, Degree + 1>> & coeff,
InputT eta)
{
std::vector<shifted_piece<Degree, InputT>> 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<Degree, InputT> & a, const shifted_piece<Degree, InputT> & b) {
return a.knot < b.knot;
});
return rows;
}
inline std::vector<uint64_t> segments_from_prefixes(
const std::vector<uint64_t> & 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<uint64_t>{wrap_share & mask};
std::vector<uint64_t> 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 <typename Key, typename InputT>
std::vector<uint64_t> segments_of(const Key & key, const std::vector<InputT> & knots,
uint64_t wrap_share)
{
const std::size_t n = knots.size();
if (n == 1)
return std::vector<uint64_t>{wrap_share & key.cmp().mask};
std::vector<uint64_t> prefix(n);
signed_prefix_parities_into(key, knots.data(), n, prefix.data());
return segments_from_prefixes(prefix, wrap_share, key.cmp().mask);
}
template <typename T>
int64_t math_lift(T value) noexcept
{
if constexpr (std::is_signed_v<T>)
return static_cast<int64_t>(value);
else
return static_cast<int64_t>(lift(value));
}
template <typename T>
T domain_min() noexcept
{
if constexpr (std::is_signed_v<T>)
return std::numeric_limits<T>::min();
else
return T{0};
}
/// Public center-space cut where `center + eta` crosses the domain end.
/// Empty when that cut is outside the domain, including `eta == 0`.
template <typename T>
std::optional<T> carry_threshold(T eta) noexcept
{
constexpr unsigned bits = dpf::utils::bitlength_of_v<T>;
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<T>)
{
if (ez > 0)
return static_cast<T>(half - ez);
if (ez < 0)
return static_cast<T>(-half - ez);
return std::nullopt;
}
else
{
if (ez == 0)
return std::nullopt;
return static_cast<T>(mod - ez);
}
}
/// `center + kappa` is the wrapped representative, as a mathematical integer.
template <typename T>
int64_t kappa_for(T left, T eta) noexcept
{
constexpr unsigned bits = dpf::utils::bitlength_of_v<T>;
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<T>)
{
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 <std::size_t Degree, typename InputT>
int piece_index(InputT point, const std::vector<InputT> & 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<int>(i);
}
return static_cast<int>(n - 1);
}
template <std::size_t Degree>
std::array<uint64_t, Degree + 1> binomial_coefficients(
const std::array<uint64_t, Degree + 1> & a, uint64_t center_limb)
{
std::array<uint64_t, Degree + 1> 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 <std::size_t Degree, typename InputT>
struct prepared_piece
{
InputT knot{};
std::array<uint64_t, Degree + 1> coeff{};
int64_t kappa = 0;
};
template <std::size_t Degree, typename InputT>
void insert_cut(std::vector<shifted_piece<Degree, InputT>> & rows, InputT point)
{
for (const auto & row : rows)
{
if (row.knot == point)
return;
}
std::vector<InputT> knots;
knots.reserve(rows.size());
for (const auto & row : rows)
knots.push_back(row.knot);
const int hot = piece_index<Degree>(point, knots);
shifted_piece<Degree, InputT> extra;
extra.knot = point;
extra.coeff = rows[static_cast<std::size_t>(hot)].coeff;
rows.push_back(std::move(extra));
std::sort(rows.begin(), rows.end(),
[](const shifted_piece<Degree, InputT> & a, const shifted_piece<Degree, InputT> & b) {
return a.knot < b.knot;
});
}
template <std::size_t Degree, typename InputT>
std::vector<prepared_piece<Degree, InputT>> prepare_pieces(
const std::vector<InputT> & knots,
const std::vector<std::array<uint64_t, Degree + 1>> & coeff,
InputT eta)
{
auto rows = shift_and_sort<Degree>(knots, coeff, eta);
constexpr unsigned bits = dpf::utils::bitlength_of_v<InputT>;
if (bits <= 62)
{
insert_cut<Degree>(rows, domain_min<InputT>());
if (const auto cut = carry_threshold(eta))
insert_cut<Degree>(rows, *cut);
}
std::vector<prepared_piece<Degree, InputT>> out;
out.reserve(rows.size());
for (const auto & row : rows)
{
prepared_piece<Degree, InputT> piece;
piece.knot = row.knot;
piece.coeff = row.coeff;
piece.kappa = kappa_for(row.knot, eta);
out.push_back(std::move(piece));
}
return out;
}
/// `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`.
template <std::size_t Degree>
std::array<uint64_t, Degree + 1> contributions(
const std::array<std::vector<uint64_t>, Degree + 1> & seg,
const std::vector<std::array<uint64_t, Degree + 1>> & coeff,
const std::vector<int64_t> & kappa)
{
std::array<uint64_t, Degree + 1> out{};
const std::size_t n = coeff.size();
for (std::size_t i = 0; i < n; ++i)
{
const auto q = binomial_coefficients<Degree>(
coeff[i], static_cast<uint64_t>(kappa[i]));
for (std::size_t k = 0; k <= Degree; ++k)
out[k] += seg[k][i] * q[k];
}
return out;
}
} // namespace offset_horner_detail
/// Both parties' comparison keys and wrap-piece shares for one center.
template <typename InputT, std::size_t Degree = offset_horner_max_degree>
struct offset_horner_keys
{
static_assert(Degree <= offset_horner_max_degree, "offset horner degree is at most 3");
static_assert(std::is_integral_v<InputT>, "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<InputT>(), dpf::gt(uint64_t{0})));
InputT center{};
/// `keys[m]` is `gt(center^m)` keyed at `center`. `.first` is party 0.
std::array<key_pair, Degree + 1> keys;
/// Random additive split of `center^m`, indexed `[power][party]`.
std::array<std::array<uint64_t, 2>, Degree + 1> wrap_share{};
};
template <typename InputT, std::size_t Degree = offset_horner_max_degree>
offset_horner_keys<InputT, Degree> make_offset_horner_keys(InputT center)
{
using namespace offset_horner_detail;
uint64_t payload[Degree + 1];
fill_payloads<Degree>(lift(center), payload);
offset_horner_keys<InputT, Degree> mat{
center,
make_key_array<InputT, Degree>(center, payload, std::make_index_sequence<Degree + 1>{}),
{}};
for (std::size_t m = 0; m <= Degree; ++m)
{
const uint64_t blind = dpf::uniform_sample<uint64_t>();
mat.wrap_share[m][0] = blind;
mat.wrap_share[m][1] = payload[m] - blind;
}
return mat;
}
/// 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::size_t Degree, typename InputT>
std::array<uint64_t, Degree + 1> offset_horner_clear_coefficients(
InputT center,
const std::vector<InputT> & knots,
const std::vector<std::array<uint64_t, Degree + 1>> & coeff,
InputT eta)
{
using namespace offset_horner_detail;
check_knots(knots, coeff.size());
const auto pieces = prepare_pieces<Degree>(knots, coeff, eta);
std::vector<InputT> cuts;
cuts.reserve(pieces.size());
for (const auto & piece : pieces)
cuts.push_back(piece.knot);
const int hot = piece_index<Degree>(center, cuts);
const auto & piece = pieces[static_cast<std::size_t>(hot)];
return binomial_coefficients<Degree>(
piece.coeff, static_cast<uint64_t>(piece.kappa));
}
/// Cleartext value of the selected piece at the wrapped `center + eta`.
template <std::size_t Degree, typename InputT>
uint64_t offset_horner_clear(
InputT center,
const std::vector<InputT> & knots,
const std::vector<std::array<uint64_t, Degree + 1>> & coeff,
InputT eta)
{
const auto c = offset_horner_clear_coefficients<Degree>(center, knots, coeff, eta);
return offset_horner_detail::horner_at<Degree>(c, offset_horner_detail::lift(center));
}
/// `Party` selects `.first` or `.second` of each key pair.
template <std::size_t Party, std::size_t Degree, typename InputT, typename KeyPair>
std::array<uint64_t, Degree + 1> offset_horner_coefficient_share(
const std::vector<KeyPair> & keys,
const std::array<std::array<uint64_t, 2>, Degree + 1> & wrap_share,
const std::vector<InputT> & knots,
const std::vector<std::array<uint64_t, Degree + 1>> & 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<Degree>(knots, coeff, eta);
std::vector<InputT> shifted(pieces.size());
std::vector<std::array<uint64_t, Degree + 1>> ordered(pieces.size());
std::vector<int64_t> 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<std::vector<uint64_t>, Degree + 1> seg;
for (std::size_t m = 0; m <= Degree; ++m)
{
seg[m] = segments_of(std::get<Party>(keys[m]), shifted, wrap_share[m][Party]);
}
return contributions<Degree>(seg, ordered, kappa);
}
/// One party's coefficient shares. `Party` is 0 or 1.
template <std::size_t Party, std::size_t Degree, typename InputT>
std::array<uint64_t, Degree + 1> offset_horner_coefficient_share(
const offset_horner_keys<InputT, Degree> & mat,
const std::vector<InputT> & knots,
const std::vector<std::array<uint64_t, Degree + 1>> & coeff,
InputT eta)
{
std::vector<typename offset_horner_keys<InputT, Degree>::key_pair> keys(
mat.keys.begin(), mat.keys.end());
return offset_horner_coefficient_share<Party, Degree>(
keys, mat.wrap_share, knots, coeff, eta);
}
/// Both parties' Horner shares from one joint Doerner–Shelat generation.
/// `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.
template <std::size_t Degree, typename InputT>
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<uint64_t, Degree + 1> coeff0{};
std::array<uint64_t, Degree + 1> coeff1{};
uint64_t value0 = 0;
uint64_t value1 = 0;
};
/// Logical comparison point for geneval's XOR shares. Matches `make_dpf(P)`
/// when `center1 = P XOR center0` or when `center0 = P` and `center1 = 0`.
template <typename InputT>
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 <std::size_t Degree, typename InputT, typename Rng>
geneval_offset_horner_result<Degree, InputT> geneval_at(
InputT center0, InputT center1, InputT center, InputT eta,
const std::vector<InputT> & knots,
const std::vector<std::array<uint64_t, Degree + 1>> & coeff,
Rng rng)
{
check_knots(knots, coeff.size());
const auto pieces = prepare_pieces<Degree>(knots, coeff, eta);
std::vector<InputT> shifted(pieces.size());
std::vector<std::array<uint64_t, Degree + 1>> ordered(pieces.size());
std::vector<int64_t> 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<Degree>(lift(center), payload);
std::array<std::array<uint64_t, 2>, Degree + 1> wrap{};
std::array<std::vector<uint64_t>, Degree + 1> seg0;
std::array<std::vector<uint64_t>, Degree + 1> seg1;
for (std::size_t m = 0; m <= Degree; ++m)
{
const auto opened = dpf::geneval_cmp(center0, center1,
shifted.begin(), shifted.end(), rng, payload[m]);
const uint64_t blind = dpf::uniform_sample<uint64_t>();
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<Degree, InputT> out;
out.center = center;
out.eta = eta;
out.coeff0 = contributions<Degree>(seg0, ordered, kappa);
out.coeff1 = contributions<Degree>(seg1, ordered, kappa);
for (uint64_t term : out.coeff0)
out.value0 += term;
for (uint64_t term : out.coeff1)
out.value1 += term;
return out;
}
} // namespace offset_horner_detail
/// Geneval-style offset Horner. The center is XOR-shared as in `geneval_point`.
/// 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.
template <std::size_t Degree = offset_horner_max_degree,
typename InputT,
typename Rng>
geneval_offset_horner_result<Degree, InputT> geneval_offset_horner(
InputT center0, InputT center1, InputT eta,
const std::vector<InputT> & knots,
const std::vector<std::array<uint64_t, Degree + 1>> & coeff,
Rng rng)
{
const InputT center = geneval_offset_horner_center(center0, center1);
return offset_horner_detail::geneval_at<Degree>(
center0, center1, center, eta, knots, coeff, std::move(rng));
}
template <std::size_t Degree = offset_horner_max_degree, typename InputT>
geneval_offset_horner_result<Degree, InputT> geneval_offset_horner(
InputT center0, InputT center1, InputT eta,
const std::vector<InputT> & knots,
const std::vector<std::array<uint64_t, Degree + 1>> & coeff)
{
using block = typename dpf::prg::aes128::block_type;
dpf::ds_randomness<block (*)(), dpf::detail::urandom_pad_rng> rng{
dpf::uniform_sample<block>, {}};
return geneval_offset_horner<Degree>(
center0, center1, eta, knots, coeff, std::move(rng));
}
/// Additive shares of the input `x` and the mask `r`. Reconstructs
/// `eta = x - r` and `center = 2r`, XOR-shares that center as `(center, 0)`,
/// and returns both parties' Horner shares of the cubic at `x + r`
/// (the group element `x + r`).
template <std::size_t Degree = offset_horner_max_degree,
typename InputT,
typename Rng>
geneval_offset_horner_result<Degree, InputT> geneval_offset_horner(
InputT x0, InputT x1, InputT r0, InputT r1,
const std::vector<InputT> & knots,
const std::vector<std::array<uint64_t, Degree + 1>> & 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 center = offset_horner_group_add(r, r);
InputT zero{};
return offset_horner_detail::geneval_at<Degree>(
center, zero, center, eta, knots, coeff, std::move(rng));
}
template <std::size_t Degree = offset_horner_max_degree, typename InputT>
geneval_offset_horner_result<Degree, InputT> geneval_offset_horner(
InputT x0, InputT x1, InputT r0, InputT r1,
const std::vector<InputT> & knots,
const std::vector<std::array<uint64_t, Degree + 1>> & coeff)
{
using block = typename dpf::prg::aes128::block_type;
dpf::ds_randomness<block (*)(), dpf::detail::urandom_pad_rng> rng{
dpf::uniform_sample<block>, {}};
return geneval_offset_horner<Degree>(
x0, x1, r0, r1, knots, coeff, std::move(rng));
}
/// One party's share of the cubic at the wrapped `center + eta`.
/// Sum the coefficient shares; they are already scaled by `center^k`.
template <std::size_t Party, std::size_t Degree, typename InputT>
uint64_t offset_horner_eval(
const offset_horner_keys<InputT, Degree> & mat,
const std::vector<InputT> & knots,
const std::vector<std::array<uint64_t, Degree + 1>> & coeff,
InputT eta)
{
const auto shares = offset_horner_coefficient_share<Party, Degree>(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__