Checkpoint the party/runtime stack before share-program and malicious-mode work.

Ship the TLS mesh, composer, Beaver/Yao/leaf MPC, prep/online paths, apps, and docs so the tree is pushable before elevating share_expr, security_mode, and prep resume.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ryan Henry 2026-09-28 05:59:19 -06:00
parent 695f8e84f7
commit 0d22946a0e
1835 changed files with 170291 additions and 2849 deletions

View file

@ -1,7 +1,8 @@
/// @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
/// @details The dealer keys one comparison at `center` whose payload is the
/// vector `1, center, center^2, center^3` in Z/2^64. The seed spine
/// is stored once; the value words grow with the degree. 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
@ -18,6 +19,7 @@
///
/// `offset_horner_at_x_plus_r` is the wiring from the reconstruction
/// the parties already do: `eta = x - r` and `center = 2r`.
/// @note Storrier, Vadapalli, Lyons, and Henry (ePrint 2023/108) evaluate a public piecewise polynomial from one point key by prefix parity. This dealer keys one comparison of a secret center, with a `dpf::vec` of the powers as the payload.
#ifndef LIBDPF_INCLUDE_GROTTO_OFFSET_HORNER_HPP__
#define LIBDPF_INCLUDE_GROTTO_OFFSET_HORNER_HPP__
@ -34,6 +36,7 @@
#include <tuple>
#include <type_traits>
#include <utility>
#include <variant>
#include <vector>
#include "dpf.hpp"
@ -41,20 +44,26 @@
namespace grotto
{
/// \complexity One modular add in the input group (cast to the unsigned width). `Θ(1)`.
/// @see grotto::offset_horner_at_x_plus_r
inline constexpr std::size_t offset_horner_max_degree = 3;
template <typename T>
HEDLEY_CONST
HEDLEY_NO_THROW
T offset_horner_group_add(T a, T b) noexcept
constexpr 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)));
}
/// \complexity One modular subtract in the input group. `Θ(1)`.
/// @see grotto::offset_horner_group_add
template <typename T>
HEDLEY_CONST
HEDLEY_NO_THROW
T offset_horner_group_sub(T a, T b) noexcept
constexpr 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)));
@ -68,17 +77,24 @@ struct offset_horner_x_plus_r
T eta{};
T center{};
};
/// \complexity Two group operations: `eta = x - r`, `center = 2r`. `Θ(1)`.
/// @param x secret input share or value, in the input group
/// @param r mask, in the input group
/// @return `eta` and `center`
/// @see grotto::make_offset_horner_keys
template <typename T>
HEDLEY_CONST
HEDLEY_NO_THROW
offset_horner_x_plus_r<T> offset_horner_at_x_plus_r(T x, T r) noexcept
constexpr 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>
template <typename InputT, std::size_t Degree = offset_horner_max_degree,
bool Verifiable = false>
struct offset_horner_keys;
namespace offset_horner_detail
@ -92,8 +108,9 @@ inline constexpr uint64_t binom[4][4] = {
};
template <typename T>
HEDLEY_CONST
HEDLEY_NO_THROW
uint64_t lift(T v) noexcept
constexpr uint64_t lift(T v) noexcept
{
if constexpr (std::is_signed_v<T>)
return static_cast<uint64_t>(static_cast<std::int64_t>(v));
@ -102,8 +119,9 @@ uint64_t lift(T v) noexcept
}
template <std::size_t Degree>
HEDLEY_PURE
HEDLEY_NO_THROW
uint64_t horner_at(const std::array<uint64_t, Degree + 1> & coeff, uint64_t point) noexcept
constexpr 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; )
@ -123,13 +141,26 @@ void fill_payloads(uint64_t base, uint64_t (&payload)[Degree + 1]) noexcept
}
}
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...>)
template <typename InputT, std::size_t Degree>
dpf::vec<uint64_t, Degree + 1> payload_vec(const uint64_t (&payload)[Degree + 1])
{
using pair = typename offset_horner_keys<InputT, Degree>::key_pair;
return std::array<pair, Degree + 1>{
dpf::make_dpf(center, dpf::gt(payload[M]))...};
dpf::vec<uint64_t, Degree + 1> v;
for (std::size_t m = 0; m <= Degree; ++m)
v[m] = payload[m];
return v;
}
template <typename InputT, std::size_t Degree>
auto make_power_key(InputT center, const uint64_t (&payload)[Degree + 1], std::false_type)
{
return dpf::make_dpf(center, dpf::gt(payload_vec<InputT, Degree>(payload)));
}
template <typename InputT, std::size_t Degree>
auto make_power_key(InputT center, const uint64_t (&payload)[Degree + 1], std::true_type)
{
return dpf::make_dpf(center, dpf::gt(payload_vec<InputT, Degree>(payload)),
dpf::verifiable{});
}
template <typename InputT>
@ -187,15 +218,190 @@ inline std::vector<uint64_t> segments_from_prefixes(
return seg;
}
/// @brief One segment walk whose comparison payload is `N` lanes.
/// Lane `m` matches a per-power `segments_of` on `gt(payload[m])`.
template <std::size_t N, typename Key, typename InputT>
std::array<std::vector<uint64_t>, N> segments_lanes(
const Key & key, const std::vector<InputT> & knots,
const std::array<uint64_t, N> & wrap_party, dpf::proof_token * pi = nullptr)
{
using Vec = dpf::vec<std::uint64_t, N>;
const std::size_t n = knots.size();
std::array<std::vector<uint64_t>, N> out;
for (auto & row : out)
row.assign(n, 0);
if (n == 1)
{
if (pi != nullptr)
{
dpf::detail::vdpf::init_proof(*pi, key);
dpf::detail::vdpf::fold_output_binding(*pi, key);
}
const uint64_t mask = key.cmp().mask;
for (std::size_t m = 0; m < N; ++m)
out[m][0] = wrap_party[m] & mask;
return out;
}
if (pi != nullptr)
{
if constexpr (!Key::is_verifiable)
throw std::invalid_argument(
"offset horner: proof token requires a verifiable key");
dpf::detail::vdpf::init_proof(*pi, key);
}
const uint64_t mask = key.cmp().mask;
auto path = dpf::make_basic_path_memoizer(key);
std::vector<Vec> prefixes(n);
for (std::size_t which = 0; which < n; ++which)
{
auto tx = key.offset_x(knots[which]);
dpf::utils::flip_msb_if_signed_integral(tx);
prefixes[which] = dpf::detail::incr::eval_payload_path_sum<Vec>(
key, tx, path, false, 0, pi);
}
if (pi != nullptr)
dpf::detail::vdpf::fold_output_binding(*pi, key);
for (std::size_t m = 0; m < N; ++m)
{
std::vector<uint64_t> prefix(n);
for (std::size_t i = 0; i < n; ++i)
prefix[i] = prefixes[i][m];
out[m] = segments_from_prefixes(prefix, wrap_party[m], mask);
}
return out;
}
/// @brief Widest lane count stored in one offset comparison (degree 16).
inline constexpr std::size_t lane_key_max = 17;
template <typename InputT, std::size_t N, bool Verifiable>
struct lane_key_slot
{
static constexpr std::size_t lanes = N;
using key_pair = std::conditional_t<Verifiable,
decltype(dpf::make_dpf(std::declval<InputT>(),
dpf::idcf(dpf::gt(dpf::vec<uint64_t, N>{})), dpf::verifiable{})),
decltype(dpf::make_dpf(std::declval<InputT>(),
dpf::idcf(dpf::gt(dpf::vec<uint64_t, N>{}))))>;
key_pair keys;
explicit lane_key_slot(key_pair k)
: keys(std::move(k)) { }
};
template <typename InputT, bool Verifiable, typename Seq>
struct lane_key_variant;
template <typename InputT, bool Verifiable, std::size_t... I>
struct lane_key_variant<InputT, Verifiable, std::index_sequence<I...>>
{
using type = std::variant<std::monostate,
lane_key_slot<InputT, I + 1, Verifiable>...>;
};
template <typename InputT, bool Verifiable>
using lane_keys = typename lane_key_variant<InputT, Verifiable,
std::make_index_sequence<lane_key_max>>::type;
template <typename InputT, std::size_t N, bool Verifiable>
lane_key_slot<InputT, N, Verifiable> make_lane_slot(
InputT center, const uint64_t * payload)
{
dpf::vec<uint64_t, N> v;
for (std::size_t i = 0; i < N; ++i)
v[i] = payload[i];
if constexpr (Verifiable)
return lane_key_slot<InputT, N, Verifiable>{
dpf::make_dpf(center, dpf::idcf(dpf::gt(v)), dpf::verifiable{})};
else
return lane_key_slot<InputT, N, Verifiable>{
dpf::make_dpf(center, dpf::idcf(dpf::gt(v)))};
}
template <typename InputT, bool Verifiable, std::size_t N>
void emplace_lane_keys(lane_keys<InputT, Verifiable> & out,
InputT center, const uint64_t * payload, std::size_t n)
{
if (n == N)
out.template emplace<lane_key_slot<InputT, N, Verifiable>>(
make_lane_slot<InputT, N, Verifiable>(center, payload));
else if constexpr (N > 1)
emplace_lane_keys<InputT, Verifiable, N - 1>(out, center, payload, n);
}
template <typename InputT, bool Verifiable>
lane_keys<InputT, Verifiable> make_lane_keys(
InputT center, const uint64_t * payload, std::size_t n)
{
if (n == 0 || n > lane_key_max)
throw std::invalid_argument("offset comparison: lane count out of range");
lane_keys<InputT, Verifiable> out;
emplace_lane_keys<InputT, Verifiable, lane_key_max>(out, center, payload, n);
return out;
}
template <std::size_t Party, typename Slot, typename InputT>
std::vector<std::vector<uint64_t>> segments_of_slot(
const Slot & slot, const std::vector<InputT> & knots,
const std::vector<std::array<uint64_t, 2>> & wrap, dpf::proof_token * pi)
{
constexpr std::size_t N = Slot::lanes;
if (wrap.size() < N)
throw std::invalid_argument("offset comparison: wrap shares shorter than the payload");
std::array<uint64_t, N> wrap_party{};
for (std::size_t m = 0; m < N; ++m)
wrap_party[m] = wrap[m][Party];
const auto seg = segments_lanes<N>(std::get<Party>(slot.keys), knots, wrap_party, pi);
std::vector<std::vector<uint64_t>> out(knots.size(), std::vector<uint64_t>(N, 0));
for (std::size_t m = 0; m < N; ++m)
for (std::size_t i = 0; i < knots.size(); ++i)
out[i][m] = seg[m][i];
return out;
}
template <std::size_t Party, typename Keys, typename InputT>
std::vector<std::vector<uint64_t>> lane_segments(
const Keys & keys,
const std::vector<InputT> & knots,
const std::vector<std::array<uint64_t, 2>> & wrap, dpf::proof_token * pi)
{
return std::visit([&](const auto & slot) -> std::vector<std::vector<uint64_t>> {
using Slot = std::decay_t<decltype(slot)>;
if constexpr (std::is_same_v<Slot, std::monostate>)
throw std::invalid_argument("offset comparison: missing key");
else
return segments_of_slot<Party>(slot, knots, wrap, pi);
}, keys);
}
inline void replicate_proof(dpf::proof_token * tokens, std::size_t n)
{
if (tokens == nullptr || n == 0)
return;
for (std::size_t m = 1; m < n; ++m)
tokens[m] = tokens[0];
}
template <typename Key, typename InputT>
std::vector<uint64_t> segments_of(const Key & key, const std::vector<InputT> & knots,
uint64_t wrap_share)
uint64_t wrap_share, dpf::proof_token * pi = nullptr)
{
const std::size_t n = knots.size();
if (n == 1)
{
// One-segment short circuit (domains ≥ 63 bits): no prefix walk.
// Bind leaf / value words so tokens are non-zero and verify.
if (pi != nullptr)
{
dpf::detail::vdpf::init_proof(*pi, key);
dpf::detail::vdpf::fold_output_binding(*pi, key);
}
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());
signed_prefix_parities_into(key, knots.data(), n, prefix.data(), pi);
return segments_from_prefixes(prefix, wrap_share, key.cmp().mask);
}
@ -222,38 +428,41 @@ T domain_min() noexcept
/// @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`
/// @param eta public offset `eta = x - r`
/// @return Public center-space cut where `center + eta` crosses the domain end
template <typename T>
HEDLEY_NO_THROW
std::optional<T> carry_threshold(T eta) noexcept
{
constexpr unsigned bits = dpf::utils::bitlength_of_v<T>;
if (bits > 62)
if constexpr (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)
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;
return static_cast<T>(mod - ez);
}
else
{
if (ez == 0)
return std::nullopt;
return static_cast<T>(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`
/// @param left left endpoint of the piece, in the input group
/// @param eta public offset `eta = x - r`
/// @return `center + kappa` is the wrapped representative, as a mathematical integer
template <typename T>
HEDLEY_NO_THROW
@ -261,24 +470,27 @@ 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)
if constexpr (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;
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;
}
}
}
@ -375,9 +587,9 @@ std::vector<prepared_piece<Degree, InputT>> prepare_pieces(
/// `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 seg per-power segment shares on the refined pieces
/// @param coeff the public coefficient
/// @param kappa the `kappa`
/// @param kappa public carry of each refined piece
/// @return `out[k]` sums to the polynomial at the wrapped input
template <std::size_t Degree>
std::array<uint64_t, Degree + 1> contributions(
@ -402,33 +614,70 @@ std::array<uint64_t, Degree + 1> contributions(
/// @brief Both parties' comparison keys and wrap-piece shares for one center.
/// @tparam InputT input domain type
/// @tparam Degree degree
template <typename InputT, std::size_t Degree = offset_horner_max_degree>
/// @tparam Verifiable when true, keys carry `dpf::verifiable`
template <typename InputT, std::size_t Degree, bool Verifiable>
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;
static constexpr bool is_verifiable = Verifiable;
using input_type = InputT;
using key_pair = decltype(dpf::make_dpf(std::declval<InputT>(), dpf::gt(uint64_t{0})));
using key_pair = std::conditional_t<Verifiable,
decltype(dpf::make_dpf(std::declval<InputT>(),
dpf::gt(dpf::vec<uint64_t, Degree + 1>{}), dpf::verifiable{})),
decltype(dpf::make_dpf(std::declval<InputT>(),
dpf::gt(dpf::vec<uint64_t, Degree + 1>{})))>;
InputT center{};
/// @brief `keys[m]` is `gt(center^m)` keyed at `center`. `.first` is party 0.
std::array<key_pair, Degree + 1> keys;
/// @brief One `gt` at the hidden center. Lane `m` of the payload is `center^m`.
/// `.first` is party 0. The seed spine is stored once.
key_pair keys;
/// @brief Random additive split of `center^m`, indexed `[power][party]`.
std::array<std::array<uint64_t, 2>, Degree + 1> wrap_share{};
};
/// \complexity One `dpf::make_dpf` of a `Degree + 1` lane comparison. `Degree` is at most 3.
/// The seed spine is one key. Value words are `Degree + 1` lanes.
/// \rounds No party interaction.
/// \communication None inside this function. Shipping the returned keys is outside it.
/// \preprocessing One `gt` key and `wrap_share[Degree + 1][2]` words of `uint64_t`.
/// @see grotto::offset_horner_eval
/// @see grotto::offset_poly_eval
template <typename InputT, std::size_t Degree = offset_horner_max_degree>
offset_horner_keys<InputT, Degree> make_offset_horner_keys(InputT center)
offset_horner_keys<InputT, Degree, false> 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>{}),
offset_horner_keys<InputT, Degree, false> mat{
make_power_key<InputT, Degree>(center, payload, std::false_type{}),
{}};
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;
}
/// \complexity One `dpf::make_dpf` of a `Degree + 1` lane comparison, with proof tokens. `Degree` is at most 3.
/// \rounds No party interaction.
/// \communication None inside this function.
/// \preprocessing One verifiable `gt` key and `wrap_share[Degree + 1][2]` words of `uint64_t`.
/// @see grotto::offset_horner_eval
template <typename InputT, std::size_t Degree = offset_horner_max_degree>
offset_horner_keys<InputT, Degree, true> make_offset_horner_keys(InputT center,
dpf::verifiable)
{
using namespace offset_horner_detail;
uint64_t payload[Degree + 1];
fill_payloads<Degree>(lift(center), payload);
offset_horner_keys<InputT, Degree, true> mat{
make_power_key<InputT, Degree>(center, payload, std::true_type{}),
{}};
for (std::size_t m = 0; m <= Degree; ++m)
{
@ -444,12 +693,15 @@ offset_horner_keys<InputT, Degree> make_offset_horner_keys(InputT center)
/// wrapped input.
/// @tparam Degree degree
/// @tparam InputT input domain type
/// @param center the `center`
/// @param knots the `knots`
/// @param center hidden comparison point in the input group
/// @param knots public breakpoints, strictly increasing
/// @param coeff the public coefficient
/// @param eta the `eta`
/// @param eta public offset `eta = x - r`
/// @return Cleartext binomial coefficients of the selected refined piece in the variable `center`:
/// Horner at `lift(center)` is the polynomial at the wrapped input
/// \complexity Same piece preparation as the online eval (`Θ(P log P)` sort plus the two optional cuts), then `Θ(Degree)` binomial coefficients on the hot piece. No keys.
/// @see grotto::offset_poly_eval
/// @see grotto::offset_jet_eval
template <std::size_t Degree, typename InputT>
std::array<uint64_t, Degree + 1> offset_horner_clear_coefficients(
InputT center,
@ -473,11 +725,13 @@ std::array<uint64_t, Degree + 1> offset_horner_clear_coefficients(
/// @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 center hidden comparison point in the input group
/// @param knots public breakpoints, strictly increasing
/// @param coeff the public coefficient
/// @param eta the `eta`
/// @param eta public offset `eta = x - r`
/// @return Cleartext value of the selected piece at the wrapped `center + eta`
/// \complexity One `offset_horner_clear_coefficients` plus a Horner loop of `Degree + 1` terms (`Degree ≤ 3`).
/// @see grotto::offset_horner_eval
template <std::size_t Degree, typename InputT>
uint64_t offset_horner_clear(
InputT center,
@ -495,25 +749,35 @@ uint64_t offset_horner_clear(
/// @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 wrap_share additive split of the keyed payload, indexed by party
/// @param knots public breakpoints, strictly increasing
/// @param coeff the public coefficient
/// @param eta the `eta`
/// @param eta public offset `eta = x - r`
/// @param tokens proof token folded by the segment walk, or null
/// @return `Party` selects `.first` or `.second` of each key pair
/// @throws std::invalid_argument if `one comparison key per power`
/// @throws std::invalid_argument if the key vector is not the single shared comparison
/// \complexity `prepare_pieces` sorts the knots and may insert two cuts. Then one segment walk of the `Degree + 1` lane payload.
/// Each walk is `signed_prefix_parities` over those `P` endpoints (a DPF path of `bitlength(InputT)` levels, reusing the common prefix).
/// 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`.
/// Time is one segment walk. Extra space is the piece vectors, `Θ(P · Degree)` words, with `Degree ≤ 3`.
/// \rounds None. `eta` is an argument; this function does not open it.
/// \communication None.
/// \preprocessing None created here. It reads the one comparison from `make_offset_horner_keys`.
/// @see grotto::offset_poly_eval
/// @see grotto::offset_jet_eval
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)
InputT eta, dpf::proof_token * tokens = nullptr)
{
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");
if (keys.size() != 1)
throw std::invalid_argument("offset horner: one comparison key");
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());
@ -525,34 +789,42 @@ std::array<uint64_t, Degree + 1> offset_horner_coefficient_share(
kappa[i] = pieces[i].kappa;
}
std::array<std::vector<uint64_t>, Degree + 1> seg;
std::array<uint64_t, Degree + 1> wrap_party{};
for (std::size_t m = 0; m <= Degree; ++m)
wrap_party[m] = wrap_share[m][Party];
dpf::proof_token * pi = (tokens != nullptr) ? &tokens[0] : nullptr;
const auto & key = std::get<Party>(keys.front());
auto seg = segments_lanes<Degree + 1>(key, shifted, wrap_party, pi);
if (tokens != nullptr)
{
seg[m] = segments_of(std::get<Party>(keys[m]), shifted, wrap_share[m][Party]);
for (std::size_t m = 1; m <= Degree; ++m)
tokens[m] = tokens[0];
}
return contributions<Degree>(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::size_t Party, std::size_t Degree, typename InputT>
/// \complexity `prepare_pieces` sorts the knots and may insert two cuts. Then one segment walk of the `Degree + 1` lane payload.
/// Each walk is `signed_prefix_parities` over those `P` endpoints (a DPF path of `bitlength(InputT)` levels, reusing the common prefix).
/// 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`.
/// Time is one segment walk. Extra space is the piece vectors, `Θ(P · Degree)` words, with `Degree ≤ 3`.
/// \rounds None. `eta` is an argument; this function does not open it.
/// \communication None.
/// \preprocessing None created here. It reads the one comparison from `make_offset_horner_keys`.
/// @see grotto::offset_poly_eval
/// @see grotto::offset_jet_eval
template <std::size_t Party, std::size_t Degree, typename InputT,
bool Verifiable = false>
std::array<uint64_t, Degree + 1> offset_horner_coefficient_share(
const offset_horner_keys<InputT, Degree> & mat,
const offset_horner_keys<InputT, Degree, Verifiable> & mat,
const std::vector<InputT> & knots,
const std::vector<std::array<uint64_t, Degree + 1>> & coeff,
InputT eta)
InputT eta, dpf::proof_token * tokens = nullptr)
{
std::vector<typename offset_horner_keys<InputT, Degree>::key_pair> keys(
mat.keys.begin(), mat.keys.end());
std::vector<typename offset_horner_keys<InputT, Degree, Verifiable>::key_pair> keys{
mat.keys};
return offset_horner_coefficient_share<Party, Degree>(
keys, mat.wrap_share, knots, coeff, eta);
keys, mat.wrap_share, knots, coeff, eta, tokens);
}
/// @brief Both parties' Horner shares from one joint Doerner–Shelat generation.
@ -566,20 +838,27 @@ struct geneval_offset_horner_result
{
static_assert(Degree <= offset_horner_max_degree, "offset horner degree is at most 3");
InputT center{};
/// @brief Public offset `eta = x - r`. F_Horner leaks only this.
InputT eta{};
std::array<uint64_t, Degree + 1> coeff0{};
std::array<uint64_t, Degree + 1> coeff1{};
uint64_t value0 = 0;
uint64_t value1 = 0;
std::array<dpf::proof_token, Degree + 1> proof0{};
std::array<dpf::proof_token, Degree + 1> proof1{};
};
/// @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`.
/// @details Dealer / joint-simulator helper only. F_Horner does not return the
/// center to the parties; `center^m` stays a shared payload.
/// @tparam InputT input domain type
/// @param center0 the `center0`
/// @param center1 the `center1`
/// @param center0 party 0 share of the center
/// @param center1 party 1 share of the center
/// @return Logical comparison point for geneval's XOR shares
/// \complexity Two `flip_msb_if_signed_integral` calls and one XOR of the input shares. `Θ(1)`.
/// @note Dealer-side helper. The signed MSB of `center0` is flipped before the XOR and flipped back, matching `make_dpf`.
/// @see grotto::geneval_offset_horner
template <typename InputT>
InputT geneval_offset_horner_center(InputT center0, InputT center1)
{
@ -618,25 +897,67 @@ geneval_offset_horner_result<Degree, InputT> geneval_at(
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 = 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<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);
}
std::array<dpf::proof_token, Degree + 1> out_proofs0{};
std::array<dpf::proof_token, Degree + 1> out_proofs1{};
using Vec = dpf::vec<uint64_t, Degree + 1>;
const auto beta = payload_vec<InputT, Degree>(payload);
auto open_lanes = [&](auto && keys) {
const auto & k0 = keys.first;
const auto & k1 = keys.second;
const uint64_t mask = k0.cmp().mask;
dpf::detail::vdpf::init_proof(out_proofs0[0], k0);
dpf::detail::vdpf::init_proof(out_proofs1[0], k1);
auto path0 = dpf::make_basic_path_memoizer(k0);
auto path1 = dpf::make_basic_path_memoizer(k1);
std::vector<Vec> pref0(shifted.size());
std::vector<Vec> pref1(shifted.size());
for (std::size_t i = 0; i < shifted.size(); ++i)
{
auto tx0 = k0.offset_x(shifted[i]);
auto tx1 = k1.offset_x(shifted[i]);
dpf::utils::flip_msb_if_signed_integral(tx0);
dpf::utils::flip_msb_if_signed_integral(tx1);
pref0[i] = dpf::detail::incr::eval_payload_path_sum<Vec>(
k0, tx0, path0, false, 0, &out_proofs0[0]);
pref1[i] = dpf::detail::incr::eval_payload_path_sum<Vec>(
k1, tx1, path1, false, 0, &out_proofs1[0]);
}
dpf::detail::vdpf::fold_output_binding(out_proofs0[0], k0);
dpf::detail::vdpf::fold_output_binding(out_proofs1[0], k1);
for (std::size_t m = 1; m <= Degree; ++m)
{
out_proofs0[m] = out_proofs0[0];
out_proofs1[m] = out_proofs1[0];
}
for (std::size_t m = 0; m <= Degree; ++m)
{
const uint64_t blind = dpf::uniform_sample<uint64_t>();
wrap[m][0] = blind;
wrap[m][1] = payload[m] - blind;
std::vector<uint64_t> lane0(shifted.size());
std::vector<uint64_t> lane1(shifted.size());
for (std::size_t i = 0; i < shifted.size(); ++i)
{
lane0[i] = pref0[i][m];
lane1[i] = pref1[i][m];
}
seg0[m] = segments_from_prefixes(lane0, wrap[m][0], mask);
seg1[m] = segments_from_prefixes(lane1, wrap[m][1], mask);
}
};
if (arith)
open_lanes(dpf::make_dpf_doerner_shelat(dpf::arith_input, center0, center1,
rng, dpf::gt(beta), dpf::verifiable{}));
else
open_lanes(dpf::make_dpf_doerner_shelat(center0, center1,
rng, dpf::gt(beta), dpf::verifiable{}));
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);
out.proof0 = out_proofs0;
out.proof1 = out_proofs1;
for (uint64_t term : out.coeff0)
out.value0 += term;
for (uint64_t term : out.coeff1)
@ -661,18 +982,24 @@ geneval_offset_horner_result<Degree, InputT> geneval_at(
/// @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.
/// A comparison value word is written on every level of the secret path.
/// The seed spine is generated once for the whole power vector.
/// @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 center0 party 0 share of the center
/// @param center1 party 1 share of the center
/// @param eta public offset `eta = x - r`
/// @param knots public breakpoints, strictly increasing
/// @param coeff the public coefficient
/// @param rng the Doerner–Shelat randomness tapes
/// @return Geneval-style offset Horner
/// \complexity One Doerner–Shelat generation of a `Degree + 1` lane comparison (`Degree ≤ 3`), then the same local piece dot as `offset_horner_eval`.
/// The dot is `Θ(P · Degree)` after those calls. `P` is the refined piece count.
/// @note Rounds and bandwidth of each `geneval_cmp` live in the DPF headers, not in this function, so they are not stated here.
/// \preprocessing The randomness object the caller passes (`Rng`). This function also samples one `uint64_t` blind per power.
/// @see grotto::offset_poly_eval
/// @see grotto::offset_jet_eval
template <std::size_t Degree = offset_horner_max_degree,
typename InputT,
typename Rng>
@ -686,6 +1013,12 @@ geneval_offset_horner_result<Degree, InputT> geneval_offset_horner(
return offset_horner_detail::geneval_at<Degree>(
center0, center1, center, eta, knots, coeff, std::move(rng));
}
/// \complexity One Doerner–Shelat generation of a `Degree + 1` lane comparison (`Degree ≤ 3`), then the same local piece dot as `offset_horner_eval`.
/// The dot is `Θ(P · Degree)` after those calls. `P` is the refined piece count.
/// @note Rounds and bandwidth of each `geneval_cmp` live in the DPF headers, not in this function, so they are not stated here.
/// \preprocessing The randomness object the caller passes (`Rng`). This function also samples one `uint64_t` blind per power.
/// @see grotto::offset_poly_eval
/// @see grotto::offset_jet_eval
template <std::size_t Degree = offset_horner_max_degree, typename InputT>
geneval_offset_horner_result<Degree, InputT> geneval_offset_horner(
@ -707,13 +1040,19 @@ geneval_offset_horner_result<Degree, InputT> geneval_offset_horner(
/// @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 center0 party 0 share of the center
/// @param center1 party 1 share of the center
/// @param eta public offset `eta = x - r`
/// @param knots public breakpoints, strictly increasing
/// @param coeff the public coefficient
/// @param rng the Doerner–Shelat randomness tapes
/// @return Additive shares of the center: `center0 + center1` is the comparison point
/// \complexity One Doerner–Shelat generation of a `Degree + 1` lane comparison (`Degree ≤ 3`), then the same local piece dot as `offset_horner_eval`.
/// The dot is `Θ(P · Degree)` after those calls. `P` is the refined piece count.
/// @note Rounds and bandwidth of each `geneval_cmp` live in the DPF headers, not in this function, so they are not stated here.
/// \preprocessing The randomness object the caller passes (`Rng`). This function also samples one `uint64_t` blind per power.
/// @see grotto::offset_poly_eval
/// @see grotto::offset_jet_eval
template <std::size_t Degree = offset_horner_max_degree,
typename InputT,
typename Rng>
@ -728,21 +1067,28 @@ geneval_offset_horner_result<Degree, InputT> geneval_offset_horner(
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`).
/// @brief Additive shares of the input `x` and the mask `r`.
/// @details Opens only `eta = (x0 - r0) + (x1 - r1)`. Passes additive shares
/// of `center = 2r` (`2·r0`, `2·r1`) to arithmetic `geneval_cmp`.
/// The clear center is used only to plant shared `center^m` payloads;
/// it is not returned. F_Horner leaks only `eta`.
/// @tparam Degree degree
/// @tparam InputT input domain type
/// @tparam Rng rng
/// @param x0 the `x0`
/// @param x1 the `x1`
/// @param x0 party 0 share of the input
/// @param x1 party 1 share of the input
/// @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 knots public breakpoints, strictly increasing
/// @param coeff the public coefficient
/// @param rng the Doerner–Shelat randomness tapes
/// @return Additive shares of the input `x` and the mask `r`
/// @return Both parties' Horner shares and the public `eta`
/// \complexity One Doerner–Shelat generation of a `Degree + 1` lane comparison (`Degree ≤ 3`), then the same local piece dot as `offset_horner_eval`.
/// The dot is `Θ(P · Degree)` after those calls. `P` is the refined piece count.
/// @note Rounds and bandwidth of each `geneval_cmp` live in the DPF headers, not in this function, so they are not stated here.
/// \preprocessing The randomness object the caller passes (`Rng`). This function also samples one `uint64_t` blind per power.
/// @see grotto::offset_poly_eval
/// @see grotto::offset_jet_eval
template <std::size_t Degree = offset_horner_max_degree,
typename InputT,
typename Rng>
@ -752,15 +1098,23 @@ geneval_offset_horner_result<Degree, InputT> geneval_offset_horner(
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);
// Open only eta; do not form clear x or r.
const InputT eta = offset_horner_group_add(
offset_horner_group_sub(x0, r0),
offset_horner_group_sub(x1, r1));
const InputT center0 = offset_horner_group_add(r0, r0);
const InputT center1 = offset_horner_group_add(r1, r1);
// Payload planting for the joint simulator; not returned to parties.
const InputT center = offset_horner_group_add(center0, center1);
return offset_horner_detail::geneval_at<Degree>(
true, center0, center1, center, eta, knots, coeff, std::move(rng));
}
/// \complexity One Doerner–Shelat generation of a `Degree + 1` lane comparison (`Degree ≤ 3`), then the same local piece dot as `offset_horner_eval`.
/// The dot is `Θ(P · Degree)` after those calls. `P` is the refined piece count.
/// @note Rounds and bandwidth of each `geneval_cmp` live in the DPF headers, not in this function, so they are not stated here.
/// \preprocessing The randomness object the caller passes (`Rng`). This function also samples one `uint64_t` blind per power.
/// @see grotto::offset_poly_eval
/// @see grotto::offset_jet_eval
template <std::size_t Degree = offset_horner_max_degree, typename InputT>
geneval_offset_horner_result<Degree, InputT> geneval_offset_horner(
@ -783,19 +1137,31 @@ geneval_offset_horner_result<Degree, InputT> geneval_offset_horner(
/// @tparam Party party index, `0` or `1`
/// @tparam Degree degree
/// @tparam InputT input domain type
/// @param mat the `mat`
/// @param knots the `knots`
/// @param mat dealer keys
/// @param knots public breakpoints, strictly increasing
/// @param coeff the public coefficient
/// @param eta the `eta`
/// @param eta public offset `eta = x - r`
/// @param tokens proof token folded by the segment walk, or null
/// @return One party's share of the cubic at the wrapped `center + eta`
template <std::size_t Party, std::size_t Degree, typename InputT>
/// \complexity `prepare_pieces` sorts the knots and may insert two cuts. Then one segment walk of the `Degree + 1` lane payload.
/// Each walk is `signed_prefix_parities` over those `P` endpoints (a DPF path of `bitlength(InputT)` levels, reusing the common prefix).
/// 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`.
/// Time is one segment walk. Extra space is the piece vectors, `Θ(P · Degree)` words, with `Degree ≤ 3`.
/// \rounds None. `eta` is an argument; this function does not open it.
/// \communication None.
/// \preprocessing None created here. It reads the one comparison from `make_offset_horner_keys`.
/// @see grotto::offset_poly_eval
/// @see grotto::offset_jet_eval
template <std::size_t Party, std::size_t Degree, typename InputT,
bool Verifiable = false>
uint64_t offset_horner_eval(
const offset_horner_keys<InputT, Degree> & mat,
const offset_horner_keys<InputT, Degree, Verifiable> & mat,
const std::vector<InputT> & knots,
const std::vector<std::array<uint64_t, Degree + 1>> & coeff,
InputT eta)
InputT eta, dpf::proof_token * tokens = nullptr)
{
const auto shares = offset_horner_coefficient_share<Party, Degree>(mat, knots, coeff, eta);
const auto shares = offset_horner_coefficient_share<Party, Degree>(
mat, knots, coeff, eta, tokens);
uint64_t value = 0;
for (uint64_t term : shares)
value += term;