/// @file dpf/incremental.hpp /// @brief Prefix-placed (incremental) DPF outputs via `dpf::at`. /// @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_DPF_INCREMENTAL_HPP__ #define LIBDPF_INCLUDE_DPF_INCREMENTAL_HPP__ #include #include #include #include #include #include #include #include #include #include #include #include "hedley/hedley.h" #include "portable-snippets/exact-int/exact-int.h" #include "dpf/dpf_key.hpp" #include "dpf/doerner_shelat.hpp" #include "dpf/eval_target.hpp" #include "dpf/eval_common.hpp" #include "dpf/utils.hpp" #include "dpf/twiddle.hpp" #include "dpf/wildcard.hpp" #include "dpf/leaf_node.hpp" #include "dpf/leaf_wrapper.hpp" #include "dpf/offset_wrapper.hpp" #include "dpf/path_memoizer.hpp" #include "dpf/output_buffer.hpp" #include "dpf/interval_memoizer.hpp" #include "dpf/subinterval_iterable.hpp" #include "dpf/subsequence_iterable.hpp" #include "dpf/dcf.hpp" #include "dpf/blocked_dcf.hpp" #include "dpf/random.hpp" namespace dpf { // `at_pack` / `at` and the placement / slot-meta machinery now live in // `dpf/placement.hpp` (included via `dpf/dpf_key.hpp`). template inline constexpr bool args_have_cmp_v = (is_cmp_spec_v> || ...); template inline constexpr bool args_have_verifiable_v = (is_verifiable_tag_v> || ...); template inline constexpr bool args_have_extractable_v = (is_extractable_tag_v> || ...); template inline constexpr bool args_have_updatable_v = (is_updatable_tag_v> || ...); /// @brief Out-parameter tag for `make_dpf`: record ring shares of a bit leaf's /// sign. `w0 - w1` is `+1` or `-1`, the sign the papers read off `Gen`'s /// final control bit. A 1-bit leaf lifts to a ring unit by multiplying /// its lifted bit share by this sign; Duoram's flag and Pika's /// early-stop leaf need it without a second (word-payload) tree. /// @details Absent the tag, keygen is unchanged. The tag holds references to /// two caller-owned ints, filled after the key is built. struct unit_sign { int & w0; int & w1; }; template struct is_unit_sign : std::false_type {}; template <> struct is_unit_sign : std::true_type {}; template inline constexpr bool args_have_unit_sign_v = (is_unit_sign>::value || ...); namespace detail { namespace incr { /// @brief Address of the (single) `unit_sign` out-parameter among `args`. template HEDLEY_NO_THROW unit_sign * find_unit_sign(Args & ...args) noexcept { unit_sign * p = nullptr; ((void)([&] { if constexpr (is_unit_sign>::value) p = &args; }()), ...); return p; } /// @brief Drop `unit_sign` args; keep the rest as a forwarding tuple. template HEDLEY_ALWAYS_INLINE decltype(auto) drop_unit_sign(T && t) { if constexpr (is_unit_sign>::value) return std::tuple<>{}; else return std::forward_as_tuple(std::forward(t)); } /// @brief Fill `us` with each party's lifted bit share at `x`. `w0 - w1` is the /// `±1` unit sign. Uses ADL `eval_point` (resolved at instantiation). template void fill_unit_sign(const K0 & k0, const K1 & k1, const InputT & x, unit_sign us) { us.w0 = static_cast(static_cast((*eval_point(k0, x)).raw())); us.w1 = static_cast(static_cast((*eval_point(k1, x)).raw())); } } // namespace incr } // namespace detail template inline constexpr bool args_have_output_mac_v = (is_output_mac_tag_v> || ...); template struct arg_has_bit_payload : std::bool_constant< std::is_same_v>, dpf::bit>> { }; template struct arg_has_bit_payload> || is_eq_spec_v>>> : std::bool_constant::beta_type>, dpf::bit>> { }; template struct arg_has_bit_payload, void> : std::bool_constant<(std::is_same_v, dpf::bit> || ... || std::is_same_v, dpf::bit>)> { }; template struct arg_has_bit_payload> || is_extractable_tag_v> || is_updatable_tag_v> || is_output_mac_tag_v>>> : std::false_type { }; template inline constexpr bool args_have_bit_payload_v = (arg_has_bit_payload>::value || ...); template inline constexpr bool args_have_eq_v = (is_eq_spec_v> || ...); template HEDLEY_NO_THROW constexpr std::size_t forced_cmp_depth_sum() noexcept { return 0; } template HEDLEY_NO_THROW constexpr std::size_t forced_cmp_depth_sum() noexcept { std::size_t m = 0; using A = std::decay_t; if constexpr (is_cmp_spec_v) m = (A::prefix == 0) ? BitLen : A::prefix; return std::max(m, forced_cmp_depth_sum()); } template inline constexpr std::size_t forced_cmp_depth_v = forced_cmp_depth_sum(); template HEDLEY_NO_THROW constexpr std::size_t forced_cmp_out_bits_sum() noexcept { return 0; } template HEDLEY_NO_THROW constexpr std::size_t forced_cmp_out_bits_sum() noexcept { std::size_t m = 0; using A = std::decay_t; if constexpr (is_cmp_spec_v) { using Beta = typename A::beta_type; m = std::is_same_v ? std::size_t{1} : utils::bitlength_of_v; } return std::max(m, forced_cmp_out_bits_sum()); } /// @brief Comparison output group width (bits) forced by any `lt`/`leq`/`gt`/`geq` /// (or `_at`) spec in the pack — 0 when there is no comparison channel. template inline constexpr std::size_t forced_cmp_out_bits_v = forced_cmp_out_bits_sum(); /// @brief True when the (single) comparison spec in the pack carries a wildcard /// payload (`lt(dpf::wildcard, ...)` etc.) — the δ is assigned after /// keygen via `dpf::assign_cmp`. /// @tparam A alignment of the rebound allocator template >> struct arg_is_wild_cmp : std::false_type {}; template struct arg_is_wild_cmp : std::bool_constant< dpf::is_wildcard_v::beta_type>> {}; template inline constexpr bool forced_cmp_wild_v = (arg_is_wild_cmp::value || ...); template >> struct arg_cmp_block : std::integral_constant {}; template struct arg_cmp_block : std::integral_constant::block_width> {}; template inline constexpr std::size_t forced_cmp_block_v = (std::size_t{0} + ... + arg_cmp_block::value); template struct arg_cmp_idcf : std::false_type {}; template struct arg_cmp_idcf>>> : spec_is_incremental> {}; template inline constexpr bool forced_cmp_idcf_v = (false || ... || arg_cmp_idcf::value); template struct spec_has_paint_fn : std::false_type {}; template struct spec_has_paint_fn().fn)>> : std::true_type {}; inline uint64_t paint_fn_adapter(std::size_t matched, uint64_t prefix, bool leaf, const void * ctx) { using fn_type = std::function; return (*static_cast(ctx))(matched, prefix, leaf); } /// @brief Runtime description of one comparison channel peeled from `make_dpf` args. struct dcf_runtime_spec { std::size_t prefix = 0; // 0 => full input bitlength uint64_t beta = 0; // if_true - if_false uint64_t false_value = 0; uint64_t mask = ~0ULL; /// @brief Payload group wider than the masked `uint64_t` ring, or a product /// (`dpf::vec`) / XOR group. `beta_g` is δ and `false_g` is `if_false`. bool custom = false; detail::group_elem beta_g{}; detail::group_elem false_g{}; cmp_kind kind = cmp_kind::lt; bool is_wildcard = false; // payload assigned after keygen (δ unknown now) /// @brief Payload group is `from_seed` / `+` / unary `-`, not the limb ring. bool use_payload_group = false; detail::payload_ops payload{}; alignas(16) unsigned char payload_beta[detail::payload_ops::cap]{}; alignas(16) unsigned char payload_false[detail::payload_ops::cap]{}; std::size_t length_bits = 0; bool incremental = false; std::function paint; }; namespace detail { namespace incr { // `placed`, `slot_meta`, `build_meta`, `build_group_order`, `lane_input`, // `filter_group`, `meta_holder`, and the `out_bits_v` / `lg_opl_v` / // `level_of_v` traits now live in `dpf/placement.hpp`. template auto expand_idpf(idpf_pack, Betas...> pack) { return std::apply([](auto && ...ys) { return std::make_tuple( placed>{ std::forward(ys)}...); }, std::move(pack.values)); } template auto flatten_one(Arg && arg) { using A = std::decay_t; if constexpr (is_cmp_spec_v) { return std::tuple<>{}; } else if constexpr (is_verifiable_tag_v || is_extractable_tag_v || is_updatable_tag_v || is_output_mac_tag_v) { return std::tuple<>{}; } else if constexpr (is_idpf_v) { return expand_idpf(std::forward(arg)); } else if constexpr (is_eq_spec_v) { constexpr auto pref = A::prefix == 0 ? BitLen : A::prefix; static_assert(pref <= BitLen, "eq_at exceeds input bitlength"); using Beta = typename A::beta_type; Beta delta = detail::dcf_impl::sub_beta(arg.if_true, arg.if_false); return std::make_tuple( placed{std::move(delta), arg.if_false}); } else if constexpr (is_at_v) { static_assert(A::prefix <= BitLen, "at exceeds input bitlength"); return std::apply([](auto && ...ys) { return std::make_tuple( placed>{ std::forward(ys)}...); }, std::forward(arg).values); } else { return std::make_tuple(placed{std::forward(arg)}); } } template void collect_cmp_one(dcf_runtime_spec & spec, bool & found, Arg && arg) { using A = std::decay_t; if constexpr (is_cmp_spec_v) { if (found) throw std::invalid_argument( "at most one lt/leq/gt/geq (or *_at) per key"); found = true; if constexpr (A::prefix != 0) spec.prefix = A::prefix; else spec.prefix = 0; spec.kind = A::kind; spec.length_bits = spec_length_bits::value; spec.incremental = spec_is_incremental::value; if constexpr (spec_has_paint_fn::value) spec.paint = arg.fn; using Beta = typename A::beta_type; using Concrete = dpf::concrete_type_t; if constexpr (detail::has_from_seed::value) { spec.use_payload_group = true; spec.payload = detail::make_payload_ops(); if constexpr (dpf::is_wildcard_v) { spec.is_wildcard = true; } else { const Concrete delta = arg.if_true - arg.if_false; std::memcpy(spec.payload_beta, &delta, sizeof(delta)); std::memcpy(spec.payload_false, &arg.if_false, sizeof(arg.if_false)); } } constexpr auto bits = [] { if constexpr (std::is_same_v) return std::size_t{1}; else return utils::bitlength_of_v; }(); spec.mask = detail::dcf_impl::default_mask_for_bits(bits); if constexpr (!detail::has_from_seed::value) { if constexpr (detail::cmp_group_info::custom) { spec.custom = true; const auto layout = detail::group_layout(); if constexpr (dpf::is_wildcard_v) { spec.is_wildcard = true; spec.beta_g = detail::group_zero(layout); spec.false_g = detail::group_zero(layout); } else { spec.beta_g = detail::group_sub( detail::group_from_beta(arg.if_true), detail::group_from_beta(arg.if_false)); spec.false_g = detail::group_from_beta(arg.if_false); } spec.beta = 0; spec.false_value = 0; } else if constexpr (dpf::is_wildcard_v) { // Payload is unknown at keygen: keep δ = if_false = 0 (open the // value CWs for the trivial payload) and record that this key's // comparison must be `assign_cmp`ed before it can be evaluated. spec.is_wildcard = true; spec.beta = 0; spec.false_value = 0; } else { spec.beta = detail::dcf_impl::beta_delta_u64( arg.if_true, arg.if_false, spec.mask); spec.false_value = detail::dcf_impl::beta_to_u64_simple( arg.if_false, spec.mask); } } } } template auto flatten_args_and_cmp(dcf_runtime_spec & spec, bool & has_cmp, Args && ...args) { has_cmp = false; (collect_cmp_one(spec, has_cmp, args), ...); auto placed = std::tuple_cat(flatten_one(std::forward(args))...); if (has_cmp && spec.prefix == 0) spec.prefix = BitLen; return placed; } template auto flatten_args(Args && ...args) { dcf_runtime_spec spec{}; bool has_cmp = false; return flatten_args_and_cmp(spec, has_cmp, std::forward(args)...); } template constexpr bool classic_pack_impl(std::index_sequence) { using first = std::tuple_element_t<0, PlacedTuple>; constexpr auto w0 = out_bits_v; // Wildcards need the incremental leaf/beaver path; classic DS rejects them. constexpr bool any_wild = (dpf::is_wildcard_v< typename std::tuple_element_t::output_type> || ...); if constexpr (any_wild) return false; else return ((std::tuple_element_t::prefix == BitLen) && ...) && ((out_bits_v::output_type> == w0) && ...); } template constexpr bool is_classic_placed() { constexpr auto n = std::tuple_size_v; if constexpr (n == 0) return false; else return classic_pack_impl( std::make_index_sequence{}); } template auto classic_values(PlacedTuple & t, std::index_sequence) { return std::make_tuple(std::move(std::get(t).value)...); } template void assign_tuple_element(A & a, B && b) { a = std::forward(b); } template void zip_install(OutL & out_leaves, OutB & out_beavers, InL && in_leaves, InB && in_beavers, std::index_sequence, std::index_sequence) { (assign_tuple_element(std::get(out_leaves), std::get(in_leaves)), ...); (assign_tuple_element(std::get(out_beavers), std::get(in_beavers)), ...); } template auto concrete_placed_value(Placed & p) { using stored = typename Placed::stored_type; if constexpr (dpf::is_arith_beta_v) { using T = typename Placed::output_type; if constexpr (utils::has_characteristic_two_v) return static_cast(p.value.y0 ^ p.value.y1); else return static_cast(p.value.y0 + p.value.y1); } else { return p.value; } } template constexpr bool placed_has_arith_beta_impl(std::index_sequence) { return (dpf::is_arith_beta_v< typename std::tuple_element_t::stored_type> || ...); } template constexpr bool placed_has_arith_beta() { constexpr auto n = std::tuple_size_v; if constexpr (n == 0) return false; else return placed_has_arith_beta_impl( std::make_index_sequence{}); } template using group_outputs_t = std::tuple< typename std::tuple_element_t::output_type...>; template void overwrite_arith_slot_one(CwProtocol & proto, const SeedT & s0, const SeedT & s1, uint8_t t0, uint8_t t1, std::size_t pos_base, InputT x0, InputT x1, PlacedTuple & placed, Leaves0T & leaves0, Leaves1T & leaves1, std::index_sequence) { using P = std::tuple_element_t; if constexpr (dpf::is_arith_beta_v) { using outs = group_outputs_t; auto & slot = std::get(placed); auto cw = proto.template open_arith_leaf( s0, s1, t0, t1, slot.value.y0, slot.value.y1, pos_base, x0, x1); std::get(leaves0) = cw; std::get(leaves1) = cw; } } template void overwrite_arith_slots(CwProtocol & proto, const SeedT & s0, const SeedT & s1, uint8_t t0, uint8_t t1, std::size_t pos_base, InputT x0, InputT x1, PlacedTuple & placed, Leaves0T & leaves0, Leaves1T & leaves1, std::index_sequence, std::index_sequence) { (overwrite_arith_slot_one(proto, s0, s1, t0, t1, pos_base, x0, x1, placed, leaves0, leaves1, std::index_sequence{}), ...); } template auto call_make_leaves(std::size_t pos_base, InputT lane_x, const SeedT & s0, const SeedT & s1, bool sign, PlacedTuple & placed, std::index_sequence) { return dpf::make_leaves(lane_x, s0, s1, sign, pos_base, concrete_placed_value(std::get(placed))...); } template auto empty_leaves(std::index_sequence) { using node = typename ExteriorPRG::block_type; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") return std::make_tuple(dpf::leaf_node_t::output_type>{}...); HEDLEY_PRAGMA(GCC diagnostic pop) } template auto empty_beavers(std::index_sequence) { using node = typename ExteriorPRG::block_type; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") return std::make_tuple(dpf::beaver< dpf::is_wildcard_v< typename std::tuple_element_t::output_type>, node, concrete_type_t< typename std::tuple_element_t::output_type>>{}...); HEDLEY_PRAGMA(GCC diagnostic pop) } template void gen_group(InputT x, const typename InteriorPRG::block_type & s0, const typename InteriorPRG::block_type & s1, uint8_t t0, uint8_t t1, PlacedTuple & placed, Leaves0T & leaves0, Beavers0T & beavers0, Leaves1T & leaves1, Beavers1T & beavers1, CwProtocol * proto = nullptr, const InputT * x0_share = nullptr, const InputT * x1_share = nullptr) { constexpr std::size_t n = std::tuple_size_v; constexpr std::size_t bitlen = utils::bitlength_of_v; using idxs = filter_group_t; constexpr auto meta = MetaHolder::value; std::size_t prefix = 0, pos_base = 0; for (std::size_t i = 0; i < n; ++i) { if (meta[i].group_id == G) { prefix = meta[i].prefix; pos_base = meta[i].pos_base; break; } } InputT lane_x = lane_input(x, prefix, bitlen); const bool sign0 = static_cast(t0 & 1u); auto built = call_make_leaves(pos_base, lane_x, s0, s1, sign0, placed, idxs{}); constexpr auto nslots = idxs{}.size(); zip_install(leaves0, beavers0, built.first.first, built.first.second, idxs{}, std::make_index_sequence{}); zip_install(leaves1, beavers1, built.second.first, built.second.second, idxs{}, std::make_index_sequence{}); if constexpr (!std::is_void_v) { if (proto != nullptr) { if (x0_share != nullptr && x1_share != nullptr) { const InputT l0 = lane_input(*x0_share, prefix, bitlen); const InputT l1 = lane_input(*x1_share, prefix, bitlen); overwrite_arith_slots( *proto, s0, s1, t0, t1, pos_base, l0, l1, placed, leaves0, leaves1, idxs{}, std::make_index_sequence{}); } else { // Clear lane as a trivial XOR sharing (lane ⊕ 0). const InputT zero{}; overwrite_arith_slots( *proto, s0, s1, t0, t1, pos_base, lane_x, zero, placed, leaves0, leaves1, idxs{}, std::make_index_sequence{}); } } } else { (void)t1; (void)proto; (void)x0_share; (void)x1_share; static_assert(!placed_has_arith_beta_impl(idxs{}), "arith_beta payloads require Doerner–Shelat gen (CwProtocol)"); } } template void for_each_index(std::index_sequence, F && f) { (static_cast(f(std::integral_constant{})), ...); } template void gen_all_groups(InputT x, const typename InteriorPRG::block_type & s0, const typename InteriorPRG::block_type & s1, uint8_t t0, uint8_t t1, PlacedTuple & placed, Leaves0T & leaves0, Beavers0T & beavers0, Leaves1T & leaves1, Beavers1T & beavers1, std::index_sequence) { (gen_group( x, s0, s1, t0, t1, placed, leaves0, beavers0, leaves1, beavers1), ...); } template auto wrap_leaves(LeavesT & leaves, BeaversT & beavers, std::index_sequence) { using node = typename ExteriorPRG::block_type; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") return std::make_tuple( dpf::leaf_wrapper< typename std::tuple_element_t::output_type, node>(std::get(leaves), std::get(beavers))...); HEDLEY_PRAGMA(GCC diagnostic pop) } template auto make_incremental_impl(InputT x, PlacedTuple placed, root_sampler_t root_sampler, const dcf_runtime_spec * cmp_spec = nullptr); } // namespace incr } // namespace detail namespace detail { namespace incr { template constexpr bool extractable_pack_ok(std::index_sequence) noexcept { return (extractable_codomain_ok_v< typename Key::template concrete_output_type> && ...); } /// @brief Map kind → flags. Tree keep-path stays on α; leq/gt plant δ on that path /// via `include_eq`. Domain-edge α yields trivial always-true/false. /// @param ch the `ch` /// @param thresh the `thresh` /// @param nbits the width in bits inline void adjust_cmp_threshold(detail::cmp_meta & ch, unsigned __int128 & thresh, std::size_t nbits) { const unsigned __int128 domain = (nbits >= 128) ? 0 : (((unsigned __int128)1 << nbits) - 1); ch.include_eq = false; ch.trivial = cmp_trivial::none; if (is_paint_kind(ch.kind)) { ch.eval_as_ge = false; return; } switch (ch.kind) { case cmp_kind::lt: ch.eval_as_ge = false; break; case cmp_kind::leq: ch.eval_as_ge = false; ch.include_eq = true; if (nbits < 128 && thresh == domain) ch.trivial = cmp_trivial::always_true; break; case cmp_kind::geq: ch.eval_as_ge = true; break; case cmp_kind::gt: ch.eval_as_ge = true; ch.include_eq = true; if (nbits < 128 && thresh == domain) ch.trivial = cmp_trivial::always_false; break; default: break; } if (ch.incremental) ch.trivial = cmp_trivial::none; } /// @brief Split the constant absorb into party shares. `target` is `if_false` for /// lt/leq, or `δ + if_false` when the path-sum is inverted (geq/gt), or the /// full if_true / if_false for trivial domain edges. Clears nothing — caller /// must not put δ on the key. /// @param target the opened payload target /// @param mask the bit mask /// @param r the `r` /// @param add0 the `add0` /// @param add1 the `add1` HEDLEY_NO_THROW inline void split_cmp_addend(uint64_t target, uint64_t mask, uint64_t r, uint64_t & add0, uint64_t & add1) noexcept { using namespace detail::dcf_impl; r &= mask; target &= mask; add0 = r; add1 = (target + neg_m(r, mask)) & mask; } /// @brief Typed overload: write party-0 / party-1 additive shares of the absorb. /// @param target the opened payload target /// @param mask the bit mask /// @param r the `r` /// @param add0 the `add0` /// @param add1 the `add1` HEDLEY_NO_THROW inline void split_cmp_addend(uint64_t target, uint64_t mask, uint64_t r, additive_share & add0, additive_share & add1) noexcept { uint64_t a0 = 0, a1 = 0; split_cmp_addend(target, mask, r, a0, a1); add0 = additive_share::from_raw(a0); add1 = additive_share::from_raw(a1); } template auto extract_addends(const PlacedTuple & placed, std::index_sequence) { return std::make_tuple(std::get(placed).addend...); } template auto make_incremental_impl(InputT x, PlacedTuple placed, root_sampler_t root_sampler, const dcf_runtime_spec * cmp_spec) { static_assert(!(CmpIdcf && CmpBlock > 0), "idcf uses the per-level path, not blocked checkpoints"); using key_type = incr_dpf_key_of_t; using interior_node = typename key_type::interior_node; using input_type = typename key_type::input_type; constexpr auto depth = key_type::depth; constexpr auto n = key_type::num_outputs; using MetaHolder = meta_holder; using namespace detail::dcf_impl; if constexpr (IsExtractable) { static_assert(n > 0, "extractable requires at least one output"); static_assert(extractable_pack_ok( std::make_index_sequence{}), "extractable: each output must embed injectively into fp61"); } utils::flip_msb_if_signed_integral(x); using tree = dpf::tree_traits; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") interior_node root[2]; HEDLEY_PRAGMA(GCC diagnostic pop) tree::root_init(root, root_sampler); typename key_type::correction_words_array correction_words{}; typename key_type::correction_advice_array correction_advice{}; typename key_type::correction_seeds_array correction_seeds{}; interior_node parent[2] = {root[0], root[1]}; auto mask = key_type::msb_mask; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") std::array snap0{}; std::array snap1{}; HEDLEY_PRAGMA(GCC diagnostic pop) std::array snap_sign{}; std::array need_snap{}; for (std::size_t i = 0; i <= depth; ++i) need_snap[i] = false; for (std::size_t i = 0; i < n; ++i) need_snap[key_type::meta[i].tree_level] = true; if (need_snap[0]) { snap0[0] = parent[0]; snap1[0] = parent[1]; snap_sign[0] = dpf::get_lo_bit(parent[0]); } detail::cmp_meta cmp{}; // ... rest continues from existing body — patched via smaller edits below typename key_type::value_cw_array value_cws{}; typename key_type::tail_array tail{}; typename key_type::tail_array tail_coeff{}; uint64_t cw_last = 0; // Wildcard payload: per-level δ-coefficients (`value_cw(1) − value_cw(0)`), // computed with a parallel β = 1 accumulator `Va1`. Unused when !CmpWild. // Blocked keys store one coefficient per checkpoint and tail slot instead. typename key_type::value_cw_array value_cw_coeff{}; uint64_t cw_last_coeff = 0; uint64_t Va1 = 0; unsigned __int128 thresh = 0; std::size_t cmp_nbits = 0; uint64_t Va = 0; uint64_t delta = 0; uint64_t false_value = 0; if (cmp_spec != nullptr) { cmp_nbits = cmp_spec->prefix ? cmp_spec->prefix : utils::bitlength_of_v; cmp.nbits = static_cast(cmp_nbits); cmp.mask = cmp_spec->mask; delta = cmp_spec->beta & cmp_spec->mask; false_value = cmp_spec->false_value & cmp_spec->mask; cmp.kind = cmp_spec->kind; cmp.active = true; cmp.incremental = cmp_spec->incremental; cmp.eval_as_ge = false; cmp.trivial = cmp_trivial::none; cmp.block_width = static_cast(CmpBlock); cmp.tail_bits = static_cast(key_type::cmp_q); auto lane = lane_input(x, cmp_nbits, utils::bitlength_of_v); thresh = static_cast( utils::to_integral_type{}(lane)); adjust_cmp_threshold(cmp, thresh, cmp_nbits); if (CmpBlock > 0 && is_paint_kind(cmp.kind)) throw std::invalid_argument( "path recipes use the per-level comparison channel"); if (cmp_spec->custom && (CmpBlock > 0 || is_paint_kind(cmp.kind))) throw std::invalid_argument( "this comparison payload uses the per-level channel"); if (cmp_spec->use_payload_group && (CmpBlock > 0 || is_paint_kind(cmp.kind))) throw std::invalid_argument( "this comparison payload uses the per-level channel"); } const bool custom_cmp = cmp_spec != nullptr && cmp_spec->custom; const bool payload_cmp = cmp_spec != nullptr && cmp_spec->use_payload_group; detail::group_elem g_delta = custom_cmp ? cmp_spec->beta_g : detail::group_elem{}; detail::group_elem g_false = custom_cmp ? cmp_spec->false_g : detail::group_elem{}; detail::group_elem g_va = detail::group_zero(g_delta); detail::group_elem g_va1 = detail::group_zero(g_delta); detail::group_elem g_one = detail::group_one(g_delta); detail::group_elem g_on = detail::group_zero(g_delta); detail::group_elem g_on_unit = detail::group_zero(g_delta); if (custom_cmp && cmp.include_eq && !is_paint_kind(cmp.kind)) { g_on = g_delta; g_on_unit = g_one; } typename key_type::value_cw_word g_last{}; typename key_type::value_cw_word g_last_coeff{}; unsigned char pva[detail::payload_ops::cap]{}; std::int64_t pva_c = 0; typename key_type::value_cw_word p_last{}; typename key_type::value_cw_word p_last_coeff{}; const detail::payload_ops * pops = payload_cmp ? &cmp_spec->payload : nullptr; const paint_callback paint_cb = (cmp_spec != nullptr && cmp_spec->paint) ? &paint_fn_adapter : nullptr; const void * paint_ctx = (cmp_spec != nullptr && cmp_spec->paint) ? &cmp_spec->paint : nullptr; const std::size_t paint_length_bits = cmp_spec != nullptr ? cmp_spec->length_bits : 0; typename key_type::prefix_cw_array prefix_cw{}; typename key_type::prefix_cw_array prefix_coeff{}; const auto on_path_scaled = [&](uint64_t scale) -> uint64_t { if (!is_paint_kind(cmp.kind)) return (cmp.include_eq ? scale : 0ULL) & cmp.mask; const uint64_t unit = detail::dcf_impl::paint_unit(cmp.kind, cmp_nbits, thresh, cmp_nbits, paint_length_bits, true, paint_cb, paint_ctx); return detail::dcf_impl::scale_plant(unit, scale, cmp.mask); }; const uint64_t on_path = on_path_scaled(delta); const uint64_t on_path_unit = on_path_scaled(1ULL); auto snap_prefix = [&](std::size_t at) { if constexpr (CmpIdcf) { if (!cmp.incremental || cmp.trivial != cmp_trivial::none) return; if (payload_cmp) { unsigned char outb[detail::payload_ops::cap]{}; std::int64_t coeff = 0; const bool wild = cmp_spec->is_wildcard; const bool plant = cmp.include_eq && !is_paint_kind(cmp.kind); detail::payload_final_cw(*pops, &parent[0], &parent[1], sizeof(parent[0]), static_cast(dpf::get_lo_bit(parent[1])), pva, pva_c, (!wild && plant) ? cmp_spec->payload_beta : nullptr, plant && wild ? 1 : 0, outb, wild ? &coeff : nullptr); prefix_cw[at] = detail::payload_to_word< typename key_type::value_cw_word>(outb, pops->size); if constexpr (CmpWild) { if (wild) prefix_coeff[at] = detail::payload_coeff_word< typename key_type::value_cw_word>(coeff); } return; } if constexpr (std::is_integral_v) { const uint64_t word = detail::dcf_impl::make_final_cw(parent[0], parent[1], static_cast(dpf::get_lo_bit(parent[1])), Va, cmp.mask, on_path); prefix_cw[at] = static_cast(word); if constexpr (CmpWild) { const uint64_t w1 = detail::dcf_impl::make_final_cw(parent[0], parent[1], static_cast(dpf::get_lo_bit(parent[1])), Va1, cmp.mask, on_path_unit); prefix_coeff[at] = static_cast( (w1 + detail::dcf_impl::neg_m(word, cmp.mask)) & cmp.mask); } } } }; auto snap_prefix_group = [&](std::size_t at) { if constexpr (CmpIdcf && std::is_integral_v) { if (!cmp.incremental || cmp.trivial != cmp_trivial::none) return; const auto word = detail::group_final_cw(parent[0], parent[1], static_cast(dpf::get_lo_bit(parent[1])), g_va, g_on); prefix_cw[at] = detail::group_to_word(word); if constexpr (CmpWild) { const auto w1 = detail::group_final_cw(parent[0], parent[1], static_cast(dpf::get_lo_bit(parent[1])), g_va1, g_on_unit); prefix_coeff[at] = detail::group_to_word( detail::group_sub(w1, word)); } } }; if constexpr (CmpIdcf) { if (cmp.active && !custom_cmp) snap_prefix(0); if (cmp.active && custom_cmp) snap_prefix_group(0); } for (std::size_t level = 0; level < depth; ++level, mask >>= 1) { bool bit = !!(mask & x); const bool is_last = tree::is_last_level(level, depth); const bool advice0 = static_cast(dpf::get_lo_bit(parent[0])); const bool advice1 = static_cast(dpf::get_lo_bit(parent[1])); auto child0 = tree::expand(parent[0], is_last); auto child1 = tree::expand(parent[1], is_last); // Value Convert stretch (HT mid: two-tweak, independent of seed expand). const auto val0 = tree::expand_value(parent[0]); const auto val1 = tree::expand_value(parent[1]); interior_node cw{}; psnip_uint8_t tpack = 0; tree::make_cw(cw, tpack, child0, child1, parent[0], parent[1], bit, is_last); parent[0] = tree::advance(parent[0], child0, cw, tpack, bit, advice0, is_last); parent[1] = tree::advance(parent[1], child1, cw, tpack, bit, advice1, is_last); correction_words[level] = cw; correction_advice[level] = tpack; if constexpr (IsVerifiable) { // Prefix bits of α through this level (same labelling as eval fold). const auto prefix = static_cast( utils::to_integral_type{}(x) >> (utils::bitlength_of_v - (level + 1))); if constexpr (CmpBlock > 0) { correction_seeds[level] = detail::vdpf::make_cs( detail::blocked::fold_spine_tag | level, prefix, parent[0], parent[1]); } else { correction_seeds[level] = detail::vdpf::make_cs(level, prefix, parent[0], parent[1]); } } if constexpr (CmpBlock == 0) { if (cmp.active && cmp.trivial == cmp_trivial::none && level < cmp_nbits && payload_cmp) { const int ai = static_cast( (thresh >> (cmp_nbits - 1 - level)) & 1); unsigned char vcw[detail::payload_ops::cap]{}; std::int64_t coeff = 0; const bool wild = cmp_spec->is_wildcard; detail::payload_value_cw(*pops, &val0[0], &val0[1], &val1[0], &val1[1], sizeof(val0[0]), static_cast(advice1), ai, pva, pva_c, wild ? nullptr : cmp_spec->payload_beta, vcw, wild ? &coeff : nullptr); value_cws[level] = detail::payload_to_word( vcw, pops->size); if constexpr (CmpWild) { if (wild) value_cw_coeff[level] = detail::payload_coeff_word(coeff); } snap_prefix(level + 1); } else if constexpr (std::is_integral_v) if (cmp.active && cmp.trivial == cmp_trivial::none && level < cmp_nbits && !custom_cmp && !payload_cmp) { const int ai = static_cast( (thresh >> (cmp_nbits - 1 - level)) & 1); uint64_t base = 0; if (is_paint_kind(cmp.kind)) { const uint64_t unit = detail::dcf_impl::paint_unit(cmp.kind, level, thresh, cmp_nbits, paint_length_bits, false, paint_cb, paint_ctx); base = detail::dcf_impl::make_value_cw_planted(val0[0], val0[1], val1[0], val1[1], static_cast(advice0), static_cast(advice1), ai, Va, detail::dcf_impl::scale_plant(unit, delta, cmp.mask), cmp.mask); if constexpr (CmpWild) { const uint64_t v1 = detail::dcf_impl::make_value_cw_planted( val0[0], val0[1], val1[0], val1[1], static_cast(advice0), static_cast(advice1), ai, Va1, detail::dcf_impl::scale_plant(unit, 1ULL, cmp.mask), cmp.mask); value_cw_coeff[level] = static_cast( (v1 + detail::dcf_impl::neg_m(base, cmp.mask)) & cmp.mask); } } else { base = make_value_cw(val0[0], val0[1], val1[0], val1[1], static_cast(advice0), static_cast(advice1), ai, Va, delta, cmp.mask); if constexpr (CmpWild) { // Same recurrence with β = 1 on a parallel accumulator; the // value CW is affine in β so `coeff = value_cw(1) − base`. const uint64_t v1 = make_value_cw(val0[0], val0[1], val1[0], val1[1], static_cast(advice0), static_cast(advice1), ai, Va1, 1ULL, cmp.mask); value_cw_coeff[level] = static_cast( (v1 + detail::dcf_impl::neg_m(base, cmp.mask)) & cmp.mask); } } value_cws[level] = static_cast(base); snap_prefix(level + 1); } else if (cmp.active && cmp.trivial == cmp_trivial::none && level < cmp_nbits && custom_cmp) { const int ai = static_cast( (thresh >> (cmp_nbits - 1 - level)) & 1); const auto base = detail::group_value_cw(val0[0], val0[1], val1[0], val1[1], static_cast(advice0), static_cast(advice1), ai, g_va, g_delta); value_cws[level] = detail::group_to_word(base); if constexpr (CmpWild) { const auto v1 = detail::group_value_cw(val0[0], val0[1], val1[0], val1[1], static_cast(advice0), static_cast(advice1), ai, g_va1, g_one); value_cw_coeff[level] = detail::group_to_word( detail::group_sub(v1, base)); } snap_prefix_group(level + 1); } } else if (cmp.active && cmp.trivial == cmp_trivial::none) { using sched = detail::blocked::schedule; const std::size_t c = level + 1; if (c <= key_type::cmp_h && sched::contains(c)) { const auto wi = sched::index(c); const uint64_t word = detail::blocked::checkpoint_word( parent[0], parent[1], delta, cmp.mask); value_cws[wi] = static_cast(word); if constexpr (CmpWild) { value_cw_coeff[wi] = static_cast( detail::blocked::checkpoint_coeff( parent[0], parent[1], cmp.mask)); } } if (c == key_type::cmp_h && key_type::cmp_q > 0) { uint64_t words[4]{}; uint64_t coeffs[4]{}; const uint64_t suffix = static_cast(thresh) & ((1ULL << key_type::cmp_q) - 1ULL); detail::blocked::tail_words(parent[0], parent[1], delta, cmp.mask, cmp.include_eq, suffix, key_type::cmp_q, words, CmpWild ? coeffs : nullptr); for (std::size_t z = 0; z < key_type::cmp_tail; ++z) { tail[z] = static_cast(words[z]); if constexpr (CmpWild) { tail_coeff[z] = static_cast(coeffs[z]); } } } } if (need_snap[level + 1]) { snap0[level + 1] = parent[0]; snap1[level + 1] = parent[1]; snap_sign[level + 1] = dpf::get_lo_bit(parent[0]); } if constexpr (CmpBlock == 0) { if (cmp.active && cmp.trivial == cmp_trivial::none && level + 1 == cmp_nbits && payload_cmp) { unsigned char outb[detail::payload_ops::cap]{}; std::int64_t coeff = 0; const bool wild = cmp_spec->is_wildcard; const bool plant = cmp.include_eq && !is_paint_kind(cmp.kind); detail::payload_final_cw(*pops, &parent[0], &parent[1], sizeof(parent[0]), static_cast(dpf::get_lo_bit(parent[1])), pva, pva_c, (!wild && plant) ? cmp_spec->payload_beta : nullptr, (!wild && plant) ? 0 : (plant ? 1 : 0), outb, wild ? &coeff : nullptr); p_last = detail::payload_to_word( outb, pops->size); if constexpr (CmpWild) { if (wild) p_last_coeff = detail::payload_coeff_word(coeff); } } else if constexpr (std::is_integral_v) if (cmp.active && cmp.trivial == cmp_trivial::none && level + 1 == cmp_nbits && !custom_cmp && !payload_cmp) { cw_last = make_final_cw(parent[0], parent[1], static_cast(dpf::get_lo_bit(parent[1])), Va, cmp.mask, on_path); if constexpr (CmpWild) { const uint64_t l1 = make_final_cw(parent[0], parent[1], static_cast(dpf::get_lo_bit(parent[1])), Va1, cmp.mask, on_path_unit); cw_last_coeff = (l1 + detail::dcf_impl::neg_m(cw_last, cmp.mask)) & cmp.mask; } } else if (cmp.active && cmp.trivial == cmp_trivial::none && level + 1 == cmp_nbits && custom_cmp) { g_last = detail::group_to_word( detail::group_final_cw(parent[0], parent[1], static_cast(dpf::get_lo_bit(parent[1])), g_va, g_on)); if constexpr (CmpWild) { const auto l1 = detail::group_final_cw(parent[0], parent[1], static_cast(dpf::get_lo_bit(parent[1])), g_va1, g_on_unit); g_last_coeff = detail::group_to_word( detail::group_sub(l1, detail::group_from_word(g_last, g_delta))); } } } } // `key_type::num_outputs` is a static constexpr, so the loop bound is a // constant expression. A local `n` is not usable here: capturing it is not // a constant, and the loop would lower to a goto. constexpr std::size_t ngroups = [] { std::size_t m = 0; for (std::size_t i = 0; i < key_type::num_outputs; ++i) m = std::max(m, key_type::meta[i].group_id + 1); return m; }(); auto leaves0 = empty_leaves( std::make_index_sequence{}); auto beavers0 = empty_beavers( std::make_index_sequence{}); auto leaves1 = empty_leaves( std::make_index_sequence{}); auto beavers1 = empty_beavers( std::make_index_sequence{}); constexpr auto order = build_group_order(key_type::meta, n); using leaf_prg = std::conditional_t, ExteriorPRG>; for_each_index(std::make_index_sequence{}, [&](auto oi) { constexpr std::size_t G = order[decltype(oi)::value]; constexpr std::size_t lvl = [] { for (std::size_t i = 0; i < key_type::num_outputs; ++i) { if (key_type::meta[i].group_id == G) return key_type::meta[i].tree_level; } return std::size_t{0}; }(); HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") gen_group( x, dpf::unset_lo_2bits(snap0[lvl]), dpf::unset_lo_2bits(snap1[lvl]), static_cast(snap_sign[lvl]), static_cast(dpf::get_lo_bit(snap1[lvl])), placed, leaves0, beavers0, leaves1, beavers1); HEDLEY_PRAGMA(GCC diagnostic pop) }); auto wrap0 = wrap_leaves( leaves0, beavers0, std::make_index_sequence{}); auto wrap1 = wrap_leaves( leaves1, beavers1, std::make_index_sequence{}); uint64_t cmp_add0 = 0, cmp_add1 = 0; typename key_type::value_cw_word g_add0{}; typename key_type::value_cw_word g_add1{}; if (cmp.active && payload_cmp) { unsigned char target[detail::payload_ops::cap]{}; detail::payload_copy(target, cmp_spec->payload_false, pops->size); if (cmp.trivial == cmp_trivial::always_true || cmp.eval_as_ge) pops->add(target, target, cmp_spec->payload_beta); unsigned char blind[detail::payload_ops::cap]{}; const auto seed = root_sampler(); pops->from_node(blind, &seed, sizeof(seed)); unsigned char neg_blind[detail::payload_ops::cap]{}; unsigned char other[detail::payload_ops::cap]{}; pops->neg(neg_blind, blind); pops->add(other, target, neg_blind); g_add0 = detail::payload_to_word( blind, pops->size); g_add1 = detail::payload_to_word( other, pops->size); } else if constexpr (std::is_integral_v) if (cmp.active && !custom_cmp && !payload_cmp) { uint64_t target = false_value; if (cmp.trivial == cmp_trivial::always_true) target = (delta + false_value) & cmp.mask; else if (cmp.trivial == cmp_trivial::always_false) target = false_value; else if (cmp.eval_as_ge) target = (delta + false_value) & cmp.mask; // Group-width addend blind (same helper the DS path uses). The blind // keeps only `popcount(mask)` live bits; the addend share is stored at // `value_cw_word` width on the key. const uint64_t r = detail::dcf_impl::sample_addend_blind(cmp.mask, [&]() -> interior_node { return root_sampler(); }); split_cmp_addend(target, cmp.mask, r, cmp_add0, cmp_add1); } else if (cmp.active && custom_cmp) { detail::group_elem target = g_false; if (cmp.trivial == cmp_trivial::always_true || cmp.eval_as_ge) target = detail::group_add(g_delta, g_false); const auto blind = detail::group_from_node(root_sampler(), g_delta); g_add0 = detail::group_to_word(blind); g_add1 = detail::group_to_word( detail::group_sub(target, blind)); } input_type off0{}, off1{}; auto adds = extract_addends(placed, std::make_index_sequence{}); key_type key0{root[0], correction_words, correction_advice, std::move(wrap0), off0, cmp, value_cws, cw_last, cmp_add0, adds, value_cw_coeff, cw_last_coeff, tail, tail_coeff, prefix_cw, prefix_coeff, correction_seeds}; key_type key1{root[1], correction_words, correction_advice, std::move(wrap1), off1, cmp, value_cws, cw_last, cmp_add1, adds, value_cw_coeff, cw_last_coeff, tail, tail_coeff, prefix_cw, prefix_coeff, correction_seeds}; if (custom_cmp) { key0.set_cmp_scalars(g_last, g_add0, g_last_coeff); key1.set_cmp_scalars(g_last, g_add1, g_last_coeff); } if (payload_cmp) { key0.set_cmp_scalars(p_last, g_add0, p_last_coeff); key1.set_cmp_scalars(p_last, g_add1, p_last_coeff); } return dpf::make_party_key_pair(std::move(key0), std::move(key1)); } template auto make_incremental_ds_impl(bool arith, InputT x0, InputT x1, RootSampler & root_sampler, CwProtocol & proto, PlacedTuple placed, const dcf_runtime_spec * cmp_spec) { static_assert(!dpf::is_wildcard_v, "Doerner–Shelat gen takes shares of a concrete point"); static_assert(sizeof(typename InteriorPRG::block_type) == sizeof(simde__m128i), "Doerner–Shelat gen uses the AES-block interior node"); static_assert(!(CmpIdcf && CmpBlock > 0), "idcf uses the per-level path, not blocked checkpoints"); using key_type = incr_dpf_key_of_t; using interior_node = typename key_type::interior_node; using input_type = typename key_type::input_type; constexpr auto depth = key_type::depth; constexpr auto n = key_type::num_outputs; using MetaHolder = meta_holder; using namespace detail::dcf_impl; proto.encode_walk_shares(x0, x1, arith); using tree = dpf::tree_traits; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") interior_node roots[2]; HEDLEY_PRAGMA(GCC diagnostic pop) tree::root_init(roots, [&]() -> interior_node { return static_cast(root_sampler()); }); const interior_node root0 = roots[0]; const interior_node root1 = roots[1]; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") ds_gen_state st; HEDLEY_PRAGMA(GCC diagnostic pop) st.init(root0, root1); typename key_type::correction_words_array correction_words{}; typename key_type::correction_advice_array correction_advice{}; typename key_type::correction_seeds_array correction_seeds{}; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") std::array snap0{}; std::array snap1{}; HEDLEY_PRAGMA(GCC diagnostic pop) std::array snap_sign{}; std::array need_snap{}; for (std::size_t i = 0; i <= depth; ++i) need_snap[i] = false; for (std::size_t i = 0; i < n; ++i) need_snap[key_type::meta[i].tree_level] = true; if (need_snap[0]) { snap0[0] = st.seed0(); snap1[0] = st.seed1(); snap_sign[0] = dpf::get_lo_bit(st.seed0()); } detail::cmp_meta cmp{}; typename key_type::value_cw_array value_cws{}; typename key_type::value_cw_array value_cw_coeff{}; typename key_type::tail_array tail{}; typename key_type::tail_array tail_coeff{}; uint64_t cw_last = 0; uint64_t cw_last_coeff = 0; ds_cmp_gen_state cmp_st{}; uint64_t delta = 0; uint64_t false_value = 0; if (cmp_spec != nullptr) { // Threshold lane is opened only inside the CW protocol (paint / // domain-edge / blocked suffix). Value-word ai uses share bits. const std::size_t cmp_nbits = cmp_spec->prefix ? cmp_spec->prefix : utils::bitlength_of_v; cmp.nbits = static_cast(cmp_nbits); cmp.mask = cmp_spec->mask; delta = cmp_spec->beta & cmp_spec->mask; false_value = cmp_spec->false_value & cmp_spec->mask; cmp.kind = cmp_spec->kind; cmp.active = true; cmp.incremental = cmp_spec->incremental; cmp.eval_as_ge = false; cmp.trivial = cmp_trivial::none; cmp.block_width = static_cast(CmpBlock); cmp.tail_bits = static_cast(key_type::cmp_q); unsigned __int128 thresh = proto.open_cmp_threshold(x0, x1, cmp_nbits); adjust_cmp_threshold(cmp, thresh, cmp_nbits); if (CmpBlock > 0 && is_paint_kind(cmp.kind)) throw std::invalid_argument( "path recipes use the per-level comparison channel"); if (cmp_spec->custom && (CmpBlock > 0 || is_paint_kind(cmp.kind))) throw std::invalid_argument( "this comparison payload uses the per-level channel"); if (cmp_spec->use_payload_group && (CmpBlock > 0 || is_paint_kind(cmp.kind))) throw std::invalid_argument( "this comparison payload uses the per-level channel"); cmp_st.active = cmp.active; cmp_st.nbits = cmp_nbits; cmp_st.mask = cmp.mask; cmp_st.beta = delta; cmp_st.include_eq = cmp.include_eq; cmp_st.trivial = cmp.trivial; cmp_st.thresh = thresh; cmp_st.kind = cmp.kind; cmp_st.paint = is_paint_kind(cmp.kind); cmp_st.length_bits = cmp_spec->length_bits; cmp_st.paint_cb = cmp_spec->paint ? &paint_fn_adapter : nullptr; cmp_st.paint_ctx = cmp_spec->paint ? &cmp_spec->paint : nullptr; cmp_st.Va = 0; // Wildcard payload: open CWs for δ = 0 and stash β = 1 coefficients // (same dual-accumulator scheme as dealer `make_incremental_impl`). if constexpr (CmpWild) { cmp_st.track_coeff = true; cmp_st.Va1 = 0; } } const bool custom_cmp = cmp_spec != nullptr && cmp_spec->custom; const bool payload_cmp = cmp_spec != nullptr && cmp_spec->use_payload_group; detail::group_elem g_delta = custom_cmp ? cmp_spec->beta_g : detail::group_elem{}; detail::group_elem g_false = custom_cmp ? cmp_spec->false_g : detail::group_elem{}; detail::group_elem g_va = detail::group_zero(g_delta); detail::group_elem g_va1 = detail::group_zero(g_delta); detail::group_elem g_one = detail::group_one(g_delta); detail::group_elem g_on = detail::group_zero(g_delta); detail::group_elem g_on_unit = detail::group_zero(g_delta); if (custom_cmp && cmp.include_eq && !is_paint_kind(cmp.kind)) { g_on = g_delta; g_on_unit = g_one; } typename key_type::value_cw_word g_last{}; typename key_type::value_cw_word g_last_coeff{}; unsigned char pva[detail::payload_ops::cap]{}; std::int64_t pva_c = 0; typename key_type::value_cw_word p_last{}; typename key_type::value_cw_word p_last_coeff{}; const detail::payload_ops * pops = payload_cmp ? &cmp_spec->payload : nullptr; typename key_type::prefix_cw_array prefix_cw{}; typename key_type::prefix_cw_array prefix_coeff{}; const uint64_t on_path = [&]() -> uint64_t { if (!is_paint_kind(cmp.kind)) return (cmp.include_eq ? delta : 0ULL) & cmp.mask; const uint64_t unit = paint_unit(cmp.kind, cmp_st.nbits, cmp_st.thresh, cmp_st.nbits, cmp_st.length_bits, true, cmp_st.paint_cb, cmp_st.paint_ctx); return scale_plant(unit, delta, cmp.mask); }(); const uint64_t on_path_unit = [&]() -> uint64_t { if (!is_paint_kind(cmp.kind)) return (cmp.include_eq ? 1ULL : 0ULL) & cmp.mask; const uint64_t unit = paint_unit(cmp.kind, cmp_st.nbits, cmp_st.thresh, cmp_st.nbits, cmp_st.length_bits, true, cmp_st.paint_cb, cmp_st.paint_ctx); return scale_plant(unit, 1ULL, cmp.mask); }(); auto snap_prefix = [&](std::size_t at) { if constexpr (CmpIdcf) { if (!cmp.incremental || cmp.trivial != cmp_trivial::none) return; if (payload_cmp) { unsigned char outb[detail::payload_ops::cap]{}; std::int64_t coeff = 0; const bool wild = cmp_spec->is_wildcard; const bool plant = cmp.include_eq && !is_paint_kind(cmp.kind); auto s0 = st.seed0(); auto s1 = st.seed1(); detail::payload_final_cw(*pops, &s0, &s1, sizeof(s0), static_cast(dpf::get_lo_bit(s1)), pva, pva_c, (!wild && plant) ? cmp_spec->payload_beta : nullptr, plant && wild ? 1 : 0, outb, wild ? &coeff : nullptr); prefix_cw[at] = detail::payload_to_word< typename key_type::value_cw_word>(outb, pops->size); if constexpr (CmpWild) { if (wild) prefix_coeff[at] = detail::payload_coeff_word< typename key_type::value_cw_word>(coeff); } return; } if constexpr (std::is_integral_v) { HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") const uint64_t word = proto.open_final_cw(st.seed0(), st.seed1(), static_cast(dpf::get_lo_bit(st.seed1())), cmp_st.Va, cmp.mask, on_path); HEDLEY_PRAGMA(GCC diagnostic pop) prefix_cw[at] = static_cast(word); if constexpr (CmpWild) { const uint64_t w1 = proto.open_final_cw(st.seed0(), st.seed1(), static_cast(dpf::get_lo_bit(st.seed1())), cmp_st.Va1, cmp.mask, on_path_unit); prefix_coeff[at] = static_cast( (w1 + neg_m(word, cmp.mask)) & cmp.mask); } } } }; auto snap_prefix_group = [&](std::size_t at) { if constexpr (CmpIdcf) { if (!cmp.incremental || cmp.trivial != cmp_trivial::none) return; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") const auto word = detail::group_final_cw(st.seed0(), st.seed1(), static_cast(dpf::get_lo_bit(st.seed1())), g_va, g_on); HEDLEY_PRAGMA(GCC diagnostic pop) prefix_cw[at] = detail::group_to_word(word); if constexpr (CmpWild) { const auto w1 = detail::group_final_cw(st.seed0(), st.seed1(), static_cast(dpf::get_lo_bit(st.seed1())), g_va1, g_on_unit); prefix_coeff[at] = detail::group_to_word( detail::group_sub(w1, word)); } } }; if constexpr (CmpIdcf) { if (cmp.active && !custom_cmp) snap_prefix(0); if (cmp.active && custom_cmp) snap_prefix_group(0); } auto mask = key_type::msb_mask; constexpr auto to_int_x = utils::to_integral_type{}; using mask_type = std::decay_t; constexpr auto to_mask = utils::to_integral_type{}; for (std::size_t level = 0; level < depth; ++level, mask >>= 1) { uint64_t vcw = 0; const auto mi = to_mask(mask); const uint8_t path_bit0 = static_cast(!!(mi & to_int_x(x0))); const uint8_t path_bit1 = static_cast(!!(mi & to_int_x(x1))); const int path_ai = static_cast((path_bit0 ^ path_bit1) & 1u); if constexpr (CmpBlock == 0) { if constexpr (std::is_integral_v) if (custom_cmp && cmp_st.active && cmp_st.trivial == cmp_trivial::none && level < cmp_st.nbits) { using tree = dpf::tree_traits; auto s0 = st.seed0(); auto s1 = st.seed1(); const auto a0 = static_cast(dpf::get_lo_bit(s0)); const auto a1 = static_cast(dpf::get_lo_bit(s1)); auto v0 = tree::expand_value(s0); auto v1 = tree::expand_value(s1); const int ai = path_ai; const auto base = detail::group_value_cw(v0[0], v0[1], v1[0], v1[1], a0, a1, ai, g_va, g_delta); value_cws[level] = detail::group_to_word(base); if constexpr (CmpWild) { const auto v1w = detail::group_value_cw(v0[0], v0[1], v1[0], v1[1], a0, a1, ai, g_va1, g_one); value_cw_coeff[level] = detail::group_to_word( detail::group_sub(v1w, base)); } } if (payload_cmp && cmp_st.active && cmp_st.trivial == cmp_trivial::none && level < cmp_st.nbits) { using tree = dpf::tree_traits; auto s0 = st.seed0(); auto s1 = st.seed1(); const auto a1 = static_cast(dpf::get_lo_bit(s1)); auto v0 = tree::expand_value(s0); auto v1 = tree::expand_value(s1); const int ai = path_ai; unsigned char vcw_b[detail::payload_ops::cap]{}; std::int64_t coeff = 0; const bool wild = cmp_spec->is_wildcard; detail::payload_value_cw(*pops, &v0[0], &v0[1], &v1[0], &v1[1], sizeof(v0[0]), a1, ai, pva, pva_c, wild ? nullptr : cmp_spec->payload_beta, vcw_b, wild ? &coeff : nullptr); value_cws[level] = detail::payload_to_word< typename key_type::value_cw_word>(vcw_b, pops->size); if constexpr (CmpWild) { if (wild) value_cw_coeff[level] = detail::payload_coeff_word< typename key_type::value_cw_word>(coeff); } } ds_advance_level(st, x0, x1, mask, level, depth, proto, correction_words[level], correction_advice[level], (!custom_cmp && !payload_cmp && cmp_st.active) ? &vcw : nullptr, (!custom_cmp && !payload_cmp && cmp_st.active) ? &cmp_st : nullptr); if constexpr (IsVerifiable) { correction_seeds[level] = proto.open_correction_seed(level, x0, x1, level + 1, st.seed0(), st.seed1()); } if constexpr (std::is_integral_v) if (!custom_cmp && !payload_cmp && cmp_st.active && cmp_st.trivial == cmp_trivial::none && level < cmp_st.nbits) { value_cws[level] = static_cast(vcw); if constexpr (CmpWild) { value_cw_coeff[level] = static_cast( cmp_st.last_vcw_coeff); } snap_prefix(level + 1); } else if (custom_cmp && cmp_st.active && cmp_st.trivial == cmp_trivial::none && level < cmp_st.nbits) { snap_prefix_group(level + 1); } } else { ds_advance_level(st, x0, x1, mask, level, depth, proto, correction_words[level], correction_advice[level]); if constexpr (IsVerifiable) { // Domain-separate blocked CS from native per-level folds. correction_seeds[level] = proto.open_correction_seed( detail::blocked::fold_spine_tag | level, x0, x1, level + 1, st.seed0(), st.seed1()); } if (cmp_st.active && cmp_st.trivial == cmp_trivial::none) { using sched = detail::blocked::schedule; const std::size_t c = level + 1; if (c <= key_type::cmp_h && sched::contains(c)) { const auto wi = sched::index(c); const uint64_t word = detail::blocked::checkpoint_word( st.seed0(), st.seed1(), delta, cmp.mask); value_cws[wi] = static_cast(word); if constexpr (CmpWild) { value_cw_coeff[wi] = static_cast( detail::blocked::checkpoint_coeff( st.seed0(), st.seed1(), cmp.mask)); } } if (c == key_type::cmp_h && key_type::cmp_q > 0) { uint64_t words[4]{}; uint64_t coeffs[4]{}; const uint64_t suffix = static_cast(cmp_st.thresh) & ((1ULL << key_type::cmp_q) - 1ULL); detail::blocked::tail_words(st.seed0(), st.seed1(), delta, cmp.mask, cmp.include_eq, suffix, key_type::cmp_q, words, CmpWild ? coeffs : nullptr); for (std::size_t z = 0; z < key_type::cmp_tail; ++z) { tail[z] = static_cast(words[z]); if constexpr (CmpWild) { tail_coeff[z] = static_cast( coeffs[z]); } } } } } if (need_snap[level + 1]) { snap0[level + 1] = st.seed0(); snap1[level + 1] = st.seed1(); snap_sign[level + 1] = dpf::get_lo_bit(st.seed0()); } if constexpr (CmpBlock == 0) { if (cmp_st.active && cmp_st.trivial == cmp_trivial::none && level + 1 == cmp_st.nbits && payload_cmp) { unsigned char outb[detail::payload_ops::cap]{}; std::int64_t coeff = 0; const bool wild = cmp_spec->is_wildcard; const bool plant = cmp.include_eq && !is_paint_kind(cmp.kind); auto s0 = st.seed0(); auto s1 = st.seed1(); detail::payload_final_cw(*pops, &s0, &s1, sizeof(s0), static_cast(dpf::get_lo_bit(s1)), pva, pva_c, (!wild && plant) ? cmp_spec->payload_beta : nullptr, plant && wild ? 1 : 0, outb, wild ? &coeff : nullptr); p_last = detail::payload_to_word( outb, pops->size); if constexpr (CmpWild) { if (wild) p_last_coeff = detail::payload_coeff_word< typename key_type::value_cw_word>(coeff); } } else if constexpr (std::is_integral_v) if (cmp_st.active && cmp_st.trivial == cmp_trivial::none && level + 1 == cmp_st.nbits && !custom_cmp && !payload_cmp) { cw_last = proto.open_final_cw(st.seed0(), st.seed1(), static_cast(dpf::get_lo_bit(st.seed1())), cmp_st.Va, cmp_st.mask, on_path); if constexpr (CmpWild) { const uint64_t l1 = proto.open_final_cw(st.seed0(), st.seed1(), static_cast(dpf::get_lo_bit(st.seed1())), cmp_st.Va1, cmp_st.mask, on_path_unit); cw_last_coeff = (l1 + neg_m(cw_last, cmp_st.mask)) & cmp_st.mask; } } else if (cmp_st.active && cmp_st.trivial == cmp_trivial::none && level + 1 == cmp_st.nbits && custom_cmp) { g_last = detail::group_to_word( detail::group_final_cw(st.seed0(), st.seed1(), static_cast(dpf::get_lo_bit(st.seed1())), g_va, g_on)); if constexpr (CmpWild) { const auto l1 = detail::group_final_cw(st.seed0(), st.seed1(), static_cast(dpf::get_lo_bit(st.seed1())), g_va1, g_on_unit); g_last_coeff = detail::group_to_word( detail::group_sub(l1, detail::group_from_word(g_last, g_delta))); } } } } constexpr std::size_t ngroups = [] { std::size_t m = 0; for (std::size_t i = 0; i < key_type::num_outputs; ++i) m = std::max(m, key_type::meta[i].group_id + 1); return m; }(); auto leaves0 = empty_leaves( std::make_index_sequence{}); auto beavers0 = empty_beavers( std::make_index_sequence{}); auto leaves1 = empty_leaves( std::make_index_sequence{}); auto beavers1 = empty_beavers( std::make_index_sequence{}); constexpr auto order = build_group_order(key_type::meta, n); // Leaf phase: hand both point shares to the protocol. Reconstruct only // inside this hook to emit public leaf CWs; the gen body never forms `x`. using leaf_prg = std::conditional_t, ExteriorPRG>; proto.open_leaf_group(x0, x1, [&](input_type sx0, input_type sx1) { const input_type x = utils::xor_input_shares(sx0, sx1); for_each_index(std::make_index_sequence{}, [&](auto oi) { constexpr std::size_t G = order[decltype(oi)::value]; constexpr std::size_t lvl = [] { for (std::size_t i = 0; i < key_type::num_outputs; ++i) { if (key_type::meta[i].group_id == G) return key_type::meta[i].tree_level; } return std::size_t{0}; }(); HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") gen_group(x, dpf::unset_lo_2bits(snap0[lvl]), dpf::unset_lo_2bits(snap1[lvl]), static_cast(snap_sign[lvl]), static_cast(dpf::get_lo_bit(snap1[lvl])), placed, leaves0, beavers0, leaves1, beavers1, &proto, &sx0, &sx1); HEDLEY_PRAGMA(GCC diagnostic pop) }); }); auto wrap0 = wrap_leaves( leaves0, beavers0, std::make_index_sequence{}); auto wrap1 = wrap_leaves( leaves1, beavers1, std::make_index_sequence{}); uint64_t cmp_add0 = 0, cmp_add1 = 0; typename key_type::value_cw_word g_add0{}; typename key_type::value_cw_word g_add1{}; if (cmp.active && payload_cmp) { unsigned char target[detail::payload_ops::cap]{}; detail::payload_copy(target, cmp_spec->payload_false, pops->size); if (cmp.trivial == cmp_trivial::always_true || cmp.eval_as_ge) pops->add(target, target, cmp_spec->payload_beta); unsigned char blind[detail::payload_ops::cap]{}; const auto seed = static_cast(root_sampler()); pops->from_node(blind, &seed, sizeof(seed)); unsigned char neg_blind[detail::payload_ops::cap]{}; unsigned char other[detail::payload_ops::cap]{}; pops->neg(neg_blind, blind); pops->add(other, target, neg_blind); g_add0 = detail::payload_to_word( blind, pops->size); g_add1 = detail::payload_to_word( other, pops->size); } else if constexpr (std::is_integral_v) if (cmp.active && !custom_cmp && !payload_cmp) { uint64_t target = false_value; if (cmp.trivial == cmp_trivial::always_true) target = (delta + false_value) & cmp.mask; else if (cmp.trivial == cmp_trivial::always_false) target = false_value; else if (cmp.eval_as_ge) target = (delta + false_value) & cmp.mask; // Group-width addend blind from the protocol. The local backend reuses // the shared root sampler (matched tapes with the dealer); the addend // share itself is stored at `value_cw_word` width on the key, so no // padded uint64 rides the wire when the payload is narrow. // For wildcard payloads, target is 0 here; `assign_cmp` later replaces // these shares with a fresh split of the real absorb target. const uint64_t r = proto.sample_addend_blind(cmp.mask, [&]() -> interior_node { return static_cast(root_sampler()); }); split_cmp_addend(target, cmp.mask, r, cmp_add0, cmp_add1); } else if (cmp.active && custom_cmp) { detail::group_elem target = g_false; if (cmp.trivial == cmp_trivial::always_true || cmp.eval_as_ge) target = detail::group_add(g_delta, g_false); const auto blind = detail::group_from_node( static_cast(root_sampler()), g_delta); g_add0 = detail::group_to_word(blind); g_add1 = detail::group_to_word( detail::group_sub(target, blind)); } input_type off0{}, off1{}; auto adds = extract_addends(placed, std::make_index_sequence{}); key_type key0{root0, correction_words, correction_advice, std::move(wrap0), off0, cmp, value_cws, cw_last, cmp_add0, adds, value_cw_coeff, cw_last_coeff, tail, tail_coeff, prefix_cw, prefix_coeff, correction_seeds}; key_type key1{root1, correction_words, correction_advice, std::move(wrap1), off1, cmp, value_cws, cw_last, cmp_add1, adds, value_cw_coeff, cw_last_coeff, tail, tail_coeff, prefix_cw, prefix_coeff, correction_seeds}; if (custom_cmp) { key0.set_cmp_scalars(g_last, g_add0, g_last_coeff); key1.set_cmp_scalars(g_last, g_add1, g_last_coeff); } if (payload_cmp) { key0.set_cmp_scalars(p_last, g_add0, p_last_coeff); key1.set_cmp_scalars(p_last, g_add1, p_last_coeff); } return dpf::make_party_key_pair(std::move(key0), std::move(key1)); } template struct saved_leaf { static constexpr std::size_t slot = Slot; T value; }; template constexpr std::size_t rewrite_slot_count() noexcept { if constexpr (is_updatable_tag_v || is_cmp_spec_v || is_verifiable_tag_v || is_extractable_tag_v || is_output_mac_tag_v || is_eq_spec_v) return 0; else if constexpr (is_at_v) return std::tuple_size_v; else if constexpr (is_idpf_v) return A::n; else return 1; } template auto rewrite_leaf(T && value) { using A = std::decay_t; if constexpr (is_wildcard_v) return std::make_pair(std::forward(value), std::tuple<>{}); else if constexpr (is_arith_beta_v) { static_assert(!is_arith_beta_v, "updatable does not apply to arith_beta"); return std::make_pair(std::tuple<>{}, std::tuple<>{}); } else { using C = concrete_type_t; C kept = static_cast(std::forward(value)); return std::make_pair(wildcard_value{}, std::make_tuple(saved_leaf{std::move(kept)})); } } template auto rewrite_placed_values(Tuple && values, std::index_sequence, std::integral_constant) { auto parts = std::make_tuple( rewrite_leaf(std::get(std::forward(values)))...); auto planted = at(std::move(std::get<0>(std::get(parts)))...); auto saved = std::tuple_cat(std::move(std::get<1>(std::get(parts)))...); return std::make_pair(std::make_tuple(std::move(planted)), std::move(saved)); } template auto rewrite_idpf_values(Tuple && values, std::index_sequence, std::index_sequence) { auto parts = std::make_tuple( rewrite_leaf(std::get(std::forward(values)))...); auto planted = idpf_at( std::move(std::get<0>(std::get(parts)))...); auto saved = std::tuple_cat(std::move(std::get<1>(std::get(parts)))...); return std::make_pair(std::make_tuple(std::move(planted)), std::move(saved)); } template auto rewrite_one(idpf_pack, Betas...> pack) { return rewrite_idpf_values(std::move(pack.values), std::index_sequence_for{}, std::index_sequence{}); } template auto rewrite_one(Arg && arg) { using A = std::decay_t; if constexpr (is_updatable_tag_v) return std::make_pair(std::tuple<>{}, std::tuple<>{}); else if constexpr (is_eq_spec_v) { static_assert(!is_eq_spec_v, "updatable does not apply to eq"); return std::make_pair(std::tuple<>{}, std::tuple<>{}); } else if constexpr (is_cmp_spec_v || is_verifiable_tag_v || is_extractable_tag_v || is_output_mac_tag_v) return std::make_pair(std::make_tuple(std::forward(arg)), std::tuple<>{}); else if constexpr (is_at_v) return rewrite_placed_values(std::move(arg.values), std::make_index_sequence>{}, std::integral_constant{}); else { auto part = rewrite_leaf(std::forward(arg)); return std::make_pair(std::make_tuple(std::move(part.first)), std::move(part.second)); } } template auto rewrite_all() { return std::make_pair(std::tuple<>{}, std::tuple<>{}); } template auto rewrite_all(Arg && arg, Rest && ...rest) { auto head = rewrite_one(std::forward(arg)); auto tail = rewrite_all>()>( std::forward(rest)...); return std::make_pair( std::tuple_cat(std::move(head.first), std::move(tail.first)), std::tuple_cat(std::move(head.second), std::move(tail.second))); } template void assign_saved_one(K0 & k0, K1 & k1, Item & item) { auto shares = dpf::additively_share(item.value); auto & w0 = std::get(k0.leaf_nodes); auto & w1 = std::get(k1.leaf_nodes); const auto b0 = w0.compute_and_get_blinded_output_share(shares.first.raw()); const auto b1 = w1.compute_and_get_blinded_output_share(shares.second.raw()); const auto l0 = w0.compute_and_get_leaf_share(b1); const auto l1 = w1.compute_and_get_leaf_share(b0); w0.reconstruct_correction_word(l1); w1.reconstruct_correction_word(l0); } template void assign_saved(K0 & k0, K1 & k1, Saved & saved) { std::apply([&](auto & ...items) { (assign_saved_one(k0, k1, items), ...); }, saved); } } // namespace incr } // namespace detail /// @brief Build two party keys for one point. /// @param x plaintext domain point /// @param ys payloads, comparison specs, and key tags /// @return the two party keys /// @note Signed domains flip the MSB before the walk. A comparison spec walks the same n levels and writes a value correction word per level. /// @note Following Boyle, Gilboa, and Ishai, CCS 2016 (full version ePrint 2018/707): one correction word per level, the n(λ+2)-bit key, not their EUROCRYPT 2015 key of 4n(λ+1) bits. /// @note Remark 3.4 of that full version is this packing. `depth` is `n - lg(outputs_per_leaf)`, and `outputs_per_leaf` is how many copies of `G` fit in one `λ`-bit block, so `ν = log2(λ / log2|G|)`. Those low input bits select the lane inside the leaf. The tree does not write a correction word for them. A comparison still writes a value word on each remaining level. /// @note Boyle, Gilboa, Ishai, and Kolobov (ePrint 2023/028) give a statistically private 3-server DPF and a perfectly private 4-server DPF. This function is a 2-party PRG key. /// @see dpf::eval_point /// @see dpf::lt /// \complexity O(n) time and O(n) key size. n is the domain bitlength (`depth`). The loop does two interior PRG expansions and writes one correction word per level, then builds one exterior leaf per output. template >> HEDLEY_WARN_UNUSED_RESULT auto make_dpf(InputT && x, OutputTs && ...ys) { if constexpr (args_have_unit_sign_v) { // Strip the `unit_sign` out-parameter, build the key from the rest, // then fill the caller's sign shares from the bit leaf at `x`. auto * us = detail::incr::find_unit_sign(ys...); auto rest = std::tuple_cat( detail::incr::drop_unit_sign(std::forward(ys))...); auto keys = std::apply([&](auto && ...zs) { return make_dpf( x, std::forward(zs)...); }, std::move(rest)); detail::incr::fill_unit_sign(keys.first, keys.second, x, *us); return keys; } else if constexpr (args_have_updatable_v) { auto bundle = detail::incr::rewrite_all<0>(std::forward(ys)...); auto keys = std::apply([&](auto && ...zs) { return make_dpf( std::forward(x), std::forward(zs)...); }, std::move(bundle.first)); detail::incr::assign_saved(keys.first, keys.second, bundle.second); return keys; } else { using input_type = std::decay_t; static_assert(!is_secret_share_v, "make_dpf: domain point must be plaintext (use raw() or reconstruct)"); static_assert((!is_secret_share_v> && ...), "make_dpf: payloads must be plaintext (use raw() or reconstruct)"); static_assert(!args_have_output_mac_v || !args_have_bit_payload_v, "output_mac: a MAC on a bare parity bit is not offered"); using node = typename ExteriorPRG::block_type; constexpr auto bitlen = utils::bitlength_of_v; constexpr std::size_t CD = forced_cmp_depth_v; constexpr std::size_t CB = forced_cmp_out_bits_v; constexpr bool CW = forced_cmp_wild_v; constexpr std::size_t BK = forced_cmp_block_v; constexpr bool ID = forced_cmp_idcf_v; constexpr bool V = args_have_verifiable_v; constexpr bool E = args_have_extractable_v; dcf_runtime_spec dcf_spec{}; bool has_cmp = false; auto placed = detail::incr::flatten_args_and_cmp(dcf_spec, has_cmp, std::forward(ys)...); using placed_tuple = decltype(placed); const dcf_runtime_spec * cs = has_cmp ? &dcf_spec : nullptr; if constexpr (std::tuple_size_v == 0) { if (!has_cmp) throw std::invalid_argument("make_dpf: no outputs"); return detail::incr::make_incremental_impl(x, std::move(placed), dpf::uniform_sample, cs); } else if constexpr (!args_have_cmp_v && !args_have_eq_v && !args_have_verifiable_v && !args_have_extractable_v && detail::incr::is_classic_placed() && !detail::incr::placed_has_arith_beta()) { auto vals = detail::incr::classic_values(placed, std::make_index_sequence>{}); return std::apply( [&](auto && ...zs) { return dpf::make_dpf( dpf::make_dpfargs(x, std::forward(zs)...)); }, std::move(vals)); } else { return detail::incr::make_incremental_impl(x, std::move(placed), dpf::uniform_sample, cs); } } } /// @brief `make_dpf` with phantom tags from `auth_profile` (empty when semi-honest). /// @tparam Profile an `auth_profile` specialization. `output_mac` is ignored here /// as a key tag; when set, bit payloads are rejected at construction. /// @tparam InteriorPRG PRG that expands interior nodes /// @tparam ExteriorPRG PRG that expands the root /// @tparam InputT input domain type /// @tparam OutputTs payload and comparison arguments /// @param x the target point /// @param ys payloads, comparison specs, and any extra tags from the profile /// @return the two party keys /// @throws std::invalid_argument if there is no output /// @see key_tags_tuple template HEDLEY_WARN_UNUSED_RESULT auto make_dpf_profile(InputT && x, OutputTs && ...ys) { static_assert(!Profile::output_mac || !args_have_bit_payload_v, "output_mac: a MAC on a bare parity bit is not offered"); return std::apply( [&](auto ...tags) { return make_dpf( std::forward(x), std::forward(ys)..., tags...); }, key_tags_tuple()); } /// \complexity O(n) time and O(n) key size. n is the domain bitlength (`depth`). The loop does two interior PRG expansions and writes one correction word per level, then builds one exterior leaf per output. template >> HEDLEY_WARN_UNUSED_RESULT auto make_dpf(InputT && x, OutputTs && ...ys) { if constexpr (args_have_updatable_v) { auto bundle = detail::incr::rewrite_all<0>(std::forward(ys)...); auto keys = std::apply([&](auto && ...zs) { return make_dpf( std::forward(x), std::forward(zs)...); }, std::move(bundle.first)); detail::incr::assign_saved(keys.first, keys.second, bundle.second); return keys; } else { using input_type = std::decay_t; static_assert(!is_secret_share_v, "make_dpf: domain point must be plaintext (use raw() or reconstruct)"); static_assert((!is_secret_share_v> && ...), "make_dpf: payloads must be plaintext (use raw() or reconstruct)"); static_assert(!args_have_output_mac_v || !args_have_bit_payload_v, "output_mac: a MAC on a bare parity bit is not offered"); using node = typename ExteriorPRG::block_type; constexpr auto bitlen = utils::bitlength_of_v; constexpr std::size_t CD = forced_cmp_depth_v; constexpr std::size_t CB = forced_cmp_out_bits_v; constexpr bool CW = forced_cmp_wild_v; constexpr std::size_t BK = forced_cmp_block_v; constexpr bool ID = forced_cmp_idcf_v; constexpr bool V = args_have_verifiable_v; constexpr bool E = args_have_extractable_v; dcf_runtime_spec dcf_spec{}; bool has_cmp = false; auto placed = detail::incr::flatten_args_and_cmp(dcf_spec, has_cmp, std::forward(ys)...); using placed_tuple = decltype(placed); const dcf_runtime_spec * cs = has_cmp ? &dcf_spec : nullptr; auto seed = root_sampler_t{RootSampler}; if constexpr (std::tuple_size_v == 0) { if (!has_cmp) throw std::invalid_argument("make_dpf: no outputs"); return detail::incr::make_incremental_impl(x, std::move(placed), seed, cs); } else if constexpr (!args_have_cmp_v && !args_have_eq_v && !args_have_verifiable_v && !args_have_extractable_v && detail::incr::is_classic_placed() && !detail::incr::placed_has_arith_beta()) { auto vals = detail::incr::classic_values(placed, std::make_index_sequence>{}); return std::apply( [&](auto && ...zs) { return dpf::make_dpf( dpf::make_dpfargs(x, std::forward(zs)...), RootSampler); }, std::move(vals)); } else { return detail::incr::make_incremental_impl(x, std::move(placed), seed, cs); } } } /// \complexity O(n) time and O(n) key size. n is the domain bitlength (`depth`). The loop does two interior PRG expansions and writes one correction word per level, then builds one exterior leaf per output. template >> HEDLEY_WARN_UNUSED_RESULT auto make_dpf(InputT && x, root_sampler_t root_sampler, OutputTs && ...ys) { if constexpr (args_have_updatable_v) { auto bundle = detail::incr::rewrite_all<0>(std::forward(ys)...); auto keys = std::apply([&](auto && ...zs) { return make_dpf( std::forward(x), std::move(root_sampler), std::forward(zs)...); }, std::move(bundle.first)); detail::incr::assign_saved(keys.first, keys.second, bundle.second); return keys; } else { using input_type = std::decay_t; static_assert(!is_secret_share_v, "make_dpf: domain point must be plaintext (use raw() or reconstruct)"); static_assert((!is_secret_share_v> && ...), "make_dpf: payloads must be plaintext (use raw() or reconstruct)"); constexpr auto bitlen = utils::bitlength_of_v; constexpr std::size_t CD = forced_cmp_depth_v; constexpr std::size_t CB = forced_cmp_out_bits_v; constexpr bool CW = forced_cmp_wild_v; constexpr std::size_t BK = forced_cmp_block_v; constexpr bool ID = forced_cmp_idcf_v; constexpr bool V = args_have_verifiable_v; constexpr bool E = args_have_extractable_v; dcf_runtime_spec dcf_spec{}; bool has_cmp = false; auto placed = detail::incr::flatten_args_and_cmp(dcf_spec, has_cmp, std::forward(ys)...); using placed_tuple = decltype(placed); using node = typename ExteriorPRG::block_type; const dcf_runtime_spec * cs = has_cmp ? &dcf_spec : nullptr; if constexpr (std::tuple_size_v == 0) { if (!has_cmp) throw std::invalid_argument("make_dpf: no outputs"); return detail::incr::make_incremental_impl(x, std::move(placed), root_sampler, cs); } else if constexpr (!args_have_cmp_v && !args_have_eq_v && !args_have_verifiable_v && !args_have_extractable_v && detail::incr::is_classic_placed() && !detail::incr::placed_has_arith_beta()) { auto vals = detail::incr::classic_values(placed, std::make_index_sequence>{}); return std::apply( [&](auto && ...zs) { return dpf::make_dpf( dpf::make_dpfargs(x, std::forward(zs)...), std::move(root_sampler)); }, std::move(vals)); } else { return detail::incr::make_incremental_impl(x, std::move(placed), root_sampler, cs); } } } // --------------------------------------------------------------------------- // make_dpf_doerner_shelat(x0, x1, ...): classic or incremental // --------------------------------------------------------------------------- /// @brief Doerner–Shelat keygen with caller-supplied roots and pad stream. /// @details The point is `x0 XOR x1` (before the signed-MSB flip `make_dpf` applies). /// `rng.pad` is the Beaver randomness; it cancels and must not draw from /// `uniform_fill` when beaver coins are being matched to `make_dpf`. /// @tparam InteriorPRG PRG that expands interior nodes. Defaults to `dpf::prg::aes128` /// @tparam ExteriorPRG PRG that expands the root. Defaults to `InteriorPRG` /// @tparam RootSampler sampler for the Doerner–Shelat root seed /// @tparam PadRng pad stream for the Doerner–Shelat protocol /// @tparam InputT input domain type /// @tparam OutputTs output ts /// @param x0 the `x0` /// @param x1 the `x1` /// @param rng the Doerner–Shelat randomness tapes /// @param ys the `ys` /// @return Doerner–Shelat keygen with caller-supplied roots and pad stream /// @note Following Jack Doerner and abhi shelat, CCS 2017 (ePrint 2017/827): one correction word per level. The other `make_dpf_doerner_shelat` overloads are the same opening. /// \complexity O(n) time. One `ds_advance_level` per level: two PRG expansions and one `prepare_level`. n is `depth`. /// \rounds No sockets. This is the in-process transcript. A networked walk is `dpf::party::dist::point_party`. /// \communication none here. `local_cw_protocol` opens the correction word locally. /// \preprocessing Per level, `prepare_level` draws one `ds_cw_pads` (two parties × a 128-bit rand, a 128-bit gamma, and a bit) and two `ds_and_pads`. Arithmetic inputs also run a carry chain of n-1 bit-AND triples in `encode_walk_shares`. template >> HEDLEY_WARN_UNUSED_RESULT auto make_dpf_doerner_shelat(InputT x0, InputT x1, ds_randomness rng, OutputTs && ...ys) { return make_dpf_doerner_shelat( false, std::move(x0), std::move(x1), std::move(rng), std::forward(ys)...); } /// @brief Doerner–Shelat with additive shares: the point is `x0 + x1` in the input /// ring (unsigned wrap; signed MSB flipped after the carry chain). /// @tparam InteriorPRG PRG that expands interior nodes. Defaults to `dpf::prg::aes128` /// @tparam ExteriorPRG PRG that expands the root. Defaults to `InteriorPRG` /// @tparam RootSampler sampler for the Doerner–Shelat root seed /// @tparam PadRng pad stream for the Doerner–Shelat protocol /// @tparam InputT input domain type /// @tparam OutputTs output ts /// @param x0 the `x0` /// @param x1 the `x1` /// @param rng the Doerner–Shelat randomness tapes /// @param ys the `ys` /// @return Doerner–Shelat with additive shares: the point is `x0 + x1` in the input ring (unsigned /// wrap; signed MSB flipped after the carry chain) /// \complexity O(n) time. One `ds_advance_level` per level: two PRG expansions and one `prepare_level`. n is `depth`. /// \rounds No sockets. This is the in-process transcript. A networked walk is `dpf::party::dist::point_party`. /// \communication none here. `local_cw_protocol` opens the correction word locally. /// \preprocessing Per level, `prepare_level` draws one `ds_cw_pads` (two parties × a 128-bit rand, a 128-bit gamma, and a bit) and two `ds_and_pads`. Arithmetic inputs also run a carry chain of n-1 bit-AND triples in `encode_walk_shares`. template >> HEDLEY_WARN_UNUSED_RESULT auto make_dpf_doerner_shelat(arith_input_t, InputT x0, InputT x1, ds_randomness rng, OutputTs && ...ys) { return make_dpf_doerner_shelat( true, std::move(x0), std::move(x1), std::move(rng), std::forward(ys)...); } /// @brief XOR-index shares, additively shared payload `y0 + y1 = β` (single concrete). /// @tparam InteriorPRG PRG that expands interior nodes. Defaults to `dpf::prg::aes128` /// @tparam ExteriorPRG PRG that expands the root. Defaults to `InteriorPRG` /// @tparam RootSampler sampler for the Doerner–Shelat root seed /// @tparam PadRng pad stream for the Doerner–Shelat protocol /// @tparam InputT input domain type /// @tparam OutputT output type /// @tparam OutputT output type /// @param x0 the `x0` /// @param x1 the `x1` /// @param rng the Doerner–Shelat randomness tapes /// @param y0 the `y0` /// @param y1 the `y1` /// @return XOR-index shares, additively shared payload `y0 + y1 = β` (single concrete) /// \complexity O(n) time. One `ds_advance_level` per level: two PRG expansions and one `prepare_level`. n is `depth`. /// \rounds No sockets. This is the in-process transcript. A networked walk is `dpf::party::dist::point_party`. /// \communication none here. `local_cw_protocol` opens the correction word locally. /// \preprocessing Per level, `prepare_level` draws one `ds_cw_pads` (two parties × a 128-bit rand, a 128-bit gamma, and a bit) and two `ds_and_pads`. Arithmetic inputs also run a carry chain of n-1 bit-AND triples in `encode_walk_shares`. template && !is_at_v && !is_wildcard_v && no_ic_pack_v>> HEDLEY_WARN_UNUSED_RESULT auto make_dpf_doerner_shelat(arith_output_t, InputT x0, InputT x1, ds_randomness rng, OutputT y0, OutputT y1) { local_cw_protocol proto{rng.pad}; return detail::make_dpf_doerner_shelat_impl( false, true, std::move(x0), std::move(x1), rng.root, proto, std::move(y0), std::move(y1)); } /// @brief Additive index and additive payload shares (single concrete). /// @tparam InteriorPRG PRG that expands interior nodes. Defaults to `dpf::prg::aes128` /// @tparam ExteriorPRG PRG that expands the root. Defaults to `InteriorPRG` /// @tparam RootSampler sampler for the Doerner–Shelat root seed /// @tparam PadRng pad stream for the Doerner–Shelat protocol /// @tparam InputT input domain type /// @tparam OutputT output type /// @tparam OutputT output type /// @param x0 the `x0` /// @param x1 the `x1` /// @param rng the Doerner–Shelat randomness tapes /// @param y0 the `y0` /// @param y1 the `y1` /// @return Additive index and additive payload shares (single concrete) /// \complexity O(n) time. One `ds_advance_level` per level: two PRG expansions and one `prepare_level`. n is `depth`. /// \rounds No sockets. This is the in-process transcript. A networked walk is `dpf::party::dist::point_party`. /// \communication none here. `local_cw_protocol` opens the correction word locally. /// \preprocessing Per level, `prepare_level` draws one `ds_cw_pads` (two parties × a 128-bit rand, a 128-bit gamma, and a bit) and two `ds_and_pads`. Arithmetic inputs also run a carry chain of n-1 bit-AND triples in `encode_walk_shares`. template && !is_at_v && !is_wildcard_v && no_ic_pack_v>> HEDLEY_WARN_UNUSED_RESULT auto make_dpf_doerner_shelat(arith_input_t, arith_output_t, InputT x0, InputT x1, ds_randomness rng, OutputT y0, OutputT y1) { local_cw_protocol proto{rng.pad}; return detail::make_dpf_doerner_shelat_impl( true, true, std::move(x0), std::move(x1), rng.root, proto, std::move(y0), std::move(y1)); } /// @brief Shared payloads via `arith_beta` / packs (XOR index). /// @tparam InteriorPRG PRG that expands interior nodes. Defaults to `dpf::prg::aes128` /// @tparam ExteriorPRG PRG that expands the root. Defaults to `InteriorPRG` /// @tparam RootSampler sampler for the Doerner–Shelat root seed /// @tparam PadRng pad stream for the Doerner–Shelat protocol /// @tparam InputT input domain type /// @tparam OutputT output type /// @tparam OutputTs output ts /// @tparam OutputTs output ts /// @param x0 the `x0` /// @param x1 the `x1` /// @param rng the Doerner–Shelat randomness tapes /// @param y the `y` /// @param ys the `ys` /// @return Shared payloads via `arith_beta` / packs (XOR index) /// \complexity O(n) time. One `ds_advance_level` per level: two PRG expansions and one `prepare_level`. n is `depth`. /// \rounds No sockets. This is the in-process transcript. A networked walk is `dpf::party::dist::point_party`. /// \communication none here. `local_cw_protocol` opens the correction word locally. /// \preprocessing Per level, `prepare_level` draws one `ds_cw_pads` (two parties × a 128-bit rand, a 128-bit gamma, and a bit) and two `ds_and_pads`. Arithmetic inputs also run a carry chain of n-1 bit-AND triples in `encode_walk_shares`. template || is_at_v || (sizeof...(OutputTs) > 0)) && no_ic_pack_v>> HEDLEY_WARN_UNUSED_RESULT auto make_dpf_doerner_shelat(arith_output_t, InputT x0, InputT x1, ds_randomness rng, OutputT && y, OutputTs && ...ys) { return make_dpf_doerner_shelat( false, std::move(x0), std::move(x1), std::move(rng), std::forward(y), std::forward(ys)...); } /// @brief Shared payloads via `arith_beta` / packs (additive index). /// @tparam InteriorPRG PRG that expands interior nodes. Defaults to `dpf::prg::aes128` /// @tparam ExteriorPRG PRG that expands the root. Defaults to `InteriorPRG` /// @tparam RootSampler sampler for the Doerner–Shelat root seed /// @tparam PadRng pad stream for the Doerner–Shelat protocol /// @tparam InputT input domain type /// @tparam OutputT output type /// @tparam OutputTs output ts /// @tparam OutputTs output ts /// @param x0 the `x0` /// @param x1 the `x1` /// @param rng the Doerner–Shelat randomness tapes /// @param y the `y` /// @param ys the `ys` /// @return Shared payloads via `arith_beta` / packs (additive index) /// \complexity O(n) time. One `ds_advance_level` per level: two PRG expansions and one `prepare_level`. n is `depth`. /// \rounds No sockets. This is the in-process transcript. A networked walk is `dpf::party::dist::point_party`. /// \communication none here. `local_cw_protocol` opens the correction word locally. /// \preprocessing Per level, `prepare_level` draws one `ds_cw_pads` (two parties × a 128-bit rand, a 128-bit gamma, and a bit) and two `ds_and_pads`. Arithmetic inputs also run a carry chain of n-1 bit-AND triples in `encode_walk_shares`. template || is_at_v || (sizeof...(OutputTs) > 0)) && no_ic_pack_v>> HEDLEY_WARN_UNUSED_RESULT auto make_dpf_doerner_shelat(arith_input_t, arith_output_t, InputT x0, InputT x1, ds_randomness rng, OutputT && y, OutputTs && ...ys) { return make_dpf_doerner_shelat( true, std::move(x0), std::move(x1), std::move(rng), std::forward(y), std::forward(ys)...); } /// \complexity O(n) time. One `ds_advance_level` per level: two PRG expansions and one `prepare_level`. n is `depth`. /// \rounds No sockets. This is the in-process transcript. A networked walk is `dpf::party::dist::point_party`. /// \communication none here. `local_cw_protocol` opens the correction word locally. /// \preprocessing Per level, `prepare_level` draws one `ds_cw_pads` (two parties × a 128-bit rand, a 128-bit gamma, and a bit) and two `ds_and_pads`. Arithmetic inputs also run a carry chain of n-1 bit-AND triples in `encode_walk_shares`. template >> HEDLEY_WARN_UNUSED_RESULT auto make_dpf_doerner_shelat(bool arith, InputT x0, InputT x1, ds_randomness rng, OutputTs && ...ys) { using input_type = std::decay_t; static_assert(!is_secret_share_v, "Doerner–Shelat: use additive_share of xor_wrapper, or raw shares"); using node = typename ExteriorPRG::block_type; constexpr auto bitlen = utils::bitlength_of_v; constexpr std::size_t CD = forced_cmp_depth_v; constexpr std::size_t CB = forced_cmp_out_bits_v; constexpr bool CW = forced_cmp_wild_v; constexpr std::size_t BK = forced_cmp_block_v; constexpr bool ID = forced_cmp_idcf_v; constexpr bool V = args_have_verifiable_v; constexpr bool E = args_have_extractable_v; dcf_runtime_spec dcf_spec{}; bool has_cmp = false; auto placed = detail::incr::flatten_args_and_cmp(dcf_spec, has_cmp, std::forward(ys)...); using placed_tuple = decltype(placed); const dcf_runtime_spec * cs = has_cmp ? &dcf_spec : nullptr; if constexpr (std::tuple_size_v == 0) { if (!has_cmp) throw std::invalid_argument("make_dpf_doerner_shelat: no outputs"); local_cw_protocol proto{rng.pad}; return detail::incr::make_incremental_ds_impl(arith, std::move(x0), std::move(x1), rng.root, proto, std::move(placed), cs); } else if constexpr (!args_have_cmp_v && !args_have_eq_v && !args_have_verifiable_v && !args_have_extractable_v && detail::incr::is_classic_placed() && !detail::incr::placed_has_arith_beta()) { auto vals = detail::incr::classic_values(placed, std::make_index_sequence>{}); local_cw_protocol proto{rng.pad}; return std::apply( [&](auto && ...zs) { return detail::make_dpf_doerner_shelat_impl(arith, std::move(x0), std::move(x1), rng.root, proto, std::forward(zs)...); }, std::move(vals)); } else { local_cw_protocol proto{rng.pad}; return detail::incr::make_incremental_ds_impl(arith, std::move(x0), std::move(x1), rng.root, proto, std::move(placed), cs); } } /// @brief Doerner–Shelat with an injectable `CwProtocol` (local or MPC backend). /// @details Signature is `make_dpf_doerner_shelat(x0, x1, root_sampler, proto, y...)` /// so it does not collide with the `ds_randomness` or bare-output overloads. /// @tparam InteriorPRG PRG that expands interior nodes. Defaults to `dpf::prg::aes128` /// @tparam ExteriorPRG PRG that expands the root. Defaults to `InteriorPRG` /// @tparam RootSampler sampler for the Doerner–Shelat root seed /// @tparam CwProtocol correction-word protocol /// @tparam InputT input domain type /// @tparam OutputT output type /// @tparam OutputTs output ts /// @tparam OutputTs output ts /// @param x0 the `x0` /// @param x1 the `x1` /// @param root_sampler the `root_sampler` /// @param proto the `proto` /// @param y the `y` /// @param ys the `ys` /// @return Doerner–Shelat with an injectable `CwProtocol` (local or MPC backend) /// \complexity O(n) time. One `ds_advance_level` per level: two PRG expansions and one `prepare_level`. n is `depth`. /// \rounds No sockets. This is the in-process transcript. A networked walk is `dpf::party::dist::point_party`. /// \communication none here. `local_cw_protocol` opens the correction word locally. /// \preprocessing Per level, `prepare_level` draws one `ds_cw_pads` (two parties × a 128-bit rand, a 128-bit gamma, and a bit) and two `ds_and_pads`. Arithmetic inputs also run a carry chain of n-1 bit-AND triples in `encode_walk_shares`. template >::value && no_ic_pack_v>> HEDLEY_WARN_UNUSED_RESULT auto make_dpf_doerner_shelat(InputT x0, InputT x1, RootSampler root_sampler, CwProtocol & proto, OutputT && y, OutputTs && ...ys) { return make_dpf_doerner_shelat( false, std::move(x0), std::move(x1), std::move(root_sampler), proto, std::forward(y), std::forward(ys)...); } /// \complexity O(n) time. One `ds_advance_level` per level: two PRG expansions and one `prepare_level`. n is `depth`. /// \rounds No sockets. This is the in-process transcript. A networked walk is `dpf::party::dist::point_party`. /// \communication none here. `local_cw_protocol` opens the correction word locally. /// \preprocessing Per level, `prepare_level` draws one `ds_cw_pads` (two parties × a 128-bit rand, a 128-bit gamma, and a bit) and two `ds_and_pads`. Arithmetic inputs also run a carry chain of n-1 bit-AND triples in `encode_walk_shares`. template >::value && no_ic_pack_v>> HEDLEY_WARN_UNUSED_RESULT auto make_dpf_doerner_shelat(arith_input_t, InputT x0, InputT x1, RootSampler root_sampler, CwProtocol & proto, OutputT && y, OutputTs && ...ys) { return make_dpf_doerner_shelat( true, std::move(x0), std::move(x1), std::move(root_sampler), proto, std::forward(y), std::forward(ys)...); } /// \complexity O(n) time. One `ds_advance_level` per level: two PRG expansions and one `prepare_level`. n is `depth`. /// \rounds No sockets. This is the in-process transcript. A networked walk is `dpf::party::dist::point_party`. /// \communication none here. `local_cw_protocol` opens the correction word locally. /// \preprocessing Per level, `prepare_level` draws one `ds_cw_pads` (two parties × a 128-bit rand, a 128-bit gamma, and a bit) and two `ds_and_pads`. Arithmetic inputs also run a carry chain of n-1 bit-AND triples in `encode_walk_shares`. template >::value && no_ic_pack_v>> HEDLEY_WARN_UNUSED_RESULT auto make_dpf_doerner_shelat(bool arith, InputT x0, InputT x1, RootSampler root_sampler, CwProtocol & proto, OutputT && y, OutputTs && ...ys) { using input_type = std::decay_t; using node = typename ExteriorPRG::block_type; constexpr auto bitlen = utils::bitlength_of_v; constexpr std::size_t CD = forced_cmp_depth_v; constexpr std::size_t CB = forced_cmp_out_bits_v; constexpr bool CW = forced_cmp_wild_v; constexpr std::size_t BK = forced_cmp_block_v; constexpr bool ID = forced_cmp_idcf_v; constexpr bool V = args_have_verifiable_v; constexpr bool E = args_have_extractable_v; dcf_runtime_spec dcf_spec{}; bool has_cmp = false; auto placed = detail::incr::flatten_args_and_cmp(dcf_spec, has_cmp, std::forward(y), std::forward(ys)...); using placed_tuple = decltype(placed); const dcf_runtime_spec * cs = has_cmp ? &dcf_spec : nullptr; if constexpr (std::tuple_size_v == 0) { if (!has_cmp) throw std::invalid_argument("make_dpf_doerner_shelat: no outputs"); return detail::incr::make_incremental_ds_impl(arith, std::move(x0), std::move(x1), root_sampler, proto, std::move(placed), cs); } else if constexpr (!args_have_cmp_v && !args_have_verifiable_v && !args_have_extractable_v && !args_have_eq_v && detail::incr::is_classic_placed() && !detail::incr::placed_has_arith_beta()) { auto vals = detail::incr::classic_values(placed, std::make_index_sequence>{}); return std::apply( [&](auto && ...zs) { return detail::make_dpf_doerner_shelat_impl(arith, std::move(x0), std::move(x1), root_sampler, proto, std::forward(zs)...); }, std::move(vals)); } else { return detail::incr::make_incremental_ds_impl(arith, std::move(x0), std::move(x1), root_sampler, proto, std::move(placed), cs); } } /// @brief Doerner–Shelat keygen. Roots and pads come from `uniform_sample`. /// @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 OutputTs output ts /// @tparam OutputTs output ts /// @param x0 the `x0` /// @param x1 the `x1` /// @param y the `y` /// @param ys the `ys` /// @return Doerner–Shelat keygen /// \complexity O(n) time. One `ds_advance_level` per level: two PRG expansions and one `prepare_level`. n is `depth`. /// \rounds No sockets. This is the in-process transcript. A networked walk is `dpf::party::dist::point_party`. /// \communication none here. `local_cw_protocol` opens the correction word locally. /// \preprocessing Per level, `prepare_level` draws one `ds_cw_pads` (two parties × a 128-bit rand, a 128-bit gamma, and a bit) and two `ds_and_pads`. Arithmetic inputs also run a carry chain of n-1 bit-AND triples in `encode_walk_shares`. template >::value && !detail::is_cw_protocol>::value && !detail::first_is_cw_protocol::value && no_ic_pack_v>> HEDLEY_WARN_UNUSED_RESULT auto make_dpf_doerner_shelat(InputT x0, InputT x1, OutputT && y, OutputTs && ...ys) { using block = typename InteriorPRG::block_type; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") ds_randomness rng{ dpf::uniform_sample, {}}; HEDLEY_PRAGMA(GCC diagnostic pop) return make_dpf_doerner_shelat( std::move(x0), std::move(x1), rng, std::forward(y), std::forward(ys)...); } /// \complexity O(n) time. One `ds_advance_level` per level: two PRG expansions and one `prepare_level`. n is `depth`. /// \rounds No sockets. This is the in-process transcript. A networked walk is `dpf::party::dist::point_party`. /// \communication none here. `local_cw_protocol` opens the correction word locally. /// \preprocessing Per level, `prepare_level` draws one `ds_cw_pads` (two parties × a 128-bit rand, a 128-bit gamma, and a bit) and two `ds_and_pads`. Arithmetic inputs also run a carry chain of n-1 bit-AND triples in `encode_walk_shares`. template >::value && !detail::is_cw_protocol>::value && !detail::first_is_cw_protocol::value && no_ic_pack_v>> HEDLEY_WARN_UNUSED_RESULT auto make_dpf_doerner_shelat(arith_input_t, InputT x0, InputT x1, OutputT && y, OutputTs && ...ys) { using block = typename InteriorPRG::block_type; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") ds_randomness rng{ dpf::uniform_sample, {}}; HEDLEY_PRAGMA(GCC diagnostic pop) return make_dpf_doerner_shelat( arith_input, std::move(x0), std::move(x1), rng, std::forward(y), std::forward(ys)...); } /// @brief XOR-index, additive payload shares (urandom roots/pads). /// @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 OutputT output type /// @param x0 the `x0` /// @param x1 the `x1` /// @param y0 the `y0` /// @param y1 the `y1` /// @return XOR-index, additive payload shares (urandom roots/pads) /// \complexity O(n) time. One `ds_advance_level` per level: two PRG expansions and one `prepare_level`. n is `depth`. /// \rounds No sockets. This is the in-process transcript. A networked walk is `dpf::party::dist::point_party`. /// \communication none here. `local_cw_protocol` opens the correction word locally. /// \preprocessing Per level, `prepare_level` draws one `ds_cw_pads` (two parties × a 128-bit rand, a 128-bit gamma, and a bit) and two `ds_and_pads`. Arithmetic inputs also run a carry chain of n-1 bit-AND triples in `encode_walk_shares`. template && !is_at_v && !is_wildcard_v>> HEDLEY_WARN_UNUSED_RESULT auto make_dpf_doerner_shelat(arith_output_t, InputT x0, InputT x1, OutputT y0, OutputT y1) { using block = typename InteriorPRG::block_type; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") ds_randomness rng{ dpf::uniform_sample, {}}; HEDLEY_PRAGMA(GCC diagnostic pop) return make_dpf_doerner_shelat( arith_output, std::move(x0), std::move(x1), rng, std::move(y0), std::move(y1)); } /// @brief Additive index and additive payload (urandom roots/pads). /// @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 OutputT output type /// @param x0 the `x0` /// @param x1 the `x1` /// @param y0 the `y0` /// @param y1 the `y1` /// @return Additive index and additive payload (urandom roots/pads) /// \complexity O(n) time. One `ds_advance_level` per level: two PRG expansions and one `prepare_level`. n is `depth`. /// \rounds No sockets. This is the in-process transcript. A networked walk is `dpf::party::dist::point_party`. /// \communication none here. `local_cw_protocol` opens the correction word locally. /// \preprocessing Per level, `prepare_level` draws one `ds_cw_pads` (two parties × a 128-bit rand, a 128-bit gamma, and a bit) and two `ds_and_pads`. Arithmetic inputs also run a carry chain of n-1 bit-AND triples in `encode_walk_shares`. template && !is_at_v && !is_wildcard_v>> HEDLEY_WARN_UNUSED_RESULT auto make_dpf_doerner_shelat(arith_input_t, arith_output_t, InputT x0, InputT x1, OutputT y0, OutputT y1) { using block = typename InteriorPRG::block_type; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") ds_randomness rng{ dpf::uniform_sample, {}}; HEDLEY_PRAGMA(GCC diagnostic pop) return make_dpf_doerner_shelat( arith_input, arith_output, std::move(x0), std::move(x1), rng, std::move(y0), std::move(y1)); } /// @brief Doerner–Shelat from party-tagged additive XOR shares of the point. /// @tparam InteriorPRG PRG that expands interior nodes. Defaults to `dpf::prg::aes128` /// @tparam ExteriorPRG PRG that expands the root. Defaults to `InteriorPRG` /// @tparam U rebound value type /// @tparam Args args /// @param x0 the `x0` /// @param x1 the `x1` /// @param args the arguments forwarded to the constructor /// @return Doerner–Shelat from party-tagged additive XOR shares of the point /// \complexity O(n) time. One `ds_advance_level` per level: two PRG expansions and one `prepare_level`. n is `depth`. /// \rounds No sockets. This is the in-process transcript. A networked walk is `dpf::party::dist::point_party`. /// \communication none here. `local_cw_protocol` opens the correction word locally. /// \preprocessing Per level, `prepare_level` draws one `ds_cw_pads` (two parties × a 128-bit rand, a 128-bit gamma, and a bit) and two `ds_and_pads`. Arithmetic inputs also run a carry chain of n-1 bit-AND triples in `encode_walk_shares`. template HEDLEY_WARN_UNUSED_RESULT auto make_dpf_doerner_shelat( const additive_share, 0> & x0, const additive_share, 1> & x1, Args && ...args) { return make_dpf_doerner_shelat( x0.raw(), x1.raw(), std::forward(args)...); } // --------------------------------------------------------------------------- // Wildcard comparison payload assignment (cw-protocol item 4) // --------------------------------------------------------------------------- namespace detail { namespace incr { /// @brief Resolve the constant absorb target for a comparison payload δ = if_true − /// if_false, matching the branch logic used at keygen. /// @param ch the `ch` /// @param delta the payload difference `if_true - if_false` /// @param false_value the `false_value` /// @return Resolve the constant absorb target for a comparison payload δ = if_true − if_false, /// matching the branch logic used at keygen HEDLEY_NO_THROW inline uint64_t cmp_assign_target(const detail::cmp_meta & ch, uint64_t delta, uint64_t false_value) noexcept { const uint64_t mask = ch.mask; if (ch.trivial == cmp_trivial::always_true) return (delta + false_value) & mask; if (ch.trivial == cmp_trivial::always_false) return false_value & mask; if (ch.eval_as_ge) return (delta + false_value) & mask; return false_value & mask; } } // namespace incr } // namespace detail /// @brief Assign a previously-wildcard comparison payload on a *pair* of keys. /// /// The key was generated with `dpf::lt(dpf::wildcard{}, ...)` (or any /// `leq`/`gt`/`geq`), which opened the value CWs / `cw_last` for δ = 0 and /// stashed the per-level δ-coefficients. This patches both keys' (public, /// identical) value CWs in place — `value_cw[i] += coeff[i]·δ`, /// `cw_last += coeff_last·δ` — and splits the constant absorb into fresh /// additive `cmp_addend` shares. No tree re-walk and no re-PRG. /// @tparam KeyT key type /// @tparam Beta payload type /// @param key0 the `key0` /// @param key1 the `key1` /// @param if_true the payload on a true comparison /// @param if_false the payload on a false comparison template void assign_cmp(KeyT & key0, KeyT & key1, const Beta & if_true, const Beta & if_false = Beta{}) { static_assert(KeyT::cmp_is_wildcard, "assign_cmp: key's comparison payload is not a wildcard"); if (!key0.has_cmp() || !key1.has_cmp()) throw std::invalid_argument("assign_cmp: key has no comparison channel"); const auto & ch = key0.cmp(); using Concrete = dpf::concrete_type_t; if constexpr (detail::has_from_seed::value) { const Concrete delta = if_true - if_false; Concrete target = if_false; if (ch.trivial == cmp_trivial::always_true || ch.eval_as_ge) target = delta + if_false; using prg = typename KeyT::interior_prg; const auto seed = dpf::uniform_sample(); const Concrete blind = Concrete::from_seed(&seed, sizeof(seed)); const Concrete share0 = blind; const Concrete share1 = target + (-blind); const auto ops = detail::make_payload_ops(); auto mix = [&](auto base_w, auto coeff_w) { unsigned char base[detail::payload_ops::cap]{}; unsigned char del[detail::payload_ops::cap]{}; unsigned char scaled[detail::payload_ops::cap]{}; unsigned char outb[detail::payload_ops::cap]{}; const Concrete base_e = detail::payload_from_word(base_w); std::memcpy(base, &base_e, sizeof(base_e)); std::memcpy(del, &delta, sizeof(delta)); ops.scale(scaled, del, detail::payload_coeff_of(coeff_w)); ops.add(outb, base, scaled); return detail::payload_to_word( outb, sizeof(Concrete)); }; key0.assign_cmp_payload(mix, detail::payload_to_word( reinterpret_cast(&share0), sizeof(share0))); key1.assign_cmp_payload(mix, detail::payload_to_word( reinterpret_cast(&share1), sizeof(share1))); return; } else if constexpr (detail::cmp_group_info::custom) { const auto layout = detail::group_layout(); const auto delta = detail::group_sub( detail::group_from_beta(if_true), detail::group_from_beta(if_false)); const auto false_value = detail::group_from_beta(if_false); detail::group_elem target = false_value; if (ch.trivial == cmp_trivial::always_true || ch.eval_as_ge) target = detail::group_add(delta, false_value); using prg = typename KeyT::interior_prg; const auto blind = detail::group_from_node( dpf::uniform_sample(), layout); const auto add0 = detail::group_to_word(blind); const auto add1 = detail::group_to_word( detail::group_sub(target, blind)); key0.assign_cmp_group(delta, add0); key1.assign_cmp_group(delta, add1); return; } const uint64_t mask = ch.mask; const uint64_t delta = detail::dcf_impl::beta_delta_u64(if_true, if_false, mask); const uint64_t false_value = detail::dcf_impl::beta_to_u64_simple(if_false, mask); const uint64_t target = detail::incr::cmp_assign_target(ch, delta, false_value); // Fresh additive split of the absorb target. Any blind works for // reconstruction; draw one so neither share reveals the payload. uint64_t add0 = 0, add1 = 0; const uint64_t r = detail::dcf_impl::sample_addend_blind(mask, [] { return dpf::uniform_sample(); }); detail::incr::split_cmp_addend(target, mask, r, add0, add1); key0.assign_cmp_delta(delta, add0); key1.assign_cmp_delta(delta, add1); } /// @brief Party-tagged overload: `make_dpf` returns distinct `party_key<0>` / /// `party_key<1>` types, so the same-type pair overload cannot bind both. /// @tparam Key key type /// @tparam Beta payload type /// @param key0 the `key0` /// @param key1 the `key1` /// @param if_true the payload on a true comparison /// @param if_false the payload on a false comparison template void assign_cmp(party_key<0, Key> & key0, party_key<1, Key> & key1, const Beta & if_true, const Beta & if_false = Beta{}) { assign_cmp(key0.key(), key1.key(), if_true, if_false); } /// @brief Party-local variant: patch one key with a *public* (already-opened) δ and a /// caller-supplied `cmp_addend` share. Both parties must call this with the /// same δ (so the public value CWs stay identical) and additive shares of the /// absorb target that reconstruct to `cmp_assign_target(cmp(), δ, if_false)`. /// @tparam KeyT key type /// @param key the `key` /// @param delta the payload difference `if_true - if_false` /// @param addend_share the `addend_share` /// @throws std::invalid_argument if `key has no comparison channel` template void assign_cmp_local(KeyT & key, uint64_t delta, uint64_t addend_share) { static_assert(KeyT::cmp_is_wildcard, "assign_cmp_local: key's comparison payload is not a wildcard"); if (!key.has_cmp()) throw std::invalid_argument( "assign_cmp_local: key has no comparison channel"); key.assign_cmp_delta(delta, addend_share); } /// @brief Share-typed overload: subtractive shares are converted with the party /// coefficient before the existing additive leaf absorb math. /// @tparam KeyT key type /// @tparam T value type /// @tparam Party party index, `0` or `1` /// @tparam Scheme scheme /// @param key the `key` /// @param delta the payload difference `if_true - if_false` /// @param addend_share the `addend_share` template void assign_cmp_local(KeyT & key, uint64_t delta, const secret_share & addend_share) { if constexpr (is_party_key_v) { static_assert(party_of_v == Party, "assign_cmp_local: share party must match party_key"); } // Comparison absorb is a (2,2) additive share. A (3,3) component and a // replicated share are not addends of a two-party comparison key. if constexpr (is_two_party_sharing_v) { const auto additive = addend_share.as_additive(); assign_cmp_local(key, delta, static_cast(additive.raw())); } else { static_assert(is_two_party_sharing_v, "assign_cmp_local: comparison addend is a (2,2) share"); } } // --------------------------------------------------------------------------- // Point / interval / sequence / cmp eval (detail + public sugar) // --------------------------------------------------------------------------- namespace internal { template inline void eval_to_level(const DpfKey & dpf, InputT && x, PathMemoizer && path, std::size_t to_level) { detail::ensure_level(dpf, x, path, to_level); } } // namespace internal namespace detail { namespace incr { /// @brief Evaluate output slot `I` of an incremental key at the programmed point. /// @details `N` must match the prefix of that slot (`at` or full input width). /// @tparam N width in bits /// @tparam I output index /// @tparam KeyT key type /// @tparam QueryT query type /// @tparam PathMemoizer path memoizer type /// @param dpf the DPF key /// @brief Party 0 absorbs a nonzero public `eq`/`eq_at` if_false addend. /// @details Leaf shares open as y0 − y1 (XOR groups: y0 ⊕ y1). Wildcard slots /// carry no comparable public addend and are skipped (their exterior walk /// already throws if unassigned). template constexpr bool party0_absorbs_public_addend(const KeyT & dpf) noexcept { using output_type = typename KeyT::template concrete_output_type; if constexpr (dpf::is_wildcard_v>) return false; else { const auto & add = std::get(dpf.public_addends); return add != output_type{} && !dpf::get_lo_bit(dpf.root()); } } /// @brief Fold `public_addends[I]` into one lane of an exterior leaf (point eval). template void absorb_public_addend_lane(const KeyT & dpf, LeafT & leaf, LaneT lane_x) { using output_type = typename KeyT::template concrete_output_type; if (!party0_absorbs_public_addend(dpf)) return; using exterior_node = typename KeyT::exterior_node; const auto & add = std::get(dpf.public_addends); auto addon = dpf::make_naked_leaf(lane_x, add); leaf = dpf::add_leaf(leaf, addon); } /// @brief Fold `public_addends[I]` into every packing lane of an exterior leaf. /// @details Interval / full / IP exterior walks materialise whole leaves; the /// constant if_false must land on every lane so each reconstructed output /// picks up the absorb (on-point: δ + if_false = if_true). template void absorb_public_addend_all_lanes(const KeyT & dpf, LeafT & leaf) { using output_type = typename KeyT::template concrete_output_type; if (!party0_absorbs_public_addend(dpf)) return; using exterior_node = typename KeyT::exterior_node; using input_type = typename KeyT::input_type; constexpr auto opl = KeyT::template outputs_per_leaf_of; const auto & add = std::get(dpf.public_addends); LeafT addon{}; for (std::size_t i = 0; i < opl; ++i) { addon = dpf::add_leaf(addon, dpf::make_naked_leaf(static_cast(i), add)); } leaf = dpf::add_leaf(leaf, addon); } /// @param dpf the DPF key /// @param x the query /// @param path the root-to-leaf path /// @return the evaluation result template > auto eval_out_point_impl( const KeyT & dpf, QueryT && x, PathMemoizer && path = PathMemoizer{}) { using key_type = KeyT; static_assert(key_type::meta[I].prefix == N, "out point eval: N does not match the prefix of output I"); using output_type = typename key_type::template concrete_output_type; // `traverse_exterior` is `noexcept`; an unassigned wildcard leaf would // otherwise `std::terminate` there. Fail loudly (matching the classic // `eval_point` path) before touching the leaf. if constexpr (dpf::is_wildcard_v>) dpf::assert_not_wildcard_output(dpf); auto tx = dpf.offset_x(x); utils::flip_msb_if_signed_integral(tx); constexpr auto level = key_type::meta[I].tree_level; internal::eval_to_level(dpf, tx, path, level); auto node = dpf.template traverse_exterior(path[level]); auto lane_x = detail::incr::lane_input(tx, N, key_type::input_bits); absorb_public_addend_lane(dpf, node, lane_x); return make_eval_dpf_output(node, lane_x); } } // namespace incr } // namespace detail /// @brief Evaluate the first deepest-prefix output (plan default for `eval_point`). /// @tparam KeyT key type /// @tparam QueryT query type /// @tparam PathMemoizer path memoizer type /// @tparam KeyT key type /// @param dpf the DPF key /// @param x the `x` /// @param path the root-to-leaf path /// @return the evaluation result /// \complexity O(n) time. n is `depth`. One interior traversal per level from the memoizer resume index through the leaf. Extra space is the path memoizer (O(n) nodes, or one node if it does not memoize). A proof token adds one fold per level walked. template , std::enable_if_t, bool> = true> auto eval_point( const KeyT & dpf, QueryT && x, PathMemoizer && path = PathMemoizer{}) { using key_type = KeyT; constexpr auto I = key_type::deepest_output; return detail::incr::eval_out_point_impl(dpf, std::forward(x), std::forward(path)); } namespace detail { namespace incr { // --------------------------------------------------------------------------- // Incremental interval / full / sequence (per-slot stop level + pos_base) // --------------------------------------------------------------------------- } // namespace incr } // namespace detail /// @brief Per-slot buffer: `num_leaf_nodes * outputs_per_leaf_of`. /// @tparam I output index /// @tparam KeyT key type /// @param num_leaf_nodes the `num_leaf_nodes` /// @return Per-slot buffer: `num_leaf_nodes * outputs_per_leaf_of` template auto make_output_buffer_for(const KeyT &, std::size_t num_leaf_nodes) { using output_type = typename KeyT::template concrete_output_type; using buffer_elem = leaf_buffer_elem_t; constexpr auto opl = KeyT::template outputs_per_leaf_of; return dpf::output_buffer(num_leaf_nodes * opl); } namespace detail { namespace incr { /// @brief Buffer sized for lane-domain interval `[from, to]` of output `I` at prefix `N`. /// @tparam N width in bits /// @tparam I output index /// @tparam KeyT key type /// @tparam LaneT lane type /// @param key the `key` /// @param from the inclusive start of the range /// @param to the `to` /// @return Buffer sized for lane-domain interval `[from, to]` of output `I` at prefix `N` template auto make_output_buffer_for_out_interval_impl(const KeyT & key, LaneT from, LaneT to) { (void)key; static_assert(KeyT::meta[I].prefix == N, "interval buffer: prefix mismatch"); using output_type = typename KeyT::template concrete_output_type; using buffer_elem = leaf_buffer_elem_t; constexpr auto opl = KeyT::template outputs_per_leaf_of; using integral_type = typename KeyT::integral_type; constexpr auto to_int = utils::to_integral_type{}; utils::flip_msb_if_signed_integral(from); utils::flip_msb_if_signed_integral(to); constexpr auto lg = KeyT::template lg_outputs_per_leaf_of; const auto from_i = static_cast(to_int(from)); const auto to_i = static_cast(to_int(to)); const integral_type from_node = utils::leaf_node_floor(from_i, lg); const integral_type to_node = utils::leaf_node_ceil_exclusive(to_i, lg); const bool wraps = utils::interval_wraps(from_i, to_i, N); const auto segs = utils::split_leaf_nodes(from_node, to_node, KeyT::meta[I].tree_level, wraps); return dpf::output_buffer(segs.total * opl); } namespace internal { template void eval_out_interval_interior(const DpfKey & dpf, IntegralT from_node, IntegralT to_node, IntervalMemoizer & memoizer) { using dpf_type = DpfKey; using node_type = typename DpfKey::interior_node; constexpr auto lg_opl = dpf_type::template lg_outputs_per_leaf_of; constexpr auto to_level = dpf_type::meta[I].tree_level; static_assert(dpf_type::meta[I].prefix == N, "out interval eval: prefix"); // Lane MSB for the N-bit subdomain (same role as key.msb_mask for classic). using input_type = typename dpf_type::input_type; const input_type lane_msb = static_cast(input_type{1} << (N - 1)); std::size_t level_index = memoizer.assign_interval(dpf, from_node, to_node); std::size_t nodes_at_level = memoizer.get_nodes_at_level(); IntegralT mask = static_cast( utils::to_integral_type{}(lane_msb) >> (level_index - 1 + lg_opl)); for (; level_index <= to_level; level_index = memoizer.advance_level(), nodes_at_level = memoizer.get_nodes_at_level(), mask >>= 1) { std::size_t i = 0, j = 0; bool from_offset = mask & from_node, to_offset = from_offset ^ (nodes_at_level & 1); const node_type cw[2] = { dpf.correction_word(level_index - 1, 0), dpf.correction_word(level_index - 1, 1)}; const bool is_last = dpf_type::tree::is_last_level(level_index - 1, dpf_type::depth); auto *prev = memoizer[level_index - 1]; auto *curr = memoizer[level_index]; if (from_offset == true) { curr[i++] = dpf_type::traverse_interior(prev[j++], cw[1], 1, is_last); } const std::size_t both_end = nodes_at_level - to_offset; while (i + 8 <= both_end) { alignas(node_type) node_type parents[4]; alignas(node_type) node_type left[4]; alignas(node_type) node_type right[4]; DPF_UNROLL_LOOP for (std::size_t t = 0; t < 4; ++t) parents[t] = prev[j + t]; dpf_type::traverse_interior01_x4(parents, cw[0], cw[1], left, right, is_last); DPF_UNROLL_LOOP for (std::size_t t = 0; t < 4; ++t) { curr[i + 2 * t] = left[t]; curr[i + 2 * t + 1] = right[t]; } i += 8; j += 4; } DPF_UNROLL_LOOP for (; i < both_end;) { auto cur_node = prev[j++]; auto kids = dpf_type::traverse_interior01(cur_node, cw[0], cw[1], is_last); curr[i++] = kids[0]; curr[i++] = kids[1]; } if (to_offset == true) { curr[i] = dpf_type::traverse_interior(prev[j], cw[0], 0, is_last); } } } template void eval_out_interval_exterior(const DpfKey & dpf, IntegralT from_node, IntegralT to_node, OutputBuffer && outbuf, IntervalMemoizer && memoizer, std::size_t start = 0) { using dpf_type = DpfKey; using output_type = typename dpf_type::template concrete_output_type; constexpr auto opl = dpf_type::template outputs_per_leaf_of; constexpr auto to_level = dpf_type::meta[I].tree_level; if (HEDLEY_UNLIKELY(to_node < from_node && to_node != IntegralT{0})) throw std::runtime_error("to_node(to_node - from_node); auto *nodes = memoizer[to_level]; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") DPF_UNROLL_LOOP for (std::size_t j = 0, k = start; j < nodes_in_interval; ++j, ++k) { auto leaf = dpf.template traverse_exterior(nodes[j]); absorb_public_addend_all_lanes(dpf, leaf); if constexpr (utils::is_packed_subbyte_v) { store_leaf_bytes(outbuf, k, leaf); } else { utils::raw_memcpy(&outbuf[k * opl], &leaf, sizeof(output_type) * opl); } } HEDLEY_PRAGMA(GCC diagnostic pop) } } // namespace internal /// @brief Evaluate output `I` over an interval in the N-bit lane subdomain. /// @details `from`/`to` are lane values in `[0, 2^N)` (not the full input domain). /// @tparam N width in bits /// @tparam I output index /// @tparam KeyT key type /// @tparam LaneT lane type /// @tparam OutputBuffer output buffer type /// @tparam IntervalMemoizer interval memoizer type /// @param dpf the DPF key /// @param from the inclusive start of the range /// @param to the `to` /// @param outbuf the `outbuf` /// @param memoizer the memoizer built for this key /// @return the evaluation result template auto eval_out_interval_impl( const KeyT & dpf, LaneT from, LaneT to, OutputBuffer && outbuf, IntervalMemoizer && memoizer) { using key_type = KeyT; static_assert(key_type::meta[I].prefix == N, "out interval eval: N does not match output I"); using integral_type = typename key_type::integral_type; constexpr auto opl = key_type::template outputs_per_leaf_of; constexpr auto lg_opl = key_type::template lg_outputs_per_leaf_of; constexpr auto to_int = utils::to_integral_type{}; utils::flip_msb_if_signed_integral(from); utils::flip_msb_if_signed_integral(to); const auto from_i = static_cast(to_int(from)); const auto to_i = static_cast(to_int(to)); integral_type from_node = utils::leaf_node_floor(from_i, lg_opl); integral_type to_node = utils::leaf_node_ceil_exclusive(to_i, lg_opl); const bool wraps = utils::interval_wraps(from_i, to_i, N); const auto segs = utils::split_leaf_nodes(from_node, to_node, key_type::meta[I].tree_level, wraps); std::size_t start = 0; for (std::size_t s = 0; s < segs.n; ++s) { const auto & seg = segs.seg[s]; internal::eval_out_interval_interior(dpf, seg.from_node, seg.to_node, memoizer); internal::eval_out_interval_exterior(dpf, seg.from_node, seg.to_node, outbuf, memoizer, start); start += seg.count; } constexpr auto mod_pow_2 = utils::mod_pow_2{}; auto from_bits = to_int(from); auto span = to_int(to) - from_bits; if constexpr (N < utils::bitlength_of_v) span &= (decltype(span){1} << N) - 1; auto from_sz = static_cast(from_bits); auto to_sz = from_sz + static_cast(span); return subinterval_iterable(std::begin(outbuf), utils::size(outbuf), from_sz, to_sz, mod_pow_2(from, lg_opl), opl); } template auto eval_out_interval_impl( const KeyT & dpf, LaneT from, LaneT to, OutputBuffer && outbuf) { using key_type = KeyT; constexpr auto L = key_type::meta[I].tree_level; using integral_type = typename key_type::integral_type; constexpr auto to_int = utils::to_integral_type{}; LaneT f = from, t = to; utils::flip_msb_if_signed_integral(f); utils::flip_msb_if_signed_integral(t); constexpr auto lg = key_type::template lg_outputs_per_leaf_of; const auto from_i = static_cast(to_int(f)); const auto to_i = static_cast(to_int(t)); const integral_type from_node = utils::leaf_node_floor(from_i, lg); const integral_type to_node = utils::leaf_node_ceil_exclusive(to_i, lg); const bool wraps = utils::interval_wraps(from_i, to_i, N); const auto segs = utils::split_leaf_nodes(from_node, to_node, L, wraps); HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") auto memo = basic_interval_memoizer_at(segs.total); HEDLEY_PRAGMA(GCC diagnostic pop) return eval_out_interval_impl(dpf, from, to, outbuf, memo); } template auto eval_out_interval_impl( const KeyT & dpf, LaneT from, LaneT to) { auto buf = make_output_buffer_for_out_interval_impl(dpf, from, to); auto it = eval_out_interval_impl(dpf, from, to, buf); return std::make_pair(std::move(buf), std::move(it)); } /// @brief Full N-bit lane-domain eval of output `I`. /// @tparam N width in bits /// @tparam I output index /// @tparam KeyT key type /// @tparam OutputBuffer output buffer type /// @tparam IntervalMemoizer interval memoizer type /// @param dpf the DPF key /// @param outbuf the `outbuf` /// @param memoizer the memoizer built for this key /// @return Full N-bit lane-domain eval of output `I` template auto eval_out_full_impl( const KeyT & dpf, OutputBuffer && outbuf, IntervalMemoizer && memoizer) { using lane_t = typename KeyT::input_type; constexpr lane_t lo = 0; constexpr lane_t hi = (N >= utils::bitlength_of_v) ? static_cast(~lane_t{0}) : static_cast((lane_t{1} << N) - 1); return eval_out_interval_impl(dpf, lo, hi, outbuf, memoizer); } template auto eval_out_full_impl( const KeyT & dpf) { using lane_t = typename KeyT::input_type; constexpr lane_t lo = 0; constexpr lane_t hi = (N >= utils::bitlength_of_v) ? static_cast(~lane_t{0}) : static_cast((lane_t{1} << N) - 1); return eval_out_interval_impl(dpf, lo, hi); } } // namespace incr } // namespace detail /// @brief Deepest-group full-domain eval (first cut of incremental `eval_full`). /// @tparam KeyT key type /// @tparam KeyT key type /// @param dpf the DPF key /// @return Deepest-group full-domain eval (first cut of incremental `eval_full`) /// \complexity Same expansion as `eval_interval` on the whole domain. L = 2^{n - lg(outputs_per_leaf)} leaf nodes, n = `depth`. Time Θ(L) interior traversals. The output buffer stores one slot per domain point (2^n). template , bool> = true> auto eval_full( const KeyT & dpf) { using key_type = KeyT; constexpr auto I = key_type::deepest_output; return detail::incr::eval_out_full_impl(dpf); } namespace detail { namespace incr { /// @brief Sequence eval over lane points for output `I` at prefix `N`. /// @tparam N width in bits /// @tparam I output index /// @tparam KeyT key type /// @tparam ForwardIterator forward iterator type /// @tparam OutputBuffer output buffer type /// @tparam PathMemoizer path memoizer type /// @param dpf the DPF key /// @param begin the iterator to the first query /// @param end the iterator past the last query /// @param outbuf the `outbuf` /// @param path the root-to-leaf path /// @return Sequence eval over lane points for output `I` at prefix `N` template > auto eval_out_sequence_impl( const KeyT & dpf, ForwardIterator begin, ForwardIterator end, OutputBuffer && outbuf, PathMemoizer && path = PathMemoizer{}) { using key_type = KeyT; static_assert(key_type::meta[I].prefix == N, "out sequence eval: prefix"); constexpr auto opl = key_type::template outputs_per_leaf_of; using output_type = typename key_type::template concrete_output_type; std::size_t i = 0; for (auto it = begin; it != end; ++it, ++i) { // Build a full-domain query whose top-N bits equal the lane point. auto lane = static_cast(*it); typename key_type::input_type q = lane; if constexpr (N < key_type::input_bits) q = static_cast( lane << (key_type::input_bits - N)); auto out = eval_out_point_impl(dpf, q, path); if constexpr (utils::is_packed_subbyte_v) { store_leaf_bytes(outbuf, i, out.node); } else { utils::raw_memcpy(&outbuf[i * opl], &out.node, sizeof(output_type) * opl); } } return subsequence_iterable(std::begin(outbuf), begin, end); } } // namespace incr } // namespace detail /// @brief Deepest-group sequence eval (first cut of incremental `eval_sequence`). /// @tparam KeyT key type /// @tparam ForwardIterator forward iterator type /// @tparam OutputBuffer output buffer type /// @tparam KeyT key type /// @param dpf the DPF key /// @param begin the iterator to the first query /// @param end the iterator past the last query /// @param outbuf the `outbuf` /// @return Deepest-group sequence eval (first cut of incremental `eval_sequence`) /// \complexity O(n k) interior traversals in the worst case and O(k) node workspace. k is the number of listed points and n is `depth`. The breadth-first buffer is 2k nodes, so each level traverses at most one node per point. Shared prefixes do fewer traversals. A recipe memoizer instead stores O(recipe leaf nodes) (see that memoizer). template , bool> = true> auto eval_sequence( const KeyT & dpf, ForwardIterator begin, ForwardIterator end, OutputBuffer && outbuf) { using key_type = KeyT; constexpr auto I = key_type::deepest_output; return detail::incr::eval_out_sequence_impl(dpf, begin, end, outbuf); } // --------------------------------------------------------------------------- // cmp eval: tree path-sum comparison (native key value CWs) // --------------------------------------------------------------------------- namespace detail { namespace incr { template uint64_t eval_cmp_path_sum(const KeyT & dpf, typename KeyT::input_type tx, PathMemoizer & path, bool as_prefix = false, std::size_t prefix_len = 0, proof_token * pi = nullptr) { using namespace detail::dcf_impl; const auto & ch = dpf.cmp(); const uint64_t mask = ch.mask; const uint64_t add = [&]() -> uint64_t { if constexpr (is_party_key_v) return dpf.cmp_addend().raw(); else return dpf.cmp_addend(); }(); if constexpr (unwrap_party_key_t::cmp_block > 0) { if (as_prefix) throw std::invalid_argument( "cmp_prefix is not defined for a blocked comparison"); return detail::blocked::eval_share(dpf, tx, path, pi); } const std::size_t full_bits = static_cast(ch.nbits); const std::size_t nbits = as_prefix ? prefix_len : full_bits; if ((ch.trivial == cmp_trivial::always_true || ch.trivial == cmp_trivial::always_false) && (!as_prefix || prefix_len == full_bits)) return add; dpf::detail::ensure_level(dpf, tx, path, nbits, pi); const int party = dpf::get_lo_bit(dpf.root()) ? 1 : 0; uint64_t V = 0; auto bit_mask = KeyT::msb_mask; for (std::size_t i = 0; i < nbits; ++i, bit_mask >>= 1) { const bool xi = !!(bit_mask & tx); const auto & parent = path[i]; const uint8_t t = static_cast(dpf::get_lo_bit(parent)); auto kids = KeyT::tree::expand_value(parent); const uint64_t v = convert_node(kids[xi ? 1 : 0], mask); const uint64_t contrib = (v + (t ? dpf.value_cw(i) : 0ULL)) & mask; V = (V + (party ? neg_m(contrib, mask) : contrib)) & mask; } const auto & leaf = path[nbits]; const uint8_t t = static_cast(dpf::get_lo_bit(leaf)); const uint64_t c = convert_node(leaf, mask); const uint64_t last = as_prefix ? dpf.prefix_cw(nbits) : dpf.cw_last(); const uint64_t contrib = (c + (t ? last : 0ULL)) & mask; V = (V + (party ? neg_m(contrib, mask) : contrib)) & mask; if (ch.eval_as_ge) V = neg_m(V, mask); return (V + add) & mask; } /// @brief Full-tree interval memoizer stopped at `StopLevel` (retains every level; /// unlike `basic_interval_memoizer_at` which ping-pongs two buffers). /// @tparam DpfKey DPF key type /// @tparam StopLevel stop level /// @tparam Allocator node allocator (defaults to aligned SIMD blocks) HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") template ::interior_node>> struct cmp_full_interval_memo { using dpf_type = unwrap_party_key_t; using integral_type = typename dpf_type::integral_type; using node_type = typename dpf_type::interior_node; using return_type = node_type *; using unique_ptr = typename Allocator::unique_ptr; static constexpr std::size_t depth = StopLevel; static constexpr bool retains_all_levels = true; explicit cmp_full_interval_memo(std::size_t output_len, Allocator alloc = Allocator{}) : output_length{output_len}, level_index{0}, level_endpoints{initialize_endpoints(output_len)}, buf{alloc.allocate_unique_ptr(level_endpoints[depth] + output_len)}, from_{std::nullopt}, to_{std::nullopt} { if (HEDLEY_UNLIKELY(buf == nullptr)) throw std::bad_alloc{}; } std::size_t assign_interval(const dpf_type & dpf, integral_type new_from, integral_type new_to) { static constexpr auto complement_of = std::bit_not{}; if (from_.has_value() == false || std::memcmp(&dpf_root_, &dpf.root(), sizeof(node_type)) != 0 || std::memcmp(&dpf_common_part_hash_, &dpf.common_part_hash(), sizeof(digest_type)) != 0 || from_.value_or(complement_of(new_from)) != new_from || to_.value_or(complement_of(new_to)) != new_to) { if (new_to - new_from > output_length) throw std::length_error("size of new interval is too large for memoizer"); (*this)[0][0] = dpf.root(); dpf_root_ = dpf.root(); dpf_common_part_hash_ = dpf.common_part_hash(); from_ = new_from; to_ = new_to; level_index = 1; } return level_index; } std::size_t advance_level() { return ++level_index; } std::size_t get_nodes_at_level() const { return get_nodes_at_level(level_index, from_.value_or(0), to_.value_or(0)); } std::size_t get_nodes_at_level(std::size_t level) const { return get_nodes_at_level(level, from_.value_or(0), to_.value_or(0)); } static std::size_t get_nodes_at_level(std::size_t level, integral_type from_node, integral_type to_node) { std::size_t offset = depth - level; return utils::shift_right(to_node - integral_type{1}, offset) - utils::shift_right(from_node, offset) + 1; } HEDLEY_NO_THROW return_type operator[](std::size_t level) const noexcept { return Allocator::assume_aligned(&buf[level_endpoints[level]]); } /// @brief Drop a cached interval so the next `assign_interval` rebuilds /// from the root (needed when folding a proof over the tree). void clear_assignment() { from_ = std::nullopt; to_ = std::nullopt; level_index = 0; } private: std::size_t output_length; std::size_t level_index; const std::array level_endpoints; unique_ptr buf; node_type dpf_root_; digest_type dpf_common_part_hash_; std::optional from_; std::optional to_; static constexpr auto initialize_endpoints(std::size_t len_in) { std::array eps{}; auto len = len_in; for (std::size_t level = depth; level > 0; --level) { len = std::min((len + 2) >> 1, std::size_t{1} << (level - 1)); eps[level] = len; } for (std::size_t level = 0; level < depth; ++level) eps[level + 1] = eps[level] + eps[level + 1]; return eps; } }; HEDLEY_PRAGMA(GCC diagnostic pop) /// @brief Fold every node at `level_index` of a truncated cmp interval into `pi`. /// @details Prefix addressing uses comparison `nbits`, not `KeyT::depth`. /// Blocked keys use a domain-separated level tag so tokens diverge /// from native per-level folds. template HEDLEY_ALWAYS_INLINE void fold_cmp_interval_level(proof_token & pi, const KeyT & dpf, std::size_t level_index, IntegralT from_node, std::size_t nbits, std::size_t nodes_at_level, const NodeT * curr) { if constexpr (!KeyT::is_verifiable) return; if (level_index == 0 || nodes_at_level == 0) return; const auto start = static_cast( utils::shift_right(from_node, nbits - level_index)); const auto & cs = dpf.correction_seeds()[level_index - 1]; for (std::size_t i = 0; i < nodes_at_level; ++i) { if constexpr (unwrap_party_key_t::cmp_block > 0) { detail::blocked::fold_spine_node(pi, level_index - 1, start + i, curr[i], cs); } else { detail::vdpf::fold_node(pi, level_index - 1, start + i, curr[i], cs); } } } template struct has_clear_assignment : std::false_type {}; template struct has_clear_assignment().clear_assignment())>> : std::true_type {}; /// @brief Expand interval interior nodes for the comparison prefix (stop = nbits). /// @tparam KeyT key type /// @tparam IntegralT integral type /// @tparam IntervalMemoizer interval memoizer type /// @param dpf the DPF key /// @param from_node the `from_node` /// @param to_node the `to_node` /// @param nbits the width in bits /// @param memoizer the memoizer built for this key /// @param tree_levels the `tree_levels` /// @param pi optional VDPF proof accumulator (once per BFS node) template void eval_cmp_interval_impl_interior(const KeyT & dpf, IntegralT from_node, IntegralT to_node, std::size_t nbits, IntervalMemoizer & memoizer, std::size_t tree_levels = static_cast(-1), proof_token * pi = nullptr) { if (tree_levels == static_cast(-1)) tree_levels = nbits; using node_type = typename KeyT::interior_node; using input_type = typename KeyT::input_type; const input_type lane_msb = static_cast(input_type{1} << (nbits - 1)); // Proving needs every truncated-tree node: a warm memoizer that resumes // past level 1 would skip upper folds. if (pi != nullptr) { if constexpr (has_clear_assignment::value) memoizer.clear_assignment(); } std::size_t level_index = memoizer.assign_interval(dpf, from_node, to_node); std::size_t nodes_at_level = memoizer.get_nodes_at_level(); IntegralT mask = static_cast( utils::to_integral_type{}(lane_msb) >> (level_index - 1)); for (; level_index <= tree_levels; level_index = memoizer.advance_level(), nodes_at_level = memoizer.get_nodes_at_level(), mask >>= 1) { std::size_t i = 0, j = 0; bool from_offset = mask & from_node, to_offset = from_offset ^ (nodes_at_level & 1); const node_type cw[2] = { dpf.correction_word(level_index - 1, 0), dpf.correction_word(level_index - 1, 1)}; const bool is_last = KeyT::tree::is_last_level(level_index - 1, KeyT::depth); auto *prev = memoizer[level_index - 1]; auto *curr = memoizer[level_index]; if (from_offset == true) curr[i++] = KeyT::traverse_interior(prev[j++], cw[1], 1, is_last); const std::size_t both_end = nodes_at_level - to_offset; for (; i < both_end;) { auto cur_node = prev[j++]; auto kids = KeyT::traverse_interior01(cur_node, cw[0], cw[1], is_last); curr[i++] = kids[0]; curr[i++] = kids[1]; } if (to_offset == true) curr[i] = KeyT::traverse_interior(prev[j], cw[0], 0, is_last); if (pi != nullptr) { fold_cmp_interval_level(*pi, dpf, level_index, from_node, nbits, nodes_at_level, curr); } } } template uint64_t eval_cmp_from_interval_memo(const KeyT & dpf, typename KeyT::integral_type lane, typename KeyT::integral_type from_lane, std::size_t nbits, IntervalMemoizer & memo) { using namespace detail::dcf_impl; const auto & ch = dpf.cmp(); const uint64_t mask = ch.mask; const uint64_t add = [&]() -> uint64_t { if constexpr (is_party_key_v) return dpf.cmp_addend().raw(); else return dpf.cmp_addend(); }(); if (ch.trivial == cmp_trivial::always_true || ch.trivial == cmp_trivial::always_false) return add; const int party = dpf::get_lo_bit(dpf.root()) ? 1 : 0; uint64_t V = 0; for (std::size_t i = 0; i < nbits; ++i) { const std::size_t shift = nbits - i; const auto from_i = from_lane >> shift; const auto q_i = lane >> shift; const std::size_t idx = static_cast(q_i - from_i); const auto & parent = memo[i][idx]; const bool xi = !!(lane & (typename KeyT::integral_type{1} << (nbits - 1 - i))); const uint8_t t = static_cast(dpf::get_lo_bit(parent)); auto kids = KeyT::tree::expand_value(parent); const uint64_t v = convert_node(kids[xi ? 1 : 0], mask); const uint64_t contrib = (v + (t ? dpf.value_cw(i) : 0ULL)) & mask; V = (V + (party ? neg_m(contrib, mask) : contrib)) & mask; } { const auto & leaf = memo[nbits][static_cast(lane - from_lane)]; const uint8_t t = static_cast(dpf::get_lo_bit(leaf)); const uint64_t c = convert_node(leaf, mask); const uint64_t contrib = (c + (t ? dpf.cw_last() : 0ULL)) & mask; V = (V + (party ? neg_m(contrib, mask) : contrib)) & mask; } if (ch.eval_as_ge) V = neg_m(V, mask); return (V + add) & mask; } } // namespace incr } // namespace detail namespace detail { namespace incr { template auto eval_group_path_sum(const KeyT & dpf, typename KeyT::input_type tx, PathMemoizer & path, bool as_prefix = false, std::size_t prefix_len = 0, proof_token * pi = nullptr) { using prg = typename unwrap_party_key_t::interior_prg; using concrete = dpf::concrete_type_t; const auto layout = detail::group_layout(); const auto & ch = dpf.cmp(); const auto add = detail::group_from_word(dpf.cmp_addend_word(), layout); const std::size_t full_bits = static_cast(ch.nbits); const std::size_t nbits = as_prefix ? prefix_len : full_bits; if ((ch.trivial == cmp_trivial::always_true || ch.trivial == cmp_trivial::always_false) && (!as_prefix || prefix_len == full_bits)) return detail::group_to_beta(add); dpf::detail::ensure_level(dpf, tx, path, nbits, pi); const int party = dpf::get_lo_bit(dpf.root()) ? 1 : 0; auto V = detail::group_zero(layout); const auto zero = detail::group_zero(layout); auto bit_mask = KeyT::msb_mask; for (std::size_t i = 0; i < nbits; ++i, bit_mask >>= 1) { const bool xi = !!(bit_mask & tx); const auto & parent = path[i]; const uint8_t t = static_cast(dpf::get_lo_bit(parent)); auto kids = KeyT::tree::expand_value(parent); const auto v = detail::group_from_node(kids[xi ? 1 : 0], layout); const auto cw = detail::group_from_word(dpf.value_cw()[i], layout); const auto contrib = detail::group_add(v, t ? cw : zero); V = detail::group_add(V, party ? detail::group_neg(contrib) : contrib); } const auto & leaf = path[nbits]; const uint8_t t = static_cast(dpf::get_lo_bit(leaf)); const auto c = detail::group_from_node(leaf, layout); const auto last = detail::group_from_word( as_prefix ? dpf.prefix_cws()[nbits] : dpf.cw_last_word(), layout); const auto contrib = detail::group_add(c, t ? last : zero); V = detail::group_add(V, party ? detail::group_neg(contrib) : contrib); if (ch.eval_as_ge) V = detail::group_neg(V); return detail::group_to_beta(detail::group_add(V, add)); } template auto eval_payload_path_sum(const KeyT & dpf, typename KeyT::input_type tx, PathMemoizer & path, bool as_prefix = false, std::size_t prefix_len = 0, proof_token * pi = nullptr) { using concrete = dpf::concrete_type_t; const auto & ch = dpf.cmp(); concrete add = detail::payload_from_word(dpf.cmp_addend_word()); const std::size_t full_bits = static_cast(ch.nbits); const std::size_t nbits = as_prefix ? prefix_len : full_bits; if ((ch.trivial == cmp_trivial::always_true || ch.trivial == cmp_trivial::always_false) && (!as_prefix || prefix_len == full_bits)) return add; dpf::detail::ensure_level(dpf, tx, path, nbits, pi); const int party = dpf::get_lo_bit(dpf.root()) ? 1 : 0; concrete V{}; auto bit_mask = KeyT::msb_mask; for (std::size_t i = 0; i < nbits; ++i, bit_mask >>= 1) { const bool xi = !!(bit_mask & tx); const auto & parent = path[i]; const uint8_t t = static_cast(dpf::get_lo_bit(parent)); auto kids = KeyT::tree::expand_value(parent); const concrete v = concrete::from_seed(&kids[xi ? 1 : 0], sizeof(kids[0])); const concrete cw = detail::payload_from_word(dpf.value_cw()[i]); const concrete contrib = v + (t ? cw : concrete{}); V = party ? (V + (-contrib)) : (V + contrib); } const auto & leaf = path[nbits]; const uint8_t t = static_cast(dpf::get_lo_bit(leaf)); const concrete c = concrete::from_seed(&leaf, sizeof(leaf)); const concrete last = detail::payload_from_word( as_prefix ? dpf.prefix_cws()[nbits] : dpf.cw_last_word()); const concrete contrib = c + (t ? last : concrete{}); V = party ? (V + (-contrib)) : (V + contrib); if (ch.eval_as_ge) V = -V; return V + add; } template > auto eval_cmp_point_impl(const KeyT & dpf, QueryT && x, PathMemoizer && path = PathMemoizer{}, proof_token * pi = nullptr) { if (!dpf.has_cmp()) throw std::invalid_argument("cmp eval: key has no comparison channel"); if (!dpf.cmp_assigned()) throw std::invalid_argument( "cmp eval: wildcard comparison payload not assigned (call assign_cmp)"); auto tx = dpf.offset_x(std::forward(x)); utils::flip_msb_if_signed_integral(tx); if constexpr (detail::has_from_seed>::value) { return make_eval_cmp_result( detail::incr::eval_payload_path_sum(dpf, tx, path, false, 0, pi)); } else if constexpr (detail::cmp_group_info>::custom) { return make_eval_cmp_result( detail::incr::eval_group_path_sum(dpf, tx, path, false, 0, pi)); } else { const uint64_t raw = detail::incr::eval_cmp_path_sum(dpf, tx, path, false, 0, pi); return make_eval_cmp_result( detail::dcf_impl::u64_to_beta(raw)); } } template > auto eval_cmp_prefix_point_impl(const KeyT & dpf, QueryT && x, PathMemoizer && path = PathMemoizer{}, proof_token * pi = nullptr) { static_assert(unwrap_party_key_t::cmp_idcf, "cmp_prefix requires an idcf key"); if (!dpf.has_cmp()) throw std::invalid_argument("cmp_prefix: key has no comparison channel"); if (!dpf.cmp().incremental) throw std::invalid_argument("cmp_prefix: key was not built with idcf"); if (!dpf.cmp_assigned()) throw std::invalid_argument( "cmp_prefix: wildcard comparison payload not assigned (call assign_cmp)"); if (L > static_cast(dpf.cmp().nbits)) throw std::invalid_argument("cmp_prefix: prefix is longer than the comparison"); auto tx = dpf.offset_x(std::forward(x)); utils::flip_msb_if_signed_integral(tx); if constexpr (detail::has_from_seed>::value) { return make_eval_cmp_result( detail::incr::eval_payload_path_sum(dpf, tx, path, true, L, pi)); } else if constexpr (detail::cmp_group_info>::custom) { return make_eval_cmp_result( detail::incr::eval_group_path_sum(dpf, tx, path, true, L, pi)); } else { const uint64_t raw = detail::incr::eval_cmp_path_sum(dpf, tx, path, true, L, pi); return make_eval_cmp_result( detail::dcf_impl::u64_to_beta(raw)); } } // --------------------------------------------------------------------------- // Buffered / interval / sequence comparison evals // --------------------------------------------------------------------------- template auto make_output_buffer_for_cmp_impl(const KeyT &, std::size_t n) { return dpf::output_buffer>(n); } template std::size_t cmp_inclusive_count(Integral a, Integral b) { if (b < a) throw std::invalid_argument("cmp interval: to < from"); const Integral one{1}; if (a == Integral{0} && static_cast(b + one) < b) throw std::length_error("cmp interval does not fit in size_t"); return static_cast(b - a + one); } template Integral cmp_exclusive_end(Integral b) { const Integral next = static_cast(b + Integral{1}); // Overflow: the exclusive end is 2^width, encoded as 0 for the walk. if (next < b) return Integral{0}; return next; } template struct interval_memoizer_retains_levels : std::false_type {}; template struct interval_memoizer_retains_levels> : std::bool_constant {}; template auto make_output_buffer_for_cmp_interval_impl(const KeyT & dpf, LaneT from, LaneT to) { if (!dpf.has_cmp()) throw std::invalid_argument("make_output_buffer(cmp): no cmp"); using integral = typename KeyT::integral_type; constexpr auto to_int = utils::to_integral_type{}; utils::flip_msb_if_signed_integral(from); utils::flip_msb_if_signed_integral(to); const auto a = static_cast(to_int(from)); const auto b = static_cast(to_int(to)); return dpf::output_buffer>( cmp_inclusive_count(a, b)); } template > void eval_cmp_sequence_impl(const KeyT & dpf, ForwardIterator begin, ForwardIterator end, OutputBuffer && outbuf, PathMemoizer && path = PathMemoizer{}, proof_token * pi = nullptr) { if (!dpf.has_cmp()) throw std::invalid_argument("cmp sequence eval: no comparison channel"); std::size_t i = 0; for (auto it = begin; it != end; ++it, ++i) outbuf[i] = eval_cmp_point_impl(dpf, *it, path, pi); } template void eval_cmp_interval_impl(const KeyT & dpf, LaneT from, LaneT to, OutputBuffer && outbuf, proof_token * pi = nullptr) { if (!dpf.has_cmp()) throw std::invalid_argument("cmp interval eval: no comparison channel"); if (!dpf.cmp_assigned()) throw std::invalid_argument( "cmp interval eval: wildcard payload not assigned (call assign_cmp)"); if constexpr (detail::cmp_group_info>::custom) { std::size_t i = 0; dpf::basic_path_memoizer path; for (LaneT q = from; ; ++q, ++i) { outbuf[i] = eval_cmp_point_impl(dpf, q, path, pi); if (q == to) break; } return; } constexpr auto to_int = utils::to_integral_type{}; utils::flip_msb_if_signed_integral(from); utils::flip_msb_if_signed_integral(to); const auto nbits = static_cast(dpf.cmp().nbits); using integral = typename KeyT::integral_type; const auto a = static_cast(to_int(from)); const auto b = static_cast(to_int(to)); const auto count = cmp_inclusive_count(a, b); // Full-tree interval memoizer retains every level (ping-pong basic_* does not), // so path-sum can read ancestors directly. constexpr std::size_t stop = KeyT::cmp_depth == 0 ? KeyT::depth : KeyT::cmp_depth; // Rebind key depth for the memoizer by using a stop-level wrapper built on // the same assign/advance API as basic_interval_memoizer_at, but allocate // per-level storage. Stop stays the logical comparison width so prefix // indexes match `nbits`, even when a blocked key's seed spine is shorter. HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") detail::incr::cmp_full_interval_memo memo{count}; HEDLEY_PRAGMA(GCC diagnostic pop) const std::size_t levels = unwrap_party_key_t::cmp_block > 0 ? unwrap_party_key_t::cmp_h : nbits; detail::incr::eval_cmp_interval_impl_interior(dpf, a, cmp_exclusive_end(b), nbits, memo, levels, pi); for (std::size_t i = 0; i < count; ++i) { const auto q = static_cast(a + static_cast(i)); const uint64_t raw = [&] { if constexpr (unwrap_party_key_t::cmp_block > 0) { HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") return detail::blocked::eval_share_memo(dpf, q, a, cmp_exclusive_end(b), memo); HEDLEY_PRAGMA(GCC diagnostic pop) } else { return detail::incr::eval_cmp_from_interval_memo( dpf, q, a, nbits, memo); } }(); outbuf[i] = make_eval_cmp_result( detail::dcf_impl::u64_to_beta(raw)); } } template void eval_cmp_interval_impl(const KeyT & dpf, LaneT from, LaneT to, OutputBuffer && outbuf, IntervalMemoizer && memo, proof_token * pi = nullptr) { if (!dpf.has_cmp()) throw std::invalid_argument("cmp interval eval: no comparison channel"); if (!dpf.cmp_assigned()) throw std::invalid_argument( "cmp interval eval: wildcard payload not assigned (call assign_cmp)"); if constexpr (detail::cmp_group_info>::custom) { (void)memo; std::size_t i = 0; dpf::basic_path_memoizer path; for (LaneT q = from; ; ++q, ++i) { outbuf[i] = eval_cmp_point_impl(dpf, q, path, pi); if (q == to) break; } return; } constexpr auto to_int = utils::to_integral_type{}; utils::flip_msb_if_signed_integral(from); utils::flip_msb_if_signed_integral(to); const auto nbits = static_cast(dpf.cmp().nbits); using integral = typename KeyT::integral_type; static_assert(interval_memoizer_retains_levels>::value, "eval_cmp_interval_impl memoizer must set retains_all_levels; a ping-pong memoizer drops ancestor levels"); const auto a = static_cast(to_int(from)); const auto b = static_cast(to_int(to)); const auto count = cmp_inclusive_count(a, b); const std::size_t levels = unwrap_party_key_t::cmp_block > 0 ? unwrap_party_key_t::cmp_h : nbits; detail::incr::eval_cmp_interval_impl_interior(dpf, a, cmp_exclusive_end(b), nbits, memo, levels, pi); for (std::size_t i = 0; i < count; ++i) { const auto q = static_cast(a + static_cast(i)); const uint64_t raw = [&] { if constexpr (unwrap_party_key_t::cmp_block > 0) { HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") return detail::blocked::eval_share_memo(dpf, q, a, cmp_exclusive_end(b), memo); HEDLEY_PRAGMA(GCC diagnostic pop) } else { return detail::incr::eval_cmp_from_interval_memo( dpf, q, a, nbits, memo); } }(); outbuf[i] = make_eval_cmp_result( detail::dcf_impl::u64_to_beta(raw)); } } template auto eval_cmp_interval_impl(const KeyT & dpf, LaneT from, LaneT to, proof_token * pi = nullptr) { auto buf = make_output_buffer_for_cmp_interval_impl(dpf, from, to); eval_cmp_interval_impl(dpf, from, to, buf, pi); return buf; } /// @brief Fold every truncated cmp-tree node of `[from, to]` into `pi`. /// @details Caller must `init_proof` first, or use `prove_cmp_interval`. template void prove_fold_cmp_interval(const KeyT & dpf, LaneT from, LaneT to, proof_token & pi) { static_assert(KeyT::is_verifiable, "prove_fold_cmp_interval: key must carry dpf::verifiable"); if (!dpf.has_cmp()) throw std::invalid_argument("prove_cmp_interval: no comparison channel"); if (!dpf.cmp_assigned()) throw std::invalid_argument( "prove_cmp_interval: wildcard payload not assigned (call assign_cmp)"); constexpr auto to_int = utils::to_integral_type{}; auto from_x = from; auto to_x = to; utils::flip_msb_if_signed_integral(from_x); utils::flip_msb_if_signed_integral(to_x); const auto nbits = static_cast(dpf.cmp().nbits); using integral = typename KeyT::integral_type; const auto a = static_cast(to_int(from_x)); const auto b = static_cast(to_int(to_x)); const auto count = cmp_inclusive_count(a, b); constexpr std::size_t stop = KeyT::cmp_depth == 0 ? KeyT::depth : KeyT::cmp_depth; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") cmp_full_interval_memo memo{count}; HEDLEY_PRAGMA(GCC diagnostic pop) const std::size_t levels = unwrap_party_key_t::cmp_block > 0 ? unwrap_party_key_t::cmp_h : nbits; eval_cmp_interval_impl_interior(dpf, a, cmp_exclusive_end(b), nbits, memo, levels, &pi); } } // namespace incr } // namespace detail } // namespace dpf #endif // LIBDPF_INCLUDE_DPF_INCREMENTAL_HPP__