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>
This commit is contained in:
Ryan Henry 2026-09-28 05:59:19 -06:00
parent 695f8e84f7
commit 0d22946a0e
1835 changed files with 170291 additions and 2849 deletions

170
party/aes_mmo_ref.hpp Normal file
View file

@ -0,0 +1,170 @@
/// @file party/aes_mmo_ref.hpp
/// @brief Portable AES-128 MMO matching `dpf::prg::aes128::eval` on the zero key.
/// @details Used to check an oblivious evaluation against the hardware PRG.
/// The cipher key is the all-zero key installed by `prg::aes128`.
#ifndef LIBDPF_PARTY_AES_MMO_REF_HPP__
#define LIBDPF_PARTY_AES_MMO_REF_HPP__
#include <array>
#include <cstdint>
#include <cstring>
#include "simde/simde/x86/avx2.h"
namespace dpf
{
namespace party
{
namespace aes_ref
{
inline std::uint8_t gmul(std::uint8_t a, std::uint8_t b)
{
std::uint8_t p = 0;
for (int i = 0; i < 8; ++i)
{
if (b & 1u)
p = static_cast<std::uint8_t>(p ^ a);
const bool hi = (a & 0x80u) != 0;
a = static_cast<std::uint8_t>(a << 1);
if (hi)
a = static_cast<std::uint8_t>(a ^ 0x1b);
b = static_cast<std::uint8_t>(b >> 1);
}
return p;
}
inline std::uint8_t ginv(std::uint8_t a)
{
if (a == 0)
return 0;
// a^254 via square-and-multiply.
std::uint8_t r = 1;
std::uint8_t b = a;
for (int e = 0; e < 7; ++e)
{
b = gmul(b, b);
r = gmul(r, b);
}
return r;
}
inline std::uint8_t sbox_at(std::uint8_t x)
{
std::uint8_t b = ginv(x);
std::uint8_t s = b;
for (int i = 0; i < 4; ++i)
{
b = static_cast<std::uint8_t>((b << 1) | (b >> 7));
s = static_cast<std::uint8_t>(s ^ b);
}
return static_cast<std::uint8_t>(s ^ 0x63);
}
inline std::uint8_t xtime(std::uint8_t x)
{
return static_cast<std::uint8_t>((x << 1) ^ ((x & 0x80) ? 0x1b : 0));
}
inline const std::uint8_t * sbox()
{
static std::uint8_t table[256];
static bool ready = false;
if (!ready)
{
for (int i = 0; i < 256; ++i)
table[i] = sbox_at(static_cast<std::uint8_t>(i));
ready = true;
}
return table;
}
inline void sub_bytes(std::uint8_t s[16])
{
const std::uint8_t * box = sbox();
for (int i = 0; i < 16; ++i)
s[i] = box[s[i]];
}
inline void shift_rows(std::uint8_t s[16])
{
std::uint8_t t = s[1];
s[1] = s[5];
s[5] = s[9];
s[9] = s[13];
s[13] = t;
t = s[2];
s[2] = s[10];
s[10] = t;
t = s[6];
s[6] = s[14];
s[14] = t;
t = s[15];
s[15] = s[11];
s[11] = s[7];
s[7] = s[3];
s[3] = t;
}
inline void mix_columns(std::uint8_t s[16])
{
for (int c = 0; c < 4; ++c)
{
const std::uint8_t i = static_cast<std::uint8_t>(c * 4);
const std::uint8_t a = s[i], b = s[i + 1], c0 = s[i + 2], d = s[i + 3];
const std::uint8_t xa = xtime(a), xb = xtime(b), xc = xtime(c0), xd = xtime(d);
s[i] = static_cast<std::uint8_t>(xa ^ xb ^ b ^ c0 ^ d);
s[i + 1] = static_cast<std::uint8_t>(a ^ xb ^ xc ^ c0 ^ d);
s[i + 2] = static_cast<std::uint8_t>(a ^ b ^ xc ^ xd ^ d);
s[i + 3] = static_cast<std::uint8_t>(xa ^ a ^ b ^ c0 ^ xd);
}
}
inline void add_round_key(std::uint8_t s[16], const std::uint8_t rk[16])
{
for (int i = 0; i < 16; ++i)
s[i] = static_cast<std::uint8_t>(s[i] ^ rk[i]);
}
inline std::array<std::uint8_t, 16> block_bytes(simde__m128i v)
{
std::array<std::uint8_t, 16> b{};
std::memcpy(b.data(), &v, 16);
return b;
}
/// @brief One MMO block: AES-128 encrypt `msg` under the zero key, then XOR `msg`.
inline simde__m128i encrypt_mmo(simde__m128i msg, std::uint32_t pos,
const std::array<simde__m128i, 11> & round_keys)
{
auto s = block_bytes(msg);
auto rk0 = block_bytes(round_keys[0]);
// eval XORs set_epi64x(0, pos) into rd_key[0] before the first AddRoundKey.
rk0[0] = static_cast<std::uint8_t>(rk0[0] ^ static_cast<std::uint8_t>(pos));
rk0[1] = static_cast<std::uint8_t>(rk0[1] ^ static_cast<std::uint8_t>(pos >> 8));
rk0[2] = static_cast<std::uint8_t>(rk0[2] ^ static_cast<std::uint8_t>(pos >> 16));
rk0[3] = static_cast<std::uint8_t>(rk0[3] ^ static_cast<std::uint8_t>(pos >> 24));
add_round_key(s.data(), rk0.data());
for (int r = 1; r < 10; ++r)
{
sub_bytes(s.data());
shift_rows(s.data());
mix_columns(s.data());
auto rk = block_bytes(round_keys[static_cast<std::size_t>(r)]);
add_round_key(s.data(), rk.data());
}
sub_bytes(s.data());
shift_rows(s.data());
auto last = block_bytes(round_keys[10]);
add_round_key(s.data(), last.data());
simde__m128i out{};
std::memcpy(&out, s.data(), 16);
return simde_mm_xor_si128(out, msg);
}
} // namespace aes_ref
} // namespace party
} // namespace dpf
#endif

6
party/aes_sbox_bp.hpp Normal file
View file

@ -0,0 +1,6 @@
/// @file party/aes_sbox_bp.hpp
/// @brief Compatibility include. The S-box table lives in dpf/aes_sbox_bp.hpp.
#ifndef LIBDPF_PARTY_AES_SBOX_BP_HPP__
#define LIBDPF_PARTY_AES_SBOX_BP_HPP__
#include "dpf/aes_sbox_bp.hpp"
#endif

46
party/cases.hpp Normal file
View file

@ -0,0 +1,46 @@
/// @file party/cases.hpp
/// @brief CLI and flow registration for (2+1) party processes.
#ifndef LIBDPF_PARTY_CASES_HPP__
#define LIBDPF_PARTY_CASES_HPP__
#include <string>
#include "dpf/net/trio.hpp"
#include "registry.hpp"
namespace dpf
{
namespace party
{
void register_core_flows();
void register_bulk_flows();
void register_extreme_flows();
void register_recent_flows();
void register_coverage_flows();
void register_gadget_flows();
inline void register_all_flows()
{
static bool once = false;
if (once)
return;
once = true;
register_core_flows();
register_bulk_flows();
register_extreme_flows();
register_recent_flows();
register_coverage_flows();
register_gadget_flows();
}
/// @brief Parse argv and run. Supports:
/// --dir DIR --case NAME [--repeat N] [--warmup W] [--metrics]
/// --dir DIR --tag TAGS [--repeat N] [--warmup W] [--metrics]
/// --list [--tag TAGS]
int run(net::role self, int argc, char ** argv);
} // namespace party
} // namespace dpf
#endif // LIBDPF_PARTY_CASES_HPP__

503
party/dist_dpf3.hpp Normal file
View file

@ -0,0 +1,503 @@
/// @file party/dist_dpf3.hpp
/// @brief Networked (2,3) Shamir DPF keygen (three key holders).
/// @details A pure two-party variant is not meaningful: after keygen each of
/// three parties must hold a Shamir share of the evaluation. Use
/// `dist_with_*_iknp` in `iknp_deal.hpp` for two-party VDPF keygen
/// without a pad dealer.
/// @brief Distributed dual-spine Doerner–Shelat keygen for (2,3) point keys.
/// @details Two `deal_point` / `point_party` runs (spines A and B) with Fig-3
/// `τ` payloads. p0 samples `τ` and `π`; overlapping halves are shipped
/// so p0 holds party-1, p2 holds party-2, p1 holds party-3.
/// Produced keys are always verifiable. Default spines keep `α` as XOR
/// shares (F_DPF3DS): `π` is peeled from the leaf seed of the shared
/// path, not from an opened point. p0 sends p1 only its `τ` halves.
/// Pass `RevealPoint=true` only when the caller wants the tree prefix
/// (and packed lane on updatable keys). Pass `dpf::updatable` for
/// beaver leaves and a later networked Fig-10 update via
/// `dist_update_payload`.
#ifndef LIBDPF_PARTY_DIST_DPF3_HPP__
#define LIBDPF_PARTY_DIST_DPF3_HPP__
#include <cstring>
#include <optional>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include "hedley/hedley.h"
#include "dist_ds.hpp"
#include "key_io.hpp"
#include "dpf/dpf3.hpp"
#include "dpf/dpf3_ds.hpp"
#include "dpf/fp61.hpp"
#include "dpf/shamir3.hpp"
#include "dpf/wildcard.hpp"
namespace dpf
{
namespace party
{
/// @brief How trio roles map onto Shamir party indices `{1,2,3}`.
/// @details **dealer**: p0→1, p1→2, p2→3 (trusted keygen on p2, then ship).
/// **dist**: p0→1, p2→2, p1→3 (`dist_with_dpf3_key` assembly).
enum class dpf3_role_map : unsigned
{
dealer = 0,
dist = 1,
};
/// @brief Shamir party index for a trio role under `map`.
HEDLEY_CONST
HEDLEY_NO_THROW
constexpr int dpf3_party_of(role r, dpf3_role_map map) noexcept
{
if (map == dpf3_role_map::dist)
{
if (r == role::p0)
return 1;
if (r == role::p2)
return 2;
return 3; // p1
}
// dealer
if (r == role::p0)
return 1;
if (r == role::p1)
return 2;
return 3; // p2
}
/// @brief Public VDPF+ offsets for one (2,3) keygen.
struct dpf3_pi_msg
{
shamir3::xor61 pi_a{};
shamir3::xor61 pi_b{};
};
/// @brief Fig-10 patch broadcast (leaf patches so p2 need not learn `α`).
template <typename Leaf>
struct dpf3_fig10_msg
{
Leaf patch_a{};
Leaf patch_b{};
shamir3::xor61 pi_a{};
shamir3::xor61 pi_b{};
};
/// @brief p1's τ halves for spines A and B (party-3 strings). Does not reveal β.
struct dpf3_tau_share
{
shamir3::xor61 t1{};
shamir3::xor61 t3{};
};
namespace detail_dist_dpf3
{
template <typename InteriorPRG, typename ExteriorPRG, typename InputT,
typename OutputT>
using spine_out0 = decltype(dist::point_party<InteriorPRG, ExteriorPRG, InputT,
OutputT, role::p0>(std::declval<trio &>(), std::declval<InputT>(),
std::declval<OutputT>()));
template <typename InteriorPRG, typename ExteriorPRG, typename InputT,
typename OutputT>
using spine_key0 = spine_out0<InteriorPRG, ExteriorPRG, InputT, OutputT>;
template <typename InteriorPRG, typename ExteriorPRG, typename InputT,
typename OutputT>
using spine_out1 = decltype(dist::point_party<InteriorPRG, ExteriorPRG, InputT,
OutputT, role::p1>(std::declval<trio &>(), std::declval<InputT>(),
std::declval<OutputT>()));
template <typename InteriorPRG, typename ExteriorPRG, typename InputT,
typename OutputT>
using spine_key1 = spine_out1<InteriorPRG, ExteriorPRG, InputT, OutputT>;
template <typename Key, typename Share>
void assign_wildcard_over_link(Key & key, Share my_share, trio & net, role self)
{
const role peer = self == role::p0 ? role::p1 : role::p0;
auto & wrap = std::get<0>(key.leaf_nodes);
auto blinded = wrap.compute_and_get_blinded_output_share(my_share);
auto peer_blinded = net.exchange_with(peer, blinded);
auto leaf = wrap.compute_and_get_leaf_share(peer_blinded);
auto peer_leaf = net.exchange_with(peer, leaf);
wrap.reconstruct_correction_word(peer_leaf);
}
/// @brief Assign XOR payload from each party's τ half (payload never opened).
template <typename Key>
void assign_spine_tau_halves(trio & net, role self, Key & key,
shamir3::xor61 my_half)
{
assign_wildcard_over_link(key, my_half, net, self);
}
template <typename Key, typename Leaf>
void apply_fig10_leaves(Key & key, const dpf3_fig10_msg<Leaf> & msg)
{
detail::dpf3_impl::apply_leaf_patch(key.a.dpf_key, msg.patch_a);
detail::dpf3_impl::apply_leaf_patch(key.b.dpf_key, msg.patch_b);
}
template <typename Key, typename Leaf>
void apply_fig10(Key & key, const dpf3_fig10_msg<Leaf> & msg)
{
apply_fig10_leaves(key, msg);
key.a.offset = msg.pi_a;
key.b.offset = msg.pi_b;
}
template <typename Key, typename Node>
HEDLEY_WARN_UNUSED_RESULT
shamir3::xor61 peel_from_seed(const Key & key, const Node & seed)
{
const auto y = key.template traverse_exterior<0>(seed);
using Y = std::decay_t<decltype(y)>;
if constexpr (is_secret_share_v<Y>)
return shamir3::xor61{y.raw()};
else if constexpr (std::is_integral_v<Y>
|| std::is_convertible_v<Y, std::uint64_t>)
return shamir3::xor61{static_cast<std::uint64_t>(y)};
else
{
std::uint64_t w = 0;
static_assert(sizeof(Y) >= sizeof(w),
"peel_from_seed: leaf narrower than xor61");
std::memcpy(&w, &y, sizeof(w));
return shamir3::xor61{w};
}
}
template <typename InputT>
struct dpf3_opened
{
InputT opened_prefix{};
/// Meaningful when the spine payload is a packed wildcard.
unsigned opened_lane = 0;
bool lane_opened = false;
};
template <typename T, typename = void>
struct has_nested_key : std::false_type {};
template <typename T>
struct has_nested_key<T, std::void_t<decltype(std::declval<T &>().key)>>
: std::true_type {};
template <typename T>
inline constexpr bool has_nested_key_v = has_nested_key<T>::value;
template <typename T, typename = void>
struct has_opened_prefix : std::false_type {};
template <typename T>
struct has_opened_prefix<T,
std::void_t<decltype(std::declval<const T &>().opened_prefix)>>
: std::true_type {};
template <typename T>
inline constexpr bool has_opened_prefix_v = has_opened_prefix<T>::value;
template <typename T, typename = void>
struct has_opened_lane : std::false_type {};
template <typename T>
struct has_opened_lane<T,
std::void_t<decltype(std::declval<const T &>().opened_lane)>>
: std::true_type {};
template <typename T>
inline constexpr bool has_opened_lane_v = has_opened_lane<T>::value;
template <typename T, typename = void>
struct has_member_dpf_key : std::false_type {};
template <typename T>
struct has_member_dpf_key<T, std::void_t<decltype(std::declval<T &>().dpf_key)>>
: std::true_type {};
template <typename Held>
auto spine_key_of(Held && held)
{
using H = std::decay_t<Held>;
if constexpr (has_member_dpf_key<H>::value)
return std::move(held.dpf_key);
else if constexpr (has_nested_key_v<H>)
return std::move(held.key);
else
return std::move(held);
}
template <typename Held, typename InputT>
void note_opened(dpf3_opened<InputT> & learned, const Held & held)
{
if constexpr (has_opened_prefix_v<std::decay_t<Held>>)
{
learned.opened_prefix = held.opened_prefix;
if constexpr (has_opened_lane_v<std::decay_t<Held>>)
{
learned.opened_lane = held.opened_lane;
learned.lane_opened = true;
}
}
}
template <typename InteriorPRG, typename ExteriorPRG, typename InputT,
typename OutputT, bool Updatable, bool RevealPoint, typename Fn1,
typename Fn2, typename Fn3>
std::optional<dpf3_opened<InputT>> dist_with_dpf3_key_impl(trio & net, role self,
InputT x0, InputT x1,
const fp61 beta, Fn1 && on1, Fn2 && on2, Fn3 && on3)
{
using X = shamir3::xor61;
using tau_quad = detail::dpf3_impl::tau_quad;
using Key0 = spine_key0<InteriorPRG, ExteriorPRG, InputT, OutputT>;
using Key1 = spine_key1<InteriorPRG, ExteriorPRG, InputT, OutputT>;
using node = typename dpf::tree_traits<InteriorPRG>::node;
constexpr bool V = true; // dist point_party always opens correction seeds
if (self == role::p2)
{
dist::deal_point<InteriorPRG, InputT, OutputT, !RevealPoint>(net);
dist::deal_point<InteriorPRG, InputT, OutputT, !RevealPoint>(net);
const auto pi = net.recv_from<dpf3_pi_msg>(role::p0, net::msg::delta);
auto key_b0 = recv_key<Key0>(net, role::p0);
auto key_a1 = recv_key<Key1>(net, role::p1);
detail::dpf3_impl::vdpf_plus_key<Key1> plus_a1{std::move(key_a1),
pi.pi_a};
detail::dpf3_impl::vdpf_plus_key<Key0> plus_b0{std::move(key_b0),
pi.pi_b};
dpf3_key<2, decltype(plus_a1), decltype(plus_b0)> k2{
std::move(plus_a1), std::move(plus_b0), V, false, Updatable};
std::forward<Fn2>(on2)(std::move(k2));
return std::nullopt;
}
// p0 samples τ from a Shamir split of β; p1 receives only (t1, t3).
tau_quad t{};
X my_a{};
X my_b{};
if (self == role::p0)
{
t = detail::dpf3_impl::sample_taus(beta);
my_a = t.t0;
my_b = t.t2;
net.send_to(role::p1, net::msg::delta, dpf3_tau_share{t.t1, t.t3});
}
else
{
const auto sh = net.recv_from<dpf3_tau_share>(role::p0, net::msg::delta);
my_a = sh.t1;
my_b = sh.t3;
t.t1 = sh.t1;
t.t3 = sh.t3;
}
dpf3_opened<InputT> learned{};
node seed_a{};
node seed_b{};
// Non-updatable and updatable both plant beaver leaves; τ halves are
// assigned without opening the spine payload. The outer Updatable flag
// only gates Fig-10.
// Spines still use per-message `exchange_with`. Wiring RoundSink here
// needs a round budget that covers wild+hash leaf mux; undersizing hangs
// the peer on `msg::round_batch`.
OutputT wild{};
if (self == role::p0)
{
auto out_a0 = dist::point_party<InteriorPRG, ExteriorPRG, InputT,
OutputT, role::p0, false, RevealPoint>(net, x0, wild, false,
&seed_a);
note_opened(learned, out_a0);
auto key_a0 = spine_key_of(std::move(out_a0));
auto out_b0 = dist::point_party<InteriorPRG, ExteriorPRG, InputT,
OutputT, role::p0, false, RevealPoint>(net, x0, wild, false,
&seed_b);
auto key_b0 = spine_key_of(std::move(out_b0));
assign_spine_tau_halves(net, self, key_a0, my_a);
assign_spine_tau_halves(net, self, key_b0, my_b);
dpf3_pi_msg pi{};
pi.pi_a = t.t0 + peel_from_seed(key_a0, seed_a);
pi.pi_b = t.t2 + peel_from_seed(key_b0, seed_b);
net.send_to(role::p1, net::msg::delta, pi);
net.send_to(role::p2, net::msg::delta, pi);
send_key(net, role::p2, key_b0);
detail::dpf3_impl::vdpf_plus_key<Key0> plus_a0{std::move(key_a0),
pi.pi_a};
detail::dpf3_impl::vdpf_plus_key<Key0> plus_b0{std::move(key_b0),
pi.pi_b};
dpf3_key<1, decltype(plus_a0), decltype(plus_b0)> k1{
std::move(plus_a0), std::move(plus_b0), V, false, Updatable};
std::forward<Fn1>(on1)(std::move(k1));
if constexpr (RevealPoint)
return learned;
return std::nullopt;
}
auto out_a1 = dist::point_party<InteriorPRG, ExteriorPRG, InputT,
OutputT, role::p1, false, RevealPoint>(net, x1, wild, false,
&seed_a);
note_opened(learned, out_a1);
auto key_a1 = spine_key_of(std::move(out_a1));
auto out_b1 = dist::point_party<InteriorPRG, ExteriorPRG, InputT,
OutputT, role::p1, false, RevealPoint>(net, x1, wild, false,
&seed_b);
auto key_b1 = spine_key_of(std::move(out_b1));
assign_spine_tau_halves(net, self, key_a1, my_a);
assign_spine_tau_halves(net, self, key_b1, my_b);
const auto pi = net.recv_from<dpf3_pi_msg>(role::p0, net::msg::delta);
send_key(net, role::p2, key_a1);
detail::dpf3_impl::vdpf_plus_key<Key1> plus_a1{std::move(key_a1),
pi.pi_a};
detail::dpf3_impl::vdpf_plus_key<Key1> plus_b1{std::move(key_b1),
pi.pi_b};
dpf3_key<3, decltype(plus_a1), decltype(plus_b1)> k3{
std::move(plus_a1), std::move(plus_b1), V, false, Updatable};
std::forward<Fn3>(on3)(std::move(k3));
if constexpr (RevealPoint)
return learned;
return std::nullopt;
}
} // namespace detail_dist_dpf3
/// @brief Distributed dual-spine (2,3) keygen over the trio.
/// @details Role map after assembly: **p0 → party 1**, **p2 → party 2**,
/// **p1 → party 3**. Keys are always verifiable. Each computing
/// party's view of `α` is only its XOR share; p1's view of `β` is
/// only its τ halves (not a clear payload). Default return is empty.
/// @tparam RevealPoint when true, p0/p1 also reconstruct the tree prefix
/// @return Opened prefix when `RevealPoint`, else empty. Empty on p2.
/// @throws std::runtime_error if a frame is truncated or tagged wrong
/// \complexity Two `point_party` spines, so the local work is two O(n) walks, plus O(1) τ arithmetic.
/// \rounds The rounds of two `point_party` calls, then one τ-share send (p0 to p1), one `dpf3_pi_msg` to p1 and p2, and one key send to p2. p2 runs two `deal_point` calls first. Counted in `dist_with_dpf3_key_impl`. Fig-10 update is `dist_update_payload`, not this function.
/// \communication Two dealer tapes (see `deal_point` / `point_party`), one `dpf3_tau_share`, one `dpf3_pi_msg` (two `xor61` values), and one key blob (`send_key`, `sizeof` of the spine key).
/// \preprocessing p2's `deal_point` pads for both spines. p0 samples the four τ strings locally from a Shamir split of β.
template <typename InteriorPRG = dpf::prg::aes128,
typename ExteriorPRG = InteriorPRG,
bool RevealPoint = false,
typename InputT,
typename Fn1,
typename Fn2,
typename Fn3>
[[nodiscard]] std::optional<InputT> dist_with_dpf3_key(trio & net, role self,
InputT x0, InputT x1, const fp61 beta, Fn1 && on1, Fn2 && on2, Fn3 && on3)
{
auto opened = detail_dist_dpf3::dist_with_dpf3_key_impl<InteriorPRG,
ExteriorPRG, InputT, dpf::wildcard_value<shamir3::xor61>, false,
RevealPoint>(net, self, x0, x1, beta, std::forward<Fn1>(on1),
std::forward<Fn2>(on2), std::forward<Fn3>(on3));
if (!opened)
return std::nullopt;
return opened->opened_prefix;
}
/// @brief What an updatable (2,3) keygen reconstructs when `RevealPoint`.
template <typename InputT>
struct dpf3_updatable_opened
{
InputT opened_prefix{};
unsigned opened_lane = 0;
};
/// @brief Distributed dual-spine (2,3) keygen with beaver leaves (Fig-10-ready).
/// @tparam RevealPoint when true, return prefix and packed lane on p0/p1
/// @return Opened values when `RevealPoint`, else empty. Empty on p2.
/// \complexity Two `point_party` spines, so the local work is two O(n) walks, plus O(1) τ arithmetic.
/// \rounds The rounds of two `point_party` calls, then one τ-share send (p0 to p1), one `dpf3_pi_msg` to p1 and p2, and one key send to p2. p2 runs two `deal_point` calls first. Counted in `dist_with_dpf3_key_impl`. Fig-10 update is `dist_update_payload`, not this function.
/// \communication Two dealer tapes (see `deal_point` / `point_party`), one `dpf3_tau_share`, one `dpf3_pi_msg` (two `xor61` values), and one key blob (`send_key`, `sizeof` of the spine key).
/// \preprocessing p2's `deal_point` pads for both spines. p0 samples the four τ strings locally from a Shamir split of β.
template <typename InteriorPRG = dpf::prg::aes128,
typename ExteriorPRG = InteriorPRG,
bool RevealPoint = false,
typename InputT,
typename Fn1,
typename Fn2,
typename Fn3>
[[nodiscard]] std::optional<dpf3_updatable_opened<InputT>> dist_with_dpf3_key(
trio & net, role self, InputT x0, InputT x1, const fp61 beta, updatable,
Fn1 && on1, Fn2 && on2, Fn3 && on3)
{
auto opened = detail_dist_dpf3::dist_with_dpf3_key_impl<InteriorPRG,
ExteriorPRG, InputT, dpf::wildcard_value<shamir3::xor61>, true,
RevealPoint>(net, self, x0, x1, beta, std::forward<Fn1>(on1),
std::forward<Fn2>(on2), std::forward<Fn3>(on3));
if (!opened)
return std::nullopt;
return dpf3_updatable_opened<InputT>{opened->opened_prefix,
opened->opened_lane};
}
/// @brief Networked Fig-10 payload update for keys from `dist_with_dpf3_key`.
/// @details p0 and p1 (who are given `α`) exchange peels, p0 samples fresh `τ`
/// and broadcasts leaf patches + new public `π`. p2 applies patches
/// without learning `α`. Requires `key.updatable`.
/// @throws std::invalid_argument if the key is not updatable
template <typename Key, typename InputT>
void dist_update_payload(trio & net, role self, Key & key, InputT alpha,
fp61 beta_new)
{
static_assert(Key::is_dpf3, "dist_update_payload: dpf3 key");
if (!key.updatable)
throw std::invalid_argument(
"dist_update_payload: key was not generated with dpf::updatable");
using X = shamir3::xor61;
using InnerA = typename Key::plus_a_type::inner_type;
using Leaf = decltype(detail::dpf3_impl::make_leaf_patch<InnerA>(alpha,
X{}));
using Msg = dpf3_fig10_msg<Leaf>;
struct peel_pair
{
X peel_a{};
X peel_b{};
};
Msg msg{};
if (self == role::p0 || self == role::p1)
{
peel_pair mine{detail::dpf3_impl::peel(key.a.dpf_key, alpha),
detail::dpf3_impl::peel(key.b.dpf_key, alpha)};
const role peer = self == role::p0 ? role::p1 : role::p0;
const peel_pair theirs = net.exchange_with(peer, mine);
if (self == role::p0)
{
// p0 = party 1 (A0,B0); p1 = party 3 (A1,B1).
// Leaf CW patches only move the half whose path control bit is set;
// compute π from a post-patch peel (same as dealer refresh_offset).
detail::dpf3_impl::tau_quad told{};
told.t0 = mine.peel_a + key.a.offset;
told.t1 = theirs.peel_a + key.a.offset;
told.t2 = mine.peel_b + key.b.offset;
told.t3 = theirs.peel_b + key.b.offset;
const auto tnew = detail::dpf3_impl::sample_taus(beta_new);
const X dA = (tnew.t0 + tnew.t1) + (told.t0 + told.t1);
const X dB = (tnew.t2 + tnew.t3) + (told.t2 + told.t3);
msg.patch_a =
detail::dpf3_impl::make_leaf_patch<InnerA>(alpha, dA);
msg.patch_b =
detail::dpf3_impl::make_leaf_patch<InnerA>(alpha, dB);
detail_dist_dpf3::apply_fig10_leaves(key, msg);
msg.pi_a = tnew.t0 + detail::dpf3_impl::peel(key.a.dpf_key, alpha);
msg.pi_b = tnew.t2 + detail::dpf3_impl::peel(key.b.dpf_key, alpha);
key.a.offset = msg.pi_a;
key.b.offset = msg.pi_b;
net.send_to(role::p1, net::msg::delta, msg);
net.send_to(role::p2, net::msg::delta, msg);
return;
}
msg = net.recv_from<Msg>(role::p0, net::msg::delta);
detail_dist_dpf3::apply_fig10(key, msg);
return;
}
msg = net.recv_from<Msg>(role::p0, net::msg::delta);
detail_dist_dpf3::apply_fig10(key, msg);
}
} // namespace party
} // namespace dpf
#endif // LIBDPF_PARTY_DIST_DPF3_HPP__

