#include #include #include #include #include #include #include #include #include #include #include #include #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 operator()(int input, std::uint64_t blind) const { const int b = static_cast(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 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; const auto seed = dpf::randomness::sample_master_seed<>(); Prg prg0(seed); Prg prg1(seed); auto s0 = dpf::protocol::make_two_move_session(N, sink0, dealer, prg0, Round1{}, Round2{}); auto s1 = dpf::protocol::make_two_move_session(N, sink1, dealer, prg1, Round1{}, Round2{}); std::vector 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(order[i] + 1)); s1.submit(i, static_cast(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(i)); const int b = static_cast(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(i + 1)); EXPECT_EQ(s1.take(i), static_cast(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; Prg prg(dpf::randomness::sample_master_seed<>()); auto s0 = dpf::protocol::make_two_move_session(N, sink0, dealer, prg, Round1Empty{}, Round2Empty{}); auto s1 = dpf::protocol::make_two_move_session(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(&v), sizeof(v)); v = 2; sink0.submit(0, 2, reinterpret_cast(&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(&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 local0(N); std::vector local1(N); for (std::size_t i = 0; i < N; ++i) { local0[i] = 10 + i; local1[i] = 100 + i; } auto make_rounds = [&](std::vector & local) { std::vector 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(&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; const auto seed = simde_mm_setzero_si128(); Prg prg(seed); dealer_cursor dealer(N, {}); std::unique_ptr 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(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( dpf::net::make_mux_sink(net, N, {sizeof(int)})); } auto sess = dpf::protocol::make_two_move_session(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(i) + base); sess.drive(); for (std::size_t i = 0; i < N; ++i) { if (sess.take(i) != static_cast(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 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 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 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 batch(dpf::net::trio & net, std::size_t count, std::vector 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(); 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 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(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(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 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 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 ba(slots[i], 0); std::vector bb(slots[i], 0); std::memcpy(ba.data(), &va, sizeof(va)); std::memcpy(bb.data(), &vb, sizeof(vb)); a.submit(static_cast(i), 0, ba.data(), ba.size()); b.submit(static_cast(i), 0, bb.data(), bb.size()); a.flush_round(static_cast(i)); EXPECT_TRUE(a.peer_ready(static_cast(i), 0)); EXPECT_TRUE(b.peer_ready(static_cast(i), 0)); std::vector ra(slots[i], 0); std::vector rb(slots[i], 0); a.read_peer(static_cast(i), 0, ra.data(), slots[i]); b.read_peer(static_cast(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 s; std::vector::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(draws); } } rng; s.sample(rng); dpf::net::deal_session(net, s); std::_Exit(0); } auto tape = dpf::net::accept_session(net); s.install_party(self == role::p0 ? 0u : 1u, tape); for (int i = 0; i < 6; ++i) s.bind_party(xs[static_cast(i)], self == role::p0 ? static_cast(i + 1) : static_cast(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 slots( static_cast(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 batch(dpf::net::trio & net, std::size_t count, std::vector slot_bytes) override { last_rounds = slot_bytes.size(); return std::make_unique( 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 s; std::vector::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(draws); } } rng; s.sample(rng); dpf::net::deal_session(net, s); std::_Exit(0); } auto tape = dpf::net::accept_session(net); s.install_party(self == role::p0 ? 0u : 1u, tape); for (int i = 0; i < N; ++i) { s.bind_party(xs[static_cast(i)], self == role::p0 ? static_cast(i + 1) : static_cast(1000 + i)); s.bind_party(ys[static_cast(i)], self == role::p0 ? static_cast(i + 3) : static_cast(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(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(i + 1) + static_cast(1000 + i); const std::uint64_t yi = static_cast(i + 3) + static_cast(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 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 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(&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(&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 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(&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(&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(&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