/// @file dpf/doerner_shelat.hpp /// @brief Doerner–Shelat generation of a dealer DPF key. /// @details Two shares of the point are walked level by level — XOR shares by /// default. `arith_input` holds additive shares of the point. A beaver /// ripple-carry converts them to XOR shares of the sum bits, and that /// sharing is what the walk consumes. The sum is not opened. /// Correction words, advice bits, seeds, and leaves are the ones /// `make_dpf` would emit for that point, the same roots, and the same /// beaver coins. Beaver pads used to hide a path bit cancel and are /// not part of the key. Pad randomness must not come from /// `uniform_fill` if the beaver tape is being matched. /// @note Following Jack Doerner and abhi shelat, CCS 2017 (ePrint 2017/827): one correction word opened per level from shares of the point. /// @note Guo, Yang, Wang, Zhang, Xie, Zhang, and Liu (ePrint 2022/1431, §5.2) generate a DPF in the COT/OLE hybrid in n+3 rounds, with no beaver-pad dealer. This header uses that dealer tape and one opening round per level. /// @copyright Copyright (c) 2019-2026 Ryan Henry and [others](@ref authors) /// @license Released under a GNU General Public v2.0 (GPLv2) license; /// see [LICENSE.md](@ref license) for details. #ifndef LIBDPF_INCLUDE_DPF_DOERNER_SHELAT_HPP__ #define LIBDPF_INCLUDE_DPF_DOERNER_SHELAT_HPP__ #include #include #include #include #include #include #include "hedley/hedley.h" #include "simde/simde/x86/avx2.h" #include "dpf/dpf_key.hpp" #include "dpf/experiment_note.hpp" #include "dpf/random.hpp" #include "dpf/dcf.hpp" #include "dpf/constrained_cmp.hpp" #include "dpf/beaver.hpp" #include "dpf/xor_wrapper.hpp" #include "dpf/leaf_node.hpp" #include "dpf/leaf_arithmetic.hpp" namespace dpf { /// @brief Tag: Doerner–Shelat / geneval takes additive shares of the point /// (`x0 + x1` in the input ring). Default calls take XOR shares. struct arith_input_t { }; inline constexpr arith_input_t arith_input{}; /// @brief Tag: payload β is additively shared (`y0 + y1`). Leaf CW is opened via /// Π_CCMP on the on-path control bits (see `open_arith_leaf`). /// @see `open_arith_leaf` struct arith_output_t { }; inline constexpr arith_output_t arith_output{}; /// @brief Additive (or XOR) shares of one concrete payload for dealerless leaf open. /// @details Use as a placed value / `at<>` element when several outputs are shared. /// @tparam T value type template struct arith_beta { using payload_type = T; T y0{}; T y1{}; }; template struct is_arith_beta : std::false_type { }; template struct is_arith_beta> : std::true_type { }; template inline constexpr bool is_arith_beta_v = is_arith_beta>::value; namespace detail { namespace incr { /// @brief `placed>::output_type` is `T` (see placement.hpp). /// @tparam T value type template struct unwrap_placed_output> { using type = T; }; } // namespace incr } // namespace detail /// @brief Roots and the Beaver-pad stream for one Doerner–Shelat generation. /// @details `root` is called twice, same as `make_dpf`: party 0 clears the low bit of /// the first sample, party 1 sets the low bit of the second. /// @tparam RootSampler sampler for the Doerner–Shelat root seed /// @tparam PadRng pad stream for the Doerner–Shelat protocol template struct ds_randomness { RootSampler root; PadRng pad; }; /// @brief Pad stream whose `block()` and `bit()` come from one PRG seed. /// @details Drop-in for `detail::urandom_pad_rng` on Doerner–Shelat dealers. /// `pseudorandom_root_sampler` is the matching root source. template struct prg_pad_rng { using block_type = typename PRG::block_type; explicit prg_pad_rng(block_type seed = dpf::uniform_sample()) : seed_(seed) { note_experiment_seed("prg_pad_rng", seed_); } block_type block() { return PRG::eval(seed_, n_++); } uint8_t bit() { const block_type drawn = block(); unsigned char low = 0; std::memcpy(&low, &drawn, 1); return static_cast(low & 1u); } const block_type & seed() const noexcept { return seed_; } private: block_type seed_{}; std::uint32_t n_ = 0; }; namespace detail { struct urandom_pad_rng { simde__m128i block() { return dpf::uniform_sample(); } uint8_t bit() { return static_cast(dpf::uniform_sample() & 1u); } }; struct ds_cw_party { simde__m128i rand; simde__m128i gamma; uint8_t bit; }; struct ds_cw_pads { ds_cw_party p0; ds_cw_party p1; }; struct ds_blind { simde__m128i msg; uint8_t bit; }; struct ds_and_pads { uint8_t a0; uint8_t a1; simde__m128i b0_share, b1_share, c0_share, c1_share; }; struct ds_and_shares { simde__m128i z0; simde__m128i z1; }; HEDLEY_NO_THROW HEDLEY_ALWAYS_INLINE simde__m128i ds_xor(simde__m128i a, simde__m128i b) noexcept { return simde_mm_xor_si128(a, b); } HEDLEY_NO_THROW HEDLEY_ALWAYS_INLINE simde__m128i ds_gate(uint8_t bit, simde__m128i block) noexcept { return dpf::get_if(block, bit & 1u); } template ds_cw_pads ds_sample_cw(PadRng & pad) { // Ideal (semi-honest): each party holds (rand, bit, gamma) with // gamma0 ⊕ gamma1 = (bit1 · rand0) ⊕ (bit0 · rand1). // The full product (bit1 · rand0) is NOT given to party 0: that would // leak bit1, and with the opened blind bit = path1 ⊕ bit1 it would // open the peer path bit (and thus α under an oblivious walk). Shares // of the XOR of both products hide both pad bits from each party. // The product share is a beaver session over the XOR ring; the clear // bit and block stay with the party that owns them. using Ring = dpf::xor_wrapper; using traits = dpf::beavers::ring_traits; auto to_ring = [](simde__m128i block) { simde_uint128 raw{}; std::memcpy(&raw, &block, sizeof(block)); return Ring{raw}; }; auto to_block = [](const Ring & ring) { simde__m128i block{}; const auto raw = static_cast(ring); std::memcpy(&block, &raw, sizeof(block)); return block; }; const Ring rand0 = to_ring(pad.block()); const Ring rand1 = to_ring(pad.block()); const bool bit0 = (pad.bit() & 1u) != 0; const bool bit1 = (pad.bit() & 1u) != 0; auto sampler = [&pad, &to_ring]() -> Ring { return to_ring(pad.block()); }; dpf::beavers::session s; auto b0 = s.bit(); auto b1 = s.bit(); auto r0 = s.input(); auto r1 = s.input(); auto prod = s(b1 * r0 + b0 * r1); s.pin(prod); s.sample(sampler); s.bind(b0, bit0 ? traits::one() : traits::zero(), sampler); s.bind(b1, bit1 ? traits::one() : traits::zero(), sampler); s.bind(r0, rand0, sampler); s.bind(r1, rand1, sampler); s.evaluate(); const auto gamma = s.value(prod); ds_cw_pads p{}; p.p0.rand = to_block(rand0); p.p1.rand = to_block(rand1); p.p0.bit = static_cast(bit0); p.p1.bit = static_cast(bit1); p.p0.gamma = to_block(gamma.p0); p.p1.gamma = to_block(gamma.p1); return p; } /// @brief Pack a XOR-ring `bit_mul` into the classical DS AND pad shape. /// @tparam Ring XOR ring whose unit is the all-ones word /// @param bm the sampled bit-mul material /// @param a0_share party 0's XOR share of the opened bit /// @return pads ready for `ds_and_open` template ds_and_pads ds_and_from_bit_mul(const dpf::beavers::bit_mul_beaver & bm, uint8_t a0_share) { using traits = dpf::beavers::ring_traits; const Ring opened = bm.bit.open(); const uint8_t a = (opened == traits::one()) ? uint8_t{1} : uint8_t{0}; ds_and_pads p{}; p.a0 = static_cast(a0_share & 1u); p.a1 = static_cast(a ^ p.a0); auto to_block = [](const Ring & r) { simde__m128i b{}; const auto v = static_cast(r); static_assert(sizeof(v) == sizeof(simde__m128i), "ds AND packs a 128-bit XOR ring into an AES block"); std::memcpy(&b, &v, sizeof(b)); return b; }; p.b0_share = to_block(bm.scalar.p0); p.b1_share = to_block(bm.scalar.p1); p.c0_share = to_block(bm.product.p0); p.c1_share = to_block(bm.product.p1); return p; } /// @brief Sample one DS AND from `sample_bit_mul`, driven by `pad` or `rng`. /// @tparam PadRng pad stream with `block()` / `bit()` /// @tparam Sample callable returning a 128-bit XOR ring element /// @param pad the Doerner–Shelat pad stream (bit for the clear Beaver bit) /// @param rng ring sampler for `sample_bit_mul` (defaults to `pad.block()`) /// @return classical AND pads for `ds_and_open` template ds_and_pads ds_sample_and(PadRng & pad, Sample && rng) { using Ring = dpf::xor_wrapper; using traits = dpf::beavers::ring_traits; auto & ring_rng = rng; std::size_t phase = 0; auto sampler = [&]() -> Ring { if (phase == 0) { ++phase; return (pad.bit() & 1u) ? traits::one() : traits::zero(); } ++phase; return ring_rng(); }; auto bm = dpf::beavers::sample_bit_mul(sampler); const uint8_t a0 = static_cast(pad.bit() & 1u); return ds_and_from_bit_mul(bm, a0); } template ds_and_pads ds_sample_and(PadRng & pad) { using Ring = dpf::xor_wrapper; auto from_pad = [&pad]() -> Ring { const simde__m128i b = pad.block(); simde_uint128 v{}; std::memcpy(&v, &b, sizeof(b)); return Ring{v}; }; return ds_sample_and(pad, from_pad); } HEDLEY_NO_THROW HEDLEY_ALWAYS_INLINE simde__m128i ds_cw_share(simde__m128i L, simde__m128i R, uint8_t my_bit, const ds_cw_party & mine, const ds_blind & their) noexcept { simde__m128i out = ds_xor(R, mine.gamma); if (my_bit & 1u) { out = ds_xor(out, ds_xor(ds_xor(L, R), their.msg)); } if (their.bit & 1u) { out = ds_xor(out, mine.rand); } return out; } HEDLEY_NO_THROW inline void ds_cw_blinds(const ds_cw_pads & p, simde__m128i L0, simde__m128i R0, uint8_t bit0, simde__m128i L1, simde__m128i R1, uint8_t bit1, ds_blind & b0, ds_blind & b1) noexcept { b0.bit = static_cast(bit0 ^ p.p0.bit); b1.bit = static_cast(bit1 ^ p.p1.bit); b0.msg = ds_xor(ds_xor(L0, R0), p.p0.rand); b1.msg = ds_xor(ds_xor(L1, R1), p.p1.rand); } HEDLEY_NO_THROW inline simde__m128i ds_cw_outs(const ds_cw_pads & p, simde__m128i L0, simde__m128i R0, uint8_t bit0, simde__m128i L1, simde__m128i R1, uint8_t bit1, const ds_blind & b0, const ds_blind & b1) noexcept { return ds_xor( ds_cw_share(L0, R0, bit0, p.p0, b1), ds_cw_share(L1, R1, bit1, p.p1, b0)); } HEDLEY_NO_THROW inline uint8_t ds_open_advice(simde__m128i L0, simde__m128i R0, uint8_t bit0, simde__m128i L1, simde__m128i R1, uint8_t bit1) noexcept { const uint8_t a00 = static_cast(dpf::get_lo_bit(L0) ^ bit0); const uint8_t a01 = static_cast(dpf::get_lo_bit(R0) ^ bit0); const uint8_t a10 = static_cast(dpf::get_lo_bit(L1) ^ bit1); const uint8_t a11 = static_cast(dpf::get_lo_bit(R1) ^ bit1); const uint8_t t0 = static_cast(a00 ^ a10 ^ 1u); const uint8_t t1 = static_cast(a01 ^ a11); return static_cast((t1 << 1) | (t0 & 1u)); } HEDLEY_NO_THROW inline void ds_next_terms(simde__m128i L, simde__m128i R, uint8_t advice, simde__m128i cw, uint8_t tpack, simde__m128i & M, simde__m128i & base) noexcept { const simde__m128i D = ds_xor(L, R); const uint8_t t0 = static_cast(tpack & 1u); const uint8_t t1 = static_cast((tpack >> 1) & 1u); const simde__m128i lo = dpf::set_lo_bit(simde_mm_setzero_si128(), 1); const simde__m128i DT = ds_gate(static_cast(t0 ^ t1), lo); const simde__m128i cw_base = ds_xor(dpf::unset_lo_bit(cw), ds_gate(t0, lo)); M = (advice & 1u) ? ds_xor(D, DT) : D; base = (advice & 1u) ? ds_xor(L, cw_base) : L; } HEDLEY_NO_THROW inline ds_and_shares ds_and_open(const ds_and_pads & p, simde__m128i M, uint8_t b_recv) noexcept { const simde__m128i e = ds_xor(ds_xor(M, p.b0_share), p.b1_share); const uint8_t d = static_cast((b_recv ^ p.a1) ^ p.a0); ds_and_shares z; z.z0 = ds_xor(ds_xor(ds_xor(ds_gate(d, e), ds_gate(d, p.b0_share)), ds_gate(p.a0, e)), p.c0_share); z.z1 = ds_xor(ds_xor(ds_gate(d, p.b1_share), ds_gate(p.a1, e)), p.c1_share); return z; } HEDLEY_NO_THROW inline simde__m128i ds_deliver(uint8_t b_exp, simde__m128i base, simde__m128i M, const ds_and_shares & z) noexcept { const simde__m128i local = ds_xor(base, ds_gate(b_exp, M)); return ds_xor(ds_xor(local, z.z0), z.z1); } /// @brief XOR shares of a bit-Beaver triple `(α, β, α∧β)`. struct ds_bit_triple { uint8_t a0; uint8_t a1; uint8_t b0; uint8_t b1; uint8_t c0; uint8_t c1; }; /// @brief Sample one bit-AND triple from `pad`, via `sample_beaver2` on the XOR ring. /// @tparam PadRng pad stream with `bit()` /// @param pad the pad stream /// @return shares of `(α, β, α∧β)` template ds_bit_triple ds_sample_bit_and(PadRng & pad) { using Bit = dpf::xor_wrapper; auto sampler = [&pad]() -> Bit { std::uint8_t packed = 0; for (int i = 0; i < 8; ++i) { packed = static_cast( packed | (static_cast(pad.bit() & 1u) << i)); } return Bit{packed}; }; const auto triple = dpf::beavers::sample_beaver2(sampler); auto low = [](Bit x) { return static_cast(static_cast(x) & 1u); }; return ds_bit_triple{ low(triple.a.p0), low(triple.a.p1), low(triple.b.p0), low(triple.b.p1), low(triple.ab.p0), low(triple.ab.p1)}; } /// @brief One party's share of `x ∧ y` after `d = x⊕α` and `e = y⊕β` are open. /// @param d the opened mask of `x` /// @param e the opened mask of `y` /// @param a this party's share of `α` /// @param b this party's share of `β` /// @param c this party's share of `α∧β` /// @param hold_de party 0 adds the public `d∧e` term /// @return this party's XOR share of the product HEDLEY_NO_THROW inline uint8_t ds_bit_and_party(uint8_t d, uint8_t e, uint8_t a, uint8_t b, uint8_t c, bool hold_de) noexcept { uint8_t z = static_cast((d & b) ^ (e & a) ^ c); if (hold_de) z = static_cast(z ^ (d & e)); return static_cast(z & 1u); } /// @brief Joint evaluation of one bit-AND. `d` and `e` are the opened masks. /// @param t the triple /// @param x0 party 0's share of `x` /// @param x1 party 1's share of `x` /// @param y0 party 0's share of `y` /// @param y1 party 1's share of `y` /// @return XOR shares of `x ∧ y` HEDLEY_NO_THROW inline std::pair ds_bit_and_shares(const ds_bit_triple & t, uint8_t x0, uint8_t x1, uint8_t y0, uint8_t y1) noexcept { const uint8_t d = static_cast(x0 ^ x1 ^ t.a0 ^ t.a1); const uint8_t e = static_cast(y0 ^ y1 ^ t.b0 ^ t.b1); return {ds_bit_and_party(d, e, t.a0, t.b0, t.c0, true), ds_bit_and_party(d, e, t.a1, t.b1, t.c1, false)}; } /// @brief Replace additive shares with XOR shares of their sum. /// @details One beaver bit-AND per bit except the last. Party 0's sum-bit /// share is `a ⊕ c0`; party 1's is `b ⊕ c1`. The carry share is /// `((a⊕c) ∧ (b⊕c)) ⊕ c`, which is the majority. Neither share is /// the sum, and the sum is not written down. /// @tparam PadRng pad stream with `bit()` /// @tparam InputT input domain type /// @param pads the pad stream /// @param x0 party 0's additive share, replaced by its XOR share of the sum /// @param x1 party 1's additive share, replaced by its XOR share of the sum template void split_additive_to_xor(PadRng & pads, InputT & x0, InputT & x1) { constexpr auto to_int = utils::to_integral_type{}; using FromI = typename utils::make_from_integral_value::integral_type; using U = std::make_unsigned_t; const U u0 = static_cast(to_int(x0)); const U u1 = static_cast(to_int(x1)); U s0 = 0; U s1 = 0; uint8_t c0 = 0; uint8_t c1 = 0; constexpr std::size_t nbits = utils::bitlength_of_v; for (std::size_t i = 0; i < nbits; ++i) { const uint8_t a = static_cast((u0 >> i) & U{1}); const uint8_t b = static_cast((u1 >> i) & U{1}); const uint8_t sum0 = static_cast(a ^ c0); const uint8_t sum1 = static_cast(b ^ c1); s0 = static_cast(s0 | (static_cast(sum0) << i)); s1 = static_cast(s1 | (static_cast(sum1) << i)); if (i + 1 == nbits) break; const ds_bit_triple triple = ds_sample_bit_and(pads); const auto prod = ds_bit_and_shares(triple, static_cast(a ^ c0), c1, c0, static_cast(b ^ c1)); c0 = static_cast(prod.first ^ c0); c1 = static_cast(prod.second ^ c1); } x0 = utils::make_from_integral_value{}(static_cast(s0)); x1 = utils::make_from_integral_value{}(static_cast(s1)); } /// @brief Per-level messages prepared before the CW protocol runs (blinds + pads). struct ds_level_blinds { ds_cw_pads cwp; ds_and_pads and0; ds_and_pads and1; ds_blind b0; ds_blind b1; simde__m128i L0, R0, L1, R1; uint8_t bit0; uint8_t bit1; }; /// @brief Opened CW, advice, and AND products delivered by a `CwProtocol`. struct ds_level_open { simde__m128i cw; uint8_t advice; ds_and_shares z0; ds_and_shares z1; uint64_t value_cw = 0; // public after open when cmp is active at this level }; /// @brief Running comparison-gen state shared across DS levels (Va residual). /// @details When `track_coeff` is set (wildcard cmp payload), a parallel β = 1 /// accumulator `Va1` is advanced alongside `Va` so the gen can stash /// `value_cw(1) − value_cw(β)` coefficients for a later `assign_cmp`. struct ds_cmp_gen_state { bool active = false; std::size_t nbits = 0; uint64_t mask = 0; uint64_t beta = 0; bool include_eq = false; cmp_trivial trivial = cmp_trivial::none; uint64_t Va = 0; unsigned __int128 thresh = 0; bool track_coeff = false; uint64_t Va1 = 0; uint64_t last_vcw_coeff = 0; cmp_kind kind = cmp_kind::lt; bool paint = false; std::size_t length_bits = 0; paint_callback paint_cb = nullptr; const void * paint_ctx = nullptr; }; /// @brief Local joint-sim mux of a packed naked leaf from XOR bit shares. /// @details Mirrors `party/oblivious_select.hpp` `mux_leaf_share`: each level /// selects with `bit0 ^ bit1` so the call site never forms a clear /// point for `make_leaves`. The joint simulator holds both shares. /// @tparam Leaf packed leaf type /// @tparam Make candidate builder `Leaf(unsigned lane)` /// @tparam Bit0 party-0 bit accessor /// @tparam Bit1 party-1 bit accessor template Leaf mux_naked_leaf_local(std::size_t lg, Make && make, Bit0 && bit0_at, Bit1 && bit1_at) { if (lg == 0) return make(0u); std::vector cand(std::size_t{1} << lg); for (std::size_t i = 0; i < cand.size(); ++i) cand[i] = make(static_cast(i)); for (std::size_t b = 0; b < lg; ++b) { const uint8_t bit = static_cast( (bit0_at(b) ^ bit1_at(b)) & 1u); std::vector next(cand.size() / 2); for (std::size_t k = 0; k < next.size(); ++k) next[k] = bit ? cand[2 * k + 1] : cand[2 * k]; cand.swap(next); } return cand[0]; } /// @brief Local joint simulation: today's `ds_cw_outs` / `ds_open_advice` / `ds_and_open`. /// @details An MPC backend would send `blinds` and return the same `ds_level_open` shape. /// @tparam PadRng pad stream for the Doerner–Shelat protocol template struct local_cw_protocol { PadRng & pads; ds_level_blinds prepare_level(simde__m128i L0, simde__m128i R0, uint8_t bit0, simde__m128i L1, simde__m128i R1, uint8_t bit1) { ds_level_blinds b; b.cwp = ds_sample_cw(pads); b.and0 = ds_sample_and(pads); b.and1 = ds_sample_and(pads); b.L0 = L0; b.R0 = R0; b.L1 = L1; b.R1 = R1; b.bit0 = bit0; b.bit1 = bit1; ds_cw_blinds(b.cwp, L0, R0, bit0, L1, R1, bit1, b.b0, b.b1); return b; } ds_level_open complete_level(std::size_t /*level*/, const ds_level_blinds & b, simde__m128i M0, simde__m128i M1, uint8_t rec0, uint8_t rec1) { ds_level_open out; out.advice = ds_open_advice(b.L0, b.R0, b.bit0, b.L1, b.R1, b.bit1); out.cw = ds_cw_outs(b.cwp, b.L0, b.R0, b.bit0, b.L1, b.R1, b.bit1, b.b0, b.b1); out.z0 = ds_and_open(b.and0, M0, rec0); out.z1 = ds_and_open(b.and1, M1, rec1); return out; } /// @brief Open CW + advice only (AND pads stay in `blinds` for a later open). /// @param b the `b` /// @return the returned `std::pair` HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") HEDLEY_NO_THROW std::pair open_cw(const ds_level_blinds & b) noexcept { return {ds_cw_outs(b.cwp, b.L0, b.R0, b.bit0, b.L1, b.R1, b.bit1, b.b0, b.b1), ds_open_advice(b.L0, b.R0, b.bit0, b.L1, b.R1, b.bit1)}; } HEDLEY_PRAGMA(GCC diagnostic pop) /// @brief Open the public value CW for this level (local: clear convert+make_value_cw). /// @details MPC backends open additive shares of the same word. /// @param b the `b` /// @param adv0 the `adv0` /// @param adv1 the `adv1` /// @param ai the `ai` /// @param Va the `Va` /// @param beta the payload /// @param mask the bit mask /// @return the returned `uint64_t` HEDLEY_NO_THROW uint64_t open_value_cw(const ds_level_blinds & b, uint8_t adv0, uint8_t adv1, int ai, uint64_t & Va, uint64_t beta, uint64_t mask) noexcept { return dcf_impl::make_value_cw(b.L0, b.R0, b.L1, b.R1, adv0, adv1, ai, Va, beta, mask); } /// @brief Open a path-paint value CW. `plant` is the scaled lose-subtree constant. /// @param b the `b` /// @param adv0 the `adv0` /// @param adv1 the `adv1` /// @param ai the `ai` /// @param Va the `Va` /// @param plant the unit plant /// @param mask the bit mask /// @return the returned `uint64_t` HEDLEY_NO_THROW uint64_t open_planted_cw(const ds_level_blinds & b, uint8_t adv0, uint8_t adv1, int ai, uint64_t & Va, uint64_t plant, uint64_t mask) noexcept { return dcf_impl::make_value_cw_planted(b.L0, b.R0, b.L1, b.R1, adv0, adv1, ai, Va, plant, mask); } HEDLEY_NO_THROW ds_and_shares open_and(const ds_and_pads & p, simde__m128i M, uint8_t b_recv) noexcept { return ds_and_open(p, M, b_recv); } /// @brief Open the final comparison leaf CW. Wraps `make_final_cw` so the /// Doerner–Shelat gen does not call it directly on reconstructed seeds; /// an MPC backend would open additive shares of the same word. /// @param s0 the `s0` /// @param s1 the `s1` /// @param t1 the `t1` /// @param Va the `Va` /// @param mask the bit mask /// @param on_path the value reconstructed on the secret path /// @return the returned `uint64_t` HEDLEY_NO_THROW uint64_t open_final_cw(simde__m128i s0, simde__m128i s1, uint8_t t1, uint64_t Va, uint64_t mask, uint64_t on_path) noexcept { return dcf_impl::make_final_cw(s0, s1, t1, Va, mask, on_path); } /// @brief Draw the group-width `cmp_addend` blind. Local joint simulation reuses /// the shared root sampler so the blind matches the dealer's; an MPC /// backend would instead pull a group-width element from the pad stream. /// @tparam BlockSampler block sampler /// @param mask the bit mask /// @param sample the `sample` /// @return the returned `uint64_t` template HEDLEY_NO_THROW uint64_t sample_addend_blind(uint64_t mask, BlockSampler && sample) noexcept { return dcf_impl::sample_addend_blind(mask, std::forward(sample)); } /// @brief Encode shares for the XOR-style CW walk. /// @details XOR inputs flip party 0's MSB, which is linear over XOR. /// Additive inputs are converted first: `split_additive_to_xor` /// draws one bit-Beaver per carry and leaves XOR shares of the /// sum. The MSB flip is then the same one XOR inputs take, so /// the walk matches `make_dpf` on the sum. The sum is not opened /// and party 1's share is not cleared. /// @tparam InputT input domain type /// @param x0 party 0's share /// @param x1 party 1's share /// @param arith `true` when `x0`, `x1` are additive template void encode_walk_shares(InputT & x0, InputT & x1, bool arith) { if (arith) split_additive_to_xor(pads, x0, x1); utils::flip_msb_if_signed_integral(x0); } /// @brief Constrained comparison Π_CCMP: open `1{x0 < x1}` when `|x0−x1|=1`. /// @param x0 the `x0` /// @param x1 the `x1` /// @return Constrained comparison Π_CCMP: open `1{x0 < x1}` when `|x0−x1|=1` HEDLEY_NO_THROW uint8_t open_ccmp(std::uint64_t x0, std::uint64_t x1) noexcept { (void)pads; // MPC backend would consume an AND pad here return dpf::local_ccmp(x0, x1); } /// @brief Open a public leaf CW for a shared payload. /// @details Splits the payload into leaf words (`naked(y0) ± naked(y1)`) so /// scalar `β = y0 + y1` (or `y0 ⊕ y1`) is never formed. The packing /// lane is muxed from XOR bit shares of the point, matching /// `mux_leaf_share` on the socket. Ring: `g = CCMP(t0,t1)` selects /// `N − M` vs `M − N` (matches `make_leaf` with `sign = t0`). /// Characteristic 2: CW = `N ⊕ M` (sign mux is a no-op under XOR). /// @tparam ExteriorPRG PRG that expands the root. Defaults to `InteriorPRG` /// @tparam I output index /// @tparam OutputsTuple outputs tuple /// @tparam InteriorBlock interior block /// @tparam OutputT output type /// @tparam InputT input domain type /// @param seed0 the `seed0` /// @param seed1 the `seed1` /// @param t0 the `t0` /// @param t1 the `t1` /// @param y0 the `y0` /// @param y1 the `y1` /// @param pos_base the `pos_base` /// @param x0 party 0's XOR share of the (lane) point /// @param x1 party 1's XOR share of the (lane) point /// @return the opened leaf correction word template auto open_arith_leaf(const InteriorBlock & seed0, const InteriorBlock & seed1, uint8_t t0, uint8_t t1, OutputT y0, OutputT y1, std::size_t pos_base, InputT x0, InputT x1) -> dpf::leaf_node_t { using output_type = OutputT; using node_type = typename ExteriorPRG::block_type; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") using leaf_type = dpf::leaf_node_t; HEDLEY_PRAGMA(GCC diagnostic pop) constexpr std::size_t lg = dpf::lg_outputs_per_leaf_v; constexpr auto to_int = utils::to_integral_type{}; auto bit0_at = [&](std::size_t b) { return static_cast((to_int(x0) >> b) & 1u); }; auto bit1_at = [&](std::size_t b) { return static_cast((to_int(x1) >> b) & 1u); }; auto naked_of = [&](output_type y) { return mux_naked_leaf_local(lg, [&](unsigned i) { return dpf::make_naked_leaf( static_cast(i), y); }, bit0_at, bit1_at); }; // Split payload across leaf words; never form scalar β. const leaf_type naked = dpf::add_leaf(naked_of(y0), naked_of(y1)); const auto M = dpf::make_leaf_mask( seed0, seed1, pos_base); if constexpr (utils::has_characteristic_two_v) { (void)t0; (void)t1; return dpf::subtract_leaf(naked, M); } else { const uint8_t g = open_ccmp(t0, t1); // CW = (−1)^{t1}(N − M): g=0 → N−M; g=1 → M−N. Matches make_leaf(sign=t0). if (g & 1u) return dpf::subtract_leaf(M, naked); return dpf::subtract_leaf(naked, M); } } /// @brief Open the comparison threshold lane from XOR shares of the point. /// @details Reconstructs only inside this protocol hook for paint units, /// domain-edge triviality, and blocked suffixes. Per-level value /// words use share bits (`bit0 ^ bit1`) instead of this value. /// @tparam InputT input domain type /// @param x0 party 0's XOR share /// @param x1 party 1's XOR share /// @param nbits width of the comparison lane /// @return the comparison threshold as an integer lane template unsigned __int128 open_cmp_threshold(InputT x0, InputT x1, std::size_t nbits) noexcept { constexpr auto to_int = utils::to_integral_type{}; constexpr std::size_t bl = utils::bitlength_of_v; const InputT x = utils::xor_input_shares(x0, x1); if (nbits >= bl) return static_cast(to_int(x)); return static_cast(to_int(x) >> (bl - nbits)); } /// @brief Public correction seed from XOR shares of the path prefix. /// @details Reconstructs the prefix only inside this protocol hook and /// returns `make_cs` (same digest as `oblivious_cs` when both /// seeds are in-process). The clear prefix is not returned. /// @tparam InputT input domain type /// @param level fold level (or blocked tag | level) /// @param x0 party 0's XOR share of the encoded point /// @param x1 party 1's XOR share of the encoded point /// @param bits number of high path bits in the prefix /// @param s0 party 0's on-path seed /// @param s1 party 1's on-path seed /// @return the public correction seed template cs_block open_correction_seed(std::size_t level, InputT x0, InputT x1, std::size_t bits, simde__m128i s0, simde__m128i s1) noexcept { constexpr auto to_int = utils::to_integral_type{}; constexpr std::size_t bl = utils::bitlength_of_v; const InputT x = utils::xor_input_shares(x0, x1); // Prefer `uint64_t` over `psnip_uint64_t{...}`: that macro expands to // `long unsigned int`, which is not a valid braced/cast type-id alone. const uint64_t prefix = (bits == 0 || bits > bl) ? uint64_t{0} : static_cast(to_int(x) >> (bl - bits)); return detail::vdpf::make_cs(level, prefix, s0, s1); } /// @brief Open a group of leaf correction words for one prefix group. /// @details Hands both XOR shares to `leaf_fn`. The builder may reconstruct /// the point only to emit public CWs (never return α to the DS /// call site). An MPC backend would run a per-group leaf CW /// exchange that never reveals `x`. Additive inputs have already /// been replaced by XOR shares of the sum. /// @tparam InputT input domain type /// @tparam LeafFn leaf fn /// @param x0 the `x0` /// @param x1 the `x1` /// @param leaf_fn the `leaf_fn` template void open_leaf_group(InputT x0, InputT x1, LeafFn && leaf_fn) { std::forward(leaf_fn)(x0, x1); } }; /// @brief Generation-side level state (seeds / home bits). Not an eval path memoizer. /// @tparam NodeT GGM node type template struct ds_gen_state { NodeT inbox[2]; int home[2]; NodeT root0; NodeT root1; HEDLEY_NO_THROW void init(NodeT r0, NodeT r1) noexcept { root0 = r0; root1 = r1; inbox[0] = r0; inbox[1] = r1; home[0] = 0; home[1] = 1; } HEDLEY_NO_THROW NodeT & seed0() noexcept { return inbox[home[0]]; } HEDLEY_NO_THROW NodeT & seed1() noexcept { return inbox[home[1]]; } HEDLEY_NO_THROW const NodeT & seed0() const noexcept { return inbox[home[0]]; } HEDLEY_NO_THROW const NodeT & seed1() const noexcept { return inbox[home[1]]; } }; /// @brief One interior level: expand, protocol open, advance both party seeds. /// @details When `cmp` is non-null and active for `level`, also opens `value_cw` via /// the protocol (no second PRG expand outside). /// @tparam InteriorPRG PRG that expands interior nodes. Defaults to `dpf::prg::aes128` /// @tparam CwProtocol correction-word protocol /// @tparam NodeT GGM node type /// @tparam InputT input domain type /// @tparam MaskT mask type /// @tparam AdviceT advice type /// @param st the `st` /// @param x0 the `x0` /// @param x1 the `x1` /// @param mask the bit mask /// @param level the tree level /// @param depth the tree depth /// @param proto the `proto` /// @param cw_out the `cw_out` /// @param advice_out the `advice_out` /// @param value_cw_out the `value_cw_out` /// @param cmp the comparison specification template void ds_advance_level(ds_gen_state & st, InputT x0, InputT x1, MaskT mask, std::size_t level, std::size_t depth, CwProtocol & proto, NodeT & cw_out, AdviceT & advice_out, uint64_t * value_cw_out = nullptr, ds_cmp_gen_state * cmp = nullptr) { using tree = dpf::tree_traits; // Integral bridge so bit extraction works for `keyword` / `modint` / // signed / bitstring the same way dealer gen does via `mask & x`. // `msb_mask` is the unsigned bit pattern; a signed input must not be // required to have that same type. constexpr auto to_int = utils::to_integral_type{}; constexpr auto to_mask = utils::to_integral_type{}; const auto mi = to_mask(mask); const uint8_t bit0 = static_cast(!!(mi & to_int(x0))); const uint8_t bit1 = static_cast(!!(mi & to_int(x1))); const bool is_last = tree::is_last_level(level, depth); NodeT s0 = st.seed0(); NodeT s1 = st.seed1(); const uint8_t adv0 = static_cast(dpf::get_lo_bit(s0)); const uint8_t adv1 = static_cast(dpf::get_lo_bit(s1)); const auto c0 = tree::expand(s0, is_last); const auto c1 = tree::expand(s1, is_last); auto blinds = proto.prepare_level(c0[0], c0[1], bit0, c1[0], c1[1], bit1); if (value_cw_out != nullptr && cmp != nullptr && cmp->active && cmp->trivial == cmp_trivial::none && level < cmp->nbits) { // Convert uses expand_value (HT: always two-tweak); seed walk used expand. const auto v0 = tree::expand_value(s0); const auto v1 = tree::expand_value(s1); auto vblinds = blinds; vblinds.L0 = v0[0]; vblinds.R0 = v0[1]; vblinds.L1 = v1[0]; vblinds.R1 = v1[1]; // Path bit from share bits (threshold bit equals the walk bit). const int ai = static_cast((bit0 ^ bit1) & 1u); if (cmp->paint) { const uint64_t unit = dcf_impl::paint_unit(cmp->kind, level, cmp->thresh, cmp->nbits, cmp->length_bits, false, cmp->paint_cb, cmp->paint_ctx); const uint64_t plant = dcf_impl::scale_plant(unit, cmp->beta, cmp->mask); *value_cw_out = proto.open_planted_cw(vblinds, adv0, adv1, ai, cmp->Va, plant, cmp->mask); if (cmp->track_coeff) { const uint64_t plant1 = dcf_impl::scale_plant(unit, 1ULL, cmp->mask); const uint64_t v1w = proto.open_planted_cw(vblinds, adv0, adv1, ai, cmp->Va1, plant1, cmp->mask); cmp->last_vcw_coeff = (v1w + dcf_impl::neg_m(*value_cw_out, cmp->mask)) & cmp->mask; } } else { *value_cw_out = proto.open_value_cw(vblinds, adv0, adv1, ai, cmp->Va, cmp->beta, cmp->mask); if (cmp->track_coeff) { // Affine coefficient: same level with β = 1 on a parallel Va. const uint64_t v1w = proto.open_value_cw(vblinds, adv0, adv1, ai, cmp->Va1, 1ULL, cmp->mask); cmp->last_vcw_coeff = (v1w + dcf_impl::neg_m(*value_cw_out, cmp->mask)) & cmp->mask; } } } auto [cw, tpack] = proto.open_cw(blinds); // Half-Tree mid levels store no advice; last level keeps BGI packing. if constexpr (tree::is_half_tree) { if (!is_last) tpack = 0; } else { // BGI: opened advice stands. } // Dealer-equivalent CW for Half-Tree mid: off-path children XOR already // matches H(s0)⊕H(s1)⊕ᾱΔ via the open. For last/BGI, open matches Gen. const uint8_t exp0 = st.home[0] == 0 ? bit0 : bit1; const uint8_t rec0 = st.home[0] == 0 ? bit1 : bit0; const uint8_t exp1 = st.home[1] == 0 ? bit0 : bit1; const uint8_t rec1 = st.home[1] == 0 ? bit1 : bit0; NodeT M0, base0, M1, base1; if constexpr (tree::is_half_tree) { if (!is_last) { // Mid: next = child[bit] ⊕ (t ? full_cw : 0). const NodeT D0 = ds_xor(c0[0], c0[1]); const NodeT D1 = ds_xor(c1[0], c1[1]); M0 = D0; base0 = (adv0 & 1u) ? ds_xor(c0[0], cw) : c0[0]; M1 = D1; base1 = (adv1 & 1u) ? ds_xor(c1[0], cw) : c1[0]; } else { ds_next_terms(c0[0], c0[1], adv0, cw, tpack, M0, base0); ds_next_terms(c1[0], c1[1], adv1, cw, tpack, M1, base1); } } else { ds_next_terms(c0[0], c0[1], adv0, cw, tpack, M0, base0); ds_next_terms(c1[0], c1[1], adv1, cw, tpack, M1, base1); } const NodeT nxt0 = ds_deliver(exp0, base0, M0, proto.open_and(blinds.and0, M0, rec0)); const NodeT nxt1 = ds_deliver(exp1, base1, M1, proto.open_and(blinds.and1, M1, rec1)); st.home[0] ^= 1; st.home[1] ^= 1; st.inbox[st.home[0]] = nxt0; st.inbox[st.home[1]] = nxt1; cw_out = cw; advice_out = tpack; } template struct is_ds_randomness : std::false_type {}; template struct is_ds_randomness> : std::true_type {}; template struct is_cw_protocol : std::false_type {}; template struct is_cw_protocol, void> : std::true_type {}; template struct is_cw_protocol().prepare_level( simde_mm_setzero_si128(), simde_mm_setzero_si128(), uint8_t{}, simde_mm_setzero_si128(), simde_mm_setzero_si128(), uint8_t{})), decltype(std::declval().open_cw( std::declval()))>> : std::true_type {}; template struct first_is_cw_protocol : std::false_type {}; template struct first_is_cw_protocol : is_cw_protocol> {}; template auto make_dpf_doerner_shelat_impl(bool arith, bool arith_out, InputT x0, InputT x1, RootSampler & root_sampler, CwProtocol & proto, OutputT y0, OutputT y1 = OutputT{}) { static_assert(!dpf::is_wildcard_v, "Doerner–Shelat gen takes shares of a concrete point"); static_assert(!dpf::is_secret_share_v, "Doerner–Shelat: pass additive_share of xor_wrapper, or raw shares"); static_assert(!dpf::is_wildcard_v, "arith_output / classic DS leaf expects a concrete payload"); static_assert(sizeof(typename InteriorPRG::block_type) == sizeof(simde__m128i), "Doerner–Shelat gen uses the AES-block interior node"); using dpf_type = utils::dpf_type_t; using node = typename dpf_type::interior_node; using input_type = typename dpf_type::input_type; using leaf_tuple = typename dpf_type::leaf_tuple; using beaver_tuple = typename dpf_type::beaver_tuple; using outputs_tuple = std::tuple; constexpr auto depth = dpf_type::depth; proto.encode_walk_shares(x0, x1, arith); using tree = dpf::tree_traits; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") node roots[2]; HEDLEY_PRAGMA(GCC diagnostic pop) tree::root_init(roots, [&]() -> node { return static_cast(root_sampler()); }); const node root0 = roots[0]; const 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 dpf_type::correction_words_array correction_words{}; typename dpf_type::correction_advice_array correction_advice{}; auto mask = dpf_type::msb_mask; for (std::size_t level = 0; level < depth; ++level, mask >>= 1) { ds_advance_level(st, x0, x1, mask, level, depth, proto, correction_words[level], correction_advice[level]); } const node parent0 = st.seed0(); const node parent1 = st.seed1(); const bool sign0 = dpf::get_lo_bit(parent0); const uint8_t t0 = static_cast(sign0); const uint8_t t1 = static_cast(dpf::get_lo_bit(parent1)); leaf_tuple leaves0{}; leaf_tuple leaves1{}; beaver_tuple beavers0{}; beaver_tuple beavers1{}; if (arith_out) { auto cw = proto.template open_arith_leaf( dpf::unset_lo_2bits(parent0), dpf::unset_lo_2bits(parent1), t0, t1, y0, y1, std::size_t{0}, x0, x1); std::get<0>(leaves0) = cw; std::get<0>(leaves1) = cw; } else { // Reconstruct the point only inside the leaf protocol hook. proto.open_leaf_group(x0, x1, [&](input_type sx0, input_type sx1) { const input_type x = utils::xor_input_shares(sx0, sx1); auto built = dpf::make_leaves(x, dpf::unset_lo_2bits(parent0), dpf::unset_lo_2bits(parent1), sign0, std::size_t{0}, y0); leaves0 = std::move(built.first.first); beavers0 = std::move(built.first.second); leaves1 = std::move(built.second.first); beavers1 = std::move(built.second.second); }); (void)y1; } input_type off0{}; input_type off1{}; return dpf::make_party_key_pair( dpf_type{root0, correction_words, correction_advice, leaves0, beavers0, off0}, dpf_type{root1, correction_words, correction_advice, leaves1, beavers1, off1}); } /// @brief Plaintext-β multi-output classic path (unchanged). /// @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 RootSampler sampler for the Doerner–Shelat root seed /// @tparam CwProtocol correction-word protocol /// @param arith the `arith` /// @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 Plaintext-β multi-output classic path (unchanged) template 0)>> auto make_dpf_doerner_shelat_impl(bool arith, InputT x0, InputT x1, RootSampler & root_sampler, CwProtocol & proto, OutputT && y, OutputTs && ...ys) { static_assert(!dpf::is_wildcard_v, "Doerner–Shelat gen takes shares of a concrete point"); static_assert(!dpf::is_secret_share_v, "Doerner–Shelat: pass additive_share of xor_wrapper, or raw shares"); static_assert(sizeof(typename InteriorPRG::block_type) == sizeof(simde__m128i), "Doerner–Shelat gen uses the AES-block interior node"); using dpf_type = utils::dpf_type_t; using node = typename dpf_type::interior_node; using input_type = typename dpf_type::input_type; constexpr auto depth = dpf_type::depth; proto.encode_walk_shares(x0, x1, arith); using tree = dpf::tree_traits; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") node roots[2]; HEDLEY_PRAGMA(GCC diagnostic pop) tree::root_init(roots, [&]() -> node { return static_cast(root_sampler()); }); const node root0 = roots[0]; const 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 dpf_type::correction_words_array correction_words{}; typename dpf_type::correction_advice_array correction_advice{}; auto mask = dpf_type::msb_mask; for (std::size_t level = 0; level < depth; ++level, mask >>= 1) { ds_advance_level(st, x0, x1, mask, level, depth, proto, correction_words[level], correction_advice[level]); } const node parent0 = st.seed0(); const node parent1 = st.seed1(); const bool sign0 = dpf::get_lo_bit(parent0); typename dpf_type::leaf_tuple leaves0{}; typename dpf_type::beaver_tuple beavers0{}; typename dpf_type::leaf_tuple leaves1{}; typename dpf_type::beaver_tuple beavers1{}; proto.open_leaf_group(x0, x1, [&](input_type sx0, input_type sx1) { const input_type x = utils::xor_input_shares(sx0, sx1); auto built = dpf::make_leaves(x, dpf::unset_lo_2bits(parent0), dpf::unset_lo_2bits(parent1), sign0, std::size_t{0}, std::forward(y), std::forward(ys)...); leaves0 = std::move(built.first.first); beavers0 = std::move(built.first.second); leaves1 = std::move(built.second.first); beavers1 = std::move(built.second.second); }); input_type off0{}; input_type off1{}; return dpf::make_party_key_pair( dpf_type{root0, correction_words, correction_advice, leaves0, beavers0, off0}, dpf_type{root1, correction_words, correction_advice, leaves1, beavers1, off1}); } /// @brief Single-output plaintext β (disambiguates from arith_out overload). /// @tparam InteriorPRG PRG that expands interior nodes. Defaults to `dpf::prg::aes128` /// @tparam ExteriorPRG PRG that expands the root. Defaults to `InteriorPRG` /// @tparam InputT input domain type /// @tparam OutputT output type /// @tparam RootSampler sampler for the Doerner–Shelat root seed /// @tparam CwProtocol correction-word protocol /// @param arith the `arith` /// @param x0 the `x0` /// @param x1 the `x1` /// @param root_sampler the `root_sampler` /// @param proto the `proto` /// @param y the `y` /// @return Single-output plaintext β (disambiguates from arith_out overload) template auto make_dpf_doerner_shelat_impl(bool arith, InputT x0, InputT x1, RootSampler & root_sampler, CwProtocol & proto, OutputT && y) { return make_dpf_doerner_shelat_impl(arith, false, std::move(x0), std::move(x1), root_sampler, proto, std::forward(y), OutputT{}); } } // namespace detail /// @brief Local CW protocol (pads cancel; same keys as dealer when roots match). template using local_cw_protocol = detail::local_cw_protocol; template using ds_gen_state = detail::ds_gen_state; // Public `make_dpf_doerner_shelat(x0, x1, ...)` lives in incremental.hpp so // classic and `at<>` / mixed-width packs share one entry point. } // namespace dpf #endif // LIBDPF_INCLUDE_DPF_DOERNER_SHELAT_HPP__