libdpf/include/dpf/geneval.hpp
Ryan Henry 0d22946a0e 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>
2026-09-28 05:59:19 -06:00

1128 lines
48 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 dpf/geneval.hpp
/// @brief Fused generation and evaluation (Doerner–Shelat on the eval trie).
/// @details `make_dpf` / `make_dpf_doerner_shelat` build a reusable key, then
/// `eval_*` walks it. `geneval_*` does both at once: one correction
/// word per level, opened from the XOR-reduction of the nodes the
/// public query actually expands. While the secret path's parent is
/// still in that trie the word matches the reusable key byte for
/// byte (same roots, same Beaver tape). After the path leaves, the
/// word is uniform and later outputs still reconstruct — off-path
/// nodes are identical across the two parties, so a dummy word
/// cancels.
///
/// Default calls take XOR shares of the point. Tagged with
/// `arith_input`, the point is the ring sum of the two shares. A
/// beaver ripple-carry converts those shares to XOR shares of the
/// sum bits before the walk, so the words match `make_dpf` on that
/// sum. The sum is not opened.
///
/// `geneval_cmp` is the comparison-channel form. The value-correction
/// word is a function of the secret path at every level, so the walk
/// stays live for the whole depth and the opened words match a
/// Doerner–Shelat comparison key. Prefix shares are
/// `eval_point(cmp, ...)` at each endpoint. Piecewise-cubic evaluation
/// on top of that is `grotto::geneval_offset_horner`.
/// @note The per-level correction opening follows Jack Doerner and abhi shelat, CCS 2017 (ePrint 2017/827). They return a reusable key. This function opens a word only for nodes on the public query trie and, with a local pad tape, sends nothing.
/// @copyright Copyright (c) 2019-2026 Ryan Henry and [others](@ref authors)
/// @license Released under a GNU General Public v2.0 (GPLv2) license;
/// see [LICENSE.md](@ref license) for details.
#ifndef LIBDPF_INCLUDE_DPF_GENEVAL_HPP__
#define LIBDPF_INCLUDE_DPF_GENEVAL_HPP__
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <iterator>
#include <stdexcept>
#include <tuple>
#include <type_traits>
#include <utility>
#include <vector>
#include "hedley/hedley.h"
#include "simde/simde/x86/avx2.h"
#include "dpf/aligned_allocator.hpp"
#include "dpf/doerner_shelat.hpp"
#include "dpf/eval_target.hpp"
#include "dpf/leaf_node.hpp"
#include "dpf/verifiable.hpp"
namespace dpf
{
/// @brief Shares and the correction words opened along the query trie.
/// @details `correction_words[i]` / `correction_advice[i]` match a reusable key at
/// the same target for every `i < live_levels`. `leaf_live` means the
/// target's leaf was in the trie, so `leaf` is that key's leaf word.
/// @tparam Output output
/// @tparam Leaf leaf
template <typename Output, typename Leaf>
struct geneval_result
{
std::vector<Output> party0;
std::vector<Output> party1;
HEDLEY_PRAGMA(GCC diagnostic push)
HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes")
std::vector<simde__m128i, aligned_allocator<simde__m128i>> correction_words;
HEDLEY_PRAGMA(GCC diagnostic pop)
std::vector<uint8_t> correction_advice;
std::size_t live_levels = 0;
bool leaf_live = false;
Leaf leaf{};
/// @brief Party 0 / 1 VDPF tokens over the live eval trie (empty when unused).
proof_token proof0{};
proof_token proof1{};
};
namespace detail
{
template <typename T>
HEDLEY_ALWAYS_INLINE
HEDLEY_NO_THROW
T geneval_mod_add(T a, T b) noexcept
{
using U = std::make_unsigned_t<T>;
U sum = static_cast<U>(static_cast<U>(a) + static_cast<U>(b));
T out;
std::memcpy(&out, &sum, sizeof(out));
return out;
}
template <typename T>
T geneval_flipped(T x)
{
utils::flip_msb_if_signed_integral(x);
return x;
}
/// @brief Leaf-node id of an already MSB-flipped input. The id is the high
/// `depth` bits; the low `lg(outputs_per_leaf)` bits select the lane.
/// @tparam Dpf dpf
/// @param x the `x`
/// @return Leaf-node id of an already MSB-flipped input
template <typename Dpf>
uint64_t geneval_leaf_id(typename Dpf::input_type x)
{
return static_cast<uint64_t>(utils::get_from_node<Dpf>(x));
}
inline uint64_t geneval_prefix(uint64_t leaf, std::size_t depth, std::size_t bits)
{
if (bits == 0)
return 0;
if (bits >= depth)
return leaf;
return leaf >> (depth - bits);
}
inline bool geneval_any_prefix(const std::vector<uint64_t> & leaves,
std::size_t depth, uint64_t id, std::size_t bits)
{
if (leaves.empty())
return false;
if (bits == 0)
return true;
const std::size_t sh = depth - bits;
const uint64_t lo = (sh >= 64) ? 0 : (id << sh);
auto it = std::lower_bound(leaves.begin(), leaves.end(), lo);
if (it == leaves.end())
return false;
return geneval_prefix(*it, depth, bits) == id;
}
template <typename Output, typename Leaf>
geneval_result<Output, Leaf> geneval_empty_result()
{
geneval_result<Output, Leaf> out;
std::memset(&out.leaf, 0, sizeof(out.leaf));
return out;
}
template <typename InteriorPRG,
typename ExteriorPRG,
typename InputT,
typename OutputT,
typename RootSampler,
typename PadRng>
auto geneval_run(bool arith, bool arith_out, InputT x0, InputT x1,
const std::vector<InputT> & queries, RootSampler & root_sampler,
PadRng & pads, OutputT y0, OutputT y1 = OutputT{})
{
static_assert(std::is_integral_v<InputT>,
"geneval input shares are an integral domain");
static_assert(!dpf::is_wildcard_v<OutputT>,
"geneval output is concrete; assign a wildcard leaf on a key");
static_assert(utils::bitlength_of_v<InputT> <= 64,
"geneval leaf ids are 64-bit");
using dpf_type = utils::dpf_type_t<InteriorPRG, ExteriorPRG, InputT, OutputT>;
using node = typename dpf_type::interior_node;
HEDLEY_PRAGMA(GCC diagnostic push)
HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes")
using leaf_node = leaf_node_t<node, OutputT>;
HEDLEY_PRAGMA(GCC diagnostic pop)
using outputs_tuple = std::tuple<OutputT>;
constexpr std::size_t depth = dpf_type::depth;
if (queries.empty())
return geneval_empty_result<OutputT, leaf_node>();
if (queries.size() > (std::size_t{1} << 22))
throw std::length_error("geneval query is too large");
local_cw_protocol<PadRng> proto{pads};
InputT x0c = x0;
InputT x1c = x1;
proto.encode_walk_shares(x0c, x1c, arith);
// Keep the secret path on share-bits. Do not form a clear alpha for leaf
// placement, live levels, or correction seeds.
std::vector<InputT> flipped;
flipped.reserve(queries.size());
std::vector<uint64_t> leaves;
leaves.reserve(queries.size());
for (const InputT & q : queries)
{
InputT fq = geneval_flipped(q);
flipped.push_back(fq);
leaves.push_back(geneval_leaf_id<dpf_type>(fq));
}
std::vector<uint64_t> unique_leaves = leaves;
std::sort(unique_leaves.begin(), unique_leaves.end());
unique_leaves.erase(std::unique(unique_leaves.begin(), unique_leaves.end()),
unique_leaves.end());
if (unique_leaves.size() > (std::size_t{1} << 20))
throw std::length_error("geneval trie is too large");
constexpr auto to_int = utils::to_integral_type<InputT>{};
using tree = dpf::tree_traits<InteriorPRG>;
HEDLEY_PRAGMA(GCC diagnostic push)
HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes")
node roots[2];
HEDLEY_PRAGMA(GCC diagnostic pop)
tree::root_init(roots, [&]() -> node {
return static_cast<node>(root_sampler());
});
const node root0 = roots[0];
const node root1 = roots[1];
struct slot
{
uint64_t id;
node s0;
node s1;
};
std::vector<slot> frontier;
frontier.push_back(slot{0, root0, root1});
HEDLEY_PRAGMA(GCC diagnostic push)
HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes")
geneval_result<OutputT, leaf_node> result;
HEDLEY_PRAGMA(GCC diagnostic pop)
std::memset(&result.leaf, 0, sizeof(result.leaf));
result.correction_words.reserve(depth);
result.correction_advice.reserve(depth);
result.proof0 = detail::vdpf::zero_proof();
result.proof1 = detail::vdpf::zero_proof();
auto mask = dpf_type::msb_mask;
bool still_live = true;
uint64_t secret_prefix = 0;
for (std::size_t level = 0; level < depth; ++level, mask >>= 1)
{
const uint8_t bit0 = static_cast<uint8_t>(!!(to_int(mask) & to_int(x0c)));
const uint8_t bit1 = static_cast<uint8_t>(!!(to_int(mask) & to_int(x1c)));
const uint64_t parent_id = secret_prefix;
const bool is_last = tree::is_last_level(level, depth);
node L0 = simde_mm_setzero_si128();
node R0 = simde_mm_setzero_si128();
node L1 = simde_mm_setzero_si128();
node R1 = simde_mm_setzero_si128();
bool level_live = false;
struct exp
{
uint64_t id;
node s0, s1, L0, R0, L1, R1;
};
std::vector<exp> exps;
exps.reserve(frontier.size());
for (const slot & n : frontier)
{
if (n.id == parent_id)
level_live = true;
const auto c0 = tree::expand(n.s0, is_last);
const auto c1 = tree::expand(n.s1, is_last);
L0 = ds_xor(L0, c0[0]);
R0 = ds_xor(R0, c0[1]);
L1 = ds_xor(L1, c1[0]);
R1 = ds_xor(R1, c1[1]);
exps.push_back(exp{n.id, n.s0, n.s1, c0[0], c0[1], c1[0], c1[1]});
}
node cw;
uint8_t advice;
if (still_live && level_live)
{
auto blinds = proto.prepare_level(L0, R0, bit0, L1, R1, bit1);
auto opened = proto.open_cw(blinds);
cw = opened.first;
advice = opened.second;
if constexpr (tree::is_half_tree)
{
if (!is_last)
advice = 0;
}
++result.live_levels;
}
else
{
still_live = false;
cw = pads.block();
if constexpr (tree::is_half_tree)
{
if (!is_last)
{
advice = 0;
}
else
{
const uint8_t t0 = static_cast<uint8_t>(pads.bit() & 1u);
const uint8_t t1 = static_cast<uint8_t>(pads.bit() & 1u);
advice = static_cast<uint8_t>((t1 << 1) | t0);
}
}
else
{
const uint8_t t0 = static_cast<uint8_t>(pads.bit() & 1u);
const uint8_t t1 = static_cast<uint8_t>(pads.bit() & 1u);
advice = static_cast<uint8_t>((t1 << 1) | t0);
}
}
result.correction_words.push_back(cw);
result.correction_advice.push_back(advice);
const node cw0 = tree::pack_cw(cw, advice, false, is_last);
const node cw1 = tree::pack_cw(cw, advice, true, is_last);
const std::size_t child_bits = level + 1;
const uint8_t secret_bit = static_cast<uint8_t>((bit0 ^ bit1) & 1u);
secret_prefix = (secret_prefix << 1) | secret_bit;
std::vector<slot> next;
next.reserve(exps.size() * 2);
for (const exp & e : exps)
{
const uint64_t left = e.id << 1;
const uint64_t right = left | 1ull;
if (geneval_any_prefix(unique_leaves, depth, left, child_bits))
{
next.push_back(slot{left,
dpf::xor_if_lo_bit(e.L0, cw0, e.s0),
dpf::xor_if_lo_bit(e.L1, cw0, e.s1)});
}
if (geneval_any_prefix(unique_leaves, depth, right, child_bits))
{
next.push_back(slot{right,
dpf::xor_if_lo_bit(e.R0, cw1, e.s0),
dpf::xor_if_lo_bit(e.R1, cw1, e.s1)});
}
}
// Fold every live child into both parties' VDPF tokens.
if (!next.empty())
{
cs_block cs{};
bool have_cs = false;
for (const slot & c : next)
{
if (c.id == secret_prefix)
{
// Prefix is the share-bit path accumulated above — not a
// fresh xor_input_shares of the point for leaf placement.
cs = detail::vdpf::make_cs(level, c.id, c.s0, c.s1);
have_cs = true;
break;
}
}
if (!have_cs)
cs = detail::vdpf::make_cs(level, next[0].id, next[0].s0,
next[0].s1);
for (const slot & c : next)
{
detail::vdpf::fold_node(result.proof0, level, c.id, c.s0, cs);
detail::vdpf::fold_node(result.proof1, level, c.id, c.s1, cs);
}
}
frontier = std::move(next);
}
const uint64_t secret_leaf = secret_prefix;
result.leaf_live = geneval_any_prefix(unique_leaves, depth, secret_leaf, depth);
if (result.leaf_live)
{
const slot * on = nullptr;
for (const slot & n : frontier)
{
if (n.id == secret_leaf)
{
on = &n;
break;
}
}
if (on == nullptr)
throw std::logic_error("geneval: secret leaf missing from trie");
if (arith_out)
{
const uint8_t t0 = static_cast<uint8_t>(dpf::get_lo_bit(on->s0));
const uint8_t t1 = static_cast<uint8_t>(dpf::get_lo_bit(on->s1));
result.leaf = proto.template open_arith_leaf<ExteriorPRG, 0, outputs_tuple>(
dpf::unset_lo_2bits(on->s0), dpf::unset_lo_2bits(on->s1), t0, t1,
y0, y1, std::size_t{0}, x0c, x1c);
}
else
{
// Mux / reconstruct only inside the leaf protocol hook.
proto.open_leaf_group(x0c, x1c, [&](InputT sx0, InputT sx1) {
const InputT x = utils::xor_input_shares(sx0, sx1);
const bool sign0 = dpf::get_lo_bit(on->s0);
auto built = dpf::make_leaves<ExteriorPRG>(x,
dpf::unset_lo_2bits(on->s0), dpf::unset_lo_2bits(on->s1),
sign0, std::size_t{0}, y0);
result.leaf = std::get<0>(built.first.first);
});
}
}
result.party0.reserve(flipped.size());
result.party1.reserve(flipped.size());
for (std::size_t i = 0; i < flipped.size(); ++i)
{
const uint64_t id = leaves[i];
const slot * n = nullptr;
for (const slot & s : frontier)
{
if (s.id == id)
{
n = &s;
break;
}
}
if (n == nullptr)
throw std::logic_error("geneval: query leaf missing from trie");
auto share0 = dpf_type::template traverse_exterior<0>(n->s0, result.leaf);
auto share1 = dpf_type::template traverse_exterior<0>(n->s1, result.leaf);
const auto lane = static_cast<std::size_t>(to_int(flipped[i]));
result.party0.push_back(extract_leaf<node, OutputT>(share0, lane));
result.party1.push_back(extract_leaf<node, OutputT>(share1, lane));
}
return result;
}
template <typename InteriorPRG,
typename ExteriorPRG,
typename InputT,
typename OutputT,
typename RootSampler,
typename PadRng>
auto geneval_run(bool arith, InputT x0, InputT x1,
const std::vector<InputT> & queries, RootSampler & root_sampler,
PadRng & pads, OutputT y)
{
return geneval_run<InteriorPRG, ExteriorPRG>(arith, false, x0, x1, queries,
root_sampler, pads, y, OutputT{});
}
template <typename InteriorPRG,
typename ExteriorPRG,
typename InputT,
typename OutputT,
typename RootSampler,
typename PadRng>
auto geneval_run(InputT x0, InputT x1, const std::vector<InputT> & queries,
RootSampler & root_sampler, PadRng & pads, OutputT y)
{
return geneval_run<InteriorPRG, ExteriorPRG>(false, false, x0, x1, queries,
root_sampler, pads, y, OutputT{});
}
template <typename InputT>
InputT geneval_from_bits(uint64_t bits)
{
using U = std::make_unsigned_t<InputT>;
U u = static_cast<U>(bits);
InputT out;
std::memcpy(&out, &u, sizeof(out));
return out;
}
template <typename InputT>
bool geneval_out_of_order(InputT from, InputT to)
{
// Numeric order. An unsigned compare of a signed value treats a negative
// `from` as larger than a positive `to`, and would reject `[-1, 1]`.
if constexpr (std::is_signed_v<InputT>)
return from > to;
else
return utils::to_integral_type<InputT>{}(from)
> utils::to_integral_type<InputT>{}(to);
}
template <typename InputT>
std::vector<InputT> geneval_full_domain()
{
constexpr std::size_t bitlen = utils::bitlength_of_v<InputT>;
if (bitlen > 20)
throw std::length_error("geneval_full domain is too large");
const uint64_t n = uint64_t{1} << bitlen;
std::vector<InputT> qs(static_cast<std::size_t>(n));
// Index `i` is the input's bit pattern, including the sign bit. A
// narrowing cast of `i` to a signed type is implementation-defined.
for (uint64_t i = 0; i < n; ++i)
qs[static_cast<std::size_t>(i)] = geneval_from_bits<InputT>(i);
return qs;
}
template <typename InputT>
std::vector<InputT> geneval_inclusive(InputT from, InputT to)
{
if (geneval_out_of_order(from, to))
{
throw std::invalid_argument("geneval_interval: from > to");
}
std::vector<InputT> qs;
InputT q = from;
const InputT one = utils::make_from_integral_value<InputT>{}(1);
for (;;)
{
qs.push_back(q);
if (q == to)
break;
q = geneval_mod_add(q, one);
if (qs.size() > (std::size_t{1} << 22))
throw std::length_error("geneval_interval is too large");
}
return qs;
}
} // namespace detail
/// @name Point geneval
/// @tparam InteriorPRG PRG that expands interior nodes. Defaults to `dpf::prg::aes128`
/// @tparam ExteriorPRG PRG that expands the root. Defaults to `InteriorPRG`
/// @tparam InputT input domain type
/// @tparam OutputT output type
/// @tparam RootSampler sampler for the Doerner–Shelat root seed
/// @tparam PadRng pad stream for the Doerner–Shelat protocol
/// @param x0 party 0's share of the secret point
/// @param x1 party 1's share of the secret point
/// @param query the query point
/// @param rng the Doerner–Shelat randomness tapes
/// @{
/// @brief The secret point is `x0 XOR x1`.
/// @param x0 party 0's share of the secret point
/// @param x1 party 1's share of the secret point
/// @param query the query point
/// @param rng the Doerner–Shelat randomness tapes
/// @param y the payload
/// @return the opened shares and correction words
/// \complexity O(n F) PRG expansions. n is `depth`. Each level expands every frontier node (two `expand` calls, one per share) and, while the secret path is live, one `prepare_level`. F is at most the number of distinct query leaves; the function rejects more than 2^20. Setup sorts the q query ids.
/// \rounds No sockets. While the path is live, each level calls `prepare_level`, which samples one `ds_cw_pads` and two `ds_and_pads` and then `open_cw`.
/// \communication none in this function.
/// \preprocessing The `ds_randomness` tape: per live level, `ds_sample_cw` draws two 128-bit blocks and two bits, and each of the two AND pads is one `ds_sample_and`.
template <typename InteriorPRG = dpf::prg::aes128,
typename ExteriorPRG = InteriorPRG,
typename InputT,
typename OutputT,
typename RootSampler,
typename PadRng>
HEDLEY_WARN_UNUSED_RESULT
auto geneval_point(InputT x0, InputT x1, InputT query,
ds_randomness<RootSampler, PadRng> rng, OutputT y)
{
return detail::geneval_run<InteriorPRG, ExteriorPRG>(false, false, x0, x1,
std::vector<InputT>{query}, rng.root, rng.pad, y, OutputT{});
}
/// @brief The secret point is `x0 + x1`.
/// @param x0 party 0's share of the secret point
/// @param x1 party 1's share of the secret point
/// @param query the query point
/// @param rng the Doerner–Shelat randomness tapes
/// @param y the payload
/// @return the opened shares and correction words
/// \complexity O(n F) PRG expansions. n is `depth`. Each level expands every frontier node (two `expand` calls, one per share) and, while the secret path is live, one `prepare_level`. F is at most the number of distinct query leaves; the function rejects more than 2^20. Setup sorts the q query ids.
/// \rounds No sockets. While the path is live, each level calls `prepare_level`, which samples one `ds_cw_pads` and two `ds_and_pads` and then `open_cw`.
/// \communication none in this function.
/// \preprocessing The `ds_randomness` tape: per live level, `ds_sample_cw` draws two 128-bit blocks and two bits, and each of the two AND pads is one `ds_sample_and`.
template <typename InteriorPRG = dpf::prg::aes128,
typename ExteriorPRG = InteriorPRG,
typename InputT,
typename OutputT,
typename RootSampler,
typename PadRng>
HEDLEY_WARN_UNUSED_RESULT
auto geneval_point(arith_input_t, InputT x0, InputT x1, InputT query,
ds_randomness<RootSampler, PadRng> rng, OutputT y)
{
return detail::geneval_run<InteriorPRG, ExteriorPRG>(true, false, x0, x1,
std::vector<InputT>{query}, rng.root, rng.pad, y, OutputT{});
}
/// @brief XOR-index shares, additively shared payload `y0 + y1 = β`.
/// @param x0 party 0's share of the secret point
/// @param x1 party 1's share of the secret point
/// @param query the query point
/// @param rng the Doerner–Shelat randomness tapes
/// @param y0 party 0's share of the payload
/// @param y1 party 1's share of the payload
/// @return the opened shares and correction words
/// \complexity O(n F) PRG expansions. n is `depth`. Each level expands every frontier node (two `expand` calls, one per share) and, while the secret path is live, one `prepare_level`. F is at most the number of distinct query leaves; the function rejects more than 2^20. Setup sorts the q query ids.
/// \rounds No sockets. While the path is live, each level calls `prepare_level`, which samples one `ds_cw_pads` and two `ds_and_pads` and then `open_cw`.
/// \communication none in this function.
/// \preprocessing The `ds_randomness` tape: per live level, `ds_sample_cw` draws two 128-bit blocks and two bits, and each of the two AND pads is one `ds_sample_and`.
template <typename InteriorPRG = dpf::prg::aes128,
typename ExteriorPRG = InteriorPRG,
typename InputT,
typename OutputT,
typename RootSampler,
typename PadRng>
HEDLEY_WARN_UNUSED_RESULT
auto geneval_point(arith_output_t, InputT x0, InputT x1, InputT query,
ds_randomness<RootSampler, PadRng> rng, OutputT y0, OutputT y1)
{
return detail::geneval_run<InteriorPRG, ExteriorPRG>(false, true, x0, x1,
std::vector<InputT>{query}, rng.root, rng.pad, y0, y1);
}
/// @brief Additive index and additive payload shares.
/// @param x0 party 0's share of the secret point
/// @param x1 party 1's share of the secret point
/// @param query the query point
/// @param rng the Doerner–Shelat randomness tapes
/// @param y0 party 0's share of the payload
/// @param y1 party 1's share of the payload
/// @return the opened shares and correction words
/// \complexity O(n F) PRG expansions. n is `depth`. Each level expands every frontier node (two `expand` calls, one per share) and, while the secret path is live, one `prepare_level`. F is at most the number of distinct query leaves; the function rejects more than 2^20. Setup sorts the q query ids.
/// \rounds No sockets. While the path is live, each level calls `prepare_level`, which samples one `ds_cw_pads` and two `ds_and_pads` and then `open_cw`.
/// \communication none in this function.
/// \preprocessing The `ds_randomness` tape: per live level, `ds_sample_cw` draws two 128-bit blocks and two bits, and each of the two AND pads is one `ds_sample_and`.
template <typename InteriorPRG = dpf::prg::aes128,
typename ExteriorPRG = InteriorPRG,
typename InputT,
typename OutputT,
typename RootSampler,
typename PadRng>
HEDLEY_WARN_UNUSED_RESULT
auto geneval_point(arith_input_t, arith_output_t, InputT x0, InputT x1,
InputT query, ds_randomness<RootSampler, PadRng> rng, OutputT y0, OutputT y1)
{
return detail::geneval_run<InteriorPRG, ExteriorPRG>(true, true, x0, x1,
std::vector<InputT>{query}, rng.root, rng.pad, y0, y1);
}
/// @}
/// @brief Geneval on the inclusive interval `[from, to]`.
/// @tparam InteriorPRG PRG that expands interior nodes. Defaults to `dpf::prg::aes128`
/// @tparam ExteriorPRG PRG that expands the root. Defaults to `InteriorPRG`
/// @tparam InputT input domain type
/// @tparam OutputT output type
/// @tparam RootSampler sampler for the Doerner–Shelat root seed
/// @tparam PadRng pad stream for the Doerner–Shelat protocol
/// @param x0 the `x0`
/// @param x1 the `x1`
/// @param from the inclusive start of the range
/// @param to the `to`
/// @param rng the Doerner–Shelat randomness tapes
/// @param y the `y`
/// @return Geneval on the inclusive interval `[from, to]`
/// \complexity O(n F) PRG expansions. n is `depth`. Each level expands every frontier node (two `expand` calls, one per share) and, while the secret path is live, one `prepare_level`. F is at most the number of distinct query leaves; the function rejects more than 2^20. Setup sorts the q query ids.
/// \rounds No sockets. While the path is live, each level calls `prepare_level`, which samples one `ds_cw_pads` and two `ds_and_pads` and then `open_cw`.
/// \communication none in this function.
/// \preprocessing The `ds_randomness` tape: per live level, `ds_sample_cw` draws two 128-bit blocks and two bits, and each of the two AND pads is one `ds_sample_and`.
template <typename InteriorPRG = dpf::prg::aes128,
typename ExteriorPRG = InteriorPRG,
typename InputT,
typename OutputT,
typename RootSampler,
typename PadRng>
HEDLEY_WARN_UNUSED_RESULT
auto geneval_interval(InputT x0, InputT x1, InputT from, InputT to,
ds_randomness<RootSampler, PadRng> rng, OutputT y)
{
return detail::geneval_run<InteriorPRG, ExteriorPRG>(false, x0, x1,
detail::geneval_inclusive(from, to), rng.root, rng.pad, y);
}
/// \complexity O(n F) PRG expansions. n is `depth`. Each level expands every frontier node (two `expand` calls, one per share) and, while the secret path is live, one `prepare_level`. F is at most the number of distinct query leaves; the function rejects more than 2^20. Setup sorts the q query ids.
/// \rounds No sockets. While the path is live, each level calls `prepare_level`, which samples one `ds_cw_pads` and two `ds_and_pads` and then `open_cw`.
/// \communication none in this function.
/// \preprocessing The `ds_randomness` tape: per live level, `ds_sample_cw` draws two 128-bit blocks and two bits, and each of the two AND pads is one `ds_sample_and`.
template <typename InteriorPRG = dpf::prg::aes128,
typename ExteriorPRG = InteriorPRG,
typename InputT,
typename OutputT,
typename RootSampler,
typename PadRng>
HEDLEY_WARN_UNUSED_RESULT
auto geneval_interval(arith_input_t, InputT x0, InputT x1, InputT from,
InputT to, ds_randomness<RootSampler, PadRng> rng, OutputT y)
{
return detail::geneval_run<InteriorPRG, ExteriorPRG>(true, x0, x1,
detail::geneval_inclusive(from, to), rng.root, rng.pad, y);
}
/// @brief Geneval on the whole domain.
/// @details Materializes the query list. Domains wider than 20 bits refuse so
/// a caller does not allocate a `2^n` vector by accident. Prefer the
/// buffer overloads when writing into a pre-sized output scratch.
/// @tparam InteriorPRG PRG that expands interior nodes. Defaults to `dpf::prg::aes128`
/// @tparam ExteriorPRG PRG that expands the root. Defaults to `InteriorPRG`
/// @tparam InputT input domain type
/// @tparam OutputT output type
/// @tparam RootSampler sampler for the Doerner–Shelat root seed
/// @tparam PadRng pad stream for the Doerner–Shelat protocol
/// @param x0 the `x0`
/// @param x1 the `x1`
/// @param rng the Doerner–Shelat randomness tapes
/// @param y the `y`
/// @return Geneval on the whole domain
/// \complexity O(n F) PRG expansions. n is `depth`. Each level expands every frontier node (two `expand` calls, one per share) and, while the secret path is live, one `prepare_level`. F is at most the number of distinct query leaves; the function rejects more than 2^20. Setup sorts the q query ids.
/// \rounds No sockets. While the path is live, each level calls `prepare_level`, which samples one `ds_cw_pads` and two `ds_and_pads` and then `open_cw`.
/// \communication none in this function.
/// \preprocessing The `ds_randomness` tape: per live level, `ds_sample_cw` draws two 128-bit blocks and two bits, and each of the two AND pads is one `ds_sample_and`.
template <typename InteriorPRG = dpf::prg::aes128,
typename ExteriorPRG = InteriorPRG,
typename InputT,
typename OutputT,
typename RootSampler,
typename PadRng>
HEDLEY_WARN_UNUSED_RESULT
auto geneval_full(InputT x0, InputT x1,
ds_randomness<RootSampler, PadRng> rng, OutputT y)
{
return detail::geneval_run<InteriorPRG, ExteriorPRG>(false, x0, x1,
detail::geneval_full_domain<InputT>(), rng.root, rng.pad, y);
}
/// \complexity O(n F) PRG expansions. n is `depth`. Each level expands every frontier node (two `expand` calls, one per share) and, while the secret path is live, one `prepare_level`. F is at most the number of distinct query leaves; the function rejects more than 2^20. Setup sorts the q query ids.
/// \rounds No sockets. While the path is live, each level calls `prepare_level`, which samples one `ds_cw_pads` and two `ds_and_pads` and then `open_cw`.
/// \communication none in this function.
/// \preprocessing The `ds_randomness` tape: per live level, `ds_sample_cw` draws two 128-bit blocks and two bits, and each of the two AND pads is one `ds_sample_and`.
template <typename InteriorPRG = dpf::prg::aes128,
typename ExteriorPRG = InteriorPRG,
typename InputT,
typename OutputT,
typename RootSampler,
typename PadRng>
HEDLEY_WARN_UNUSED_RESULT
auto geneval_full(arith_input_t, InputT x0, InputT x1,
ds_randomness<RootSampler, PadRng> rng, OutputT y)
{
return detail::geneval_run<InteriorPRG, ExteriorPRG>(true, x0, x1,
detail::geneval_full_domain<InputT>(), rng.root, rng.pad, y);
}
/// @brief Geneval on a public sequence, in the order given.
/// @tparam InteriorPRG PRG that expands interior nodes. Defaults to `dpf::prg::aes128`
/// @tparam ExteriorPRG PRG that expands the root. Defaults to `InteriorPRG`
/// @tparam InputT input domain type
/// @tparam OutputT output type
/// @tparam RootSampler sampler for the Doerner–Shelat root seed
/// @tparam PadRng pad stream for the Doerner–Shelat protocol
/// @tparam ForwardIterator forward iterator type
/// @param x0 the `x0`
/// @param x1 the `x1`
/// @param begin the iterator to the first query
/// @param end the iterator past the last query
/// @param rng the Doerner–Shelat randomness tapes
/// @param y the `y`
/// @return Geneval on a public sequence, in the order given
/// \complexity O(n F) PRG expansions. n is `depth`. Each level expands every frontier node (two `expand` calls, one per share) and, while the secret path is live, one `prepare_level`. F is at most the number of distinct query leaves; the function rejects more than 2^20. Setup sorts the q query ids.
/// \rounds No sockets. While the path is live, each level calls `prepare_level`, which samples one `ds_cw_pads` and two `ds_and_pads` and then `open_cw`.
/// \communication none in this function.
/// \preprocessing The `ds_randomness` tape: per live level, `ds_sample_cw` draws two 128-bit blocks and two bits, and each of the two AND pads is one `ds_sample_and`.
template <typename InteriorPRG = dpf::prg::aes128,
typename ExteriorPRG = InteriorPRG,
typename InputT,
typename OutputT,
typename RootSampler,
typename PadRng,
typename ForwardIterator>
HEDLEY_WARN_UNUSED_RESULT
auto geneval_sequence(InputT x0, InputT x1, ForwardIterator begin,
ForwardIterator end, ds_randomness<RootSampler, PadRng> rng, OutputT y)
{
std::vector<InputT> qs(begin, end);
return detail::geneval_run<InteriorPRG, ExteriorPRG>(false, x0, x1,
std::move(qs), rng.root, rng.pad, y);
}
/// \complexity O(n F) PRG expansions. n is `depth`. Each level expands every frontier node (two `expand` calls, one per share) and, while the secret path is live, one `prepare_level`. F is at most the number of distinct query leaves; the function rejects more than 2^20. Setup sorts the q query ids.
/// \rounds No sockets. While the path is live, each level calls `prepare_level`, which samples one `ds_cw_pads` and two `ds_and_pads` and then `open_cw`.
/// \communication none in this function.
/// \preprocessing The `ds_randomness` tape: per live level, `ds_sample_cw` draws two 128-bit blocks and two bits, and each of the two AND pads is one `ds_sample_and`.
template <typename InteriorPRG = dpf::prg::aes128,
typename ExteriorPRG = InteriorPRG,
typename InputT,
typename OutputT,
typename RootSampler,
typename PadRng,
typename ForwardIterator>
HEDLEY_WARN_UNUSED_RESULT
auto geneval_sequence(arith_input_t, InputT x0, InputT x1,
ForwardIterator begin, ForwardIterator end,
ds_randomness<RootSampler, PadRng> rng, OutputT y)
{
std::vector<InputT> qs(begin, end);
return detail::geneval_run<InteriorPRG, ExteriorPRG>(true, x0, x1,
std::move(qs), rng.root, rng.pad, y);
}
/// @brief Opened comparison key material and one prefix share per endpoint.
/// @details `live_levels` is the full depth: a comparison value word depends on the
/// secret path at every level, so there is no early dummy-word tail.
struct geneval_cmp_result
{
std::vector<uint64_t> party0;
std::vector<uint64_t> party1;
HEDLEY_PRAGMA(GCC diagnostic push)
HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes")
std::vector<simde__m128i, aligned_allocator<simde__m128i>> correction_words;
HEDLEY_PRAGMA(GCC diagnostic pop)
std::vector<uint8_t> correction_advice;
std::vector<uint64_t> value_cw;
std::vector<uint64_t> tail_cw;
uint64_t cw_last = 0;
uint64_t addend0 = 0;
uint64_t addend1 = 0;
uint64_t mask = 0;
std::size_t live_levels = 0;
proof_token proof0{};
proof_token proof1{};
};
/// @name Comparison geneval
/// @tparam InputT input domain type
/// @tparam ForwardIterator forward iterator type
/// @tparam RootSampler sampler for the Doerner–Shelat root seed
/// @tparam PadRng pad stream for the Doerner–Shelat protocol
/// @param x0 party 0's share of the secret point
/// @param x1 party 1's share of the secret point
/// @param begin the iterator to the first query
/// @param end the iterator past the last query
/// @param rng the Doerner–Shelat randomness tapes
/// @return the opened comparison shares
/// @{
/// @brief `x0 XOR x1` is the secret point, in the same share convention as
/// `geneval_point`. `spec` is an `lt` / `leq` / `gt` / `geq` pack. Each
/// endpoint is returned in order as the two parties' `eval_point(cmp, ...)`
/// shares. An empty range opens nothing.
/// @tparam Spec comparison or interval specification
/// @param x0 party 0's share of the secret point
/// @param x1 party 1's share of the secret point
/// @param begin the iterator to the first query
/// @param end the iterator past the last query
/// @param rng the Doerner–Shelat randomness tapes
/// @param spec the comparison specification
template <typename InputT,
typename ForwardIterator,
typename RootSampler,
typename PadRng,
typename Spec>
HEDLEY_WARN_UNUSED_RESULT
geneval_cmp_result geneval_cmp(InputT x0, InputT x1,
ForwardIterator begin, ForwardIterator end,
ds_randomness<RootSampler, PadRng> rng, Spec spec)
{
geneval_cmp_result out;
if (begin == end)
return out;
auto keys = make_dpf_doerner_shelat(std::move(x0), std::move(x1),
std::move(rng), std::move(spec), dpf::verifiable{});
const auto & k0 = keys.first;
const auto & k1 = keys.second;
using key_type = std::decay_t<decltype(k0)>;
constexpr std::size_t depth = key_type::depth;
out.live_levels = depth;
out.mask = k0.cmp().mask;
out.cw_last = k0.cw_last();
out.addend0 = k0.cmp_addend().raw();
out.addend1 = k1.cmp_addend().raw();
out.correction_words.resize(depth);
out.correction_advice.resize(depth);
if constexpr (key_type::cmp_block > 0)
{
out.value_cw.resize(key_type::cmp_checkpoints);
for (std::size_t i = 0; i < key_type::cmp_checkpoints; ++i)
out.value_cw[i] = k0.value_cw(i);
out.tail_cw.resize(key_type::cmp_tail);
for (std::size_t z = 0; z < key_type::cmp_tail; ++z)
out.tail_cw[z] = k0.tail_cw(z);
}
else
out.value_cw.resize(depth);
for (std::size_t level = 0; level < depth; ++level)
{
out.correction_words[level] = k0.correction_word(level);
out.correction_advice[level] = static_cast<uint8_t>(k0.correction_advice(level));
if constexpr (key_type::cmp_block == 0)
out.value_cw[level] = k0.value_cw(level);
}
detail::vdpf::init_proof(out.proof0, k0);
detail::vdpf::init_proof(out.proof1, k1);
auto path0 = make_basic_path_memoizer(k0);
auto path1 = make_basic_path_memoizer(k1);
for (auto it = begin; it != end; ++it)
{
out.party0.push_back(
detail::incr::eval_cmp_point_impl(k0, *it, path0, &out.proof0).raw());
out.party1.push_back(
detail::incr::eval_cmp_point_impl(k1, *it, path1, &out.proof1).raw());
}
detail::vdpf::fold_output_binding(out.proof0, k0);
detail::vdpf::fold_output_binding(out.proof1, k1);
return out;
}
/// @brief Additive shares of the point (`x0 + x1`).
/// @tparam Spec comparison or interval specification
/// @param x0 party 0's share of the secret point
/// @param x1 party 1's share of the secret point
/// @param begin the iterator to the first query
/// @param end the iterator past the last query
/// @param rng the Doerner–Shelat randomness tapes
/// @param spec the comparison specification
template <typename InputT,
typename ForwardIterator,
typename RootSampler,
typename PadRng,
typename Spec>
HEDLEY_WARN_UNUSED_RESULT
geneval_cmp_result geneval_cmp(arith_input_t, InputT x0, InputT x1,
ForwardIterator begin, ForwardIterator end,
ds_randomness<RootSampler, PadRng> rng, Spec spec)
{
geneval_cmp_result out;
if (begin == end)
return out;
auto keys = make_dpf_doerner_shelat(arith_input, std::move(x0), std::move(x1),
std::move(rng), std::move(spec), dpf::verifiable{});
const auto & k0 = keys.first;
const auto & k1 = keys.second;
using key_type = std::decay_t<decltype(k0)>;
constexpr std::size_t depth = key_type::depth;
out.live_levels = depth;
out.mask = k0.cmp().mask;
out.cw_last = k0.cw_last();
out.addend0 = k0.cmp_addend().raw();
out.addend1 = k1.cmp_addend().raw();
out.correction_words.resize(depth);
out.correction_advice.resize(depth);
if constexpr (key_type::cmp_block > 0)
{
out.value_cw.resize(key_type::cmp_checkpoints);
for (std::size_t i = 0; i < key_type::cmp_checkpoints; ++i)
out.value_cw[i] = k0.value_cw(i);
out.tail_cw.resize(key_type::cmp_tail);
for (std::size_t z = 0; z < key_type::cmp_tail; ++z)
out.tail_cw[z] = k0.tail_cw(z);
}
else
out.value_cw.resize(depth);
for (std::size_t level = 0; level < depth; ++level)
{
out.correction_words[level] = k0.correction_word(level);
out.correction_advice[level] = static_cast<uint8_t>(k0.correction_advice(level));
if constexpr (key_type::cmp_block == 0)
out.value_cw[level] = k0.value_cw(level);
}
detail::vdpf::init_proof(out.proof0, k0);
detail::vdpf::init_proof(out.proof1, k1);
auto path0 = make_basic_path_memoizer(k0);
auto path1 = make_basic_path_memoizer(k1);
for (auto it = begin; it != end; ++it)
{
out.party0.push_back(
detail::incr::eval_cmp_point_impl(k0, *it, path0, &out.proof0).raw());
out.party1.push_back(
detail::incr::eval_cmp_point_impl(k1, *it, path1, &out.proof1).raw());
}
detail::vdpf::fold_output_binding(out.proof0, k0);
detail::vdpf::fold_output_binding(out.proof1, k1);
return out;
}
/// @brief `gt(beta)` on XOR shares of the point. `if_false` is 0.
/// @param x0 party 0's share of the secret point
/// @param x1 party 1's share of the secret point
/// @param begin the iterator to the first query
/// @param end the iterator past the last query
/// @param rng the Doerner–Shelat randomness tapes
/// @param beta the true payload
template <typename InputT,
typename ForwardIterator,
typename RootSampler,
typename PadRng>
HEDLEY_WARN_UNUSED_RESULT
geneval_cmp_result geneval_cmp(InputT x0, InputT x1,
ForwardIterator begin, ForwardIterator end,
ds_randomness<RootSampler, PadRng> rng, uint64_t beta)
{
return geneval_cmp(std::move(x0), std::move(x1), begin, end,
std::move(rng), dpf::gt(beta));
}
/// @brief `gt(beta)` on additive shares of the point. `if_false` is 0.
/// @param x0 party 0's share of the secret point
/// @param x1 party 1's share of the secret point
/// @param begin the iterator to the first query
/// @param end the iterator past the last query
/// @param rng the Doerner–Shelat randomness tapes
/// @param beta the true payload
template <typename InputT,
typename ForwardIterator,
typename RootSampler,
typename PadRng>
HEDLEY_WARN_UNUSED_RESULT
geneval_cmp_result geneval_cmp(arith_input_t, InputT x0, InputT x1,
ForwardIterator begin, ForwardIterator end,
ds_randomness<RootSampler, PadRng> rng, uint64_t beta)
{
return geneval_cmp(arith_input, std::move(x0), std::move(x1), begin, end,
std::move(rng), dpf::gt(beta));
}
/// @}
namespace detail
{
/// @brief Copy party shares from a geneval result into caller buffers.
template <typename Result, typename Buf0, typename Buf1>
void geneval_fill_buffers(const Result & r, Buf0 & buf0, Buf1 & buf1)
{
const std::size_t n = r.party0.size();
if (utils::size(buf0) < n || utils::size(buf1) < n)
throw std::length_error("geneval: output buffer is too small");
for (std::size_t i = 0; i < n; ++i)
{
buf0[i] = r.party0[i];
buf1[i] = r.party1[i];
}
}
} // namespace detail
/// @name Geneval into caller buffers
/// @details Thin overloads that run the same trie walk, then copy party shares
/// into `buf0` / `buf1` (same layout as `eval_interval` / `eval_sequence`
/// output buffers). Memoizer arguments for the fused trie are internal;
/// path memoizers live on `geneval_cmp` / `geneval_ic`.
/// @{
/// \complexity O(n F) PRG expansions. n is `depth`. Each level expands every frontier node (two `expand` calls, one per share) and, while the secret path is live, one `prepare_level`. F is at most the number of distinct query leaves; the function rejects more than 2^20. Setup sorts the q query ids.
/// \rounds No sockets. While the path is live, each level calls `prepare_level`, which samples one `ds_cw_pads` and two `ds_and_pads` and then `open_cw`.
/// \communication none in this function.
/// \preprocessing The `ds_randomness` tape: per live level, `ds_sample_cw` draws two 128-bit blocks and two bits, and each of the two AND pads is one `ds_sample_and`.
template <typename InteriorPRG = dpf::prg::aes128,
typename ExteriorPRG = InteriorPRG,
typename InputT,
typename OutputT,
typename RootSampler,
typename PadRng,
typename Buf0,
typename Buf1>
HEDLEY_WARN_UNUSED_RESULT
auto geneval_point(InputT x0, InputT x1, InputT query,
ds_randomness<RootSampler, PadRng> rng, OutputT y, Buf0 & buf0, Buf1 & buf1)
{
auto r = geneval_point<InteriorPRG, ExteriorPRG>(std::move(x0),
std::move(x1), query, std::move(rng), std::move(y));
detail::geneval_fill_buffers(r, buf0, buf1);
return r;
}
/// \complexity O(n F) PRG expansions. n is `depth`. Each level expands every frontier node (two `expand` calls, one per share) and, while the secret path is live, one `prepare_level`. F is at most the number of distinct query leaves; the function rejects more than 2^20. Setup sorts the q query ids.
/// \rounds No sockets. While the path is live, each level calls `prepare_level`, which samples one `ds_cw_pads` and two `ds_and_pads` and then `open_cw`.
/// \communication none in this function.
/// \preprocessing The `ds_randomness` tape: per live level, `ds_sample_cw` draws two 128-bit blocks and two bits, and each of the two AND pads is one `ds_sample_and`.
template <typename InteriorPRG = dpf::prg::aes128,
typename ExteriorPRG = InteriorPRG,
typename InputT,
typename OutputT,
typename RootSampler,
typename PadRng,
typename Buf0,
typename Buf1>
HEDLEY_WARN_UNUSED_RESULT
auto geneval_interval(InputT x0, InputT x1, InputT from, InputT to,
ds_randomness<RootSampler, PadRng> rng, OutputT y, Buf0 & buf0, Buf1 & buf1)
{
auto r = geneval_interval<InteriorPRG, ExteriorPRG>(std::move(x0),
std::move(x1), from, to, std::move(rng), std::move(y));
detail::geneval_fill_buffers(r, buf0, buf1);
return r;
}
/// \complexity O(n F) PRG expansions. n is `depth`. Each level expands every frontier node (two `expand` calls, one per share) and, while the secret path is live, one `prepare_level`. F is at most the number of distinct query leaves; the function rejects more than 2^20. Setup sorts the q query ids.
/// \rounds No sockets. While the path is live, each level calls `prepare_level`, which samples one `ds_cw_pads` and two `ds_and_pads` and then `open_cw`.
/// \communication none in this function.
/// \preprocessing The `ds_randomness` tape: per live level, `ds_sample_cw` draws two 128-bit blocks and two bits, and each of the two AND pads is one `ds_sample_and`.
template <typename InteriorPRG = dpf::prg::aes128,
typename ExteriorPRG = InteriorPRG,
typename InputT,
typename OutputT,
typename RootSampler,
typename PadRng,
typename Buf0,
typename Buf1>
HEDLEY_WARN_UNUSED_RESULT
auto geneval_full(InputT x0, InputT x1,
ds_randomness<RootSampler, PadRng> rng, OutputT y, Buf0 & buf0, Buf1 & buf1)
{
auto r = geneval_full<InteriorPRG, ExteriorPRG>(std::move(x0),
std::move(x1), std::move(rng), std::move(y));
detail::geneval_fill_buffers(r, buf0, buf1);
return r;
}
/// \complexity O(n F) PRG expansions. n is `depth`. Each level expands every frontier node (two `expand` calls, one per share) and, while the secret path is live, one `prepare_level`. F is at most the number of distinct query leaves; the function rejects more than 2^20. Setup sorts the q query ids.
/// \rounds No sockets. While the path is live, each level calls `prepare_level`, which samples one `ds_cw_pads` and two `ds_and_pads` and then `open_cw`.
/// \communication none in this function.
/// \preprocessing The `ds_randomness` tape: per live level, `ds_sample_cw` draws two 128-bit blocks and two bits, and each of the two AND pads is one `ds_sample_and`.
template <typename InteriorPRG = dpf::prg::aes128,
typename ExteriorPRG = InteriorPRG,
typename InputT,
typename OutputT,
typename RootSampler,
typename PadRng,
typename ForwardIterator,
typename Buf0,
typename Buf1>
HEDLEY_WARN_UNUSED_RESULT
auto geneval_sequence(InputT x0, InputT x1, ForwardIterator begin,
ForwardIterator end, ds_randomness<RootSampler, PadRng> rng, OutputT y,
Buf0 & buf0, Buf1 & buf1)
{
auto r = geneval_sequence<InteriorPRG, ExteriorPRG>(std::move(x0),
std::move(x1), begin, end, std::move(rng), std::move(y));
detail::geneval_fill_buffers(r, buf0, buf1);
return r;
}
/// @}
} // namespace dpf
#endif // LIBDPF_INCLUDE_DPF_GENEVAL_HPP__