libdpf/include/dpf/doerner_shelat.hpp

1368 lines
50 KiB
C++
Raw Normal View History

/// @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 <cstddef>
#include <cstdint>
#include <cstring>
#include <type_traits>
#include <utility>
#include <vector>
#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 <typename T>
struct arith_beta
{
using payload_type = T;
T y0{};
T y1{};
};
template <typename T>
struct is_arith_beta : std::false_type
{
};
template <typename T>
struct is_arith_beta<arith_beta<T>> : std::true_type
{
};
template <typename T>
inline constexpr bool is_arith_beta_v = is_arith_beta<std::decay_t<T>>::value;
namespace detail
{
namespace incr
{
/// @brief `placed<N, arith_beta<T>>::output_type` is `T` (see placement.hpp).
/// @tparam T value type
template <typename T>
struct unwrap_placed_output<arith_beta<T>>
{
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 <typename RootSampler, typename PadRng>
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<PRG>` is the matching root source.
template <typename PRG = dpf::prg::aes128>
struct prg_pad_rng
{
using block_type = typename PRG::block_type;
explicit prg_pad_rng(block_type seed = dpf::uniform_sample<block_type>())
: 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<uint8_t>(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<simde__m128i>();
}
uint8_t bit()
{
return static_cast<uint8_t>(dpf::uniform_sample<unsigned char>() & 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 <typename PadRng>
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<simde_uint128>;
using traits = dpf::beavers::ring_traits<Ring>;
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<typename Ring::value_type>(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<Ring> 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<uint8_t>(bit0);
p.p1.bit = static_cast<uint8_t>(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 <typename Ring>
ds_and_pads ds_and_from_bit_mul(const dpf::beavers::bit_mul_beaver<Ring> & bm,
uint8_t a0_share)
{
using traits = dpf::beavers::ring_traits<Ring>;
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<uint8_t>(a0_share & 1u);
p.a1 = static_cast<uint8_t>(a ^ p.a0);
auto to_block = [](const Ring & r) {
simde__m128i b{};
const auto v = static_cast<typename Ring::value_type>(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 <typename PadRng, typename Sample>
ds_and_pads ds_sample_and(PadRng & pad, Sample && rng)
{
using Ring = dpf::xor_wrapper<simde_uint128>;
using traits = dpf::beavers::ring_traits<Ring>;
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<Ring>(sampler);
const uint8_t a0 = static_cast<uint8_t>(pad.bit() & 1u);
return ds_and_from_bit_mul(bm, a0);
}
template <typename PadRng>
ds_and_pads ds_sample_and(PadRng & pad)
{
using Ring = dpf::xor_wrapper<simde_uint128>;
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<uint8_t>(bit0 ^ p.p0.bit);
b1.bit = static_cast<uint8_t>(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<uint8_t>(dpf::get_lo_bit(L0) ^ bit0);
const uint8_t a01 = static_cast<uint8_t>(dpf::get_lo_bit(R0) ^ bit0);
const uint8_t a10 = static_cast<uint8_t>(dpf::get_lo_bit(L1) ^ bit1);
const uint8_t a11 = static_cast<uint8_t>(dpf::get_lo_bit(R1) ^ bit1);
const uint8_t t0 = static_cast<uint8_t>(a00 ^ a10 ^ 1u);
const uint8_t t1 = static_cast<uint8_t>(a01 ^ a11);
return static_cast<uint8_t>((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<uint8_t>(tpack & 1u);
const uint8_t t1 = static_cast<uint8_t>((tpack >> 1) & 1u);
const simde__m128i lo = dpf::set_lo_bit(simde_mm_setzero_si128(), 1);
const simde__m128i DT = ds_gate(static_cast<uint8_t>(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<uint8_t>((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 <typename PadRng>
ds_bit_triple ds_sample_bit_and(PadRng & pad)
{
using Bit = dpf::xor_wrapper<std::uint8_t>;
auto sampler = [&pad]() -> Bit {
std::uint8_t packed = 0;
for (int i = 0; i < 8; ++i)
{
packed = static_cast<std::uint8_t>(
packed | (static_cast<std::uint8_t>(pad.bit() & 1u) << i));
}
return Bit{packed};
};
const auto triple = dpf::beavers::sample_beaver2<Bit>(sampler);
auto low = [](Bit x) {
return static_cast<uint8_t>(static_cast<std::uint8_t>(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<uint8_t>((d & b) ^ (e & a) ^ c);
if (hold_de)
z = static_cast<uint8_t>(z ^ (d & e));
return static_cast<uint8_t>(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<uint8_t, uint8_t> 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<uint8_t>(x0 ^ x1 ^ t.a0 ^ t.a1);
const uint8_t e = static_cast<uint8_t>(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 <typename PadRng, typename InputT>
void split_additive_to_xor(PadRng & pads, InputT & x0, InputT & x1)
{
constexpr auto to_int = utils::to_integral_type<InputT>{};
using FromI = typename utils::make_from_integral_value<InputT>::integral_type;
using U = std::make_unsigned_t<FromI>;
const U u0 = static_cast<U>(to_int(x0));
const U u1 = static_cast<U>(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<InputT>;
for (std::size_t i = 0; i < nbits; ++i)
{
const uint8_t a = static_cast<uint8_t>((u0 >> i) & U{1});
const uint8_t b = static_cast<uint8_t>((u1 >> i) & U{1});
const uint8_t sum0 = static_cast<uint8_t>(a ^ c0);
const uint8_t sum1 = static_cast<uint8_t>(b ^ c1);
s0 = static_cast<U>(s0 | (static_cast<U>(sum0) << i));
s1 = static_cast<U>(s1 | (static_cast<U>(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<uint8_t>(a ^ c0), c1,
c0, static_cast<uint8_t>(b ^ c1));
c0 = static_cast<uint8_t>(prod.first ^ c0);
c1 = static_cast<uint8_t>(prod.second ^ c1);
}
x0 = utils::make_from_integral_value<InputT>{}(static_cast<FromI>(s0));
x1 = utils::make_from_integral_value<InputT>{}(static_cast<FromI>(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 <typename Leaf, typename Make, typename Bit0, typename Bit1>
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<Leaf> cand(std::size_t{1} << lg);
for (std::size_t i = 0; i < cand.size(); ++i)
cand[i] = make(static_cast<unsigned>(i));
for (std::size_t b = 0; b < lg; ++b)
{
const uint8_t bit = static_cast<uint8_t>(
(bit0_at(b) ^ bit1_at(b)) & 1u);
std::vector<Leaf> 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 <typename PadRng>
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<simde__m128i, uint8_t>`
HEDLEY_PRAGMA(GCC diagnostic push)
HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes")
HEDLEY_NO_THROW
std::pair<simde__m128i, uint8_t> 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 <typename BlockSampler>
HEDLEY_NO_THROW
uint64_t sample_addend_blind(uint64_t mask, BlockSampler && sample) noexcept
{
return dcf_impl::sample_addend_blind(mask,
std::forward<BlockSampler>(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 <typename InputT>
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 <typename ExteriorPRG, std::size_t I = 0, typename OutputsTuple,
typename InteriorBlock, typename OutputT, typename InputT>
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<typename ExteriorPRG::block_type,
OutputT>
{
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<node_type, output_type>;
HEDLEY_PRAGMA(GCC diagnostic pop)
constexpr std::size_t lg =
dpf::lg_outputs_per_leaf_v<output_type, node_type>;
constexpr auto to_int = utils::to_integral_type<InputT>{};
auto bit0_at = [&](std::size_t b) {
return static_cast<uint8_t>((to_int(x0) >> b) & 1u);
};
auto bit1_at = [&](std::size_t b) {
return static_cast<uint8_t>((to_int(x1) >> b) & 1u);
};
auto naked_of = [&](output_type y) {
return mux_naked_leaf_local<leaf_type>(lg,
[&](unsigned i) {
return dpf::make_naked_leaf<node_type>(
static_cast<InputT>(i), y);
},
bit0_at, bit1_at);
};
// Split payload across leaf words; never form scalar β.
const leaf_type naked =
dpf::add_leaf<output_type>(naked_of(y0), naked_of(y1));
const auto M = dpf::make_leaf_mask<ExteriorPRG, I, OutputsTuple, InteriorBlock>(
seed0, seed1, pos_base);
if constexpr (utils::has_characteristic_two_v<output_type>)
{
(void)t0;
(void)t1;
return dpf::subtract_leaf<output_type>(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<output_type>(M, naked);
return dpf::subtract_leaf<output_type>(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 <typename InputT>
unsigned __int128 open_cmp_threshold(InputT x0, InputT x1,
std::size_t nbits) noexcept
{
constexpr auto to_int = utils::to_integral_type<InputT>{};
constexpr std::size_t bl = utils::bitlength_of_v<InputT>;
const InputT x = utils::xor_input_shares(x0, x1);
if (nbits >= bl)
return static_cast<unsigned __int128>(to_int(x));
return static_cast<unsigned __int128>(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 <typename InputT>
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<InputT>{};
constexpr std::size_t bl = utils::bitlength_of_v<InputT>;
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<uint64_t>(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 <typename InputT, typename LeafFn>
void open_leaf_group(InputT x0, InputT x1, LeafFn && leaf_fn)
{
std::forward<LeafFn>(leaf_fn)(x0, x1);
}
};
/// @brief Generation-side level state (seeds / home bits). Not an eval path memoizer.
/// @tparam NodeT GGM node type
template <typename NodeT>
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 <typename InteriorPRG, typename CwProtocol, typename NodeT,
typename InputT, typename MaskT, typename AdviceT>
void ds_advance_level(ds_gen_state<NodeT> & 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<InteriorPRG>;
// Integral bridge so bit extraction works for `keyword` / `modint` /
// signed / bitstring the same way dealer gen does via `mask & x`.
// `msb_mask` is the unsigned bit pattern; a signed input must not be
// required to have that same type.
constexpr auto to_int = utils::to_integral_type<InputT>{};
constexpr auto to_mask = utils::to_integral_type<MaskT>{};
const auto mi = to_mask(mask);
const uint8_t bit0 = static_cast<uint8_t>(!!(mi & to_int(x0)));
const uint8_t bit1 = static_cast<uint8_t>(!!(mi & to_int(x1)));
const bool is_last = tree::is_last_level(level, depth);
NodeT s0 = st.seed0();
NodeT s1 = st.seed1();
const uint8_t adv0 = static_cast<uint8_t>(dpf::get_lo_bit(s0));
const uint8_t adv1 = static_cast<uint8_t>(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<int>((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 <typename T>
struct is_ds_randomness : std::false_type {};
template <typename RootSampler, typename PadRng>
struct is_ds_randomness<ds_randomness<RootSampler, PadRng>> : std::true_type {};
template <typename T, typename = void>
struct is_cw_protocol : std::false_type {};
template <typename PadRng>
struct is_cw_protocol<local_cw_protocol<PadRng>, void> : std::true_type {};
template <typename T>
struct is_cw_protocol<T,
std::void_t<decltype(std::declval<T &>().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<T &>().open_cw(
std::declval<const ds_level_blinds &>()))>>
: std::true_type {};
template <typename ...Ts>
struct first_is_cw_protocol : std::false_type {};
template <typename T, typename ...Rest>
struct first_is_cw_protocol<T, Rest...>
: is_cw_protocol<std::decay_t<T>> {};
template <typename InteriorPRG,
typename ExteriorPRG,
typename InputT,
typename OutputT,
typename RootSampler,
typename CwProtocol>
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<InputT>,
"Doerner–Shelat gen takes shares of a concrete point");
static_assert(!dpf::is_secret_share_v<InputT>,
"Doerner–Shelat: pass additive_share of xor_wrapper, or raw shares");
static_assert(!dpf::is_wildcard_v<OutputT>,
"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<InteriorPRG, ExteriorPRG, InputT, OutputT>;
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<OutputT>;
constexpr auto depth = dpf_type::depth;
proto.encode_walk_shares(x0, x1, arith);
using tree = dpf::tree_traits<InteriorPRG>;
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<node>(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<node> 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<InteriorPRG>(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<uint8_t>(sign0);
const uint8_t t1 = static_cast<uint8_t>(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<ExteriorPRG, 0, outputs_tuple>(
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<ExteriorPRG>(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 <typename InteriorPRG,
typename ExteriorPRG,
typename InputT,
typename OutputT,
typename ...OutputTs,
typename RootSampler,
typename CwProtocol,
typename = std::enable_if_t<(sizeof...(OutputTs) > 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<InputT>,
"Doerner–Shelat gen takes shares of a concrete point");
static_assert(!dpf::is_secret_share_v<InputT>,
"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<InteriorPRG, ExteriorPRG, InputT,
OutputT, OutputTs...>;
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<InteriorPRG>;
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<node>(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<node> 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<InteriorPRG>(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<ExteriorPRG>(x,
dpf::unset_lo_2bits(parent0), dpf::unset_lo_2bits(parent1), sign0,
std::size_t{0}, std::forward<OutputT>(y),
std::forward<OutputTs>(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 <typename InteriorPRG,
typename ExteriorPRG,
typename InputT,
typename OutputT,
typename RootSampler,
typename CwProtocol>
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<InteriorPRG, ExteriorPRG>(arith, false,
std::move(x0), std::move(x1), root_sampler, proto,
std::forward<OutputT>(y), OutputT{});
}
} // namespace detail
/// @brief Local CW protocol (pads cancel; same keys as dealer when roots match).
template <typename PadRng>
using local_cw_protocol = detail::local_cw_protocol<PadRng>;
template <typename NodeT>
using ds_gen_state = detail::ds_gen_state<NodeT>;
// 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__