libdpf/include/grotto/offset_jet.hpp

442 lines
18 KiB
C++
Raw Normal View History

/// @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<x}f(i)\f$, which needs one extra
/// degree so \f$\binom{x}{k+1}\f$ is in the key.
///
/// Binomials modulo \f$2^{64}\f$ use a falling factorial modulo
/// \f$2^{64+v_2(k!)}\f$ (\f$v_2(16!)=15\f$), then multiply by the
/// inverse of the odd part of \f$k!\f$. Dividing by \f$k!\f$ inside
/// \f$\mathbb{Z}/2^{64}\f$ alone is not exact.
/// @copyright Copyright (c) 2019-2026 Ryan Henry and [others](@ref authors)
/// @license Released under a GNU General Public v2.0 (GPLv2) license.
#ifndef LIBDPF_INCLUDE_GROTTO_OFFSET_JET_HPP__
#define LIBDPF_INCLUDE_GROTTO_OFFSET_JET_HPP__
#include "grotto/offset_poly.hpp"
#include <cstddef>
#include <cstdint>
#include <stdexcept>
#include <utility>
#include <vector>
namespace grotto
{
inline constexpr std::size_t offset_jet_max_degree = offset_poly_max_degree;
template <typename InputT>
struct offset_jet_keys
{
static_assert(std::is_integral_v<InputT>, "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<uint64_t, degree + 1>`.
offset_horner_detail::lane_keys<InputT, false> keys{};
offset_horner_detail::lane_keys<InputT, true> keys_v{};
std::vector<std::array<uint64_t, 2>> 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<unsigned __int128>(0)
: (static_cast<unsigned __int128>(1) << bits) - 1u;
unsigned __int128 falling = 1;
for (unsigned i = 0; i < k; ++i)
{
const unsigned __int128 term = static_cast<unsigned __int128>(n - i) & mask;
falling = (falling * term) & mask;
}
falling = (falling * inv_odd_u64(odd)) & mask;
falling >>= val2;
return static_cast<uint64_t>(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<uint64_t>(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<uint64_t>(-n);
const uint64_t mag = binom_u64(m + k - 1u, k);
return (k & 1u) ? static_cast<uint64_t>(0) - mag : mag;
}
inline std::vector<uint64_t> chu_vandermonde(
const std::vector<uint64_t> & center_jet, std::int64_t kappa)
{
const std::size_t degree = center_jet.empty() ? 0 : center_jet.size() - 1;
std::vector<uint64_t> 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<unsigned>(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 <typename InputT>
offset_jet_keys<InputT> make_offset_jet_keys(InputT center, std::size_t degree)
{
using namespace offset_horner_detail;
offset_jet_detail::check_degree(degree);
offset_jet_keys<InputT> mat;
mat.degree = degree;
mat.verifiable = false;
std::vector<uint64_t> 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<unsigned>(k));
const uint64_t blind = dpf::uniform_sample<uint64_t>();
mat.wrap_share.push_back({blind, payload[k] - blind});
}
mat.keys = offset_horner_detail::make_lane_keys<InputT, false>(
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 <typename InputT>
offset_jet_keys<InputT> 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<InputT> mat;
mat.degree = degree;
mat.verifiable = true;
std::vector<uint64_t> 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<unsigned>(k));
const uint64_t blind = dpf::uniform_sample<uint64_t>();
mat.wrap_share.push_back({blind, payload[k] - blind});
}
mat.keys_v = offset_horner_detail::make_lane_keys<InputT, true>(
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<uint64_t> offset_jet_difference_coeff(
const std::vector<uint64_t> & coeff)
{
if (coeff.empty())
return {};
std::vector<uint64_t> 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<z}a_k\binom{i}{k}=a_k\binom{z}{k+1}\f$, so the prefix
/// coefficients are \f$(0,a_0,\ldots,a_{d-1})\f$. The key degree must
/// be one larger than the polynomial degree.
/// \complexity One copy of `coeff`, shifted up by one index. The result is one longer than `coeff`, so the key degree must be one larger. `Θ(degree)`.
/// @see grotto::offset_jet_dot
inline std::vector<uint64_t> offset_jet_prefix_coeff(
const std::vector<uint64_t> & coeff)
{
std::vector<uint64_t> 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 <typename InputT>
std::vector<std::int64_t> offset_jet_kappas(
const std::vector<InputT> & 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<uint64_t> & coeff,
const std::vector<uint64_t> & 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::size_t Party, typename InputT>
std::vector<uint64_t> offset_jet_shares(
const offset_jet_keys<InputT> & mat,
const std::vector<InputT> & 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<std::vector<uint64_t>> dummy(knots.size(),
std::vector<uint64_t>(mat.degree + 1, 0));
const auto pieces = prepare(knots, dummy, eta);
std::vector<InputT> shifted;
shifted.reserve(pieces.size());
for (const auto & piece : pieces)
shifted.push_back(piece.knot);
std::vector<uint64_t> jet(mat.degree + 1, 0);
// Public kappa table and Chu factors are independent of the power index.
std::vector<std::vector<uint64_t>> 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<unsigned>(t));
}
dpf::proof_token * pi = (mat.verifiable && tokens != nullptr) ? &tokens[0] : nullptr;
const auto table = mat.verifiable
? lane_segments<Party>(mat.keys_v, shifted, mat.wrap_share, pi)
: lane_segments<Party>(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 <std::size_t Party, typename InputT>
uint64_t offset_jet_eval(
const offset_jet_keys<InputT> & mat,
const std::vector<InputT> & knots,
const std::vector<uint64_t> & 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<Party>(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 <typename InputT>
uint64_t offset_jet_clear(
InputT center,
const std::vector<InputT> & knots,
const std::vector<uint64_t> & coeff,
InputT eta)
{
using namespace offset_poly_detail;
if (coeff.empty())
return 0;
const std::size_t degree = coeff.size() - 1;
std::vector<std::vector<uint64_t>> dummy(knots.size(),
std::vector<uint64_t>(degree + 1, 0));
const auto pieces = prepare(knots, dummy, eta);
std::vector<InputT> 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<uint64_t> center_jet(degree + 1);
for (std::size_t k = 0; k <= degree; ++k)
center_jet[k] = offset_jet_detail::binom_i64(base, static_cast<unsigned>(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__