libdpf/test/tests/protocol_batch_test.cpp

1048 lines
35 KiB
C++
Raw Normal View History

#include <gtest/gtest.h>
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <numeric>
#include <random>
#include <string>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>
#include "dpf/beaver.hpp"
#include "dpf/buffered_prg.hpp"
#include "dpf/net/dealer_cursor.hpp"
#include "dpf/net/memory_sink.hpp"
#include "dpf/net/mux_sink.hpp"
#include "dpf/net/party_tape_io.hpp"
#include "dpf/net/sink_exchange.hpp"
#include "dpf/net/stream_sink.hpp"
#include "dpf/net/trio.hpp"
#include "dpf/protocol.hpp"
#include "flow_util.hpp"
#include "simde/simde/x86/avx2.h"
namespace
{
using dpf::protocol::empty_pad;
using dpf::net::dealer_cursor;
using dpf::net::make_memory_sink_pair;
using dpf::net::role;
struct Round1
{
std::pair<int, int> operator()(int input, std::uint64_t blind) const
{
const int b = static_cast<int>(blind & 0xffffu);
return {input - b, b};
}
};
struct Round2
{
int operator()(int forward, int peer_swap, std::uint64_t blind,
empty_pad) const
{
(void)blind;
return forward + peer_swap;
}
};
struct Round1Empty
{
std::pair<int, int> operator()(int input, empty_pad) const
{
return {input, input};
}
};
struct Round2Empty
{
int operator()(int forward, int peer_swap, empty_pad, empty_pad) const
{
return forward + peer_swap;
}
};
TEST(ProtocolBatch, MemoryTwoMoveShuffled)
{
constexpr std::size_t N = 100;
auto [sink0, sink1] = make_memory_sink_pair(N, {sizeof(int)});
dealer_cursor dealer(N, {});
using Prg = dpf::randomness::aes_buffered_prg<std::uint64_t, std::uint64_t>;
const auto seed = dpf::randomness::sample_master_seed<>();
Prg prg0(seed);
Prg prg1(seed);
auto s0 = dpf::protocol::make_two_move_session<int, int, std::uint64_t,
empty_pad, int>(N, sink0, dealer, prg0, Round1{}, Round2{});
auto s1 = dpf::protocol::make_two_move_session<int, int, std::uint64_t,
empty_pad, int>(N, sink1, dealer, prg1, Round1{}, Round2{});
std::vector<std::size_t> order(N);
std::iota(order.begin(), order.end(), 0);
std::mt19937 rng(0xC0FFEE);
std::shuffle(order.begin(), order.end(), rng);
for (std::size_t i = 0; i < N; ++i)
{
s0.submit(order[i], static_cast<int>(order[i] + 1));
s1.submit(i, static_cast<int>(i + 7));
}
s0.drive();
s1.drive();
for (std::size_t i = 0; i < N; ++i)
{
// Each party reconstructs input0 + input1 from shares of the blind.
// Round1: fwd = in - b, swap = b. Peer sees the other party's b.
// Round2: fwd + peer_swap = (in_me - b_me) + b_peer.
// Sum of both outputs is not simply in0+in1; check local oracle:
const auto blind = prg0.at<0>(static_cast<std::uint64_t>(i));
const int b = static_cast<int>(blind & 0xffffu);
// Party 0 submitted order-permuted; rebuild expectation for index i
// as party 0's view after drive.
(void)b;
EXPECT_EQ(s0.take(i), s0.take(i));
EXPECT_EQ(s1.take(i), s1.take(i));
}
// Both parties used the same PRG seed, so for a fixed index the blinds
// match. Party 0 input at index j is j+1 when submitted as order mapping:
// s0.submit(order[k], order[k]+1) => input[order[k]] = order[k]+1
// so input0[i] = i+1. Party 1 input[i] = i+7.
// out0 = (i+1 - b) + b = i+1
// out1 = (i+7 - b) + b = i+7
for (std::size_t i = 0; i < N; ++i)
{
EXPECT_EQ(s0.take(i), static_cast<int>(i + 1));
EXPECT_EQ(s1.take(i), static_cast<int>(i + 7));
}
}
TEST(ProtocolBatch, MemoryEmptyBlindAndCorrection)
{
constexpr std::size_t N = 16;
auto [sink0, sink1] = make_memory_sink_pair(N, {sizeof(int)});
dealer_cursor dealer(N, {});
using Prg = dpf::randomness::aes_buffered_prg<std::uint64_t>;
Prg prg(dpf::randomness::sample_master_seed<>());
auto s0 = dpf::protocol::make_two_move_session<int, int, empty_pad,
empty_pad, int>(N, sink0, dealer, prg, Round1Empty{}, Round2Empty{});
auto s1 = dpf::protocol::make_two_move_session<int, int, empty_pad,
empty_pad, int>(N, sink1, dealer, prg, Round1Empty{}, Round2Empty{});
for (std::size_t i = 0; i < N; ++i)
{
s0.submit(i, 3);
s1.submit(i, 4);
}
s0.drive();
s1.drive();
for (std::size_t i = 0; i < N; ++i)
{
// fwd=in, swap=in; out = fwd + peer_swap = in_me + in_peer
EXPECT_EQ(s0.take(i), 3 + 4);
EXPECT_EQ(s1.take(i), 4 + 3);
}
}
TEST(ProtocolBatch, MemoryHoleBlocksPrefix)
{
auto [sink0, sink1] = make_memory_sink_pair(3, {sizeof(int)});
int v = 1;
sink0.submit(0, 0, reinterpret_cast<const std::uint8_t *>(&v), sizeof(v));
v = 2;
sink0.submit(0, 2, reinterpret_cast<const std::uint8_t *>(&v), sizeof(v));
sink0.flush();
// Index 1 is a hole: only index 0 may be delivered.
EXPECT_TRUE(sink1.peer_ready(0, 0));
EXPECT_FALSE(sink1.peer_ready(0, 1));
EXPECT_FALSE(sink1.peer_ready(0, 2));
v = 9;
sink0.submit(0, 1, reinterpret_cast<const std::uint8_t *>(&v), sizeof(v));
sink0.flush();
EXPECT_TRUE(sink1.peer_ready(0, 1));
EXPECT_TRUE(sink1.peer_ready(0, 2));
}
TEST(ProtocolBatch, ScheduleSessionMemory)
{
constexpr std::size_t N = 8;
auto [sink0, sink1] = make_memory_sink_pair(N, {sizeof(std::uint64_t)});
std::vector<std::uint64_t> local0(N);
std::vector<std::uint64_t> local1(N);
for (std::size_t i = 0; i < N; ++i)
{
local0[i] = 10 + i;
local1[i] = 100 + i;
}
auto make_rounds = [&](std::vector<std::uint64_t> & local) {
std::vector<dpf::protocol::schedule_round> rounds(1);
rounds[0].slot_bytes = sizeof(std::uint64_t);
rounds[0].produce = [&](std::size_t index, const std::uint8_t *,
std::size_t, std::uint8_t * out) {
std::memcpy(out, &local[index], sizeof(std::uint64_t));
};
return rounds;
};
dpf::protocol::schedule_session a(N, sink0, make_rounds(local0));
dpf::protocol::schedule_session b(N, sink1, make_rounds(local1));
for (std::size_t i = 0; i < N; ++i)
{
a.submit(i);
b.submit(N - 1 - i);
}
a.drive();
b.drive();
for (std::size_t i = 0; i < N; ++i)
{
EXPECT_TRUE(a.done(i));
EXPECT_TRUE(b.done(i));
std::uint64_t peer = 0;
sink0.read_peer(0, i, reinterpret_cast<std::uint8_t *>(&peer),
sizeof(peer));
EXPECT_EQ(peer, local1[i]);
}
}
void run_trio_two_move(bool use_stream)
{
constexpr std::size_t N = 32;
const std::string dir = "/tmp/libdpf-protocol-batch-XXXXXX";
std::string path = dir;
ASSERT_NE(mkdtemp(path.data()), nullptr);
auto child = [&](role self) {
using Prg = dpf::randomness::aes_buffered_prg<std::uint64_t, std::uint64_t>;
const auto seed = simde_mm_setzero_si128();
Prg prg(seed);
dealer_cursor dealer(N, {});
std::unique_ptr<dpf::net::RoundSink> sink;
dpf::net::trio net;
if (use_stream)
{
auto s = dpf::net::connect_stream_sink(self,
self == role::p0 ? role::p1 : role::p0, path, N,
{sizeof(int)});
struct holder : dpf::net::RoundSink
{
dpf::net::stream_sink inner;
explicit holder(dpf::net::stream_sink s) : inner(std::move(s)) {}
std::size_t count() const noexcept override
{
return inner.count();
}
std::size_t rounds() const noexcept override
{
return inner.rounds();
}
std::size_t slot_bytes(std::uint16_t r) const override
{
return inner.slot_bytes(r);
}
void submit(std::uint16_t r, std::size_t i,
const std::uint8_t * b, std::size_t n) override
{
inner.submit(r, i, b, n);
}
bool peer_ready(std::uint16_t r, std::size_t i) const override
{
return inner.peer_ready(r, i);
}
void read_peer(std::uint16_t r, std::size_t i, std::uint8_t * o,
std::size_t n) const override
{
inner.read_peer(r, i, o, n);
}
void flush() override { inner.flush(); }
void flush_round(std::uint16_t r) override
{
inner.flush_round(r);
}
void poll() override { inner.poll(); }
};
sink = std::make_unique<holder>(std::move(s));
}
else
{
net = dpf::net::trio::connect_pair(self, path);
struct holder : dpf::net::RoundSink
{
dpf::net::mux_sink inner;
explicit holder(dpf::net::mux_sink s) : inner(std::move(s)) {}
std::size_t count() const noexcept override
{
return inner.count();
}
std::size_t rounds() const noexcept override
{
return inner.rounds();
}
std::size_t slot_bytes(std::uint16_t r) const override
{
return inner.slot_bytes(r);
}
void submit(std::uint16_t r, std::size_t i,
const std::uint8_t * b, std::size_t n) override
{
inner.submit(r, i, b, n);
}
bool peer_ready(std::uint16_t r, std::size_t i) const override
{
return inner.peer_ready(r, i);
}
void read_peer(std::uint16_t r, std::size_t i, std::uint8_t * o,
std::size_t n) const override
{
inner.read_peer(r, i, o, n);
}
void flush() override { inner.flush(); }
void flush_round(std::uint16_t r) override
{
inner.flush_round(r);
}
void poll() override { inner.poll(); }
};
sink = std::make_unique<holder>(
dpf::net::make_mux_sink(net, N, {sizeof(int)}));
}
auto sess = dpf::protocol::make_two_move_session<int, int, std::uint64_t,
empty_pad, int>(N, *sink, dealer, prg, Round1{}, Round2{});
const int base = self == role::p0 ? 1 : 7;
for (std::size_t i = 0; i < N; ++i)
sess.submit(i, static_cast<int>(i) + base);
sess.drive();
for (std::size_t i = 0; i < N; ++i)
{
if (sess.take(i) != static_cast<int>(i) + base)
std::_Exit(2);
}
std::_Exit(0);
};
pid_t p0 = fork();
ASSERT_GE(p0, 0);
if (p0 == 0)
child(role::p0);
pid_t p1 = fork();
ASSERT_GE(p1, 0);
if (p1 == 0)
child(role::p1);
int st0 = 0;
int st1 = 0;
waitpid(p0, &st0, 0);
waitpid(p1, &st1, 0);
EXPECT_TRUE(WIFEXITED(st0) && WEXITSTATUS(st0) == 0);
EXPECT_TRUE(WIFEXITED(st1) && WEXITSTATUS(st1) == 0);
}
TEST(ProtocolBatch, MuxTwoMove)
{
run_trio_two_move(false);
}
TEST(ProtocolBatch, StreamTwoMove)
{
run_trio_two_move(true);
}
/// @brief Counts hooked trio helpers; framing stays on the mesh.
struct counting_hook : dpf::net::mesh_comm_hook
{
int sends = 0;
int recvs = 0;
int exchanges = 0;
int batches = 0;
void send_bytes(dpf::net::trio & net, unsigned peer_id, dpf::net::msg tag,
const void * data, std::size_t n) override
{
++sends;
mesh_comm_hook::send_bytes(net, peer_id, tag, data, n);
}
std::vector<std::uint8_t> recv_bytes(dpf::net::trio & net, unsigned peer_id,
dpf::net::msg tag) override
{
++recvs;
return mesh_comm_hook::recv_bytes(net, peer_id, tag);
}
std::vector<std::uint8_t> exchange_bytes(dpf::net::trio & net,
unsigned peer_id, dpf::net::msg tag, const void * data,
std::size_t n) override
{
++exchanges;
return mesh_comm_hook::exchange_bytes(net, peer_id, tag, data, n);
}
std::vector<std::uint8_t> exchange_vec_bytes(dpf::net::trio & net,
unsigned peer_id, dpf::net::msg tag, const void * data,
std::size_t nbytes) override
{
++exchanges;
return mesh_comm_hook::exchange_vec_bytes(net, peer_id, tag, data,
nbytes);
}
std::unique_ptr<dpf::net::RoundSink> batch(dpf::net::trio & net,
std::size_t count, std::vector<std::size_t> slot_bytes) override
{
++batches;
return mesh_comm_hook::batch(net, count, std::move(slot_bytes));
}
};
TEST(ProtocolBatch, HookRoutesDealExchangeAndBeaver)
{
char tmpl[] = "/tmp/dpf-hook-XXXXXX";
ASSERT_NE(mkdtemp(tmpl), nullptr);
const std::string path = tmpl;
auto child = [&](role self) {
counting_hook hook;
auto net = dpf::net::trio::connect_local(self, path);
net.set_hook(&hook);
// Dealer pad through hooked send/recv.
if (self == role::p2)
{
struct split
{
std::uint64_t p0 = 11;
std::uint64_t p1 = 31;
} s;
net.deal(s);
}
else
{
const auto pad = net.accept_deal<std::uint64_t>();
if (self == role::p0 && pad != 11ull)
std::_Exit(3);
if (self == role::p1 && pad != 31ull)
std::_Exit(4);
}
// Beaver online through hooked batch (no mux_sink name at the call).
dpf::beavers::session<std::uint64_t> s;
auto x = s.input();
auto y = s.input();
auto z = s(x * y);
if (self == role::p2)
{
struct Counter
{
int draws = 0;
std::uint64_t operator()()
{
++draws;
return 0x9e3779b97f4a7c15ull * static_cast<std::uint64_t>(draws);
}
} rng;
s.sample(rng);
dpf::net::deal_session(net, s);
if (hook.sends < 2)
std::_Exit(5);
std::_Exit(0);
}
auto tape = dpf::net::accept_session<std::uint64_t>(net);
s.install_party(self == role::p0 ? 0u : 1u, tape);
s.bind_party(x, self == role::p0 ? 3ull : 4ull);
s.bind_party(y, self == role::p0 ? 5ull : 6ull);
dpf::party::util::evaluate_online(s, net, self);
const auto open = net.open_with(
self == role::p0 ? role::p1 : role::p0, s.value_party(z));
// Shares (3,4) and (5,6) reconstruct to 7 * 11 = 77.
if (self == role::p0 && open != 77ull)
std::_Exit(6);
if (hook.batches < 1)
std::_Exit(7);
if (hook.recvs < 1)
std::_Exit(8);
std::_Exit(0);
};
pid_t p0 = fork();
ASSERT_GE(p0, 0);
if (p0 == 0)
child(role::p0);
pid_t p1 = fork();
ASSERT_GE(p1, 0);
if (p1 == 0)
child(role::p1);
pid_t p2 = fork();
ASSERT_GE(p2, 0);
if (p2 == 0)
child(role::p2);
int st0 = 0, st1 = 0, st2 = 0;
waitpid(p0, &st0, 0);
waitpid(p1, &st1, 0);
waitpid(p2, &st2, 0);
EXPECT_TRUE(WIFEXITED(st0) && WEXITSTATUS(st0) == 0) << WEXITSTATUS(st0);
EXPECT_TRUE(WIFEXITED(st1) && WEXITSTATUS(st1) == 0) << WEXITSTATUS(st1);
EXPECT_TRUE(WIFEXITED(st2) && WEXITSTATUS(st2) == 0) << WEXITSTATUS(st2);
}
/// @brief Oversized stream sink + sink_exchange must not hang or pay one
/// exchange per idle round (flush_round path only).
TEST(ProtocolBatch, StreamFlushRoundNoIdleStorm)
{
constexpr std::size_t R = 64;
constexpr std::size_t steps = 8;
char tmpl[] = "/tmp/dpf-stream-flush-XXXXXX";
ASSERT_NE(mkdtemp(tmpl), nullptr);
const std::string path = tmpl;
auto child = [&](role self) {
alarm(15);
std::vector<std::size_t> slots(R, sizeof(std::uint64_t));
auto sink = dpf::net::connect_stream_sink(self,
self == role::p0 ? role::p1 : role::p0, path, 1, std::move(slots));
dpf::net::sink_exchange ex(sink, 0);
for (std::size_t i = 0; i < steps; ++i)
{
const std::uint64_t mine =
(self == role::p0 ? 0x1000ull : 0x2000ull) + i;
const auto peer = ex(mine);
const std::uint64_t expect =
(self == role::p0 ? 0x2000ull : 0x1000ull) + i;
if (peer != expect)
std::_Exit(2);
}
// flush_round path: exactly one duplex per step, not one per idle round.
if (sink.exchanges() != steps)
std::_Exit(3);
std::_Exit(0);
};
pid_t p0 = fork();
ASSERT_GE(p0, 0);
if (p0 == 0)
child(role::p0);
pid_t p1 = fork();
ASSERT_GE(p1, 0);
if (p1 == 0)
child(role::p1);
int st0 = 0;
int st1 = 0;
waitpid(p0, &st0, 0);
waitpid(p1, &st1, 0);
EXPECT_TRUE(WIFEXITED(st0) && WEXITSTATUS(st0) == 0) << WEXITSTATUS(st0);
EXPECT_TRUE(WIFEXITED(st1) && WEXITSTATUS(st1) == 0) << WEXITSTATUS(st1);
}
/// @brief Mux flush_round must not invent extra link exchanges beyond the
/// number of interactive steps (no per-idle-round storm).
TEST(ProtocolBatch, MuxFlushRoundExchangeCount)
{
constexpr std::size_t R = 64;
constexpr std::size_t steps = 8;
char tmpl[] = "/tmp/dpf-mux-flush-XXXXXX";
ASSERT_NE(mkdtemp(tmpl), nullptr);
const std::string path = tmpl;
auto child = [&](role self) {
alarm(15);
auto net = dpf::net::trio::connect_pair(self, path);
std::vector<std::size_t> slots(R, sizeof(std::uint64_t));
dpf::net::mux_sink sink = dpf::net::make_mux_sink(net, 1, std::move(slots));
dpf::net::sink_exchange ex(sink, 0);
for (std::size_t i = 0; i < steps; ++i)
{
const std::uint64_t mine =
(self == role::p0 ? 0xA000ull : 0xB000ull) + i;
const auto peer = ex(mine);
const std::uint64_t expect =
(self == role::p0 ? 0xB000ull : 0xA000ull) + i;
if (peer != expect)
std::_Exit(2);
}
if (sink.exchanges() != steps)
std::_Exit(3);
std::_Exit(0);
};
pid_t p0 = fork();
ASSERT_GE(p0, 0);
if (p0 == 0)
child(role::p0);
pid_t p1 = fork();
ASSERT_GE(p1, 0);
if (p1 == 0)
child(role::p1);
int st0 = 0;
int st1 = 0;
waitpid(p0, &st0, 0);
waitpid(p1, &st1, 0);
EXPECT_TRUE(WIFEXITED(st0) && WEXITSTATUS(st0) == 0) << WEXITSTATUS(st0);
EXPECT_TRUE(WIFEXITED(st1) && WEXITSTATUS(st1) == 0) << WEXITSTATUS(st1);
}
/// @brief Memory sink_exchange across a padded round budget (ds-walk sized).
TEST(ProtocolBatch, MemorySinkExchangePaddedRounds)
{
const auto slots = dpf::net::ds_walk_slot_bytes(8, false);
ASSERT_GE(slots.size(), 16u);
auto [a, b] = make_memory_sink_pair(1, slots);
// Interleave submits then one flush_round — memory hub moves both directions.
for (std::size_t i = 0; i < 12; ++i)
{
const std::uint64_t va = 0x11ull + i;
const std::uint64_t vb = 0x22ull + i;
// memory_sink: one flush_round after both submits (shared hub).
std::vector<std::uint8_t> ba(slots[i], 0);
std::vector<std::uint8_t> bb(slots[i], 0);
std::memcpy(ba.data(), &va, sizeof(va));
std::memcpy(bb.data(), &vb, sizeof(vb));
a.submit(static_cast<std::uint16_t>(i), 0, ba.data(), ba.size());
b.submit(static_cast<std::uint16_t>(i), 0, bb.data(), bb.size());
a.flush_round(static_cast<std::uint16_t>(i));
EXPECT_TRUE(a.peer_ready(static_cast<std::uint16_t>(i), 0));
EXPECT_TRUE(b.peer_ready(static_cast<std::uint16_t>(i), 0));
std::vector<std::uint8_t> ra(slots[i], 0);
std::vector<std::uint8_t> rb(slots[i], 0);
a.read_peer(static_cast<std::uint16_t>(i), 0, ra.data(), slots[i]);
b.read_peer(static_cast<std::uint16_t>(i), 0, rb.data(), slots[i]);
std::uint64_t pa = 0;
std::uint64_t pb = 0;
std::memcpy(&pa, ra.data(), sizeof(pa));
std::memcpy(&pb, rb.data(), sizeof(pb));
EXPECT_EQ(pa, vb);
EXPECT_EQ(pb, va);
}
}
/// @brief Hooked beaver online on an oversized batch must not exhaust rounds
/// or deadlock under a wall-clock alarm.
TEST(ProtocolBatch, HookBeaverOnlinePaddedBatch)
{
char tmpl[] = "/tmp/dpf-hook-pad-XXXXXX";
ASSERT_NE(mkdtemp(tmpl), nullptr);
const std::string path = tmpl;
auto child = [&](role self) {
alarm(20);
counting_hook hook;
auto net = dpf::net::trio::connect_local(self, path);
net.set_hook(&hook);
dpf::beavers::session<std::uint64_t> s;
std::vector<dpf::beavers::session<std::uint64_t>::wire> xs;
xs.reserve(6);
for (int i = 0; i < 6; ++i)
xs.push_back(s.input());
auto z = s(xs[0] * xs[1] + xs[2] * xs[3] + xs[4] * xs[5]);
if (self == role::p2)
{
struct Counter
{
int draws = 0;
std::uint64_t operator()()
{
++draws;
return 0x9e3779b97f4a7c15ull
* static_cast<std::uint64_t>(draws);
}
} rng;
s.sample(rng);
dpf::net::deal_session(net, s);
std::_Exit(0);
}
auto tape = dpf::net::accept_session<std::uint64_t>(net);
s.install_party(self == role::p0 ? 0u : 1u, tape);
for (int i = 0; i < 6; ++i)
s.bind_party(xs[static_cast<std::size_t>(i)],
self == role::p0 ? static_cast<std::uint64_t>(i + 1)
: static_cast<std::uint64_t>(10 + i));
// Pad far beyond max_ready_round to mimic ds_walk oversizing.
const int need = std::max(1, s.max_ready_round());
std::vector<std::size_t> slots(
static_cast<std::size_t>(need) + 48,
sizeof(std::uint32_t) + 64 * sizeof(std::uint64_t));
auto sink = net.batch(1, std::move(slots));
dpf::party::util::evaluate_online_on_sink(s, *sink, 0);
const auto open = net.open_with(
self == role::p0 ? role::p1 : role::p0, s.value_party(z));
// (1*11)+(2*12)+(3*13) tied to party shares: p0 binds 1..6, p1 10..15
// products reconstruct (1+10)*(2+11) wait — each wire is additive share.
// x_i = p0_i + p1_i = (i+1)+(10+i) = 11+2i
// z = x0*x1 + x2*x3 + x4*x5
// = 11*13 + 15*17 + 19*21 = 143 + 255 + 399 = 797
if (self == role::p0 && open != 797ull)
std::_Exit(4);
if (hook.batches < 1)
std::_Exit(5);
std::_Exit(0);
};
pid_t p0 = fork();
ASSERT_GE(p0, 0);
if (p0 == 0)
child(role::p0);
pid_t p1 = fork();
ASSERT_GE(p1, 0);
if (p1 == 0)
child(role::p1);
pid_t p2 = fork();
ASSERT_GE(p2, 0);
if (p2 == 0)
child(role::p2);
int st0 = 0, st1 = 0, st2 = 0;
waitpid(p0, &st0, 0);
waitpid(p1, &st1, 0);
waitpid(p2, &st2, 0);
EXPECT_TRUE(WIFEXITED(st0) && WEXITSTATUS(st0) == 0) << WEXITSTATUS(st0);
EXPECT_TRUE(WIFEXITED(st1) && WEXITSTATUS(st1) == 0) << WEXITSTATUS(st1);
EXPECT_TRUE(WIFEXITED(st2) && WEXITSTATUS(st2) == 0) << WEXITSTATUS(st2);
}
/// @brief Wide shallow circuits must not allocate O(wires) round windows.
TEST(ProtocolBatch, BeaverOnlineRoundBudgetTight)
{
char tmpl[] = "/tmp/dpf-beaver-budget-XXXXXX";
ASSERT_NE(mkdtemp(tmpl), nullptr);
const std::string path = tmpl;
auto child = [&](role self) {
alarm(30);
struct hold_hook : dpf::net::mesh_comm_hook
{
std::size_t last_rounds = 0;
std::uint64_t exchanges = 0;
struct wrap : dpf::net::RoundSink
{
dpf::net::mux_sink inner;
std::uint64_t * out = nullptr;
wrap(dpf::net::mux_sink s, std::uint64_t * o)
: inner(std::move(s)), out(o)
{ }
~wrap() override
{
if (out)
*out = inner.exchanges();
}
std::size_t count() const noexcept override
{
return inner.count();
}
std::size_t rounds() const noexcept override
{
return inner.rounds();
}
std::size_t slot_bytes(std::uint16_t r) const override
{
return inner.slot_bytes(r);
}
void submit(std::uint16_t r, std::size_t i,
const std::uint8_t * b, std::size_t n) override
{
inner.submit(r, i, b, n);
}
bool peer_ready(std::uint16_t r, std::size_t i) const override
{
return inner.peer_ready(r, i);
}
void read_peer(std::uint16_t r, std::size_t i, std::uint8_t * o,
std::size_t n) const override
{
inner.read_peer(r, i, o, n);
}
void flush() override { inner.flush(); }
void flush_round(std::uint16_t r) override
{
inner.flush_round(r);
}
void poll() override { inner.poll(); }
};
std::unique_ptr<dpf::net::RoundSink> batch(dpf::net::trio & net,
std::size_t count, std::vector<std::size_t> slot_bytes) override
{
last_rounds = slot_bytes.size();
return std::make_unique<wrap>(
dpf::net::make_mux_sink(net, count, std::move(slot_bytes)),
&exchanges);
}
} h;
auto net = dpf::net::trio::connect_local(self, path);
net.set_hook(&h);
constexpr int N = 256;
dpf::beavers::session<std::uint64_t> s;
std::vector<dpf::beavers::session<std::uint64_t>::wire> xs, ys;
xs.reserve(N);
ys.reserve(N);
for (int i = 0; i < N; ++i)
{
xs.push_back(s.input());
ys.push_back(s.input());
}
auto acc = xs[0] * ys[0];
for (int i = 1; i < N; ++i)
acc = acc + xs[i] * ys[i];
auto z = s(acc);
if (self == role::p2)
{
if (s.max_ready_round() != 1)
std::_Exit(2);
struct Counter
{
int draws = 0;
std::uint64_t operator()()
{
++draws;
return 0x9e3779b97f4a7c15ull
* static_cast<std::uint64_t>(draws);
}
} rng;
s.sample(rng);
dpf::net::deal_session(net, s);
std::_Exit(0);
}
auto tape = dpf::net::accept_session<std::uint64_t>(net);
s.install_party(self == role::p0 ? 0u : 1u, tape);
for (int i = 0; i < N; ++i)
{
s.bind_party(xs[static_cast<std::size_t>(i)],
self == role::p0 ? static_cast<std::uint64_t>(i + 1)
: static_cast<std::uint64_t>(1000 + i));
s.bind_party(ys[static_cast<std::size_t>(i)],
self == role::p0 ? static_cast<std::uint64_t>(i + 3)
: static_cast<std::uint64_t>(2000 + i));
}
dpf::party::util::evaluate_online(s, net, self);
// Depth-1 product sum: budget is O(1), not O(N).
const int expect_budget = dpf::party::util::beaver_batch_round_budget(
s.max_ready_round(), s.wire_count());
if (static_cast<int>(h.last_rounds) != expect_budget)
std::_Exit(3);
if (h.last_rounds > 48)
std::_Exit(4);
if (h.exchanges == 0 || h.exchanges > h.last_rounds)
std::_Exit(5);
const auto open = net.open_with(
self == role::p0 ? role::p1 : role::p0, s.value_party(z));
// Reconstruct expected: sum_i (i+1+1000+i)*(i+3+2000+i)
// = sum_i (1001+2i)*(2003+2i)
if (self == role::p0)
{
std::uint64_t expect = 0;
for (int i = 0; i < N; ++i)
{
const std::uint64_t xi =
static_cast<std::uint64_t>(i + 1) + static_cast<std::uint64_t>(1000 + i);
const std::uint64_t yi =
static_cast<std::uint64_t>(i + 3) + static_cast<std::uint64_t>(2000 + i);
expect += xi * yi;
}
if (open != expect)
std::_Exit(6);
}
std::_Exit(0);
};
pid_t p0 = fork();
ASSERT_GE(p0, 0);
if (p0 == 0)
child(role::p0);
pid_t p1 = fork();
ASSERT_GE(p1, 0);
if (p1 == 0)
child(role::p1);
pid_t p2 = fork();
ASSERT_GE(p2, 0);
if (p2 == 0)
child(role::p2);
int st0 = 0, st1 = 0, st2 = 0;
waitpid(p0, &st0, 0);
waitpid(p1, &st1, 0);
waitpid(p2, &st2, 0);
EXPECT_TRUE(WIFEXITED(st0) && WEXITSTATUS(st0) == 0) << WEXITSTATUS(st0);
EXPECT_TRUE(WIFEXITED(st1) && WEXITSTATUS(st1) == 0) << WEXITSTATUS(st1);
EXPECT_TRUE(WIFEXITED(st2) && WEXITSTATUS(st2) == 0) << WEXITSTATUS(st2);
}
/// @brief Multi-lane sequential sink_exchange (signum pattern) must not hang.
TEST(ProtocolBatch, MultiLaneSequentialSinkExchange)
{
constexpr std::size_t lanes = 4;
constexpr std::size_t steps = 6;
char tmpl[] = "/tmp/dpf-multilane-XXXXXX";
ASSERT_NE(mkdtemp(tmpl), nullptr);
const std::string path = tmpl;
auto child = [&](role self) {
alarm(20);
auto net = dpf::net::trio::connect_pair(self, path);
std::vector<std::size_t> slots(steps, sizeof(std::uint64_t));
auto sink = dpf::net::make_mux_sink(net, lanes, std::move(slots));
for (std::size_t lane = 0; lane < lanes; ++lane)
{
dpf::net::sink_exchange ex(sink, lane);
for (std::size_t r = 0; r < steps; ++r)
{
const std::uint64_t mine =
(self == role::p0 ? 0x10ull : 0x20ull) + lane * 100 + r;
const auto peer = ex(mine);
const std::uint64_t expect =
(self == role::p0 ? 0x20ull : 0x10ull) + lane * 100 + r;
if (peer != expect)
std::_Exit(2);
}
}
// Sequential lanes: steps exchanges per lane.
if (sink.exchanges() != lanes * steps)
std::_Exit(3);
std::_Exit(0);
};
pid_t p0 = fork();
ASSERT_GE(p0, 0);
if (p0 == 0)
child(role::p0);
pid_t p1 = fork();
ASSERT_GE(p1, 0);
if (p1 == 0)
child(role::p1);
int st0 = 0, st1 = 0;
waitpid(p0, &st0, 0);
waitpid(p1, &st1, 0);
EXPECT_TRUE(WIFEXITED(st0) && WEXITSTATUS(st0) == 0) << WEXITSTATUS(st0);
EXPECT_TRUE(WIFEXITED(st1) && WEXITSTATUS(st1) == 0) << WEXITSTATUS(st1);
}
/// @brief Mux flush() bundles many pending rounds into one exchange.
TEST(ProtocolBatch, MuxFlushBundlesPendingRounds)
{
constexpr std::size_t R = 16;
char tmpl[] = "/tmp/dpf-mux-bundle-XXXXXX";
ASSERT_NE(mkdtemp(tmpl), nullptr);
const std::string path = tmpl;
auto child = [&](role self) {
alarm(15);
auto net = dpf::net::trio::connect_pair(self, path);
std::vector<std::size_t> slots(R, sizeof(std::uint32_t));
auto sink = dpf::net::make_mux_sink(net, 1, std::move(slots));
for (std::uint16_t r = 0; r < R; ++r)
{
const std::uint32_t v =
(self == role::p0 ? 100u : 200u) + r;
sink.submit(r, 0, reinterpret_cast<const std::uint8_t *>(&v),
sizeof(v));
}
sink.flush(); // one exchange for all R rounds
if (sink.exchanges() != 1)
std::_Exit(2);
for (std::uint16_t r = 0; r < R; ++r)
{
if (!sink.peer_ready(r, 0))
std::_Exit(3);
std::uint32_t peer = 0;
sink.read_peer(r, 0, reinterpret_cast<std::uint8_t *>(&peer),
sizeof(peer));
const std::uint32_t expect =
(self == role::p0 ? 200u : 100u) + r;
if (peer != expect)
std::_Exit(4);
}
std::_Exit(0);
};
pid_t p0 = fork();
ASSERT_GE(p0, 0);
if (p0 == 0)
child(role::p0);
pid_t p1 = fork();
ASSERT_GE(p1, 0);
if (p1 == 0)
child(role::p1);
int st0 = 0, st1 = 0;
waitpid(p0, &st0, 0);
waitpid(p1, &st1, 0);
EXPECT_TRUE(WIFEXITED(st0) && WEXITSTATUS(st0) == 0) << WEXITSTATUS(st0);
EXPECT_TRUE(WIFEXITED(st1) && WEXITSTATUS(st1) == 0) << WEXITSTATUS(st1);
}
/// @brief Stream flush() after multi-round submits must rendezvous (no hang).
TEST(ProtocolBatch, StreamFlushAllRoundsRendezvous)
{
constexpr std::size_t R = 8;
char tmpl[] = "/tmp/dpf-stream-bundle-XXXXXX";
ASSERT_NE(mkdtemp(tmpl), nullptr);
const std::string path = tmpl;
auto child = [&](role self) {
alarm(20);
std::vector<std::size_t> slots(R, sizeof(std::uint32_t));
auto sink = dpf::net::connect_stream_sink(self,
self == role::p0 ? role::p1 : role::p0, path, 1, std::move(slots));
// Stagger: p0 fills even rounds first, p1 fills odd — then both flush().
for (std::uint16_t r = 0; r < R; ++r)
{
const bool mine =
(self == role::p0) ? (r % 2 == 0) : (r % 2 == 1);
if (!mine)
continue;
const std::uint32_t v = 50u + r;
sink.submit(r, 0, reinterpret_cast<const std::uint8_t *>(&v),
sizeof(v));
}
sink.flush();
// After global flush both should have peer data for the rounds the
// peer filled; own rounds may still be empty on the peer side until
// the peer also submitted — second pass fills the rest.
for (std::uint16_t r = 0; r < R; ++r)
{
const bool peer_filled =
(self == role::p0) ? (r % 2 == 1) : (r % 2 == 0);
if (peer_filled && !sink.peer_ready(r, 0))
std::_Exit(2);
}
for (std::uint16_t r = 0; r < R; ++r)
{
const bool mine =
(self == role::p0) ? (r % 2 == 1) : (r % 2 == 0);
if (!mine)
continue;
const std::uint32_t v = 50u + r;
sink.submit(r, 0, reinterpret_cast<const std::uint8_t *>(&v),
sizeof(v));
}
sink.flush();
for (std::uint16_t r = 0; r < R; ++r)
{
if (!sink.peer_ready(r, 0))
std::_Exit(3);
std::uint32_t peer = 0;
sink.read_peer(r, 0, reinterpret_cast<std::uint8_t *>(&peer),
sizeof(peer));
if (peer != 50u + r)
std::_Exit(4);
}
std::_Exit(0);
};
pid_t p0 = fork();
ASSERT_GE(p0, 0);
if (p0 == 0)
child(role::p0);
pid_t p1 = fork();
ASSERT_GE(p1, 0);
if (p1 == 0)
child(role::p1);
int st0 = 0, st1 = 0;
waitpid(p0, &st0, 0);
waitpid(p1, &st1, 0);
EXPECT_TRUE(WIFEXITED(st0) && WEXITSTATUS(st0) == 0) << WEXITSTATUS(st0);
EXPECT_TRUE(WIFEXITED(st1) && WEXITSTATUS(st1) == 0) << WEXITSTATUS(st1);
}
} // namespace