2726
party/dist_ds.hpp Normal file

File diff suppressed because it is too large Load diff

810
party/flow_util.hpp Normal file
View file

@ -0,0 +1,810 @@
/// @file party/flow_util.hpp
/// @brief Shared helpers for (2+1) party flow bodies.
#ifndef LIBDPF_PARTY_FLOW_UTIL_HPP__
#define LIBDPF_PARTY_FLOW_UTIL_HPP__
#include <cstdint>
#include <stdexcept>
#include <utility>
#include <vector>
#include "dpf/beaver.hpp"
#include "dpf/compose.hpp"
#include "dpf/net/mux_sink.hpp" // trio::batch default
#include "dpf/net/party_tape_io.hpp"
#include "dpf/net/round_sink.hpp"
#include "dpf/net/trio.hpp"
#include "dpf/secret_share.hpp"
#include "dpf/verifiable.hpp"
#include <algorithm>
#include <cstring>
#include <map>
#include <memory>
#include <type_traits>
#include <vector>
namespace dpf
{
namespace party
{
namespace util
{
using net::role;
using net::trio;
using u64 = std::uint64_t;
struct Counter
{
int draws = 0;
u64 operator()()
{
++draws;
return 0x9e3779b97f4a7c15ull * static_cast<u64>(draws);
}
};
inline void require(bool cond, const char * msg)
{
if (!cond)
throw std::runtime_error(msg);
}
inline std::pair<u64, u64> split_u64(u64 secret, unsigned tag = 1)
{
u64 p0 = 0x9e3779b97f4a7c15ull * (tag + 1u);
return {p0, secret - p0};
}
template <typename Ring>
std::pair<Ring, Ring> split_ring(Ring secret, unsigned tag = 1)
{
using traits = beavers::ring_traits<Ring>;
Ring p0 = Ring{static_cast<u64>(0x9e3779b97f4a7c15ull * (tag + 1u))};
return {p0, traits::sub(secret, p0)};
}
template <typename Ring>
Ring open_additive(trio & net, role self, Ring mine)
{
role peer = self == role::p0 ? role::p1 : role::p0;
return net.open_with(peer, mine);
}
/// @brief Open many independent additive shares in one vector exchange.
template <typename Ring>
std::vector<Ring> open_additive(trio & net, role self, const std::vector<Ring> & mine)
{
role peer = self == role::p0 ? role::p1 : role::p0;
return net.open_vec_with(peer, mine);
}
template <typename T>
T open_subtractive(trio & net, role self, T mine)
{
role peer = self == role::p0 ? role::p1 : role::p0;
T theirs = net.exchange_with(peer, mine);
return self == role::p0 ? static_cast<T>(mine - theirs)
: static_cast<T>(theirs - mine);
}
/// @brief Open many independent subtractive shares in one vector exchange.
template <typename T>
std::vector<T> open_subtractive(trio & net, role self, const std::vector<T> & mine)
{
role peer = self == role::p0 ? role::p1 : role::p0;
auto theirs = net.exchange_vec_with(peer, mine);
require(theirs.size() == mine.size(), "open_subtractive vec size");
std::vector<T> out(mine.size());
for (std::size_t i = 0; i < mine.size(); ++i)
{
out[i] = self == role::p0 ? static_cast<T>(mine[i] - theirs[i])
: static_cast<T>(theirs[i] - mine[i]);
}
return out;
}
template <typename T>
constexpr T share_bits(T s) noexcept
{
return s;
}
template <typename T, std::size_t Party, sharing Scheme>
constexpr T share_bits(const secret_share<T, Party, Scheme> & s) noexcept
{
return s.raw();
}
inline void install_and_bind_u64(beavers::session<u64> & s, trio & net, role self,
const std::vector<beavers::session<u64>::wire> & inputs,
const std::vector<u64> & secrets)
{
auto tape = net::accept_session<u64>(net);
s.install_party(self == role::p0 ? 0u : 1u, tape);
for (std::size_t i = 0; i < inputs.size(); ++i)
{
auto [p0, p1] = split_u64(secrets[i], static_cast<unsigned>(i + 1));
s.bind_party(inputs[i], self == role::p0 ? p0 : p1);
}
}
template <typename Ring>
void install_and_bind(beavers::session<Ring> & s, trio & net, role self,
const std::vector<typename beavers::session<Ring>::wire> & inputs,
const std::vector<Ring> & secrets)
{
auto tape = net::accept_session<Ring>(net);
s.install_party(self == role::p0 ? 0u : 1u, tape);
for (std::size_t i = 0; i < inputs.size(); ++i)
{
auto [p0, p1] = split_ring<Ring>(secrets[i], static_cast<unsigned>(i + 1));
s.bind_party(inputs[i], self == role::p0 ? p0 : p1);
}
}
/// @brief Host `evaluate_party_batch` on a RoundSink (one instance).
/// @details Each vector exchange is one schedule round. The sink must have been
/// sized with enough rounds and `slot_bytes >= 4 + n * sizeof(Ring)`
/// for the largest batch this session will open. `index` selects the
/// instance lane when the sink holds many circuits; all parties must
/// drive the same `index` sequence. Prefer `count == 1` sinks.
template <typename Ring>
void evaluate_online_on_sink(beavers::session<Ring> & s, net::RoundSink & sink,
std::size_t index = 0)
{
std::uint16_t flush_round = 0;
s.evaluate_party_batch([&](std::vector<Ring> mine) {
if (flush_round >= sink.rounds())
throw std::runtime_error("evaluate_online_on_sink: out of rounds");
const std::size_t slot = sink.slot_bytes(flush_round);
const std::size_t need = sizeof(std::uint32_t) + mine.size() * sizeof(Ring);
if (need > slot)
throw std::runtime_error("evaluate_online_on_sink: slot too small");
std::vector<std::uint8_t> buf(slot, 0);
const std::uint32_t n = static_cast<std::uint32_t>(mine.size());
std::memcpy(buf.data(), &n, 4);
if (!mine.empty())
std::memcpy(buf.data() + 4, mine.data(), mine.size() * sizeof(Ring));
sink.submit(flush_round, index, buf.data(), buf.size());
sink.flush_round(flush_round);
sink.poll();
if (!sink.peer_ready(flush_round, index))
throw std::runtime_error("evaluate_online_on_sink: peer missing");
std::vector<std::uint8_t> peer_buf(slot);
sink.read_peer(flush_round, index, peer_buf.data(), slot);
std::uint32_t pn = 0;
std::memcpy(&pn, peer_buf.data(), 4);
if (pn != n)
throw std::runtime_error("evaluate_online_on_sink: peer size");
std::vector<Ring> peer(n);
if (n != 0)
std::memcpy(peer.data(), peer_buf.data() + 4, n * sizeof(Ring));
++flush_round;
return peer;
});
}
/// @brief Sink round budget for `evaluate_party_batch` / auth-batch.
/// @details One input flush and one output flush per ready-round, plus a small
/// slack for same-round dependency flushes. Must not grow with
/// `wire_count`: a 2k-wire depth-1 circuit still needs only a handful
/// of vector exchanges, and allocating O(wires) round windows is a
/// multi-megabyte tax on every online evaluate.
inline int beaver_batch_round_budget(int max_ready_round,
std::size_t /*wire_count*/) noexcept
{
return std::max(1, max_ready_round * 2 + 16);
}
/// @brief Online beaver opens hosted on a count-1 batch sink from `trio::batch`.
template <typename Ring>
void evaluate_online(beavers::session<Ring> & s, trio & net, role self)
{
(void)self;
const int rounds =
beaver_batch_round_budget(s.max_ready_round(), s.wire_count());
// Upper bound: every wire δ in one vector exchange, plus length prefix.
const std::size_t slot =
sizeof(std::uint32_t) + s.wire_count() * sizeof(Ring);
std::vector<std::size_t> slots(static_cast<std::size_t>(rounds), slot);
auto sink = net.batch(1, std::move(slots));
evaluate_online_on_sink(s, *sink, 0);
}
/// @brief Drive many independent circuits on one RoundSink (one index each).
/// @details Each circuit runs to completion on its lane. Callers that need a
/// single prefix flush across instances must submit lockstep themselves
/// via `schedule_session`; this helper is the portable multi-lane host
/// of `evaluate_party_batch` on an existing sink.
template <typename Ring>
void evaluate_online_multi_on_sink(
std::vector<beavers::session<Ring> *> sessions, net::RoundSink & sink)
{
if (sessions.size() != sink.count())
throw std::invalid_argument("evaluate_online_multi_on_sink: count");
for (std::size_t i = 0; i < sessions.size(); ++i)
{
if (!sessions[i])
throw std::invalid_argument("evaluate_online_multi_on_sink: null");
evaluate_online_on_sink(*sessions[i], sink, i);
}
}
/// @brief Authenticated `evaluate_party_auth_batch` on a RoundSink.
template <typename Ring>
void evaluate_online_auth_on_sink(beavers::session<Ring> & s,
net::RoundSink & sink, std::size_t index = 0)
{
using opening = beavers::auth_opening<Ring>;
std::uint16_t flush_round = 0;
s.evaluate_party_auth_batch([&](std::vector<opening> mine) {
if (flush_round >= sink.rounds())
throw std::runtime_error("evaluate_online_auth_on_sink: out of rounds");
const std::size_t slot = sink.slot_bytes(flush_round);
const std::size_t need =
sizeof(std::uint32_t) + mine.size() * sizeof(opening);
if (need > slot)
throw std::runtime_error("evaluate_online_auth_on_sink: slot too small");
std::vector<std::uint8_t> buf(slot, 0);
const std::uint32_t n = static_cast<std::uint32_t>(mine.size());
std::memcpy(buf.data(), &n, 4);
if (!mine.empty())
std::memcpy(buf.data() + 4, mine.data(), mine.size() * sizeof(opening));
sink.submit(flush_round, index, buf.data(), buf.size());
sink.flush_round(flush_round);
sink.poll();
if (!sink.peer_ready(flush_round, index))
throw std::runtime_error("evaluate_online_auth_on_sink: peer missing");
std::vector<std::uint8_t> peer_buf(slot);
sink.read_peer(flush_round, index, peer_buf.data(), slot);
std::uint32_t pn = 0;
std::memcpy(&pn, peer_buf.data(), 4);
if (pn != n)
throw std::runtime_error("evaluate_online_auth_on_sink: peer size");
std::vector<opening> peer(n);
if (n != 0)
std::memcpy(peer.data(), peer_buf.data() + 4, n * sizeof(opening));
++flush_round;
return peer;
});
}
/// @brief Authenticated opening path when the session tape carries IT-MACs.
/// @details Call only after the dealer ran `set_mac_key` and `deal_session`.
/// Uses `trio::batch` so a installed `comm_hook` can replace the sink.
template <typename Ring>
void evaluate_online_auth(beavers::session<Ring> & s, trio & net, role self)
{
(void)self;
using opening = beavers::auth_opening<Ring>;
const int rounds =
beaver_batch_round_budget(s.max_ready_round(), s.wire_count());
const std::size_t slot =
sizeof(std::uint32_t) + s.wire_count() * sizeof(opening);
std::vector<std::size_t> slots(static_cast<std::size_t>(rounds), slot);
auto sink = net.batch(1, std::move(slots));
evaluate_online_auth_on_sink(s, *sink, 0);
}
/// @brief u64 beaver δ host using `party_batch_stepper` (one per ABY session).
struct u64_beaver_host final : protocol::beaver_host
{
explicit u64_beaver_host(
std::vector<beavers::session<std::uint64_t> *> sessions)
{
steppers_.reserve(sessions.size());
for (auto * s : sessions)
{
if (s == nullptr)
steppers_.push_back(nullptr);
else
steppers_.push_back(
std::make_unique<beavers::party_batch_stepper<std::uint64_t>>(
*s));
}
}
void pack(std::size_t session_index, std::size_t /*barrier_index*/,
std::uint8_t * dst, std::size_t n) override
{
if (session_index >= steppers_.size() || !steppers_[session_index])
throw std::runtime_error("u64_beaver_host: missing session");
auto mine = steppers_[session_index]->take_local();
const std::uint32_t count = static_cast<std::uint32_t>(mine.size());
const std::size_t need =
sizeof(std::uint32_t) + mine.size() * sizeof(std::uint64_t);
if (n < need)
throw std::runtime_error("u64_beaver_host: slot too small");
std::memset(dst, 0, n);
std::memcpy(dst, &count, sizeof(count));
if (!mine.empty())
std::memcpy(dst + sizeof(count), mine.data(),
mine.size() * sizeof(std::uint64_t));
}
void apply(std::size_t session_index, std::size_t /*barrier_index*/,
const std::uint8_t * src, std::size_t n) override
{
if (session_index >= steppers_.size() || !steppers_[session_index])
throw std::runtime_error("u64_beaver_host: missing session");
if (n < sizeof(std::uint32_t))
throw std::runtime_error("u64_beaver_host: peer short");
std::uint32_t count = 0;
std::memcpy(&count, src, sizeof(count));
const std::size_t need =
sizeof(std::uint32_t) + static_cast<std::size_t>(count)
* sizeof(std::uint64_t);
if (n < need)
throw std::runtime_error("u64_beaver_host: peer size");
std::vector<std::uint64_t> peer(count);
if (count != 0)
std::memcpy(peer.data(), src + sizeof(count),
peer.size() * sizeof(std::uint64_t));
steppers_[session_index]->apply_peer(peer);
}
private:
std::vector<std::unique_ptr<beavers::party_batch_stepper<std::uint64_t>>>
steppers_;
};
/// @brief IT-MAC δ host: `uint32 n ‖ auth_opening<Ring>[n]`.
/// @details `apply` checks each tag via `party_auth_batch_stepper`.
template <typename Ring>
struct auth_beaver_host final : protocol::beaver_host
{
using opening = beavers::auth_opening<Ring>;
explicit auth_beaver_host(std::vector<beavers::session<Ring> *> sessions)
{
static_assert(std::is_trivially_copyable_v<opening>,
"auth_opening must be trivially copyable");
steppers_.reserve(sessions.size());
for (auto * s : sessions)
{
if (s == nullptr)
steppers_.push_back(nullptr);
else
steppers_.push_back(
std::make_unique<beavers::party_auth_batch_stepper<Ring>>(*s));
}
}
void pack(std::size_t session_index, std::size_t, std::uint8_t * dst,
std::size_t n) override
{
if (session_index >= steppers_.size() || !steppers_[session_index])
throw std::runtime_error("auth_beaver_host: missing session");
auto mine = steppers_[session_index]->take_local();
const std::uint32_t count = static_cast<std::uint32_t>(mine.size());
const std::size_t need = sizeof(std::uint32_t) + mine.size() * sizeof(opening);
if (n < need)
throw std::runtime_error("auth_beaver_host: slot too small");
std::memset(dst, 0, n);
std::memcpy(dst, &count, sizeof(count));
if (!mine.empty())
std::memcpy(dst + sizeof(count), mine.data(),
mine.size() * sizeof(opening));
}
void apply(std::size_t session_index, std::size_t, const std::uint8_t * src,
std::size_t n) override
{
if (session_index >= steppers_.size() || !steppers_[session_index])
throw std::runtime_error("auth_beaver_host: missing session");
if (n < sizeof(std::uint32_t))
throw std::runtime_error("auth_beaver_host: peer short");
std::uint32_t count = 0;
std::memcpy(&count, src, sizeof(count));
const std::size_t need =
sizeof(std::uint32_t) + static_cast<std::size_t>(count) * sizeof(opening);
if (n < need)
throw std::runtime_error("auth_beaver_host: peer size");
std::vector<opening> peer(count);
if (count != 0)
std::memcpy(peer.data(), src + sizeof(count),
peer.size() * sizeof(opening));
steppers_[session_index]->apply_peer(peer);
}
private:
std::vector<std::unique_ptr<beavers::party_auth_batch_stepper<Ring>>> steppers_;
};
using u64_auth_beaver_host = auth_beaver_host<std::uint64_t>;
/// @brief Schedule a composer and drive it on a trio batch sink.
/// @details Uses `default_plan()`: RoundSink rounds == `plan::rounds()` ==
/// exchange-bearing waves. Pass `opt.from_exchange_wave` /
/// `compact_sink` for adaptive tails; `opt.beavers` for live δ.
inline void drive_composed(protocol::composer & c, trio & net,
std::vector<std::vector<std::uint8_t>> & values,
const std::map<std::uint32_t, protocol::kernel_fn> & kernels,
std::size_t count = 1, protocol::drive_options opt = {})
{
auto p = c.default_plan();
auto slots = opt.compact_sink ? p.slot_bytes_from(opt.from_exchange_wave)
: p.slot_bytes_all();
if (slots.empty())
slots.push_back(0);
auto sink = net.batch(count, std::move(slots));
protocol::drive(p, *sink, values, kernels, c.party(), opt);
}
/// @brief Drive a compose plan on the trio mesh (2PC peer + 3PC RSS neighbor).
/// @details Shared semantics with `protocol::drive`: throws on missing kernels,
/// domain-correct opens, optional beaver host, incremental
/// `from_exchange_wave`. Per-exchange peer maps: `y` → neighbor ring,
/// else p0↔p1 (p2 idle).
inline void drive_composed_trio(protocol::composer & c, trio & net, role self,
std::vector<std::vector<std::uint8_t>> & values,
const std::map<std::uint32_t, protocol::kernel_fn> & kernels,
protocol::drive_options opt = {})
{
using protocol::domain;
using protocol::effect;
namespace opcodes = protocol::opcodes;
auto p = c.default_plan();
if (values.size() < p.nodes().size())
values.resize(p.nodes().size());
auto ensure = [&](std::uint32_t id) {
const std::size_t need = p.value_bytes_of(id);
if (values[id].size() < need)
values[id].assign(need, 0);
};
for (std::uint32_t id = 0; id < p.nodes().size(); ++id)
ensure(id);
const role peer = self == role::p0 ? role::p1 : role::p0;
const role rss_next =
static_cast<role>((static_cast<unsigned>(self) + 1u) % 3u);
auto run_groups = [&](const protocol::wave_info & wave) {
for (const auto & g : wave.groups)
{
if (g.kind == effect::convert)
{
for (auto n : g.nodes)
{
ensure(n.id);
const auto & ins = p.inputs_of(n.id);
if (ins.empty())
continue;
if (p.alias_of(n.id) && ins.size() == 1
&& p.value_bytes_of(n.id) == p.value_bytes_of(ins[0]))
{
values[n.id] = values[ins[0]];
continue;
}
if (ins.size() == 1)
{
protocol::detail::run_builtin_convert(g.opcode,
c.party(), values[ins[0]].data(),
p.value_bytes_of(ins[0]), values[n.id].data(),
p.value_bytes_of(n.id));
}
else if (ins.size() == 2
&& g.opcode == opcodes::conv_y2rss)
{
const auto half = p.value_bytes_of(ins[0]);
std::memcpy(values[n.id].data(),
values[ins[0]].data(), half);
std::memcpy(values[n.id].data() + half,
values[ins[1]].data(), half);
}
}
continue;
}
if (protocol::detail::is_beaver_opcode(g.opcode))
continue;
if (g.opcode == opcodes::fss_fuse_segment)
{
for (auto n : g.nodes)
{
ensure(n.id);
const auto & ins = p.inputs_of(n.id);
if (!ins.empty())
ensure(ins[0]);
protocol::detail::apply_fuse_segment(p, values, n.id, 1);
}
continue;
}
auto kit = kernels.find(g.opcode);
protocol::kernel_fn builtin;
const protocol::kernel_fn * fn = nullptr;
if (kit != kernels.end())
fn = &kit->second;
else if (protocol::detail::has_builtin_walk_kernel(g.opcode))
{
builtin = protocol::detail::builtin_walk_kernel(g.opcode);
fn = &builtin;
}
else if (g.kind == effect::blind)
{
for (auto n : g.nodes)
{
ensure(n.id);
std::fill(values[n.id].begin(), values[n.id].end(), 0);
}
continue;
}
else
{
throw std::runtime_error(
"drive_composed_trio missing kernel for opcode "
+ std::to_string(g.opcode));
}
for (auto n : g.nodes)
{
ensure(n.id);
std::vector<protocol::block_span> ins;
for (auto in_id : p.inputs_of(n.id))
{
ensure(in_id);
ins.push_back(protocol::block_span{values[in_id].data(), 1,
p.value_bytes_of(in_id)});
}
protocol::block_span out{values[n.id].data(), 1,
p.value_bytes_of(n.id)};
(*fn)(g.opcode, {n}, ins, out, 1);
}
}
};
std::size_t exchange_i = 0;
for (std::size_t w = 0; w < p.waves(); ++w)
{
const auto & wave = p.wave(w);
const bool skip = !wave.exchanges.empty()
&& exchange_i < opt.from_exchange_wave;
if (!skip)
run_groups(wave);
if (wave.exchanges.empty() || wave.slot_bytes == 0)
continue;
if (exchange_i < opt.from_exchange_wave)
{
++exchange_i;
continue;
}
++exchange_i;
// Split peer maps: y → RSS neighbor; dealer_pad → p2; else → 2PC peer.
std::vector<std::uint8_t> two_pc;
std::vector<std::uint8_t> rss;
std::vector<std::uint8_t> dealer;
struct zero_mask { std::size_t n = 0; domain dom = domain::a; };
std::vector<zero_mask> zeros;
std::vector<std::uint8_t> kinds; // 0 = 2pc, 1 = rss, 2 = dealer, 3 = zero
kinds.reserve(wave.exchanges.size());
for (auto ex : wave.exchanges)
{
ensure(ex.id);
const auto nb = p.value_bytes_of(ex.id);
const auto op = p.opcode_of(ex.id);
const bool neighbor = protocol::detail::is_rss_neighbor_open(p, ex.id);
const bool from_dealer = op == opcodes::dealer_pad;
const bool zero = op == opcodes::dealer_zero;
kinds.push_back(zero ? 3 : (from_dealer ? 2 : (neighbor ? 1 : 0)));
if (zero)
zeros.push_back(zero_mask{nb, p.domain_of(ex.id)});
std::vector<std::uint8_t> chunk(nb, 0);
if (protocol::detail::is_beaver_opcode(op) && opt.beavers != nullptr)
{
const auto sess = static_cast<std::size_t>(p.level_of(ex.id));
const auto bi =
static_cast<std::size_t>(op - opcodes::beaver_delta);
opt.beavers->pack(sess, bi, chunk.data(), nb);
std::memcpy(values[ex.id].data(), chunk.data(), nb);
}
else
{
const auto & ins = p.inputs_of(ex.id);
if (!ins.empty() && !protocol::detail::is_beaver_opcode(op))
{
const auto src = ins[0];
ensure(src);
const auto sb = p.value_bytes_of(src);
std::memcpy(chunk.data(), values[src].data(),
std::min(sb, nb));
std::memcpy(values[ex.id].data(), values[src].data(),
std::min(sb, nb));
}
else
std::memcpy(chunk.data(), values[ex.id].data(), nb);
}
if (from_dealer)
dealer.insert(dealer.end(), chunk.begin(), chunk.end());
else if (zero)
continue;
else if (neighbor)
rss.insert(rss.end(), chunk.begin(), chunk.end());
else
two_pc.insert(two_pc.end(), chunk.begin(), chunk.end());
}
std::vector<std::uint8_t> two_pc_peer;
std::vector<std::uint8_t> rss_peer;
std::vector<std::uint8_t> dealer_peer;
std::vector<std::vector<std::uint8_t>> zero_recv(zeros.size());
if (!rss.empty())
rss_peer = net.exchange_vec_with(rss_next, rss);
if (!two_pc.empty())
{
if (self == role::p2)
{
// Dealer idle on 2PC opens — leave peer zeros; reconstruct
// will not run for p2 on those slots.
two_pc_peer.assign(two_pc.size(), 0);
}
else
two_pc_peer = net.exchange_vec_with(peer, two_pc);
}
if (!dealer.empty())
{
if (self == role::p2)
{
net.send_bytes_to(role::p0, net::msg::bytes, dealer.data(),
dealer.size());
net.send_bytes_to(role::p1, net::msg::bytes, dealer.data(),
dealer.size());
dealer_peer = dealer;
}
else
dealer_peer = net.recv_bytes_from(role::p2, net::msg::bytes);
}
if (!zeros.empty())
{
if (self == role::p2)
{
for (std::size_t zi = 0; zi < zeros.size(); ++zi)
{
std::vector<std::uint8_t> a(zeros[zi].n), b(zeros[zi].n);
for (auto & byte : a)
byte = dpf::uniform_sample<std::uint8_t>();
if (zeros[zi].dom == domain::b)
b = a;
else if (zeros[zi].n % 8 == 0)
{
for (std::size_t i = 0; i < a.size(); i += 8)
{
std::uint64_t w = 0;
std::memcpy(&w, a.data() + i, 8);
w = static_cast<std::uint64_t>(0) - w;
std::memcpy(b.data() + i, &w, 8);
}
}
else
{
for (std::size_t i = 0; i < a.size(); ++i)
b[i] = a[i];
}
net.send_bytes_to(role::p0, net::msg::bytes, a.data(), a.size());
net.send_bytes_to(role::p1, net::msg::bytes, b.data(), b.size());
}
}
else
{
for (std::size_t zi = 0; zi < zeros.size(); ++zi)
zero_recv[zi] = net.recv_bytes_from(role::p2, net::msg::bytes);
}
}
std::size_t off2 = 0, offr = 0, offd = 0, zi = 0;
for (std::size_t i = 0; i < wave.exchanges.size(); ++i)
{
auto ex = wave.exchanges[i];
const auto nb = p.value_bytes_of(ex.id);
ensure(ex.id);
const auto op = p.opcode_of(ex.id);
const bool neighbor = kinds[i] == 1;
const bool from_dealer = kinds[i] == 2;
if (kinds[i] == 3)
{
if (self != role::p2)
{
if (zi >= zero_recv.size() || zero_recv[zi].size() < nb)
throw std::runtime_error("drive_composed_trio: zero mask");
std::memcpy(values[ex.id].data(), zero_recv[zi].data(), nb);
}
++zi;
continue;
}
if (from_dealer)
{
if (dealer_peer.size() < offd + nb)
throw std::runtime_error("drive_composed_trio: dealer size");
std::memcpy(values[ex.id].data(), dealer_peer.data() + offd, nb);
offd += nb;
continue;
}
const std::uint8_t * peer_bytes =
neighbor ? rss_peer.data() + offr : two_pc_peer.data() + off2;
if (neighbor)
offr += nb;
else
off2 += nb;
if (self == role::p2 && !neighbor)
continue;
if (protocol::detail::is_beaver_opcode(op) && opt.beavers != nullptr)
{
const auto sess = static_cast<std::size_t>(p.level_of(ex.id));
const auto bi =
static_cast<std::size_t>(op - opcodes::beaver_delta);
opt.beavers->apply(sess, bi, peer_bytes, nb);
}
domain d = p.domain_of(ex.id);
const auto & ins = p.inputs_of(ex.id);
if (!ins.empty())
d = p.domain_of(ins[0]);
if (neighbor)
{
std::memcpy(values[ex.id].data(), peer_bytes, nb);
}
else
{
std::vector<std::uint8_t> mine(nb, 0);
if (!ins.empty() && !protocol::detail::is_beaver_opcode(op))
{
const auto sb = p.value_bytes_of(ins[0]);
std::memcpy(mine.data(), values[ins[0]].data(),
std::min(sb, nb));
}
else
std::memcpy(mine.data(), values[ex.id].data(), nb);
protocol::detail::reconstruct_open(d, c.party(), mine.data(),
peer_bytes, nb, values[ex.id].data(), opt.field);
}
}
}
}
/// @brief Schedule a fused Express/Sabre point+audit walk on a composer.
/// @details Prefer this over `open_and_sketch` when the sketch can ride in the
/// last CW flush — one fewer exchange-bearing wave.
inline protocol::composer::walk_result schedule_fused_audit(
protocol::composer & c, protocol::node seed, std::size_t depth,
std::size_t slot_bytes, protocol::node sketch)
{
return c.fss_point_fused(seed, depth, slot_bytes, sketch);
}
/// @brief Fold opened outputs into a sketch and exchange for `sketch_verify`.
/// @details Used only by extractable party flows. Prefer Express/Sabre
/// `schedule_fused_audit` / `fss_point_fused` so the sketch rides in
/// the last CW flush instead of adding a round. Non-extractable keys
/// leave `note_sketch` as a no-op; callers still exchange empty shares.
template <typename KeyT, typename YRange, typename RRange>
bool open_and_sketch(trio & net, role self, sketch_share & local,
YRange && ys, RRange && rs)
{
note_sketch<KeyT>(local, std::forward<YRange>(ys), std::forward<RRange>(rs));
role peer = self == role::p0 ? role::p1 : role::p0;
sketch_share theirs = net.exchange_with(peer, local, net::msg::sketch_share);
if (self == role::p0)
return sketch_verify(local, theirs);
return sketch_verify(theirs, local);
}
/// @brief Keep parties aligned across `--repeat` iterations.
inline void sync_round(trio & net, role self, std::uint64_t round)
{
if (self == role::p2)
{
net.to(role::p0).send(net::msg::case_ok, round);
net.to(role::p1).send(net::msg::case_ok, round);
return;
}
auto got = net.to(role::p2).recv<std::uint64_t>(net::msg::case_ok);
require(got == round, "repeat sync");
}
} // namespace util
} // namespace party
} // namespace dpf
#endif // LIBDPF_PARTY_FLOW_UTIL_HPP__

