/// @file grotto/lut_union.hpp /// @brief One comparison for several piecewise LUTs. /// @details The breakpoints of every LUT are shifted by the public `eta` and /// refined with the same carry cuts as offset Horner. Their union is /// one sorted knot vector. One comparison, payload /// `1, center, …, center^d` at the maximum degree, then one /// prefix-parity walk of that union. Each LUT piece is a circular /// span of those union knots (one polynomial, one public `kappa`), /// so the functions are independent dots of the same segment table. /// /// The geneval schedule is one full-depth `fss_cmp`. Its round count /// is the input bitlength. It does not grow with the number of LUTs /// or the number of union knots. Prefix parity stays local. /// @copyright Copyright (c) 2019-2026 Ryan Henry and [others](@ref authors) /// @license Released under a GNU General Public v2.0 (GPLv2) license. #ifndef LIBDPF_INCLUDE_GROTTO_LUT_UNION_HPP__ #define LIBDPF_INCLUDE_GROTTO_LUT_UNION_HPP__ #include "hedley/hedley.h" #include "dpf/compose.hpp" #include "grotto/constant_lut.hpp" #include "grotto/easy_lut.hpp" #include "grotto/offset_poly.hpp" #include #include #include #include #include #include #include namespace grotto { /// @brief Public piecewise polynomial. `knots` are strictly increasing left /// endpoints. `coeff[i][k]` is the degree-`k` coefficient of that piece /// in `Z/2^64`. Every row of one LUT has the same length. /// @tparam InputT input domain type template struct piecewise_lut { static_assert(std::is_integral_v, "lut union domain must be an integer group"); std::vector knots; std::vector> coeff; }; /// @brief One refined piece of one LUT, as a circular span of the union. /// @details `coeff` is already the binomial shift by `kappa`. The dot with /// segment shares of `center^m` is the piece's contribution. /// Indices run over `lut_union_plan::knots`. When `begin <= end` the /// span is `[begin, end)`. When `begin > end` it wraps: /// `[begin, knots) ∪ [0, end)`. /// @tparam InputT input domain type template struct lut_span { std::size_t begin = 0; std::size_t end = 0; std::int64_t kappa = 0; std::vector coeff; }; /// @brief Union of several LUTs at one public `eta`, plus the geneval shape. /// @tparam InputT input domain type template struct lut_union_plan { static_assert(std::is_integral_v, "lut union domain must be an integer group"); /// @brief One comparison, however many LUTs and knots are in the union. static constexpr std::size_t comparisons = 1; /// @brief One memoized prefix-parity walk of `knots`. Local: no rounds. static constexpr std::size_t prefix_walks = 1; /// @brief Public offset the knots were shifted by. InputT eta{}; /// @brief Maximum degree across the LUTs. The comparison payload has `lanes` words. std::size_t degree = 0; /// @brief `degree + 1`. std::size_t lanes = 0; /// @brief Geneval / comparison depth: bitlength of `InputT`. One correction per level. std::size_t depth = 0; /// @brief Union of the refined, `eta`-shifted knots. Sorted, unique. std::vector knots; /// @brief `funcs[t]` is LUT `t`, in the order passed to `make_lut_union_plan`. std::vector>> funcs; /// @brief Number of union breakpoints the prefix walk visits. HEDLEY_NO_THROW std::size_t endpoints() const noexcept { return knots.size(); } /// @brief Interactive geneval rounds: one correction word per input bit. HEDLEY_NO_THROW std::size_t geneval_rounds() const noexcept { return depth; } }; /// @brief Comparison slot for `schedule_lut_union`. /// @details One AES block, or the power-vector width when that is wider. /// @param lanes payload lanes (`degree + 1`) /// @return Slot width in bytes /// \complexity `Θ(1)`. inline std::size_t lut_union_slot_bytes(std::size_t lanes) { const std::size_t value = lanes * sizeof(std::uint64_t); return value < 16u ? 16u : value; } /// @brief Exact `easy_lut` as a piecewise program. /// @details A denominator other than 1 is a rounding division. That is not a /// dot in `Z/2^64`, so those tables are rejected. Trailing powers that /// are zero on every piece are dropped. /// @tparam Raw signed raw word /// @param lut the cleartext table /// @return Piecewise program on the same bounds /// @throws std::invalid_argument if a denominator is not 1, or the table is empty /// \complexity One pass over the pieces. `Θ(P)`. /// @see grotto::easy_lut template piecewise_lut piecewise_from_easy(const easy_lut & lut) { if (lut.bounds.empty() || lut.bounds.size() != lut.c0.size() || lut.c0.size() != lut.c1.size() || lut.c1.size() != lut.c2.size() || lut.c2.size() != lut.den.size()) throw std::invalid_argument("lut union: easy lut pieces are incomplete"); std::size_t width = 1; for (std::size_t i = 0; i < lut.den.size(); ++i) { if (lut.den[i] != 1) throw std::invalid_argument( "lut union: easy lut denominator is not an exact Z/2^64 dot"); if (lut.c1[i] != 0) width = std::max(width, 2); if (lut.c2[i] != 0) width = std::max(width, 3); } piecewise_lut out; out.knots = lut.bounds; out.coeff.resize(lut.bounds.size()); for (std::size_t i = 0; i < lut.bounds.size(); ++i) { const uint64_t row[3] = { static_cast(lut.c0[i]), static_cast(lut.c1[i]), static_cast(lut.c2[i])}; out.coeff[i].assign(row, row + width); } return out; } /// @brief Degree-0 `constant_lut` as a piecewise program. /// @tparam Raw signed raw word /// @param lut the cleartext table /// @return One constant coefficient per bound /// @throws std::invalid_argument if the table is empty or mis-sized /// \complexity One pass over the pieces. `Θ(P)`. /// @see grotto::constant_lut template piecewise_lut piecewise_from_constant(const constant_lut & lut) { if (lut.bounds.empty() || lut.bounds.size() != lut.values.size()) throw std::invalid_argument("lut union: constant lut pieces are incomplete"); piecewise_lut out; out.knots = lut.bounds; out.coeff.resize(lut.values.size()); for (std::size_t i = 0; i < lut.values.size(); ++i) out.coeff[i] = {static_cast(lut.values[i])}; return out; } namespace lut_union_detail { template std::size_t knot_index(const std::vector & knots, InputT knot) { const auto it = std::lower_bound(knots.begin(), knots.end(), knot); if (it == knots.end() || *it != knot) throw std::logic_error("lut union: refined knot missing from the union"); return static_cast(it - knots.begin()); } template void mark_span(std::vector & seen, const lut_span & span, std::size_t n) { const auto mark = [&](std::size_t j) { if (j >= n || seen[j] != 0) throw std::logic_error("lut union: piece spans overlap or leave the union"); seen[j] = 1; }; if (span.begin <= span.end) { for (std::size_t j = span.begin; j < span.end; ++j) mark(j); } else { for (std::size_t j = span.begin; j < n; ++j) mark(j); for (std::size_t j = 0; j < span.end; ++j) mark(j); } } template uint64_t dot_span(std::size_t n, std::size_t begin, std::size_t end, const std::vector & coeff, At && at) { uint64_t value = 0; const auto visit = [&](std::size_t j) { for (std::size_t m = 0; m < coeff.size(); ++m) value += at(j, m) * coeff[m]; }; if (begin <= end) { for (std::size_t j = begin; j < end; ++j) visit(j); } else { for (std::size_t j = begin; j < n; ++j) visit(j); for (std::size_t j = 0; j < end; ++j) visit(j); } return value; } template std::vector dot_plan(const lut_union_plan & plan, const Table & table) { const std::size_t n = plan.knots.size(); std::vector out(plan.funcs.size(), 0); for (std::size_t f = 0; f < plan.funcs.size(); ++f) { for (const auto & span : plan.funcs[f]) { if (span.coeff.size() > plan.lanes) throw std::invalid_argument("lut union: piece wider than the plan"); out[f] += dot_span(n, span.begin, span.end, span.coeff, [&](std::size_t j, std::size_t m) { return table[j][m]; }); } } return out; } } // namespace lut_union_detail /// @brief Union the refined breakpoints and record each LUT's spans. /// @tparam InputT input domain type /// @param luts piecewise programs on the same domain /// @param eta public offset `eta = x - r` /// @return Plan whose geneval depth is `bitlength(InputT)` and whose /// comparison count is 1 /// @throws std::invalid_argument if `luts` is empty, a degree exceeds 16, or a /// LUT's knots and coefficient rows disagree /// \complexity Each LUT is prepared once (`Θ(P log P)` per LUT, including the /// carry cuts). The union sort is `Θ(U log U)` for `U` the total refined /// knots. Building the spans is `Θ(U)`. No keys. /// \rounds None. /// \communication None. /// @see grotto::lut_union_eval /// @see grotto::schedule_lut_union template lut_union_plan make_lut_union_plan( const std::vector> & luts, InputT eta) { if (luts.empty()) throw std::invalid_argument("lut union: no luts"); using prepared = offset_poly_detail::prepared; std::vector> refined; refined.reserve(luts.size()); std::size_t degree = 0; std::vector knots; for (const auto & lut : luts) { if (lut.coeff.empty() || lut.coeff.front().empty()) throw std::invalid_argument("lut union: coefficient row is empty"); const std::size_t width = lut.coeff.front().size(); if (width - 1 > offset_poly_max_degree) throw std::invalid_argument("lut union: degree exceeds 16"); degree = std::max(degree, width - 1); auto rows = offset_poly_detail::prepare(lut.knots, lut.coeff, eta); for (const auto & row : rows) knots.push_back(row.knot); refined.push_back(std::move(rows)); } std::sort(knots.begin(), knots.end()); knots.erase(std::unique(knots.begin(), knots.end()), knots.end()); lut_union_plan plan; plan.eta = eta; plan.degree = degree; plan.lanes = degree + 1; plan.depth = dpf::utils::bitlength_of_v; plan.knots = std::move(knots); plan.funcs.resize(refined.size()); const std::size_t n = plan.knots.size(); for (std::size_t f = 0; f < refined.size(); ++f) { const auto & rows = refined[f]; std::vector seen(n, 0); plan.funcs[f].reserve(rows.size()); if (rows.size() == 1) { lut_span span; span.begin = 0; span.end = n; span.kappa = rows[0].kappa; span.coeff = offset_poly_detail::binomial_shift( rows[0].coeff, static_cast(rows[0].kappa)); lut_union_detail::mark_span(seen, span, n); plan.funcs[f].push_back(std::move(span)); } else { for (std::size_t i = 0; i < rows.size(); ++i) { lut_span span; span.begin = lut_union_detail::knot_index(plan.knots, rows[i].knot); const std::size_t next = (i + 1 == rows.size()) ? 0 : i + 1; span.end = lut_union_detail::knot_index(plan.knots, rows[next].knot); span.kappa = rows[i].kappa; span.coeff = offset_poly_detail::binomial_shift( rows[i].coeff, static_cast(rows[i].kappa)); lut_union_detail::mark_span(seen, span, n); plan.funcs[f].push_back(std::move(span)); } } for (unsigned char bit : seen) { if (bit == 0) throw std::logic_error("lut union: a union knot is outside every piece"); } } return plan; } /// @brief Each LUT's share, from one segment walk of the union. /// @tparam Party party index, `0` or `1` /// @tparam InputT input domain type /// @param mat one comparison whose degree is at least `plan.degree` /// @param plan the union plan /// @param tokens proof token folded by the segment walk, or null /// @return `out[t]` is party `Party`'s share of LUT `t` /// @throws std::invalid_argument if the key is narrower than the plan /// \complexity One `lane_segments` walk of the union (`prefix_walks == 1`), /// then a dot per LUT piece. The walk is `signed` prefix parity over `U` /// endpoints of a `lanes`-wide payload, reusing the common prefix. /// Arithmetic after the walk is `Θ(Σ pieces · degree)`. /// \rounds None. `eta` is already in the plan. /// \communication None. /// \preprocessing None created here. Uses `make_offset_poly_keys` at `plan.degree`. /// @see grotto::offset_poly_eval /// @see grotto::geneval_lut_union template std::vector lut_union_eval( const offset_poly_keys & mat, const lut_union_plan & plan, dpf::proof_token * tokens = nullptr) { static_assert(Party < 2, "lut union party is 0 or 1"); using namespace offset_horner_detail; if (mat.degree < plan.degree) throw std::invalid_argument("lut union: key degree is below the union"); const std::size_t lanes = mat.verifiable ? mat.keys_v.index() : mat.keys.index(); if (lanes != mat.degree + 1) throw std::invalid_argument("lut union: comparison payload width differs from the degree"); dpf::proof_token * pi = (mat.verifiable && tokens != nullptr) ? &tokens[0] : nullptr; const auto table = mat.verifiable ? lane_segments(mat.keys_v, plan.knots, mat.wrap_share, pi) : lane_segments(mat.keys, plan.knots, mat.wrap_share, nullptr); if (mat.verifiable) replicate_proof(tokens, mat.degree + 1); return lut_union_detail::dot_plan(plan, table); } /// @brief Both parties' LUT shares from one Doerner–Shelat comparison. /// @tparam InputT input domain type template struct geneval_lut_union_result { /// @brief Public offset copied from the plan. InputT eta{}; std::vector value0; std::vector value1; }; namespace lut_union_detail { template geneval_lut_union_result geneval_from_keys( const Keys & keys, const lut_union_plan & plan, const uint64_t * payload) { std::array wrap0{}; std::array wrap1{}; for (std::size_t m = 0; m < N; ++m) { const uint64_t blind = dpf::uniform_sample(); wrap0[m] = blind; wrap1[m] = payload[m] - blind; } const auto seg0 = offset_horner_detail::segments_lanes( keys.first, plan.knots, wrap0, nullptr); const auto seg1 = offset_horner_detail::segments_lanes( keys.second, plan.knots, wrap1, nullptr); const std::size_t pieces = plan.knots.size(); std::vector> table0(pieces, std::vector(N)); std::vector> table1(pieces, std::vector(N)); for (std::size_t m = 0; m < N; ++m) { for (std::size_t j = 0; j < pieces; ++j) { table0[j][m] = seg0[m][j]; table1[j][m] = seg1[m][j]; } } geneval_lut_union_result out; out.eta = plan.eta; out.value0 = dot_plan(plan, table0); out.value1 = dot_plan(plan, table1); return out; } template geneval_lut_union_result geneval_lut_union_n( bool arith, InputT center0, InputT center1, const lut_union_plan & plan, const uint64_t * payload, Rng rng) { dpf::vec beta; for (std::size_t m = 0; m < N; ++m) beta[m] = payload[m]; if (arith) { auto keys = dpf::make_dpf_doerner_shelat(dpf::arith_input, center0, center1, std::move(rng), dpf::gt(beta), dpf::verifiable{}); return geneval_from_keys(keys, plan, payload); } auto keys = dpf::make_dpf_doerner_shelat(center0, center1, std::move(rng), dpf::gt(beta), dpf::verifiable{}); return geneval_from_keys(keys, plan, payload); } template geneval_lut_union_result geneval_dispatch( bool arith, InputT center0, InputT center1, const lut_union_plan & plan, const uint64_t * payload, Rng & rng) { if (plan.lanes == N) return geneval_lut_union_n(arith, center0, center1, plan, payload, std::move(rng)); if constexpr (N > 1) return geneval_dispatch(arith, center0, center1, plan, payload, rng); throw std::invalid_argument("lut union: lane count out of range"); } template geneval_lut_union_result geneval_lut_union_at( bool arith, InputT center0, InputT center1, InputT center, const lut_union_plan & plan, Rng rng) { if (plan.lanes == 0 || plan.lanes > offset_horner_detail::lane_key_max) throw std::invalid_argument("lut union: lane count out of range"); if (plan.knots.empty()) throw std::invalid_argument("lut union: plan has no knots"); std::vector payload(plan.lanes, 1); const uint64_t base = offset_horner_detail::lift(center); for (std::size_t m = 1; m < plan.lanes; ++m) payload[m] = payload[m - 1] * base; return geneval_dispatch( arith, center0, center1, plan, payload.data(), rng); } } // namespace lut_union_detail /// @brief Geneval of every LUT. The center is XOR-shared as in `geneval_point`. /// @details One Doerner–Shelat comparison of `plan.lanes` words, then the same /// local span dots as `lut_union_eval`. Further LUTs do not add a /// comparison or a prefix walk. /// @tparam InputT input domain type /// @tparam Rng Doerner–Shelat randomness /// @param center0 party 0 share of the center /// @param center1 party 1 share of the center /// @param plan the union plan /// @param rng the Doerner–Shelat randomness tapes /// @return Both parties' shares of every LUT /// \complexity One Doerner–Shelat generation of a `plan.lanes` comparison /// (`degree ≤ 16`), then one prefix-parity walk of the `U` union knots. /// The dots are `Θ(Σ pieces · degree)`. /// @note Rounds of that comparison are `plan.geneval_rounds()` (`bitlength` of /// the input). They are not multiplied by the LUT count or by `U`. /// \preprocessing The randomness object the caller passes (`Rng`). This function /// also samples one `uint64_t` blind per power. /// @see grotto::geneval_offset_horner /// @see grotto::schedule_lut_union template geneval_lut_union_result geneval_lut_union( InputT center0, InputT center1, const lut_union_plan & plan, Rng rng) { const InputT center = geneval_offset_horner_center(center0, center1); return lut_union_detail::geneval_lut_union_at( false, center0, center1, center, plan, std::move(rng)); } /// @brief Same as `geneval_lut_union`, with the library's urandom pad tape. /// \complexity Same as the overload that takes `Rng`. /// \preprocessing Samples a `dpf::ds_randomness` tape from `uniform_sample`. /// @see grotto::geneval_lut_union template geneval_lut_union_result geneval_lut_union( InputT center0, InputT center1, const lut_union_plan & plan) { using block = typename dpf::prg::aes128::block_type; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") dpf::ds_randomness rng{ dpf::uniform_sample, {}}; HEDLEY_PRAGMA(GCC diagnostic pop) return geneval_lut_union(center0, center1, plan, std::move(rng)); } /// @brief Geneval when `center0 + center1` is the center in the input group. /// \complexity Same as the XOR-share overload: one comparison, one prefix walk. /// @see grotto::geneval_lut_union template geneval_lut_union_result geneval_lut_union( dpf::arith_input_t, InputT center0, InputT center1, const lut_union_plan & plan, Rng rng) { const InputT center = offset_horner_group_add(center0, center1); return lut_union_detail::geneval_lut_union_at( true, center0, center1, center, plan, std::move(rng)); } /// @brief Additive-share geneval with the library's urandom pad tape. /// \complexity Same as the overload that takes `Rng`. /// @see grotto::geneval_lut_union template geneval_lut_union_result geneval_lut_union( dpf::arith_input_t, InputT center0, InputT center1, const lut_union_plan & plan) { using block = typename dpf::prg::aes128::block_type; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") dpf::ds_randomness rng{ dpf::uniform_sample, {}}; HEDLEY_PRAGMA(GCC diagnostic pop) return geneval_lut_union(dpf::arith_input, center0, center1, plan, std::move(rng)); } /// @brief Record one full-depth comparison on an existing seed. /// @details Does not walk the union knots and does not emit a node per LUT. /// Prefix parity of `plan.knots` is local after this comparison. /// @tparam InputT input domain type /// @param composer the schedule being built /// @param seed FSS seed already recorded on `composer` /// @param plan the union plan /// @return The comparison leaf (`domain::a`) /// @throws std::invalid_argument if the plan depth or lane count is zero /// \complexity `depth` expand/step pairs. `depth` is `bitlength(InputT)`. /// Extra composer nodes are `Θ(depth)`, independent of `plan.funcs.size()` /// and of `plan.endpoints()`. /// \rounds `plan.geneval_rounds()` once the composer is scheduled. /// \communication One correction slot per level, `lut_union_slot_bytes(plan.lanes)` wide. /// @see grotto::make_lut_union_plan template dpf::protocol::node schedule_lut_union( dpf::protocol::composer & composer, dpf::protocol::node seed, const lut_union_plan & plan) { if (plan.depth == 0 || plan.lanes == 0) throw std::invalid_argument("lut union: plan has no comparison"); return composer.fss_cmp(seed, plan.depth, lut_union_slot_bytes(plan.lanes)); } /// @brief Record a fresh seed and one full-depth comparison for `plan`. /// \complexity One input node plus `schedule_lut_union` on that seed. /// \rounds `plan.geneval_rounds()`. /// @see grotto::schedule_lut_union template dpf::protocol::node schedule_lut_union( dpf::protocol::composer & composer, const lut_union_plan & plan) { auto seed = composer.input(dpf::protocol::domain::fss, 16); return schedule_lut_union(composer, seed, plan); } } // namespace grotto #endif // LIBDPF_INCLUDE_GROTTO_LUT_UNION_HPP__