#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "dpf/app_flow.hpp" #include "dpf/compose.hpp" #include "dpf/compose_async.hpp" #include "dpf/launch.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/connect.hpp" #include "dpf/net/party_session.hpp" #include "dpf/net/policy.hpp" #include "dpf/net/round_lane.hpp" #include "dpf/net/stream_array.hpp" #include "dpf/net/sync_stream_array.hpp" #include "dpf/online_session.hpp" #include "dpf/party_run.hpp" #include "dpf/party_runner.hpp" #include "dpf/run_config.hpp" namespace { using dpf::protocol::domain; using clock_type = std::chrono::steady_clock; void put(std::vector> & values, dpf::protocol::node n, std::uint64_t v) { if (values.size() <= n.id) values.resize(n.id + 1); values[n.id].assign(8, 0); std::memcpy(values[n.id].data(), &v, 8); } std::uint64_t get(const std::vector> & values, dpf::protocol::node n, std::size_t lane = 0) { std::uint64_t v = 0; std::memcpy(&v, values[n.id].data() + 8 * lane, 8); return v; } /// Pump `io` until `n` of the counted completions arrive or 5 s pass. void pump(asio::io_context & io, std::atomic & left) { const auto deadline = clock_type::now() + std::chrono::seconds(5); while (left.load() > 0 && clock_type::now() < deadline) { if (io.stopped()) io.restart(); io.run_one_for(std::chrono::milliseconds(10)); } } void step(asio::io_context & io) { if (io.stopped()) io.restart(); io.run_one_for(std::chrono::milliseconds(5)); } unsigned short free_port() { asio::io_context io; asio::ip::tcp::acceptor a(io); dpf::net::open_listener(a, 0); return a.local_endpoint().port(); } /// Two parties: x0 + x1 opened, then `k` dependent steps (+1 and reopen). struct chain { dpf::protocol::plan plan; dpf::protocol::node x; dpf::protocol::node out; }; constexpr std::uint32_t k_inc = 94001; chain make_chain(std::size_t party, int steps) { dpf::protocol::composer c(party); chain ch; ch.x = c.input(domain::a, 8); auto cur = ch.x; for (int i = 0; i < steps; ++i) { auto e = c.exchange(cur); cur = c.compute(k_inc, {e}, domain::a, 8); } ch.out = cur; ch.plan = c.schedule(); return ch; } dpf::protocol::kernel_fn inc_kernel(std::chrono::milliseconds sleep = {}) { return [sleep](std::uint32_t, const std::vector &, const std::vector & inputs, dpf::protocol::block_span output, std::size_t lanes) { if (sleep.count() > 0) std::this_thread::sleep_for(sleep); for (std::size_t l = 0; l < lanes; ++l) { std::uint64_t v = 0; std::memcpy(&v, inputs[0].at(l), 8); v += 1; std::memcpy(output.at(l), &v, 8); } }; } /// Expected result of `make_chain` from inputs a, b. std::uint64_t chain_value(std::uint64_t a, std::uint64_t b, int steps) { // Each step opens (both parties hold the prior value) and adds 1. std::uint64_t v = a + b + 1; for (int i = 1; i < steps; ++i) v = 2 * v + 1; return v; } } // namespace // --------------------------------------------------------------------------- // Explicit configuration // --------------------------------------------------------------------------- TEST(RunConfig, EnvAndArgsShareKeys) { ::setenv("DPF_LANES", "4", 1); ::setenv("DPF_WIRE_WINDOW", "65536", 1); auto cfg = dpf::app::run_config::from_env(); ::unsetenv("DPF_LANES"); ::unsetenv("DPF_WIRE_WINDOW"); EXPECT_EQ(cfg.n_lanes, 4u); EXPECT_EQ(cfg.policy.window_bytes, 65536u); const char * argv[] = {"x", "--transport=parallel", "--framing=always", "--instances=3", "--sndbuf=262144", "--trials=5", "positional"}; const auto rest = cfg.apply_args(7, const_cast(argv)); ASSERT_EQ(rest.size(), 1u); EXPECT_EQ(rest[0], "positional"); EXPECT_EQ(cfg.kind, dpf::net::transport::parallel); EXPECT_EQ(cfg.framing, dpf::net::framing_mode::always); EXPECT_EQ(cfg.instances, 3u); EXPECT_EQ(cfg.policy.socket.send_buffer, 262144); EXPECT_EQ(cfg.trials, 5u); EXPECT_NE(cfg.summary().find("transport=parallel"), std::string::npos); EXPECT_THROW(cfg.set("lanse", "3"), std::invalid_argument); EXPECT_THROW(cfg.set("lanes", "three"), std::invalid_argument); EXPECT_THROW(cfg.set("transport", "udp"), std::invalid_argument); EXPECT_THROW(cfg.set("chunk", std::to_string(std::size_t{64} << 20)), std::invalid_argument); } TEST(RoundLane, FramingModesAndLimits) { dpf::net::round_lane_map a(4, 3, dpf::net::framing_mode::always); EXPECT_TRUE(a.framed); EXPECT_EQ(a.lane(2), 2u); dpf::net::round_lane_map b(2, 6); EXPECT_TRUE(b.framed); EXPECT_EQ(b.lane(5), 1u); EXPECT_THROW(dpf::net::round_lane_map(2, 6, dpf::net::framing_mode::never), std::invalid_argument); EXPECT_THROW(dpf::net::round_lane_map(1, 70000), std::invalid_argument); EXPECT_EQ(dpf::net::lane_count_for_rounds(20), 8u); EXPECT_EQ(dpf::net::lane_count_for_rounds(20, dpf::net::lanes_one_per_round), 20u); EXPECT_EQ(dpf::net::lane_count_for_rounds(3, 8), 3u); } // --------------------------------------------------------------------------- // Sink hello and framing // --------------------------------------------------------------------------- namespace { std::string sink_mismatch(std::vector s0, std::vector s1, std::size_t c0, std::size_t c1, dpf::net::framing_mode f0, dpf::net::framing_mode f1) { asio::io_context io; auto ends = dpf::net::make_async_memory_stream_pair(io, 4); dpf::net::sink_options o0, o1; o0.framing = f0; o1.framing = f1; dpf::net::async_round_sink a(ends.first, s0, c0, o0); dpf::net::async_round_sink b(ends.second, s1, c1, o1); const auto deadline = clock_type::now() + std::chrono::seconds(2); while (clock_type::now() < deadline) { try { (void)a.peer_ready(0, 0); (void)b.peer_ready(0, 0); } catch (const std::exception & e) { return e.what(); } step(io); } return {}; } } // namespace TEST(Sink, HelloNamesTheDisagreement) { using fm = dpf::net::framing_mode; auto rounds = sink_mismatch({8, 8}, {8}, 1, 1, fm::automatic, fm::automatic); EXPECT_NE(rounds.find("rounds"), std::string::npos) << rounds; auto widths = sink_mismatch({8, 8}, {8, 16}, 1, 1, fm::automatic, fm::automatic); EXPECT_NE(widths.find("slot widths"), std::string::npos) << widths; auto inst = sink_mismatch({8}, {8}, 2, 1, fm::automatic, fm::automatic); EXPECT_NE(inst.find("instances 1 vs 2"), std::string::npos) << inst; auto frame = sink_mismatch({8}, {8}, 1, 1, fm::always, fm::automatic); EXPECT_NE(frame.find("framing"), std::string::npos) << frame; } TEST(Sink, FramedPartialPrefixReadyPerInstance) { asio::io_context io; auto ends = dpf::net::make_async_memory_stream_pair(io, 1); dpf::net::sink_options so; so.framing = dpf::net::framing_mode::always; dpf::net::async_round_sink a(ends.first, {8}, 4, so); dpf::net::async_round_sink b(ends.second, {8}, 4, so); std::uint8_t slot[8] = {1}; a.submit(0, 0, slot, 8); a.submit(0, 1, slot, 8); a.flush_round(0); const auto deadline = clock_type::now() + std::chrono::seconds(2); while (!b.peer_ready(0, 1) && clock_type::now() < deadline) step(io); EXPECT_TRUE(b.peer_ready(0, 0)); EXPECT_TRUE(b.peer_ready(0, 1)); EXPECT_FALSE(b.peer_ready(0, 2)); a.submit(0, 2, slot, 8); a.submit(0, 3, slot, 8); a.flush_round(0); while (!b.peer_ready(0, 3) && clock_type::now() < deadline) step(io); EXPECT_TRUE(b.peer_ready(0, 3)); EXPECT_TRUE(a.framed()); } TEST(Sink, PeerFailureSurfacesImmediately) { asio::io_context io; auto ends = dpf::net::make_async_memory_stream_pair(io, 2); dpf::net::async_round_sink a(ends.first, {8, 8}, 1); { dpf::net::async_round_sink b(ends.second, {8, 8}, 1); for (int i = 0; i < 50; ++i) io.poll(); } ends.second.close(); const auto t0 = clock_type::now(); bool threw = false; try { for (int i = 0; i < 1000 && !threw; ++i) { (void)a.peer_ready(0, 0); a.wait_io_for(std::chrono::milliseconds(5)); } } catch (const std::system_error &) { threw = true; } EXPECT_TRUE(threw); EXPECT_LT(clock_type::now() - t0, std::chrono::seconds(2)); } // --------------------------------------------------------------------------- // Drive-loop budgets // --------------------------------------------------------------------------- TEST(Drive, HealthyPlanLongerThanWaitTimeout) { const int steps = 6; auto c0 = make_chain(0, steps); auto c1 = make_chain(1, steps); std::map k{ {k_inc, inc_kernel(std::chrono::milliseconds(25))}}; std::vector> v0(c0.plan.nodes().size()), v1(c1.plan.nodes().size()); put(v0, c0.x, 1); put(v1, c1.x, 2); dpf::protocol::drive_options opt; opt.wait_timeout = std::chrono::milliseconds(100); const auto t0 = clock_type::now(); dpf::protocol::drive_both_on_async_streams(c0.plan, c1.plan, v0, v1, k, 1, opt); EXPECT_GT(clock_type::now() - t0, std::chrono::milliseconds(120)); EXPECT_EQ(get(v0, c0.out), chain_value(1, 2, steps)); } TEST(Drive, EdgeAndRoundBudgetsNameTheWait) { auto c0 = make_chain(0, 2); asio::io_context io0, io1; auto ends = dpf::net::make_async_dual_memory_stream_pair(io0, io1, 2); std::vector> v0(c0.plan.nodes().size()); put(v0, c0.x, 1); std::map k{{k_inc, inc_kernel()}}; dpf::protocol::drive_options edge; edge.wait_timeout = std::chrono::seconds(20); edge.edge_timeout[dpf::net::edge_peer] = std::chrono::milliseconds(40); auto t0 = clock_type::now(); try { dpf::protocol::drive_plan_on_async_streams(c0.plan, ends.first, v0, k, 0, 1, edge); FAIL() << "one-sided drive should time out"; } catch (const std::runtime_error & e) { const std::string what = e.what(); EXPECT_NE(what.find("peer"), std::string::npos) << what; EXPECT_NE(what.find("budget 40"), std::string::npos) << what; } EXPECT_LT(clock_type::now() - t0, std::chrono::seconds(5)); auto ends2 = dpf::net::make_async_dual_memory_stream_pair(io0, io1, 2); std::vector> v1(c0.plan.nodes().size()); put(v1, c0.x, 1); dpf::protocol::drive_options round; round.wait_timeout = std::chrono::seconds(20); round.round_timeout[0] = std::chrono::milliseconds(30); t0 = clock_type::now(); EXPECT_THROW(dpf::protocol::drive_plan_on_async_streams(c0.plan, ends2.first, v1, k, 0, 1, round), std::runtime_error); EXPECT_LT(clock_type::now() - t0, std::chrono::seconds(5)); } // --------------------------------------------------------------------------- // Backends: windows, stats, scheduling, graceful close // --------------------------------------------------------------------------- TEST(Memory, WindowSetterAndGracefulClose) { asio::io_context io; auto ends = dpf::net::make_async_memory_stream_pair(io, 1); ends.first.set_window_bytes(100); EXPECT_EQ(ends.first.window_bytes(), 100u); std::vector a(300, 1), b(300, 2); std::atomic left{2}; ends.first.async_write(0, a.data(), a.size(), [&](auto) { --left; }); ends.first.async_write(0, b.data(), b.size(), [&](auto) { --left; }); EXPECT_EQ(ends.first.buffered_bytes(), 600u); ends.first.close(); std::vector got(600); std::atomic rd{1}; std::error_code rec; ends.second.async_read(0, got.data(), got.size(), [&](const std::error_code & ec) { rec = ec; --rd; }); pump(io, rd); EXPECT_FALSE(rec); EXPECT_EQ(got[0], 1); EXPECT_EQ(got[599], 2); const auto st = ends.first.stats(); EXPECT_EQ(st.payload_out, 600u); EXPECT_TRUE(st.closed); } namespace { /// Localhost mux pair, both ends on `io`. struct mux_pair { std::unique_ptr a; std::unique_ptr b; }; mux_pair make_mux_pair(asio::io_context & io, std::size_t lanes, const dpf::net::wire_policy & pol = {}) { asio::ip::tcp::acceptor acc(io); dpf::net::open_listener(acc, 0); asio::ip::tcp::socket s0(io), s1(io); std::thread t([&] { dpf::net::connect_until(s1, "127.0.0.1", acc.local_endpoint().port(), std::chrono::seconds(5)); }); dpf::net::accept_until(acc, s0, std::chrono::seconds(5)); t.join(); mux_pair out; out.a = std::make_unique(io, std::move(s0), 0, 1, lanes, pol); out.b = std::make_unique(io, std::move(s1), 1, 0, lanes, pol); return out; } } // namespace TEST(Mux, StatsCountHeaders) { asio::io_context io; auto m = make_mux_pair(io, 2); std::vector out(100, 7), in(100); std::atomic left{2}; m.a->async_write(0, out.data(), out.size(), [&](auto) { --left; }); m.b->async_read(0, in.data(), in.size(), [&](auto) { --left; }); pump(io, left); const auto s = m.a->stats(); EXPECT_EQ(s.payload_out, 100u); EXPECT_EQ(s.bytes_out, 110u); EXPECT_EQ(s.frames_out, 1u); EXPECT_EQ(in, out); } TEST(Mux, SmallWriteIsNotQueuedBehindLargeWrite) { asio::io_context io; dpf::net::wire_policy pol; pol.chunk_bytes = 16 << 10; pol.window_bytes = 0; auto m = make_mux_pair(io, 2, pol); std::vector big(8u << 20, 1), small(8, 2); std::vector big_in(big.size()), small_in(8); std::atomic left{2}; std::atomic order{0}; int big_at = 0, small_at = 0; m.a->async_write(0, big.data(), big.size(), [](auto) {}); m.a->async_write(1, small.data(), small.size(), [](auto) {}); m.b->async_read(0, big_in.data(), big_in.size(), [&](auto) { big_at = ++order; --left; }); m.b->async_read(1, small_in.data(), small_in.size(), [&](auto) { small_at = ++order; --left; }); pump(io, left); EXPECT_EQ(small_at, 1); EXPECT_EQ(big_at, 2); EXPECT_EQ(small_in, small); EXPECT_EQ(big_in, big); EXPECT_GT(m.a->stats().frames_out, 100u); } TEST(Mux, DestroyAfterWriteStillDelivers) { asio::io_context io; auto m = make_mux_pair(io, 1); std::vector out(4u << 20, 9), in(out.size()); m.a->async_write(0, out.data(), out.size(), [](auto) {}); m.a.reset(); std::atomic left{1}; std::error_code rec; m.b->async_read(0, in.data(), in.size(), [&](const std::error_code & ec) { rec = ec; --left; }); pump(io, left); EXPECT_FALSE(rec); EXPECT_EQ(in, out); } TEST(Parallel, PerLaneWindows) { asio::io_context io; std::atomic port{0}; std::vector a, b; std::thread t([&] { a = dpf::net::accept_parallel_tcp(io, port, 2); }); while (port.load() == 0) std::this_thread::yield(); b = dpf::net::connect_parallel_tcp(io, "127.0.0.1", port, 2); t.join(); dpf::net::wire_policy pol; pol.window_bytes = 1000; dpf::net::async_parallel_stream_array pa(io, std::move(a), pol); dpf::net::async_parallel_stream_array pb(io, std::move(b), pol); EXPECT_EQ(pa.lane_window_bytes(0), 1000u); EXPECT_EQ(pa.window_bytes(), 2000u); pa.set_window_bytes(500); EXPECT_EQ(pa.lane_window_bytes(1), 500u); std::vector out(64, 3), in(64); std::atomic left{2}; pa.async_write(1, out.data(), out.size(), [&](auto) { --left; }); pb.async_read(1, in.data(), in.size(), [&](auto) { --left; }); pump(io, left); EXPECT_EQ(in, out); EXPECT_EQ(pa.stats().bytes_out, 68u); } // --------------------------------------------------------------------------- // Synchronous mux: same wire as async mux, no flush deadlock // --------------------------------------------------------------------------- TEST(SyncMux, InteroperatesWithAsyncMux) { // Blocking sync faces need one side to read first so the peer's write can // complete (the old fd mux pumped both directions while blocked). std::atomic port{0}; std::vector from_sync(3000, 5), from_async(5000, 6); std::vector got_sync(from_async.size()), got_async(from_sync.size()); std::exception_ptr err; std::mutex err_mu; auto note = [&](std::exception_ptr e) { std::lock_guard lock(err_mu); if (!err) err = std::move(e); }; std::thread sync_side([&] { try { dpf::run::tcp_pair_mux(1, "127.0.0.1", port, 2, [&](unsigned, dpf::net::mux_stream_array & mux) { mux.read(0, got_sync.data(), got_sync.size()); mux.write(1, from_sync.data(), from_sync.size()); mux.flush(1); }); } catch (...) { note(std::current_exception()); } }); try { dpf::run::tcp_pair_mux(0, "127.0.0.1", port, 2, [&](unsigned, dpf::net::mux_stream_array & mux) { mux.write(0, from_async.data(), from_async.size()); mux.flush(0); mux.read(1, got_async.data(), got_async.size()); }); } catch (...) { note(std::current_exception()); } sync_side.join(); if (err) std::rethrow_exception(err); EXPECT_EQ(got_async, from_sync); EXPECT_EQ(got_sync, from_async); } TEST(SyncMux, LargeCrossFlushDoesNotDeadlock) { std::atomic port{0}; std::vector big(4u << 20); for (std::size_t i = 0; i < big.size(); ++i) big[i] = static_cast(i); std::atomic ok{0}; auto side = [&](unsigned party) { dpf::run::tcp_pair_mux(party, "127.0.0.1", port, 1, [&](unsigned, dpf::net::mux_stream_array & mux) { mux.write(0, big.data(), big.size()); mux.flush(0); std::vector in(big.size()); mux.read(0, in.data(), in.size()); if (in == big) ++ok; }); }; std::thread t0([&] { side(0); }); std::thread t1([&] { side(1); }); t0.join(); t1.join(); EXPECT_EQ(ok.load(), 2); } // --------------------------------------------------------------------------- // Sessions: static tables, deadlines, per-edge transport, reconnect // --------------------------------------------------------------------------- TEST(Session, ConnectHasADeadline) { asio::io_context io; asio::ip::tcp::socket s(io); const auto port = free_port(); const auto t0 = clock_type::now(); EXPECT_THROW(dpf::net::connect_until(s, "127.0.0.1", port, std::chrono::milliseconds(200)), std::system_error); EXPECT_LT(clock_type::now() - t0, std::chrono::seconds(2)); } TEST(Session, StaticTableAnyStartOrder) { const std::vector table = { {"127.0.0.1", free_port()}, {"127.0.0.1", free_port()}}; std::atomic ok{0}; auto run = [&](unsigned me, std::chrono::milliseconds delay) { std::this_thread::sleep_for(delay); asio::io_context io; dpf::net::party_session s(io, me, 2, std::size_t{1}); s.join(table); std::uint64_t mine = me + 10, got = 0; std::atomic left{2}; s.peer(1 - me).async_write(0, &mine, 8, [&](auto) { --left; }); s.peer(1 - me).async_read(0, &got, 8, [&](auto) { --left; }); pump(io, left); if (got == 11u - me) ++ok; }; std::thread late([&] { run(0, std::chrono::milliseconds(300)); }); std::thread early([&] { run(1, std::chrono::milliseconds(0)); }); late.join(); early.join(); EXPECT_EQ(ok.load(), 2); } TEST(Session, TransportMismatchIsNamed) { auto ports = dpf::net::make_mesh_ports(2); std::string e0, e1; auto run = [&](unsigned me, dpf::net::transport t, std::string & err) { try { asio::io_context io; dpf::net::session_options so; so.n_lanes = 2; so.limits.accept = std::chrono::milliseconds(2000); dpf::net::party_session s(io, me, 2, so); s.set_edge_transport(1 - me, t); s.join("127.0.0.1", ports); } catch (const std::exception & e) { err = e.what(); } }; std::thread t0([&] { run(0, dpf::net::transport::mux, e0); }); std::thread t1([&] { run(1, dpf::net::transport::parallel, e1); }); t0.join(); t1.join(); const std::string both = e0 + " | " + e1; EXPECT_NE(both.find("transport"), std::string::npos) << both; } TEST(Session, ParallelAndSctpEdges) { std::vector kinds = {dpf::net::transport::parallel}; if (dpf::net::sctp_available()) kinds.push_back(dpf::net::transport::sctp); for (auto kind : kinds) { auto ports = dpf::net::make_mesh_ports(2); std::atomic ok{0}; std::string err; std::mutex mu; auto run = [&](unsigned me) { try { asio::io_context io; dpf::net::session_options so; so.n_lanes = 3; so.kind = kind; // SCTP links cannot be encrypted. so.security.encrypt = kind != dpf::net::transport::sctp; dpf::net::party_session s(io, me, 2, so); s.join("127.0.0.1", ports); std::vector out(1000, static_cast(me + 1)); std::vector in(1000); std::atomic left{2}; s.peer(1 - me).async_write(2, out.data(), out.size(), [&](auto) { --left; }); s.peer(1 - me).async_read(2, in.data(), in.size(), [&](auto) { --left; }); pump(io, left); if (in[0] == static_cast(2 - me) && s.edge_stats(1 - me).payload_out == 1000u) ++ok; } catch (const std::exception & e) { std::lock_guard lock(mu); err = e.what(); } }; std::thread t0([&] { run(0); }); std::thread t1([&] { run(1); }); t0.join(); t1.join(); EXPECT_EQ(ok.load(), 2) << dpf::net::transport_name(kind) << ": " << err; } } TEST(Session, ReconnectResumesMidPlan) { const int steps = 5; auto c0 = make_chain(0, steps); auto c1 = make_chain(1, steps); auto ports = dpf::net::make_mesh_ports(2); std::uint64_t result[2] = {0, 0}; std::uint64_t resumes[2] = {0, 0}; std::string err; std::mutex mu; auto run = [&](unsigned me, const chain & ch) { try { asio::io_context io; dpf::net::party_session s(io, me, 2, std::size_t{steps}); s.join("127.0.0.1", ports); int calls = 0; std::map k{ {k_inc, [&, base = inc_kernel()](std::uint32_t op, const std::vector & ns, const std::vector & in, dpf::protocol::block_span out, std::size_t lanes) { if (me == 0 && ++calls == 2) s.peer(1).close(); base(op, ns, in, out, lanes); }}}; dpf::net::sink_options so; so.reconnect = s.reconnector(1 - me); dpf::net::async_round_sink sink(s.peer(1 - me), ch.plan.slot_bytes_all(), 1, so); std::vector> v(ch.plan.nodes().size()); put(v, ch.x, me + 1); dpf::protocol::drive_via_schedule(ch.plan, sink, v, k, me); result[me] = get(v, ch.out); resumes[me] = sink.stats().resumes; } catch (const std::exception & e) { std::lock_guard lock(mu); err += std::string(e.what()) + "; "; } }; std::thread t0([&] { run(0, c0); }); std::thread t1([&] { run(1, c1); }); t0.join(); t1.join(); ASSERT_TRUE(err.empty()) << err; EXPECT_EQ(result[0], chain_value(1, 2, steps)); EXPECT_EQ(result[1], chain_value(1, 2, steps)); EXPECT_EQ(resumes[0], 1u); EXPECT_EQ(resumes[1], 1u); } TEST(Session, DealerLinkHasItsOwnEpoch) { asio::io_context dio, pio; std::atomic port{0}; std::string err; std::mutex mu; std::thread dealer([&] { try { dpf::net::dealer_session d(dio, 1); port.store(d.listen()); d.accept_parties(); std::uint64_t v = 5; std::atomic left{1}; d.party(0).async_write(0, &v, 8, [&](auto) { --left; }); pump(dio, left); d.reconnect(0); v = 6; left = 1; d.party(0).async_write(0, &v, 8, [&](auto) { --left; }); pump(dio, left); } catch (const std::exception & e) { std::lock_guard lock(mu); err += e.what(); } }); while (port.load() == 0) std::this_thread::yield(); dpf::net::party_session p(pio, 0, 2, std::size_t{1}); p.connect_dealer("127.0.0.1", port.load()); std::uint64_t got = 0; std::atomic left{1}; p.dealer().async_read(0, &got, 8, [&](auto) { --left; }); pump(pio, left); EXPECT_EQ(got, 5u); p.reconnect_dealer(); left = 1; p.dealer().async_read(0, &got, 8, [&](auto) { --left; }); pump(pio, left); dealer.join(); EXPECT_TRUE(err.empty()) << err; EXPECT_EQ(got, 6u); } // --------------------------------------------------------------------------- // N-party runner and harness // --------------------------------------------------------------------------- TEST(Runner, TwoPartyOnEveryTransport) { std::vector kinds = {dpf::net::transport::async_memory, dpf::net::transport::local, dpf::net::transport::mux, dpf::net::transport::parallel}; if (dpf::net::sctp_available()) kinds.push_back(dpf::net::transport::sctp); const int steps = 4; for (auto kind : kinds) { auto c0 = make_chain(0, steps); auto c1 = make_chain(1, steps); dpf::app::party_values v0(c0.plan.nodes().size()), v1(c1.plan.nodes().size()); put(v0, c0.x, 3); put(v1, c1.x, 4); dpf::app::run_config cfg; cfg.kind = kind; cfg.n_lanes = 2; cfg.security.encrypt = kind != dpf::net::transport::sctp; std::map k{{k_inc, inc_kernel()}}; const auto r = dpf::run_two_party(c0.plan, c1.plan, v0, v1, k, cfg); EXPECT_EQ(get(v0, c0.out), chain_value(3, 4, steps)) << dpf::net::transport_name(kind); EXPECT_EQ(get(v1, c1.out), chain_value(3, 4, steps)) << dpf::net::transport_name(kind); EXPECT_GT(r.wire[0].payload_out, 0u) << dpf::net::transport_name(kind); if (kind == dpf::net::transport::async_memory || kind == dpf::net::transport::sctp) EXPECT_EQ(r.wire[0].bytes_out, r.wire[0].payload_out); else EXPECT_GT(r.wire[0].bytes_out, r.wire[0].payload_out) << dpf::net::transport_name(kind); } } TEST(Runner, InstancesAndComputePool) { const int steps = 3; auto c0 = make_chain(0, steps); auto c1 = make_chain(1, steps); dpf::app::party_values v0(c0.plan.nodes().size()), v1(c1.plan.nodes().size()); v0[c0.x.id].assign(8 * 4, 0); v1[c1.x.id].assign(8 * 4, 0); for (std::size_t l = 0; l < 4; ++l) { std::uint64_t a = l, b = 10; std::memcpy(v0[c0.x.id].data() + 8 * l, &a, 8); std::memcpy(v1[c1.x.id].data() + 8 * l, &b, 8); } dpf::app::run_config cfg; cfg.instances = 4; cfg.compute_threads = 2; cfg.framing = dpf::net::framing_mode::always; std::map k{{k_inc, inc_kernel()}}; (void)dpf::run_two_party(c0.plan, c1.plan, v0, v1, k, cfg); for (std::size_t l = 0; l < 4; ++l) EXPECT_EQ(get(v0, c0.out, l), chain_value(l, 10, steps)); } TEST(Runner, StarOnEveryTransport) { std::vector kinds = {dpf::net::transport::async_memory, dpf::net::transport::local, dpf::net::transport::mux, dpf::net::transport::parallel}; if (dpf::net::sctp_available()) kinds.push_back(dpf::net::transport::sctp); // pirsona_bitmore_fetch(L) is a star over 2^L servers. constexpr std::size_t L = 2; constexpr std::size_t n = std::size_t{1} << L; constexpr std::size_t query = 16 * L; for (auto kind : kinds) { auto seeds = std::make_shared>>(n); auto answers = std::make_shared>>(n); for (std::size_t i = 0; i < n; ++i) { (*seeds)[i].assign(query, static_cast(i + 1)); (*answers)[i].assign(8, static_cast(0x40 + i)); } auto client = dpf::protocol::pirsona_bitmore_fetch(L, 16, 8, seeds, answers); dpf::app::run_config cfg; cfg.kind = kind; cfg.n_lanes = 1; cfg.wait_timeout = std::chrono::seconds(10); cfg.security.encrypt = kind != dpf::net::transport::sctp; EXPECT_NO_THROW(dpf::session::drive_async_star(n, {query, 8u}, std::move(client), [&](std::size_t i) { return dpf::protocol::star_server_reply_rounds(query, 8, (*answers)[i]); }, cfg)) << dpf::net::transport_name(kind); } } TEST(Runner, ShortInputIsRejectedNotZeroed) { auto c0 = make_chain(0, 1); auto c1 = make_chain(1, 1); dpf::app::party_values v0(c0.plan.nodes().size()), v1(c1.plan.nodes().size()); put(v0, c0.x, 1); put(v1, c1.x, 2); dpf::app::run_config cfg; cfg.instances = 4; std::map k{{k_inc, inc_kernel()}}; try { (void)dpf::run_two_party(c0.plan, c1.plan, v0, v1, k, cfg); FAIL() << "8-byte input for 4 instances should be rejected"; } catch (const std::exception & e) { EXPECT_NE(std::string(e.what()).find("input node"), std::string::npos) << e.what(); } } TEST(Runner, ThreePartyDealerRingAndPeer) { struct built { dpf::protocol::plan plan; dpf::protocol::node y, ring, pad, mask, x, open; }; auto make = [](std::size_t party) { dpf::protocol::composer c(party); built b; b.y = c.input(domain::y, 8); b.ring = c.rss_from_y(b.y); b.pad = c.input(domain::a, 8); b.mask = c.dealer_deliver(b.pad); b.x = c.input(domain::a, 8); b.open = c.exchange(b.x); b.plan = c.schedule(); return b; }; for (auto kind : {dpf::net::transport::async_memory, dpf::net::transport::mux}) { built b[3] = {make(0), make(1), make(2)}; std::vector values(3); for (int i = 0; i < 3; ++i) { values[i].resize(b[i].plan.nodes().size()); put(values[i], b[i].y, 100 + i); put(values[i], b[i].pad, i == 2 ? 77 : 0); put(values[i], b[i].x, i == 0 ? 20 : 22); } dpf::app::run_config cfg; cfg.kind = kind; (void)dpf::app::run_parties({b[0].plan, b[1].plan, b[2].plan}, values, {}, cfg); for (int i = 0; i < 3; ++i) { std::uint64_t own = 0, next = 0; std::memcpy(&own, values[i][b[i].ring.id].data(), 8); std::memcpy(&next, values[i][b[i].ring.id].data() + 8, 8); EXPECT_EQ(own, 100u + i) << dpf::net::transport_name(kind); EXPECT_EQ(next, 100u + (i + 1) % 3) << dpf::net::transport_name(kind); } for (int i = 0; i < 2; ++i) { EXPECT_EQ(get(values[i], b[i].mask), 77u); EXPECT_EQ(get(values[i], b[i].open), 42u); } } } TEST(Runner, SeparateProcessesFromAStaticTable) { const std::vector peers = { "127.0.0.1:" + std::to_string(free_port()), "127.0.0.1:" + std::to_string(free_port())}; const std::string list = peers[0] + "," + peers[1]; const int steps = 3; pid_t kids[2]; for (int me = 0; me < 2; ++me) { const pid_t pid = ::fork(); ASSERT_GE(pid, 0); if (pid == 0) { int code = 1; try { const std::string party = "--party=" + std::to_string(me); const std::string pl = "--peers=" + list; const char * argv[] = {"node", party.c_str(), pl.c_str(), "--transport=mux", "--lanes=2"}; const auto args = dpf::app::parse_node_args(5, const_cast(argv), dpf::app::run_config{}); auto ch = make_chain(static_cast(me), steps); dpf::app::party_values v(ch.plan.nodes().size()); put(v, ch.x, me == 0 ? 5 : 6); std::map k{ {k_inc, inc_kernel()}}; (void)dpf::app::run_node(args, ch.plan, v, k); code = get(v, ch.out) == chain_value(5, 6, steps) ? 0 : 2; } catch (const std::exception & e) { std::fprintf(stderr, "party %d: %s\n", me, e.what()); code = 3; } ::_exit(code); } kids[me] = pid; } for (int me = 0; me < 2; ++me) { int status = 0; ASSERT_EQ(::waitpid(kids[me], &status, 0), kids[me]); ASSERT_TRUE(WIFEXITED(status)); EXPECT_EQ(WEXITSTATUS(status), 0) << "party " << me; } } TEST(Harness, TrialsConfigAndWireRecorded) { auto c0 = make_chain(0, 2); auto c1 = make_chain(1, 2); std::vector inputs(2); inputs[0].resize(c0.plan.nodes().size()); inputs[1].resize(c1.plan.nodes().size()); put(inputs[0], c0.x, 1); put(inputs[1], c1.x, 2); std::map k{{k_inc, inc_kernel()}}; dpf::app::run_config cfg; cfg.kind = dpf::net::transport::mux; cfg.warmup = 1; cfg.trials = 3; dpf::experiment ex("chain", "p0"); const auto cost = dpf::app::exercise_parties({c0.plan, c1.plan}, inputs, k, &ex, cfg); EXPECT_EQ(ex.trials().size(), 3u); EXPECT_GT(cost.wall_ns, 0u); EXPECT_GT(cost.wire_out, cost.bytes); EXPECT_EQ(ex.wire().bytes_out, cost.wire_out); bool has_transport = false; for (const auto & kv : ex.config()) has_transport = has_transport || (kv.first == "transport" && kv.second == "mux"); EXPECT_TRUE(has_transport); const std::string dir = "/tmp/libdpf_net_control_" + std::to_string(::getpid()); ex.write_csv(dir); for (const char * f : {"/config.csv", "/trials.csv", "/wire.csv"}) { std::ifstream in(dir + f); EXPECT_TRUE(static_cast(in)) << f; } (void)::system(("rm -rf '" + dir + "'").c_str()); } TEST(Prep, ShippedOverADealerSession) { dpf::prep::demand d; d.ring_triples = 2; dpf::app::run_config cfg; cfg.kind = dpf::net::transport::mux; auto shipped = dpf::session::ship_prep(d, cfg); std::uint8_t a0[8], b0[8], c0[8], a1[8], b1[8], c1[8]; shipped.party0.take_ring(a0, b0, c0); shipped.party1.take_ring(a1, b1, c1); std::uint64_t ta[2], tb[2], tc[2]; std::memcpy(&ta[0], a0, 8); std::memcpy(&tb[0], b0, 8); std::memcpy(&tc[0], c0, 8); std::memcpy(&ta[1], a1, 8); std::memcpy(&tb[1], b1, 8); std::memcpy(&tc[1], c1, 8); EXPECT_EQ((ta[0] + ta[1]) * (tb[0] + tb[1]), tc[0] + tc[1]); } TEST(Sink, WindowGatesPipelining) { asio::io_context io0, io1; auto ends = dpf::net::make_async_dual_memory_stream_pair(io0, io1, 1, 64); dpf::net::sink_options so; so.drain_timeout = std::chrono::milliseconds(300); dpf::net::async_round_sink a(ends.first, {256}, 1, so); std::vector slot(256, 1); a.submit(0, 0, slot.data(), slot.size()); EXPECT_THROW(a.flush_round(0), std::runtime_error); EXPECT_FALSE(a.can_send_ahead()); }