465
party/flows_bulk.cpp Normal file
View file

@ -0,0 +1,465 @@
/// @file party/flows_bulk.cpp
/// @brief Parametric beaver / DCF / Horner flows for coverage and benchmarks.
#include "cases.hpp"
#include "flow_util.hpp"
#include "key_io.hpp"
#include "registry.hpp"
#include <cstdint>
#include <string>
#include <vector>
#include "simde/simde/x86/avx2.h"
#include "dpf/beaver.hpp"
#include "dpf/dcf.hpp"
#include "dpf/dpf_key.hpp"
#include "dpf/eval_point.hpp"
#include "dpf/eval_unified.hpp"
#include "dpf/incremental.hpp"
#include "dpf/prg_aes.hpp"
#include <tuple>
namespace dpf
{
namespace party
{
namespace bulk
{
using util::Counter;
using util::evaluate_online;
using util::install_and_bind_u64;
using util::open_additive;
using util::require;
using util::role;
using util::share_bits;
using util::trio;
using util::u64;
// ---- parametric beaver product ----
template <u64 X, u64 Y>
int beaver_product_xy(role self, trio & net)
{
beavers::session<u64> s;
auto x = s.input();
auto y = s.input();
auto z = s(x * y);
if (self == role::p2)
{
Counter rng;
s.sample(rng);
net::deal_session(net, s);
return 0;
}
install_and_bind_u64(s, net, self, {x, y}, {X, Y});
evaluate_online(s, net, self);
auto o = open_additive(net, self, s.value_party(z));
if (self == role::p0)
require(o == X * Y, "product_xy");
return 0;
}
// ---- parametric dot ----
template <std::size_t N>
int beaver_dot_n(role self, trio & net)
{
beavers::session<u64> s;
std::vector<beavers::session<u64>::wire> xs, ys;
std::vector<u64> secrets;
u64 expect = 0;
for (std::size_t i = 0; i < N; ++i)
{
xs.push_back(s.input());
ys.push_back(s.input());
u64 a = static_cast<u64>(i + 1);
u64 b = static_cast<u64>(N - i);
secrets.push_back(a);
expect += a * b;
}
for (std::size_t i = 0; i < N; ++i)
secrets.push_back(static_cast<u64>(N - i));
auto z = s.dot(xs, ys);
if (self == role::p2)
{
Counter rng;
s.sample(rng);
require(s.round_of(z) == 1, "dot round");
net::deal_session(net, s);
return 0;
}
std::vector<beavers::session<u64>::wire> wires = xs;
wires.insert(wires.end(), ys.begin(), ys.end());
install_and_bind_u64(s, net, self, wires, secrets);
evaluate_online(s, net, self);
auto o = open_additive(net, self, s.value_party(z));
if (self == role::p0)
require(o == expect, "dot_n");
return 0;
}
// ---- parametric scale ----
template <std::size_t N>
int beaver_scale_n(role self, trio & net)
{
beavers::session<u64> s;
auto sc = s.input();
std::vector<beavers::session<u64>::wire> lanes;
std::vector<u64> secrets{3u};
for (std::size_t i = 0; i < N; ++i)
{
lanes.push_back(s.input());
secrets.push_back(static_cast<u64>(i + 1));
}
auto out = s.scale(sc, lanes);
if (self == role::p2)
{
Counter rng;
s.sample(rng);
net::deal_session(net, s);
return 0;
}
std::vector<beavers::session<u64>::wire> wires{sc};
wires.insert(wires.end(), lanes.begin(), lanes.end());
install_and_bind_u64(s, net, self, wires, secrets);
evaluate_online(s, net, self);
std::vector<u64> mine;
mine.reserve(N);
for (std::size_t i = 0; i < N; ++i)
mine.push_back(s.value_party(out[i]));
auto opened = open_additive(net, self, mine);
if (self == role::p0)
{
for (std::size_t i = 0; i < N; ++i)
require(opened[i] == 3u * static_cast<u64>(i + 1), "scale_n");
}
return 0;
}
// ---- parametric stream ----
template <std::size_t N>
int beaver_stream_n(role self, trio & net)
{
using block = prg::aes128::block_type;
block seed = simde_mm_set_epi64x(0x9, static_cast<long long>(N));
if (self == role::p2)
{
beavers::oracle<u64> src(seed, 8);
std::vector<beavers::beaver2<u64>> bulk(N);
beavers::fill_beaver2(src, 0, bulk.data(), N);
require(bulk[0].ab.open() == bulk[0].a.open() * bulk[0].b.open(), "s0");
require(bulk[N - 1].ab.open() == bulk[N - 1].a.open() * bulk[N - 1].b.open(),
"sN");
std::vector<u64> p0, p1;
p0.reserve(N * 4);
p1.reserve(N * 4);
for (std::size_t i = 0; i < N; ++i)
{
p0.insert(p0.end(),
{bulk[i].a.p0, bulk[i].b.p0, bulk[i].ab.p0, bulk[i].out.p0});
p1.insert(p1.end(),
{bulk[i].a.p1, bulk[i].b.p1, bulk[i].ab.p1, bulk[i].out.p1});
}
net.to(role::p0).send_vec(p0);
net.to(role::p1).send_vec(p1);
return 0;
}
auto shares = net.to(role::p2).recv_vec<u64>();
require(shares.size() == N * 4, "stream size");
// Online open copies 0 and N/2.
auto open_copy = [&](std::size_t i, u64 sx, u64 sy) {
u64 a = shares[i * 4 + 0];
u64 b = shares[i * 4 + 1];
u64 ab = shares[i * 4 + 2];
auto [x0, x1] = util::split_u64(sx, static_cast<unsigned>(i * 2 + 1));
auto [y0, y1] = util::split_u64(sy, static_cast<unsigned>(i * 2 + 2));
u64 xs = self == role::p0 ? x0 : x1;
u64 ys = self == role::p0 ? y0 : y1;
role peer = self == role::p0 ? role::p1 : role::p0;
u64 dx = (xs + a) + net.exchange_with(peer, xs + a);
u64 dy = (ys + b) + net.exchange_with(peer, ys + b);
u64 z = ab;
z -= dx * b;
z -= dy * a;
if (self == role::p0)
z += dx * dy;
auto open = open_additive(net, self, z);
if (self == role::p0)
require(open == sx * sy, "stream online");
};
open_copy(0, 6u, 7u);
open_copy(N / 2, 3u, 11u);
return 0;
}
// ---- parametric public horner (one round) ----
template <std::size_t Degree>
int beaver_horner_d(role self, trio & net)
{
beavers::session<u64> s;
auto x = s.input();
beavers::session<u64>::wire z;
if constexpr (Degree == 1)
z = s.horner(x, {1ull, 2ull});
else if constexpr (Degree == 2)
z = s.horner(x, {1ull, 2ull, 3ull});
else if constexpr (Degree == 3)
z = s.horner(x, {1ull, 2ull, 3ull, 4ull});
else if constexpr (Degree == 4)
z = s.horner(x, {1ull, 2ull, 3ull, 4ull, 5ull});
else if constexpr (Degree == 5)
z = s.horner(x, {1ull, 2ull, 3ull, 4ull, 5ull, 6ull});
else if constexpr (Degree == 6)
z = s.horner(x, {1ull, 2ull, 3ull, 4ull, 5ull, 6ull, 7ull});
else if constexpr (Degree == 7)
z = s.horner(x, {1ull, 2ull, 3ull, 4ull, 5ull, 6ull, 7ull, 8ull});
else
z = s.horner(x, {1ull, 2ull, 3ull, 4ull, 5ull, 6ull, 7ull, 8ull, 9ull});
const u64 xv = 5u;
u64 expect = 0;
u64 pow = 1;
for (std::size_t i = 0; i <= Degree; ++i)
{
expect += static_cast<u64>(i + 1) * pow;
pow *= xv;
}
if (self == role::p2)
{
require(s.round_of(z) == 1, "horner one round");
Counter rng;
s.sample(rng);
net::deal_session(net, s);
return 0;
}
install_and_bind_u64(s, net, self, {x}, {xv});
evaluate_online(s, net, self);
auto o = open_additive(net, self, s.value_party(z));
if (self == role::p0)
require(o == expect, "horner_d");
return 0;
}
// ---- sign * horner one round ----
int beaver_sign_horner(role self, trio & net)
{
beavers::session<u64> s;
auto sgn = s.input();
auto x = s.input();
auto z = s.horner(sgn, x, {u64{4}, u64{5}, u64{6}}); // sgn*(4+5x+6x^2)
if (self == role::p2)
{
require(s.round_of(z) == 1, "sign horner round");
Counter rng;
s.sample(rng);
net::deal_session(net, s);
return 0;
}
install_and_bind_u64(s, net, self, {sgn, x}, {2u, 3u});
evaluate_online(s, net, self);
auto o = open_additive(net, self, s.value_party(z));
if (self == role::p0)
require(o == 2u * (4u + 5u * 3u + 6u * 9u), "sign_horner");
return 0;
}
// ---- DCF kinds ----
template <int Kind> // 0=lt 1=leq 2=gt 3=geq
int dcf_kind_point(role self, trio & net)
{
using Input = std::uint8_t;
const Input alpha = 0x40;
const Input x0 = 0x15;
const Input x1 = static_cast<Input>(alpha ^ x0);
const std::uint64_t beta = 9;
auto check = [&](const auto & key) {
auto below = eval_point(cmp, key, static_cast<Input>(alpha - 1));
auto on = eval_point(cmp, key, alpha);
auto above = eval_point(cmp, key, static_cast<Input>(alpha + 1));
std::vector<u64> mine{
share_bits(below) & key.cmp().mask,
share_bits(on) & key.cmp().mask,
share_bits(above) & key.cmp().mask};
auto opened = open_additive(net, self, mine);
const auto ob = opened[0] & key.cmp().mask;
const auto oo = opened[1] & key.cmp().mask;
const auto oa = opened[2] & key.cmp().mask;
if (self == role::p0)
{
if constexpr (Kind == 0)
require(ob == beta && oo == 0 && oa == 0, "lt");
else if constexpr (Kind == 1)
require(ob == beta && oo == beta && oa == 0, "leq");
else if constexpr (Kind == 2)
require(ob == 0 && oo == 0 && oa == beta, "gt");
else
require(ob == 0 && oo == beta && oa == beta, "geq");
}
};
if constexpr (Kind == 0)
{
require_opened(dist_with_cmp_key<prg::aes128, prg::aes128, true>(net, self, x0, x1, lt(beta), check, check),
self, encoded_xor_point(x0, x1));
}
else if constexpr (Kind == 1)
{
require_opened(dist_with_cmp_key<prg::aes128, prg::aes128, true>(net, self, x0, x1, leq(beta), check, check),
self, encoded_xor_point(x0, x1));
}
else if constexpr (Kind == 2)
{
require_opened(dist_with_cmp_key<prg::aes128, prg::aes128, true>(net, self, x0, x1, gt(beta), check, check),
self, encoded_xor_point(x0, x1));
}
else
{
require_opened(dist_with_cmp_key<prg::aes128, prg::aes128, true>(net, self, x0, x1, geq(beta), check, check),
self, encoded_xor_point(x0, x1));
}
return 0;
}
// ---- classic point eval bench ----
template <std::uint8_t Alpha, std::uint64_t Beta>
int dpf_point_ab(role self, trio & net)
{
using Input = std::uint8_t;
const Input alpha{Alpha};
const Input x0 = static_cast<Input>(alpha ^ Input{0x5a});
const Input x1 = static_cast<Input>(alpha ^ x0);
auto on = [&](const auto & key) {
auto y = *eval_point(key, alpha);
auto open = util::open_subtractive(net, self, share_bits(y));
if (self == role::p0)
require(open == Beta, "point");
auto yz = *eval_point(key, static_cast<Input>(Alpha ^ 1u));
auto oz = util::open_subtractive(net, self, share_bits(yz));
if (self == role::p0)
require(oz == 0u, "point off");
};
require_tree_prefix(
dist_with_point_key<prg::aes128, prg::aes128, true>(net, self, x0, x1, std::uint64_t{Beta}, on, on),
self, verifiable_tree_prefix<prg::aes128, prg::aes128, std::uint64_t>(
x0, x1));
return 0;
}
// ---- multivariate poly one-round ----
int beaver_poly_cubic_cluster(role self, trio & net)
{
beavers::session<u64> s;
auto x = s.input();
auto y = s.input();
auto z = s.input();
auto out = s(x * y * z + x * y + z);
if (self == role::p2)
{
// Degree-3 monomials need two interactive rounds (see beaver_test xyz).
require(s.round_of(out) == 2, "cluster round");
Counter rng;
s.sample(rng);
net::deal_session(net, s);
return 0;
}
install_and_bind_u64(s, net, self, {x, y, z}, {2u, 3u, 5u});
evaluate_online(s, net, self);
auto o = open_additive(net, self, s.value_party(out));
if (self == role::p0)
require(o == 2u * 3u * 5u + 2u * 3u + 5u, "cluster");
return 0;
}
int beaver_like_terms(role self, trio & net)
{
beavers::session<u64> s;
auto x = s.input();
auto y = s.input();
auto z = s(3 * x * y + 2 * x * y);
if (self == role::p2)
{
Counter rng;
s.sample(rng);
net::deal_session(net, s);
return 0;
}
install_and_bind_u64(s, net, self, {x, y}, {4u, 5u});
evaluate_online(s, net, self);
auto o = open_additive(net, self, s.value_party(z));
if (self == role::p0)
require(o == 5u * 4u * 5u, "like");
return 0;
}
#define REG(name, tags, fn, benchable) \
register_flow(flow{#name, tags, fn, benchable})
} // namespace bulk
void register_bulk_flows()
{
using namespace bulk;
REG(beaver_product_3_5, "beaver product bench", (beaver_product_xy<3, 5>), true);
REG(beaver_product_11_13, "beaver product bench", (beaver_product_xy<11, 13>), true);
REG(beaver_product_100_200, "beaver product bench", (beaver_product_xy<100, 200>), true);
REG(beaver_product_max8, "beaver product bench",
(beaver_product_xy<0xffffffffffffffffull, 2>), true);
REG(beaver_dot_n1, "beaver dot bench", beaver_dot_n<1>, true);
REG(beaver_dot_n2, "beaver dot bench", beaver_dot_n<2>, true);
REG(beaver_dot_n4, "beaver dot bench", beaver_dot_n<4>, true);
REG(beaver_dot_n8, "beaver dot bench", beaver_dot_n<8>, true);
REG(beaver_dot_n16, "beaver dot bench", beaver_dot_n<16>, true);
REG(beaver_dot_n32, "beaver dot bench", beaver_dot_n<32>, true);
REG(beaver_scale_n1, "beaver scale bench", beaver_scale_n<1>, true);
REG(beaver_scale_n4, "beaver scale bench", beaver_scale_n<4>, true);
REG(beaver_scale_n16, "beaver scale bench", beaver_scale_n<16>, true);
REG(beaver_scale_n64, "beaver scale bench", beaver_scale_n<64>, true);
REG(beaver_stream_n8, "beaver stream bench", beaver_stream_n<8>, true);
REG(beaver_stream_n32, "beaver stream bench", beaver_stream_n<32>, true);
REG(beaver_stream_n128, "beaver stream bench", beaver_stream_n<128>, true);
REG(beaver_stream_n512, "beaver stream bench", beaver_stream_n<512>, true);
REG(beaver_stream_n2048, "beaver stream bench", beaver_stream_n<2048>, true);
REG(beaver_horner_d1, "beaver horner grotto bench", beaver_horner_d<1>, true);
REG(beaver_horner_d2, "beaver horner grotto bench", beaver_horner_d<2>, true);
REG(beaver_horner_d3, "beaver horner grotto bench", beaver_horner_d<3>, true);
REG(beaver_horner_d4, "beaver horner grotto bench", beaver_horner_d<4>, true);
REG(beaver_horner_d5, "beaver horner grotto bench", beaver_horner_d<5>, true);
REG(beaver_horner_d6, "beaver horner grotto bench", beaver_horner_d<6>, true);
REG(beaver_horner_d7, "beaver horner grotto bench", beaver_horner_d<7>, true);
REG(beaver_horner_d8, "beaver horner grotto bench", beaver_horner_d<8>, true);
REG(beaver_sign_horner, "beaver horner grotto bench", beaver_sign_horner, true);
REG(beaver_poly_cubic_cluster, "beaver poly bench", beaver_poly_cubic_cluster, true);
REG(beaver_like_terms, "beaver poly bench", beaver_like_terms, true);
REG(dcf_lt, "dcf grotto bench", dcf_kind_point<0>, true);
REG(dcf_leq, "dcf grotto bench", dcf_kind_point<1>, true);
REG(dcf_gt, "dcf grotto bench", dcf_kind_point<2>, true);
REG(dcf_geq, "dcf grotto bench", dcf_kind_point<3>, true);
REG(dpf_point_2a_7, "dpf geneval bench", (dpf_point_ab<0x2a, 7>), true);
REG(dpf_point_00_1, "dpf geneval bench", (dpf_point_ab<0x00, 1>), true);
REG(dpf_point_ff_99, "dpf geneval bench", (dpf_point_ab<0xff, 99>), true);
REG(dpf_point_80_42, "dpf geneval bench", (dpf_point_ab<0x80, 42>), true);
}
#undef REG
} // namespace party
} // namespace dpf

792
party/flows_coverage.cpp Normal file
View file

@ -0,0 +1,792 @@
/// @file party/flows_coverage.cpp
/// @brief Party flows that close protocol / type gaps not hit by other amalgams.
/// @details Amalgamated into run.cpp (do not compile as a second TU). Dealer
/// (p2) distributes keys for paint / incremental / exotic payloads
/// because oblivious DS rejects paint kinds. Geneval full/sequence and
/// arith_output are dealer-run and share-split. CCMP is a 2-party mesh.
#include "cases.hpp"
#include "dist_dpf3.hpp"
#include "flow_util.hpp"
#include "key_io.hpp"
#include "registry.hpp"
#include <array>
#include <cstdint>
#include <cstring>
#include <string>
#include <utility>
#include <vector>
#include "simde/simde/x86/avx2.h"
#include "dpf.hpp"
#include "dpf/bitstring.hpp"
#include "dpf/constrained_cmp.hpp"
#include "dpf/dcf.hpp"
#include "dpf/dpf3_ds.hpp"
#include "dpf/eval_full.hpp"
#include "dpf/eval_inner_product.hpp"
#include "dpf/eval_point.hpp"
#include "dpf/eval_sequence.hpp"
#include "dpf/field128.hpp"
#include "dpf/geneval.hpp"
#include "dpf/json.hpp"
#include "dpf/keyword2.hpp"
#include "dpf/modint.hpp"
#include "dpf/p256.hpp"
#include "grotto/fixedpoint.hpp"
#include "grotto/nmod.hpp"
namespace dpf
{
namespace party
{
namespace coverage
{
using util::open_additive;
using util::open_subtractive;
using util::require;
using util::role;
using util::share_bits;
using util::trio;
using util::u64;
static constexpr char cov_kw_pat[] = "[ab]{8}";
// ---- shared helpers --------------------------------------------------------
std::uint64_t paint_matched_unit(std::size_t matched, std::uint64_t, bool)
{
return static_cast<std::uint64_t>(matched);
}
std::uint64_t lcp_len(std::uint8_t x, std::uint8_t alpha, std::size_t n = 8)
{
for (std::size_t i = 0; i < n; ++i)
{
const std::uint8_t shift = static_cast<std::uint8_t>(n - 1 - i);
if (((x >> shift) & 1) != ((alpha >> shift) & 1))
return static_cast<std::uint64_t>(i);
}
return n;
}
template <typename Spec, typename Fn0, typename Fn1>
void deal_cmp_keys(trio & net, role self, Spec spec, const Fn0 & on0,
const Fn1 & on1)
{
using Input = std::uint8_t;
const Input alpha = 0xB4;
if (self == role::p2)
{
auto [k0, k1] = make_dpf(alpha, spec);
send_key(net.to(role::p0), k0);
send_key(net.to(role::p1), k1);
return;
}
if (self == role::p0)
{
using K = std::decay_t<decltype(std::get<0>(make_dpf(Input{}, spec)))>;
auto key = recv_key<K>(net.to(role::p2));
on0(key, alpha);
return;
}
using K = std::decay_t<decltype(std::get<1>(make_dpf(Input{}, spec)))>;
auto key = recv_key<K>(net.to(role::p2));
on1(key, alpha);
}
template <typename Key>
void check_paint_grid(trio & net, role self, const Key & key, std::uint8_t alpha,
auto expect_fn, const char * tag)
{
const u64 mask = key.cmp().mask;
const std::array<std::uint8_t, 8> pts{
0, 1, alpha, static_cast<std::uint8_t>(alpha ^ 1u), 0x55, 0xAA, 0xFE, 0xFF};
for (std::uint8_t x : pts)
{
const u64 mine = share_bits(eval_point(cmp, key, x)) & mask;
const u64 opened = open_additive(net, self, mine) & mask;
if (self == role::p0)
require(opened == (expect_fn(x, alpha) & mask), tag);
}
}
// ---- paints (F_DCF path paints; dealer distribute) --------------------------
int cov_paint_lcp(role self, trio & net)
{
auto on = [&](const auto & key, std::uint8_t alpha) {
check_paint_grid(net, self, key, alpha,
[](std::uint8_t x, std::uint8_t a) { return lcp_len(x, a); },
"paint lcp");
};
deal_cmp_keys(net, self, lcp(u64{1}), on, on);
return 0;
}
int cov_paint_one_hot(role self, trio & net)
{
auto on = [&](const auto & key, std::uint8_t alpha) {
check_paint_grid(net, self, key, alpha,
[](std::uint8_t x, std::uint8_t a) {
return 1ULL << lcp_len(x, a);
},
"paint one_hot");
};
deal_cmp_keys(net, self, diverge_one_hot(u64{1}), on, on);
return 0;
}
int cov_paint_break_bit(role self, trio & net)
{
auto on = [&](const auto & key, std::uint8_t alpha) {
check_paint_grid(net, self, key, alpha,
[](std::uint8_t x, std::uint8_t a) {
const auto d = lcp_len(x, a);
if (d >= 8)
return 0ULL;
return 3ULL * ((a >> (7 - d)) & 1);
},
"paint break_bit");
};
deal_cmp_keys(net, self, break_bit(u64{3}), on, on);
return 0;
}
int cov_paint_path(role self, trio & net)
{
auto on = [&](const auto & key, std::uint8_t alpha) {
check_paint_grid(net, self, key, alpha,
[](std::uint8_t x, std::uint8_t a) { return lcp_len(x, a); },
"paint path");
};
deal_cmp_keys(net, self, path_paint(paint_matched_unit), on, on);
return 0;
}
int cov_paint_prefix_with_length(role self, trio & net)
{
auto on = [&](const auto & key, std::uint8_t alpha) {
check_paint_grid(net, self, key, alpha,
[](std::uint8_t x, std::uint8_t a) {
const auto d = lcp_len(x, a);
const u64 low = d == 0 ? 0
: (d >= 8 ? a : static_cast<u64>(a) >> (8 - d));
return (low << 4) | d;
},
"paint prefix_with_length");
};
deal_cmp_keys(net, self, prefix_with_length<4>(u64{1}), on, on);
return 0;
}
// ---- F_IDPF / eq_at / idcf -------------------------------------------------
int cov_idpf_at(role self, trio & net)
{
using Input = std::uint16_t;
const Input alpha = 0xa5c3;
if (self == role::p2)
{
auto [k0, k1] = make_dpf(alpha, at<8>(std::uint8_t{42}),
at<12>(std::uint16_t{7}));
send_key(net.to(role::p0), k0);
send_key(net.to(role::p1), k1);
return 0;
}
auto check = [&](const auto & key) {
const auto y8 = *eval_point(out<0, 8>, key, alpha);
const auto o8 = open_subtractive(net, self, share_bits(y8));
if (self == role::p0)
require(o8 == std::uint8_t{42}, "idpf at8");
const auto y12 = *eval_point(out<1, 12>, key, alpha);
const auto o12 = open_subtractive(net, self, share_bits(y12));
if (self == role::p0)
require(o12 == std::uint16_t{7}, "idpf at12");
};
if (self == role::p0)
{
using K = std::decay_t<decltype(std::get<0>(
make_dpf(Input{}, at<8>(std::uint8_t{}), at<12>(std::uint16_t{}))))>;
check(recv_key<K>(net.to(role::p2)));
return 0;
}
using K = std::decay_t<decltype(std::get<1>(
make_dpf(Input{}, at<8>(std::uint8_t{}), at<12>(std::uint16_t{}))))>;
check(recv_key<K>(net.to(role::p2)));
return 0;
}
int cov_eq_at(role self, trio & net)
{
using Input = std::uint16_t;
const Input alpha = 0x1234;
if (self == role::p2)
{
auto [k0, k1] = make_dpf(alpha,
eq_at<8>(std::uint16_t{99}, std::uint16_t{7}));
send_key(net.to(role::p0), k0);
send_key(net.to(role::p1), k1);
return 0;
}
auto check = [&](const auto & key) {
// High 8 bits of alpha match → true branch 99 on that prefix lane.
const auto y_on = *eval_point(out<0, 8>, key, alpha);
const auto o_on = open_subtractive(net, self, share_bits(y_on));
if (self == role::p0)
require(o_on == std::uint16_t{99}, "eq_at on");
const Input miss = static_cast<Input>(alpha ^ 0x0100);
const auto y_off = *eval_point(out<0, 8>, key, miss);
const auto o_off = open_subtractive(net, self, share_bits(y_off));
if (self == role::p0)
require(o_off == std::uint16_t{7}, "eq_at off");
};
if (self == role::p0)
{
using K = std::decay_t<decltype(std::get<0>(
make_dpf(Input{}, eq_at<8>(std::uint16_t{}, std::uint16_t{}))))>;
check(recv_key<K>(net.to(role::p2)));
return 0;
}
using K = std::decay_t<decltype(std::get<1>(
make_dpf(Input{}, eq_at<8>(std::uint16_t{}, std::uint16_t{}))))>;
check(recv_key<K>(net.to(role::p2)));
return 0;
}
int cov_idcf(role self, trio & net)
{
using Input = std::uint8_t;
const Input alpha = 0x40;
const u64 beta = 5;
if (self == role::p2)
{
auto [k0, k1] = make_dpf(alpha, idcf(gt(beta)));
send_key(net.to(role::p0), k0);
send_key(net.to(role::p1), k1);
return 0;
}
auto check = [&](const auto & key) {
const u64 mask = key.cmp().mask;
for (Input x : {Input{0x3f}, alpha, Input{0x41}})
{
const u64 mine = share_bits(eval_point(cmp, key, x)) & mask;
const u64 opened = open_additive(net, self, mine) & mask;
if (self == role::p0)
{
const u64 want = x > alpha ? beta : 0u;
require(opened == want, "idcf gt");
}
}
};
if (self == role::p0)
{
using K = std::decay_t<decltype(std::get<0>(
make_dpf(Input{}, idcf(gt(u64{})))))>;
check(recv_key<K>(net.to(role::p2)));
return 0;
}
using K = std::decay_t<decltype(std::get<1>(
make_dpf(Input{}, idcf(gt(u64{})))))>;
check(recv_key<K>(net.to(role::p2)));
return 0;
}
// ---- F_GenEval full / sequence / arith_output ------------------------------
struct CovPad
{
std::uint64_t n = 1;
simde__m128i block()
{
auto v = simde_mm_set_epi64x(static_cast<long long>(n),
static_cast<long long>(n * 9 + 3));
n += 2;
return v;
}
std::uint8_t bit() { return static_cast<std::uint8_t>(n++ & 1u); }
};
simde__m128i cov_roots[32];
int cov_ri = 0;
simde__m128i cov_take_root() { return cov_roots[cov_ri++]; }
void cov_reset_roots()
{
cov_ri = 0;
for (int i = 0; i < 32; ++i)
cov_roots[i] = simde_mm_set_epi64x(0x2222 * (i + 1), 0xBEEF0000u + i * 13);
}
HEDLEY_PRAGMA(GCC diagnostic push)
HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes")
ds_randomness<simde__m128i (*)(), CovPad> cov_rng()
{
return {cov_take_root, CovPad{}};
}
HEDLEY_PRAGMA(GCC diagnostic pop)
int cov_geneval_full(role self, trio & net)
{
using Input = std::uint8_t;
using Output = std::uint8_t;
const Input alpha = 0x3c;
const Input x0 = 0x10;
const Input x1 = static_cast<Input>(alpha ^ x0);
const Output y = 0x7e;
if (self == role::p2)
{
cov_reset_roots();
auto g = geneval_full(x0, x1, cov_rng(), y);
require(g.party0.size() == 256u, "geneval_full size");
net.to(role::p0).send_vec(g.party0);
net.to(role::p1).send_vec(g.party1);
return 0;
}
auto mine = net.to(role::p2).recv_vec<Output>();
require(mine.size() == 256u, "geneval_full recv");
const Output opened = open_subtractive(net, self, mine[alpha]);
const Output off = open_subtractive(net, self, mine[0]);
if (self == role::p0)
{
require(opened == y, "geneval_full on");
require(off == Output{0}, "geneval_full off");
}
return 0;
}
int cov_geneval_sequence(role self, trio & net)
{
using Input = std::uint8_t;
using Output = std::uint16_t;
const Input alpha = 0x2a;
const Input x0 = 0x07;
const Input x1 = static_cast<Input>(alpha ^ x0);
const Output y = 99;
const std::array<Input, 5> seq{0, 1, alpha, 200, 255};
if (self == role::p2)
{
cov_reset_roots();
auto g = geneval_sequence(x0, x1, seq.begin(), seq.end(), cov_rng(), y);
require(g.party0.size() == seq.size(), "geneval_seq size");
net.to(role::p0).send_vec(g.party0);
net.to(role::p1).send_vec(g.party1);
return 0;
}
auto mine = net.to(role::p2).recv_vec<Output>();
require(mine.size() == seq.size(), "geneval_seq recv");
for (std::size_t i = 0; i < seq.size(); ++i)
{
const Output opened = open_subtractive(net, self, mine[i]);
if (self == role::p0)
require(opened == (seq[i] == alpha ? y : Output{0}), "geneval_seq");
}
return 0;
}
int cov_arith_output(role self, trio & net)
{
using Input = std::uint8_t;
using Output = std::uint32_t;
const Input alpha = 0x2a;
const Input x0 = 0x55;
const Input x1 = static_cast<Input>(alpha ^ x0);
const Output beta = 0x01020304;
const Output y0 = 0x00010002;
const Output y1 = static_cast<Output>(beta - y0);
if (self == role::p2)
{
cov_reset_roots();
auto g = geneval_point(arith_output, x0, x1, alpha, cov_rng(), y0, y1);
require(g.party0.size() == 1u && g.leaf_live, "arith_output live");
net.to(role::p0).send(net::msg::delta, g.party0[0]);
net.to(role::p1).send(net::msg::delta, g.party1[0]);
return 0;
}
const Output mine = net.to(role::p2).recv<Output>(net::msg::delta);
const Output opened = open_subtractive(net, self, mine);
if (self == role::p0)
require(opened == beta, "arith_output");
return 0;
}
// ---- F_CCMP ----------------------------------------------------------------
int cov_ccmp(role self, trio & net)
{
if (self == role::p2)
return 0;
const u64 mine = self == role::p0 ? 4u : 5u;
const role peer = self == role::p0 ? role::p1 : role::p0;
std::uint8_t z0 = 0, z1 = 0, l = 0;
detail::ccmp_party_terms(mine, self == role::p0 ? 0 : 1, z0, z1, l);
const std::uint8_t peer_z0 = net.exchange_with(peer, z0);
const std::uint8_t peer_z1 = net.exchange_with(peer, z1);
const std::uint8_t peer_l = net.exchange_with(peer, l);
const std::uint8_t opened_z0 = static_cast<std::uint8_t>(z0 ^ peer_z0);
const std::uint8_t opened_z1 = static_cast<std::uint8_t>(z1 ^ peer_z1);
const std::uint8_t t = static_cast<std::uint8_t>(opened_z0 & opened_z1);
const std::uint8_t l1 = self == role::p1 ? l : peer_l;
const std::uint8_t got = static_cast<std::uint8_t>(t ^ l1);
require(got == local_ccmp(4u, 5u), "ccmp mesh");
require(got == 1u, "ccmp 4<5");
bool bad = false;
try
{
(void)local_ccmp(4u, 6u);
}
catch (const std::invalid_argument &)
{
bad = true;
}
require(bad, "ccmp rejects");
return 0;
}
// ---- exotic payloads / domains ---------------------------------------------
template <typename Output>
int cov_payload_point(role self, trio & net, Output beta, const char * tag)
{
using Input = std::uint8_t;
const Input alpha = 0x11;
if (self == role::p2)
{
auto [k0, k1] = make_dpf(alpha, beta);
send_key(net.to(role::p0), k0);
send_key(net.to(role::p1), k1);
return 0;
}
auto check = [&](const auto & key) {
const auto on = *eval_point(key, alpha);
const auto off = *eval_point(key, static_cast<Input>(alpha ^ 1u));
const Output o_on = open_subtractive(net, self, share_bits(on));
const Output o_off = open_subtractive(net, self, share_bits(off));
if (self == role::p0)
{
require(o_on == beta, tag);
require(o_off == Output{}, tag);
}
};
if (self == role::p0)
{
using K = std::decay_t<decltype(std::get<0>(make_dpf(Input{}, beta)))>;
check(recv_key<K>(net.to(role::p2)));
return 0;
}
using K = std::decay_t<decltype(std::get<1>(make_dpf(Input{}, beta)))>;
check(recv_key<K>(net.to(role::p2)));
return 0;
}
int cov_field128(role self, trio & net)
{
return cov_payload_point(self, net, field128{42}, "field128");
}
int cov_gf2(role self, trio & net)
{
return cov_payload_point(self, net, dpf::gf2{1}, "gf2");
}
int cov_gf22(role self, trio & net)
{
return cov_payload_point(self, net, dpf::gf22{3}, "gf22");
}
int cov_gf24(role self, trio & net)
{
return cov_payload_point(self, net, dpf::gf24{0xa}, "gf24");
}
int cov_gf28(role self, trio & net)
{
return cov_payload_point(self, net, dpf::gf28{0x1b}, "gf28");
}
int cov_gf216(role self, trio & net)
{
return cov_payload_point(self, net, dpf::gf216{0x2d}, "gf216");
}
int cov_gf232(role self, trio & net)
{
return cov_payload_point(self, net, dpf::gf232{0x90200001u}, "gf232");
}
int cov_gf264(role self, trio & net)
{
return cov_payload_point(self, net, dpf::gf264{0x11}, "gf264");
}
int cov_p256(role self, trio & net)
{
return cov_payload_point(self, net, p256{1}, "p256");
}
int cov_bitstring(role self, trio & net)
{
using bs = bitstring<16>;
return cov_payload_point(self, net, bs{0xBEEFu}, "bitstring");
}
int cov_fixedpoint(role self, trio & net)
{
using fp = grotto::fixedpoint<16>;
return cov_payload_point(self, net, fp::from_raw(0x00018000), "fixedpoint");
}
int cov_keyword2(role self, trio & net)
{
using kw = keyword2<cov_kw_pat>;
const kw alpha{"abababab"};
const u64 beta = 3;
if (self == role::p2)
{
auto [k0, k1] = make_dpf(alpha, beta);
send_key(net.to(role::p0), k0);
send_key(net.to(role::p1), k1);
return 0;
}
auto check = [&](const auto & key) {
const auto on = *eval_point(key, alpha);
const auto off = *eval_point(key, kw{"babababa"});
const auto o_on = open_subtractive(net, self, share_bits(on));
const auto o_off = open_subtractive(net, self, share_bits(off));
if (self == role::p0)
{
require(o_on == beta, "keyword2 on");
require(o_off == 0u, "keyword2 off");
}
};
if (self == role::p0)
{
using K = std::decay_t<decltype(std::get<0>(make_dpf(kw{}, u64{})))>;
check(recv_key<K>(net.to(role::p2)));
return 0;
}
using K = std::decay_t<decltype(std::get<1>(make_dpf(kw{}, u64{})))>;
check(recv_key<K>(net.to(role::p2)));
return 0;
}
int cov_modint_domain(role self, trio & net)
{
using in_t = modint<10>;
const in_t alpha{3};
if (self == role::p2)
{
auto [k0, k1] = make_dpf(alpha, at<6>(std::uint8_t{1}), std::uint16_t{2});
send_key(net.to(role::p0), k0);
send_key(net.to(role::p1), k1);
return 0;
}
auto check = [&](const auto & key) {
const auto y0 = *eval_point(out<0, 6>, key, alpha);
const auto o0 = open_subtractive(net, self, share_bits(y0));
if (self == role::p0)
require(o0 == std::uint8_t{1}, "modint at6");
const auto y1 = *eval_point(out<1>, key, alpha);
const auto o1 = open_subtractive(net, self, share_bits(y1));
if (self == role::p0)
require(o1 == std::uint16_t{2}, "modint leaf");
};
if (self == role::p0)
{
using K = std::decay_t<decltype(std::get<0>(
make_dpf(in_t{}, at<6>(std::uint8_t{}), std::uint16_t{})))>;
check(recv_key<K>(net.to(role::p2)));
return 0;
}
using K = std::decay_t<decltype(std::get<1>(
make_dpf(in_t{}, at<6>(std::uint8_t{}), std::uint16_t{})))>;
check(recv_key<K>(net.to(role::p2)));
return 0;
}
// ---- grotto nmod (public reduction helper over the mesh) -------------------
int cov_nmod(role self, trio & net)
{
// 42 / 10 with 1/10 ≈ 0x199999999999999a / 2^64, keep 8 residue bits.
const std::int64_t x_raw = 42;
const unsigned x_bits = 0;
const unsigned __int128 recip = 0x199999999999999aull;
const unsigned recip_bits = 64;
const unsigned residue_bits = 8;
auto got = grotto::nmod(x_raw, x_bits, recip, recip_bits, residue_bits);
if (self == role::p2)
{
net.to(role::p0).send(net::msg::delta, got.quotient);
net.to(role::p0).send(net::msg::delta, got.residue);
net.to(role::p1).send(net::msg::delta, got.quotient);
net.to(role::p1).send(net::msg::delta, got.residue);
return 0;
}
const auto q = net.to(role::p2).recv<std::int64_t>(net::msg::delta);
const auto r = net.to(role::p2).recv<std::int64_t>(net::msg::delta);
require(q == got.quotient && r == got.residue, "nmod agree");
require(q == 4 && r != 0, "nmod 42/10");
return 0;
}
// ---- JSON key round-trip over the mesh -------------------------------------
int cov_json_roundtrip(role self, trio & net)
{
using Input = std::uint8_t;
const Input alpha = 0x2a;
const u64 beta = 17;
if (self == role::p2)
{
auto [k0, k1] = make_dpf(alpha, beta);
const std::string s0 = json::to_json(k0);
const std::string s1 = json::to_json(k1);
net.to(role::p0).send_bytes(net::msg::dpf_key,
reinterpret_cast<const std::uint8_t *>(s0.data()), s0.size());
net.to(role::p1).send_bytes(net::msg::dpf_key,
reinterpret_cast<const std::uint8_t *>(s1.data()), s1.size());
return 0;
}
auto body = net.to(role::p2).recv_bytes(net::msg::dpf_key);
const std::string s(reinterpret_cast<const char *>(body.data()), body.size());
using K0 = std::decay_t<decltype(std::get<0>(make_dpf(Input{}, u64{})))>;
using K1 = std::decay_t<decltype(std::get<1>(make_dpf(Input{}, u64{})))>;
if (self == role::p0)
{
auto key = json::from_json<K0>(s);
const auto y = *eval_point(key, alpha);
const auto open = open_subtractive(net, self, share_bits(y));
require(open == beta, "json on");
return 0;
}
auto key = json::from_json<K1>(s);
const auto y = *eval_point(key, alpha);
(void)open_subtractive(net, self, share_bits(y));
return 0;
}
// ---- F_DPF3DS local API (dealer make_dpf3_doerner_shelat) -------------------
int cov_dpf3_ds_local(role self, trio & net)
{
using Input = std::uint8_t;
const Input alpha = 0x2a;
const Input x0 = 0x11;
const Input x1 = static_cast<Input>(alpha ^ x0);
const fp61 beta{9};
if (self == role::p2)
{
auto [k1, k2, k3] = make_dpf3_doerner_shelat(x0, x1, beta);
send_key(net.to(role::p0), k1);
send_key(net.to(role::p1), k2);
const fp61 y = eval_point(k3, alpha);
const fp61 got = recent::open_shamir3(net, self, y);
require(got == beta, "dpf3_ds on");
const fp61 z = eval_point(k3, static_cast<Input>(alpha ^ 1u));
const fp61 off = recent::open_shamir3(net, self, z);
require(off == fp61{}, "dpf3_ds off");
return 0;
}
if (self == role::p0)
{
using K = std::decay_t<decltype(std::get<0>(
make_dpf3_doerner_shelat(Input{}, Input{}, fp61{})))>;
auto key = recv_key<K>(net.to(role::p2));
(void)recent::open_shamir3(net, self, eval_point(key, alpha));
(void)recent::open_shamir3(net, self,
eval_point(key, static_cast<Input>(alpha ^ 1u)));
return 0;
}
using K = std::decay_t<decltype(std::get<1>(
make_dpf3_doerner_shelat(Input{}, Input{}, fp61{})))>;
auto key = recv_key<K>(net.to(role::p2));
(void)recent::open_shamir3(net, self, eval_point(key, alpha));
(void)recent::open_shamir3(net, self,
eval_point(key, static_cast<Input>(alpha ^ 1u)));
return 0;
}
// ---- eval_inner_product on a point DPF -------------------------------------
int cov_eval_inner_product(role self, trio & net)
{
using Input = std::uint8_t;
const Input alpha = 42;
const u64 beta = 7;
if (self == role::p2)
{
auto [k0, k1] = make_dpf(alpha, beta);
send_key(net.to(role::p0), k0);
send_key(net.to(role::p1), k1);
return 0;
}
auto check = [&](const auto & key) {
std::vector<u64> w(11, 1);
const u64 mine = share_bits(
eval_inner_product(paired, key, Input{40}, Input{50}, w));
// Point leaves are subtractive; the guided-tour reconstruct is additive
// on the opened difference after both parties contribute.
const u64 opened = open_subtractive(net, self, mine);
if (self == role::p0)
require(opened == beta * w[2], "eval_inner_product");
};
if (self == role::p0)
{
using K = std::decay_t<decltype(std::get<0>(make_dpf(Input{}, u64{})))>;
check(recv_key<K>(net.to(role::p2)));
return 0;
}
using K = std::decay_t<decltype(std::get<1>(make_dpf(Input{}, u64{})))>;
check(recv_key<K>(net.to(role::p2)));
return 0;
}
#define REG(name, tags, fn) \
register_flow(flow{#name, tags, fn, false})
} // namespace coverage
void register_coverage_flows()
{
using namespace coverage;
REG(cov_paint_lcp, "coverage paint dcf", cov_paint_lcp);
REG(cov_paint_one_hot, "coverage paint dcf", cov_paint_one_hot);
REG(cov_paint_break_bit, "coverage paint dcf", cov_paint_break_bit);
REG(cov_paint_path, "coverage paint dcf", cov_paint_path);
REG(cov_paint_prefix_with_length, "coverage paint dcf",
cov_paint_prefix_with_length);
REG(cov_idpf_at, "coverage idpf", cov_idpf_at);
REG(cov_eq_at, "coverage eq_at", cov_eq_at);
REG(cov_idcf, "coverage idcf", cov_idcf);
REG(cov_geneval_full, "coverage geneval", cov_geneval_full);
REG(cov_geneval_sequence, "coverage geneval", cov_geneval_sequence);
REG(cov_arith_output, "coverage arith_output", cov_arith_output);
REG(cov_ccmp, "coverage ccmp", cov_ccmp);
REG(cov_field128, "coverage payload", cov_field128);
REG(cov_gf2, "coverage payload", cov_gf2);
REG(cov_gf22, "coverage payload", cov_gf22);
REG(cov_gf24, "coverage payload", cov_gf24);
REG(cov_gf28, "coverage payload", cov_gf28);
REG(cov_gf216, "coverage payload", cov_gf216);
REG(cov_gf232, "coverage payload", cov_gf232);
REG(cov_gf264, "coverage payload", cov_gf264);
REG(cov_p256, "coverage payload", cov_p256);
REG(cov_bitstring, "coverage payload", cov_bitstring);
REG(cov_fixedpoint, "coverage payload", cov_fixedpoint);
REG(cov_keyword2, "coverage keyword2", cov_keyword2);
REG(cov_modint_domain, "coverage modint", cov_modint_domain);
REG(cov_nmod, "coverage nmod", cov_nmod);
REG(cov_json_roundtrip, "coverage json", cov_json_roundtrip);
REG(cov_dpf3_ds_local, "coverage dpf3_ds", cov_dpf3_ds_local);
REG(cov_eval_inner_product, "coverage eval_inner_product",
cov_eval_inner_product);
}
#undef REG
} // namespace party
} // namespace dpf

1080
party/flows_extreme.cpp Normal file

File diff suppressed because it is too large Load diff

642
party/flows_gadget.cpp Normal file
View file

@ -0,0 +1,642 @@
/// @file party/flows_gadget.cpp
/// @brief Bench flows for word garbling, stacked Yao, FLUTE, and hidden shuffle.
/// @details Amalgamated into run.cpp. Every cross-party byte goes through
/// `net::trio` (the same local mesh `party_bench` already dials).
/// Two-party gadgets use the p0–p1 link. FLUTE's dealer is p2.
/// The hidden shuffle is the three ring passes, and the receiver
/// feeds the arrived array into the next party step.
#include "cases.hpp"
#include <cstdint>
#include <cstring>
#include <stdexcept>
#include <vector>
#include "dpf/arith_garble.hpp"
#include "dpf/flute.hpp"
#include "dpf/shuffle.hpp"
#include "dpf/yao.hpp"
#include "dpf/yao_stack.hpp"
namespace dpf
{
namespace party
{
namespace gadget
{
using net::role;
using net::trio;
void require(bool cond, const char * msg)
{
if (!cond)
throw std::runtime_error(msg);
}
void put_lab(std::vector<std::uint16_t> & blob, const arith_garble::lab & lab)
{
blob.insert(blob.end(), lab.d.begin(), lab.d.end());
}
arith_garble::lab take_lab(const std::uint16_t *& p, const std::uint16_t * end,
std::uint16_t mod)
{
if (static_cast<std::size_t>(end - p) < arith_garble::k_digits)
throw std::runtime_error("arith garble: short label");
arith_garble::lab lab;
lab.mod = mod;
for (std::size_t i = 0; i < arith_garble::k_digits; ++i)
lab.d[i] = *p++;
return lab;
}
std::uint16_t take_u16(const std::uint16_t *& p, const std::uint16_t * end)
{
if (p == end)
throw std::runtime_error("arith garble: short frame");
return *p++;
}
/// @brief p0 garbles and ships the evaluator view. p1 evaluates that view.
void arith_exchange(trio & net, role self, const arith_garble::circuit & c,
const std::uint16_t * semantic)
{
const auto plain = arith_garble::eval_plain(c, semantic);
if (self == role::p0)
{
auto st = arith_garble::detail::garble(c);
auto active = arith_garble::detail::evaluate(c, st, semantic);
std::size_t out_i = 0;
for (std::uint32_t id : c.outputs())
{
const std::uint16_t mod = c.nodes()[id].mod;
const std::uint16_t got = arith_garble::open_shares(
mod, st.zero[id].d[0], active[id].d[0]);
require(got == plain[id], "arith garble: garbler open");
++out_i;
}
require(out_i == c.outputs().size(), "arith garble: outputs");
std::vector<std::uint16_t> blob;
const auto & nodes = c.nodes();
for (std::uint32_t i = 0; i < nodes.size(); ++i)
{
const auto & nd = nodes[i];
if (nd.code == arith_garble::circuit::op::in)
put_lab(blob, active[i]);
else if (nd.code == arith_garble::circuit::op::proj)
{
for (const auto & row : st.proj[i].row)
put_lab(blob, row);
}
else if (nd.code == arith_garble::circuit::op::pass)
{
put_lab(blob, st.pass[i].payload[0]);
put_lab(blob, st.pass[i].payload[1]);
blob.push_back(st.pass[i].flag_ct[0]);
blob.push_back(st.pass[i].flag_ct[1]);
}
}
for (std::uint32_t id : c.outputs())
blob.push_back(st.zero[id].d[0]);
net.send_vec_to(role::p1, net::msg::bytes, blob);
return;
}
require(self == role::p1, "arith garble: party");
auto blob = net.recv_vec_from<std::uint16_t>(role::p0, net::msg::bytes);
const std::uint16_t * p = blob.data();
const std::uint16_t * end = p + blob.size();
arith_garble::detail::garble_state st;
const auto & nodes = c.nodes();
st.zero.resize(nodes.size());
st.delta.assign(static_cast<std::size_t>(arith_garble::k_max_mod) + 1,
arith_garble::lab{});
for (std::uint16_t m = 1; m <= arith_garble::k_max_mod; ++m)
st.delta[m].mod = m;
st.proj.resize(nodes.size());
st.pass.resize(nodes.size());
std::size_t n_in = 0;
for (const auto & nd : nodes)
if (nd.code == arith_garble::circuit::op::in)
++n_in;
for (std::uint32_t i = 0; i < nodes.size(); ++i)
{
const auto & nd = nodes[i];
if (nd.code == arith_garble::circuit::op::in)
st.zero[i] = take_lab(p, end, nd.mod);
else if (nd.code == arith_garble::circuit::op::proj)
{
const std::uint16_t m = nodes[nd.a].mod;
require(m > 0, "arith garble: projection modulus");
st.proj[i].row.resize(static_cast<std::size_t>(m - 1));
for (std::uint16_t color = 1; color < m; ++color)
st.proj[i].row[static_cast<std::size_t>(color - 1)] =
take_lab(p, end, nd.mod);
}
else if (nd.code == arith_garble::circuit::op::pass)
{
st.pass[i].payload[0] = take_lab(p, end, nd.mod);
st.pass[i].payload[1] = take_lab(p, end, nd.mod);
st.pass[i].flag_ct[0] = take_u16(p, end);
st.pass[i].flag_ct[1] = take_u16(p, end);
}
}
std::vector<std::uint16_t> masks;
masks.reserve(c.outputs().size());
for (std::size_t i = 0; i < c.outputs().size(); ++i)
masks.push_back(take_u16(p, end));
require(p == end, "arith garble: trailing bytes");
std::vector<std::uint16_t> zeros(n_in, 0);
auto active = arith_garble::detail::evaluate(c, st, zeros.data());
std::size_t out_i = 0;
for (std::uint32_t id : c.outputs())
{
const std::uint16_t mod = nodes[id].mod;
const std::uint16_t got = arith_garble::open_shares(
mod, masks[out_i], active[id].d[0]);
require(got == plain[id], "arith garble: evaluator open");
++out_i;
}
}
int run_proj(role self, trio & net, std::uint16_t mod)
{
if (self == role::p2)
return 0;
arith_garble::circuit c;
auto x = c.input(mod);
std::vector<std::uint16_t> phi(mod);
for (std::uint16_t i = 0; i < mod; ++i)
phi[i] = static_cast<std::uint16_t>((i * 3) % mod);
c.out(c.project(x, mod, std::move(phi)));
const std::uint16_t in = static_cast<std::uint16_t>(mod / 2);
arith_exchange(net, self, c, &in);
return 0;
}
int run_mul(role self, trio & net, std::uint16_t p)
{
if (self == role::p2)
return 0;
arith_garble::circuit c;
auto x = c.input(p);
auto y = c.input(p);
c.out(c.mul(x, y));
const std::uint16_t in[2] = {static_cast<std::uint16_t>(p - 1), 2};
arith_exchange(net, self, c, in);
return 0;
}
int run_thresh(role self, trio & net, unsigned b)
{
if (self == role::p2)
return 0;
arith_garble::circuit c;
const auto mod = static_cast<std::uint16_t>(b + 1);
std::vector<arith_garble::wire> bits;
bits.reserve(b);
for (unsigned i = 0; i < b; ++i)
bits.push_back(c.input(mod));
c.out(c.threshold(bits, static_cast<std::uint16_t>(b / 2)));
std::vector<std::uint16_t> in(b, 0);
for (unsigned i = 0; i < b; i += 2)
in[i] = 1;
arith_exchange(net, self, c, in.data());
return 0;
}
int run_chain(role self, trio & net)
{
if (self == role::p2)
return 0;
arith_garble::circuit c;
auto x = c.input(7);
auto y = c.input(7);
auto z = c.mul(x, y);
for (int i = 0; i < 3; ++i)
z = c.mul(z, x);
c.out(z);
const std::uint16_t in[2] = {2, 3};
arith_exchange(net, self, c, in);
return 0;
}
yao::netlist and_n(unsigned n)
{
yao::netlist nl;
std::vector<yao::bit> in;
in.reserve(n);
for (unsigned i = 0; i < n; ++i)
in.push_back(nl.shared_in());
auto acc = in[0];
for (unsigned i = 1; i < n; ++i)
acc = nl.and_(acc, in[i]);
nl.out(acc);
return nl;
}
yao::netlist xor2()
{
yao::netlist nl;
auto a = nl.shared_in();
auto b = nl.shared_in();
nl.out(nl.xor_(a, b));
return nl;
}
/// @brief Garbled tables and the evaluator's labels on the p0–p1 channel.
/// @details Party 1 runs `eval_party1` on the bytes that arrived. The choice
/// labels are the OT output for party 1's bits; base OT is its own
/// bench (`iknp`), not this garble row.
void yao_ship(trio & net, role self, const yao::netlist & nl,
const std::uint8_t * p0_bits, const std::uint8_t * p1_bits)
{
const role peer = self == role::p0 ? role::p1 : role::p0;
auto & link = net.to(peer);
std::size_t nchoice = 0;
std::size_t ndirect = 0;
for (std::uint32_t i = 0; i < nl.n_in(); ++i)
{
const auto kind = nl.kind_at(i);
if (kind == yao::input::shared || kind == yao::input::priv1)
++nchoice;
if (kind == yao::input::shared || kind == yao::input::priv0)
++ndirect;
}
const std::size_t ntable = static_cast<std::size_t>(nl.n_and()) * 2u;
std::vector<std::uint8_t> share;
if (self == role::p0)
{
auto g = yao::detail::garble_party0(nl, p0_bits);
require(g.tables.size() == ntable, "yao: table size");
require(g.direct.size() == ndirect, "yao: direct labels");
require(g.ot0.size() == nchoice && g.ot1.size() == nchoice, "yao: ot labels");
std::vector<yao::block> chosen(nchoice);
std::size_t oi = 0;
for (std::uint32_t i = 0; i < nl.n_in(); ++i)
{
const auto kind = nl.kind_at(i);
if (kind != yao::input::shared && kind != yao::input::priv1)
continue;
chosen[oi] = (p1_bits[i] & 1u) ? g.ot1[oi] : g.ot0[oi];
++oi;
}
std::vector<yao::block> blob;
blob.reserve(ntable + ndirect + nchoice);
blob.insert(blob.end(), g.tables.begin(), g.tables.end());
blob.insert(blob.end(), g.direct.begin(), g.direct.end());
blob.insert(blob.end(), chosen.begin(), chosen.end());
link.send_vec(blob, net::msg::bytes);
share = std::move(g.share);
}
else
{
const auto blob = link.recv_vec<yao::block>(net::msg::bytes);
require(blob.size() == ntable + ndirect + nchoice, "yao: payload size");
std::vector<yao::block> tables(ntable);
std::vector<yao::block> direct(ndirect);
std::vector<yao::block> chosen(nchoice);
if (ntable != 0)
std::memcpy(tables.data(), blob.data(), ntable * sizeof(yao::block));
if (ndirect != 0)
std::memcpy(direct.data(), blob.data() + ntable,
ndirect * sizeof(yao::block));
if (nchoice != 0)
std::memcpy(chosen.data(), blob.data() + ntable + ndirect,
nchoice * sizeof(yao::block));
share = yao::detail::eval_party1(nl, tables, direct, chosen);
}
auto other = net.exchange_vec_with(peer, share, net::msg::delta);
require(other.size() == share.size(), "yao: share length");
std::vector<std::uint8_t> semantic(nl.n_in());
for (std::uint32_t i = 0; i < nl.n_in(); ++i)
semantic[i] = static_cast<std::uint8_t>(p0_bits[i] ^ p1_bits[i]);
auto plain = yao::eval_plain(nl, semantic.data());
require(plain.size() == share.size(), "yao: output length");
for (std::size_t i = 0; i < plain.size(); ++i)
{
const auto opened = static_cast<std::uint8_t>(share[i] ^ other[i]);
require(opened == plain[i], "yao: open");
}
}
int run_if(role self, trio & net, unsigned heavy, unsigned light)
{
if (self == role::p2)
return 0;
auto then_nl = and_n(heavy);
std::vector<std::uint8_t> h0(heavy, 1);
std::vector<std::uint8_t> h1(heavy, 0);
if (self == role::p0)
{
auto else_nl = and_n(light);
std::vector<std::uint8_t> l0(light, 1);
std::vector<std::uint8_t> l1(light, 1);
auto stacked = yao::eval_if(then_nl, else_nl, 0, 0, h0.data(), h1.data(),
l0.data(), l1.data());
std::vector<std::uint8_t> semantic(heavy, 1);
auto plain = yao::eval_plain(then_nl, semantic.data());
require(stacked.share0.size() == plain.size(), "yao stack: outputs");
for (std::size_t i = 0; i < plain.size(); ++i)
{
const auto opened = static_cast<std::uint8_t>(
stacked.share0[i] ^ stacked.share1[i]);
require(opened == plain[i], "yao stack: if");
}
require(stacked.stack_blocks > 0, "yao stack: empty");
}
yao_ship(net, self, then_nl, h0.data(), h1.data());
return 0;
}
int run_hot(role self, trio & net, unsigned k)
{
if (self == role::p2)
return 0;
auto active = and_n(2);
const std::uint8_t bits0[2] = {1, 0};
const std::uint8_t bits1[2] = {0, 1};
if (self == role::p0)
{
std::vector<yao::netlist> branches;
std::vector<std::vector<std::uint8_t>> p0;
std::vector<std::vector<std::uint8_t>> p1;
branches.reserve(k);
for (unsigned i = 0; i < k; ++i)
{
branches.push_back(i % 2 == 0 ? and_n(2) : xor2());
p0.push_back({1, static_cast<std::uint8_t>(i & 1u)});
p1.push_back({0, 1});
}
auto stacked = yao::eval_one_hot(branches, 0, 0, p0, p1);
const std::uint8_t semantic[2] = {1, 1};
auto plain = yao::eval_plain(active, semantic);
require(stacked.share0.size() == plain.size(), "yao stack: one-hot outputs");
for (std::size_t i = 0; i < plain.size(); ++i)
{
const auto opened = static_cast<std::uint8_t>(
stacked.share0[i] ^ stacked.share1[i]);
require(opened == plain[i], "yao stack: one-hot");
}
require(stacked.stack_blocks > 0, "yao stack: one-hot empty");
}
yao_ship(net, self, active, bits0, bits1);
return 0;
}
std::vector<std::uint8_t> flute_columns(unsigned delta, unsigned n_out)
{
const unsigned rows = 1u << delta;
std::vector<std::uint8_t> columns(static_cast<std::size_t>(n_out) * rows);
for (unsigned w = 0; w < n_out; ++w)
for (unsigned j = 0; j < rows; ++j)
columns[static_cast<std::size_t>(w) * rows + j] =
static_cast<std::uint8_t>(((j >> (w % delta)) ^ w) & 1u);
return columns;
}
std::vector<std::uint8_t> flute_bits(unsigned delta)
{
std::vector<std::uint8_t> bits(delta, 0);
bits[0] = 1;
if (delta > 2)
bits[2] = 1;
return bits;
}
std::vector<std::uint8_t> flute_pack(const flute::detail::setup & s, unsigned party)
{
std::vector<std::uint8_t> out;
out.insert(out.end(), s.m.begin(), s.m.end());
out.insert(out.end(), s.share[party].begin(), s.share[party].end());
out.insert(out.end(), s.lamz[party].begin(), s.lamz[party].end());
return out;
}
int run_flute(role self, trio & net, unsigned delta, unsigned n_out)
{
const auto columns = flute_columns(delta, n_out);
const auto x = flute_bits(delta);
const unsigned rows = 1u << delta;
const std::uint32_t full = rows - 1u;
if (self == role::p2)
{
auto setup = flute::detail::make_setup(delta, n_out, 2, x.data());
auto to0 = flute_pack(setup, 0);
auto to1 = flute_pack(setup, 1);
net.send_bytes_to(role::p0, net::msg::bytes, to0.data(), to0.size());
net.send_bytes_to(role::p1, net::msg::bytes, to1.data(), to1.size());
return 0;
}
const unsigned party = self == role::p0 ? 0u : 1u;
auto bytes = net.recv_bytes_from(role::p2, net::msg::bytes);
const std::size_t need = static_cast<std::size_t>(delta) + rows + n_out;
require(bytes.size() == need, "flute: deal size");
flute::detail::setup setup;
setup.m.assign(bytes.begin(), bytes.begin() + delta);
setup.share.assign(2, {});
setup.share[party].assign(bytes.begin() + delta, bytes.begin() + delta + rows);
setup.lamz.assign(2, {});
setup.lamz[party].assign(bytes.end() - static_cast<std::ptrdiff_t>(n_out),
bytes.end());
std::vector<std::uint8_t> mine(static_cast<std::size_t>(n_out) * 2u);
for (unsigned w = 0; w < n_out; ++w)
{
const std::uint8_t * column =
columns.data() + static_cast<std::size_t>(w) * rows;
mine[w] = flute::detail::party_v(setup, party, delta, full, column,
setup.lamz[party][w]);
mine[n_out + w] = setup.lamz[party][w];
}
const role peer = self == role::p0 ? role::p1 : role::p0;
auto theirs = net.exchange_vec_with(peer, mine, net::msg::bytes);
auto expect = flute::eval_plain(delta, n_out, columns.data(), x.data());
for (unsigned w = 0; w < n_out; ++w)
{
const std::uint8_t * column =
columns.data() + static_cast<std::size_t>(w) * rows;
const std::uint8_t t = flute::detail::dot_column(
full, delta, setup.m.data(), column);
const auto opened = static_cast<std::uint8_t>(
mine[w] ^ theirs[w] ^ t ^ mine[n_out + w] ^ theirs[n_out + w]);
require(opened == expect[w], "flute: open");
}
return 0;
}
rss::seed_bundle shuffle_bundle()
{
rss::seed_bundle bundle{};
auto fill = [](rss::seed_block & seed, std::uint8_t tag) {
auto * p = reinterpret_cast<std::uint8_t *>(&seed);
for (std::size_t i = 0; i < sizeof(seed); ++i)
p[i] = static_cast<std::uint8_t>(tag + i * 17u);
};
fill(bundle.k01, 1);
fill(bundle.k12, 2);
fill(bundle.k20, 3);
return bundle;
}
shuffle::shuffle_party_view<std::uint64_t> shuffle_view(unsigned me, std::size_t n)
{
std::uint64_t rng = 0xA5A5A5A5A5A5A5A5ull;
auto draw = [&] {
rng = rng * 6364136223846793005ull + 1u;
return rng;
};
shuffle::shuffle_party_view<std::uint64_t> view;
view.own.resize(n);
view.next.resize(n);
for (std::size_t i = 0; i < n; ++i)
{
const auto a = draw();
const auto b = draw();
const auto c = static_cast<std::uint64_t>(i) - a - b;
if (me == 0)
{
view.own[i] = a;
view.next[i] = b;
}
else if (me == 1)
{
view.own[i] = b;
view.next[i] = c;
}
else
{
view.own[i] = c;
view.next[i] = a;
}
}
return view;
}
int run_shuffle(role self, trio & net, std::size_t n)
{
const unsigned me = static_cast<unsigned>(self);
const auto bundle = shuffle_bundle();
const std::uint64_t index = 1;
auto seeds = rss::party_seeds::from_bundle(bundle, me);
auto view = shuffle_view(me, n);
const unsigned order[3] = {2u, 0u, 1u};
for (unsigned left : order)
{
const unsigned u = shuffle::hidden_u_party(left);
const unsigned side = shuffle::hidden_side_party(left);
if (me == u)
{
auto step = shuffle::shuffle_hidden_pass<std::uint64_t>(
me, seeds, view, index, left, nullptr);
require(step.out.sends, "shuffle: u sends");
require(step.out.to == side, "shuffle: u recipient");
net.send_vec_to(static_cast<role>(step.out.to), net::msg::ring_vector,
step.out.data);
view = std::move(step.view);
}
else if (me == side)
{
auto inbound = net.recv_vec_from<std::uint64_t>(
static_cast<role>(u), net::msg::ring_vector);
auto step = shuffle::shuffle_hidden_pass<std::uint64_t>(
me, seeds, view, index, left, &inbound);
require(step.out.sends, "shuffle: side sends");
require(step.out.to == left, "shuffle: side recipient");
net.send_vec_to(static_cast<role>(step.out.to), net::msg::ring_vector,
step.out.data);
view = std::move(step.view);
}
else
{
auto inbound = net.recv_vec_from<std::uint64_t>(
static_cast<role>(side), net::msg::ring_vector);
auto step = shuffle::shuffle_hidden_pass<std::uint64_t>(
me, seeds, view, index, left, &inbound);
require(!step.out.sends, "shuffle: left-out is silent");
view = std::move(step.view);
}
}
if (self == role::p0)
{
auto from1 = net.recv_vec_from<std::uint64_t>(role::p1, net::msg::ring_vector);
auto from2 = net.recv_vec_from<std::uint64_t>(role::p2, net::msg::ring_vector);
require(from1.size() == n && from2.size() == n, "shuffle: open size");
std::vector<std::uint64_t> clear(n);
for (std::size_t i = 0; i < n; ++i)
clear[i] = i;
auto expect = shuffle::permute(clear,
shuffle::permutation_from_seed(bundle.k01, n, index));
expect = shuffle::permute(expect,
shuffle::permutation_from_seed(bundle.k12, n, index));
expect = shuffle::permute(expect,
shuffle::permutation_from_seed(bundle.k20, n, index));
for (std::size_t i = 0; i < n; ++i)
{
const auto got = view.own[i] + from1[i] + from2[i];
require(got == expect[i], "shuffle: open");
}
}
else
{
net.send_vec_to(role::p0, net::msg::ring_vector, view.own);
}
return 0;
}
int arith_proj_m5(role self, trio & net) { return run_proj(self, net, 5); }
int arith_proj_m17(role self, trio & net) { return run_proj(self, net, 17); }
int arith_proj_m64(role self, trio & net) { return run_proj(self, net, 64); }
int arith_mul_p5(role self, trio & net) { return run_mul(self, net, 5); }
int arith_mul_p7(role self, trio & net) { return run_mul(self, net, 7); }
int arith_mul_p11(role self, trio & net) { return run_mul(self, net, 11); }
int arith_thresh_b8(role self, trio & net) { return run_thresh(self, net, 8); }
int arith_thresh_b16(role self, trio & net) { return run_thresh(self, net, 16); }
int arith_chain_mul4(role self, trio & net) { return run_chain(self, net); }
int yao_if_4_2(role self, trio & net) { return run_if(self, net, 4, 2); }
int yao_if_16_8(role self, trio & net) { return run_if(self, net, 16, 8); }
int yao_onehot_k4(role self, trio & net) { return run_hot(self, net, 4); }
int yao_onehot_k8(role self, trio & net) { return run_hot(self, net, 8); }
int flute_d2(role self, trio & net) { return run_flute(self, net, 2, 1); }
int flute_d4(role self, trio & net) { return run_flute(self, net, 4, 1); }
int flute_d8(role self, trio & net) { return run_flute(self, net, 8, 1); }
int flute_d4_o8(role self, trio & net) { return run_flute(self, net, 4, 8); }
int shuffle_n16(role self, trio & net) { return run_shuffle(self, net, 16); }
int shuffle_n64(role self, trio & net) { return run_shuffle(self, net, 64); }
int shuffle_n256(role self, trio & net) { return run_shuffle(self, net, 256); }
#define REG(name, tags, fn) \
register_flow(flow{#name, tags, fn, true})
} // namespace gadget
void register_gadget_flows()
{
using namespace gadget;
REG(arith_proj_m5, "bench arith garble", arith_proj_m5);
REG(arith_proj_m17, "bench arith garble", arith_proj_m17);
REG(arith_proj_m64, "bench arith garble", arith_proj_m64);
REG(arith_mul_p5, "bench arith garble", arith_mul_p5);
REG(arith_mul_p7, "bench arith garble", arith_mul_p7);
REG(arith_mul_p11, "bench arith garble", arith_mul_p11);
REG(arith_thresh_b8, "bench arith garble", arith_thresh_b8);
REG(arith_thresh_b16, "bench arith garble", arith_thresh_b16);
REG(arith_chain_mul4, "bench arith garble", arith_chain_mul4);
REG(yao_if_4_2, "bench yao stack", yao_if_4_2);
REG(yao_if_16_8, "bench yao stack", yao_if_16_8);
REG(yao_onehot_k4, "bench yao stack", yao_onehot_k4);
REG(yao_onehot_k8, "bench yao stack", yao_onehot_k8);
REG(flute_d2, "bench flute", flute_d2);
REG(flute_d4, "bench flute", flute_d4);
REG(flute_d8, "bench flute", flute_d8);
REG(flute_d4_o8, "bench flute", flute_d4_o8);
REG(shuffle_n16, "bench shuffle rss", shuffle_n16);
REG(shuffle_n64, "bench shuffle rss", shuffle_n64);
REG(shuffle_n256, "bench shuffle rss", shuffle_n256);
}
} // namespace party
} // namespace dpf

1654
party/flows_recent.cpp Normal file

File diff suppressed because it is too large Load diff

847
party/iknp_deal.hpp Normal file
View file

@ -0,0 +1,847 @@
/// @file party/iknp_deal.hpp
/// @brief Two-party IKNP setup for every tape `dist_ds.hpp` takes from p2.
/// @details p0 and p1 sample the same pad correlations the dealer would, write
/// them into a local inbox, and then run the existing walk. p2 is not
/// connected. Covered tapes: additive carry, point (reveal and
/// oblivious hash, wildcard, half-tree, extractable), comparison
/// (edge, value-word, blocked suffix, verifiable hash), and interval
/// containment.
#ifndef LIBDPF_PARTY_IKNP_DEAL_HPP__
#define LIBDPF_PARTY_IKNP_DEAL_HPP__
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <vector>
#include "hedley/hedley.h"
#include "dist_ds.hpp"
#include "dpf/iknp.hpp"
namespace dpf
{
namespace party
{
namespace iknp_deal
{
using net::role;
using net::trio;
struct tape
{
std::vector<std::uint8_t> bytes;
template <typename T>
void put(net::msg tag, const T & value)
{
static_assert(std::is_trivially_copyable_v<T>);
append(tag, &value, sizeof(T));
}
template <typename T>
void put_vec(net::msg tag, const T * data, std::size_t n)
{
static_assert(std::is_trivially_copyable_v<T>);
append(tag, data, n * sizeof(T));
}
void append(net::msg tag, const void * payload, std::size_t n)
{
if (n > 0xffffffffu)
throw std::invalid_argument("iknp tape frame");
const auto len = static_cast<std::uint32_t>(n);
const auto t = net::to_u16(tag);
std::uint8_t hdr[6];
std::memcpy(hdr, &len, 4);
std::memcpy(hdr + 4, &t, 2);
bytes.insert(bytes.end(), hdr, hdr + 6);
if (n != 0)
{
const auto * p = static_cast<const std::uint8_t *>(payload);
bytes.insert(bytes.end(), p, p + n);
}
}
};
struct cursor
{
const iknp::material * mat = nullptr;
std::size_t block = 0;
std::size_t bit = 0;
std::size_t b2a = 0;
std::size_t cw = 0;
std::size_t zero = 0;
std::vector<dist::ring_zero_share_msg> zeros;
dist::and_share_msg take_block()
{
const auto & s = mat->blocks.at(block++);
dist::and_share_msg m;
m.a = s.a;
m.b = s.b;
m.c = s.c;
return m;
}
dist::bit_and_pad_msg take_bit()
{
const auto & s = mat->bits.at(bit++);
return dist::bit_and_pad_msg{s.a, s.b, s.c};
}
dist::b2a_pad_msg take_b2a()
{
const auto & s = mat->b2a.at(b2a++);
return dist::b2a_pad_msg{s.r, s.add};
}
dist::cw_pad_msg take_cw()
{
const auto & s = mat->cws.at(cw++);
dist::cw_pad_msg m;
m.rand = s.rand;
m.gamma = s.gamma;
m.bit = s.bit;
return m;
}
dist::ring_zero_share_msg take_zero()
{
return zeros.at(zero++);
}
void done() const
{
if (mat == nullptr)
return;
if (block != mat->blocks.size() || bit != mat->bits.size()
|| b2a != mat->b2a.size() || cw != mat->cws.size()
|| zero != zeros.size())
throw std::runtime_error("iknp tape did not consume every pad");
}
};
inline int me_of(role self)
{
if (self == role::p0)
return 0;
if (self == role::p1)
return 1;
throw std::invalid_argument("iknp parties are p0 and p1");
}
inline void exchange_zeros(trio & net, int me,
const std::vector<std::uint64_t> & masks, cursor & cur)
{
cur.zeros.resize(masks.size());
const role peer = me == 0 ? role::p1 : role::p0;
for (std::size_t i = 0; i < masks.size(); ++i)
{
if (me == 0)
{
const auto r = dist::random_ring_word(masks[i]);
cur.zeros[i].share = r;
net.send_to(peer, net::msg::ring_vector, detail::dcf_impl::neg_m(r, masks[i]));
}
else
{
cur.zeros[i].share = net.recv_from<std::uint64_t>(peer, net::msg::ring_vector);
}
}
}
inline void barrier(trio & net, role self)
{
const role peer = self == role::p0 ? role::p1 : role::p0;
const std::uint8_t ready = 1;
(void)net.exchange_with(peer, ready, net::msg::delta);
}
inline simde__m128i sample_root(int me)
{
const auto r = dpf::uniform_sample<simde__m128i>();
return me == 0 ? dpf::unset_lo_bit(r) : dpf::set_lo_bit(r);
}
inline dist::level_pad_msg take_level(cursor & cur, int me)
{
const auto cw = cur.take_cw();
const auto first = cur.take_block();
const auto second = cur.take_block();
dist::level_pad_msg m;
m.cw = cw;
if (me == 0)
{
m.mine = first;
m.theirs = second;
}
else
{
m.mine = second;
m.theirs = first;
}
return m;
}
struct demand
{
std::size_t blocks = 0;
std::size_t bits = 0;
std::size_t b2a = 0;
std::size_t cws = 0;
std::vector<std::uint64_t> zero_masks;
};
inline void add_point(demand & d, std::size_t depth, std::size_t n_leaf,
bool oblivious_hash, bool leaf_b2a)
{
d.cws += depth;
d.blocks += depth * 2 + n_leaf;
if (oblivious_hash)
d.bits += depth * dist::hash_level_and_count();
if (leaf_b2a)
d.b2a += n_leaf * 128;
}
inline void add_comparison(demand & d, std::size_t depth, std::size_t prefix,
std::size_t cmp_block, std::size_t cmp_q, std::size_t cmp_h,
bool verifiable, bool oblivious, bool edge, std::uint64_t mask)
{
d.cws += depth;
d.blocks += depth * 2;
if (oblivious && edge && prefix > 1)
d.bits += prefix - 1;
if (oblivious)
{
if (cmp_block == 0)
{
const std::size_t nlev = prefix < depth ? prefix : depth;
d.bits += nlev * 128;
d.b2a += nlev * 129;
}
if (cmp_block > 0 && cmp_q > 0 && cmp_h > 0 && cmp_h <= depth)
{
d.bits += 1;
d.b2a += 3;
}
if (verifiable)
d.bits += depth * dist::hash_level_and_count();
}
d.zero_masks.push_back(mask);
}
template <std::size_t NLeaf>
void emit_point(tape & out, cursor & cur, int me, std::size_t depth,
bool half, bool oblivious_hash, bool leaf_b2a)
{
static_assert(NLeaf >= 1, "leaf mux sends at least one pad");
if (half)
out.put(net::msg::dpf_key, sample_root(me));
for (std::size_t level = 0; level < depth; ++level)
{
out.put(net::msg::beaver_tape, take_level(cur, me));
if (oblivious_hash)
{
const std::size_t n = dist::hash_level_and_count();
std::vector<dist::bit_and_pad_msg> hash(n);
for (std::size_t i = 0; i < n; ++i)
hash[i] = cur.take_bit();
out.put_vec(net::msg::beaver_tape, hash.data(), hash.size());
}
}
dist::leaf_pad_msg<NLeaf> lanes{};
for (std::size_t i = 0; i < NLeaf; ++i)
lanes.lanes[i] = cur.take_block();
out.put(net::msg::beaver_tape, lanes);
if (leaf_b2a)
{
std::vector<dist::b2a_pad_msg> pads(NLeaf * 128);
for (auto & p : pads)
p = cur.take_b2a();
out.put_vec(net::msg::beaver_tape, pads.data(), pads.size());
}
}
inline void emit_comparison(tape & out, cursor & cur, int me, std::size_t depth,
std::size_t prefix, std::size_t cmp_block, std::size_t cmp_q,
std::size_t cmp_h, bool verifiable, bool oblivious, bool edge, bool half)
{
if (half)
out.put(net::msg::dpf_key, sample_root(me));
if (oblivious && edge && prefix > 1)
{
std::vector<dist::bit_and_pad_msg> edge_pads(prefix - 1);
for (auto & p : edge_pads)
p = cur.take_bit();
out.put_vec(net::msg::beaver_tape, edge_pads.data(), edge_pads.size());
}
for (std::size_t level = 0; level < depth; ++level)
{
out.put(net::msg::beaver_tape, take_level(cur, me));
if (oblivious)
{
if (cmp_block == 0 && level < prefix)
{
dist::cmp_level_obliv_msg msg{};
for (std::size_t i = 0; i < 128; ++i)
{
const auto bit = cur.take_bit();
const auto b2 = cur.take_b2a();
msg.bits[i] = dist::word_bit_for(bit, b2);
}
msg.ai = cur.take_b2a();
out.put(net::msg::beaver_tape, msg);
}
if (cmp_block > 0 && cmp_q > 0 && level + 1 == cmp_h)
{
dist::suffix_obliv_msg msg{};
msg.and_pad = cur.take_bit();
for (int i = 0; i < 3; ++i)
msg.b2a[i] = cur.take_b2a();
out.put(net::msg::beaver_tape, msg);
}
if (verifiable)
{
const std::size_t n = dist::hash_level_and_count();
std::vector<dist::bit_and_pad_msg> hash(n);
for (std::size_t i = 0; i < n; ++i)
hash[i] = cur.take_bit();
out.put_vec(net::msg::beaver_tape, hash.data(), hash.size());
}
}
}
out.put(net::msg::beaver_tape, cur.take_zero());
}
template <typename Concrete, typename Leaf>
void exchange_wildcard(trio & net, int me, dist::wildcard_leaf_pad_msg<Concrete, Leaf> & pad)
{
const role peer = me == 0 ? role::p1 : role::p0;
Concrete mine_out = dpf::uniform_sample<Concrete>();
Leaf mine_vec = dist::random_leaf<Leaf>();
Leaf peer_vec = net.exchange_with(peer, mine_vec, net::msg::ring_vector);
Leaf cross_for_peer = dpf::multiply_leaf(peer_vec, mine_out);
Leaf cross = net.exchange_with(peer, cross_for_peer, net::msg::ring_vector);
Leaf zero{};
if (me == 0)
{
zero = dist::random_leaf<Leaf>();
const Leaf neg = dpf::subtract_leaf<Concrete>(Leaf{}, zero);
net.send_to(peer, net::msg::ring_vector, neg);
}
else
{
zero = net.recv_from<Leaf>(peer, net::msg::ring_vector);
}
pad.output_blind = mine_out;
pad.vector_blind = mine_vec;
pad.peer_vector_blind = peer_vec;
pad.cross = cross;
pad.zero_leaf = zero;
}
inline void install(trio & net, role self, tape & out, cursor & cur)
{
cur.done();
barrier(net, self);
net.install_inbox(std::move(out.bytes));
}
template <typename InteriorPRG, typename InputT, typename OutputT,
bool ObliviousHash, bool Additive>
void prepare_point(trio & net, role self)
{
const int me = me_of(self);
using dpf_type = utils::dpf_type_t<InteriorPRG, InteriorPRG, InputT, OutputT,
verifiable>;
using tree = dpf::tree_traits<InteriorPRG>;
constexpr std::size_t depth = dpf_type::depth;
constexpr std::size_t lg = dpf_type::lg_outputs_per_leaf;
constexpr std::size_t n_leaf = dist::leaf_and_slots(lg);
constexpr bool leaf_b2a = lg > 0
&& !utils::has_characteristic_two_v<dpf::concrete_type_t<OutputT>>
&& (!dpf::is_wildcard_v<OutputT> || ObliviousHash);
demand d;
if constexpr (Additive)
d.bits += utils::bitlength_of_v<InputT> - 1;
add_point(d, depth, n_leaf, ObliviousHash, leaf_b2a);
const role peer = self == role::p0 ? role::p1 : role::p0;
auto mat = iknp::sample(net.to(peer), me, d.blocks, d.bits, d.b2a, d.cws);
cursor cur;
cur.mat = &mat;
tape out;
if constexpr (Additive)
{
constexpr std::size_t nbits = utils::bitlength_of_v<InputT>;
for (std::size_t i = 0; i + 1 < nbits; ++i)
out.put(net::msg::beaver_tape, cur.take_bit());
}
emit_point<n_leaf>(out, cur, me, depth, tree::is_half_tree, ObliviousHash,
leaf_b2a);
if constexpr (dpf::is_wildcard_v<OutputT>)
{
using concrete = dpf::concrete_type_t<OutputT>;
using leaf_type = dpf::leaf_node_t<typename dpf_type::exterior_node, concrete>;
dist::wildcard_leaf_pad_msg<concrete, leaf_type> pad{};
exchange_wildcard<concrete, leaf_type>(net, me, pad);
out.put(net::msg::beaver_tape, pad);
}
install(net, self, out, cur);
}
template <typename InteriorPRG, typename ExteriorPRG, typename InputT,
bool Oblivious, typename Spec, typename ...Tags>
void prepare_comparison(trio & net, role self, const Spec & spec,
const Tags & ...tags)
{
const int me = me_of(self);
using pair_type = dist::comparison_pair_t<InteriorPRG, ExteriorPRG, InputT,
Spec, Tags...>;
using key_type = typename pair_type::first_type::key_type;
using tree = dpf::tree_traits<InteriorPRG>;
(void)std::initializer_list<int>{((void)tags, 0)...};
dcf_runtime_spec runtime{};
bool found = false;
detail::incr::collect_cmp_one(runtime, found, spec);
if (!found)
throw std::invalid_argument("dist comparison keygen needs one comparison");
if (runtime.prefix == 0)
runtime.prefix = utils::bitlength_of_v<InputT>;
if constexpr (Oblivious)
{
if (is_paint_kind(runtime.kind))
throw std::invalid_argument(
"oblivious comparison does not support paint kinds");
}
const bool edge = runtime.kind == cmp_kind::leq || runtime.kind == cmp_kind::gt;
demand d;
add_comparison(d, key_type::depth, runtime.prefix, key_type::cmp_block,
key_type::cmp_q, key_type::cmp_h, key_type::is_verifiable, Oblivious,
edge, runtime.mask);
const role peer = self == role::p0 ? role::p1 : role::p0;
auto mat = iknp::sample(net.to(peer), me, d.blocks, d.bits, d.b2a, d.cws);
cursor cur;
cur.mat = &mat;
exchange_zeros(net, me, d.zero_masks, cur);
tape out;
emit_comparison(out, cur, me, key_type::depth, runtime.prefix,
key_type::cmp_block, key_type::cmp_q, key_type::cmp_h,
key_type::is_verifiable, Oblivious, edge, tree::is_half_tree);
install(net, self, out, cur);
}
template <typename InteriorPRG, typename ExteriorPRG, typename InputT,
bool Reveal, typename Beta>
void prepare_ic(trio & net, role self, const ic_pack<Beta> & spec)
{
const int me = me_of(self);
using input_type = std::decay_t<InputT>;
const auto inner = detail::ic_impl::inner_lt(spec);
const std::uint64_t gmask = detail::ic_impl::group_mask_of<Beta>();
constexpr std::size_t nbits = utils::bitlength_of_v<input_type>;
using pair_type = dist::comparison_pair_t<InteriorPRG, ExteriorPRG,
input_type, decltype(inner)>;
using key_type = typename pair_type::first_type::key_type;
using tree = dpf::tree_traits<InteriorPRG>;
dcf_runtime_spec runtime{};
bool found = false;
detail::incr::collect_cmp_one(runtime, found, inner);
if (!found)
throw std::invalid_argument("dist comparison keygen needs one comparison");
if (runtime.prefix == 0)
runtime.prefix = nbits;
constexpr bool oblivious = !Reveal;
if constexpr (oblivious)
{
if (is_paint_kind(runtime.kind))
throw std::invalid_argument(
"oblivious comparison does not support paint kinds");
}
demand d;
if constexpr (oblivious)
{
d.bits += dist::ic_gamma_and_count(nbits);
d.bits += dist::ic_correction_and_count(nbits);
d.b2a += 4;
}
const bool edge = runtime.kind == cmp_kind::leq || runtime.kind == cmp_kind::gt;
add_comparison(d, key_type::depth, runtime.prefix, key_type::cmp_block,
key_type::cmp_q, key_type::cmp_h, key_type::is_verifiable, oblivious,
edge, runtime.mask);
if constexpr (Reveal)
{
d.zero_masks.push_back(gmask);
d.zero_masks.push_back(gmask);
}
else
{
d.zero_masks.push_back(gmask);
}
const role peer = self == role::p0 ? role::p1 : role::p0;
auto mat = iknp::sample(net.to(peer), me, d.blocks, d.bits, d.b2a, d.cws);
cursor cur;
cur.mat = &mat;
exchange_zeros(net, me, d.zero_masks, cur);
tape out;
if constexpr (oblivious)
{
const std::size_t gamma_n = dist::ic_gamma_and_count(nbits);
const std::size_t corr_n = dist::ic_correction_and_count(nbits);
std::vector<dist::bit_and_pad_msg> gamma(gamma_n), corr(corr_n);
for (auto & p : gamma)
p = cur.take_bit();
for (auto & p : corr)
p = cur.take_bit();
std::vector<dist::b2a_pad_msg> b2(4);
for (auto & p : b2)
p = cur.take_b2a();
out.put_vec(net::msg::beaver_tape, gamma.data(), gamma.size());
out.put_vec(net::msg::beaver_tape, corr.data(), corr.size());
out.put_vec(net::msg::beaver_tape, b2.data(), b2.size());
}
emit_comparison(out, cur, me, key_type::depth, runtime.prefix,
key_type::cmp_block, key_type::cmp_q, key_type::cmp_h,
key_type::is_verifiable, oblivious, edge, tree::is_half_tree);
out.put(net::msg::beaver_tape, cur.take_zero());
if constexpr (Reveal)
out.put(net::msg::beaver_tape, cur.take_zero());
install(net, self, out, cur);
}
} // namespace iknp_deal
/// @brief Two-party point keygen. Same walk as `dist_with_point_key`; pads
/// come from IKNP instead of p2.
/// @details Ideal (semi-honest): p0/p1 input XOR or additive shares of α and
/// payload β; each outputs its verifiable DPF key. With
/// `RevealPoint`, the tree prefix (and packed wildcard lane when
/// applicable) may be opened. Without it, α, peer seeds, peer
/// payload shares, and path-opening pad bits stay hidden.
/// \complexity Same tree walk as `dist_with_point_key`, plus one IKNP
/// `sample`. With tape length `T = Θ(n)` on a reveal point
/// (or `T = Θ(n · hash_level_and_count())` when oblivious),
/// `sample` does two Chou–Orlandi base sessions of `κ = 128`
/// OTs on P-256 and two OT-extension directions of cost
/// `Θ(κ T)` bits, then the usual `n` Doerner–Shelat opens.
/// Dealer `make_dpf` is still 0 rounds / 0 bytes; Half-Tree
/// §5.2 (ePrint 2022/1431) is a different `n+3`-round COT/OLE
/// hybrid this path does not implement.
/// \rounds Base OT + extension (constant rounds) then the usual per-level
/// DS exchanges; no p2.
/// \communication IKNP seed OT and extension rows (`Θ(κ T)` bits), then
/// the same peer messages as the dealer walk (no dealer
/// frames).
/// \preprocessing IKNP replaces the dealer tape; there is no offline p2.
template <typename InteriorPRG = dpf::prg::aes128,
typename ExteriorPRG = InteriorPRG,
bool RevealPoint = false,
typename InputT,
typename OutputT,
typename Fn0,
typename Fn1>
HEDLEY_WARN_UNUSED_RESULT
auto dist_with_point_key_iknp(
trio & net, role self, InputT x0, InputT x1, OutputT beta, Fn0 && on0,
Fn1 && on1, bool already_encoded = false, bool additive = false)
{
using opened_type = point_key_opened<InputT>;
using dpf_type = utils::dpf_type_t<InteriorPRG, ExteriorPRG, InputT,
OutputT, verifiable>;
constexpr bool wild_lane = RevealPoint && dpf::is_wildcard_v<OutputT>
&& dpf_type::lg_outputs_per_leaf > 0;
if (additive && already_encoded)
throw std::invalid_argument(
"additive DS input is converted to XOR shares; it is not pre-opened");
if (self == role::p2)
throw std::invalid_argument("iknp point keygen has no dealer");
if (additive)
iknp_deal::prepare_point<InteriorPRG, InputT, OutputT, !RevealPoint, true>(
net, self);
else
iknp_deal::prepare_point<InteriorPRG, InputT, OutputT, !RevealPoint, false>(
net, self);
if (additive)
{
if (self == role::p0)
x0 = dist::additive_to_xor_share<role::p0>(net, x0);
else
x1 = dist::additive_to_xor_share<role::p1>(net, x1);
}
if constexpr (RevealPoint)
{
auto finish = [](auto result) {
opened_type out;
out.opened_prefix = result.opened_prefix;
if constexpr (wild_lane)
out.opened_lane = result.opened_lane;
return out;
};
if (self == role::p0)
{
auto result = dist::point_party<InteriorPRG, ExteriorPRG, InputT,
OutputT, role::p0, false, true>(net, x0, beta, already_encoded);
std::forward<Fn0>(on0)(result.dpf_key);
return std::optional<opened_type>(finish(std::move(result)));
}
auto result = dist::point_party<InteriorPRG, ExteriorPRG, InputT,
OutputT, role::p1, false, true>(net, x1, beta, already_encoded);
std::forward<Fn1>(on1)(result.dpf_key);
return std::optional<opened_type>(finish(std::move(result)));
}
else
{
if (self == role::p0)
std::forward<Fn0>(on0)(dist::point_party<InteriorPRG, ExteriorPRG,
InputT, OutputT, role::p0, false, false>(
net, x0, beta, already_encoded));
else
std::forward<Fn1>(on1)(dist::point_party<InteriorPRG, ExteriorPRG,
InputT, OutputT, role::p1, false, false>(
net, x1, beta, already_encoded));
}
}
/// @brief Two-party extractable point keygen.
template <typename InteriorPRG = dpf::prg::aes128,
typename ExteriorPRG = InteriorPRG,
typename InputT,
typename OutputT,
typename Fn0,
typename Fn1>
void dist_with_extractable_point_key_iknp(trio & net, role self, InputT x0,
InputT x1, OutputT beta, Fn0 && on0, Fn1 && on1)
{
static_assert(!dpf::is_wildcard_v<OutputT>,
"extractable socket gen does not reconstruct a wildcard lane");
if (self == role::p2)
throw std::invalid_argument("iknp point keygen has no dealer");
iknp_deal::prepare_point<InteriorPRG, InputT, OutputT, false, false>(
net, self);
if (self == role::p0)
{
on0(dist::point_party<InteriorPRG, ExteriorPRG, InputT, OutputT,
role::p0, true>(net, x0, beta));
}
else
{
on1(dist::point_party<InteriorPRG, ExteriorPRG, InputT, OutputT,
role::p1, true>(net, x1, beta));
}
}
/// @brief Two-party comparison keygen.
template <typename InteriorPRG = dpf::prg::aes128,
typename ExteriorPRG = InteriorPRG,
bool Reveal = false,
typename InputT,
typename Spec,
typename Fn0,
typename Fn1,
typename ...Tags>
auto dist_with_cmp_key_iknp(trio & net, role self, InputT x0, InputT x1,
const Spec & spec, Fn0 && on0, Fn1 && on1, const Tags & ...tags)
-> std::conditional_t<Reveal, std::optional<InputT>, suppressed_point>
{
if (self == role::p2)
throw std::invalid_argument("iknp comparison keygen has no dealer");
iknp_deal::prepare_comparison<InteriorPRG, ExteriorPRG, InputT, !Reveal>(
net, self, spec, tags...);
if constexpr (Reveal)
{
if (self == role::p0)
{
auto result = dist::comparison_party<role::p0, InteriorPRG,
ExteriorPRG, false, true>(net, x0, spec, tags...);
std::forward<Fn0>(on0)(result.dpf_key);
return result.opened_point;
}
auto result = dist::comparison_party<role::p1, InteriorPRG,
ExteriorPRG, false, true>(net, x1, spec, tags...);
std::forward<Fn1>(on1)(result.dpf_key);
return result.opened_point;
}
else
{
if (self == role::p0)
{
std::forward<Fn0>(on0)(dist::comparison_party<role::p0, InteriorPRG,
ExteriorPRG, false, false>(net, x0, spec, tags...));
}
else
{
std::forward<Fn1>(on1)(dist::comparison_party<role::p1, InteriorPRG,
ExteriorPRG, false, false>(net, x1, spec, tags...));
}
return {};
}
}
/// @brief Two-party interval keygen.
template <typename InteriorPRG = dpf::prg::aes128,
typename ExteriorPRG = InteriorPRG,
bool Reveal = false,
typename InputT,
typename Beta,
typename Fn0,
typename Fn1>
auto dist_with_ic_key_iknp(trio & net, role self, InputT r0, InputT r1,
const ic_pack<Beta> & spec, Fn0 && on0, Fn1 && on1)
-> std::conditional_t<Reveal, std::optional<std::decay_t<InputT>>,
suppressed_point>
{
using input_type = std::decay_t<InputT>;
using out_beta = concrete_type_t<Beta>;
detail::ic_impl::check_input<input_type>();
detail::ic_impl::check_bounds<input_type>(spec);
if constexpr (detail::cmp_group_info<Beta>::custom)
{
throw std::invalid_argument(
"dist interval keygen currently supports <=64-bit ring payloads");
if constexpr (Reveal)
return std::nullopt;
else
return {};
}
else
{
if (self == role::p2)
throw std::invalid_argument("iknp interval keygen has no dealer");
const auto inner_spec = detail::ic_impl::inner_lt(spec);
const std::uint64_t gmask = detail::ic_impl::group_mask_of<Beta>();
const std::uint64_t nmask = detail::ic_impl::input_mask_of<input_type>();
iknp_deal::prepare_ic<InteriorPRG, ExteriorPRG, input_type, Reveal>(
net, self, spec);
const input_type mine = self == role::p0 ? r0 : r1;
std::uint64_t delta = 0;
std::uint64_t fval = 0;
if constexpr (!is_wildcard_v<Beta>)
{
delta = detail::dcf_impl::beta_delta_u64(
spec.if_true, spec.if_false, gmask);
fval = detail::dcf_impl::beta_to_u64_simple(spec.if_false, gmask);
}
if constexpr (Reveal)
{
const role peer = self == role::p0 ? role::p1 : role::p0;
const input_type theirs =
net.exchange_with(peer, mine, net::msg::delta);
const input_type r = utils::xor_input_shares(mine, theirs);
const std::uint64_t r_bits = detail::ic_impl::bits_of(r);
input_type gamma = detail::ic_impl::gamma_of(r);
utils::flip_msb_if_signed_integral(gamma);
const std::uint64_t cr = detail::ic_impl::correction(
r_bits, spec.lo, spec.hi, nmask, gmask);
const std::uint64_t absorb =
(detail::ic_impl::mul_mask(delta, cr, gmask) + fval) & gmask;
const auto recv_side_shares = [&]() {
const auto z0 =
net.recv_from<dist::ring_zero_share_msg>(role::p2, net::msg::beaver_tape);
const auto z1 =
net.recv_from<dist::ring_zero_share_msg>(role::p2, net::msg::beaver_tape);
const std::uint64_t side = self == role::p1 ? 1ULL : 0ULL;
const std::uint64_t dshare = is_wildcard_v<Beta>
? 0ULL : (z0.share + side * delta) & gmask;
const std::uint64_t cshare = is_wildcard_v<Beta>
? 0ULL : (z1.share + side * absorb) & gmask;
const std::uint64_t dcoeff = is_wildcard_v<Beta>
? (z0.share + side) & gmask : 0ULL;
const std::uint64_t ccoeff = is_wildcard_v<Beta>
? (z1.share + side * cr) & gmask : 0ULL;
return std::array<std::uint64_t, 4>{
dshare, cshare, dcoeff, ccoeff};
};
if (self == role::p0)
{
auto inner = dist::comparison_party<role::p0, InteriorPRG,
ExteriorPRG, true>(net, gamma, inner_spec);
const auto shares = recv_side_shares();
using raw_key = typename decltype(inner)::key_type;
auto key = detail::ic_impl::make_side<0, raw_key, input_type,
out_beta>(std::move(inner), spec.lo, spec.hi, nmask, gmask,
shares[0], shares[1], shares[2], shares[3]);
on0(std::move(key));
}
else
{
auto inner = dist::comparison_party<role::p1, InteriorPRG,
ExteriorPRG, true>(net, gamma, inner_spec);
const auto shares = recv_side_shares();
using raw_key = typename decltype(inner)::key_type;
auto key = detail::ic_impl::make_side<1, raw_key, input_type,
out_beta>(std::move(inner), spec.lo, spec.hi, nmask, gmask,
shares[0], shares[1], shares[2], shares[3]);
on1(std::move(key));
}
return r;
}
else
{
input_type gamma_share{};
std::uint64_t cr_share = 0;
if (self == role::p0)
{
auto got = dist::ic_gamma_and_correction<0, input_type>(net,
mine, spec.lo, spec.hi, nmask, gmask);
gamma_share = got.first;
cr_share = got.second;
}
else
{
auto got = dist::ic_gamma_and_correction<1, input_type>(net,
mine, spec.lo, spec.hi, nmask, gmask);
gamma_share = got.first;
cr_share = got.second;
}
const std::uint64_t absorb_share = is_wildcard_v<Beta>
? 0ULL
: (detail::ic_impl::mul_mask(delta, cr_share, gmask)
+ (self == role::p1 ? fval : 0ULL)) & gmask;
const std::uint64_t cr_coeff_share =
is_wildcard_v<Beta> ? cr_share : 0ULL;
const auto recv_delta = [&]() {
const auto z0 =
net.recv_from<dist::ring_zero_share_msg>(role::p2, net::msg::beaver_tape);
const std::uint64_t side = self == role::p1 ? 1ULL : 0ULL;
const std::uint64_t dshare = is_wildcard_v<Beta>
? 0ULL : (z0.share + side * delta) & gmask;
const std::uint64_t dcoeff = is_wildcard_v<Beta>
? (z0.share + side) & gmask : 0ULL;
return std::array<std::uint64_t, 2>{dshare, dcoeff};
};
if (self == role::p0)
{
auto inner = dist::comparison_party<role::p0, InteriorPRG,
ExteriorPRG, false, false>(net, gamma_share, inner_spec);
const auto d = recv_delta();
using raw_key = typename decltype(inner)::key_type;
auto key = detail::ic_impl::make_side<0, raw_key, input_type,
out_beta>(std::move(inner), spec.lo, spec.hi, nmask, gmask,
d[0], absorb_share, d[1], cr_coeff_share);
on0(std::move(key));
}
else
{
auto inner = dist::comparison_party<role::p1, InteriorPRG,
ExteriorPRG, false, false>(net, gamma_share, inner_spec);
const auto d = recv_delta();
using raw_key = typename decltype(inner)::key_type;
auto key = detail::ic_impl::make_side<1, raw_key, input_type,
out_beta>(std::move(inner), spec.lo, spec.hi, nmask, gmask,
d[0], absorb_share, d[1], cr_coeff_share);
on1(std::move(key));
}
return {};
}
}
}
} // namespace party
} // namespace dpf
#endif // LIBDPF_PARTY_IKNP_DEAL_HPP__

70
party/key_io.hpp Normal file
View file

@ -0,0 +1,70 @@
/// @file party/key_io.hpp
/// @brief Ship a copyable DPF key as a framed byte blob.
#ifndef LIBDPF_PARTY_KEY_IO_HPP__
#define LIBDPF_PARTY_KEY_IO_HPP__
#include <cstdint>
#include <cstring>
#include <new>
#include <stdexcept>
#include <utility>
#include <vector>
#include "dpf/net/channel.hpp"
#include "dpf/net/trio.hpp"
namespace dpf
{
namespace party
{
template <typename Key>
void send_key(net::channel & c, const Key & key)
{
Key copy(key);
c.send_bytes(net::msg::dpf_key,
reinterpret_cast<const std::uint8_t *>(&copy), sizeof(Key));
}
template <typename Key>
Key recv_key(net::channel & c)
{
auto body = c.recv_bytes(net::msg::dpf_key);
if (body.size() != sizeof(Key))
throw std::runtime_error("recv_key size mismatch");
alignas(Key) unsigned char raw[sizeof(Key)];
std::memcpy(raw, body.data(), sizeof(Key));
Key * p = std::launder(reinterpret_cast<Key *>(raw));
Key out(std::move(*p));
p->~Key();
return out;
}
/// @brief Send a key blob through the trio hook (or mesh when unset).
template <typename Key>
void send_key(net::trio & net, net::role peer, const Key & key)
{
Key copy(key);
net.send_bytes_to(peer, net::msg::dpf_key,
reinterpret_cast<const std::uint8_t *>(&copy), sizeof(Key));
}
/// @brief Receive a key blob through the trio hook (or mesh when unset).
template <typename Key>
Key recv_key(net::trio & net, net::role peer)
{
auto body = net.recv_bytes_from(peer, net::msg::dpf_key);
if (body.size() != sizeof(Key))
throw std::runtime_error("recv_key size mismatch");
alignas(Key) unsigned char raw[sizeof(Key)];
std::memcpy(raw, body.data(), sizeof(Key));
Key * p = std::launder(reinterpret_cast<Key *>(raw));
Key out(std::move(*p));
p->~Key();
return out;
}
} // namespace party
} // namespace dpf
#endif // LIBDPF_PARTY_KEY_IO_HPP__

633
party/oblivious_hash.hpp Normal file
View file

@ -0,0 +1,633 @@
/// @file party/oblivious_hash.hpp
/// @brief Correction-seed hash that keeps the path prefix shared.
/// @details Matches `detail::vdpf::hash_node` without either party learning
/// the prefix. The opened value is only `H(s0) XOR H(s1)`.
/// SubBytes uses the Boyar–Peralta AES S-box (ePrint 2011/332;
/// Yale CMT SLP with 32 ANDs). Bit×bit ANDs use bit Beaver triples
/// and are scheduled by multiplicative depth (six exchanges per
/// SubBytes), so one level costs `8×16×10×32 = 40960` bit-ANDs.
#ifndef LIBDPF_PARTY_OBLIVIOUS_HASH_HPP__
#define LIBDPF_PARTY_OBLIVIOUS_HASH_HPP__
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <stdexcept>
#include <vector>
#include "simde/simde/x86/avx2.h"
#include "aes_mmo_ref.hpp"
#include "aes_sbox_bp.hpp"
#include "dpf/doerner_shelat.hpp"
#include "dpf/prg_aes.hpp"
#include "dpf/verifiable.hpp"
#include "dpf/net/round_sink.hpp"
#include "dpf/net/sink_exchange.hpp"
/* aes_bp is file-scope */
/// @brief AND-triples consumed by one level of `oblivious_cs`.
inline constexpr std::size_t hash_level_and_count() noexcept
{
// Eight AES (two seeds × four MMO lanes) × 16 bytes × 10 SubBytes
// × 32 Boyar–Peralta ANDs.
return 8u * 16u * 10u * aes_bp::and_count;
}
inline std::vector<bit_and_pad_msg> sample_and_tape(std::size_t n, int party)
{
::dpf::detail::urandom_pad_rng pads;
std::vector<bit_and_pad_msg> out(n);
for (std::size_t i = 0; i < n; ++i)
{
const auto full = ::dpf::detail::ds_sample_bit_and(pads);
out[i] = bit_pad_for(full, party);
}
return out;
}
inline std::vector<std::uint8_t> pack_mask_bits(
const std::uint8_t * d, const std::uint8_t * e, std::size_t n)
{
std::vector<std::uint8_t> out((2u * n + 7u) / 8u);
for (std::size_t i = 0; i < n; ++i)
{
const std::size_t db = 2u * i;
const std::size_t eb = db + 1u;
if (d[i] & 1u)
out[db / 8u] = static_cast<std::uint8_t>(
out[db / 8u] | static_cast<std::uint8_t>(1u << (db % 8u)));
if (e[i] & 1u)
out[eb / 8u] = static_cast<std::uint8_t>(
out[eb / 8u] | static_cast<std::uint8_t>(1u << (eb % 8u)));
}
return out;
}
inline void unpack_mask_bits(const std::vector<std::uint8_t> & packed,
std::uint8_t * d, std::uint8_t * e, std::size_t n)
{
if (packed.size() != (2u * n + 7u) / 8u)
throw std::runtime_error("oblivious AND packed size");
for (std::size_t i = 0; i < n; ++i)
{
const std::size_t db = 2u * i;
const std::size_t eb = db + 1u;
d[i] = static_cast<std::uint8_t>(
(packed[db / 8u] >> (db % 8u)) & 1u);
e[i] = static_cast<std::uint8_t>(
(packed[eb / 8u] >> (eb % 8u)) & 1u);
}
}
/// @brief One exchange of packed `(x⊕α, y⊕β)` masks for `n` bit-ANDs.
template <int Me>
std::vector<std::uint8_t> batch_bit_and(net::trio & net,
const std::uint8_t * x, const std::uint8_t * y, std::size_t n,
const bit_and_pad_msg * triples)
{
const role peer = Me == 0 ? role::p1 : role::p0;
std::vector<std::uint8_t> d(n), e(n);
for (std::size_t i = 0; i < n; ++i)
{
d[i] = static_cast<std::uint8_t>(x[i] ^ triples[i].a);
e[i] = static_cast<std::uint8_t>(y[i] ^ triples[i].b);
}
const auto mine = pack_mask_bits(d.data(), e.data(), n);
std::vector<std::uint8_t> peer_bytes;
if constexpr (Me == 0)
{
net.send_bytes_to(peer, net::msg::delta, mine.data(), mine.size());
peer_bytes = net.recv_bytes_from(peer, net::msg::delta);
}
else
{
peer_bytes = net.recv_bytes_from(peer, net::msg::delta);
net.send_bytes_to(peer, net::msg::delta, mine.data(), mine.size());
}
std::vector<std::uint8_t> pd(n), pe(n);
unpack_mask_bits(peer_bytes, pd.data(), pe.data(), n);
// Standard Beaver: z = [c] ⊕ (d∧[b]) ⊕ ([a]∧e) ⊕ (d∧e), and the public
// `d∧e` term is added on one party only. Adding it on both cancels under
// XOR and the S-box (and every correction seed) diverges from `hash_node`.
std::vector<std::uint8_t> shares(n);
for (std::size_t i = 0; i < n; ++i)
{
const std::uint8_t od = static_cast<std::uint8_t>(d[i] ^ pd[i]);
const std::uint8_t oe = static_cast<std::uint8_t>(e[i] ^ pe[i]);
shares[i] = detail::ds_bit_and_party(
od, oe, triples[i].a, triples[i].b, triples[i].c, Me == 0);
}
return shares;
}
/// @brief Packed bit-AND layer on a RoundSink. Slot must fit `4 + packed`.
template <int Me>
std::vector<std::uint8_t> batch_bit_and_on_sink(dpf::net::RoundSink & sink,
std::uint16_t & round, std::size_t index, const std::uint8_t * x,
const std::uint8_t * y, std::size_t n, const bit_and_pad_msg * triples)
{
std::vector<std::uint8_t> d(n), e(n);
for (std::size_t i = 0; i < n; ++i)
{
d[i] = static_cast<std::uint8_t>(x[i] ^ triples[i].a);
e[i] = static_cast<std::uint8_t>(y[i] ^ triples[i].b);
}
const auto mine = pack_mask_bits(d.data(), e.data(), n);
if (round >= sink.rounds())
throw std::runtime_error("batch_bit_and_on_sink: out of rounds");
const std::size_t slot = sink.slot_bytes(round);
const std::size_t need = sizeof(std::uint32_t) + mine.size();
if (need > slot)
throw std::runtime_error("batch_bit_and_on_sink: slot too small");
std::vector<std::uint8_t> buf(slot, 0);
const std::uint32_t nbytes = static_cast<std::uint32_t>(mine.size());
std::memcpy(buf.data(), &nbytes, 4);
if (!mine.empty())
std::memcpy(buf.data() + 4, mine.data(), mine.size());
sink.submit(round, index, buf.data(), buf.size());
sink.flush_round(round);
sink.poll();
if (!sink.peer_ready(round, index))
throw std::runtime_error("batch_bit_and_on_sink: peer missing");
std::vector<std::uint8_t> peer_buf(slot);
sink.read_peer(round, index, peer_buf.data(), slot);
std::uint32_t pn = 0;
std::memcpy(&pn, peer_buf.data(), 4);
if (pn != nbytes)
throw std::runtime_error("batch_bit_and_on_sink: peer size");
std::vector<std::uint8_t> peer_bytes(pn);
if (pn != 0)
std::memcpy(peer_bytes.data(), peer_buf.data() + 4, pn);
++round;
std::vector<std::uint8_t> pd(n), pe(n);
unpack_mask_bits(peer_bytes, pd.data(), pe.data(), n);
std::vector<std::uint8_t> shares(n);
for (std::size_t i = 0; i < n; ++i)
{
const std::uint8_t od = static_cast<std::uint8_t>(d[i] ^ pd[i]);
const std::uint8_t oe = static_cast<std::uint8_t>(e[i] ^ pe[i]);
shares[i] = detail::ds_bit_and_party(
od, oe, triples[i].a, triples[i].b, triples[i].c, Me == 0);
}
return shares;
}
/// @brief Shared SubBytes via Boyar–Peralta (32 ANDs per byte, depth-batched).
template <int Me>
void sub_bytes_shared(net::trio & net, std::uint8_t * state, std::size_t bytes,
const bit_and_pad_msg * & tape)
{
std::vector<std::uint8_t> wire(bytes * aes_bp::wire_count);
auto at = [&](std::size_t b, std::size_t w) -> std::uint8_t & {
return wire[b * aes_bp::wire_count + w];
};
for (std::size_t b = 0; b < bytes; ++b)
{
for (int i = 0; i < 8; ++i)
at(b, static_cast<std::size_t>(i)) =
static_cast<std::uint8_t>((state[b] >> (7 - i)) & 1u);
}
std::array<bool, aes_bp::wire_count> ready{};
for (std::size_t i = 0; i < 8; ++i)
ready[i] = true;
std::array<bool, aes_bp::op_count> done{};
std::size_t finished = 0;
while (finished < aes_bp::op_count)
{
bool progress = true;
while (progress)
{
progress = false;
for (std::size_t oi = 0; oi < aes_bp::op_count; ++oi)
{
if (done[oi])
continue;
const auto kind = aes_bp::ops[oi][0];
if (kind == 1)
continue;
const auto dst = aes_bp::ops[oi][1];
const auto a = aes_bp::ops[oi][2];
const auto bb = aes_bp::ops[oi][3];
if (!ready[a] || !ready[bb])
continue;
for (std::size_t b = 0; b < bytes; ++b)
{
auto v = static_cast<std::uint8_t>(at(b, a) ^ at(b, bb));
if (kind == 2)
{
if constexpr (Me == 0)
v = static_cast<std::uint8_t>(v ^ 1u);
}
at(b, dst) = v;
}
ready[dst] = true;
done[oi] = true;
++finished;
progress = true;
}
}
std::vector<std::size_t> ands;
ands.reserve(aes_bp::and_count);
for (std::size_t oi = 0; oi < aes_bp::op_count; ++oi)
{
if (done[oi] || aes_bp::ops[oi][0] != 1)
continue;
const auto a = aes_bp::ops[oi][2];
const auto bb = aes_bp::ops[oi][3];
if (ready[a] && ready[bb])
ands.push_back(oi);
}
if (ands.empty())
{
if (finished != aes_bp::op_count)
throw std::runtime_error("oblivious SubBytes stuck");
break;
}
const std::size_t n = ands.size() * bytes;
std::vector<std::uint8_t> xs(n), ys(n);
for (std::size_t ai = 0; ai < ands.size(); ++ai)
{
const auto a = aes_bp::ops[ands[ai]][2];
const auto bb = aes_bp::ops[ands[ai]][3];
for (std::size_t b = 0; b < bytes; ++b)
{
xs[ai * bytes + b] = at(b, a);
ys[ai * bytes + b] = at(b, bb);
}
}
const auto prod = batch_bit_and<Me>(net, xs.data(), ys.data(), n, tape);
tape += n;
for (std::size_t ai = 0; ai < ands.size(); ++ai)
{
const auto dst = aes_bp::ops[ands[ai]][1];
for (std::size_t b = 0; b < bytes; ++b)
at(b, dst) = static_cast<std::uint8_t>(prod[ai * bytes + b] & 1u);
ready[dst] = true;
done[ands[ai]] = true;
++finished;
}
}
for (std::size_t b = 0; b < bytes; ++b)
{
std::uint8_t out = 0;
for (int i = 0; i < 8; ++i)
out = static_cast<std::uint8_t>(
out | (at(b, aes_bp::out_wire[static_cast<std::size_t>(i)])
<< (7 - i)));
state[b] = out;
}
}
/// @brief SubBytes with AND layers on a RoundSink (multi-instance friendly).
template <int Me>
void sub_bytes_shared_on_sink(dpf::net::RoundSink & sink, std::uint16_t & round,
std::size_t index, std::uint8_t * state, std::size_t bytes,
const bit_and_pad_msg * & tape)
{
std::vector<std::uint8_t> wire(bytes * aes_bp::wire_count);
auto at = [&](std::size_t b, std::size_t w) -> std::uint8_t & {
return wire[b * aes_bp::wire_count + w];
};
for (std::size_t b = 0; b < bytes; ++b)
{
for (int i = 0; i < 8; ++i)
at(b, static_cast<std::size_t>(i)) =
static_cast<std::uint8_t>((state[b] >> (7 - i)) & 1u);
}
std::array<bool, aes_bp::wire_count> ready{};
for (std::size_t i = 0; i < 8; ++i)
ready[i] = true;
std::array<bool, aes_bp::op_count> done{};
std::size_t finished = 0;
while (finished < aes_bp::op_count)
{
bool progress = true;
while (progress)
{
progress = false;
for (std::size_t oi = 0; oi < aes_bp::op_count; ++oi)
{
if (done[oi])
continue;
const auto kind = aes_bp::ops[oi][0];
if (kind == 1)
continue;
const auto dst = aes_bp::ops[oi][1];
const auto a = aes_bp::ops[oi][2];
const auto bb = aes_bp::ops[oi][3];
if (!ready[a] || !ready[bb])
continue;
for (std::size_t b = 0; b < bytes; ++b)
{
auto v = static_cast<std::uint8_t>(at(b, a) ^ at(b, bb));
if (kind == 2)
{
if constexpr (Me == 0)
v = static_cast<std::uint8_t>(v ^ 1u);
}
at(b, dst) = v;
}
ready[dst] = true;
done[oi] = true;
++finished;
progress = true;
}
}
std::vector<std::size_t> ands;
ands.reserve(aes_bp::and_count);
for (std::size_t oi = 0; oi < aes_bp::op_count; ++oi)
{
if (done[oi] || aes_bp::ops[oi][0] != 1)
continue;
const auto a = aes_bp::ops[oi][2];
const auto bb = aes_bp::ops[oi][3];
if (ready[a] && ready[bb])
ands.push_back(oi);
}
if (ands.empty())
{
if (finished != aes_bp::op_count)
throw std::runtime_error("oblivious SubBytes stuck");
break;
}
const std::size_t n = ands.size() * bytes;
std::vector<std::uint8_t> xs(n), ys(n);
for (std::size_t ai = 0; ai < ands.size(); ++ai)
{
const auto a = aes_bp::ops[ands[ai]][2];
const auto bb = aes_bp::ops[ands[ai]][3];
for (std::size_t b = 0; b < bytes; ++b)
{
xs[ai * bytes + b] = at(b, a);
ys[ai * bytes + b] = at(b, bb);
}
}
const auto prod = batch_bit_and_on_sink<Me>(
sink, round, index, xs.data(), ys.data(), n, tape);
tape += n;
for (std::size_t ai = 0; ai < ands.size(); ++ai)
{
const auto dst = aes_bp::ops[ands[ai]][1];
for (std::size_t b = 0; b < bytes; ++b)
at(b, dst) = static_cast<std::uint8_t>(prod[ai * bytes + b] & 1u);
ready[dst] = true;
done[ands[ai]] = true;
++finished;
}
}
for (std::size_t b = 0; b < bytes; ++b)
{
std::uint8_t out = 0;
for (int i = 0; i < 8; ++i)
out = static_cast<std::uint8_t>(
out | (at(b, aes_bp::out_wire[static_cast<std::size_t>(i)])
<< (7 - i)));
state[b] = out;
}
}
inline void shift_rows_bytes(std::uint8_t s[16])
{
::dpf::party::aes_ref::shift_rows(s);
}
inline void mix_columns_bytes(std::uint8_t s[16])
{
::dpf::party::aes_ref::mix_columns(s);
}
inline void xor_bytes(std::uint8_t * dst, const std::uint8_t * src, std::size_t n)
{
for (std::size_t i = 0; i < n; ++i)
dst[i] = static_cast<std::uint8_t>(dst[i] ^ src[i]);
}
inline std::array<std::uint8_t, 16> m128_bytes(simde__m128i v)
{
std::array<std::uint8_t, 16> b{};
std::memcpy(b.data(), &v, 16);
return b;
}
/// @brief Public correction seed for one level. Prefix shares stay local.
template <int Me>
cs_block oblivious_cs(net::trio & net, std::size_t level,
std::uint64_t prefix_share, simde__m128i my_seed,
const bit_and_pad_msg * tape)
{
const auto schedule = prg::aes128_key(simde_mm_setzero_si128());
const auto rk = [&](int r) {
return m128_bytes(schedule.rd_key[static_cast<std::size_t>(r)]);
};
constexpr std::size_t blocks = 8;
std::uint8_t state[blocks][16]{};
std::uint8_t feed[blocks][16]{};
auto place_prefix = [&](std::uint8_t * dst) {
for (int i = 0; i < 8; ++i)
dst[i] = static_cast<std::uint8_t>(
dst[i] ^ static_cast<std::uint8_t>(prefix_share >> (8 * i)));
};
for (int owner = 0; owner < 2; ++owner)
{
const bool mine = (owner == Me);
for (int pos = 0; pos < 4; ++pos)
{
const std::size_t bi = static_cast<std::size_t>(owner * 4 + pos);
if (mine)
{
auto bytes = m128_bytes(my_seed);
const std::uint64_t tag = 0x5600ull | (level & 0xffffu);
for (int i = 0; i < 8; ++i)
bytes[8 + i] = static_cast<std::uint8_t>(
bytes[8 + i]
^ static_cast<std::uint8_t>(tag >> (8 * i)));
place_prefix(bytes.data());
std::memcpy(feed[bi], bytes.data(), 16);
std::memcpy(state[bi], bytes.data(), 16);
}
else
{
std::uint8_t bytes[16]{};
place_prefix(bytes);
std::memcpy(feed[bi], bytes, 16);
std::memcpy(state[bi], bytes, 16);
}
if constexpr (Me == 0)
{
auto key0 = rk(0);
key0[0] = static_cast<std::uint8_t>(
key0[0] ^ static_cast<std::uint8_t>(pos));
key0[1] = static_cast<std::uint8_t>(
key0[1]
^ static_cast<std::uint8_t>(static_cast<unsigned>(pos) >> 8));
key0[2] = static_cast<std::uint8_t>(
key0[2]
^ static_cast<std::uint8_t>(
static_cast<unsigned>(pos) >> 16));
key0[3] = static_cast<std::uint8_t>(
key0[3]
^ static_cast<std::uint8_t>(
static_cast<unsigned>(pos) >> 24));
xor_bytes(state[bi], key0.data(), 16);
}
}
}
auto all_bytes = [&]() -> std::uint8_t * { return &state[0][0]; };
const bit_and_pad_msg * cursor = tape;
for (int round = 1; round <= 10; ++round)
{
sub_bytes_shared<Me>(net, all_bytes(), blocks * 16, cursor);
for (std::size_t b = 0; b < blocks; ++b)
shift_rows_bytes(state[b]);
if (round < 10)
{
for (std::size_t b = 0; b < blocks; ++b)
mix_columns_bytes(state[b]);
}
if constexpr (Me == 0)
{
const auto key = rk(round);
for (std::size_t b = 0; b < blocks; ++b)
xor_bytes(state[b], key.data(), 16);
}
}
if (cursor != tape + hash_level_and_count())
throw std::runtime_error("oblivious hash consumed the wrong AND count");
for (std::size_t b = 0; b < blocks; ++b)
xor_bytes(state[b], feed[b], 16);
cs_block mine{};
for (int pos = 0; pos < 4; ++pos)
{
std::uint8_t mixed[16]{};
xor_bytes(mixed, state[static_cast<std::size_t>(pos)], 16);
xor_bytes(mixed, state[static_cast<std::size_t>(4 + pos)], 16);
std::memcpy(&mine[static_cast<std::size_t>(pos)], mixed, 16);
}
return open_cs<Me>(net, mine);
}
/// @brief `oblivious_cs` with AND layers on a RoundSink; final open stays on trio.
template <int Me>
cs_block oblivious_cs_on_sink(net::trio & net, dpf::net::RoundSink & sink,
std::uint16_t & round, std::size_t index, std::size_t level,
std::uint64_t prefix_share, simde__m128i my_seed,
const bit_and_pad_msg * tape)
{
const auto schedule = prg::aes128_key(simde_mm_setzero_si128());
const auto rk = [&](int r) {
return m128_bytes(schedule.rd_key[static_cast<std::size_t>(r)]);
};
constexpr std::size_t blocks = 8;
std::uint8_t state[blocks][16]{};
std::uint8_t feed[blocks][16]{};
auto place_prefix = [&](std::uint8_t * dst) {
for (int i = 0; i < 8; ++i)
dst[i] = static_cast<std::uint8_t>(
dst[i] ^ static_cast<std::uint8_t>(prefix_share >> (8 * i)));
};
for (int owner = 0; owner < 2; ++owner)
{
const bool mine = (owner == Me);
for (int pos = 0; pos < 4; ++pos)
{
const std::size_t bi = static_cast<std::size_t>(owner * 4 + pos);
if (mine)
{
auto bytes = m128_bytes(my_seed);
const std::uint64_t tag = 0x5600ull | (level & 0xffffu);
for (int i = 0; i < 8; ++i)
bytes[8 + i] = static_cast<std::uint8_t>(
bytes[8 + i]
^ static_cast<std::uint8_t>(tag >> (8 * i)));
place_prefix(bytes.data());
std::memcpy(feed[bi], bytes.data(), 16);
std::memcpy(state[bi], bytes.data(), 16);
}
else
{
std::uint8_t bytes[16]{};
place_prefix(bytes);
std::memcpy(feed[bi], bytes, 16);
std::memcpy(state[bi], bytes, 16);
}
if constexpr (Me == 0)
{
auto key0 = rk(0);
key0[0] = static_cast<std::uint8_t>(
key0[0] ^ static_cast<std::uint8_t>(pos));
key0[1] = static_cast<std::uint8_t>(
key0[1]
^ static_cast<std::uint8_t>(static_cast<unsigned>(pos) >> 8));
key0[2] = static_cast<std::uint8_t>(
key0[2]
^ static_cast<std::uint8_t>(
static_cast<unsigned>(pos) >> 16));
key0[3] = static_cast<std::uint8_t>(
key0[3]
^ static_cast<std::uint8_t>(
static_cast<unsigned>(pos) >> 24));
xor_bytes(state[bi], key0.data(), 16);
}
}
}
auto all_bytes = [&]() -> std::uint8_t * { return &state[0][0]; };
const bit_and_pad_msg * cursor = tape;
for (int aes_round = 1; aes_round <= 10; ++aes_round)
{
sub_bytes_shared_on_sink<Me>(
sink, round, index, all_bytes(), blocks * 16, cursor);
for (std::size_t b = 0; b < blocks; ++b)
shift_rows_bytes(state[b]);
if (aes_round < 10)
{
for (std::size_t b = 0; b < blocks; ++b)
mix_columns_bytes(state[b]);
}
if constexpr (Me == 0)
{
const auto key = rk(aes_round);
for (std::size_t b = 0; b < blocks; ++b)
xor_bytes(state[b], key.data(), 16);
}
}
if (cursor != tape + hash_level_and_count())
throw std::runtime_error("oblivious hash consumed the wrong AND count");
for (std::size_t b = 0; b < blocks; ++b)
xor_bytes(state[b], feed[b], 16);
cs_block mine{};
for (int pos = 0; pos < 4; ++pos)
{
std::uint8_t mixed[16]{};
xor_bytes(mixed, state[static_cast<std::size_t>(pos)], 16);
xor_bytes(mixed, state[static_cast<std::size_t>(4 + pos)], 16);
std::memcpy(&mine[static_cast<std::size_t>(pos)], mixed, 16);
}
return open_cs<Me>(net, mine);
}
#endif

296
party/oblivious_select.hpp Normal file
View file

@ -0,0 +1,296 @@
/// @file party/oblivious_select.hpp
/// @brief Shared-output muxes for the socket Doerner–Shelat walk.
/// @details Included from inside `dpf::party::dist`. Products stay shared.
/// The only opened values are public correction words.
#ifndef LIBDPF_PARTY_OBLIVIOUS_SELECT_HPP__
#define LIBDPF_PARTY_OBLIVIOUS_SELECT_HPP__
template <int Me, typename T>
std::vector<T> exch_vec(net::trio & net, const std::vector<T> & mine)
{
const role peer = Me == 0 ? role::p1 : role::p0;
if constexpr (Me == 0)
{
net.to(peer).send_vec(mine, net::msg::delta);
return net.to(peer).template recv_vec<T>(net::msg::delta);
}
else
{
auto got = net.to(peer).template recv_vec<T>(net::msg::delta);
net.to(peer).send_vec(mine, net::msg::delta);
return got;
}
}
/// @brief This party's XOR share of `bit * block`. The product is not opened.
template <typename Exchange>
simde__m128i beaver_xor_share_bit(Exchange && exch, bool i_hold_block,
std::uint8_t bit_share, simde__m128i block, const and_share_msg & mine)
{
and_round1_msg msg{};
msg.d_bit = static_cast<std::uint8_t>(bit_share ^ mine.a);
msg.b_bit = mine.b;
msg.a_m = mine.a;
msg.e_m = i_hold_block ? detail::ds_xor(block, mine.b) : mine.b;
const and_round1_msg peer = exch(msg);
const std::uint8_t d = static_cast<std::uint8_t>(msg.d_bit ^ peer.d_bit);
const simde__m128i e = i_hold_block
? detail::ds_xor(msg.e_m, peer.b_bit)
: detail::ds_xor(peer.e_m, mine.b);
simde__m128i z = detail::ds_xor(
detail::ds_xor(detail::ds_gate(d, mine.b), detail::ds_gate(mine.a, e)),
mine.c);
if (i_hold_block)
z = detail::ds_xor(z, detail::ds_gate(d, e));
return z;
}
/// @brief Additive leaf share of a XOR-shared block. Limb width matches `leaf_add`.
template <int Me>
simde__m128i b2a_leaf(net::trio & net, simde__m128i xor_share,
const b2a_pad_msg * pads, std::size_t limb_bytes)
{
std::uint8_t bytes[16]{};
std::memcpy(bytes, &xor_share, 16);
std::vector<std::uint8_t> masked(128);
for (int i = 0; i < 128; ++i)
{
const std::uint8_t bit = static_cast<std::uint8_t>(
(bytes[i / 8] >> (i % 8)) & 1u);
masked[static_cast<std::size_t>(i)] = static_cast<std::uint8_t>(
bit ^ pads[i].r);
}
const auto peer = exch_vec<Me>(net, masked);
const std::size_t limb_bits = limb_bytes * 8;
const std::size_t nlimbs = 16 / limb_bytes;
const std::uint64_t limb_mask = limb_bits == 64
? ~std::uint64_t{0}
: ((std::uint64_t{1} << limb_bits) - 1);
std::uint64_t acc[16]{};
for (int i = 0; i < 128; ++i)
{
const std::uint8_t c = static_cast<std::uint8_t>(
masked[static_cast<std::size_t>(i)] ^ peer[static_cast<std::size_t>(i)]);
std::uint64_t s = pads[i].add;
if (c != 0)
s = Me == 0 ? (std::uint64_t{1} - pads[i].add)
: (std::uint64_t{0} - pads[i].add);
const std::size_t limb = static_cast<std::size_t>(i) / limb_bits;
const unsigned shift = static_cast<unsigned>(
static_cast<std::size_t>(i) % limb_bits);
acc[limb] += s << shift;
}
std::uint8_t outb[16]{};
for (std::size_t k = 0; k < nlimbs; ++k)
{
const std::uint64_t v = acc[k] & limb_mask;
std::memcpy(outb + k * limb_bytes, &v, limb_bytes);
}
simde__m128i out{};
std::memcpy(&out, outb, 16);
return out;
}
template <int Me, typename Concrete, typename Leaf>
Leaf open_leaf_sum(net::trio & net, Leaf mine)
{
const role peer = Me == 0 ? role::p1 : role::p0;
const Leaf theirs = net.exchange_with(peer, mine, net::msg::ring_vector);
return leaf_add<Concrete>(mine, theirs);
}
/// @brief Public leaf CW from a shared naked leaf and this party's mask.
template <int Me, typename Concrete, typename Leaf>
Leaf cw_from_selected(net::trio & net, Leaf selected, Leaf mask, bool sign)
{
constexpr bool char2 = utils::has_characteristic_two_v<Concrete>;
Leaf share{};
if constexpr (char2)
{
share = leaf_add<Concrete>(selected, mask);
}
else if (sign)
{
if constexpr (Me == 0)
share = leaf_add<Concrete>(selected, mask);
else
share = dpf::subtract_leaf<Concrete>(selected, mask);
}
else if constexpr (Me == 0)
{
const Leaf neg = dpf::subtract_leaf<Concrete>(Leaf{}, selected);
share = dpf::subtract_leaf<Concrete>(neg, mask);
}
else
{
share = dpf::subtract_leaf<Concrete>(mask, selected);
}
return open_leaf_sum<Me, Concrete>(net, share);
}
/// @brief Share of the leaf selected by the low `lg` bits of the target.
/// @details Party 0's `make(i)` is the candidate. Party 1's is zero. Each mux
/// multiplies the selector by both parties' deltas and keeps the sum
/// shared, so neither party sees the lane.
template <int Me, typename Concrete, typename Leaf, typename Exchange,
typename Make, typename BitAt>
Leaf mux_leaf_share(net::trio & net, Exchange && exch, std::size_t lg,
Make && make, BitAt && bit_at, const and_share_msg * ands,
const b2a_pad_msg * b2a)
{
static_assert(sizeof(Leaf) == sizeof(simde__m128i),
"shared leaf mux covers a single AES block");
constexpr bool char2 = utils::has_characteristic_two_v<Concrete>;
constexpr std::size_t limb = [] {
constexpr std::size_t s = sizeof(Concrete);
return (s == 1 || s == 2 || s == 4 || s == 8) ? s : std::size_t{8};
}();
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));
std::size_t pad_i = 0;
std::size_t b2a_i = 0;
for (std::size_t b = 0; b < lg; ++b)
{
const std::uint8_t bit = bit_at(b);
std::vector<Leaf> next(cand.size() / 2);
for (std::size_t k = 0; k < next.size(); ++k)
{
const Leaf lo = cand[2 * k];
const Leaf hi = cand[2 * k + 1];
const Leaf delta = dpf::subtract_leaf<Concrete>(hi, lo);
simde__m128i dblock{};
std::memcpy(&dblock, &delta, sizeof(dblock));
const simde__m128i zero = simde_mm_setzero_si128();
simde__m128i zx{};
simde__m128i zy{};
if constexpr (Me == 0)
{
zx = beaver_xor_share_bit(exch, true, bit, dblock, ands[pad_i++]);
zy = beaver_xor_share_bit(exch, false, bit, zero, ands[pad_i++]);
}
else
{
zx = beaver_xor_share_bit(exch, false, bit, zero, ands[pad_i++]);
zy = beaver_xor_share_bit(exch, true, bit, dblock, ands[pad_i++]);
}
Leaf prod{};
if constexpr (char2)
{
const simde__m128i z = detail::ds_xor(zx, zy);
std::memcpy(&prod, &z, sizeof(prod));
}
else
{
const simde__m128i ax = b2a_leaf<Me>(net, zx, b2a + b2a_i, limb);
b2a_i += 128;
const simde__m128i ay = b2a_leaf<Me>(net, zy, b2a + b2a_i, limb);
b2a_i += 128;
Leaf px{};
Leaf py{};
std::memcpy(&px, &ax, sizeof(px));
std::memcpy(&py, &ay, sizeof(py));
prod = leaf_add<Concrete>(px, py);
}
next[k] = leaf_add<Concrete>(lo, prod);
}
cand.swap(next);
}
return cand[0];
}
template <int Me>
std::uint8_t and_bit_share(net::trio & net, std::uint8_t x, std::uint8_t y,
const bit_and_pad_msg & pad)
{
const role peer = Me == 0 ? role::p1 : role::p0;
const bit_and_mask_msg mine{
static_cast<std::uint8_t>(x ^ pad.a),
static_cast<std::uint8_t>(y ^ pad.b)};
const bit_and_mask_msg got = net.exchange_with(peer, mine, net::msg::delta);
const std::uint8_t d = static_cast<std::uint8_t>(mine.x ^ got.x);
const std::uint8_t e = static_cast<std::uint8_t>(mine.y ^ got.y);
return detail::ds_bit_and_party(d, e, pad.a, pad.b, pad.c, Me == 0);
}
template <int Me>
std::uint64_t b2a_bit(net::trio & net, std::uint8_t bit, const b2a_pad_msg & pad)
{
const role peer = Me == 0 ? role::p1 : role::p0;
const std::uint8_t masked = static_cast<std::uint8_t>(bit ^ pad.r);
const std::uint8_t other = net.exchange_with(peer, masked, net::msg::delta);
const std::uint8_t c = static_cast<std::uint8_t>(masked ^ other);
if (c == 0)
return pad.add;
if constexpr (Me == 0)
return std::uint64_t{1} - pad.add;
else
return std::uint64_t{0} - pad.add;
}
/// @brief Additive share of `bit * word`. `word` is meaningful only for the holder.
template <int Me>
std::uint64_t mul_known_word(net::trio & net, bool i_hold, std::uint64_t word,
std::uint8_t bit, const word_bit_pad * pads)
{
std::vector<bit_and_mask_msg> mine(64);
for (int i = 0; i < 64; ++i)
{
const std::uint8_t wbit = i_hold
? static_cast<std::uint8_t>((word >> i) & 1u) : std::uint8_t{0};
mine[static_cast<std::size_t>(i)] = bit_and_mask_msg{
static_cast<std::uint8_t>(bit ^ pads[i].a),
static_cast<std::uint8_t>(wbit ^ pads[i].b)};
}
const auto peer = exch_vec<Me>(net, mine);
std::vector<std::uint8_t> masked(64);
std::uint8_t prod[64]{};
for (int i = 0; i < 64; ++i)
{
const auto & m = mine[static_cast<std::size_t>(i)];
const auto & g = peer[static_cast<std::size_t>(i)];
const std::uint8_t d = static_cast<std::uint8_t>(m.x ^ g.x);
const std::uint8_t e = static_cast<std::uint8_t>(m.y ^ g.y);
prod[i] = detail::ds_bit_and_party(
d, e, pads[i].a, pads[i].b, pads[i].c, Me == 0);
masked[static_cast<std::size_t>(i)] = static_cast<std::uint8_t>(
prod[i] ^ pads[i].r);
}
const auto opened = exch_vec<Me>(net, masked);
std::uint64_t acc = 0;
for (int i = 0; i < 64; ++i)
{
const std::uint8_t c = static_cast<std::uint8_t>(
masked[static_cast<std::size_t>(i)] ^ opened[static_cast<std::size_t>(i)]);
std::uint64_t s = pads[i].add;
if (c != 0)
s = Me == 0 ? (std::uint64_t{1} - pads[i].add)
: (std::uint64_t{0} - pads[i].add);
acc += s << i;
}
return acc;
}
/// @brief XOR-share of the AND of every bit in `bits`. `n == 0` returns 1 on p0.
template <int Me>
std::uint8_t and_tree(net::trio & net, const std::uint8_t * bits, std::size_t n,
const bit_and_pad_msg * pads)
{
if (n == 0)
return Me == 0 ? std::uint8_t{1} : std::uint8_t{0};
std::uint8_t acc = bits[0];
for (std::size_t i = 1; i < n; ++i)
acc = and_bit_share<Me>(net, acc, bits[i], pads[i - 1]);
return acc;
}
template <int Me>
std::uint8_t open_bit(net::trio & net, std::uint8_t mine)
{
const role peer = Me == 0 ? role::p1 : role::p0;
const std::uint8_t other = net.exchange_with(peer, mine, net::msg::delta);
return static_cast<std::uint8_t>(mine ^ other);
}
#endif

6
party/p0.cpp Normal file
View file

@ -0,0 +1,6 @@
#include "cases.hpp"
int main(int argc, char ** argv)
{
return dpf::party::run(dpf::net::role::p0, argc, argv);
}

6
party/p1.cpp Normal file
View file

@ -0,0 +1,6 @@
#include "cases.hpp"
int main(int argc, char ** argv)
{
return dpf::party::run(dpf::net::role::p1, argc, argv);
}

6
party/p2.cpp Normal file
View file

@ -0,0 +1,6 @@
#include "cases.hpp"
int main(int argc, char ** argv)
{
return dpf::party::run(dpf::net::role::p2, argc, argv);
}

173
party/party_bench.cpp Normal file
View file

@ -0,0 +1,173 @@
/// @file party/party_bench.cpp
/// @brief Time specific (2+1) flows. Examples:
/// party_bench --list
/// party_bench --tag beaver,bench --repeat 20 --warmup 2
/// party_bench --case beaver_dot_n32 --repeat 50 --warmup 5
/// party_bench --tag iknp --repeat 1 --warmup 0
#include "cases.hpp"
#include "registry.hpp"
#include "spawn.hpp"
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>
int main(int argc, char ** argv)
{
dpf::party::register_all_flows();
std::string tag;
std::string case_name;
std::uint64_t repeat = 10;
std::uint64_t warmup = 2;
bool list_only = false;
bool metrics = true;
for (int i = 1; i < argc; ++i)
{
std::string a = argv[i];
auto need = [&](const char * f) {
if (i + 1 >= argc)
{
std::cerr << "missing value for " << f << "\n";
std::exit(2);
}
return std::string(argv[++i]);
};
if (a == "--list")
list_only = true;
else if (a == "--tag")
tag = need("--tag");
else if (a == "--case")
case_name = need("--case");
else if (a == "--repeat")
repeat = std::stoull(need("--repeat"));
else if (a == "--warmup")
warmup = std::stoull(need("--warmup"));
else if (a == "--no-metrics")
metrics = false;
else if (a == "--help" || a == "-h")
{
std::cout
<< "party_bench --list | --case NAME | --tag TAGS\n"
<< " [--repeat N=10] [--warmup W=2] [--no-metrics]\n"
<< "With metrics, each role also reports bytes_sent/recv and\n"
<< "frames_sent/recv for the flow itself (not the repeat barrier).\n"
<< "Tags are AND-matched (comma/space). Common tags:\n"
<< " beaver product dot scale stream horner poly mux\n"
<< " dpf wildcard geneval ds verifiable dcf grotto extreme bench smoke\n"
<< " arith garble yao flute shuffle (word, stack, table, hidden column)\n"
<< " iknp (two-party; spawned as p0/p1 only)\n";
return 0;
}
}
if (list_only)
{
dpf::party::list_flows(std::cout, tag.empty() ? case_name : tag);
return 0;
}
std::vector<const dpf::party::flow *> flows;
if (!case_name.empty())
{
auto * f = dpf::party::find_flow(case_name);
if (!f)
{
std::cerr << "unknown flow: " << case_name << "\n";
return 2;
}
flows.push_back(f);
}
else
{
if (tag.empty())
tag = "bench";
flows = dpf::party::select_flows(tag);
if (flows.empty())
{
std::cerr << "no flows match tag: " << tag << "\n";
return 2;
}
}
dpf::party::spawn_opts opts;
opts.repeat = repeat;
opts.warmup = warmup;
opts.metrics = metrics;
auto pair_flow = [](const dpf::party::flow * f) {
return dpf::party::flow_is_pair(*f);
};
auto flow_of = [](const std::string & line) {
const auto p = line.find("flow=");
if (p == std::string::npos)
return std::string{};
const auto e = line.find(' ', p + 5);
return line.substr(p + 5, e == std::string::npos ? std::string::npos : e - (p + 5));
};
auto avg_ns_of = [](const std::string & line) {
const auto p = line.find("avg_ns=");
if (p == std::string::npos)
return 0.0;
return std::stod(line.substr(p + 7));
};
std::cout << "flow\twall_ms\tp0\tp1\tp2\trepeat\twarmup\n";
int fails = 0;
bool mixed = false;
const bool first_pair = pair_flow(flows.front());
for (const auto * f : flows)
mixed = mixed || (pair_flow(f) != first_pair);
// One mesh for a homogeneous tag. wall_ms on each row is that flow's
// slowest role. The process startup is paid once, as suite_wall_ms.
if (case_name.empty() && !mixed && flows.size() > 1)
{
opts.by_tag = true;
auto r = first_pair ? dpf::party::spawn_pair_flow(tag, opts)
: dpf::party::spawn_trio_flow(tag, opts);
for (const auto * f : flows)
{
double slow = 0;
std::vector<std::string> lines;
for (const auto & line : r.metrics_lines)
{
if (flow_of(line) != f->name)
continue;
lines.push_back(line);
slow = std::max(slow, avg_ns_of(line) / 1.0e6);
}
if (lines.empty())
slow = r.wall_ms;
std::cout << f->name << '\t' << slow << '\t'
<< r.rc[0] << '\t' << r.rc[1] << '\t' << r.rc[2] << '\t'
<< repeat << '\t' << warmup << '\n';
for (const auto & line : lines)
std::cout << " " << line << '\n';
}
std::cout << "suite_wall_ms\t" << r.wall_ms << '\n';
if (r.rc[0] || r.rc[1] || r.rc[2])
++fails;
}
else
{
for (const auto * f : flows)
{
auto r = pair_flow(f) ? dpf::party::spawn_pair_flow(f->name, opts)
: dpf::party::spawn_trio_flow(f->name, opts);
std::cout << f->name << '\t' << r.wall_ms << '\t'
<< r.rc[0] << '\t' << r.rc[1] << '\t' << r.rc[2] << '\t'
<< repeat << '\t' << warmup << '\n';
for (const auto & line : r.metrics_lines)
std::cout << " " << line << '\n';
if (r.rc[0] || r.rc[1] || r.rc[2])
++fails;
}
}
return fails ? 1 : 0;
}

136
party/registry.cpp Normal file
View file

@ -0,0 +1,136 @@
/// @file party/registry.cpp
#include "registry.hpp"
#include <algorithm>
#include <iostream>
#include <sstream>
#include <stdexcept>
namespace dpf
{
namespace party
{
namespace
{
std::vector<flow> & table()
{
static std::vector<flow> t;
return t;
}
std::vector<std::string> split_csv(std::string_view s)
{
std::vector<std::string> out;
std::string cur;
for (char c : s)
{
if (c == ',' || c == ' ')
{
if (!cur.empty())
{
out.push_back(cur);
cur.clear();
}
}
else
cur.push_back(c);
}
if (!cur.empty())
out.push_back(cur);
return out;
}
} // namespace
void register_flow(flow f)
{
if (f.name == nullptr || f.run == nullptr)
throw std::invalid_argument("register_flow: null name or run");
for (const auto & e : table())
{
if (e.name == f.name || std::string_view(e.name) == f.name)
throw std::invalid_argument(std::string("duplicate flow: ") + f.name);
}
table().push_back(f);
}
bool flow_has_tag(const flow & f, std::string_view tag)
{
if (tag.empty())
return true;
if (f.tags == nullptr)
return false;
std::string_view tags(f.tags);
std::size_t i = 0;
while (i < tags.size())
{
while (i < tags.size() && tags[i] == ' ')
++i;
std::size_t j = i;
while (j < tags.size() && tags[j] != ' ')
++j;
if (j > i && tags.substr(i, j - i) == tag)
return true;
i = j;
}
return false;
}
bool flow_is_pair(const flow & f)
{
const std::string_view name = f.name ? f.name : "";
if (name.size() >= 5 && name.substr(0, 5) == "iknp_")
return true;
return flow_has_tag(f, "iknp");
}
const flow * find_flow(std::string_view name)
{
for (const auto & f : table())
{
if (f.name == name)
return &f;
}
return nullptr;
}
std::vector<const flow *> select_flows(std::string_view filter)
{
std::vector<const flow *> out;
auto want = split_csv(filter);
for (const auto & f : table())
{
bool ok = true;
for (const auto & t : want)
{
if (!flow_has_tag(f, t))
{
ok = false;
break;
}
}
if (ok)
out.push_back(&f);
}
return out;
}
void list_flows(std::ostream & os, std::string_view filter)
{
auto sel = select_flows(filter);
for (const flow * f : sel)
{
os << f->name << '\t' << (f->tags ? f->tags : "")
<< (f->bench ? "\tbench" : "\tsmoke") << '\n';
}
os << "# " << sel.size() << " flows\n";
}
std::size_t flow_count()
{
return table().size();
}
} // namespace party
} // namespace dpf

51
party/registry.hpp Normal file
View file

@ -0,0 +1,51 @@
/// @file party/registry.hpp
/// @brief Named (2+1) party flows with tags for filtering and benchmarking.
#ifndef LIBDPF_PARTY_REGISTRY_HPP__
#define LIBDPF_PARTY_REGISTRY_HPP__
#include <cstddef>
#include <ostream>
#include <string>
#include <string_view>
#include <vector>
#include "dpf/net/trio.hpp"
namespace dpf
{
namespace party
{
using flow_fn = int (*)(net::role self, net::trio & net);
/// @brief One runnable flow. `tags` is a space-separated tag list.
struct flow
{
const char * name = nullptr;
const char * tags = nullptr;
flow_fn run = nullptr;
/// @brief When false, skip under `--tag bench` (reject / handshake-only).
bool bench = true;
};
void register_flow(flow f);
void register_all_flows();
const flow * find_flow(std::string_view name);
bool flow_has_tag(const flow & f, std::string_view tag);
/// @brief Two-party flow: spawn p0 and p1 only. IKNP has no dealer.
bool flow_is_pair(const flow & f);
/// @brief All flows, or those matching every comma-separated tag in `filter`.
/// Empty filter returns every registered flow.
std::vector<const flow *> select_flows(std::string_view filter);
void list_flows(std::ostream & os, std::string_view filter = {});
std::size_t flow_count();
} // namespace party
} // namespace dpf
#endif // LIBDPF_PARTY_REGISTRY_HPP__

1518
party/run.cpp Normal file

File diff suppressed because it is too large Load diff

219
party/spawn.hpp Normal file
View file

@ -0,0 +1,219 @@
/// @file party/spawn.hpp
/// @brief Spawn p0/p1/p2 for one flow (used by gtest and party_bench).
#ifndef LIBDPF_PARTY_SPAWN_HPP__
#define LIBDPF_PARTY_SPAWN_HPP__
#include <chrono>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>
#ifndef LIBDPF_PARTY_BIN_DIR
#define LIBDPF_PARTY_BIN_DIR "."
#endif
namespace dpf
{
namespace party
{
inline std::string party_bin(const char * name)
{
const char * env = std::getenv("LIBDPF_PARTY_BIN_DIR");
std::string dir = env ? env : LIBDPF_PARTY_BIN_DIR;
return dir + "/" + name;
}
struct spawn_result
{
int rc[3] = {0, 0, 0};
double wall_ms = 0;
/// @brief Per-role metric lines (from --metrics), ordered p0/p1/p2.
std::vector<std::string> metrics_lines;
};
struct spawn_opts
{
std::uint64_t repeat = 1;
std::uint64_t warmup = 0;
bool metrics = false;
/// @brief Pass `case_name` as `--tag` so one mesh runs every match.
bool by_tag = false;
};
inline spawn_result spawn_trio_flow(const std::string & case_name,
const spawn_opts & opts = {})
{
namespace fs = std::filesystem;
auto dir = fs::temp_directory_path()
/ ("libdpf_party_" + std::to_string(::getpid()) + "_" + case_name);
fs::create_directories(dir);
std::string rep = std::to_string(opts.repeat);
std::string warm = std::to_string(opts.warmup);
const auto t0 = std::chrono::steady_clock::now();
pid_t kids[3];
const char * bins[3] = {"p0", "p1", "p2"};
for (int i = 0; i < 3; ++i)
{
kids[i] = fork();
if (kids[i] < 0)
{
fs::remove_all(dir);
throw std::runtime_error("fork failed");
}
if (kids[i] == 0)
{
auto bin = party_bin(bins[i]);
const char * sel = opts.by_tag ? "--tag" : "--case";
if (opts.metrics)
{
execl(bin.c_str(), bin.c_str(),
"--dir", dir.string().c_str(),
sel, case_name.c_str(),
"--repeat", rep.c_str(),
"--warmup", warm.c_str(),
"--metrics",
static_cast<char *>(nullptr));
}
else
{
execl(bin.c_str(), bin.c_str(),
"--dir", dir.string().c_str(),
sel, case_name.c_str(),
"--repeat", rep.c_str(),
"--warmup", warm.c_str(),
static_cast<char *>(nullptr));
}
_exit(126);
}
}
spawn_result out;
for (int i = 0; i < 3; ++i)
{
int status = 0;
if (waitpid(kids[i], &status, 0) < 0)
out.rc[i] = 125;
else
out.rc[i] = WIFEXITED(status) ? WEXITSTATUS(status) : 124;
}
const auto t1 = std::chrono::steady_clock::now();
out.wall_ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
if (opts.metrics)
{
const char * roles[3] = {"p0", "p1", "p2"};
for (const char * role : roles)
{
auto path = dir / (std::string("metrics.") + role);
std::ifstream in(path);
std::string line;
while (std::getline(in, line))
{
if (!line.empty())
out.metrics_lines.push_back(line);
}
}
}
fs::remove_all(dir);
return out;
}
/// @brief Spawn only p0 and p1 (no dealer). For IKNP / two-party flows.
inline spawn_result spawn_pair_flow(const std::string & case_name,
const spawn_opts & opts = {})
{
namespace fs = std::filesystem;
auto dir = fs::temp_directory_path()
/ ("libdpf_pair_" + std::to_string(::getpid()) + "_" + case_name);
fs::create_directories(dir);
std::string rep = std::to_string(opts.repeat);
std::string warm = std::to_string(opts.warmup);
const auto t0 = std::chrono::steady_clock::now();
pid_t kids[2];
const char * bins[2] = {"p0", "p1"};
for (int i = 0; i < 2; ++i)
{
kids[i] = fork();
if (kids[i] < 0)
{
fs::remove_all(dir);
throw std::runtime_error("fork failed");
}
if (kids[i] == 0)
{
auto bin = party_bin(bins[i]);
const char * sel = opts.by_tag ? "--tag" : "--case";
if (opts.metrics)
{
execl(bin.c_str(), bin.c_str(),
"--dir", dir.string().c_str(),
sel, case_name.c_str(),
"--repeat", rep.c_str(),
"--warmup", warm.c_str(),
"--metrics",
"--pair",
static_cast<char *>(nullptr));
}
else
{
execl(bin.c_str(), bin.c_str(),
"--dir", dir.string().c_str(),
sel, case_name.c_str(),
"--repeat", rep.c_str(),
"--warmup", warm.c_str(),
"--pair",
static_cast<char *>(nullptr));
}
_exit(126);
}
}
spawn_result out;
for (int i = 0; i < 2; ++i)
{
int status = 0;
if (waitpid(kids[i], &status, 0) < 0)
out.rc[i] = 125;
else
out.rc[i] = WIFEXITED(status) ? WEXITSTATUS(status) : 124;
}
out.rc[2] = 0;
const auto t1 = std::chrono::steady_clock::now();
out.wall_ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
if (opts.metrics)
{
const char * roles[2] = {"p0", "p1"};
for (const char * role : roles)
{
auto path = dir / (std::string("metrics.") + role);
std::ifstream in(path);
std::string line;
while (std::getline(in, line))
{
if (!line.empty())
out.metrics_lines.push_back(line);
}
}
}
fs::remove_all(dir);
return out;
}
} // namespace party
} // namespace dpf
#endif // LIBDPF_PARTY_SPAWN_HPP__