libdpf/include/dpf/doerner_shelat.hpp
Ryan Henry 0d22946a0e Checkpoint the party/runtime stack before share-program and malicious-mode work.
Ship the TLS mesh, composer, Beaver/Yao/leaf MPC, prep/online paths, apps, and docs so the tree is pushable before elevating share_expr, security_mode, and prep resume.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-28 05:59:19 -06:00

1367 lines
50 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// @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__