/// @file dpf/interval.hpp /// @brief Public-bound interval containment on one comparison key. /// @details `make_dpf(r, ic(p, q, β))` hides the mask `r` and the payload `β`. /// The bounds are public. Reconstruction is `β` when /// `p ≤ (x − r) mod 2^n ≤ q`, and the false payload otherwise. /// /// The key is one `lt` comparison at `γ = r − 1`, the Boyle–Chandran– /// Gilboa–Gupta–Ishai–Kumar–Rathee reduction (EUROCRYPT 2021, Fig. 3; /// ePrint 2020/1392). Their Section 4.1 is one DCF for a public interval, /// where the earlier gate used about two. /// Evaluation walks that key at the two public shifts of `x` and adds /// a secret-shared correction. Seed corrections, advice bits, leaves, /// and the path memoizer stay single-path. /// @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_INTERVAL_HPP__ #define LIBDPF_INCLUDE_DPF_INTERVAL_HPP__ #include #include #include #include #include #include "hedley/hedley.h" #include "dpf/dcf.hpp" #include "dpf/eval_unified.hpp" #include "dpf/geneval.hpp" #include "dpf/grow.hpp" #include "dpf/grow_ds.hpp" #include "dpf/incremental.hpp" #include "dpf/output_buffer.hpp" #include "dpf/secret_share.hpp" #include "dpf/utils.hpp" namespace dpf { template struct ic_pack { static constexpr bool is_ic = true; using beta_type = Beta; uint64_t lo = 0; uint64_t hi = 0; Beta if_true{}; Beta if_false{}; }; /// @brief Spec tag and factory. `dpf::ic(p, q, beta)` builds a pack; /// `eval_point(dpf::ic, key, x)` evaluates it. struct ic_fn { template HEDLEY_WARN_UNUSED_RESULT ic_pack> operator()(Lo lo, Hi hi, Beta t, Beta f = detail::dcf_impl::default_false>()) const { ic_pack> spec; spec.lo = static_cast(lo); spec.hi = static_cast(hi); spec.if_true = std::move(t); spec.if_false = std::move(f); return spec; } }; inline constexpr ic_fn ic{}; template struct is_ic_key : std::false_type {}; /// @brief One party's interval key: the inner comparison, the public bounds, and the /// secret correction shares. /// @tparam Party party index, `0` or `1` /// @tparam Key key type /// @tparam Input input domain type /// @tparam Beta payload type template struct ic_key { static constexpr std::size_t party = Party; static constexpr bool wildcard = Key::cmp_is_wildcard; using input_type = Input; using key_type = party_key; using beta_type = Beta; using share_type = std::conditional_t< detail::cmp_group_info>::custom, detail::group_elem, uint64_t>; /// @brief Inner comparison key. Eval that accepts a `dpf_key` also accepts /// this object and reads `dpf_key`. key_type dpf_key; uint64_t lo = 0; uint64_t hi = 0; uint64_t input_mask = 0; uint64_t group_mask = 0; /// @brief Share of `δ`. Public `c_x ∈ {-1,0,1}` scales it locally. share_type delta_share{}; /// @brief Share of `δ · c_r + if_false`. share_type cr_share{}; /// @brief Wildcard only: shares of `1` and of `c_r`, scaled by `δ` in `assign_cmp`. share_type delta_coeff{}; share_type cr_coeff{}; bool assigned = !wildcard; ic_key(key_type k, uint64_t lo_in, uint64_t hi_in, uint64_t nmask, uint64_t gmask, share_type dshare, share_type cshare, share_type dcoeff, share_type ccoeff) noexcept(std::is_nothrow_move_constructible_v) : dpf_key(std::move(k)) , lo(lo_in) , hi(hi_in) , input_mask(nmask) , group_mask(gmask) , delta_share(dshare) , cr_share(cshare) , delta_coeff(dcoeff) , cr_coeff(ccoeff) {} }; template struct is_ic_key> : std::true_type {}; template inline constexpr bool is_ic_key_v = is_ic_key>::value; namespace detail { namespace ic_impl { template HEDLEY_CONST HEDLEY_NO_THROW HEDLEY_ALWAYS_INLINE constexpr uint64_t input_mask_of() noexcept { constexpr auto n = utils::bitlength_of_v; if constexpr (n >= 64) return ~uint64_t{0}; else return (uint64_t{1} << n) - 1ULL; } template HEDLEY_CONST HEDLEY_NO_THROW HEDLEY_ALWAYS_INLINE constexpr uint64_t bits_of(Input x) noexcept { constexpr auto to_int = utils::to_integral_type{}; return static_cast(to_int(x)) & input_mask_of(); } template HEDLEY_CONST HEDLEY_NO_THROW HEDLEY_ALWAYS_INLINE constexpr Input input_from_bits(uint64_t u) noexcept { return static_cast(u & input_mask_of()); } HEDLEY_CONST HEDLEY_NO_THROW HEDLEY_ALWAYS_INLINE constexpr uint64_t embed_small(int s, uint64_t mask) noexcept { if (s >= 0) return static_cast(s) & mask; return dcf_impl::neg_m(static_cast(-s), mask); } HEDLEY_CONST HEDLEY_NO_THROW HEDLEY_ALWAYS_INLINE constexpr uint64_t mul_mask(uint64_t a, uint64_t b, uint64_t mask) noexcept { return static_cast(static_cast(a) * b) & mask; } /// @brief Public integer in Fig. 3, before it is embedded in the payload group. /// @param r the `r` /// @param p the `p` /// @param q the `q` /// @param nmask the mask of the live input bits /// @return Public integer in Fig. 3, before it is embedded in the payload group HEDLEY_CONST HEDLEY_NO_THROW HEDLEY_ALWAYS_INLINE constexpr int correction_s(uint64_t r, uint64_t p, uint64_t q, uint64_t nmask) noexcept { const uint64_t aq = (q + r) & nmask; const uint64_t ap = (p + r) & nmask; const uint64_t q0 = (q + 1ULL) & nmask; const uint64_t aq0 = (q0 + r) & nmask; return (ap > aq ? 1 : 0) - (ap > p ? 1 : 0) + (aq0 > q0 ? 1 : 0) + (aq == nmask ? 1 : 0); } /// @brief Dealer correction in Fig. 3, as an element of the payload group. /// @param r the `r` /// @param p the `p` /// @param q the `q` /// @param nmask the mask of the live input bits /// @param gmask the mask of the live payload bits /// @return Dealer correction in Fig. 3, as an element of the payload group HEDLEY_CONST HEDLEY_NO_THROW HEDLEY_ALWAYS_INLINE constexpr uint64_t correction(uint64_t r, uint64_t p, uint64_t q, uint64_t nmask, uint64_t gmask) noexcept { return embed_small(correction_s(r, p, q, nmask), gmask); } HEDLEY_CONST HEDLEY_NO_THROW HEDLEY_ALWAYS_INLINE constexpr int public_cx(uint64_t x, uint64_t p, uint64_t q, uint64_t nmask) noexcept { const uint64_t q0 = (q + 1ULL) & nmask; return (x > p ? 1 : 0) - (x > q0 ? 1 : 0); } HEDLEY_CONST HEDLEY_NO_THROW HEDLEY_ALWAYS_INLINE constexpr uint64_t shift_p(uint64_t x, uint64_t p, uint64_t nmask) noexcept { return (x + (nmask - p)) & nmask; } HEDLEY_CONST HEDLEY_NO_THROW HEDLEY_ALWAYS_INLINE constexpr uint64_t shift_q0(uint64_t x, uint64_t q, uint64_t nmask) noexcept { const uint64_t q0 = (q + 1ULL) & nmask; return (x + (nmask - q0)) & nmask; } template HEDLEY_PURE HEDLEY_NO_THROW HEDLEY_ALWAYS_INLINE uint64_t opened_u64(const T & v, uint64_t mask) noexcept { if constexpr (is_secret_share_v>) return dcf_impl::beta_to_u64_simple(v.raw(), mask); else return dcf_impl::beta_to_u64_simple(v, mask); } inline void split_target(uint64_t target, uint64_t mask, uint64_t & s0, uint64_t & s1) { const uint64_t blind = dcf_impl::sample_addend_blind(mask, [] { return dpf::uniform_sample(); }); incr::split_cmp_addend(target, mask, blind, s0, s1); } template void check_input() { static_assert(std::is_unsigned_v && !std::is_same_v, "ic: input type must be an unsigned integer of at most 64 bits"); static_assert(utils::bitlength_of_v <= 64, "ic: input type must be an unsigned integer of at most 64 bits"); static_assert(utils::bitlength_of_v > 0, "ic: input type must be an unsigned integer of at most 64 bits"); } template void check_bounds(const ic_pack & spec) { const uint64_t nmask = input_mask_of(); if (spec.lo > nmask || spec.hi > nmask) throw std::invalid_argument("ic: bound does not fit in the input domain"); if (spec.lo > spec.hi) throw std::invalid_argument("ic: require lo <= hi (the interval does not wrap)"); } template HEDLEY_CONST HEDLEY_NO_THROW HEDLEY_ALWAYS_INLINE constexpr uint64_t group_mask_of() noexcept { using B = concrete_type_t; if constexpr (std::is_same_v) return 1ULL; else return dcf_impl::default_mask_for_bits(utils::bitlength_of_v); } template ic_key make_side(party_key key, uint64_t lo, uint64_t hi, uint64_t nmask, uint64_t gmask, Share delta_share, Share cr_share, Share delta_coeff, Share cr_coeff) { return ic_key(std::move(key), lo, hi, nmask, gmask, delta_share, cr_share, delta_coeff, cr_coeff); } template auto finish(uint64_t r_bits, const ic_pack & spec, Pair && inner) { using in_type = std::decay_t; using party0 = std::decay_t; using raw_key = typename party0::key_type; using out_beta = concrete_type_t; const uint64_t nmask = input_mask_of(); const uint64_t gmask = group_mask_of(); constexpr bool wild = is_wildcard_v; if constexpr (detail::cmp_group_info::custom) { using prg = typename raw_key::interior_prg; const auto layout = detail::group_layout(); auto delta = detail::group_zero(layout); auto fval = detail::group_zero(layout); if constexpr (!wild) { delta = detail::group_sub(detail::group_from_beta(spec.if_true), detail::group_from_beta(spec.if_false)); fval = detail::group_from_beta(spec.if_false); } const auto cr = detail::group_scalar( correction_s(r_bits, spec.lo, spec.hi, nmask), layout); auto splitg = [&](const detail::group_elem & target, detail::group_elem & a, detail::group_elem & b) { const auto blind = detail::group_from_node( dpf::uniform_sample(), layout); a = blind; b = detail::group_sub(target, blind); }; detail::group_elem d0{}, d1{}, c0{}, c1{}, dc0{}, dc1{}, cc0{}, cc1{}; if constexpr (wild) { splitg(detail::group_one(layout), dc0, dc1); splitg(cr, cc0, cc1); } else { splitg(delta, d0, d1); splitg(detail::group_add(detail::group_mul(delta, cr), fval), c0, c1); } auto k0 = make_side<0, raw_key, in_type, out_beta>(std::move(inner.first), spec.lo, spec.hi, nmask, gmask, d0, c0, dc0, cc0); auto k1 = make_side<1, raw_key, in_type, out_beta>(std::move(inner.second), spec.lo, spec.hi, nmask, gmask, d1, c1, dc1, cc1); return std::make_pair(std::move(k0), std::move(k1)); } else { uint64_t delta = 0; uint64_t fval = 0; if constexpr (!wild) { delta = dcf_impl::beta_delta_u64(spec.if_true, spec.if_false, gmask); fval = dcf_impl::beta_to_u64_simple(spec.if_false, gmask); } const uint64_t cr = correction(r_bits, spec.lo, spec.hi, nmask, gmask); uint64_t d0 = 0, d1 = 0, c0 = 0, c1 = 0; uint64_t dc0 = 0, dc1 = 0, cc0 = 0, cc1 = 0; if constexpr (wild) { split_target(1ULL & gmask, gmask, dc0, dc1); split_target(cr, gmask, cc0, cc1); } else { split_target(delta, gmask, d0, d1); const uint64_t absorb = (mul_mask(delta, cr, gmask) + fval) & gmask; split_target(absorb, gmask, c0, c1); } auto k0 = make_side<0, raw_key, in_type, out_beta>(std::move(inner.first), spec.lo, spec.hi, nmask, gmask, d0, c0, dc0, cc0); auto k1 = make_side<1, raw_key, in_type, out_beta>(std::move(inner.second), spec.lo, spec.hi, nmask, gmask, d1, c1, dc1, cc1); return std::make_pair(std::move(k0), std::move(k1)); } } template auto inner_lt(const ic_pack & spec) { using B = std::decay_t; if constexpr (is_wildcard_v) return lt(spec.if_true, spec.if_false); else if constexpr (detail::cmp_group_info::custom) { const auto layout = detail::group_layout>(); const auto delta = detail::group_sub( detail::group_from_beta(spec.if_true), detail::group_from_beta(spec.if_false)); return lt(detail::group_to_beta(delta), detail::group_to_beta(detail::group_zero(layout))); } else { const uint64_t gmask = group_mask_of(); const uint64_t delta = dcf_impl::beta_delta_u64(spec.if_true, spec.if_false, gmask); return lt(dcf_impl::u64_to_beta(delta), dcf_impl::u64_to_beta(0)); } } template Input gamma_of(Input r) { const uint64_t nmask = input_mask_of(); const uint64_t ru = bits_of(r); return input_from_bits((ru - 1ULL) & nmask); } template auto eval_one(const IcKey & k, Query && x, Memo & memo, proof_token * pi = nullptr) { if (!k.assigned) throw std::invalid_argument( "ic eval: wildcard payload not assigned (call assign_cmp)"); using in_type = typename IcKey::input_type; using beta = typename IcKey::beta_type; const uint64_t xu = bits_of(in_type(std::forward(x))); const uint64_t xp = shift_p(xu, k.lo, k.input_mask); const uint64_t xq = shift_q0(xu, k.hi, k.input_mask); if constexpr (detail::cmp_group_info::custom) { auto opened = [](const auto & v) { if constexpr (is_secret_share_v>) return detail::group_from_beta(v.raw()); else return detail::group_from_beta(v); }; const auto a = opened(detail::incr::eval_cmp_point_impl(k.dpf_key, input_from_bits(xp), memo, pi)); const auto b = opened(detail::incr::eval_cmp_point_impl(k.dpf_key, input_from_bits(xq), memo, pi)); const int cx = public_cx(xu, k.lo, k.hi, k.input_mask); auto scaled = detail::group_zero(a); if (cx == 1) scaled = k.delta_share; else if (cx == -1) scaled = detail::group_neg(k.delta_share); const auto y = detail::group_add(detail::group_add( detail::group_add(detail::group_neg(a), b), k.cr_share), scaled); return make_eval_cmp_result( detail::group_to_beta(y)); } else { const uint64_t a = opened_u64( detail::incr::eval_cmp_point_impl(k.dpf_key, input_from_bits(xp), memo, pi), k.group_mask); const uint64_t b = opened_u64( detail::incr::eval_cmp_point_impl(k.dpf_key, input_from_bits(xq), memo, pi), k.group_mask); const int cx = public_cx(xu, k.lo, k.hi, k.input_mask); uint64_t scaled = 0; if (cx == 1) scaled = k.delta_share & k.group_mask; else if (cx == -1) scaled = dcf_impl::neg_m(k.delta_share, k.group_mask); const uint64_t y = (dcf_impl::neg_m(a, k.group_mask) + b + k.cr_share + scaled) & k.group_mask; return make_eval_cmp_result( dcf_impl::u64_to_beta(y)); } } } // namespace ic_impl } // namespace detail /// @brief Dealer key for public bounds `spec` and secret mask `r`. /// @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 Beta payload type /// @param r the secret input mask /// @param spec the public bounds and payloads /// @return Dealer key for public bounds `spec` and secret mask `r` /// @note Following Boyle, Chandran, Gilboa, Gupta, Ishai, Kumar, and Rathee, EUROCRYPT 2021, Fig. 3 (ePrint 2020/1392): one comparison key, evaluated at two public shifts. /// \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 && r, const ic_pack & spec) { using input_type = std::decay_t; detail::ic_impl::check_input(); detail::ic_impl::check_bounds(spec); const uint64_t r_bits = detail::ic_impl::bits_of(input_type(r)); const input_type gamma = detail::ic_impl::gamma_of(input_type(r)); auto inner = make_dpf(gamma, detail::ic_impl::inner_lt(spec)); return detail::ic_impl::finish(r_bits, spec, std::move(inner)); } /// @name Doerner–Shelat interval keys /// @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 Beta payload type /// @param r0 party 0's share of the mask /// @param r1 party 1's share of the mask /// @param spec the public bounds and payloads /// @{ /// @brief XOR shares. `r0 XOR r1` is the secret mask. /// @tparam RootSampler sampler for the Doerner–Shelat root seed /// @tparam PadRng pad stream for the Doerner–Shelat protocol /// @param r0 party 0's share of the mask /// @param r1 party 1's share of the mask /// @param rng the Doerner–Shelat randomness tapes /// @param spec the public bounds and payloads /// @param tags optional `verifiable` or `extractable` markers /// @return the two party keys /// \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 r0, InputT r1, ds_randomness rng, const ic_pack & spec, Tags && ...tags) { using input_type = std::decay_t; detail::ic_impl::check_input(); detail::ic_impl::check_bounds(spec); const input_type r = utils::xor_input_shares(r0, r1); const uint64_t r_bits = detail::ic_impl::bits_of(r); const input_type gamma = detail::ic_impl::gamma_of(r); const input_type g0 = r0; const input_type g1 = utils::xor_input_shares(g0, gamma); auto inner = make_dpf_doerner_shelat(g0, g1, std::move(rng), detail::ic_impl::inner_lt(spec), std::forward(tags)...); return detail::ic_impl::finish(r_bits, spec, std::move(inner)); } /// @brief Additive shares. `r0 + r1` is the secret mask; γ = (r0 + r1) − 1. /// @tparam RootSampler sampler for the Doerner–Shelat root seed /// @tparam PadRng pad stream for the Doerner–Shelat protocol /// @param r0 party 0's share of the mask /// @param r1 party 1's share of the mask /// @param rng the Doerner–Shelat randomness tapes /// @param spec the public bounds and payloads /// @param tags optional `verifiable` or `extractable` markers /// @return the two party keys /// \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 r0, InputT r1, ds_randomness rng, const ic_pack & spec, Tags && ...tags) { using input_type = std::decay_t; detail::ic_impl::check_input(); detail::ic_impl::check_bounds(spec); const uint64_t nmask = detail::ic_impl::input_mask_of(); const uint64_t r_bits = (detail::ic_impl::bits_of(r0) + detail::ic_impl::bits_of(r1)) & nmask; const input_type r = detail::ic_impl::input_from_bits(r_bits); // Additive shares of γ = r − 1: (r0 − 1, r1). const input_type g0 = detail::ic_impl::input_from_bits( (detail::ic_impl::bits_of(r0) - 1ULL) & nmask); const input_type g1 = r1; auto inner = make_dpf_doerner_shelat( arith_input, g0, g1, std::move(rng), detail::ic_impl::inner_lt(spec), std::forward(tags)...); return detail::ic_impl::finish( detail::ic_impl::bits_of(r), spec, std::move(inner)); } /// @brief XOR shares, sampled from the library entropy source. /// @return the two party keys /// \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 r0, InputT r1, const ic_pack & spec, Tags && ...tags) { using block = typename InteriorPRG::block_type; ds_randomness rng{ dpf::uniform_sample, {}}; return make_dpf_doerner_shelat( std::move(r0), std::move(r1), rng, spec, std::forward(tags)...); } /// @brief Additive shares, sampled from the library entropy source. /// @return the two party keys /// \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 r0, InputT r1, const ic_pack & spec, Tags && ...tags) { using block = typename InteriorPRG::block_type; ds_randomness rng{ dpf::uniform_sample, {}}; return make_dpf_doerner_shelat( arith_input, std::move(r0), std::move(r1), rng, spec, std::forward(tags)...); } /// @} /// @brief Open a wildcard interval payload onto an existing key pair. /// @tparam Key key type /// @tparam Input input domain type /// @tparam Beta payload type /// @tparam Payload concrete payload type /// @param k0 the `k0` /// @param k1 the `k1` /// @param if_true the payload on a true comparison /// @param if_false the payload on a false comparison template void assign_cmp(ic_key<0, Key, Input, Beta> & k0, ic_key<1, Key, Input, Beta> & k1, const Payload & if_true, const Payload & if_false = Payload{}) { static_assert(Key::cmp_is_wildcard, "assign_cmp: interval payload is not a wildcard"); if constexpr (detail::cmp_group_info>::custom) { using prg = typename Key::interior_prg; 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 fval = detail::group_from_beta(if_false); assign_cmp(k0.dpf_key, k1.dpf_key, detail::group_to_beta(delta), detail::group_to_beta(detail::group_zero(layout))); k0.delta_share = detail::group_mul(k0.delta_coeff, delta); k1.delta_share = detail::group_mul(k1.delta_coeff, delta); k0.cr_share = detail::group_mul(k0.cr_coeff, delta); k1.cr_share = detail::group_mul(k1.cr_coeff, delta); const auto blind = detail::group_from_node( dpf::uniform_sample(), layout); const auto f1 = detail::group_sub(fval, blind); k0.cr_share = detail::group_add(k0.cr_share, blind); k1.cr_share = detail::group_add(k1.cr_share, f1); k0.assigned = true; k1.assigned = true; return; } else { const uint64_t mask = k0.group_mask; const uint64_t delta = detail::dcf_impl::beta_delta_u64(if_true, if_false, mask); const uint64_t fval = detail::dcf_impl::beta_to_u64_simple(if_false, mask); assign_cmp(k0.dpf_key, k1.dpf_key, detail::dcf_impl::u64_to_beta(delta), detail::dcf_impl::u64_to_beta(0)); k0.delta_share = detail::ic_impl::mul_mask(k0.delta_coeff, delta, mask); k1.delta_share = detail::ic_impl::mul_mask(k1.delta_coeff, delta, mask); k0.cr_share = detail::ic_impl::mul_mask(k0.cr_coeff, delta, mask); k1.cr_share = detail::ic_impl::mul_mask(k1.cr_coeff, delta, mask); uint64_t f0 = 0, f1 = 0; detail::ic_impl::split_target(fval, mask, f0, f1); k0.cr_share = (k0.cr_share + f0) & mask; k1.cr_share = (k1.cr_share + f1) & mask; k0.assigned = true; k1.assigned = true; } } /// @brief Point evaluation. `memo` is a path memoizer for the inner comparison key. /// @tparam IcKey interval-containment key type /// @tparam Query query point type /// @tparam Memo path memoizer type /// @param key the key to evaluate /// @param x the `x` /// @param memo the memoizer reused across queries /// @return Point evaluation /// \complexity Two comparison point-walks (`eval_cmp_point_impl`), each O(n) interior steps, plus O(1) group arithmetic. n is the key depth. A path memoizer reuses a shared prefix. template , typename = std::enable_if_t>> HEDLEY_WARN_UNUSED_RESULT auto eval_point(ic_fn, const IcKey & key, Query && x, Memo && memo = Memo{}) { return detail::ic_impl::eval_one(key, std::forward(x), memo); } /// @name Interval evaluation /// @tparam IcKey interval-containment key type /// @tparam Lane input-domain lane type /// @tparam Buffer output buffer type /// @param key the interval key /// @param from the inclusive start of the range /// @param to the inclusive end of the range /// @param buf the output buffer /// @throws std::invalid_argument if `to < from` /// @{ /// @brief Inclusive interval `[from, to]` on the input domain. /// @tparam Memo path memoizer type /// @param key the interval key /// @param from the inclusive start of the range /// @param to the inclusive end of the range /// @param buf the output buffer /// @param memo the memoizer reused across queries /// \complexity One `eval_one` per input from `from` through `to`. Each `eval_one` is two comparison point-walks, so this is not the truncated-tree interval walk. A path memoizer reuses prefixes across those walks. Counted the `for (x = a; x != b; ++x)` loop. template >> void eval_interval(ic_fn, const IcKey & key, Lane from, Lane to, Buffer && buf, Memo && memo) { using in_type = typename IcKey::input_type; const uint64_t nmask = key.input_mask; const uint64_t a = detail::ic_impl::bits_of(in_type(from)); const uint64_t b = detail::ic_impl::bits_of(in_type(to)); if (a > b) throw std::invalid_argument("ic interval: to < from"); std::size_t i = 0; for (uint64_t x = a;; ++x) { buf[i++] = detail::ic_impl::eval_one(key, detail::ic_impl::input_from_bits(x), memo); if (x == b) break; if (x == nmask) throw std::invalid_argument("ic interval: to < from"); } } /// @brief Inclusive interval `[from, to]`, with a fresh path memoizer. /// \complexity One `eval_one` per input from `from` through `to`. Each `eval_one` is two comparison point-walks, so this is not the truncated-tree interval walk. A path memoizer reuses prefixes across those walks. Counted the `for (x = a; x != b; ++x)` loop. template >> void eval_interval(ic_fn, const IcKey & key, Lane from, Lane to, Buffer && buf) { basic_path_memoizer memo; eval_interval(ic, key, from, to, std::forward(buf), memo); } /// @} /// @name Sequence evaluation /// @tparam IcKey interval-containment key type /// @tparam Iter iterator type /// @tparam Buffer output buffer type /// @param key the interval key /// @param begin the iterator to the first query /// @param end the iterator past the last query /// @param buf the output buffer /// @{ /// @brief Evaluate the points in `[begin, end)`. /// @tparam Memo path memoizer type /// @param key the interval key /// @param begin the iterator to the first query /// @param end the iterator past the last query /// @param buf the output buffer /// @param memo the memoizer reused across queries /// \complexity One `eval_one` per input from `from` through `to`. Each `eval_one` is two comparison point-walks, so this is not the truncated-tree interval walk. A path memoizer reuses prefixes across those walks. Counted the `for (x = a; x != b; ++x)` loop. template >> void eval_sequence(ic_fn, const IcKey & key, Iter begin, Iter end, Buffer && buf, Memo && memo) { std::size_t i = 0; for (auto it = begin; it != end; ++it, ++i) buf[i] = detail::ic_impl::eval_one(key, *it, memo); } /// @brief Evaluate `[begin, end)`, with a fresh path memoizer. /// \complexity One `eval_one` per input from `from` through `to`. Each `eval_one` is two comparison point-walks, so this is not the truncated-tree interval walk. A path memoizer reuses prefixes across those walks. Counted the `for (x = a; x != b; ++x)` loop. template >> void eval_sequence(ic_fn, const IcKey & key, Iter begin, Iter end, Buffer && buf) { basic_path_memoizer memo; eval_sequence(ic, key, begin, end, std::forward(buf), memo); } /// @} /// @brief Buffer of `n` interval shares. /// @tparam IcKey interval-containment key type /// @param n the `n` /// @return Buffer of `n` interval shares template >> HEDLEY_WARN_UNUSED_RESULT auto make_output_buffer(ic_fn, const IcKey &, std::size_t n) { using beta = typename IcKey::beta_type; using elem = cmp_buffer_elem_t; return output_buffer(n); } /// @brief Buffer large enough for the inclusive interval `[from, to]`. /// @tparam IcKey interval-containment key type /// @tparam Lane input-domain lane type /// @param key the `key` /// @param from the inclusive start of the range /// @param to the `to` /// @return Buffer large enough for the inclusive interval `[from, to]` /// @throws std::invalid_argument if `to < from` template >> HEDLEY_WARN_UNUSED_RESULT auto make_output_buffer(ic_fn, const IcKey & key, Lane from, Lane to) { using in_type = typename IcKey::input_type; const uint64_t a = detail::ic_impl::bits_of(in_type(from)); const uint64_t b = detail::ic_impl::bits_of(in_type(to)); if (a > b) throw std::invalid_argument("ic interval: to < from"); const uint64_t n = b - a + 1ULL; return make_output_buffer(ic, key, static_cast(n)); } /// @name Interval geneval /// @tparam InputT input domain type /// @tparam Iter iterator type /// @tparam RootSampler sampler for the Doerner–Shelat root seed /// @tparam PadRng pad stream for the Doerner–Shelat protocol /// @tparam Beta payload type /// @param r0 party 0's share of the mask /// @param r1 party 1's share of the mask /// @param begin the iterator to the first query /// @param end the iterator past the last query /// @param rng the Doerner–Shelat randomness tapes /// @param spec the public bounds and payloads /// @return the opened party shares /// @{ namespace detail { namespace ic_impl { /// @brief Fill `out` from a verifiable IC key pair, reusing one path memoizer. template void geneval_ic_eval(geneval_cmp_result & out, const IcKey0 & k0, const IcKey1 & k1, Iter begin, Iter end) { using key_type = unwrap_party_key_t; constexpr std::size_t depth = key_type::depth; out.live_levels = depth; out.mask = k0.dpf_key.cmp().mask; out.cw_last = k0.dpf_key.cw_last(); out.addend0 = k0.dpf_key.cmp_addend().raw(); out.addend1 = k1.dpf_key.cmp_addend().raw(); out.correction_words.resize(depth); out.correction_advice.resize(depth); if constexpr (key_type::cmp_block > 0) { out.value_cw.resize(key_type::cmp_checkpoints); for (std::size_t i = 0; i < key_type::cmp_checkpoints; ++i) out.value_cw[i] = k0.dpf_key.value_cw(i); out.tail_cw.resize(key_type::cmp_tail); for (std::size_t z = 0; z < key_type::cmp_tail; ++z) out.tail_cw[z] = k0.dpf_key.tail_cw(z); } else out.value_cw.resize(depth); for (std::size_t level = 0; level < depth; ++level) { out.correction_words[level] = k0.dpf_key.correction_word(level); out.correction_advice[level] = static_cast(k0.dpf_key.correction_advice(level)); if constexpr (key_type::cmp_block == 0) out.value_cw[level] = k0.dpf_key.value_cw(level); } // Empty query lists keep default-constructed (zero) tokens. `verify` // rejects the all-zero token, so an empty geneval does not verify as // two matching zeros. detail::vdpf::init_proof(out.proof0, k0.dpf_key); detail::vdpf::init_proof(out.proof1, k1.dpf_key); auto path0 = make_basic_path_memoizer(k0.dpf_key); auto path1 = make_basic_path_memoizer(k1.dpf_key); for (auto it = begin; it != end; ++it) { out.party0.push_back(opened_u64( eval_one(k0, *it, path0, &out.proof0), out.mask)); out.party1.push_back(opened_u64( eval_one(k1, *it, path1, &out.proof1), out.mask)); } detail::vdpf::fold_output_binding(out.proof0, k0.dpf_key); detail::vdpf::fold_output_binding(out.proof1, k1.dpf_key); } } // namespace ic_impl } // namespace detail /// @brief XOR mask. `r0 XOR r1` is the secret mask. Each query is /// returned already combined into the interval share. /// @details Builds a verifiable inner comparison key and folds correction /// seeds into `proof0` / `proof1` while reusing one path memoizer per party. /// An empty query range leaves both tokens zero; those do not verify. template HEDLEY_WARN_UNUSED_RESULT geneval_cmp_result geneval_ic(InputT r0, InputT r1, Iter begin, Iter end, ds_randomness rng, const ic_pack & spec) { static_assert(!is_wildcard_v, "geneval_ic: payload must be concrete (assign_cmp is a separate step)"); geneval_cmp_result out; if (begin == end) return out; auto keys = make_dpf_doerner_shelat(std::move(r0), std::move(r1), std::move(rng), spec, dpf::verifiable{}); detail::ic_impl::geneval_ic_eval(out, keys.first, keys.second, begin, end); return out; } /// @brief Additive mask. `r0 + r1` is the secret mask. template HEDLEY_WARN_UNUSED_RESULT geneval_cmp_result geneval_ic(arith_input_t, InputT r0, InputT r1, Iter begin, Iter end, ds_randomness rng, const ic_pack & spec) { static_assert(!is_wildcard_v, "geneval_ic: payload must be concrete (assign_cmp is a separate step)"); geneval_cmp_result out; if (begin == end) return out; auto keys = make_dpf_doerner_shelat(arith_input, std::move(r0), std::move(r1), std::move(rng), spec, dpf::verifiable{}); detail::ic_impl::geneval_ic_eval(out, keys.first, keys.second, begin, end); return out; } /// @} // --------------------------------------------------------------------------- // Grow adaptations: run on the inner `dpf_key`, then refresh `cr_share` from // the secret mask `r` when depth changes (`add_output` copies the fields). // --------------------------------------------------------------------------- template HEDLEY_WARN_UNUSED_RESULT auto extend(const ic_key & k0, const ic_key & k1, uint64_t r, bool bit, InputT x, Specs &&... specs) { auto inner = dpf::extend(k0.dpf_key, k1.dpf_key, bit, x, std::forward(specs)...); using new_raw = typename std::decay_t::key_type; const uint64_t nmask = detail::ic_impl::input_mask_of(); const uint64_t gmask = k0.group_mask; using share_t = typename ic_key::share_type; share_t c0{}; share_t c1{}; if constexpr (is_wildcard_v) { // Wildcard: keep cr_coeff; concrete cr_share is filled by assign_cmp. c0 = k0.cr_share; c1 = k1.cr_share; } else if constexpr (detail::cmp_group_info::custom) { using prg = typename new_raw::interior_prg; const auto layout = detail::group_layout>(); const auto cr_g = detail::group_scalar( detail::ic_impl::correction_s(r, k0.lo, k0.hi, nmask), layout); const auto target = detail::group_add( detail::group_mul(k0.delta_share, cr_g), // wrong: need open δ detail::group_zero(layout)); (void)target; // Reconstruct δ = d0+d1, if_false from old cr, then new cr = δ·c_r + f. const auto delta = detail::group_add(k0.delta_share, k1.delta_share); const auto old_cr_open = detail::group_add(k0.cr_share, k1.cr_share); const auto old_cr_term = detail::group_mul(delta, detail::group_scalar( detail::ic_impl::correction_s(r, k0.lo, k0.hi, k0.input_mask), layout)); const auto if_false = detail::group_sub(old_cr_open, old_cr_term); const auto new_target = detail::group_add(detail::group_mul(delta, cr_g), if_false); const auto blind = detail::group_from_node( dpf::uniform_sample(), layout); c0 = blind; c1 = detail::group_sub(new_target, blind); } else { const uint64_t delta = (static_cast(k0.delta_share) + static_cast(k1.delta_share)) & gmask; const uint64_t old_cr_open = (static_cast(k0.cr_share) + static_cast(k1.cr_share)) & gmask; const uint64_t old_cr_term = detail::ic_impl::mul_mask(delta, detail::ic_impl::correction(r, k0.lo, k0.hi, k0.input_mask, gmask), gmask); const uint64_t if_false = (old_cr_open + detail::dcf_impl::neg_m(old_cr_term, gmask)) & gmask; const uint64_t cr = detail::ic_impl::correction(r, k0.lo, k0.hi, nmask, gmask); const uint64_t target = (detail::ic_impl::mul_mask(delta, cr, gmask) + if_false) & gmask; const uint64_t blind = detail::dcf_impl::sample_addend_blind(gmask, [] { return dpf::uniform_sample(); }); uint64_t a0 = 0, a1 = 0; detail::incr::split_cmp_addend(target, gmask, blind, a0, a1); c0 = static_cast(a0); c1 = static_cast(a1); } auto out0 = detail::ic_impl::make_side( std::move(inner.first), k0.lo, k0.hi, nmask, gmask, k0.delta_share, c0, k0.delta_coeff, k0.cr_coeff); auto out1 = detail::ic_impl::make_side( std::move(inner.second), k1.lo, k1.hi, nmask, gmask, k1.delta_share, c1, k1.delta_coeff, k1.cr_coeff); out0.assigned = k0.assigned; out1.assigned = k1.assigned; return std::make_pair(std::move(out0), std::move(out1)); } template HEDLEY_WARN_UNUSED_RESULT auto extend(const ic_key & k0, const ic_key & k1, uint64_t r, InputT x, Specs &&... specs) { const bool bit = detail::grow_impl::bit_at( static_cast(x), Key::depth); return extend(k0, k1, r, bit, x, std::forward(specs)...); } template HEDLEY_WARN_UNUSED_RESULT auto add_output(const ic_key & k0, const ic_key & k1, InputT x, Specs &&... specs) { auto inner = dpf::add_output(k0.dpf_key, k1.dpf_key, x, std::forward(specs)...); using new_raw = typename std::decay_t::key_type; auto out0 = detail::ic_impl::make_side( std::move(inner.first), k0.lo, k0.hi, k0.input_mask, k0.group_mask, k0.delta_share, k0.cr_share, k0.delta_coeff, k0.cr_coeff); auto out1 = detail::ic_impl::make_side( std::move(inner.second), k1.lo, k1.hi, k1.input_mask, k1.group_mask, k1.delta_share, k1.cr_share, k1.delta_coeff, k1.cr_coeff); out0.assigned = k0.assigned; out1.assigned = k1.assigned; return std::make_pair(std::move(out0), std::move(out1)); } template HEDLEY_WARN_UNUSED_RESULT auto extend(const ic_key & k0, const ic_key & k1, uint64_t r, Memo0 & m0, Memo1 & m1, bool bit, InputT x, Specs &&... specs) { auto inner = dpf::extend(k0.dpf_key, k1.dpf_key, m0, m1, bit, x, std::forward(specs)...); using new_raw = typename std::decay_t::key_type; const uint64_t nmask = detail::ic_impl::input_mask_of(); const uint64_t gmask = k0.group_mask; using share_t = typename ic_key::share_type; share_t c0{}; share_t c1{}; if constexpr (is_wildcard_v) { c0 = k0.cr_share; c1 = k1.cr_share; } else if constexpr (detail::cmp_group_info::custom) { using prg = typename new_raw::interior_prg; const auto layout = detail::group_layout>(); const auto cr_g = detail::group_scalar( detail::ic_impl::correction_s(r, k0.lo, k0.hi, nmask), layout); const auto delta = detail::group_add(k0.delta_share, k1.delta_share); const auto old_cr_open = detail::group_add(k0.cr_share, k1.cr_share); const auto old_cr_term = detail::group_mul(delta, detail::group_scalar( detail::ic_impl::correction_s(r, k0.lo, k0.hi, k0.input_mask), layout)); const auto if_false = detail::group_sub(old_cr_open, old_cr_term); const auto new_target = detail::group_add(detail::group_mul(delta, cr_g), if_false); const auto blind = detail::group_from_node( dpf::uniform_sample(), layout); c0 = blind; c1 = detail::group_sub(new_target, blind); } else { const uint64_t delta = (static_cast(k0.delta_share) + static_cast(k1.delta_share)) & gmask; const uint64_t old_cr_open = (static_cast(k0.cr_share) + static_cast(k1.cr_share)) & gmask; const uint64_t old_cr_term = detail::ic_impl::mul_mask(delta, detail::ic_impl::correction(r, k0.lo, k0.hi, k0.input_mask, gmask), gmask); const uint64_t if_false = (old_cr_open + detail::dcf_impl::neg_m(old_cr_term, gmask)) & gmask; const uint64_t cr = detail::ic_impl::correction(r, k0.lo, k0.hi, nmask, gmask); const uint64_t target = (detail::ic_impl::mul_mask(delta, cr, gmask) + if_false) & gmask; const uint64_t blind = detail::dcf_impl::sample_addend_blind(gmask, [] { return dpf::uniform_sample(); }); uint64_t a0 = 0, a1 = 0; detail::incr::split_cmp_addend(target, gmask, blind, a0, a1); c0 = static_cast(a0); c1 = static_cast(a1); } auto out0 = detail::ic_impl::make_side( std::move(inner.first), k0.lo, k0.hi, nmask, gmask, k0.delta_share, c0, k0.delta_coeff, k0.cr_coeff); auto out1 = detail::ic_impl::make_side( std::move(inner.second), k1.lo, k1.hi, nmask, gmask, k1.delta_share, c1, k1.delta_coeff, k1.cr_coeff); out0.assigned = k0.assigned; out1.assigned = k1.assigned; return std::make_pair(std::move(out0), std::move(out1)); } template HEDLEY_WARN_UNUSED_RESULT auto add_output(const ic_key & k0, const ic_key & k1, Memo0 & m0, Memo1 & m1, InputT x, Specs &&... specs) { auto inner = dpf::add_output(k0.dpf_key, k1.dpf_key, m0, m1, x, std::forward(specs)...); using new_raw = typename std::decay_t::key_type; auto out0 = detail::ic_impl::make_side( std::move(inner.first), k0.lo, k0.hi, k0.input_mask, k0.group_mask, k0.delta_share, k0.cr_share, k0.delta_coeff, k0.cr_coeff); auto out1 = detail::ic_impl::make_side( std::move(inner.second), k1.lo, k1.hi, k1.input_mask, k1.group_mask, k1.delta_share, k1.cr_share, k1.delta_coeff, k1.cr_coeff); out0.assigned = k0.assigned; out1.assigned = k1.assigned; return std::make_pair(std::move(out0), std::move(out1)); } template HEDLEY_WARN_UNUSED_RESULT auto extend_ds(const ic_key & k0, const ic_key & k1, uint64_t r, Memo0 & m0, Memo1 & m1, InputT x0, InputT x1, CwProtocol & proto, Specs &&... specs) { auto inner = dpf::extend_ds(k0.dpf_key, k1.dpf_key, m0, m1, x0, x1, proto, std::forward(specs)...); using new_raw = typename std::decay_t::key_type; const uint64_t nmask = detail::ic_impl::input_mask_of(); const uint64_t gmask = k0.group_mask; using share_t = typename ic_key::share_type; share_t c0{}; share_t c1{}; if constexpr (is_wildcard_v) { c0 = k0.cr_share; c1 = k1.cr_share; } else if constexpr (detail::cmp_group_info::custom) { using prg = typename new_raw::interior_prg; const auto layout = detail::group_layout>(); const auto cr_g = detail::group_scalar( detail::ic_impl::correction_s(r, k0.lo, k0.hi, nmask), layout); const auto delta = detail::group_add(k0.delta_share, k1.delta_share); const auto old_cr_open = detail::group_add(k0.cr_share, k1.cr_share); const auto old_cr_term = detail::group_mul(delta, detail::group_scalar( detail::ic_impl::correction_s(r, k0.lo, k0.hi, k0.input_mask), layout)); const auto if_false = detail::group_sub(old_cr_open, old_cr_term); const auto new_target = detail::group_add(detail::group_mul(delta, cr_g), if_false); const auto blind = detail::group_from_node( dpf::uniform_sample(), layout); c0 = blind; c1 = detail::group_sub(new_target, blind); } else { const uint64_t delta = (static_cast(k0.delta_share) + static_cast(k1.delta_share)) & gmask; const uint64_t old_cr_open = (static_cast(k0.cr_share) + static_cast(k1.cr_share)) & gmask; const uint64_t old_cr_term = detail::ic_impl::mul_mask(delta, detail::ic_impl::correction(r, k0.lo, k0.hi, k0.input_mask, gmask), gmask); const uint64_t if_false = (old_cr_open + detail::dcf_impl::neg_m(old_cr_term, gmask)) & gmask; const uint64_t cr = detail::ic_impl::correction(r, k0.lo, k0.hi, nmask, gmask); const uint64_t target = (detail::ic_impl::mul_mask(delta, cr, gmask) + if_false) & gmask; const uint64_t blind = detail::dcf_impl::sample_addend_blind(gmask, [] { return dpf::uniform_sample(); }); uint64_t a0 = 0, a1 = 0; detail::incr::split_cmp_addend(target, gmask, blind, a0, a1); c0 = static_cast(a0); c1 = static_cast(a1); } auto out0 = detail::ic_impl::make_side( std::move(inner.first), k0.lo, k0.hi, nmask, gmask, k0.delta_share, c0, k0.delta_coeff, k0.cr_coeff); auto out1 = detail::ic_impl::make_side( std::move(inner.second), k1.lo, k1.hi, nmask, gmask, k1.delta_share, c1, k1.delta_coeff, k1.cr_coeff); out0.assigned = k0.assigned; out1.assigned = k1.assigned; return std::make_pair(std::move(out0), std::move(out1)); } template HEDLEY_WARN_UNUSED_RESULT auto add_output_ds(const ic_key & k0, const ic_key & k1, Memo0 & m0, Memo1 & m1, InputT x0, InputT x1, CwProtocol & proto, Specs &&... specs) { auto inner = dpf::add_output_ds(k0.dpf_key, k1.dpf_key, m0, m1, x0, x1, proto, std::forward(specs)...); using new_raw = typename std::decay_t::key_type; auto out0 = detail::ic_impl::make_side( std::move(inner.first), k0.lo, k0.hi, k0.input_mask, k0.group_mask, k0.delta_share, k0.cr_share, k0.delta_coeff, k0.cr_coeff); auto out1 = detail::ic_impl::make_side( std::move(inner.second), k1.lo, k1.hi, k1.input_mask, k1.group_mask, k1.delta_share, k1.cr_share, k1.delta_coeff, k1.cr_coeff); out0.assigned = k0.assigned; out1.assigned = k1.assigned; return std::make_pair(std::move(out0), std::move(out1)); } } // namespace dpf #endif // LIBDPF_INCLUDE_DPF_INTERVAL_HPP__