Record Grotto half-ulp tables and comparison geneval, and factor shared beaver terms before the quotient.
Horner and window evaluation need those tables in the tree. Comparison geneval opens the same value words as a Doerner–Shelat key. A factor common to every polynomial term is multiplied first so that preprocessing stays smaller. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
3f10e05176
commit
875f09fec1
14 changed files with 42668 additions and 184 deletions
|
|
@ -14,9 +14,12 @@
|
|||
/// A polynomial is a sum of monomials in several wires.
|
||||
/// `2 + 3*x + 4*y + 5*x*y + 6*pow(x, 2) + pow(x, 2)*y + x*y*z`
|
||||
/// is one round. `λ_x²` is stored once whether it appears as `x²`,
|
||||
/// inside `x² y`, or in a second polynomial. `sgn * (x*y + pow(x, 2))`
|
||||
/// is the sign-corrected form and reuses those powers. A product that
|
||||
/// uses an output of an earlier polynomial is a later round.
|
||||
/// inside `x² y`, or in a second polynomial. Wires that occur with the
|
||||
/// same exponents in every term, as in `a3*(x*z)^3 + a2*(x*z)^2 + a1*(x*z) + a0`,
|
||||
/// are multiplied first and the univariate polynomial is a later round.
|
||||
/// A factor shared by every term, such as a sign or a piecewise scale,
|
||||
/// is applied after the quotient when that uses fewer preprocessing
|
||||
/// values. A lone secret summand is added from its value share.
|
||||
///
|
||||
/// Doerner–Shelat's per-level AND is a `bit_mul` of a fresh bit and
|
||||
/// a fresh block. A wildcard leaf is a `scale` of one scalar by each
|
||||
|
|
@ -720,9 +723,23 @@ public:
|
|||
}
|
||||
else if (g.kind == gate_kind::poly)
|
||||
{
|
||||
for (const auto & term : g.terms)
|
||||
for (auto id : term.factors)
|
||||
for (const auto & step : g.steps)
|
||||
{
|
||||
if (step.value_wire >= 0)
|
||||
{
|
||||
const auto id = static_cast<std::uint32_t>(step.value_wire);
|
||||
if (!wires_[id].value_ready)
|
||||
throw std::logic_error("beaver wire is not ready to open");
|
||||
continue;
|
||||
}
|
||||
for (auto [id, exp] : step.delta)
|
||||
{
|
||||
(void)exp;
|
||||
ensure_delta(id);
|
||||
}
|
||||
if (step.mask_wire >= 0)
|
||||
ensure_delta(static_cast<std::uint32_t>(step.mask_wire));
|
||||
}
|
||||
val = eval_poly(g);
|
||||
}
|
||||
else
|
||||
|
|
@ -858,6 +875,7 @@ private:
|
|||
Ring scale{};
|
||||
int bundle = -1;
|
||||
int mask_wire = -1;
|
||||
int value_wire = -1;
|
||||
bool public_only = false;
|
||||
};
|
||||
|
||||
|
|
@ -1176,67 +1194,108 @@ private:
|
|||
for (std::uint32_t id = 0; id < wires_.size(); ++id)
|
||||
already[id] = needs_blind(id) ? 1 : 0;
|
||||
auto saved = bundles_;
|
||||
std::vector<std::vector<poly_step>> compiled;
|
||||
compiled.reserve(pieces.size());
|
||||
for (const auto & piece : pieces)
|
||||
(void)compile_poly(piece);
|
||||
std::size_t added = bundles_.size() - saved.size();
|
||||
bundles_ = std::move(saved);
|
||||
compiled.push_back(compile_poly(piece));
|
||||
const auto bundle_base = saved.size();
|
||||
std::size_t added = bundles_.size() - bundle_base;
|
||||
std::map<std::uint32_t, char> blinds;
|
||||
for (const auto & piece : pieces)
|
||||
auto note = [&](std::uint32_t id) {
|
||||
if (id < already.size() && already[id] != 0)
|
||||
return;
|
||||
blinds[id] = 1;
|
||||
};
|
||||
for (std::size_t i = bundle_base; i < bundles_.size(); ++i)
|
||||
{
|
||||
for (const auto & term : piece)
|
||||
for (const auto & part : bundles_[i].parts)
|
||||
{
|
||||
for (auto id : term.factors)
|
||||
for (auto [wid, exp] : part.lam)
|
||||
{
|
||||
if (id >= already.size() || already[id] == 0)
|
||||
blinds[id] = 1;
|
||||
(void)exp;
|
||||
note(wid);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto & steps : compiled)
|
||||
{
|
||||
for (const auto & step : steps)
|
||||
{
|
||||
if (step.mask_wire >= 0)
|
||||
note(static_cast<std::uint32_t>(step.mask_wire));
|
||||
for (auto [wid, exp] : step.delta)
|
||||
{
|
||||
(void)exp;
|
||||
note(wid);
|
||||
}
|
||||
}
|
||||
}
|
||||
bundles_ = std::move(saved);
|
||||
return added + blinds.size();
|
||||
}
|
||||
|
||||
static std::vector<poly_term> drop_one(std::vector<poly_term> terms, std::uint32_t wire_id)
|
||||
{
|
||||
for (auto & term : terms)
|
||||
{
|
||||
auto it = std::find(term.factors.begin(), term.factors.end(), wire_id);
|
||||
if (it != term.factors.end())
|
||||
term.factors.erase(it);
|
||||
}
|
||||
return terms;
|
||||
}
|
||||
|
||||
static bool wire_in_every(const std::vector<poly_term> & terms, std::uint32_t wire_id)
|
||||
{
|
||||
for (const auto & term : terms)
|
||||
{
|
||||
if (factor_exp(term, wire_id) == 0)
|
||||
return false;
|
||||
}
|
||||
return !terms.empty();
|
||||
}
|
||||
|
||||
wire schedule_terms(std::vector<poly_term> terms)
|
||||
{
|
||||
if (terms.size() < 2)
|
||||
return emit_terms(std::move(terms));
|
||||
|
||||
std::vector<std::vector<poly_term>> best{terms};
|
||||
std::size_t best_cost = estimate_pieces(best);
|
||||
|
||||
std::map<std::uint32_t, char> seen;
|
||||
for (const auto & term : terms)
|
||||
for (auto id : term.factors)
|
||||
seen[id] = 1;
|
||||
for (auto [wire_id, _] : seen)
|
||||
{
|
||||
bool common = true;
|
||||
for (const auto & term : terms)
|
||||
{
|
||||
if (factor_exp(term, wire_id) == 0)
|
||||
common = false;
|
||||
}
|
||||
if (!common)
|
||||
continue;
|
||||
std::vector<poly_term> quot = terms;
|
||||
for (auto & term : quot)
|
||||
{
|
||||
auto it = std::find(term.factors.begin(), term.factors.end(), wire_id);
|
||||
if (it != term.factors.end())
|
||||
term.factors.erase(it);
|
||||
}
|
||||
const std::uint32_t mid = static_cast<std::uint32_t>(wires_.size());
|
||||
poly_term mul;
|
||||
mul.coeff = traits::one();
|
||||
mul.factors = {mid, wire_id};
|
||||
std::vector<std::vector<poly_term>> seq{std::move(quot), {std::move(mul)}};
|
||||
std::size_t cost = estimate_pieces(seq);
|
||||
auto consider = [&](std::vector<std::vector<poly_term>> seq) {
|
||||
const std::size_t cost = estimate_pieces(seq);
|
||||
if (cost < best_cost)
|
||||
{
|
||||
best = std::move(seq);
|
||||
best_cost = cost;
|
||||
}
|
||||
};
|
||||
|
||||
std::map<std::uint32_t, char> seen;
|
||||
for (const auto & term : terms)
|
||||
for (auto id : term.factors)
|
||||
seen[id] = 1;
|
||||
|
||||
std::vector<std::uint32_t> common;
|
||||
for (auto [wire_id, present] : seen)
|
||||
{
|
||||
(void)present;
|
||||
if (wire_in_every(terms, wire_id))
|
||||
common.push_back(wire_id);
|
||||
}
|
||||
const auto fresh = static_cast<std::uint32_t>(wires_.size());
|
||||
for (auto wire_id : common)
|
||||
{
|
||||
poly_term mul;
|
||||
mul.coeff = traits::one();
|
||||
mul.factors = {fresh, wire_id};
|
||||
consider({drop_one(terms, wire_id), {std::move(mul)}});
|
||||
}
|
||||
|
||||
std::map<std::vector<std::uint8_t>, std::vector<std::uint32_t>> clusters;
|
||||
for (auto [wire_id, _] : seen)
|
||||
for (auto [wire_id, present] : seen)
|
||||
{
|
||||
(void)present;
|
||||
std::vector<std::uint8_t> shape;
|
||||
shape.reserve(terms.size());
|
||||
bool any = false;
|
||||
|
|
@ -1247,13 +1306,13 @@ private:
|
|||
any = any || e != 0;
|
||||
}
|
||||
if (any)
|
||||
clusters[shape].push_back(wire_id);
|
||||
clusters[std::move(shape)].push_back(wire_id);
|
||||
}
|
||||
for (auto & [shape, group] : clusters)
|
||||
{
|
||||
(void)shape;
|
||||
if (group.size() < 2)
|
||||
continue;
|
||||
const std::uint32_t mid = static_cast<std::uint32_t>(wires_.size());
|
||||
poly_term prod;
|
||||
prod.coeff = traits::one();
|
||||
prod.factors = group;
|
||||
|
|
@ -1270,26 +1329,28 @@ private:
|
|||
next.factors.push_back(f);
|
||||
}
|
||||
for (std::uint8_t i = 0; i < e; ++i)
|
||||
next.factors.push_back(mid);
|
||||
next.factors.push_back(fresh);
|
||||
rewritten.push_back(std::move(next));
|
||||
}
|
||||
std::vector<std::vector<poly_term>> seq{{std::move(prod)}, std::move(rewritten)};
|
||||
std::size_t cost = estimate_pieces(seq);
|
||||
if (cost < best_cost)
|
||||
consider({{prod}, rewritten});
|
||||
|
||||
std::map<std::uint32_t, char> rewritten_seen;
|
||||
for (const auto & term : rewritten)
|
||||
for (auto id : term.factors)
|
||||
rewritten_seen[id] = 1;
|
||||
const auto later = fresh + 1;
|
||||
for (auto [wire_id, present] : rewritten_seen)
|
||||
{
|
||||
best = std::move(seq);
|
||||
best_cost = cost;
|
||||
(void)present;
|
||||
if (!wire_in_every(rewritten, wire_id))
|
||||
continue;
|
||||
poly_term mul;
|
||||
mul.coeff = traits::one();
|
||||
mul.factors = {later, wire_id};
|
||||
consider({{prod}, drop_one(rewritten, wire_id), {std::move(mul)}});
|
||||
}
|
||||
}
|
||||
|
||||
if (best.size() != 1 && terms.size() == 1 && terms[0].factors.size() <= 4)
|
||||
{
|
||||
std::fprintf(stderr, "split factors=%zu pieces=%zu cost=%zu flat_factors=",
|
||||
terms[0].factors.size(), best.size(), best_cost);
|
||||
for (auto f : terms[0].factors)
|
||||
std::fprintf(stderr, "%u ", f);
|
||||
std::fprintf(stderr, "\n");
|
||||
}
|
||||
wire last{};
|
||||
for (auto & piece : best)
|
||||
last = emit_terms(std::move(piece));
|
||||
|
|
@ -1352,10 +1413,7 @@ private:
|
|||
if (b.parts[0].lam == key)
|
||||
return b.share;
|
||||
}
|
||||
throw std::logic_error(
|
||||
"beaver monomial was not prepared (key " + std::to_string(key.size())
|
||||
+ " bundles " + std::to_string(bundles_.size())
|
||||
+ " gates " + std::to_string(gates_.size()) + ")");
|
||||
throw std::logic_error("beaver monomial was not prepared");
|
||||
}
|
||||
|
||||
unsigned exponent_of(const exp_list & key, std::uint32_t id) const
|
||||
|
|
@ -1512,8 +1570,17 @@ private:
|
|||
std::map<exp_list, Ring> lams;
|
||||
};
|
||||
std::map<exp_list, bucket> buckets;
|
||||
std::vector<poly_step> steps;
|
||||
for (const auto & term : terms)
|
||||
{
|
||||
if (term.factors.size() == 1)
|
||||
{
|
||||
poly_step step;
|
||||
step.scale = term.coeff;
|
||||
step.value_wire = static_cast<int>(term.factors[0]);
|
||||
steps.push_back(std::move(step));
|
||||
continue;
|
||||
}
|
||||
auto groups = group_exponents(term.factors);
|
||||
(void)expansion_size(groups);
|
||||
for_each_term(groups, [&](const exp_list & key) {
|
||||
|
|
@ -1544,7 +1611,6 @@ private:
|
|||
});
|
||||
}
|
||||
|
||||
std::vector<poly_step> steps;
|
||||
for (auto & [delta, slot] : buckets)
|
||||
{
|
||||
if (!(slot.pub == traits::zero()))
|
||||
|
|
@ -1627,6 +1693,13 @@ private:
|
|||
Ring s1 = traits::zero();
|
||||
for (const auto & step : g.steps)
|
||||
{
|
||||
if (step.value_wire >= 0)
|
||||
{
|
||||
const auto & val = wires_[static_cast<std::size_t>(step.value_wire)].value;
|
||||
s0 = traits::add(s0, traits::mul(step.scale, val.p0));
|
||||
s1 = traits::add(s1, traits::mul(step.scale, val.p1));
|
||||
continue;
|
||||
}
|
||||
Ring pub = pow_delta(step.delta);
|
||||
if (step.public_only)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -371,16 +371,19 @@ struct ds_gen_state
|
|||
/// When `cmp` is non-null and active for `level`, also opens `value_cw` via
|
||||
/// the protocol (no second PRG expand outside).
|
||||
template <typename InteriorPRG, typename CwProtocol, typename NodeT,
|
||||
typename InputT, typename AdviceT>
|
||||
typename InputT, typename MaskT, typename AdviceT>
|
||||
void ds_advance_level(ds_gen_state<NodeT> & st, InputT x0, InputT x1,
|
||||
InputT mask, std::size_t level, CwProtocol & proto, NodeT & cw_out,
|
||||
MaskT mask, std::size_t level, CwProtocol & proto, NodeT & cw_out,
|
||||
AdviceT & advice_out, uint64_t * value_cw_out = nullptr,
|
||||
ds_cmp_gen_state * cmp = nullptr)
|
||||
{
|
||||
// Integral bridge so bit extraction works for `keyword` / `modint` /
|
||||
// signed / bitstring the same way dealer gen does via `mask & x`.
|
||||
// `msb_mask` is the unsigned bit pattern; a signed input must not be
|
||||
// required to have that same type.
|
||||
constexpr auto to_int = utils::to_integral_type<InputT>{};
|
||||
const auto mi = to_int(mask);
|
||||
constexpr auto to_mask = utils::to_integral_type<MaskT>{};
|
||||
const auto mi = to_mask(mask);
|
||||
const uint8_t bit0 = static_cast<uint8_t>(!!(mi & to_int(x0)));
|
||||
const uint8_t bit1 = static_cast<uint8_t>(!!(mi & to_int(x1)));
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,13 @@
|
|||
/// a public query. It samples a random target, runs geneval there,
|
||||
/// and shifts the query by `target - x`, which is what
|
||||
/// `offset_x` does after a wildcard key is bound to `x`.
|
||||
///
|
||||
/// `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`.
|
||||
/// @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.
|
||||
|
|
@ -37,6 +44,7 @@
|
|||
|
||||
#include "dpf/aligned_allocator.hpp"
|
||||
#include "dpf/doerner_shelat.hpp"
|
||||
#include "dpf/eval_target.hpp"
|
||||
#include "dpf/leaf_node.hpp"
|
||||
|
||||
namespace dpf
|
||||
|
|
@ -633,6 +641,83 @@ auto geneval_sequence(wildcard_input_t, InputT x0, InputT x1,
|
|||
begin, end, std::move(rng), [] { return dpf::uniform_sample<InputT>(); }, y);
|
||||
}
|
||||
|
||||
/// Opened comparison key material and one prefix share per endpoint.
|
||||
/// `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;
|
||||
std::vector<simde__m128i, aligned_allocator<simde__m128i>> correction_words;
|
||||
std::vector<uint8_t> correction_advice;
|
||||
std::vector<uint64_t> value_cw;
|
||||
uint64_t cw_last = 0;
|
||||
uint64_t addend0 = 0;
|
||||
uint64_t addend1 = 0;
|
||||
uint64_t mask = 0;
|
||||
std::size_t live_levels = 0;
|
||||
};
|
||||
|
||||
/// Doerner–Shelat comparison geneval. `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.
|
||||
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));
|
||||
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);
|
||||
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));
|
||||
out.value_cw[level] = k0.value_cw(level);
|
||||
}
|
||||
for (auto it = begin; it != end; ++it)
|
||||
{
|
||||
out.party0.push_back(eval_point(dpf::cmp, k0, *it).raw());
|
||||
out.party1.push_back(eval_point(dpf::cmp, k1, *it).raw());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/// `gt(beta)` comparison geneval. `if_false` is 0.
|
||||
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));
|
||||
}
|
||||
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_GENEVAL_HPP__
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue