#include #include #include #include #include #include #include #include #include #include #include #include #include #include "dpf/app_flow.hpp" #include "dpf/async_protocol.hpp" #include "dpf/compose_async.hpp" #include "dpf/net/async_round_sink.hpp" #include "dpf/net/async_sctp_stream_array.hpp" #include "dpf/net/async_stream_array.hpp" #include "dpf/net/round_lane.hpp" #include "dpf/net/io_pool.hpp" #include "dpf/net/party_session.hpp" #include "dpf/net/tcp_mesh.hpp" #include "dpf/bit_inject.hpp" #include "dpf/circuit.hpp" #include "dpf/compose.hpp" #include "dpf/online_session.hpp" #include "dpf/party_run.hpp" #include "dpf/prep_source.hpp" #include "dpf/buffered_prg.hpp" #include "dpf/factory_gadgets.hpp" #include "dpf/factory_tapes.hpp" #include "dpf/net/dealer_cursor.hpp" #include "dpf/protocol_factory.hpp" #include "dpf/protocol_roles.hpp" #include "dpf/revealing.hpp" #include "dpf/schedule_streams.hpp" #include "dpf/cost_pass.hpp" #include "dpf/edabit.hpp" #include "dpf/fixed_share.hpp" #include "dpf/gilboa.hpp" #include "dpf/matmul.hpp" #include "dpf/net/edge_mesh.hpp" #include "dpf/net/memory_sink.hpp" #include "dpf/net/stream_array.hpp" #include "dpf/net/stream_mesh.hpp" #include "dpf/net/stream_edge_mesh.hpp" #include "dpf/protocol_roles.hpp" #include "dpf/ot_pack.hpp" #include "dpf/protocol.hpp" #include "dpf/random.hpp" #include "dpf/rss_seed.hpp" #include "dpf/share_cmp.hpp" #include "dpf/share_expr.hpp" #include "dpf/share_vec.hpp" #include "dpf/shuffle.hpp" #include "dpf/trunc.hpp" using dpf::protocol::domain; using dpf::protocol::phase; using dpf::protocol::round_dir; using dpf::protocol::schedule_round; using dpf::protocol::schedule_session; using dpf::protocol::receive_rule; namespace { void drive_both(const dpf::protocol::plan & p, std::vector> & v0, std::vector> & v1) { auto slots = p.slot_bytes_all(); if (slots.empty()) throw std::logic_error("drive_both: plan has no exchange"); auto sinks = dpf::net::make_memory_sink_pair(1, slots); std::map kernels; std::mutex mu; std::exception_ptr err; auto run = [&](dpf::net::memory_sink & sink, std::vector> & values, std::size_t party) { try { dpf::protocol::drive(p, sink, values, kernels, party); } catch (...) { std::lock_guard lock(mu); if (!err) err = std::current_exception(); } }; std::thread t0([&] { run(sinks.first, v0, 0); }); std::thread t1([&] { run(sinks.second, v1, 1); }); t0.join(); t1.join(); if (err) std::rethrow_exception(err); } void put_raw(std::vector> & values, dpf::protocol::node n, const void * src, std::size_t nbyte) { values[n.id].assign(nbyte, 0); std::memcpy(values[n.id].data(), src, nbyte); } } // namespace // --------------------------------------------------------------------------- // Wave 1 // --------------------------------------------------------------------------- TEST(RssSeed, RandomReplicatedSums) { auto bundle = dpf::rss::sample_seed_bundle(); auto all = dpf::rss::random_replicated_all(bundle, 7); EXPECT_EQ(all.p0.next, all.p1.own); EXPECT_EQ(all.p1.next, all.p2.own); EXPECT_EQ(all.p2.next, all.p0.own); } TEST(RssSeed, ZeroSharesSumToZero) { auto bundle = dpf::rss::sample_seed_bundle(); auto s0 = dpf::rss::party_seeds::from_bundle(bundle, 0); auto s1 = dpf::rss::party_seeds::from_bundle(bundle, 1); auto s2 = dpf::rss::party_seeds::from_bundle(bundle, 2); const auto z0 = dpf::rss::zero_share(s0, 3); const auto z1 = dpf::rss::zero_share(s1, 3); const auto z2 = dpf::rss::zero_share(s2, 3); EXPECT_EQ(static_cast(z0 + z1 + z2), 0u); } TEST(RssSeed, MulLocalThenRefresh) { auto bundle = dpf::rss::sample_seed_bundle(); const std::uint64_t x = 11, y = 13; auto [xs0, xs1, xs2] = dpf::make_replicated_shares(x); auto [ys0, ys1, ys2] = dpf::make_replicated_shares(y); auto s0 = dpf::rss::party_seeds::from_bundle(bundle, 0); auto s1 = dpf::rss::party_seeds::from_bundle(bundle, 1); auto s2 = dpf::rss::party_seeds::from_bundle(bundle, 2); const auto y0 = dpf::rss::rss_mul_local(s0, xs0.own, xs0.next, ys0.own, ys0.next, 0); const auto y1 = dpf::rss::rss_mul_local(s1, xs1.own, xs1.next, ys1.own, ys1.next, 0); const auto y2 = dpf::rss::rss_mul_local(s2, xs2.own, xs2.next, ys2.own, ys2.next, 0); EXPECT_EQ(static_cast(y0 + y1 + y2), x * y); } TEST(ScheduleSession, PipelineCreditIndependentPads) { auto tape = std::make_shared>(); auto rounds = dpf::protocol::make_pad_rounds(2, 8, tape); EXPECT_EQ(rounds[0].round_phase, phase::setup); EXPECT_EQ(rounds[0].dir, round_dir::send_next); auto [a, b] = dpf::net::make_memory_sink_pair(1, {8, 8}); schedule_session s0(1, a, rounds, /*pipeline_credit=*/1); schedule_session s1(1, b, rounds, /*pipeline_credit=*/1); s0.submit(0); s1.submit(0); s0.drive(); s1.drive(); EXPECT_TRUE(s0.done(0)); EXPECT_TRUE(s1.done(0)); EXPECT_GT(s0.tally().setup_bytes_out, 0u); } TEST(Compose, BinNotLocalConvertible) { EXPECT_FALSE(dpf::protocol::detail::local_convertible(domain::bin, domain::a)); dpf::protocol::composer c(0); auto a = c.input(domain::a, 8); EXPECT_THROW(c.as(a, domain::bin), std::invalid_argument); } TEST(Compose, ArithKernelsPresent) { EXPECT_TRUE(dpf::protocol::detail::has_builtin_walk_kernel( dpf::protocol::opcodes::trunc_prob)); EXPECT_TRUE(dpf::protocol::detail::has_builtin_walk_kernel( dpf::protocol::opcodes::rss_zero)); EXPECT_TRUE(dpf::protocol::detail::has_builtin_walk_kernel( dpf::protocol::opcodes::bin_a2b)); EXPECT_TRUE(dpf::protocol::detail::has_builtin_walk_kernel( dpf::protocol::opcodes::share_cmp)); } TEST(Compose, RssZeroFromSeeds) { auto bundle = dpf::rss::sample_seed_bundle(); auto seeds = dpf::rss::party_seeds::from_bundle(bundle, 0); dpf::protocol::composer c(0); auto z = c.rss_zero_mask(domain::a, 8, 3); auto p = c.default_plan(); std::vector> values(p.nodes().size()); dpf::protocol::drive_options opt; opt.rss_seeds = &seeds; dpf::protocol::detail::run_arith_local(p, z.id, 0, 0, 1, values, opt); std::uint64_t got = 0; std::memcpy(&got, values[z.id].data(), 8); EXPECT_EQ(got, dpf::rss::zero_share(seeds, 3)); } TEST(Compose, CheckOpenRejectsCorrupt) { dpf::protocol::composer c(0); auto x = c.input(domain::a, 8); auto ex = c.exchange(x); auto p = c.default_plan(); auto slots = p.slot_bytes_all(); ASSERT_FALSE(slots.empty()); auto [sink0, sink1] = dpf::net::make_memory_sink_pair(1, slots); std::vector> v0(p.nodes().size()); std::vector> v1(p.nodes().size()); const std::uint64_t secret = 42; v0[x.id].assign(8, 0); v1[x.id].assign(8, 0); std::memcpy(v0[x.id].data(), &secret, 8); std::map> oracle; std::vector expect(8, 0); std::memcpy(expect.data(), &secret, 8); oracle[ex.id] = expect; dpf::protocol::drive_options opt; opt.cleartext = true; opt.check_open = true; opt.clear_oracle = &oracle; std::uint64_t bad = 7; std::memcpy(v1[x.id].data(), &bad, 8); std::map kernels; std::exception_ptr err; std::thread t0([&] { try { dpf::protocol::drive(p, sink0, v0, kernels, 0, opt); } catch (...) { err = std::current_exception(); } }); std::thread t1([&] { try { dpf::protocol::drive(p, sink1, v1, kernels, 1, opt); } catch (...) { err = std::current_exception(); } }); t0.join(); t1.join(); ASSERT_TRUE(err != nullptr); } // --------------------------------------------------------------------------- // Wave 2 // --------------------------------------------------------------------------- TEST(EdaBit, A2BThenReconstruct) { for (unsigned ell : {1u, 8u, 32u, 64u}) { const std::uint64_t x = 0x123456789abcdef0ull & ((ell >= 64) ? ~0ull : ((1ull << ell) - 1ull)); auto eda = dpf::edabit::sample_edabit_pair(ell); EXPECT_EQ(eda.p0.arith + eda.p1.arith, eda.clear_r); auto bits = dpf::edabit::a2b_gmw_pair(eda, x, std::uint64_t{0}); const auto got = dpf::edabit::reconstruct_bits(bits.first, bits.second, ell); EXPECT_EQ(got, x) << "ell=" << ell; } } TEST(EdaBit, RssArithEqualsBits) { auto bundle = dpf::rss::sample_seed_bundle(); auto all = dpf::edabit::sample_rss_all(bundle, 16, 0); EXPECT_EQ(all.p0.arith + all.p1.arith + all.p2.arith, all.clear_r); auto bit = [](const std::vector & p, unsigned i) { return static_cast((p[i / 8u] >> (i % 8u)) & 1u); }; std::uint64_t from_bits = 0; for (unsigned i = 0; i < 16; ++i) { EXPECT_EQ(bit(all.p0.bits_next, i), bit(all.p1.bits_packed, i)); EXPECT_EQ(bit(all.p1.bits_next, i), bit(all.p2.bits_packed, i)); EXPECT_EQ(bit(all.p2.bits_next, i), bit(all.p0.bits_packed, i)); const unsigned b = bit(all.p0.bits_packed, i) ^ bit(all.p1.bits_packed, i) ^ bit(all.p2.bits_packed, i); from_bits |= static_cast(b) << i; } EXPECT_EQ(from_bits, all.clear_r); } TEST(EdaBit, GmwA2bMatchesClear) { const std::uint64_t x0 = 30, x1 = 12; auto eda = dpf::edabit::sample_edabit_pair(8); auto bits = dpf::edabit::a2b_gmw_pair(eda, x0, x1); EXPECT_EQ(dpf::edabit::reconstruct_bits(bits.first, bits.second, 8), x0 + x1); } TEST(EdaBit, DaBitPairConsistent) { auto d = dpf::ot::sample_dabit_pair(); EXPECT_EQ(static_cast(d.p0.arith + d.p1.arith), static_cast((d.p0.bit ^ d.p1.bit) & 1u)); } TEST(BitInject, ClearMatches) { EXPECT_EQ(dpf::bit_inject::inject_clear(1, 0, 5, 7), 12u); EXPECT_EQ(dpf::bit_inject::inject_clear(1, 1, 5, 7), 0u); } TEST(BitInject, RssAndLocal) { auto bundle = dpf::rss::sample_seed_bundle(); auto s0 = dpf::rss::party_seeds::from_bundle(bundle, 0); auto s1 = dpf::rss::party_seeds::from_bundle(bundle, 1); auto s2 = dpf::rss::party_seeds::from_bundle(bundle, 2); const auto y0 = dpf::bit_inject::rss_and_local(s0, 1, 0, 1, 0, 0); const auto y1 = dpf::bit_inject::rss_and_local(s1, 0, 0, 0, 0, 0); const auto y2 = dpf::bit_inject::rss_and_local(s2, 0, 1, 0, 1, 0); EXPECT_EQ(static_cast(y0 ^ y1 ^ y2), 1u); } // --------------------------------------------------------------------------- // Wave 3 // --------------------------------------------------------------------------- TEST(Trunc, ProbErrorAtMostOne) { const std::uint64_t x0 = 100, x1 = 50; const auto got = dpf::trunc::trunc_prob_clear(x0, x1, 3); const auto exact = (x0 + x1) >> 3; EXPECT_LE(got > exact ? got - exact : exact - got, 1u); } TEST(Trunc, ExactPartyUsesDelta) { const std::uint64_t x = 0x12345; const std::uint64_t x0 = 0x1111, x1 = x - x0; auto prep = dpf::trunc::make_trunc_exact_prep(20, 4); auto [t0, t1] = dpf::trunc::trunc_exact_pair(x0, x1, prep); EXPECT_EQ(t0 + t1, dpf::trunc::trunc_exact_clear(x0, x1, 20, 4)); // Changing delta (via different x) changes the party-0 share. auto [u0, u1] = dpf::trunc::trunc_exact_pair(x0 + 16, x1, prep); EXPECT_NE(t0, u0); (void)u1; } TEST(Trunc, MulTruncClear) { auto z = dpf::trunc::mul_trunc_clear(3, 0, 5, 0, 0); EXPECT_EQ(z.z0 + z.z1, 15u); } TEST(ShareCmp, ReluMaxDivParty) { EXPECT_EQ(dpf::share_cmp::relu_clear(5, 0, 64), 5u); EXPECT_EQ(dpf::share_cmp::relu_clear(1ull << 63, 0, 64), 0u); auto [r0, r1] = dpf::share_cmp::relu_party_pair(5, 0, 64); EXPECT_EQ(r0 + r1, 5u); auto [m0, m1] = dpf::share_cmp::max_party_pair(3, 0, 9, 0, 64); EXPECT_EQ(m0 + m1, 9u); const std::uint64_t hi = 1ull << 63; auto prep = dpf::share_cmp::sample_msb_prep(64); auto gt_hi = dpf::share_cmp::gt_party_pair( hi + 5, 0, hi, 0, prep, 64); EXPECT_EQ(static_cast(gt_hi.first ^ gt_hi.second), 1u); auto gt_lo = dpf::share_cmp::gt_party_pair( 1, 0, hi, 0, prep, 64); EXPECT_EQ(static_cast(gt_lo.first ^ gt_lo.second), 0u); EXPECT_EQ(dpf::share_cmp::div_clear(20, 4), 5u); auto [q0, q1] = dpf::share_cmp::div_party_pair(20, 0, 4, 0, 16); EXPECT_EQ(q0 + q1, 5u); EXPECT_TRUE(dpf::share_cmp::range_ok_clear(15, 4)); } TEST(ShareCmp, Declassify) { auto [a, b] = dpf::share_cmp::share_input(42, 0); EXPECT_EQ(dpf::share_cmp::declassify(a, b), 42u); } // --------------------------------------------------------------------------- // Wave 4 // --------------------------------------------------------------------------- TEST(ShareVec, MulDeclassify) { std::vector clear = {1, 2, 3, 4}; auto [x0, x1] = dpf::share_vec::share(clear); auto [y0, y1] = dpf::share_vec::share(clear); auto [z0, z1] = dpf::share_vec::mul(x0, x1, y0, y1); auto prod = dpf::share_vec::declassify(z0, z1); for (std::size_t i = 0; i < clear.size(); ++i) EXPECT_EQ(prod[i], clear[i] * clear[i]); } TEST(FixedShare, MulViaTrunc) { using F = dpf::fixed<16, 16>; auto a = F::from_integer(3); auto b = F::from_integer(4); auto c = F::mul_clear(a, b); EXPECT_EQ(c.raw >> 16, 12u); std::vector ca = {a.raw}, cb = {b.raw}; auto [x0, x1] = dpf::share_vec::share(ca); auto [y0, y1] = dpf::share_vec::share(cb); auto [z0, z1] = dpf::fixed_mul_share<16, 16>(x0, x1, y0, y1); auto got = dpf::share_vec::declassify(z0, z1); EXPECT_EQ(got[0], c.raw); } TEST(ShareExpr, MulAddDeclassify) { const std::uint64_t x = 6, y = 7, w = 5; dpf::expr::recorder r0(0), r1(1); auto [x0, x1] = dpf::share_cmp::share_input(x, 0); auto [y0, y1] = dpf::share_cmp::share_input(y, 0); auto [w0, w1] = dpf::share_cmp::share_input(w, 0); auto hx0 = r0.bind(x0); auto hy0 = r0.bind(y0); auto hw0 = r0.bind(w0); auto hx1 = r1.bind(x1); auto hy1 = r1.bind(y1); auto hw1 = r1.bind(w1); auto pz0 = r0.mul(hx0, hy0); auto pz1 = r1.mul(hx1, hy1); dpf::expr::recorder::complete_mul(r0, r1, pz0, pz1); auto z0 = r0.add(pz0, hw0); auto z1 = r1.add(pz1, hw1); auto [s0, s1] = dpf::net::make_memory_sink_pair(1, {sizeof(std::uint64_t)}); dpf::net::edge_mesh m0; dpf::net::edge_mesh m1; m0.sinks.push_back(&s0); m1.sinks.push_back(&s1); r0.bind_mesh(std::move(m0)); r1.bind_mesh(std::move(m1)); EXPECT_EQ(dpf::expr::recorder::declassify(r0, r1, z0, z1), 47u); } TEST(ShareExpr, BandArity) { dpf::expr::recorder r0(0), r1(1); // XOR shares of bits 1,1,1 auto a0 = r0.bind(1); auto b0 = r0.bind(1); auto c0 = r0.bind(1); auto a1 = r1.bind(0); auto b1 = r1.bind(0); auto c1 = r1.bind(0); auto o0 = r0.band({a0, b0, c0}); auto o1 = r1.band({a1, b1, c1}); EXPECT_EQ(r0.last_and_arity(), 3u); dpf::expr::recorder::complete_band(r0, r1, o0, o1); EXPECT_EQ(static_cast( r0.local_value(o0) ^ r1.local_value(o1)), 1u); } TEST(Gilboa, MulFromOtMatchesClear) { auto packs = dpf::ot::sample_dealer_pair(16, 0, 16); const std::uint64_t x0 = 30, x1 = 12, y0 = 3, y1 = 4; auto prod = dpf::gilboa::mul_from_ot_pair(packs.first, packs.second, x0, x1, y0, y1, 16); EXPECT_EQ(prod.z0 + prod.z1, dpf::gilboa::mul_clear(x0, x1, y0, y1)); } TEST(Gilboa, FillTapeOtRepeatedWire) { dpf::beavers::session s; auto a = s.input(); auto out = s.product(a, a); s.pin(out); auto packs = dpf::ot::sample_dealer_pair(0, 0, 32); auto tapes = dpf::gilboa::fill_tape_ot_pair(s, packs.first, packs.second); ASSERT_FALSE(tapes.first.monomial.empty()); ASSERT_GT(tapes.first.lambda.size(), 0u); const auto lam = tapes.first.lambda[a.id()] + tapes.second.lambda[a.id()]; const auto mono = tapes.first.monomial[0] + tapes.second.monomial[0]; EXPECT_EQ(mono, lam * lam); } TEST(Gilboa, FillTapeDealer) { dpf::beavers::session s; auto a = s.input(); auto b = s.input(); auto out = s.product(a, b); s.pin(out); auto tape = dpf::gilboa::fill_tape_dealer(s, 0); EXPECT_FALSE(tape.lambda.empty()); } // --------------------------------------------------------------------------- // Wave 5 // --------------------------------------------------------------------------- TEST(Matmul, EightByEight) { dpf::matmul::dims d{8, 8, 8}; std::vector x(64), y(64); for (std::size_t i = 0; i < 64; ++i) { x[i] = i + 1; y[i] = (i % 8) + 1; } auto clear = dpf::matmul::clear_mul(x, y, d); auto [z0, z1] = dpf::matmul::mul_shared(x, y, d); for (std::size_t i = 0; i < clear.size(); ++i) EXPECT_EQ(z0[i] + z1[i], clear[i]); } TEST(Shuffle, CliqueSendNext) { auto bundle = dpf::rss::sample_seed_bundle(); std::vector v(8); std::iota(v.begin(), v.end(), 0); const std::size_t n = v.size(); dpf::shuffle::shuffle_party_view held[3]; for (auto & h : held) { h.own.assign(n, 0); h.next.assign(n, 0); } for (std::size_t i = 0; i < n; ++i) { const auto a = dpf::uniform_sample(); const auto b = dpf::uniform_sample(); const auto c = static_cast(v[i] - a - b); held[0].own[i] = a; held[0].next[i] = b; held[1].own[i] = b; held[1].next[i] = c; held[2].own[i] = c; held[2].next[i] = a; } dpf::shuffle::shuffle_party_view perm[3]; for (unsigned p = 0; p < 3; ++p) perm[p] = dpf::shuffle::shuffle_party(held[p], bundle, 0); auto clique = dpf::net::make_memory_clique(3, 1, {n * sizeof(std::uint64_t)}); auto send = [&](std::size_t from, std::size_t to, const std::vector & data) { std::vector buf(n * sizeof(std::uint64_t)); std::memcpy(buf.data(), data.data(), buf.size()); clique.end(from, to).submit(0, 0, buf.data(), buf.size()); clique.end(from, to).flush(); }; send(0, 1, perm[0].own); send(1, 2, perm[1].own); send(2, 0, perm[2].own); auto recv = [&](std::size_t me, std::size_t from) { auto & sink = clique.end(me, from); unsigned spins = 0; while (!sink.peer_ready(0, 0)) { sink.poll(); if (++spins > 100000u) throw std::runtime_error("shuffle peer"); } std::vector buf(n * sizeof(std::uint64_t)); sink.read_peer(0, 0, buf.data(), buf.size()); std::vector out(n); std::memcpy(out.data(), buf.data(), buf.size()); return out; }; auto r0 = dpf::shuffle::shuffle_refresh(perm[0].own, recv(0, 2)); auto r1 = dpf::shuffle::shuffle_refresh(perm[1].own, recv(1, 0)); auto r2 = dpf::shuffle::shuffle_refresh(perm[2].own, recv(2, 1)); std::vector opened(n); for (std::size_t i = 0; i < n; ++i) opened[i] = r0.own[i] + r1.own[i] + r2.own[i]; EXPECT_TRUE(dpf::shuffle::is_permutation_of(v, opened)); EXPECT_EQ(opened, dpf::shuffle::shuffle_clear(v, bundle, 0)); } TEST(Compose, CheckOpenHashRing) { const std::uint64_t word = 42; auto run = [](std::uint64_t w0, std::uint64_t w1, std::uint64_t w2) { auto clique = dpf::net::make_memory_clique(3, 1, {8}); std::uint8_t b0[8], b1[8], b2[8]; std::memcpy(b0, &w0, 8); std::memcpy(b1, &w1, 8); std::memcpy(b2, &w2, 8); std::exception_ptr err; std::thread t0([&] { try { dpf::protocol::exchange_check_hash_ring(clique.end(0, 1), clique.end(0, 2), b0, 8); } catch (...) { err = std::current_exception(); } }); std::thread t1([&] { try { dpf::protocol::exchange_check_hash_ring(clique.end(1, 2), clique.end(1, 0), b1, 8); } catch (...) { err = std::current_exception(); } }); std::thread t2([&] { try { dpf::protocol::exchange_check_hash_ring(clique.end(2, 0), clique.end(2, 1), b2, 8); } catch (...) { err = std::current_exception(); } }); t0.join(); t1.join(); t2.join(); return err; }; EXPECT_TRUE(run(word, word, word) == nullptr); EXPECT_TRUE(run(word, word ^ 1u, word) != nullptr); } TEST(Compose, TruncExactKernel) { dpf::protocol::composer c(0); auto x = c.input(domain::a, 8); auto r = c.input(domain::a, 8); auto delta = c.input(domain::a, 8); auto wrap = c.input(domain::a, 8); auto out = c.trunc_exact_open(x, r, delta, wrap, 4); auto p = c.default_plan(); std::vector> values(p.nodes().size()); auto put = [&](dpf::protocol::node n, std::uint64_t v) { values[n.id].assign(8, 0); std::memcpy(values[n.id].data(), &v, 8); }; put(x, 0x20); put(r, 0x5); put(delta, 0x10); put(wrap, 1); dpf::protocol::drive_options opt; dpf::protocol::detail::run_arith_local(p, out.id, 0, 0, 1, values, opt); std::uint64_t got = 0; std::memcpy(&got, values[out.id].data(), 8); const auto expect = dpf::trunc::trunc_exact_party( 0x20, 0x5, 0x10, 1, 4, 0, 0); EXPECT_EQ(got, expect); } TEST(Compose, B2aOpenMatchesClear) { constexpr unsigned ell = 8; const std::uint64_t x = 0x3c; dpf::protocol::composer c(0); auto bits = c.input(domain::bin, 1); auto rbits = c.input(domain::bin, 1); auto rarith = c.input(domain::a, ell * 8); auto out = c.bin_b2a(bits, rbits, rarith, ell); auto p = c.default_plan(); std::vector> v0(p.nodes().size()); std::vector> v1(p.nodes().size()); std::uint8_t b0 = static_cast(x), b1 = 0, rb0 = 0, rb1 = 0; std::uint64_t a0[ell]{}, a1[ell]{}; for (unsigned i = 0; i < ell; ++i) { auto d = dpf::ot::sample_dabit_pair(); if (d.p0.bit) rb0 = static_cast(rb0 | (1u << i)); if (d.p1.bit) rb1 = static_cast(rb1 | (1u << i)); a0[i] = d.p0.arith; a1[i] = d.p1.arith; } put_raw(v0, bits, &b0, 1); put_raw(v1, bits, &b1, 1); put_raw(v0, rbits, &rb0, 1); put_raw(v1, rbits, &rb1, 1); put_raw(v0, rarith, a0, sizeof a0); put_raw(v1, rarith, a1, sizeof a1); drive_both(p, v0, v1); std::uint64_t z0 = 0, z1 = 0; std::memcpy(&z0, v0[out.id].data(), 8); std::memcpy(&z1, v1[out.id].data(), 8); EXPECT_EQ(z0 + z1, x); } TEST(Compose, MulTruncOpen) { const std::uint64_t x = 6, y = 7; const std::uint64_t x0 = 100, x1 = x - 100, y0 = 9, y1 = y - 9; const std::uint64_t a0 = 3, a1 = 5, b0 = 8, b1 = 1; const std::uint64_t c = (a0 + a1) * (b0 + b1); const std::uint64_t c0 = 20, c1 = c - 20; dpf::protocol::composer comp(0); auto nx = comp.input(domain::a, 8); auto ny = comp.input(domain::a, 8); auto na = comp.input(domain::a, 8); auto nb = comp.input(domain::a, 8); auto nc = comp.input(domain::a, 8); auto out = comp.mul_trunc_open(nx, ny, na, nb, nc, 0); auto p = comp.default_plan(); std::vector> v0(p.nodes().size()); std::vector> v1(p.nodes().size()); put_raw(v0, nx, &x0, 8); put_raw(v1, nx, &x1, 8); put_raw(v0, ny, &y0, 8); put_raw(v1, ny, &y1, 8); put_raw(v0, na, &a0, 8); put_raw(v1, na, &a1, 8); put_raw(v0, nb, &b0, 8); put_raw(v1, nb, &b1, 8); put_raw(v0, nc, &c0, 8); put_raw(v1, nc, &c1, 8); drive_both(p, v0, v1); std::uint64_t z0 = 0, z1 = 0; std::memcpy(&z0, v0[out.id].data(), 8); std::memcpy(&z1, v1[out.id].data(), 8); EXPECT_EQ(z0 + z1, x * y); } TEST(Compose, InjectAndMuxOpen) { auto tp = dpf::ot::sample_bit_ring_triple_pair(); dpf::protocol::composer c(0); auto bit = c.input(domain::a, 8); auto x = c.input(domain::a, 8); auto a = c.input(domain::a, 8); auto b = c.input(domain::a, 8); auto cc = c.input(domain::a, 8); auto prod = c.bin_inject(bit, x, a, b, cc); auto plan = c.default_plan(); std::vector> v0(plan.nodes().size()); std::vector> v1(plan.nodes().size()); const std::uint64_t one = 1, zero = 0; const std::uint64_t x0 = 20, x1 = 22; const std::uint64_t a0 = tp.p0.a, a1 = tp.p1.a; put_raw(v0, bit, &one, 8); put_raw(v1, bit, &zero, 8); put_raw(v0, x, &x0, 8); put_raw(v1, x, &x1, 8); put_raw(v0, a, &a0, 8); put_raw(v1, a, &a1, 8); put_raw(v0, b, &tp.p0.b, 8); put_raw(v1, b, &tp.p1.b, 8); put_raw(v0, cc, &tp.p0.c, 8); put_raw(v1, cc, &tp.p1.c, 8); drive_both(plan, v0, v1); std::uint64_t z0 = 0, z1 = 0; std::memcpy(&z0, v0[prod.id].data(), 8); std::memcpy(&z1, v1[prod.id].data(), 8); EXPECT_EQ(z0 + z1, x0 + x1); } TEST(Compose, GmwAndOpen) { auto tp = dpf::ot::sample_bit_triple_pair(); dpf::protocol::composer c(0); auto p0 = c.input(domain::bin, 1); auto q0 = c.input(domain::bin, 1); auto a = c.input(domain::bin, 1); auto b = c.input(domain::bin, 1); auto cc = c.input(domain::bin, 1); auto out = c.gmw_and(p0, q0, a, b, cc); auto plan = c.default_plan(); std::vector> v0(plan.nodes().size()); std::vector> v1(plan.nodes().size()); const std::uint8_t one = 1, zero = 0; put_raw(v0, p0, &one, 1); put_raw(v1, p0, &zero, 1); put_raw(v0, q0, &one, 1); put_raw(v1, q0, &zero, 1); put_raw(v0, a, &tp.p0.a, 1); put_raw(v1, a, &tp.p1.a, 1); put_raw(v0, b, &tp.p0.b, 1); put_raw(v1, b, &tp.p1.b, 1); put_raw(v0, cc, &tp.p0.c, 1); put_raw(v1, cc, &tp.p1.c, 1); drive_both(plan, v0, v1); EXPECT_EQ(static_cast(v0[out.id][0] ^ v1[out.id][0]), 1u); } TEST(Gilboa, FillTapeOtReadsStash) { dpf::beavers::session s; auto a = s.input(); auto out = s.product(a, a); s.pin(out); auto packs = dpf::ot::sample_dealer_pair(0, 0, 32); auto tapes = dpf::gilboa::fill_tape_ot_pair(s, packs.first, packs.second); auto again = dpf::gilboa::fill_tape_ot(s, packs.first, 0); EXPECT_EQ(again.lambda, tapes.first.lambda); EXPECT_EQ(again.monomial, tapes.first.monomial); dpf::ot::pack bare; EXPECT_THROW(dpf::gilboa::fill_tape_ot(s, bare, 0), std::runtime_error); } TEST(BitInject, RssAndRefresh) { auto bundle = dpf::rss::sample_seed_bundle(); auto s0 = dpf::rss::party_seeds::from_bundle(bundle, 0); auto s1 = dpf::rss::party_seeds::from_bundle(bundle, 1); auto s2 = dpf::rss::party_seeds::from_bundle(bundle, 2); const auto e0 = dpf::bit_inject::rss_and_local(s0, 1, 0, 1, 0, 0); const auto e1 = dpf::bit_inject::rss_and_local(s1, 0, 0, 0, 0, 0); const auto e2 = dpf::bit_inject::rss_and_local(s2, 0, 1, 0, 1, 0); auto clique = dpf::net::make_memory_clique(3, 1, {1}); auto send = [&](std::size_t from, std::size_t to, std::uint8_t byte) { clique.end(from, to).submit(0, 0, &byte, 1); clique.end(from, to).flush(); }; send(0, 1, e0); send(1, 2, e1); send(2, 0, e2); auto recv = [&](std::size_t me, std::size_t from) { auto & sink = clique.end(me, from); unsigned spins = 0; while (!sink.peer_ready(0, 0)) { sink.poll(); if (++spins > 100000u) throw std::runtime_error("rss and peer"); } std::uint8_t b = 0; sink.read_peer(0, 0, &b, 1); return b; }; EXPECT_EQ(recv(0, 2), e2); EXPECT_EQ(recv(1, 0), e0); EXPECT_EQ(recv(2, 1), e1); EXPECT_EQ(static_cast(e0 ^ e1 ^ e2), 1u); } TEST(Shuffle, SelectSumCountArgmax) { std::vector pred = {1, 0, 1, 1}; std::vector row = {10, 20, 30, 40}; EXPECT_EQ(dpf::shuffle::sum_if_clear(pred, row), 80u); EXPECT_EQ(dpf::shuffle::count_if_clear(pred), 3u); auto am = dpf::shuffle::argmax_clear(row, 64); EXPECT_EQ(am.value, 40u); EXPECT_EQ(am.one_hot[3], 1); } TEST(Circuit, MulOpenFromDealerFile) { dpf::mpc::circuit c(2); auto x = c.input(); auto y = c.input(); auto prod = c.mul(x, y); auto opened = c.open(prod); auto views = dpf::prep::deal_views(c.prep()); const auto path0 = std::string{"/tmp/libdpf-prep-p0.bin"}; const auto path1 = std::string{"/tmp/libdpf-prep-p1.bin"}; dpf::prep::write_file(path0, views.first); dpf::prep::write_file(path1, views.second); auto loaded0 = dpf::prep::read_file(path0); auto loaded1 = dpf::prep::read_file(path1); EXPECT_EQ(loaded0, views.first); EXPECT_EQ(loaded1, views.second); std::uint64_t got[2]{}; dpf::run::threads_2(c.slot_bytes(), [&](unsigned party, dpf::net::RoundSink & sink) { dpf::mpc::party me(c, party); me.bind(x, party == 0 ? 6 : 0); me.bind(y, party == 0 ? 7 : 0); auto bytes = party == 0 ? loaded0 : loaded1; me.run(sink, dpf::prep::cursor(std::move(bytes))); got[party] = me.read(opened); }); EXPECT_EQ(got[0], 42u); EXPECT_EQ(got[1], 42u); } TEST(Circuit, PrivateInputAndTcpPrep) { dpf::mpc::circuit c(8); auto secret = c.priv_input(0); auto opened = c.open(secret); std::atomic port{0}; std::vector view0, view1; std::thread listener([&] { dpf::run::tcp_pair(0, "127.0.0.1", port, [&](dpf::net::channel & ch) { view0 = dpf::prep::setup_2pc_sampled(ch, 0, c.prep()); }); }); while (port.load() == 0) std::this_thread::yield(); std::thread dialer([&] { dpf::run::tcp_pair(1, "127.0.0.1", port, [&](dpf::net::channel & ch) { view1 = dpf::prep::setup_2pc_sampled(ch, 1, c.prep()); }); }); listener.join(); dialer.join(); EXPECT_FALSE(view0.empty()); EXPECT_FALSE(view1.empty()); std::uint64_t got[2]{}; dpf::run::threads_2(c.slot_bytes(), [&](unsigned party, dpf::net::RoundSink & sink) { dpf::mpc::party me(c, party); if (party == 0) me.bind_priv(secret, 42); auto bytes = party == 0 ? view0 : view1; me.run(sink, dpf::prep::cursor(std::move(bytes))); got[party] = me.read(opened); }); EXPECT_EQ(got[0], 42u); EXPECT_EQ(got[1], 42u); EXPECT_GE(dpf::revealing::known_count, 2u); } TEST(Run, ThreePartyDeclassify) { const std::uint64_t shares[3] = {10, 20, 12}; std::uint64_t opened[3]{}; dpf::run::threads_on_clique(3, {8}, [&](unsigned me, dpf::net::memory_clique & clique) { std::uint8_t mine[8]{}, sum[8]{}; std::memcpy(mine, &shares[me], 8); dpf::run::declassify_ring(clique, me, mine, 8, sum); std::memcpy(&opened[me], sum, 8); }); EXPECT_EQ(opened[0], 42u); EXPECT_EQ(opened[1], 42u); EXPECT_EQ(opened[2], 42u); } TEST(CostPass, EmptyPlanNoSynthetic) { dpf::protocol::composer c(0); auto p = c.default_plan(); auto r_rounds = dpf::cost::annotate(p, dpf::beavers::schedule_objective::rounds); auto r_prep = dpf::cost::annotate(p, dpf::beavers::schedule_objective::prep); EXPECT_TRUE(r_rounds.choices.empty()); EXPECT_TRUE(r_prep.choices.empty()); } TEST(CostPass, RealCmpGetsChoice) { dpf::protocol::composer c(0); auto x = c.input(domain::a, 8); auto y = c.input(domain::a, 8); (void)c.share_gt(x, y); auto p = c.default_plan(); auto r = dpf::cost::annotate(p, dpf::beavers::schedule_objective::rounds); ASSERT_FALSE(r.choices.empty()); EXPECT_EQ(r.choices[0].pick, dpf::cost::strategy::dcf_mask); } TEST(StreamArray, MemoryDealerRingTriple) { auto w0 = dpf::net::make_memory_stream_pair(1); auto w1 = dpf::net::make_memory_stream_pair(1); dpf::factory::make_dealer(w0.first, w1.first, 1, [] { return dpf::factory::deal_ring_triple(2); }); auto v0 = dpf::factory::detail::read_pod( w0.second, 0); auto v1 = dpf::factory::detail::read_pod( w1.second, 0); std::uint64_t a0 = 0, b0 = 0, c0 = 0, a1 = 0, b1 = 0, c1 = 0; std::memcpy(&a0, v0.a, 2); std::memcpy(&b0, v0.b, 2); std::memcpy(&c0, v0.c, 2); std::memcpy(&a1, v1.a, 2); std::memcpy(&b1, v1.b, 2); std::memcpy(&c1, v1.c, 2); const std::uint64_t mask = 0xffffu; EXPECT_EQ(((a0 + a1) * (b0 + b1)) & mask, (c0 + c1) & mask); } TEST(StreamArray, FileDealerRoundTrip) { const std::string base = "/tmp/libdpf-stream-arr"; { dpf::net::file_stream_array out0(base + "-p0", 1, true); dpf::net::file_stream_array out1(base + "-p1", 1, true); dpf::factory::make_dealer(out0, out1, 1, [] { return dpf::factory::deal_ring_triple(2); }); } dpf::net::file_stream_array in0(base + "-p0", 1, false); dpf::net::file_stream_array in1(base + "-p1", 1, false); auto v0 = dpf::factory::detail::read_pod( in0, 0); auto v1 = dpf::factory::detail::read_pod( in1, 0); std::uint64_t a0 = 0, b0 = 0, c0 = 0, a1 = 0, b1 = 0, c1 = 0; std::memcpy(&a0, v0.a, 2); std::memcpy(&b0, v0.b, 2); std::memcpy(&c0, v0.c, 2); std::memcpy(&a1, v1.a, 2); std::memcpy(&b1, v1.b, 2); std::memcpy(&c1, v1.c, 2); EXPECT_EQ(((a0 + a1) * (b0 + b1)) & 0xffffu, (c0 + c1) & 0xffffu); } TEST(StreamArray, MuxLocalhostDealer) { std::atomic port{0}; dpf::factory::ring_triple_view got0{}, got1{}; std::thread t0([&] { dpf::run::tcp_pair_mux(0, "127.0.0.1", port, 1, [&](unsigned, dpf::net::mux_stream_array & mux) { // Party 0 acts as dealer write end toward party 1, and also // keeps a local memory view for itself. auto local = dpf::net::make_memory_stream_pair(1); dpf::factory::make_dealer(local.first, mux, 1, [] { return dpf::factory::deal_ring_triple(2); }); got0 = dpf::factory::detail::read_pod< dpf::factory::ring_triple_view>(local.second, 0); }); }); while (port.load() == 0) std::this_thread::yield(); std::thread t1([&] { dpf::run::tcp_pair_mux(1, "127.0.0.1", port, 1, [&](unsigned, dpf::net::mux_stream_array & mux) { got1 = dpf::factory::detail::read_pod< dpf::factory::ring_triple_view>(mux, 0); }); }); t0.join(); t1.join(); std::uint64_t a0 = 0, b0 = 0, c0 = 0, a1 = 0, b1 = 0, c1 = 0; std::memcpy(&a0, got0.a, 2); std::memcpy(&b0, got0.b, 2); std::memcpy(&c0, got0.c, 2); std::memcpy(&a1, got1.a, 2); std::memcpy(&b1, got1.b, 2); std::memcpy(&c1, got1.c, 2); EXPECT_EQ(((a0 + a1) * (b0 + b1)) & 0xffffu, (c0 + c1) & 0xffffu); } TEST(Factory, GmwAndRoundMemory) { auto peer = dpf::net::make_memory_stream_pair(1); auto d0 = dpf::net::make_memory_stream_pair(1); auto d1 = dpf::net::make_memory_stream_pair(1); dpf::factory::make_dealer(d0.first, d1.first, 1, dpf::factory::make_gmw_and_dealer_functor()); std::uint8_t z0 = 0, z1 = 0; auto fac0 = dpf::factory::make_protocol_factory( dpf::factory::gmw_and_round0{}, dpf::factory::gmw_and_round1{0}); auto fac1 = dpf::factory::make_protocol_factory( dpf::factory::gmw_and_round0{}, dpf::factory::gmw_and_round1{1}); std::thread t0([&] { auto p = fac0.create(1, peer.first, d0.second); z0 = p(0, std::pair{1, 1}); }); std::thread t1([&] { auto p = fac1.create(1, peer.second, d1.second); z1 = p(0, std::pair{0, 0}); }); t0.join(); t1.join(); EXPECT_EQ(static_cast(z0 ^ z1), 1u); } TEST(Circuit, MulOpenFromStreamArray) { dpf::mpc::circuit c(2); auto x = c.input(); auto y = c.input(); auto prod = c.mul(x, y); auto opened = c.open(prod); auto views = dpf::prep::deal_views(c.prep()); auto w0 = dpf::net::make_memory_stream_pair(1); auto w1 = dpf::net::make_memory_stream_pair(1); w0.first.write(0, views.first.data(), views.first.size()); w0.first.flush(0); w1.first.write(0, views.second.data(), views.second.size()); w1.first.flush(0); const std::size_t nbytes = views.first.size(); std::uint64_t got[2]{}; dpf::run::threads_2(c.slot_bytes(), [&](unsigned party, dpf::net::RoundSink & sink) { dpf::mpc::party me(c, party); me.bind(x, party == 0 ? 6 : 0); me.bind(y, party == 0 ? 7 : 0); auto & dealer = party == 0 ? w0.second : w1.second; me.run(sink, dealer, nbytes); got[party] = me.read(opened); }); EXPECT_EQ(got[0], 42u); EXPECT_EQ(got[1], 42u); } TEST(StreamArray, MultiRoundSinkExchange) { constexpr std::size_t slot = 8; dpf::run::threads_2_streams(2, [&](unsigned party, dpf::net::stream_array & sa) { dpf::net::stream_array_sink sink(sa, std::vector{slot, slot}); const std::uint64_t mine = party == 0 ? 11u : 22u; std::uint8_t buf[slot]{}; std::memcpy(buf, &mine, sizeof(mine)); sink.submit(0, 0, buf, slot); sink.flush_round(0); EXPECT_TRUE(sink.peer_ready(0, 0)); std::uint8_t peer0[slot]{}; sink.read_peer(0, 0, peer0, slot); const std::uint64_t other0 = party == 0 ? 22u : 11u; std::uint64_t got0 = 0; std::memcpy(&got0, peer0, slot); EXPECT_EQ(got0, other0); const std::uint64_t mine1 = mine + 1u; std::memcpy(buf, &mine1, sizeof(mine1)); sink.submit(1, 0, buf, slot); sink.flush_round(1); std::uint8_t peer1[slot]{}; sink.read_peer(1, 0, peer1, slot); std::uint64_t got1 = 0; std::memcpy(&got1, peer1, slot); EXPECT_EQ(got1, other0 + 1u); }); } TEST(Run, ThreePartyDeclassifyStreamClique) { const std::uint64_t shares[3] = {10, 20, 12}; std::uint64_t opened[3]{}; auto clique = dpf::net::make_memory_stream_clique(3, 1); std::exception_ptr err; std::mutex mu; std::vector ts; for (unsigned me = 0; me < 3; ++me) { ts.emplace_back([&, me] { try { std::uint8_t mine[8]{}, sum[8]{}; std::memcpy(mine, &shares[me], 8); dpf::run::declassify_streams(clique, me, mine, 8, sum); std::memcpy(&opened[me], sum, 8); } catch (...) { std::lock_guard lock(mu); if (!err) err = std::current_exception(); } }); } for (auto & t : ts) t.join(); if (err) std::rethrow_exception(err); EXPECT_EQ(opened[0], 42u); EXPECT_EQ(opened[1], 42u); EXPECT_EQ(opened[2], 42u); } TEST(Factory, BeaverMulStreamMemory) { auto peer = dpf::net::make_memory_stream_pair(2); auto d0 = dpf::net::make_memory_stream_pair(1); auto d1 = dpf::net::make_memory_stream_pair(1); dpf::factory::make_dealer(d0.first, d1.first, 1, dpf::factory::make_beaver_mul_dealer_functor()); const std::uint64_t x0 = 6, x1 = 0, y0 = 7, y1 = 0; std::uint64_t z0 = 0, z1 = 0; auto fac = dpf::factory::make_beaver_mul_factory( dpf::factory::beaver_mul_round0{8}, dpf::factory::beaver_mul_round1{8, 0}); auto fac1 = dpf::factory::make_beaver_mul_factory( dpf::factory::beaver_mul_round0{8}, dpf::factory::beaver_mul_round1{8, 1}); std::thread t0([&] { auto p = fac.create(1, peer.first, d0.second); z0 = p(0, dpf::factory::beaver_mul_input{x0, y0}); }); std::thread t1([&] { auto p = fac1.create(1, peer.second, d1.second); z1 = p(0, dpf::factory::beaver_mul_input{x1, y1}); }); t0.join(); t1.join(); EXPECT_EQ(z0 + z1, 42u); } TEST(Circuit, RecordsGmwAndA2bGtTruncMux) { dpf::mpc::circuit c(8); auto x = c.input(); auto y = c.input(); auto b0 = c.input(); auto b1 = c.input(); (void)c.gmw_and(b0, b1); (void)c.a2b(x, 8); (void)c.gt(x, y, 8); (void)c.trunc_exact(x, 16, 4); (void)c.mux(b0, x, y); EXPECT_GE(c.prep().bit_triples, 1u); EXPECT_GE(c.prep().ring_triples, 1u); EXPECT_GE(c.prep().dabits, 1u); EXPECT_FALSE(c.slot_bytes().empty()); } TEST(Factory, MuxOnlineStreamMemory) { auto peer = dpf::net::make_memory_stream_pair(2); auto d0 = dpf::net::make_memory_stream_pair(1); auto d1 = dpf::net::make_memory_stream_pair(1); dpf::factory::make_dealer(d0.first, d1.first, 1, [] { return dpf::factory::deal_ring_triple(8); }); std::uint64_t z0 = 0, z1 = 0; std::thread t0([&] { z0 = dpf::factory::mux_online(0, 1, 40, 2, peer.first, d0.second); }); std::thread t1([&] { z1 = dpf::factory::mux_online(1, 0, 0, 0, peer.second, d1.second); }); t0.join(); t1.join(); EXPECT_EQ(z0 + z1, 40u); } namespace { std::uint64_t xor_bit_shares(std::uint64_t s0, std::uint64_t s1, unsigned width) { const std::uint64_t mask = width >= 64 ? ~std::uint64_t{0} : ((std::uint64_t{1} << width) - 1u); return (s0 ^ s1) & mask; } } // namespace TEST(Factory, A2bTapeOnline) { constexpr unsigned width = 8; auto peer = dpf::net::make_memory_stream_pair(2); auto d0 = dpf::net::make_memory_stream_pair(2); auto d1 = dpf::net::make_memory_stream_pair(2); dpf::factory::deal_a2b_tape(width, 8, d0.first, d1.first); const std::uint64_t x0 = 30, x1 = 12; std::uint64_t b0 = 0, b1 = 0; std::thread t0([&] { b0 = dpf::factory::a2b_online(0, x0, width, peer.first, d0.second); }); std::thread t1([&] { b1 = dpf::factory::a2b_online(1, x1, width, peer.second, d1.second); }); t0.join(); t1.join(); EXPECT_EQ(xor_bit_shares(b0, b1, width), x0 + x1); } TEST(Factory, TruncTapeOnline) { constexpr unsigned n = 20, s = 4; auto peer = dpf::net::make_memory_stream_pair(2); auto d0 = dpf::net::make_memory_stream_pair(2); auto d1 = dpf::net::make_memory_stream_pair(2); dpf::factory::deal_trunc_tape(n, s, 8, d0.first, d1.first); const std::uint64_t x = 0xabcde; const std::uint64_t x0 = 0x11111, x1 = x - x0; std::uint64_t t0 = 0, t1 = 0; std::thread t0th([&] { t0 = dpf::factory::trunc_exact_online(0, x0, n, s, peer.first, d0.second); }); std::thread t1th([&] { t1 = dpf::factory::trunc_exact_online(1, x1, n, s, peer.second, d1.second); }); t0th.join(); t1th.join(); EXPECT_EQ(t0 + t1, dpf::trunc::trunc_exact_clear(x0, x1, n, s)); } TEST(Factory, GtTapeOnline) { constexpr unsigned width = 8; auto peer = dpf::net::make_memory_stream_pair(5); auto d0 = dpf::net::make_memory_stream_pair(5); auto d1 = dpf::net::make_memory_stream_pair(5); dpf::factory::deal_gt_tape(width, 8, d0.first, d1.first); const std::uint64_t x0 = 50, x1 = 7, y0 = 10, y1 = 20; std::uint8_t g0 = 0, g1 = 0; std::thread t0([&] { g0 = dpf::factory::gt_online(0, x0, y0, width, peer.first, d0.second); }); std::thread t1([&] { g1 = dpf::factory::gt_online(1, x1, y1, width, peer.second, d1.second); }); t0.join(); t1.join(); const std::uint64_t x = x0 + x1; const std::uint64_t y = y0 + y1; const unsigned expect = x > y ? 1u : 0u; EXPECT_EQ(static_cast(g0 ^ g1), expect); } TEST(StreamArray, DealerCursorFromStreamPrg) { constexpr std::size_t N = 8; auto d0 = dpf::net::make_memory_stream_pair(1); auto d1 = dpf::net::make_memory_stream_pair(1); using Prg = dpf::randomness::aes_buffered_prg; const auto seed = dpf::randomness::sample_master_seed(); Prg prg0(seed); Prg prg1(seed); dpf::factory::make_dealer_prg(d0.first, d1.first, N, prg0, prg1, [](Prg & p0, Prg & /*p1*/, std::size_t j) { auto t = dpf::factory::deal_bit_triple(); dpf::factory::gmw_and_blind v0{t.first.a, t.first.b, t.first.c}; dpf::factory::gmw_and_blind v1{t.second.a, t.second.b, t.second.c}; (void)p0.at<0>(static_cast(j)); return std::pair{v0, v1}; }); dpf::net::stream_dealer_cursor cur0(d0.second, N, {sizeof(dpf::factory::gmw_and_blind)}); dpf::net::stream_dealer_cursor cur1(d1.second, N, {sizeof(dpf::factory::gmw_and_blind)}); for (std::size_t i = 0; i < N; ++i) { const auto v0 = cur0.at(0, i); const auto v1 = cur1.at(0, i); const unsigned c = static_cast((v0.c ^ v1.c) & 1u); const unsigned ab = static_cast( ((v0.a ^ v1.a) & 1u) & ((v0.b ^ v1.b) & 1u)); EXPECT_EQ(c, ab); } } TEST(Factory, ScheduleOnStreams) { auto peer = dpf::net::make_memory_stream_pair(1); std::vector local0(4), local1(4); for (std::size_t i = 0; i < 4; ++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; }; std::uint64_t sum0 = 0, sum1 = 0; std::thread t0([&] { auto s = dpf::protocol::make_owning_schedule_on_streams(4, peer.first, make_rounds(local0)); for (std::size_t i = 0; i < 4; ++i) s.submit(i); s.drive(); for (std::size_t i = 0; i < 4; ++i) { std::uint64_t v = 0; s.sink->read_peer(0, i, reinterpret_cast(&v), sizeof(v)); sum0 += v; } }); std::thread t1([&] { auto s = dpf::protocol::make_owning_schedule_on_streams(4, peer.second, make_rounds(local1)); for (std::size_t i = 0; i < 4; ++i) s.submit(i); s.drive(); for (std::size_t i = 0; i < 4; ++i) { std::uint64_t v = 0; s.sink->read_peer(0, i, reinterpret_cast(&v), sizeof(v)); sum1 += v; } }); t0.join(); t1.join(); EXPECT_EQ(sum0, 100u + 101u + 102u + 103u); EXPECT_EQ(sum1, 10u + 11u + 12u + 13u); } TEST(Roles, DealerWriteFilesRoundTrip) { const std::string base = "/tmp/libdpf-roles-prep"; dpf::prep::demand d; d.limb = 2; d.ring_triples = 1; dpf::roles::dealer_write_files(base, d); auto curs = dpf::roles::online_open_file_array(base); std::uint8_t a[8]{}, b[8]{}, c[8]{}; curs.first.take_ring(a, b, c); curs.second.take_ring(a, b, c); EXPECT_EQ(curs.first.limb(), 2); EXPECT_EQ(curs.second.limb(), 2); } TEST(Compose, DrivePlanOnStreamsOpen) { dpf::protocol::composer c0(0); dpf::protocol::composer c1(1); auto x0 = c0.input(domain::a, 8); auto x1 = c1.input(domain::a, 8); auto e0 = c0.exchange(x0); auto e1 = c1.exchange(x1); auto p0 = c0.schedule(); auto p1 = c1.schedule(); std::vector> v0(p0.nodes().size()), v1(p1.nodes().size()); const std::uint64_t a = 3, b = 5; put_raw(v0, x0, &a, 8); put_raw(v1, x1, &b, 8); dpf::protocol::drive_both_on_streams(p0, p1, v0, v1); std::uint64_t open0 = 0, open1 = 0; std::memcpy(&open0, v0[e0.id].data(), 8); std::memcpy(&open1, v1[e1.id].data(), 8); EXPECT_EQ(open0, 8u); EXPECT_EQ(open1, 8u); } TEST(Compose, DrivePlanOnStreamsTwoWaves) { constexpr std::uint32_t k_step = 91001; auto make = [](std::size_t party, std::uint64_t in) { dpf::protocol::composer c(party); auto x = c.input(domain::a, 8); auto e0 = c.exchange(x); auto y = c.compute(k_step, {e0}, domain::a, 8); auto e1 = c.exchange(y); return std::make_tuple(c.schedule(), x, e1, in); }; auto [p0, x0, out0, in0] = make(0, 11); auto [p1, x1, out1, in1] = make(1, 19); ASSERT_EQ(p0.rounds(), 2u); dpf::protocol::kernel_fn step = [](std::uint32_t, const std::vector &, const std::vector & inputs, dpf::protocol::block_span output, std::size_t) { std::uint64_t v = 0; std::memcpy(&v, inputs[0].at(0), 8); v += 1; std::memcpy(output.at(0), &v, 8); }; std::map k{{k_step, step}}; std::vector> v0(p0.nodes().size()), v1(p1.nodes().size()); put_raw(v0, x0, &in0, 8); put_raw(v1, x1, &in1, 8); dpf::protocol::drive_both_on_streams(p0, p1, v0, v1, k); std::uint64_t o0 = 0, o1 = 0; std::memcpy(&o0, v0[out0.id].data(), 8); std::memcpy(&o1, v1[out1.id].data(), 8); EXPECT_EQ(o0, 62u); EXPECT_EQ(o1, 62u); } TEST(Compose, DrivePlanOnStreamsGmwAnd) { auto tp = dpf::ot::sample_bit_triple_pair(); dpf::protocol::composer c(0); auto p0 = c.input(domain::bin, 1); auto q0 = c.input(domain::bin, 1); auto a = c.input(domain::bin, 1); auto b = c.input(domain::bin, 1); auto cc = c.input(domain::bin, 1); auto out = c.gmw_and(p0, q0, a, b, cc); auto plan = c.default_plan(); std::vector> v0(plan.nodes().size()); std::vector> v1(plan.nodes().size()); const std::uint8_t one = 1, zero = 0; put_raw(v0, p0, &one, 1); put_raw(v1, p0, &zero, 1); put_raw(v0, q0, &one, 1); put_raw(v1, q0, &zero, 1); put_raw(v0, a, &tp.p0.a, 1); put_raw(v1, a, &tp.p1.a, 1); put_raw(v0, b, &tp.p0.b, 1); put_raw(v1, b, &tp.p1.b, 1); put_raw(v0, cc, &tp.p0.c, 1); put_raw(v1, cc, &tp.p1.c, 1); dpf::protocol::drive_both_on_streams(plan, v0, v1); EXPECT_EQ(static_cast(v0[out.id][0] ^ v1[out.id][0]), 1u); } TEST(Compose, DrivePlanOnStreamsDeepEightWaves) { // Gap-1 stress: eight successive opens on stream arrays (FSS-depth shape). auto make = [](std::size_t party, std::uint64_t in) { dpf::protocol::composer c(party); auto x = c.input(domain::a, 8); dpf::protocol::node last = x; for (int i = 0; i < 8; ++i) last = c.exchange(last); return std::make_tuple(c.schedule(), x, last, in); }; auto [p0, x0, out0, in0] = make(0, 1); auto [p1, x1, out1, in1] = make(1, 2); ASSERT_EQ(p0.rounds(), 8u); std::vector> v0(p0.nodes().size()), v1(p1.nodes().size()); put_raw(v0, x0, &in0, 8); put_raw(v1, x1, &in1, 8); dpf::protocol::drive_options opt; opt.pipeline_credit = 2; dpf::protocol::drive_both_on_streams(p0, p1, v0, v1, {}, 1, opt); std::uint64_t o0 = 0, o1 = 0; std::memcpy(&o0, v0[out0.id].data(), 8); std::memcpy(&o1, v1[out1.id].data(), 8); // Each exchange opens the sum; after first open both hold 3, then 6, … EXPECT_EQ(o0, o1); EXPECT_EQ(o0, 3u << 7); } TEST(Compose, DrivePlanOnStreamsMuxTcp) { dpf::protocol::composer c0(0); dpf::protocol::composer c1(1); auto x0 = c0.input(domain::a, 8); auto x1 = c1.input(domain::a, 8); auto e0 = c0.exchange(x0); auto e1 = c1.exchange(x1); auto p0 = c0.schedule(); auto p1 = c1.schedule(); const auto nstreams = p0.slot_bytes_all().size(); ASSERT_EQ(nstreams, 1u); std::atomic port{0}; std::uint64_t open0 = 0, open1 = 0; std::exception_ptr err; std::mutex mu; std::thread t0([&] { try { dpf::run::tcp_pair_mux(0, "127.0.0.1", port, nstreams, [&](unsigned, dpf::net::mux_stream_array & mux) { std::vector> v(p0.nodes().size()); const std::uint64_t a = 7; put_raw(v, x0, &a, 8); dpf::protocol::drive_plan_on_streams(p0, mux, v, {}, 0); std::memcpy(&open0, v[e0.id].data(), 8); }); } catch (...) { std::lock_guard lock(mu); if (!err) err = std::current_exception(); } }); while (port.load() == 0) std::this_thread::yield(); std::thread t1([&] { try { dpf::run::tcp_pair_mux(1, "127.0.0.1", port, nstreams, [&](unsigned, dpf::net::mux_stream_array & mux) { std::vector> v(p1.nodes().size()); const std::uint64_t b = 35; put_raw(v, x1, &b, 8); dpf::protocol::drive_plan_on_streams(p1, mux, v, {}, 1); std::memcpy(&open1, v[e1.id].data(), 8); }); } catch (...) { std::lock_guard lock(mu); if (!err) err = std::current_exception(); } }); t0.join(); t1.join(); if (err) std::rethrow_exception(err); EXPECT_EQ(open0, 42u); EXPECT_EQ(open1, 42u); } TEST(Compose, DrivePlanOnStreamMeshRssCopy) { dpf::protocol::composer c0(0); dpf::protocol::composer c1(1); auto y0 = c0.input(domain::y, 8); auto y1 = c1.input(domain::y, 8); auto r0 = c0.rss_from_y(y0); auto r1 = c1.rss_from_y(y1); (void)r0; (void)r1; auto p = c0.schedule(); ASSERT_EQ(p.exchange_waves(), 1u); const auto slots = dpf::protocol::slot_bytes_by_channel(p); ASSERT_EQ(slots.rss_next.size(), 1u); ASSERT_TRUE(slots.peer.empty()); auto rss = dpf::net::make_memory_stream_pair(1); std::vector> val0(p.nodes().size()), val1(p.nodes().size()); const std::uint64_t mine0 = 10, mine1 = 20; put_raw(val0, y0, &mine0, 8); put_raw(val1, y1, &mine1, 8); auto sinks0 = dpf::net::make_stream_edge_sinks(nullptr, {}, &rss.first, slots.rss_next, nullptr, {}, 1); auto sinks1 = dpf::net::make_stream_edge_sinks(nullptr, {}, &rss.second, slots.rss_next, nullptr, {}, 1); std::exception_ptr err; std::mutex mu; std::thread t0([&] { try { dpf::protocol::drive_plan_on_stream_mesh(p, sinks0, val0, {}, 0); } catch (...) { std::lock_guard lock(mu); if (!err) err = std::current_exception(); } }); std::thread t1([&] { try { dpf::protocol::drive_plan_on_stream_mesh(p, sinks1, val1, {}, 1); } catch (...) { std::lock_guard lock(mu); if (!err) err = std::current_exception(); } }); t0.join(); t1.join(); if (err) std::rethrow_exception(err); std::uint64_t own0 = 0, next0 = 0, own1 = 0, next1 = 0; std::memcpy(&own0, val0[r0.id].data(), 8); std::memcpy(&next0, val0[r0.id].data() + 8, 8); std::memcpy(&own1, val1[r1.id].data(), 8); std::memcpy(&next1, val1[r1.id].data() + 8, 8); EXPECT_EQ(own0, mine0); EXPECT_EQ(next0, mine1); EXPECT_EQ(own1, mine1); EXPECT_EQ(next1, mine0); } TEST(Gilboa, MulTapeViaStreamArrays) { auto d0 = dpf::net::make_memory_stream_pair(1); auto d1 = dpf::net::make_memory_stream_pair(1); dpf::factory::deal_gilboa_mul_tape(d0.first, d1.first, 16); auto p0 = dpf::factory::read_ot_pack_wire(d0.second, 0); auto p1 = dpf::factory::read_ot_pack_wire(d1.second, 0); const std::uint64_t x0 = 30, x1 = 12, y0 = 3, y1 = 4; auto prod = dpf::gilboa::mul_from_ot_pair(p0, p1, x0, x1, y0, y1, 16); EXPECT_EQ(prod.z0 + prod.z1, dpf::gilboa::mul_clear(x0, x1, y0, y1)); } TEST(Gilboa, MulOnlineViaStreamArrays) { auto peer = dpf::net::make_memory_stream_pair(1); auto d0 = dpf::net::make_memory_stream_pair(1); auto d1 = dpf::net::make_memory_stream_pair(1); dpf::factory::deal_gilboa_mul_tape(d0.first, d1.first, 16); const std::uint64_t x0 = 30, x1 = 12, y0 = 3, y1 = 4; std::uint64_t z0 = 0, z1 = 0; std::thread t0([&] { z0 = dpf::factory::gilboa_mul_online(0, x0, y0, peer.first, d0.second, 16); }); std::thread t1([&] { z1 = dpf::factory::gilboa_mul_online(1, x1, y1, peer.second, d1.second, 16); }); t0.join(); t1.join(); EXPECT_EQ(z0 + z1, dpf::gilboa::mul_clear(x0, x1, y0, y1)); } TEST(Factory, RssRefreshRingStreamClique) { auto clique = dpf::net::make_memory_stream_clique(3, 1); auto bundle = dpf::rss::sample_seed_bundle(); std::uint64_t sum_own = 0, sum_next = 0; std::exception_ptr err; std::mutex mu; std::vector ts; for (unsigned me = 0; me < 3; ++me) { ts.emplace_back([&, me] { try { const auto seeds = dpf::rss::party_seeds::from_bundle(bundle, me); const auto z = dpf::rss::zero_share(seeds, 7); std::uint8_t mine[8]{}, own[8]{}, next[8]{}; std::memcpy(mine, &z, 8); dpf::factory::rss_refresh_ring_online(clique, me, mine, 8, own, next); std::uint64_t o = 0, n = 0; std::memcpy(&o, own, 8); std::memcpy(&n, next, 8); std::lock_guard lock(mu); sum_own += o; sum_next += n; } catch (...) { std::lock_guard lock(mu); if (!err) err = std::current_exception(); } }); } for (auto & t : ts) t.join(); if (err) std::rethrow_exception(err); EXPECT_EQ(sum_own, 0u); EXPECT_EQ(sum_next, 0u); } TEST(StreamArray, SctpStreamArrayStubThrows) { dpf::net::sctp_stream_array arr(4); EXPECT_EQ(arr.size(), 4u); EXPECT_THROW(dpf::net::sctp_stream_array(0), std::invalid_argument); EXPECT_THROW( dpf::net::sctp_stream_array(dpf::net::sctp_association{-1}), std::logic_error); std::uint8_t byte = 1; EXPECT_THROW(arr.write(0, &byte, 1), std::logic_error); EXPECT_THROW(arr.read(0, &byte, 1), std::logic_error); EXPECT_THROW(arr.flush(0), std::logic_error); } namespace { // Two rounds shared by the async byte-protocol tests. Round 0 replaces the // state with the peer's byte; round 1 adds one to the state, sends it, and // folds in the peer's reply. Two symmetric parties starting from the empty // state both end at 7. No blind is used (`blind_bytes == 0`). inline std::vector async_seven_rounds() { dpf::factory::async_byte_round r0{}; r0.blind_bytes = 0; r0.msg_bytes = 1; r0.produce = [](std::vector & state, const std::uint8_t *, std::size_t) { std::uint8_t v = state.empty() ? 3 : state[0]; return std::vector{v}; }; r0.finish = [](std::vector & state, const std::uint8_t * peer, std::size_t, const std::uint8_t *, std::size_t) { state.assign(1, peer[0]); }; dpf::factory::async_byte_round r1 = r0; r1.produce = [](std::vector & state, const std::uint8_t *, std::size_t) { return std::vector{static_cast(state[0] + 1)}; }; r1.finish = [](std::vector & state, const std::uint8_t * peer, std::size_t, const std::uint8_t *, std::size_t) { state[0] = static_cast((state[0] + peer[0]) & 0xffu); }; return {r0, r1}; } } // namespace TEST(Factory, AsyncByteProtocolRoundCallbacks) { // Overlapped, event-driven runner over an async memory pair; io_context // drives both parties to completion — no threads, no busy-waiting. asio::io_context io; auto peer = dpf::net::make_async_memory_stream_pair(io, 2); auto rounds = async_seven_rounds(); std::vector done0; std::vector done1; auto p0 = dpf::async::make_overlapped_byte_protocol(peer.first, nullptr, rounds, [&](std::size_t r) { done0.push_back(r); }); auto p1 = dpf::async::make_overlapped_byte_protocol(peer.second, nullptr, rounds, [&](std::size_t r) { done1.push_back(r); }); std::error_code ec0, ec1; std::vector out0, out1; dpf::async::run_overlapped(io, [&] { p0->start(0, {}, [&](const std::error_code & e, std::vector s) { ec0 = e; out0 = std::move(s); }); p1->start(0, {}, [&](const std::error_code & e, std::vector s) { ec1 = e; out1 = std::move(s); }); }); ASSERT_FALSE(ec0); ASSERT_FALSE(ec1); ASSERT_EQ(out0.size(), 1u); ASSERT_EQ(out1.size(), 1u); EXPECT_EQ(out0[0], 7u); EXPECT_EQ(out1[0], 7u); ASSERT_EQ(done0.size(), 2u); ASSERT_EQ(done1.size(), 2u); EXPECT_EQ(done0[0], 0u); EXPECT_EQ(done0[1], 1u); EXPECT_EQ(done1[0], 0u); EXPECT_EQ(done1[1], 1u); } // Two symmetric parties, two overlapped rounds, one io_context. Each round // adds the peer's byte to the running state: 10/20 -> 30/30 -> 60/60. Purely // event-driven; the io_context returns only when both parties finish. TEST(AsyncIo, MemoryOverlapTwoRounds) { asio::io_context io; auto peer = dpf::net::make_async_memory_stream_pair(io, 2); auto add_rounds = [] { dpf::factory::async_byte_round r{}; r.blind_bytes = 0; r.msg_bytes = 1; r.produce = [](std::vector & state, const std::uint8_t *, std::size_t) { return std::vector{state[0]}; }; r.finish = [](std::vector & state, const std::uint8_t * peer, std::size_t, const std::uint8_t *, std::size_t) { state[0] = static_cast((state[0] + peer[0]) & 0xffu); }; return std::vector{r, r}; }; auto p0 = dpf::async::make_overlapped_byte_protocol(peer.first, nullptr, add_rounds()); auto p1 = dpf::async::make_overlapped_byte_protocol(peer.second, nullptr, add_rounds()); std::vector out0, out1; dpf::async::run_overlapped(io, [&] { p0->start(0, {10}, [&](const std::error_code &, std::vector s) { out0 = std::move(s); }); p1->start(0, {20}, [&](const std::error_code &, std::vector s) { out1 = std::move(s); }); }); ASSERT_EQ(out0.size(), 1u); ASSERT_EQ(out1.size(), 1u); EXPECT_EQ(out0[0], 60u); EXPECT_EQ(out1[0], 60u); } // One TCP socket per party, N logical streams multiplexed on it. Two rounds // exchanged fully overlapped (write and peer-read issued together). Each party // runs its own io_context; io.stop() from the done handler tears down the // persistent read loop once the protocol finishes. TEST(AsyncIo, MuxLocalhostOverlap) { std::atomic port{0}; std::uint8_t out0 = 0, out1 = 0; auto add_rounds = [] { dpf::factory::async_byte_round r{}; r.blind_bytes = 0; r.msg_bytes = 1; r.produce = [](std::vector & state, const std::uint8_t *, std::size_t) { return std::vector{state[0]}; }; r.finish = [](std::vector & state, const std::uint8_t * peer, std::size_t, const std::uint8_t *, std::size_t) { state[0] = static_cast((state[0] + peer[0]) & 0xffu); }; return std::vector{r, r}; }; std::thread t0([&] { asio::io_context io; asio::ip::tcp::acceptor acc(io, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port.load())); port.store(acc.local_endpoint().port()); asio::ip::tcp::socket sock(io); acc.accept(sock); dpf::net::async_mux_stream_array mux(io, std::move(sock), 0, 1, 2); auto p = dpf::async::make_overlapped_byte_protocol(mux, nullptr, add_rounds()); dpf::async::run_overlapped(io, [&] { p->start(0, {10}, [&](const std::error_code &, std::vector s) { out0 = s[0]; io.stop(); }); }); }); while (port.load() == 0) std::this_thread::yield(); std::thread t1([&] { asio::io_context io; asio::ip::tcp::socket sock(io); asio::ip::tcp::resolver res(io); auto eps = res.resolve("127.0.0.1", std::to_string(port.load())); asio::connect(sock, eps); dpf::net::async_mux_stream_array mux(io, std::move(sock), 1, 0, 2); auto p = dpf::async::make_overlapped_byte_protocol(mux, nullptr, add_rounds()); dpf::async::run_overlapped(io, [&] { p->start(0, {20}, [&](const std::error_code &, std::vector s) { out1 = s[0]; io.stop(); }); }); }); t0.join(); t1.join(); EXPECT_EQ(out0, 60u); EXPECT_EQ(out1, 60u); } // One TCP connection PER stream index; concurrent exchanges on stream 0 and // stream 1 run overlapped on a single io_context per party. TEST(AsyncIo, ParallelSocketsTwoStreams) { std::atomic port{0}; std::uint8_t p0_in0 = 0, p0_in1 = 0, p1_in0 = 0, p1_in1 = 0; std::thread t0([&] { asio::io_context io; auto socks = dpf::net::accept_parallel_tcp(io, port, 2); dpf::net::async_parallel_stream_array arr(io, std::move(socks)); std::uint8_t o0 = 0xA0, o1 = 0xA1; auto pending = std::make_shared(2); dpf::async::run_overlapped(io, [&, pending] { auto on = [&, pending](const std::error_code &) { if (--*pending == 0) io.stop(); }; dpf::async::async_exchange(arr, 0, &o0, 1, &p0_in0, 1, on); dpf::async::async_exchange(arr, 1, &o1, 1, &p0_in1, 1, on); }); }); while (port.load() == 0) std::this_thread::yield(); std::thread t1([&] { asio::io_context io; auto socks = dpf::net::connect_parallel_tcp(io, "127.0.0.1", port, 2); dpf::net::async_parallel_stream_array arr(io, std::move(socks)); std::uint8_t o0 = 0xB0, o1 = 0xB1; auto pending = std::make_shared(2); dpf::async::run_overlapped(io, [&, pending] { auto on = [&, pending](const std::error_code &) { if (--*pending == 0) io.stop(); }; dpf::async::async_exchange(arr, 0, &o0, 1, &p1_in0, 1, on); dpf::async::async_exchange(arr, 1, &o1, 1, &p1_in1, 1, on); }); }); t0.join(); t1.join(); EXPECT_EQ(p0_in0, 0xB0u); EXPECT_EQ(p0_in1, 0xB1u); EXPECT_EQ(p1_in0, 0xA0u); EXPECT_EQ(p1_in1, 0xA1u); } // One SCTP association, index i -> SCTP stream i. Two rounds exchanged fully // overlapped over localhost (party 0 accepts, party 1 connects). Mirrors the // mux/parallel overlap tests but on the real SCTP backend. Requires Linux + // libsctp AND a kernel that supports SCTP sockets; otherwise the test skips. TEST(AsyncIo, SctpLocalhostOverlap) { #if DPF_HAS_LIBSCTP // Kernel may lack the SCTP module even when libsctp headers exist. { const int probe = ::socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP); if (probe < 0) GTEST_SKIP() << "kernel SCTP unavailable (socket: " << std::strerror(errno) << ")"; ::close(probe); } std::atomic port{0}; std::uint8_t out0 = 0, out1 = 0; auto add_rounds = [] { dpf::factory::async_byte_round r{}; r.blind_bytes = 0; r.msg_bytes = 1; r.produce = [](std::vector & state, const std::uint8_t *, std::size_t) { return std::vector{state[0]}; }; r.finish = [](std::vector & state, const std::uint8_t * peer, std::size_t, const std::uint8_t *, std::size_t) { state[0] = static_cast((state[0] + peer[0]) & 0xffu); }; return std::vector{r, r}; }; std::exception_ptr err; std::mutex emu; auto note = [&](std::exception_ptr e) { std::lock_guard lk(emu); if (!err) err = std::move(e); }; std::thread t0([&] { try { asio::io_context io; const int fd = dpf::net::accept_sctp_association(io, port, 2); dpf::net::async_sctp_stream_array arr(io, fd, 2); auto p = dpf::async::make_overlapped_byte_protocol(arr, nullptr, add_rounds()); dpf::async::run_overlapped(io, [&] { p->start(0, {10}, [&](const std::error_code &, std::vector s) { out0 = s[0]; io.stop(); }); }); } catch (...) { note(std::current_exception()); } }); while (port.load() == 0) std::this_thread::yield(); std::thread t1([&] { try { asio::io_context io; const int fd = dpf::net::connect_sctp_association(io, "127.0.0.1", port, 2); dpf::net::async_sctp_stream_array arr(io, fd, 2); auto p = dpf::async::make_overlapped_byte_protocol(arr, nullptr, add_rounds()); dpf::async::run_overlapped(io, [&] { p->start(0, {20}, [&](const std::error_code &, std::vector s) { out1 = s[0]; io.stop(); }); }); } catch (...) { note(std::current_exception()); } }); t0.join(); t1.join(); if (err) std::rethrow_exception(err); EXPECT_EQ(out0, 60u); EXPECT_EQ(out1, 60u); #else GTEST_SKIP() << "libsctp not available (Linux + required)"; #endif } // The performance harness runs on the framework: exercise_plan honours // DPF_TRANSPORT. Here we drive a small open plan on the synchronous stream // framework and confirm the reported cost matches the plan. TEST(AppFlow, ExercisePlanUsesStreamFramework) { ::setenv("DPF_TRANSPORT", "stream", 1); dpf::protocol::composer c(0); auto x = c.input(domain::a, 8); (void)c.exchange(x); auto p = c.default_plan(); const auto got = dpf::app::exercise_plan(p); ::unsetenv("DPF_TRANSPORT"); EXPECT_EQ(got.rounds, p.rounds()); std::size_t want_bytes = 0; for (auto n : p.slot_bytes_all()) want_bytes += n; EXPECT_EQ(got.bytes, want_bytes); } // The default transport (async_memory) drives the same plan over the real // event-driven framework in-process. TEST(AppFlow, ExercisePlanAsyncMemoryFramework) { ::unsetenv("DPF_TRANSPORT"); // default: async_memory dpf::protocol::composer c(0); auto x = c.input(domain::a, 8); (void)c.exchange(x); auto p = c.default_plan(); const auto got = dpf::app::exercise_plan(p); EXPECT_EQ(got.rounds, p.rounds()); EXPECT_EQ(dpf::app::transport_from_env(), dpf::app::transport_kind::async_memory); } // Deep dependent plan on a 2-lane pool: rounds >> streams, framed demux. TEST(AsyncIo, LaneReuseDeepPlan) { constexpr std::uint32_t k_step = 92001; auto make = [](std::size_t party, std::uint64_t in) { dpf::protocol::composer c(party); auto x = c.input(domain::a, 8); auto cur = x; for (int i = 0; i < 6; ++i) { auto e = c.exchange(cur); cur = c.compute(k_step, {e}, domain::a, 8); } return std::make_tuple(c.schedule(), x, cur, in); }; auto [p0, x0, out0, in0] = make(0, 1); auto [p1, x1, out1, in1] = make(1, 2); ASSERT_GE(p0.slot_bytes_all().size(), 6u); EXPECT_EQ(dpf::net::lane_count_for_rounds(p0.slot_bytes_all().size(), 2), 2u); dpf::protocol::kernel_fn step = [](std::uint32_t, const std::vector &, const std::vector & inputs, dpf::protocol::block_span output, std::size_t) { std::uint64_t v = 0; std::memcpy(&v, inputs[0].at(0), 8); v += 1; std::memcpy(output.at(0), &v, 8); }; std::map k{{k_step, step}}; std::vector> v0(p0.nodes().size()), v1(p1.nodes().size()); put_raw(v0, x0, &in0, 8); put_raw(v1, x1, &in1, 8); dpf::protocol::drive_options opt; opt.n_lanes = 2; dpf::protocol::drive_both_on_async_streams(p0, p1, v0, v1, k, 1, opt); std::uint64_t o0 = 0, o1 = 0; std::memcpy(&o0, v0[out0.id].data(), 8); std::memcpy(&o1, v1[out1.id].data(), 8); // Dependent opens double each wave (both parties hold the prior open), then +1. // 1+2 → 3 →+1→ 4 →open→ 8 →+1→ 9 → … → final 159 after 6 steps. EXPECT_EQ(o0, o1); EXPECT_EQ(o0, 159u); } // Compose plans ride the overlapped async stack (not a separate factory path). TEST(ComposeAsync, DriveBothOnAsyncStreams) { dpf::protocol::composer c0(0); dpf::protocol::composer c1(1); auto x0 = c0.input(domain::a, 8); auto x1 = c1.input(domain::a, 8); auto o0 = c0.exchange(x0); auto o1 = c1.exchange(x1); auto p0 = c0.schedule(); auto p1 = c1.schedule(); std::vector> v0(p0.nodes().size()), v1(p1.nodes().size()); std::uint64_t a = 11, b = 31; put_raw(v0, x0, &a, 8); put_raw(v1, x1, &b, 8); dpf::protocol::drive_both_on_async_streams(p0, p1, v0, v1); std::uint64_t open0 = 0, open1 = 0; std::memcpy(&open0, v0[o0.id].data(), 8); std::memcpy(&open1, v1[o1.id].data(), 8); EXPECT_EQ(open0, 42u); EXPECT_EQ(open1, 42u); } // Overlapped factory protocol on a single peer stream (lane reuse by modulo). TEST(AsyncIo, OverlappedLaneModulo) { asio::io_context io; auto peer = dpf::net::make_async_memory_stream_pair(io, 1); using round = dpf::factory::async_byte_round; std::vector rounds(3); for (std::size_t r = 0; r < 3; ++r) { rounds[r].msg_bytes = 4; rounds[r].blind_bytes = 0; rounds[r].produce = [r](const std::vector &, const std::uint8_t *, std::size_t) { std::vector out(4, static_cast(r + 1)); return out; }; rounds[r].finish = [r](std::vector & st, const std::uint8_t * in, std::size_t n, const std::uint8_t *, std::size_t) { ASSERT_EQ(n, 4u); st.push_back(in[0]); EXPECT_EQ(in[0], static_cast(r + 1)); }; } std::vector done0, done1; std::error_code ec0, ec1; auto p0 = dpf::async::make_overlapped_byte_protocol(peer.first, nullptr, rounds); auto p1 = dpf::async::make_overlapped_byte_protocol(peer.second, nullptr, rounds); dpf::async::run_overlapped(io, [&] { p0->start(0, {}, [&](const std::error_code & ec, std::vector st) { ec0 = ec; done0 = std::move(st); }); p1->start(0, {}, [&](const std::error_code & ec, std::vector st) { ec1 = ec; done1 = std::move(st); }); }); EXPECT_FALSE(ec0); EXPECT_FALSE(ec1); ASSERT_EQ(done0.size(), 3u); ASSERT_EQ(done1.size(), 3u); } // Three parties join a real TCP mux clique and exchange 8 bytes on each edge. TEST(TcpMesh, ThreePartyMuxExchange) { constexpr unsigned n = 3; std::atomic ok{0}; dpf::run::threads_on_tcp_mesh(n, /*streams=*/1, [&](unsigned me, dpf::net::tcp_mesh & mesh) { EXPECT_EQ(mesh.parties, n); for (unsigned peer = 0; peer < n; ++peer) { if (peer == me) continue; std::uint64_t mine = (static_cast(me) << 8) | peer; std::uint64_t got = 0; auto & link = mesh.peer(peer); link.write(0, &mine, 8); link.flush(0); link.read(0, &got, 8); const std::uint64_t expect = (static_cast(peer) << 8) | me; EXPECT_EQ(got, expect); } ok.fetch_add(1); }); EXPECT_EQ(ok.load(), static_cast(n)); } // Composer accepts party ids beyond 0..2 (N-party meshes). TEST(Compose, NPartyComposerAllowed) { dpf::protocol::composer c5(5); EXPECT_EQ(c5.party(), 5u); } TEST(Session, ShipPrepAsync) { dpf::prep::demand d; d.limb = 8; d.ring_triples = 2; const auto shipped = dpf::session::ship_prep(d); EXPECT_EQ(shipped.bytes0, shipped.bytes1); EXPECT_GT(shipped.bytes0, 18u); std::uint8_t a0[8]{}, b0[8]{}, c0[8]{}, a1[8]{}, b1[8]{}, c1[8]{}; auto p0 = shipped.party0; auto p1 = shipped.party1; p0.take_ring(a0, b0, c0); p1.take_ring(a1, b1, c1); std::uint64_t sa = 0, sb = 0, sc = 0, ta = 0, tb = 0, tc = 0; std::memcpy(&sa, a0, 8); std::memcpy(&sb, b0, 8); std::memcpy(&sc, c0, 8); std::memcpy(&ta, a1, 8); std::memcpy(&tb, b1, 8); std::memcpy(&tc, c1, 8); EXPECT_EQ((sa + ta) * (sb + tb), sc + tc); } TEST(Session, HushmapAsyncDealerAndPeer) { EXPECT_NO_THROW(dpf::session::drive_hushmap_add(3)); } TEST(Session, PirsonaAsyncStar) { constexpr std::size_t L = 1; constexpr std::size_t n = 2; auto seeds = std::make_shared>>(n); auto answers = std::make_shared>>(n); for (std::size_t i = 0; i < n; ++i) { (*seeds)[i].assign(16, static_cast(i + 1)); (*answers)[i].assign(8, static_cast(0x40 + i)); } auto client = dpf::protocol::pirsona_bitmore_fetch(L, 16, 8, seeds, answers); EXPECT_NO_THROW(dpf::session::drive_async_star(n, {16u, 8u}, std::move(client), [&](std::size_t i) { return dpf::protocol::star_server_reply_rounds(16, 8, (*answers)[i]); })); } TEST(WireWindow, MuxTinyWindowStillCompletes) { asio::io_context io0; asio::io_context io1; dpf::net::wire_policy pol; pol.window_bytes = 64; std::atomic port{0}; std::atomic ok{0}; std::exception_ptr err; std::mutex mu; std::thread t0([&] { try { asio::ip::tcp::acceptor acc(io0, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), 0)); port.store(acc.local_endpoint().port()); asio::ip::tcp::socket sock(io0); acc.accept(sock); dpf::net::async_mux_stream_array mux(io0, std::move(sock), 0, 1, 1, pol); EXPECT_EQ(mux.window_bytes(), 64u); std::vector out(200, 0xab); std::vector in(200); std::atomic left{2}; mux.async_write(0, out.data(), out.size(), [&](const std::error_code & ec) { EXPECT_FALSE(ec); left.fetch_sub(1); }); mux.async_read(0, in.data(), in.size(), [&](const std::error_code & ec) { EXPECT_FALSE(ec); left.fetch_sub(1); }); while (left.load() != 0) io0.run_one(); EXPECT_EQ(in, out); ok.fetch_add(1); } catch (...) { std::lock_guard lock(mu); err = std::current_exception(); } }); std::thread t1([&] { try { while (port.load() == 0) std::this_thread::yield(); asio::ip::tcp::socket sock(io1); asio::ip::tcp::resolver res(io1); auto eps = res.resolve("127.0.0.1", std::to_string(port.load())); asio::connect(sock, eps); dpf::net::async_mux_stream_array mux(io1, std::move(sock), 1, 0, 1); std::vector out(200, 0xab); std::vector in(200); std::atomic left{2}; mux.async_write(0, out.data(), out.size(), [&](const std::error_code & ec) { EXPECT_FALSE(ec); left.fetch_sub(1); }); mux.async_read(0, in.data(), in.size(), [&](const std::error_code & ec) { EXPECT_FALSE(ec); left.fetch_sub(1); }); while (left.load() != 0) io1.run_one(); ok.fetch_add(1); } catch (...) { std::lock_guard lock(mu); err = std::current_exception(); } }); t0.join(); t1.join(); if (err) std::rethrow_exception(err); EXPECT_EQ(ok.load(), 2); } TEST(IoPool, PostsComputeOnSharedWorkers) { dpf::net::io_pool pool(2); EXPECT_GE(pool.size(), 2u); constexpr int n = 32; std::atomic sum{0}; for (int i = 0; i < n; ++i) pool.post([&] { sum.fetch_add(1); }); for (int spins = 0; sum.load() != n && spins < 1000000; ++spins) std::this_thread::yield(); EXPECT_EQ(sum.load(), n); } namespace { void exchange_u64(dpf::net::async_stream_array & link, std::uint64_t mine, std::uint64_t expect) { std::uint64_t got = 0; std::atomic left{2}; std::error_code wec, rec; link.async_write(0, &mine, 8, [&](const std::error_code & ec) { wec = ec; left.fetch_sub(1); }); link.async_read(0, &got, 8, [&](const std::error_code & ec) { rec = ec; left.fetch_sub(1); }); for (int spins = 0; left.load() != 0 && spins < 1000000; ++spins) std::this_thread::yield(); ASSERT_EQ(left.load(), 0); EXPECT_FALSE(wec); EXPECT_FALSE(rec); EXPECT_EQ(got, expect); } } // namespace TEST(PartySession, JoinReconnectAndDealerOnOnePool) { dpf::net::io_pool pool(2); auto ports = dpf::net::make_mesh_ports(2); std::exception_ptr err; std::mutex mu; std::thread t0([&] { try { dpf::net::party_session me(pool.context(), 0, 2, 1); me.join("127.0.0.1", ports); exchange_u64(me.peer(1), 0x10, 0x21); me.reconnect(1); exchange_u64(me.peer(1), 0x30, 0x41); } catch (...) { std::lock_guard lock(mu); err = std::current_exception(); } }); std::thread t1([&] { try { dpf::net::party_session me(pool.context(), 1, 2, 1); me.join("127.0.0.1", ports); exchange_u64(me.peer(0), 0x21, 0x10); me.reconnect(0); exchange_u64(me.peer(0), 0x41, 0x30); } catch (...) { std::lock_guard lock(mu); err = std::current_exception(); } }); t0.join(); t1.join(); if (err) std::rethrow_exception(err); // Dealer is its own link, not a mesh neighbor. std::atomic dport{0}; std::thread dealer([&] { try { dpf::net::party_session d(pool.context(), 0, 2, 1); dport.store(d.listen()); d.accept_dealer(); exchange_u64(d.dealer(), 7, 9); } catch (...) { std::lock_guard lock(mu); if (!err) err = std::current_exception(); } }); std::thread party([&] { try { while (dport.load() == 0) std::this_thread::yield(); dpf::net::party_session p(pool.context(), 1, 2, 1); p.connect_dealer("127.0.0.1", dport.load()); EXPECT_TRUE(p.has_dealer()); exchange_u64(p.dealer(), 9, 7); } catch (...) { std::lock_guard lock(mu); if (!err) err = std::current_exception(); } }); dealer.join(); party.join(); if (err) std::rethrow_exception(err); } TEST(SocketTune, NoDelayOnConnectedSocket) { asio::io_context io; std::atomic port{0}; asio::ip::tcp::no_delay server_delay(false); asio::ip::tcp::no_delay client_delay(false); std::thread acc([&] { asio::ip::tcp::acceptor a(io, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), 0)); port.store(a.local_endpoint().port()); asio::ip::tcp::socket s(io); a.accept(s); dpf::net::tune_tcp(s); s.get_option(server_delay); }); while (port.load() == 0) std::this_thread::yield(); asio::ip::tcp::socket c(io); asio::ip::tcp::resolver res(io); asio::connect(c, res.resolve("127.0.0.1", std::to_string(port.load()))); dpf::net::tune_tcp(c); c.get_option(client_delay); acc.join(); EXPECT_TRUE(server_delay.value()); EXPECT_TRUE(client_delay.value()); } TEST(IoPool, ComputeDoesNotUseSocketContext) { dpf::net::io_pool pool(2, 2); EXPECT_GE(pool.compute_size(), 2u); std::atomic done{false}; std::thread::id compute_id; const auto caller = std::this_thread::get_id(); pool.post_compute([&] { compute_id = std::this_thread::get_id(); done.store(true); }); for (int i = 0; !done.load() && i < 1000000; ++i) std::this_thread::yield(); EXPECT_TRUE(done.load()); EXPECT_NE(compute_id, caller); } TEST(DriveOptions, LaneCountAndWaitTimeout) { dpf::protocol::drive_options opt; opt.n_lanes = 2; opt.wait_timeout = std::chrono::milliseconds(50); EXPECT_EQ(dpf::protocol::lanes_for_plan(6, opt), 2u); dpf::protocol::composer c0(0); auto x = c0.input(domain::a, 8); (void)c0.exchange(x); auto p = c0.schedule(); asio::io_context io0; asio::io_context io1; auto ends = dpf::net::make_async_dual_memory_stream_pair(io0, io1, 1); auto work = asio::make_work_guard(io0); std::vector> values(p.nodes().size()); std::uint64_t in = 1; put_raw(values, x, &in, 8); EXPECT_THROW( dpf::protocol::drive_plan_on_async_streams(p, ends.first, values, {}, 0, 1, opt), std::runtime_error); } TEST(DriveOptions, KernelRunsOnComputePool) { constexpr std::uint32_t k_step = 93001; dpf::net::io_pool pool(2, 2); std::thread::id kid; dpf::protocol::kernel_fn step = [&](std::uint32_t, const std::vector &, const std::vector & inputs, dpf::protocol::block_span output, std::size_t) { kid = std::this_thread::get_id(); std::uint64_t v = 0; std::memcpy(&v, inputs[0].at(0), 8); v += 1; std::memcpy(output.at(0), &v, 8); }; auto make = [&](std::size_t party, std::uint64_t in) { dpf::protocol::composer c(party); auto xin = c.input(domain::a, 8); auto e = c.exchange(xin); auto y = c.compute(k_step, {e}, domain::a, 8); return std::make_tuple(c.schedule(), xin, y, in); }; auto [p0, x0, out0, in0] = make(0, 4); auto [p1, x1, out1, in1] = make(1, 6); std::map k{{k_step, step}}; std::vector> v0(p0.nodes().size()), v1(p1.nodes().size()); put_raw(v0, x0, &in0, 8); put_raw(v1, x1, &in1, 8); dpf::protocol::drive_options opt; opt.workers = &pool; const auto caller = std::this_thread::get_id(); dpf::protocol::drive_both_on_async_streams(p0, p1, v0, v1, k, 1, opt); EXPECT_NE(kid, caller); std::uint64_t o0 = 0; std::memcpy(&o0, v0[out0.id].data(), 8); EXPECT_EQ(o0, 11u); } TEST(PartySession, WindowAndLaneOverride) { dpf::net::io_pool pool(2); auto ports = dpf::net::make_mesh_ports(2); std::atomic win0{0}; std::atomic streams0{0}; std::thread t0([&] { dpf::net::party_session me(pool.context(), 0, 2, 1); me.set_window_bytes(4096); me.join("127.0.0.1", ports, 4); win0.store(me.peer(1).window_bytes()); streams0.store(me.streams()); std::uint64_t mine = 1, got = 0; std::atomic left{2}; me.peer(1).async_write(0, &mine, 8, [&](const std::error_code &) { left.fetch_sub(1); }); me.peer(1).async_read(0, &got, 8, [&](const std::error_code &) { left.fetch_sub(1); }); for (int i = 0; left.load() != 0 && i < 1000000; ++i) std::this_thread::yield(); EXPECT_EQ(got, 2u); }); std::thread t1([&] { dpf::net::party_session me(pool.context(), 1, 2, 1); me.set_window_bytes(4096); me.join("127.0.0.1", ports, 4); std::uint64_t mine = 2, got = 0; std::atomic left{2}; me.peer(0).async_write(0, &mine, 8, [&](const std::error_code &) { left.fetch_sub(1); }); me.peer(0).async_read(0, &got, 8, [&](const std::error_code &) { left.fetch_sub(1); }); for (int i = 0; left.load() != 0 && i < 1000000; ++i) std::this_thread::yield(); EXPECT_EQ(got, 1u); }); t0.join(); t1.join(); EXPECT_EQ(win0.load(), 4096u); EXPECT_EQ(streams0.load(), 4u); }