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>
1367 lines
50 KiB
C++
1367 lines
50 KiB
C++
/// @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__
|