#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "dpf/beaver.hpp" #include "dpf/compose.hpp" #include "dpf/iknp.hpp" #include "dpf/app_flow.hpp" #include "dpf/app_plans.hpp" #include "dpf/experiment.hpp" #include "dpf/iknp_graphs.hpp" #include "dpf/mesh_apps.hpp" #include "dpf/net/edge_mesh.hpp" #include "dpf/net/memory_sink.hpp" #include "dpf/net/sink_exchange.hpp" #include "dpf/pad_graphs.hpp" #include "dpf/prg_aes.hpp" #include "dpf/random.hpp" #include "dpf/session_host.hpp" namespace { using dpf::protocol::composer; using dpf::protocol::domain; using dpf::protocol::effect; using dpf::protocol::kernel_fn; using dpf::protocol::node; namespace opcodes = dpf::protocol::opcodes; using dpf::protocol::op_flags; using dpf::net::make_memory_sink_pair; TEST(Compose, SharedBlindInterned) { composer c(0); auto x = c.input(domain::a, 8); int blind_calls = 0; auto b0 = c.blind(x, 42); auto b1 = c.blind(x, 42); EXPECT_EQ(b0, b1); auto p = c.schedule(); EXPECT_EQ(p.effect_count(effect::blind), 1u); std::map kernels; kernels[42] = [&](std::uint32_t, const std::vector & nodes, const std::vector &, dpf::protocol::block_span out, std::size_t) { ++blind_calls; for (std::size_t i = 0; i < out.lanes; ++i) { std::uint64_t v = 0x1111; std::memcpy(out.at(i), &v, 8); } EXPECT_EQ(nodes.size(), 1u); }; auto [sink0, sink1] = make_memory_sink_pair(1, p.slot_bytes_all()); (void)sink1; std::vector> values; values.resize(c.node_count()); values[x.id].resize(8); std::uint64_t xv = 7; std::memcpy(values[x.id].data(), &xv, 8); dpf::protocol::drive(p, sink0, values, kernels, 0); EXPECT_EQ(blind_calls, 1); std::uint64_t got = 0; std::memcpy(&got, values[b0.id].data(), 8); EXPECT_EQ(got, 0x1111u); } TEST(Compose, LevelWalkSharesExpands) { composer c(0); auto seed = c.input(domain::fss, 16); auto a = c.fss_point(seed, 2, 16); const auto shared = c.schedule().effect_count(effect::expand); // depth 2: one fused L||R expand per level (hand DPF PRG stretch). EXPECT_EQ(shared, 2u); auto b = c.fss_point(seed, 2, 16); EXPECT_EQ(c.schedule().effect_count(effect::expand), shared); auto other = c.input(domain::fss, 16); auto d = c.fss_cmp(other, 2, 16); EXPECT_EQ(c.schedule().effect_count(effect::expand), shared * 2); EXPECT_EQ(c.domain_of(a), domain::b); EXPECT_EQ(c.domain_of(b), domain::b); EXPECT_EQ(c.domain_of(d), domain::a); } TEST(Compose, ExchangeWaves) { composer c(0); auto x = c.input(domain::a, 8); auto e0 = c.exchange(x); auto e1 = c.exchange(c.compute(opcodes::user_base + 10, {e0}, domain::a, 8)); auto e2 = c.exchange(c.compute(opcodes::user_base + 10, {e1}, domain::a, 8)); (void)e2; auto p = c.schedule(); EXPECT_EQ(p.rounds(), 3u); composer c2(0); auto a = c2.input(domain::a, 8); auto b = c2.input(domain::a, 8); auto o0 = c2.exchange(a); auto o1 = c2.exchange(b); (void)o0; (void)o1; auto p2 = c2.schedule(); EXPECT_EQ(p2.rounds(), 1u); } TEST(Compose, ExchangeReconstructsOnMemorySink) { composer c0(0); 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); (void)e0; (void)e1; auto p0 = c0.schedule(); auto p1 = c1.schedule(); ASSERT_EQ(p0.rounds(), 1u); ASSERT_EQ(p1.rounds(), 1u); auto [sink0, sink1] = make_memory_sink_pair(1, p0.slot_bytes_all()); std::uint64_t a = 3, b = 5; sink0.submit(0, 0, reinterpret_cast(&a), 8); sink1.submit(0, 0, reinterpret_cast(&b), 8); sink0.flush(); ASSERT_TRUE(sink0.peer_ready(0, 0)); ASSERT_TRUE(sink1.peer_ready(0, 0)); std::uint64_t peer0 = 0, peer1 = 0; sink0.read_peer(0, 0, reinterpret_cast(&peer0), 8); sink1.read_peer(0, 0, reinterpret_cast(&peer1), 8); EXPECT_EQ(a + peer0, 8u); EXPECT_EQ(b + peer1, 8u); } TEST(Compose, CommutativeComputeInterned) { composer c(0); auto x = c.input(domain::a, 8); auto y = c.input(domain::a, 8); auto p0 = c.compute(99, {x, y}, domain::a, 8, op_flags::commutative); auto p1 = c.compute(99, {y, x}, domain::a, 8, op_flags::commutative); EXPECT_EQ(p0, p1); } TEST(Compose, SimdGroupOneKernelCall) { composer c(0); auto nodes = c.fan(8, [&](std::size_t) { return c.input(domain::a, 8); }); std::vector outs; for (auto n : nodes) outs.push_back(c.compute(77, {n}, domain::a, 8)); auto p = c.schedule(); int calls = 0; std::size_t total_nodes = 0; std::map kernels; kernels[77] = [&](std::uint32_t, const std::vector & ns, const std::vector & ins, dpf::protocol::block_span out, std::size_t sink_lanes) { ++calls; total_nodes += ns.size(); EXPECT_EQ(ns.size(), 8u); EXPECT_EQ(out.lanes, 8u * sink_lanes); EXPECT_EQ(ins.size(), 1u); for (std::size_t i = 0; i < out.lanes; ++i) std::memset(out.at(i), 1, 8); }; auto slots = p.slot_bytes_all(); if (slots.empty()) slots.push_back(0); auto [sink0, sink1] = make_memory_sink_pair(1, std::move(slots)); (void)sink1; std::vector> values(c.node_count()); for (auto n : nodes) values[n.id].assign(8, 0); dpf::protocol::drive(p, sink0, values, kernels, 0); EXPECT_EQ(calls, 1); EXPECT_EQ(total_nodes, 8u); ASSERT_GE(p.waves(), 1u); std::size_t grouped = 0; for (const auto & g : p.wave(0).groups) { if (g.opcode == 77) grouped += g.nodes.size(); } EXPECT_EQ(grouped, 8u); (void)outs; } TEST(Compose, DomainConversionInternedAndRejected) { composer c(0); auto leaf = c.input(domain::b, 8); auto a0 = c.as(leaf, domain::a); auto a1 = c.as(leaf, domain::a); EXPECT_EQ(a0, a1); auto p = c.schedule(); EXPECT_EQ(p.conversion_count(domain::b, domain::a), 1u); EXPECT_THROW(c.as(leaf, domain::rss), std::invalid_argument); // Inverse fold auto back = c.as(a0, domain::b); EXPECT_EQ(back, leaf); } TEST(Compose, RssProductInterned) { composer c(0); auto x = c.input(domain::rss, 16); auto y = c.input(domain::rss, 16); auto f0 = c.rss_product(x, y); auto f1 = c.rss_product(y, x); // commutative EXPECT_EQ(f0, f1); EXPECT_EQ(c.domain_of(f0), domain::y); auto own = c.input(domain::y, 8); auto next = c.input(domain::y, 8); auto r = c.y2rss(own, next); EXPECT_EQ(c.domain_of(r), domain::rss); } TEST(Compose, AbyProductSharesBlind) { composer c(0); auto & s = c.aby(); auto x = s.input(); auto y = s.input(); auto z = s.input(); auto xy = s.product(x, y); auto xz = s.product(x, z); (void)xy; (void)xz; // One blind for x across both products. s.sample(); EXPECT_EQ(s.round_of(xy), s.round_of(xz)); // opened wires create barrier exchanges on the composer auto nxy = c.opened(xy); auto nxz = c.opened(xz); EXPECT_EQ(c.domain_of(nxy), domain::a); EXPECT_EQ(c.domain_of(nxz), domain::a); auto barriers = s.exchange_barriers(); EXPECT_FALSE(barriers.empty()); } // Practical gap (SUBLEQ scale / Grotto η): opening a product must schedule // every prior δ flush, not only the product's barrier. TEST(Compose, BeaverBarrierChainMaterializesPriors) { composer c(0); auto & s = c.aby(); auto x = s.input(); auto y = s.input(); auto z = s.product(x, y); // Nested use so z is δ-opened in a later barrier (hand SUBLEQ scale path). auto w = s.product(z, x); (void)w; s.sample(); const auto barriers = s.exchange_barriers(); ASSERT_GE(barriers.size(), 2u); auto nz = c.opened(z); (void)nz; // Opening z must pull in every prior flush (factor open), not only z's. std::size_t beaver_ex = 0; for (std::uint32_t id = 0; id < c.node_count(); ++id) { if (c.effect_of(node{id}) != effect::exchange) continue; // Beaver barrier opcodes are beaver_delta + index. ++beaver_ex; } // FSS-free composer: every exchange is a beaver barrier. EXPECT_GE(beaver_ex, 2u); } // Practical gap (Duoram / PIR + Beaver): beaver waves must not hitch onto // unrelated FSS exchange node ids (old fake inputs = {sess, barrier_index}). TEST(Compose, BeaverBarrierWavesIndependentOfFssNodeIds) { composer c(0); auto seed = c.input(domain::fss, 16); auto leaf = c.fss_point(seed, 3, 16); (void)leaf; const auto fss_only = c.schedule(); // depth d → d exchange waves + final consumer wave. EXPECT_EQ(fss_only.effect_count(effect::exchange), 3u); EXPECT_EQ(fss_only.rounds(), 3u); // == exchange_waves EXPECT_EQ(fss_only.exchange_waves(), 3u); auto & s = c.aby(); auto x = s.input(); auto y = s.input(); auto z = s.product(x, y); s.sample(); auto nz = c.opened(z); (void)nz; auto p = c.schedule(); // Independent beaver opens add exchange nodes; they must not disappear // or be ordered solely by colliding with FSS node ids. EXPECT_GT(p.effect_count(effect::exchange), 3u); } // Practical gap (grotto_signum_lut): N comparison walks share expands when // seeded alike; packed same-depth opens are one wave per level. TEST(Compose, GrottoShapedFanSharesSeedExpands) { composer c(0); auto seed = c.input(domain::fss, 16); constexpr std::size_t n = 7; auto leaves = c.fan(n, [&](std::size_t) { return c.fss_cmp(seed, 4, 16); }); EXPECT_EQ(leaves.size(), n); auto p = c.schedule(); // One fused expand per level for the shared seed, not n×depth. EXPECT_EQ(p.effect_count(effect::expand), 4u); EXPECT_EQ(p.effect_count(effect::exchange), 4u); EXPECT_EQ(p.rounds(), 4u); // == exchange_waves (no empty sink round) } // Latency objective: keep sign×linear in one round (Pika / online Grotto). // Prep objective (default) still peels for Appendix-E savings. TEST(Compose, AbySessionUsesRoundAwareSchedule) { composer c(0); auto & s = c.aby(); EXPECT_EQ(s.get_schedule_objective(), dpf::beavers::schedule_objective::rounds); auto sgn = s.input(); auto x = s.input(); auto a0 = s.input(); auto a1 = s.input(); auto lin = s(sgn * (a1 * x + a0)); EXPECT_EQ(s.round_of(lin), 1); dpf::beavers::session prep; prep.set_schedule_objective(dpf::beavers::schedule_objective::prep); auto ps = prep.input(); auto px = prep.input(); auto pa0 = prep.input(); auto pa1 = prep.input(); auto plin = prep(ps * (pa1 * px + pa0)); EXPECT_EQ(prep.round_of(plin), 2); EXPECT_LT(prep.preprocessing_count(), s.preprocessing_count()); } // Express/Sabre: sketch rides in the last CW flush — no extra exchange round. TEST(Compose, ExpressShapedSketchFusedIntoLastCw) { composer c(0); auto seed = c.input(domain::fss, 16); auto sketch = c.input(domain::a, 8); // Naive: walk, then a sketch exchange that depends on the leaf. auto leaf = c.fss_point(seed, 4, 16); auto sk_dep = c.compute(opcodes::user_base + 50, {leaf, sketch}, domain::a, 8); auto sk_ex = c.exchange(sk_dep); (void)sk_ex; auto naive_plan = c.schedule(); composer c2(0); auto seed2 = c2.input(domain::fss, 16); auto sketch2 = c2.input(domain::a, 8); auto wr = c2.fss_point_fused(seed2, 4, 16, sketch2); auto fused_plan = c2.schedule(); EXPECT_EQ(c2.domain_of(wr.leaf), domain::b); EXPECT_EQ(c2.domain_of(wr.trailer_open), domain::a); EXPECT_EQ(naive_plan.effect_count(effect::exchange), 5u); EXPECT_EQ(fused_plan.effect_count(effect::exchange), 4u); // Same leaf-wave depth, but naive pays a fifth exchange-bearing wave. EXPECT_EQ(fused_plan.rounds(), 4u); EXPECT_EQ(naive_plan.exchange_waves(), 5u); EXPECT_EQ(fused_plan.exchange_waves(), 4u); // Last CW slot carries step‖trailer. const auto & last_wave = fused_plan.wave(3); ASSERT_FALSE(last_wave.exchanges.empty()); EXPECT_EQ(fused_plan.value_bytes_of(last_wave.exchanges[0].id), 16u + 8u); EXPECT_TRUE(fused_plan.wave(4).exchanges.empty()); } TEST(Compose, BeaverExchangeBarriersMatchBatch) { dpf::beavers::session s; auto x = s.input(); auto y = s.input(); auto z = s.product(x, y); (void)z; s.sample(); auto barriers = s.exchange_barriers(); EXPECT_GE(barriers.size(), 1u); // Party path: stepper completes with same number of barriers. dpf::beavers::session s0; dpf::beavers::session s1; auto x0 = s0.input(); auto y0 = s0.input(); auto z0 = s0.product(x0, y0); auto x1 = s1.input(); auto y1 = s1.input(); auto z1 = s1.product(x1, y1); (void)z0; (void)z1; s0.sample(); // copy tape auto tape = s0.export_party(0); // Use install after sampling on dealer session - parties need tapes. // Simpler: evaluate_party_batch with matching exchange count. dpf::beavers::session dealer; auto dx = dealer.input(); auto dy = dealer.input(); auto dz = dealer.product(dx, dy); (void)dz; dealer.sample(); auto t0 = dealer.export_party(0); auto t1 = dealer.export_party(1); dpf::beavers::session p0; dpf::beavers::session p1; auto px0 = p0.input(); auto py0 = p0.input(); auto pz0 = p0.product(px0, py0); auto px1 = p1.input(); auto py1 = p1.input(); auto pz1 = p1.product(px1, py1); p0.install_party(0, t0); p1.install_party(1, t1); p0.bind_party(px0, 3); p0.bind_party(py0, 4); p1.bind_party(px1, 0); p1.bind_party(py1, 0); auto b0 = p0.exchange_barriers(); auto b1 = p1.exchange_barriers(); ASSERT_EQ(b0.size(), b1.size()); dpf::beavers::party_batch_stepper step0(p0); dpf::beavers::party_batch_stepper step1(p1); EXPECT_EQ(step0.barrier_count(), b0.size()); while (!step0.done()) { auto m0 = step0.take_local(); auto m1 = step1.take_local(); ASSERT_EQ(m0.size(), m1.size()); step0.apply_peer(m1); step1.apply_peer(m0); } EXPECT_TRUE(step1.done()); EXPECT_EQ(p0.value_party(pz0) + p1.value_party(pz1), 12u); } // --------------------------------------------------------------------------- // Flow choreography: realistic schedules and the abstractions that unlock them. // --------------------------------------------------------------------------- TEST(Compose, FlowSimpleAbyProductOneRound) { composer c(0); auto x = c.input(domain::a, 8); auto y = c.input(domain::a, 8); auto z = c.aby_product(x, y); (void)z; auto p = c.schedule(); EXPECT_EQ(p.exchange_waves(), 1u); } TEST(Compose, FlowIndepFssWalksPackSameWaves) { composer c(0); auto s0 = c.input(domain::fss, 16); auto s1 = c.input(domain::fss, 16); auto l0 = c.fss_point(s0, 5, 16); auto l1 = c.fss_point(s1, 5, 16); (void)l0; (void)l1; auto p = c.schedule(); EXPECT_EQ(p.effect_count(effect::exchange), 10u); // 5+5 slots EXPECT_EQ(p.exchange_waves(), 5u); // packed per level EXPECT_EQ(p.effect_count(effect::expand), 10u); } TEST(Compose, FlowBitMoreParallelBitWalks) { // Keyword PIR / BitMore: L independent bit keys, same depth — one wave/level. composer c(0); constexpr std::size_t L = 8; auto seeds = c.fan(L, [&](std::size_t) { return c.input(domain::fss, 16); }); auto leaves = c.fan(L, [&](std::size_t i) { return c.fss_point(seeds[i], 6, 16); }); EXPECT_EQ(leaves.size(), L); auto p = c.schedule(); EXPECT_EQ(p.exchange_waves(), 6u); EXPECT_EQ(p.effect_count(effect::exchange), L * 6u); } TEST(Compose, FlowEarlyStopDropsInteractiveLevels) { // Pika / small-output PIR: BGI Remark 3.4 packs ν low bits into the leaf. composer full(0); auto seed = full.input(domain::fss, 16); auto leaf_full = full.fss_point(seed, 8, 16); (void)leaf_full; auto p_full = full.schedule(); composer early(0); auto seed_e = early.input(domain::fss, 16); auto leaf_e = early.fss_point_early_stop(seed_e, 8, /*early_stop=*/3, 16); EXPECT_EQ(early.domain_of(leaf_e), domain::b); auto p_early = early.schedule(); EXPECT_EQ(p_full.exchange_waves(), 8u); EXPECT_EQ(p_early.exchange_waves(), 5u); EXPECT_LT(p_early.effect_count(effect::exchange), p_full.effect_count(effect::exchange)); } TEST(Compose, FlowPrefixCheckpointsNoExtraRounds) { // Poplar / idpf_agg: prefix share after each CW, still depth exchange waves. composer c(0); auto seed = c.input(domain::fss, 16); auto wr = c.level_walk_prefixes(seed, 4, 16, /*prefix_bytes=*/8); EXPECT_EQ(wr.at_level.size(), 4u); auto p = c.schedule(); EXPECT_EQ(p.exchange_waves(), 4u); EXPECT_EQ(p.effect_count(effect::exchange), 4u); for (auto pref : wr.at_level) EXPECT_EQ(c.domain_of(pref), domain::a); } TEST(Compose, FlowSizedSlotsBlockWidth) { // DCF block_width: CW only at checkpoints — narrower slots between. composer c(0); auto seed = c.input(domain::fss, 16); const std::vector slots = {16, 4, 4, 16}; auto tip = c.level_walk_sized(seed, slots); (void)tip; auto p = c.schedule(); EXPECT_EQ(p.exchange_waves(), 4u); EXPECT_EQ(p.value_bytes_of(p.wave(0).exchanges[0].id), 16u); EXPECT_EQ(p.value_bytes_of(p.wave(1).exchanges[0].id), 4u); EXPECT_EQ(p.value_bytes_of(p.wave(3).exchanges[0].id), 16u); } TEST(Compose, FlowRssProductRefreshOneRound) { // 3PC Duoram-style: local rss_mul then neighbor y-exchange → RSS. composer c(0); auto x = c.input(domain::rss, 16); auto y = c.input(domain::rss, 16); auto z = c.rss_product_replicated(x, y); EXPECT_EQ(c.domain_of(z), domain::rss); auto p = c.schedule(); EXPECT_EQ(p.exchange_waves(), 1u); EXPECT_EQ(p.effect_count(effect::exchange), 1u); } TEST(Compose, FlowRssChainTwoProductsTwoRounds) { composer c(0); auto a = c.input(domain::rss, 16); auto b = c.input(domain::rss, 16); auto d = c.input(domain::rss, 16); auto ab = c.rss_product_replicated(a, b); auto abd = c.rss_product_replicated(ab, d); EXPECT_EQ(c.domain_of(abd), domain::rss); auto p = c.schedule(); EXPECT_EQ(p.exchange_waves(), 2u); } TEST(Compose, FlowIndepRssRefreshesPackOneWave) { composer c(0); auto x0 = c.input(domain::rss, 16); auto y0 = c.input(domain::rss, 16); auto x1 = c.input(domain::rss, 16); auto y1 = c.input(domain::rss, 16); auto z0 = c.rss_product_replicated(x0, y0); auto z1 = c.rss_product_replicated(x1, y1); (void)z0; (void)z1; auto p = c.schedule(); EXPECT_EQ(p.exchange_waves(), 1u); EXPECT_EQ(p.effect_count(effect::exchange), 2u); } TEST(Compose, FlowDuoramWriteScaleWaitsForLeaf) { // FSS leaf feeds ABY scale product — beaver flush must follow the walk. composer c(0); auto seed = c.input(domain::fss, 16); auto leaf = c.fss_point(seed, 4, 16); auto scale = c.input(domain::a, 8); auto scaled = c.aby_product(leaf, scale); auto p = c.schedule(); EXPECT_GE(p.wave_of(scaled), p.wave_of(leaf)); EXPECT_GE(p.exchange_waves(), 4u); // Factor δ that imports the leaf cannot sit in an earlier exchange wave. bool saw_beaver_after_leaf = false; for (std::size_t w = 0; w < p.rounds(); ++w) { for (auto ex : p.wave(w).exchanges) { if (p.opcode_of(ex.id) < opcodes::beaver_delta) continue; if (w >= p.wave_of(leaf)) saw_beaver_after_leaf = true; } } EXPECT_TRUE(saw_beaver_after_leaf); } TEST(Compose, FlowWalkPlusIndepAbyPackEarlyWaves) { // Independent beaver product may share early CW waves (no FSS dep). composer c(0); auto seed = c.input(domain::fss, 16); auto leaf = c.fss_point(seed, 3, 16); (void)leaf; auto x = c.input(domain::a, 8); auto y = c.input(domain::a, 8); auto z = c.aby_product(x, y); (void)z; auto p = c.schedule(); // 3 CW waves; beaver δ packs into wave 0 alongside first CW. EXPECT_EQ(p.exchange_waves(), 3u); EXPECT_FALSE(p.wave(0).exchanges.empty()); EXPECT_GE(p.wave(0).exchanges.size(), 2u); } TEST(Compose, FlowSubleqFetchAndScale) { // Instruction fetch (FSS) then scale product — ABY waits for the leaf. composer c(0); auto pc_seed = c.input(domain::fss, 16); auto instr = c.fss_point(pc_seed, 5, 16); auto scale = c.input(domain::a, 8); auto scaled = c.aby_product(c.as(instr, domain::a), scale); auto & s = c.aby(); auto flag = s.input(); auto a0 = s.input(); auto a1 = s.input(); auto x = s.input(); auto lin = s(flag * (a1 * x + a0)); s.sample(); auto nlin = c.opened(lin); (void)nlin; auto p = c.schedule(); EXPECT_GE(p.wave_of(scaled), p.wave_of(instr)); EXPECT_EQ(s.round_of(lin), 1); } TEST(Compose, FlowGrottoFanPlusSketchFuse) { composer c(0); auto seed = c.input(domain::fss, 16); auto sketch = c.input(domain::a, 8); auto leaves = c.fan(4, [&](std::size_t) { return c.fss_cmp(seed, 3, 16); }); auto wr = c.fss_point_fused(seed, 3, 16, sketch); (void)leaves; (void)wr; auto p = c.schedule(); // Shared expands across fan + fused walk; 3 exchange waves total. EXPECT_EQ(p.exchange_waves(), 3u); EXPECT_EQ(p.effect_count(effect::expand), 3u); } TEST(Compose, FlowExpressMailboxAndAudit) { composer c(0); auto seed = c.input(domain::fss, 16); auto sketch = c.input(domain::a, 8); auto wr = c.fss_point_fused(seed, 6, 16, sketch); EXPECT_EQ(c.domain_of(wr.trailer_open), domain::a); auto p = c.schedule(); EXPECT_EQ(p.exchange_waves(), 6u); EXPECT_EQ(p.value_bytes_of(p.wave(5).exchanges[0].id), 24u); } TEST(Compose, FlowReshareYUsesRssFromY) { composer c(0); auto y = c.input(domain::y, 8); auto r = c.reshare(y, domain::rss); EXPECT_EQ(c.domain_of(r), domain::rss); EXPECT_EQ(c.schedule().exchange_waves(), 1u); } TEST(Compose, FlowDsWalkFourOpensPerLevel) { composer c(0); auto seed = c.input(domain::fss, 16); auto tip = c.level_walk_ds(seed, 3, 16); (void)tip; auto p = c.schedule(); // 3 levels × (blind, share, advice, AND1, AND2) = 15 exchange waves. EXPECT_EQ(p.exchange_waves(), 15u); EXPECT_EQ(p.effect_count(effect::exchange), 15u); } TEST(Compose, FlowDsWalkWithOhAndMux) { composer c(0); auto seed = c.input(domain::fss, 16); auto tip = c.level_walk_ds(seed, 2, 16, /*oh=*/true, /*lg_outputs=*/2); (void)tip; auto p = c.schedule(); // 2×(5 + 80 OH) + mux_nodes=2*(4-1)=6 → 170 + 6 = 176 exchanges. EXPECT_EQ(p.effect_count(effect::exchange), 176u); auto budget = dpf::net::compose_ds_slot_bytes(2, 16, true, 2); EXPECT_EQ(budget.size(), 176u); EXPECT_EQ(p.exchange_waves(), budget.size()); } TEST(Compose, FlowDsWalkSizedMatchesBudget) { composer c(0); auto slots = dpf::net::compose_ds_slot_bytes(3, 16, false, 0); auto tip = c.level_walk_ds_sized(c.input(domain::fss, 16), slots, false, 0); (void)tip; EXPECT_EQ(c.schedule().exchange_waves(), slots.size()); EXPECT_EQ(slots.size(), 15u); } TEST(Compose, DefaultPlanMatchesScheduleAndSink) { composer c(0); auto leaf = c.fss_point(c.input(domain::fss, 16), 3, 16); (void)leaf; auto p = c.default_plan(); auto s = c.schedule(); EXPECT_EQ(p.rounds(), s.exchange_waves()); EXPECT_EQ(p.rounds(), p.slot_bytes_all().size()); EXPECT_EQ(p.rounds(), 3u); } TEST(Compose, FlowAdaptivePrefixOneWavePerStep) { composer c(0); auto seed = c.input(domain::fss, 16); auto f = c.begin_adaptive_prefix(seed); f = c.step_adaptive_prefix(f, 16, 8); EXPECT_TRUE(f.awaiting_retain); // After step only: one open in the plan; tip is still the prior state. EXPECT_EQ(c.default_plan().rounds(), 1u); f = c.retain_adaptive_prefix(f, /*child=*/0); EXPECT_FALSE(f.awaiting_retain); f = c.step_adaptive_prefix(f, 16, 8); f = c.retain_adaptive_prefix(f, /*child=*/1); auto p = c.default_plan(); EXPECT_EQ(p.exchange_waves(), 2u); EXPECT_EQ(p.rounds(), 2u); EXPECT_EQ(f.depth_done, 2u); EXPECT_FALSE(f.awaiting_retain); } TEST(Compose, AdaptiveIncrementalDriveSkipsFlushedWaves) { composer c(0); auto f = c.begin_adaptive_prefix(c.input(domain::fss, 16)); f = c.step_adaptive_prefix(f, 16, 8); auto p = c.default_plan(); ASSERT_EQ(p.exchange_waves(), 1u); f.exchanges_flushed = p.exchange_waves(); f = c.retain_adaptive_prefix(f, 0); f = c.step_adaptive_prefix(f, 16, 8); p = c.default_plan(); ASSERT_EQ(p.exchange_waves(), 2u); auto tail = p.slot_bytes_from(f.exchanges_flushed); EXPECT_EQ(tail.size(), 1u); EXPECT_EQ(p.slot_bytes_from(0).size(), 2u); // compact_sink + from_exchange_wave: RoundSink sized to the tail only. dpf::protocol::drive_options opt; opt.from_exchange_wave = f.exchanges_flushed; opt.compact_sink = true; EXPECT_EQ(opt.from_exchange_wave, 1u); f = c.retain_adaptive_prefix(f, 1); EXPECT_EQ(f.depth_done, 2u); } TEST(Compose, ReconstructOpenDomainAlgebra) { std::uint8_t out[8]; std::uint64_t a = 10, b = 3; dpf::protocol::detail::reconstruct_open(domain::a, 0, reinterpret_cast(&a), reinterpret_cast(&b), 8, out); std::uint64_t sum = 0; std::memcpy(&sum, out, 8); EXPECT_EQ(sum, 13u); dpf::protocol::detail::reconstruct_open(domain::b, 0, reinterpret_cast(&a), reinterpret_cast(&b), 8, out); std::uint64_t diff0 = 0; std::memcpy(&diff0, out, 8); EXPECT_EQ(diff0, 7u); dpf::protocol::detail::reconstruct_open(domain::b, 1, reinterpret_cast(&b), reinterpret_cast(&a), 8, out); std::uint64_t diff1 = 0; std::memcpy(&diff1, out, 8); EXPECT_EQ(diff1, 7u); // party1: peer - mine = 10 - 3 dpf::protocol::detail::reconstruct_open(domain::y, 0, reinterpret_cast(&a), reinterpret_cast(&b), 8, out); std::uint64_t y = 0; std::memcpy(&y, out, 8); EXPECT_EQ(y, 3u); } TEST(Compose, FlowMultipointFanPacksAnswers) { composer c(0); auto mr = c.multipoint_fan(3, [&](std::size_t) { return c.input(domain::fss, 16); }, 4, 16, 8); EXPECT_EQ(mr.leaves.size(), 3u); auto p = c.schedule(); // 3×4 CW opens pack per level (4 waves) + 1 answer pack = 5. EXPECT_EQ(p.exchange_waves(), 5u); EXPECT_EQ(c.effect_of(mr.answers_open), effect::exchange); } TEST(Compose, FlowMultiLaneAbyIndependentBarriers) { composer c(0); auto x0 = c.input(domain::a, 8); auto y0 = c.input(domain::a, 8); auto x1 = c.input(domain::a, 8); auto y1 = c.input(domain::a, 8); auto z0 = c.aby_product(x0, y0, /*lane=*/0); auto z1 = c.aby_product(x1, y1, /*lane=*/1); (void)z0; (void)z1; auto p = c.schedule(); // Two independent lane sessions — one exchange wave, two barrier nodes. EXPECT_EQ(p.exchange_waves(), 1u); EXPECT_EQ(p.effect_count(effect::exchange), 2u); } TEST(Compose, FlowDeferExpandZeroOnlineRounds) { composer c(0); auto seed = c.input(domain::fss, 16); auto buf = c.defer_expand(seed, 5, 16); auto rotated = c.rotate_share(buf, 3); (void)rotated; EXPECT_EQ(c.schedule().exchange_waves(), 0u); } TEST(Compose, FlowLeafLaterThenApply) { composer c(0); auto seed = c.input(domain::fss, 16); auto ll = c.leaf_later_walk(seed, 3, 16); auto F = c.input(domain::a, 16); auto done = c.apply_leaf_correction(ll.values, ll.control, F); (void)done; auto p = c.schedule(); EXPECT_EQ(p.exchange_waves(), 3u); // path CWs only; apply is local } TEST(Compose, ReshareAcrossPartyCountRefusesOpen) { composer c(0); auto a = c.input(domain::a, 8); auto rss = c.reshare(a, domain::rss); EXPECT_EQ(c.domain_of(rss), domain::rss); EXPECT_EQ(c.schedule().exchange_waves(), 1u); EXPECT_THROW(c.reshare(rss, domain::a), std::invalid_argument); composer c2(0); auto fresh = c2.reshare_fresh(c2.input(domain::a, 8), domain::a); (void)fresh; EXPECT_EQ(c2.schedule().exchange_waves(), 1u); } TEST(Compose, ExchangeFuseOneOpen) { composer c(0); auto fr = c.exchange_fuse( {c.input(domain::a, 8), c.input(domain::a, 4)}); EXPECT_EQ(fr.segments.size(), 2u); EXPECT_EQ(c.value_bytes(fr.opened), 12u); EXPECT_EQ(c.schedule().exchange_waves(), 1u); EXPECT_EQ(c.schedule().aux_of(fr.segments[1].id), 8u); } TEST(Compose, CuckooProbesDedupToFan) { composer c(0); auto mr = c.schedule_cuckoo_probes({3, 1, 3}, [&](std::size_t) { return c.input(domain::fss, 16); }, 2, 16, 8); EXPECT_EQ(mr.leaves.size(), 2u); } TEST(Compose, DealerDeliverSchedulesOneExchange) { composer c(2); auto pad = c.dealer_deliver(c.input(domain::a, 16)); EXPECT_EQ(c.effect_of(pad), effect::exchange); EXPECT_EQ(c.schedule().opcode_of(pad.id), opcodes::dealer_pad); EXPECT_EQ(c.schedule().exchange_waves(), 1u); } TEST(Compose, AuthBarrierUsesOpeningWidth) { composer c(0); c.aby().set_mac_key(dpf::mac_key{9}); auto z = c.aby_product(c.input(domain::a, 8), c.input(domain::a, 8)); (void)z; auto p = c.schedule(); bool saw = false; for (auto n : p.nodes()) { if (p.effect_of(n.id) != effect::exchange) continue; if (p.opcode_of(n.id) < opcodes::beaver_delta) continue; saw = true; const auto elem = sizeof(dpf::beavers::auth_opening); EXPECT_EQ((p.value_bytes_of(n.id) - sizeof(std::uint32_t)) % elem, 0u); EXPECT_GT(p.value_bytes_of(n.id), sizeof(std::uint32_t) + sizeof(std::uint64_t)); } EXPECT_TRUE(saw); } TEST(Compose, Fp61FieldOpen) { std::uint8_t out[8]; const std::uint64_t a = (std::uint64_t{1} << 61) - 2; const std::uint64_t b = 5; dpf::protocol::detail::reconstruct_open(domain::a, 0, reinterpret_cast(&a), reinterpret_cast(&b), 8, out, dpf::protocol::field_open::fp61); std::uint64_t r = 0; std::memcpy(&r, out, 8); // (2^61-2) ≡ -1, so -1+5 ≡ 4 in the field. EXPECT_EQ(r, 4u); } TEST(Compose, BuiltinWalkKernelDrivesDefer) { composer c(0); auto buf = c.defer_expand(c.input(domain::fss, 16), 3, 16); (void)buf; auto p = c.default_plan(); ASSERT_EQ(p.exchange_waves(), 0u); auto [sink0, sink1] = make_memory_sink_pair(1, {}); (void)sink1; std::vector> values; std::map kernels; EXPECT_NO_THROW(dpf::protocol::drive(p, sink0, values, kernels, 0)); } TEST(Compose, PrgExpandMatchesAes) { std::uint8_t seed[16] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; std::uint8_t out[32] = {}; dpf::protocol::block_span in{seed, 1, 16}; dpf::protocol::block_span dst{out, 1, 32}; auto fn = dpf::protocol::detail::builtin_walk_kernel(opcodes::fss_expand_pair); fn(opcodes::fss_expand_pair, {}, {in}, dst, 1); simde__m128i s{}; std::memcpy(&s, seed, 16); auto both = dpf::prg::aes128::eval01(s); EXPECT_EQ(std::memcmp(out, both.data(), 32), 0); } TEST(Compose, WordOpenAddsEveryLane) { std::uint64_t mine[2] = {1, 2}; std::uint64_t peer[2] = {3, 4}; std::uint64_t out[2] = {}; dpf::protocol::detail::reconstruct_open(domain::a, 0, reinterpret_cast(mine), reinterpret_cast(peer), 16, reinterpret_cast(out)); EXPECT_EQ(out[0], 4u); EXPECT_EQ(out[1], 6u); } TEST(Compose, DsSinkCoversComposePlan) { auto composed = dpf::net::compose_ds_slot_bytes(4, 16, true, 1); auto sink = dpf::net::ds_walk_slot_bytes(4, true, 1); ASSERT_GE(sink.size(), composed.size()); for (std::size_t i = 0; i < composed.size(); ++i) EXPECT_GE(sink[i], composed[i]); } TEST(Compose, IknpSetupRoundsAddToPlan) { const auto setup = dpf::iknp::setup_rounds(2, 1, 1, 2); EXPECT_GT(setup, 4u); composer c(0); auto leaf = c.fss_point(c.input(domain::fss, 16), 3, 16); (void)leaf; auto p = c.default_plan(); EXPECT_EQ(p.rounds_including(setup), p.rounds() + setup); EXPECT_EQ(p.rounds(), 3u); } TEST(Compose, FleetSurvivesChaoticWaits) { composer c(0); auto x = c.input(domain::a, 8); (void)c.exchange(x); const auto t0 = std::chrono::steady_clock::now(); dpf::app::run_fleet(c, 24, 0xC0FFEEu); const double sec = std::chrono::duration( std::chrono::steady_clock::now() - t0).count(); // Serializing every stall would be tens of seconds. Overlap keeps it small. EXPECT_LT(sec, 0.05); } TEST(Compose, ClientServersTwoWavesNoServerTalk) { composer c(0); EXPECT_THROW(c.client_servers(1, 8, 4), std::invalid_argument); EXPECT_THROW(c.client_servers(2, 0, 4), std::invalid_argument); auto q = c.client_servers(3, 16, 8); auto p = c.default_plan(); EXPECT_EQ(p.rounds(), 2u); EXPECT_EQ(p.slot_bytes(0), 3u * 16u); EXPECT_EQ(p.slot_bytes(1), 3u * 8u); EXPECT_LT(p.wave_of(q.upload), p.wave_of(q.answer)); EXPECT_EQ(p.opcode_of(q.upload.id), opcodes::client_upload); EXPECT_EQ(p.opcode_of(q.answer.id), opcodes::client_answer); } TEST(Compose, ClientUploadCopiesPeerBytes) { composer c0(0); composer c1(1); auto q0 = c0.client_servers(2, 4, 4); auto q1 = c1.client_servers(2, 4, 4); auto p0 = c0.default_plan(); auto p1 = c1.default_plan(); auto [s0, s1] = make_memory_sink_pair(1, p0.slot_bytes_all()); std::vector> v0(p0.nodes().size()), v1(p1.nodes().size()); const auto src0 = p0.inputs_of(q0.upload.id)[0]; const auto src1 = p1.inputs_of(q1.upload.id)[0]; v0[src0] = {1, 2, 3, 4, 5, 6, 7, 8}; v1[src1] = {8, 7, 6, 5, 4, 3, 2, 1}; std::map kernels; std::thread t0([&] { dpf::protocol::drive(p0, s0, v0, kernels, 0); }); std::thread t1([&] { dpf::protocol::drive(p1, s1, v1, kernels, 1); }); t0.join(); t1.join(); ASSERT_EQ(v0[q0.upload.id].size(), 8u); EXPECT_EQ(v0[q0.upload.id][0], 8); EXPECT_EQ(v1[q1.upload.id][0], 1); } TEST(Compose, ParkYieldsUntilPeerSubmits) { composer c0(0); composer c1(1); (void)c0.exchange(c0.input(domain::a, 8)); (void)c1.exchange(c1.input(domain::a, 8)); auto p0 = c0.default_plan(); auto p1 = c1.default_plan(); auto [s0, s1] = make_memory_sink_pair(1, p0.slot_bytes_all()); std::vector> v0, v1; dpf::protocol::drive_cursor a, b; dpf::protocol::drive_options opt; opt.park_if_waiting = true; opt.one_exchange = true; opt.cursor = &a; dpf::protocol::drive(p0, s0, v0, {}, 0, opt); EXPECT_TRUE(a.awaiting_peer); EXPECT_FALSE(a.done); opt.cursor = &b; dpf::protocol::drive(p1, s1, v1, {}, 1, opt); EXPECT_FALSE(b.awaiting_peer); opt.cursor = &a; for (int i = 0; i < 4 && !a.done; ++i) dpf::protocol::drive(p0, s0, v0, {}, 0, opt); opt.cursor = &b; for (int i = 0; i < 4 && !b.done; ++i) dpf::protocol::drive(p1, s1, v1, {}, 1, opt); EXPECT_TRUE(a.done); EXPECT_TRUE(b.done); } TEST(Compose, OneExchangeStopsAfterFirstRound) { composer c0(0); composer c1(1); auto chain = [](composer & c) { auto x = c.input(domain::a, 8); auto e0 = c.exchange(x); auto mid = c.compute(opcodes::fss_rotate, {e0}, domain::a, 8); (void)c.exchange(mid); }; chain(c0); chain(c1); auto p0 = c0.default_plan(); auto p1 = c1.default_plan(); ASSERT_EQ(p0.rounds(), 2u); auto [s0, s1] = make_memory_sink_pair(1, p0.slot_bytes_all()); std::vector> v0, v1; dpf::protocol::drive_cursor a, b; dpf::protocol::drive_options opt; opt.park_if_waiting = true; opt.one_exchange = true; auto kick = [&](auto & p, auto & sink, auto & values, auto & cur, std::size_t party) { opt.cursor = &cur; dpf::protocol::drive(p, sink, values, {}, party, opt); }; kick(p0, s0, v0, a, 0); kick(p1, s1, v1, b, 1); kick(p0, s0, v0, a, 0); EXPECT_FALSE(a.done); EXPECT_FALSE(b.done); EXPECT_EQ(a.exchange_i, 1u); EXPECT_EQ(b.exchange_i, 1u); EXPECT_EQ(p0.rounds(), 2u); } TEST(Compose, FleetOneInstanceAndEmptyPlan) { composer empty(0); (void)empty.input(domain::a, 8); dpf::app::run_fleet(empty, 4, 1); composer one(0); (void)one.fss_point(one.input(domain::fss, 16), 2, 16); dpf::app::run_fleet(one, 1, 2); EXPECT_THROW(dpf::app::run_fleet(one, 0, 3), std::invalid_argument); } TEST(Compose, ExerciseReportsScheduleCost) { composer c(0); (void)c.fss_point_early_stop(c.input(domain::fss, 16), 8, 3, 16); const auto got = dpf::app::exercise(c); EXPECT_EQ(got.rounds, 5u); EXPECT_EQ(got.bytes, 80u); } TEST(Compose, MissingKernelThrows) { composer c(0); (void)c.compute(5000, {c.input(domain::a, 8)}, domain::a, 8); auto p = c.default_plan(); auto [s0, s1] = make_memory_sink_pair(1, {0}); (void)s1; std::vector> values; EXPECT_THROW(dpf::protocol::drive(p, s0, values, {}, 0), std::runtime_error); } TEST(Compose, StepXorKernel) { std::uint8_t a[4] = {0xff, 0x00, 0x0f, 0xf0}; std::uint8_t b[4] = {0x0f, 0xff, 0xf0, 0x00}; std::uint8_t out[4] = {}; dpf::protocol::block_span in0{a, 1, 4}; dpf::protocol::block_span in1{b, 1, 4}; dpf::protocol::block_span dst{out, 1, 4}; auto fn = dpf::protocol::detail::builtin_walk_kernel(opcodes::fss_step + 1); fn(opcodes::fss_step + 1, {}, {in0, in1}, dst, 1); EXPECT_EQ(out[0], static_cast(0xff ^ 0x0f)); EXPECT_EQ(out[1], static_cast(0x00 ^ 0xff)); EXPECT_EQ(out[2], static_cast(0x0f ^ 0xf0)); EXPECT_EQ(out[3], static_cast(0xf0 ^ 0x00)); } TEST(Compose, AdaptiveRejectsBadOrder) { composer c(0); auto f = c.begin_adaptive_prefix(c.input(domain::fss, 16)); EXPECT_THROW(c.retain_adaptive_prefix(f, 0), std::logic_error); f = c.step_adaptive_prefix(f, 16, 8); EXPECT_THROW(c.step_adaptive_prefix(f, 16, 8), std::logic_error); EXPECT_THROW(c.retain_adaptive_prefix(f, 2), std::invalid_argument); f = c.retain_adaptive_prefix(f, 1); f = c.step_adaptive_prefix(f, 16, 8); auto p = c.default_plan(); EXPECT_EQ(p.rounds(), 2u); EXPECT_EQ(p.slot_bytes_from(1).size(), 1u); EXPECT_TRUE(p.slot_bytes_from(2).empty()); } TEST(Compose, ExchangeFuseThreeSegments) { composer c(0); EXPECT_THROW(c.exchange_fuse({c.input(domain::a, 4)}), std::invalid_argument); auto fr = c.exchange_fuse({c.input(domain::a, 4), c.input(domain::a, 8), c.input(domain::b, 2)}); ASSERT_EQ(fr.segments.size(), 3u); auto p = c.schedule(); EXPECT_EQ(p.rounds(), 1u); EXPECT_EQ(p.aux_of(fr.segments[0].id), 0u); EXPECT_EQ(p.aux_of(fr.segments[1].id), 4u); EXPECT_EQ(p.aux_of(fr.segments[2].id), 12u); EXPECT_EQ(p.value_bytes_of(fr.opened.id), 14u); } TEST(Compose, ReshareFreshAndSameDomain) { composer c(0); auto a = c.input(domain::a, 8); auto same = c.reshare(a, domain::a); EXPECT_EQ(c.domain_of(same), domain::a); auto fresh = c.reshare_fresh(a, domain::b); EXPECT_EQ(c.domain_of(fresh), domain::b); auto p = c.default_plan(); EXPECT_EQ(p.rounds(), 1u); bool saw_zero = false; for (auto n : p.nodes()) { if (p.opcode_of(n.id) == opcodes::dealer_zero) saw_zero = true; } EXPECT_TRUE(saw_zero); EXPECT_THROW(c.reshare_with_mask(a, c.input(domain::a, 4), domain::a), std::invalid_argument); } TEST(Compose, CuckooProbesRejectsEmpty) { composer c(0); EXPECT_THROW(c.schedule_cuckoo_probes({}, [&](std::size_t) { return c.input(domain::fss, 16); }, 2, 16, 8), std::invalid_argument); } TEST(Compose, IknpSetupRoundsMonotone) { EXPECT_EQ(dpf::iknp::setup_rounds(0, 0, 0, 0), 0u); const auto base = dpf::iknp::setup_rounds(1, 0, 0, 0); EXPECT_GE(base, 6u); EXPECT_GT(dpf::iknp::setup_rounds(1, 0, 0, 1), base); EXPECT_GT(dpf::iknp::setup_rounds(1, 0, 1, 0), base); EXPECT_GE(dpf::iknp::setup_rounds(9000, 0, 0, 0), base); composer c(0); (void)c.exchange(c.input(domain::a, 8)); EXPECT_EQ(c.default_plan().rounds_including(base), 1u + base); } TEST(Compose, DsBudgetEnvelopeSweep) { for (std::size_t depth : {1u, 2u, 4u}) { for (bool oh : {false, true}) { for (std::size_t lg : {0u, 1u, 3u}) { auto composed = dpf::net::compose_ds_slot_bytes(depth, 16, oh, lg); auto sink = dpf::net::ds_walk_slot_bytes(depth, oh, lg); ASSERT_GE(sink.size(), composed.size()) << depth << oh << lg; for (std::size_t i = 0; i < composed.size(); ++i) EXPECT_GE(sink[i], composed[i]); composer c(0); (void)c.level_walk_ds(c.input(domain::fss, 16), depth, 16, oh, lg); EXPECT_EQ(c.default_plan().rounds(), composed.size()); } } } EXPECT_THROW(dpf::net::compose_ds_slot_bytes(0, 16, false), std::invalid_argument); composer bad(0); EXPECT_THROW(bad.level_walk_ds_sized(bad.input(domain::fss, 16), {16, 16}, false), std::invalid_argument); } TEST(Compose, XorFallbackForOddWidths) { std::uint8_t mine[3] = {0xff, 0x00, 0x0f}; std::uint8_t peer[3] = {0x0f, 0xff, 0xf0}; std::uint8_t out[3] = {}; dpf::protocol::detail::reconstruct_open(domain::a, 0, mine, peer, 3, out); EXPECT_EQ(out[0], static_cast(0xff ^ 0x0f)); EXPECT_EQ(out[1], static_cast(0x00 ^ 0xff)); EXPECT_EQ(out[2], static_cast(0x0f ^ 0xf0)); } TEST(Compose, DealerDeliverRejectedOnRoundSink) { composer c(0); (void)c.dealer_deliver(c.input(domain::a, 8)); auto p = c.default_plan(); auto [s0, s1] = make_memory_sink_pair(1, p.slot_bytes_all()); (void)s1; std::vector> values; EXPECT_THROW(dpf::protocol::drive(p, s0, values, {}, 0), std::runtime_error); } TEST(Compose, DriveViaScheduleReconstructsOpen) { composer c0(0); 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(); ASSERT_EQ(p0.rounds(), 1u); auto [sink0, sink1] = make_memory_sink_pair(1, p0.slot_bytes_all()); std::vector> v0, v1; v0.resize(p0.nodes().size()); v1.resize(p1.nodes().size()); std::uint64_t a = 3, b = 5; v0[x0.id].assign(8, 0); v1[x1.id].assign(8, 0); std::memcpy(v0[x0.id].data(), &a, 8); std::memcpy(v1[x1.id].data(), &b, 8); std::thread t0([&] { dpf::protocol::drive_via_schedule(p0, sink0, v0, {}, 0); }); dpf::protocol::drive_via_schedule(p1, sink1, v1, {}, 1); t0.join(); 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); auto rounds = dpf::protocol::plan_to_schedule(p0, v0, {}, 0, 1); ASSERT_EQ(rounds.size(), 1u); EXPECT_EQ(rounds[0].channel, dpf::protocol::edge_channel::peer); EXPECT_EQ(rounds[0].recv, dpf::protocol::receive_rule::domain_open); } TEST(Compose, DriveViaScheduleTwoWaves) { constexpr std::uint32_t k_step = 5000; auto make = [](std::size_t party, std::uint64_t in) { 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, e0, e1, in); }; auto [p0, x0, e0a, e0b, in0] = make(0, 11); auto [p1, x1, e1a, e1b, in1] = make(1, 19); (void)e0a; (void)e1a; ASSERT_EQ(p0.rounds(), 2u); 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}}; auto [sink0, sink1] = make_memory_sink_pair(1, p0.slot_bytes_all()); std::vector> v0(p0.nodes().size()), v1(p1.nodes().size()); v0[x0.id].resize(8); v1[x1.id].resize(8); std::memcpy(v0[x0.id].data(), &in0, 8); std::memcpy(v1[x1.id].data(), &in1, 8); std::thread t0([&] { dpf::protocol::drive_via_schedule(p0, sink0, v0, k, 0); }); dpf::protocol::drive_via_schedule(p1, sink1, v1, k, 1); t0.join(); std::uint64_t o0 = 0, o1 = 0; std::memcpy(&o0, v0[e0b.id].data(), 8); std::memcpy(&o1, v1[e1b.id].data(), 8); // First open = 11+19=30; each adds 1 locally → 31; second open = 31+31=62. EXPECT_EQ(o0, 62u); EXPECT_EQ(o1, 62u); } TEST(Compose, ScheduleBranchSkipsAfterOpen) { using dpf::protocol::schedule_round; using dpf::protocol::schedule_session; auto [sink0, sink1] = make_memory_sink_pair(1, {sizeof(std::uint64_t), sizeof(std::uint64_t)}); std::vector rounds(2); rounds[0].slot_bytes = sizeof(std::uint64_t); rounds[0].produce = [](std::size_t, const std::uint8_t *, std::size_t, std::uint8_t * out) { std::uint64_t v = 7; std::memcpy(out, &v, 8); }; rounds[1].slot_bytes = sizeof(std::uint64_t); rounds[1].branch = [](std::size_t, const std::uint8_t * peer, std::size_t n) { if (peer == nullptr || n < 8) return true; std::uint64_t v = 0; std::memcpy(&v, peer, 8); return v != 7; // peer sent 7 → prune }; rounds[1].produce = [](std::size_t, const std::uint8_t *, std::size_t, std::uint8_t * out) { std::uint64_t v = 99; std::memcpy(out, &v, 8); }; schedule_session a(1, sink0, rounds); schedule_session b(1, sink1, rounds); a.submit(0); b.submit(0); a.drive(); b.drive(); EXPECT_TRUE(a.done(0)); EXPECT_TRUE(b.done(0)); EXPECT_FALSE(sink0.peer_ready(1, 0)); EXPECT_FALSE(sink1.peer_ready(1, 0)); } TEST(Compose, ScheduleEdgeChannelRssNext) { using dpf::protocol::edge_channel; using dpf::protocol::edge_sinks; using dpf::protocol::schedule_round; using dpf::protocol::schedule_session; auto [peer0, peer1] = make_memory_sink_pair(1, {8u}); auto [rss0, rss1] = make_memory_sink_pair(1, {8u}); std::vector rounds(2); rounds[0].slot_bytes = 8; rounds[0].channel = edge_channel::peer; rounds[0].edge = dpf::protocol::edge_peer; rounds[0].sink_round = 0; rounds[0].produce = [](std::size_t, const std::uint8_t *, std::size_t, std::uint8_t * out) { std::uint64_t v = 1; std::memcpy(out, &v, 8); }; rounds[1].slot_bytes = 8; rounds[1].channel = edge_channel::rss_next; rounds[1].edge = dpf::protocol::edge_rss_next; rounds[1].sink_round = 0; rounds[1].recv = dpf::protocol::receive_rule::copy_peer; rounds[1].produce = [](std::size_t, const std::uint8_t * peer, std::size_t n, std::uint8_t * out) { std::uint64_t v = 2; if (peer != nullptr && n >= 8) std::memcpy(&v, peer, 8); v += 10; std::memcpy(out, &v, 8); }; edge_sinks e0{&peer0, &rss0, nullptr}; edge_sinks e1{&peer1, &rss1, nullptr}; schedule_session a(1, e0, rounds); schedule_session b(1, e1, rounds); a.submit(0); b.submit(0); a.drive(); b.drive(); EXPECT_TRUE(a.done(0)); EXPECT_TRUE(b.done(0)); std::uint64_t rss_peer = 0; rss0.read_peer(0, 0, reinterpret_cast(&rss_peer), 8); EXPECT_EQ(rss_peer, 11u); // peer's round0 value 1, then +10 on rss round } TEST(Compose, PadSetupRoundsSpliceBeforePlan) { composer c0(0); composer c1(1); auto x0 = c0.input(domain::a, 8); auto x1 = c1.input(domain::a, 8); (void)c0.exchange(x0); (void)c1.exchange(x1); auto p0 = c0.default_plan(); auto p1 = c1.default_plan(); const auto setup = dpf::iknp::setup_rounds(1, 0, 0, 0); ASSERT_GT(setup, 0u); auto tape0 = std::make_shared>(); auto tape1 = std::make_shared>(); std::vector> v0(p0.nodes().size()), v1(p1.nodes().size()); v0[x0.id].assign(8, 1); v1[x1.id].assign(8, 2); auto r0 = dpf::protocol::plan_with_pad_setup(p0, setup, 16, v0, {}, 0, 1, tape0); auto r1 = dpf::protocol::plan_with_pad_setup(p1, setup, 16, v1, {}, 1, 1, tape1); EXPECT_EQ(r0.size(), setup + p0.rounds()); EXPECT_EQ(r0[0].recv, dpf::protocol::receive_rule::xor_bytes); EXPECT_EQ(r0[setup].sink_round, static_cast(setup)); EXPECT_EQ(p0.rounds_including(setup), r0.size()); std::vector slots; for (const auto & r : r0) slots.push_back(r.slot_bytes); auto [s0, s1] = make_memory_sink_pair(1, slots); dpf::protocol::schedule_session a(1, s0, std::move(r0)); dpf::protocol::schedule_session b(1, s1, std::move(r1)); a.submit(0); b.submit(0); for (unsigned spins = 0; !a.done(0) || !b.done(0); ++spins) { ASSERT_LT(spins, 100000u); a.drive(); b.drive(); } EXPECT_TRUE(a.done(0)); EXPECT_TRUE(b.done(0)); } TEST(Compose, RssFromYTagsNeighborChannel) { composer c(0); auto y = c.input(domain::y, 8); auto r = c.rss_from_y(y); (void)r; auto p = c.schedule(); std::vector> values(p.nodes().size()); auto rounds = dpf::protocol::plan_to_schedule(p, values, {}, 0, 1); ASSERT_EQ(rounds.size(), 1u); EXPECT_EQ(rounds[0].channel, dpf::protocol::edge_channel::rss_next); EXPECT_EQ(rounds[0].recv, dpf::protocol::receive_rule::copy_peer); } TEST(Compose, HandVsScheduleBaselineOpen) { // Phase 0: hand submit/flush vs interleaved drive_via_schedule — same open. constexpr int kIters = 200; std::uint64_t a = 3, b = 5; const auto t_hand0 = std::chrono::steady_clock::now(); for (int i = 0; i < kIters; ++i) { auto [x0, x1] = make_memory_sink_pair(1, {8u}); x0.submit(0, 0, reinterpret_cast(&a), 8); x1.submit(0, 0, reinterpret_cast(&b), 8); x0.flush(); x1.flush(); std::uint64_t p0 = 0; x0.read_peer(0, 0, reinterpret_cast(&p0), 8); EXPECT_EQ(a + p0, 8u); } const auto hand_ns = std::chrono::duration_cast( std::chrono::steady_clock::now() - t_hand0) .count(); const auto t_sched0 = std::chrono::steady_clock::now(); for (int i = 0; i < kIters; ++i) { composer c0(0); 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(); auto [s0, s1] = make_memory_sink_pair(1, p0.slot_bytes_all()); std::vector> v0(p0.nodes().size()), v1(p1.nodes().size()); v0[x0.id].assign(8, 0); v1[x1.id].assign(8, 0); std::memcpy(v0[x0.id].data(), &a, 8); std::memcpy(v1[x1.id].data(), &b, 8); auto r0 = dpf::protocol::plan_to_schedule(p0, v0, {}, 0, 1); auto r1 = dpf::protocol::plan_to_schedule(p1, v1, {}, 1, 1); dpf::protocol::schedule_session a(1, s0, std::move(r0)); dpf::protocol::schedule_session b(1, s1, std::move(r1)); a.submit(0); b.submit(0); for (unsigned spins = 0; !a.done(0) || !b.done(0); ++spins) { ASSERT_LT(spins, 100000u); a.drive(); b.drive(); } dpf::protocol::finish_schedule(p0, s0, v0, {}, 0); dpf::protocol::finish_schedule(p1, s1, v1, {}, 1); std::uint64_t o = 0; std::memcpy(&o, v0[e0.id].data(), 8); EXPECT_EQ(o, 8u); (void)e1; } const auto sched_ns = std::chrono::duration_cast( std::chrono::steady_clock::now() - t_sched0) .count(); // Correctness is the gate; print ratio for the Phase 0 harness. EXPECT_GT(hand_ns, 0); EXPECT_GT(sched_ns, 0); RecordProperty("hand_ns", static_cast(hand_ns)); RecordProperty("sched_ns", static_cast(sched_ns)); } TEST(Compose, StarUploadAnswerTwoServers) { using dpf::net::make_memory_star; auto star = make_memory_star(2, 1, {8u, 4u}); auto queries = std::make_shared>>( std::vector>{{1, 2, 3, 4, 5, 6, 7, 8}, {8, 7, 6, 5, 4, 3, 2, 1}}); auto answers = std::make_shared>>( std::vector>{{9, 9, 9, 9}, {1, 1, 1, 1}}); auto client_rounds = dpf::protocol::star_upload_answer_graph(2, 8, 4, queries, answers); // Servers: on edge 0 locally, echo upload then send answer. auto server_rounds = [&](std::size_t /*si*/, std::vector ans) { std::vector r(2); r[0].slot_bytes = 8; r[0].edge = 0; r[0].sink_round = 0; r[0].recv = dpf::protocol::receive_rule::copy_peer; r[0].produce = [](std::size_t, const std::uint8_t *, std::size_t, std::uint8_t * out) { std::memset(out, 0, 8); }; r[1].slot_bytes = 4; r[1].edge = 0; r[1].sink_round = 1; r[1].recv = dpf::protocol::receive_rule::copy_peer; r[1].produce = [ans](std::size_t, const std::uint8_t * peer, std::size_t n, std::uint8_t * out) { (void)peer; (void)n; std::memcpy(out, ans.data(), 4); }; return r; }; dpf::protocol::schedule_session client(1, star.client_mesh(), std::move(client_rounds), false); dpf::protocol::schedule_session s0(1, star.server_edge(0), server_rounds(0, (*answers)[0]), false); dpf::protocol::schedule_session s1(1, star.server_edge(1), server_rounds(1, (*answers)[1]), false); client.submit(0); s0.submit(0); s1.submit(0); for (unsigned spins = 0; !client.done(0) || !s0.done(0) || !s1.done(0); ++spins) { ASSERT_LT(spins, 100000u); client.drive(); s0.drive(); s1.drive(); } EXPECT_TRUE(client.done(0)); } TEST(Compose, ScheduleJumpAfterOpen) { using dpf::protocol::schedule_round; using dpf::protocol::schedule_session; auto [sink0, sink1] = make_memory_sink_pair(1, {8u, 8u, 8u}); std::vector rounds(3); for (std::size_t r = 0; r < 3; ++r) { rounds[r].slot_bytes = 8; rounds[r].edge = dpf::protocol::edge_peer; rounds[r].sink_round = static_cast(r); rounds[r].produce = [r](std::size_t, const std::uint8_t *, std::size_t, std::uint8_t * out) { std::uint64_t v = r + 1; std::memcpy(out, &v, 8); }; } // After round 0, jump to round 2 (skip 1). rounds[0].next = [](std::size_t, const std::uint8_t *, std::size_t) -> std::optional { return std::uint16_t{2}; }; schedule_session a(1, sink0, rounds); schedule_session b(1, sink1, rounds); a.submit(0); b.submit(0); for (unsigned spins = 0; !a.done(0) || !b.done(0); ++spins) { ASSERT_LT(spins, 100000u); a.drive(); b.drive(); } EXPECT_FALSE(sink0.peer_ready(1, 0)); // skipped EXPECT_TRUE(sink0.peer_ready(2, 0) || sink1.peer_ready(2, 0)); } TEST(Compose, SessionHostDrivesQueuedPlans) { composer c0(0); composer c1(1); auto x0 = c0.input(domain::a, 8); auto x1 = c1.input(domain::a, 8); (void)c0.exchange(x0); (void)c1.exchange(x1); auto p0 = c0.schedule(); auto p1 = c1.schedule(); auto [s0, s1] = make_memory_sink_pair(1, p0.slot_bytes_all()); dpf::protocol::edge_mesh m0{{&s0}}; dpf::protocol::edge_mesh m1{{&s1}}; dpf::protocol::session_host h0(0, m0); dpf::protocol::session_host h1(1, m1); h0.values().resize(p0.nodes().size()); h1.values().resize(p1.nodes().size()); std::uint64_t a = 4, b = 6; h0.values()[x0.id].assign(8, 0); h1.values()[x1.id].assign(8, 0); std::memcpy(h0.values()[x0.id].data(), &a, 8); std::memcpy(h1.values()[x1.id].data(), &b, 8); h0.push(p0); h1.push(p1); std::thread th([&] { h0.drive_until_idle(); }); h1.drive_until_idle(); th.join(); } TEST(Compose, ApplyPeerFieldSumAndEqCheck) { composer c(0); auto x = c.input(domain::a, 8); auto e = c.exchange(x); auto p = c.schedule(); std::vector> values(p.nodes().size()); values[x.id].assign(8, 0); values[e.id].assign(8, 0); std::uint64_t mine = 10, peer = 7; std::memcpy(values[x.id].data(), &mine, 8); const auto & wave = p.wave(0); // Force field_sum via reconstruct path by calling apply with domain open // then eq_check manually on matching tags. dpf::protocol::drive_options opt; opt.field = dpf::protocol::field_open::fp61; std::uint8_t peer_bytes[8]; std::memcpy(peer_bytes, &peer, 8); // domain_open sum dpf::protocol::detail::apply_peer_slot(p, wave, 0, 0, 1, peer_bytes, 8, values, opt); std::uint64_t open = 0; std::memcpy(&open, values[e.id].data(), 8); EXPECT_EQ(open, 17u); std::uint8_t tag[8] = {1, 2, 3, 4, 5, 6, 7, 8}; values[x.id].assign(tag, tag + 8); values[e.id].assign(8, 0); // eq_check: build a fake wave rule by direct memcmp path EXPECT_NO_THROW({ if (std::memcmp(values[x.id].data(), tag, 8) != 0) throw std::runtime_error("eq"); }); } TEST(Compose, IknpAndDuAtallahPadGraphs) { auto tape = std::make_shared>(); auto ik = dpf::protocol::iknp_setup_graph(1, 0, 0, 0, tape); EXPECT_EQ(ik.size(), dpf::iknp::setup_rounds(1, 0, 0, 0)); auto da = dpf::protocol::du_atallah_mul_graph(tape); EXPECT_EQ(da.size(), 2u); EXPECT_EQ(da[0].channel, dpf::protocol::edge_channel::dealer); EXPECT_EQ(da[1].channel, dpf::protocol::edge_channel::peer); } TEST(Compose, PirsonaAndHushmapMicroPlans) { auto seeds = std::make_shared>>(); auto answers = std::make_shared>>(); auto fetch = dpf::protocol::pirsona_bitmore_fetch(1, 16, 8, seeds, answers); EXPECT_EQ(fetch.size(), 4u); // 2 servers × (upload+answer) auto tape = std::make_shared>(); auto add = dpf::protocol::hushmap_add_schedule(3, tape); EXPECT_EQ(add.size(), 3u + 2u); // 3 dealer triples + 2 opens auto p = dpf::protocol::keyword_pir_plan(0, 16, 4); EXPECT_EQ(p.rounds(), 2u); } TEST(Compose, AppPlansRoundCounts) { EXPECT_EQ(dpf::protocol::n_server_pir_plan(0, 2, 16, 4).rounds(), 2u); EXPECT_EQ(dpf::protocol::keyword_pir_compose_plan(0, 8).rounds(), 2u); EXPECT_EQ(dpf::protocol::mailbox_write_fused_plan(0).rounds(), 8u); EXPECT_EQ(dpf::protocol::bitmore_fan_plan(0).rounds(), 6u); EXPECT_EQ(dpf::protocol::subleq_instruction_plan(0).rounds(), 8u); EXPECT_EQ(dpf::protocol::pika_lookup_plan(0).rounds(), 5u); EXPECT_EQ(dpf::protocol::duoram_update_plan(0).rounds(), 8u); EXPECT_EQ(dpf::protocol::poplar_prefix_plan(0).rounds(), 8u); EXPECT_EQ(dpf::protocol::ledger23_append_plan(0).rounds(), 2u); EXPECT_EQ(dpf::protocol::floram_ds_plan(0).rounds(), 40u); EXPECT_EQ(dpf::protocol::fss_point_plan(0).rounds(), 8u); EXPECT_EQ(dpf::protocol::fss_cmp_plan(0).rounds(), 8u); EXPECT_EQ(dpf::protocol::range_count_plan(0).rounds(), 8u); EXPECT_EQ(dpf::protocol::psi_cuckoo_plan(0, {0, 1, 0}).rounds(), 9u); EXPECT_EQ(dpf::protocol::idpf_agg_plan(0, 16).rounds(), 16u); } TEST(Compose, DriveStarUploadAnswer) { auto star = dpf::net::make_memory_star(2, 1, {8u, 4u}); auto queries = std::make_shared>>(2); auto answers = std::make_shared>>(2); (*queries)[0].assign(8, 1); (*queries)[1].assign(8, 2); (*answers)[0].assign(4, 0x10); (*answers)[1].assign(4, 0x20); auto client = dpf::protocol::star_upload_answer_graph(2, 8, 4, queries, answers); dpf::protocol::drive_star(star, std::move(client), [&](std::size_t i) { return dpf::protocol::star_server_reply_rounds(8, 4, (*answers)[i]); }); } TEST(Compose, ReceiveRuleAuxTags) { // Aux high-byte tags select field_sum / any_two / verify / eq_check. composer c(0); auto x = c.input(domain::a, 8); auto e = c.exchange(x); auto p = c.schedule(); // Default exchange is domain_open. EXPECT_EQ(dpf::protocol::detail::exchange_receive_rule(p, e.id), dpf::protocol::receive_rule::domain_open); } TEST(Compose, ExperimentReplayUniformStream) { dpf::experiment::master_seed master{}; std::uint64_t a = 0, b = 0, c = 0; { dpf::experiment ex("replay"); master = ex.seed(); a = dpf::uniform_sample(); b = dpf::uniform_sample(); c = dpf::uniform_sample(); EXPECT_GT(dpf::random_bytes_count(), 0u); } { auto ex = dpf::experiment::replay("replay", master); EXPECT_EQ(dpf::uniform_sample(), a); EXPECT_EQ(dpf::uniform_sample(), b); EXPECT_EQ(dpf::uniform_sample(), c); EXPECT_EQ(ex.seed_hex().size(), 64u); } } TEST(Compose, ExperimentThreadsIndependentMasters) { dpf::experiment::master_seed s0{}, s1{}; std::uint64_t v0 = 0, v1 = 0; std::thread t0([&] { dpf::experiment ex("t0", "p0"); s0 = ex.seed(); v0 = dpf::uniform_sample(); }); std::thread t1([&] { dpf::experiment ex("t1", "p1"); s1 = ex.seed(); v1 = dpf::uniform_sample(); }); t0.join(); t1.join(); EXPECT_NE(s0, s1); // Distinct masters almost surely yield distinct first words. EXPECT_NE(v0, v1); } TEST(Compose, MeasurePlanReportsRoundsAndSeed) { auto plan = dpf::protocol::fss_point_plan(0); auto ex = dpf::app::measure_plan("fss_point", plan); EXPECT_EQ(ex.interactive_rounds(), 8u); EXPECT_EQ(ex.plan_bytes_out(), 128u); EXPECT_FALSE(ex.rounds().empty()); EXPECT_GT(ex.rounds().front().bytes_out, 0u); EXPECT_EQ(ex.seed_hex().size(), 64u); EXPECT_GT(ex.wall_ns(), 0u); const std::string dir = "/tmp/libdpf_experiment_csv_test"; #if defined(_WIN32) (void)dir; #else (void)::system(("rm -rf " + dir + " && mkdir -p " + dir).c_str()); ex.write_csv(dir); std::ifstream summary(dir + "/summary.csv"); ASSERT_TRUE(summary.good()); std::string header; ASSERT_TRUE(static_cast(std::getline(summary, header))); EXPECT_NE(header.find("master_seed"), std::string::npos); std::string row; ASSERT_TRUE(static_cast(std::getline(summary, row))); EXPECT_NE(row.find("fss_point"), std::string::npos); #endif } } // namespace