/// @file vdpf_adversarial_test.cpp /// @brief Brute-force and corruption checks for verifiable evaluation. /// @details Covers bug classes seen while bringing the proofs up: subtractive /// reconstruction is not commutative, a single untouched control bit /// can hide a seed flip, and a proof convention must still reject a /// flipped token. Small domains are checked at every point. #include #include #include #include #include "dpf.hpp" namespace { using Input = std::uint8_t; struct Pad { std::uint64_t n = 1; simde__m128i block() { auto v = simde_mm_set_epi64x(static_cast(n), static_cast(n * 9 + 3)); n += 2; return v; } void fill(void * p, std::size_t nbytes) { auto * b = static_cast(p); for (std::size_t i = 0; i < nbytes; ++i) b[i] = static_cast(n + i * 17); n += nbytes; } std::uint8_t bit() { return static_cast(n++ & 1u); } }; dpf::ds_randomness tape() { HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") return {dpf::uniform_sample, Pad{}}; HEDLEY_PRAGMA(GCC diagnostic pop) } template void expect_ordered_reconstruct(const Y0 & y0, const Y1 & y1, const Want & want) { EXPECT_EQ(dpf::reconstruct(y0, y1), want); // The typed overload accepts either party order. A raw subtraction does not. EXPECT_EQ(dpf::reconstruct(y1, y0), want); const auto swapped = static_cast(y1.raw() - y0.raw()); if (y0.raw() != y1.raw()) EXPECT_NE(swapped, want); } } // namespace TEST(VdpfAdversarial, PointFullDomainValuesAndProofs) { const Input alpha = 0; const std::uint64_t beta = 9; auto [k0, k1] = dpf::make_dpf(alpha, beta, dpf::verifiable{}); for (int x = 0; x < 256; ++x) { const Input q = static_cast(x); dpf::proof_token a{}, b{}; const auto y0 = *dpf::eval_point(k0, q, dpf::prove(a)); const auto y1 = *dpf::eval_point(k1, q, dpf::prove(b)); const std::uint64_t want = q == alpha ? beta : 0; expect_ordered_reconstruct(y0, y1, want); EXPECT_TRUE(dpf::verify(a, b)) << int(q); } } TEST(VdpfAdversarial, HalfTreeFullDomainValuesAndProofs) { using Ht = dpf::prg::aes128_ccr; const Input alpha = 255; const std::uint64_t beta = 0x1001; auto [k0, k1] = dpf::make_dpf(alpha, beta, dpf::verifiable{}); EXPECT_TRUE(decltype(k0)::tree::is_half_tree); for (int x = 0; x < 256; ++x) { const Input q = static_cast(x); dpf::proof_token a{}, b{}; const auto y0 = *dpf::eval_point(k0, q, dpf::prove(a)); const auto y1 = *dpf::eval_point(k1, q, dpf::prove(b)); expect_ordered_reconstruct(y0, y1, q == alpha ? beta : 0ull); EXPECT_TRUE(dpf::verify(a, b)) << int(q); } } TEST(VdpfAdversarial, ComparisonAndBlockedFullDomain) { const Input alpha = 0x40; auto native = dpf::make_dpf(alpha, dpf::lt(std::uint64_t{1}), dpf::verifiable{}); auto blocked = dpf::make_dpf(alpha, dpf::block_width<4>(dpf::lt(std::uint64_t{1})), dpf::verifiable{}); for (int x = 0; x < 256; ++x) { const Input q = static_cast(x); dpf::proof_token n0{}, n1{}, b0{}, b1{}; const auto ny0 = dpf::eval_point(dpf::cmp, native.first, q, dpf::prove(n0)); const auto ny1 = dpf::eval_point(dpf::cmp, native.second, q, dpf::prove(n1)); const auto by0 = dpf::eval_point(dpf::cmp, blocked.first, q, dpf::prove(b0)); const auto by1 = dpf::eval_point(dpf::cmp, blocked.second, q, dpf::prove(b1)); const std::uint64_t want = q < alpha ? 1u : 0u; EXPECT_EQ(dpf::reconstruct(ny0, ny1) & native.first.cmp().mask, want) << int(q); EXPECT_EQ(dpf::reconstruct(by0, by1) & blocked.first.cmp().mask, want) << int(q); EXPECT_TRUE(dpf::verify(n0, n1)) << int(q); EXPECT_TRUE(dpf::verify(b0, b1)) << int(q); } } TEST(VdpfAdversarial, ExtractableFp61FullDomainSketch) { const Input alpha = 0x7f; const dpf::fp61 beta{42}; auto [k0, k1] = dpf::make_dpf(alpha, beta, dpf::extractable{}, dpf::verifiable{}); std::array s0{}, s1{}, r{}; for (int x = 0; x < 256; ++x) { const Input q = static_cast(x); dpf::proof_token a{}, b{}; const auto y0 = *dpf::eval_point(k0, q, dpf::prove(a)); const auto y1 = *dpf::eval_point(k1, q, dpf::prove(b)); EXPECT_EQ(dpf::reconstruct(y0, y1), q == alpha ? beta : dpf::fp61{0}) << int(q); EXPECT_TRUE(dpf::verify(a, b)) << int(q); s0[static_cast(x)] = y0.raw(); s1[static_cast(x)] = y1.raw(); r[static_cast(x)] = dpf::fp61{static_cast(3 * x + 1)}; } const auto honest0 = s0; EXPECT_TRUE(dpf::sketch_verify(dpf::sketch_fold(s0, r), dpf::sketch_fold(s1, r))); // A second hot point is weight 2. Changing only the magnitude of the // single hot point stays weight 1 and must still verify. s0[0] = s0[0] + beta; EXPECT_FALSE(dpf::sketch_verify(dpf::sketch_fold(s0, r), dpf::sketch_fold(s1, r))); auto r_bad = r; r_bad[alpha] = r_bad[alpha] + dpf::fp61{1}; EXPECT_FALSE(dpf::sketch_verify( dpf::sketch_fold(honest0, r_bad), dpf::sketch_fold(s1, r))); } TEST(VdpfAdversarial, SeedAndWordCorruptionAreVisible) { const Input alpha = 0x2a; auto [k0, k1] = dpf::make_dpf(alpha, std::uint64_t{5}, dpf::verifiable{}); EXPECT_TRUE(dpf::same_public_part(k0, k1)); for (auto & cs : const_cast::correction_seeds_array &>( k0.correction_seeds())) cs[0] = simde_mm_xor_si128(cs[0], simde_mm_set1_epi8(1)); EXPECT_FALSE(dpf::same_public_part(k0, k1)); int proof_fail = 0; for (int x = 0; x < 256; ++x) { const Input q = static_cast(x); dpf::proof_token a{}, b{}; (void)*dpf::eval_point(k0, q, dpf::prove(a)); (void)*dpf::eval_point(k1, q, dpf::prove(b)); if (!dpf::verify(a, b)) ++proof_fail; } EXPECT_GT(proof_fail, 0); auto [h0, h1] = dpf::make_dpf(alpha, std::uint64_t{5}, dpf::verifiable{}); // A level-0 word is invisible when that party's root control bit is 0. // Flip every correction word so a later on-path level is corrupted. auto & words = const_cast::correction_words_array &>( h0.correction_words()); for (auto & word : words) word = simde_mm_xor_si128(word, simde_mm_set1_epi8(0x5a)); EXPECT_FALSE(dpf::same_public_part(h0, h1)); int value_fail = 0; for (int x = 0; x < 256; ++x) { const Input q = static_cast(x); const auto got = dpf::reconstruct(*dpf::eval_point(h0, q), *dpf::eval_point(h1, q)); const std::uint64_t want = q == alpha ? 5u : 0u; if (got != want) ++value_fail; } EXPECT_GT(value_fail, 0); } TEST(VdpfAdversarial, EmptySequenceProofVerifies) { auto [k0, k1] = dpf::make_dpf(Input{1}, std::uint64_t{1}, dpf::verifiable{}); const std::vector none; dpf::proof_token a{}, b{}; dpf::prove_sequence(k0, none.begin(), none.end(), dpf::prove(a)); dpf::prove_sequence(k1, none.begin(), none.end(), dpf::prove(b)); EXPECT_TRUE(dpf::verify(a, b)); } TEST(VdpfAdversarial, GenevalFullDomainProofAndPartyOrder) { const Input alpha = 0x3c; const Input x0 = 0x10; const Input x1 = static_cast(alpha ^ x0); const std::uint64_t y = 0x7e; auto g = dpf::geneval_full(x0, x1, tape(), y); ASSERT_EQ(g.party0.size(), 256u); EXPECT_TRUE(dpf::verify(g.proof0, g.proof1)); for (int q = 0; q < 256; ++q) { const auto want = static_cast(q) == alpha ? y : 0ull; EXPECT_EQ(static_cast(g.party0[static_cast(q)] - g.party1[static_cast(q)]), want) << q; if (g.party0[static_cast(q)] != g.party1[static_cast(q)]) { EXPECT_NE(static_cast(g.party1[static_cast(q)] - g.party0[static_cast(q)]), want) << q; } } g.proof0[0] = simde_mm_xor_si128(g.proof0[0], simde_mm_set1_epi8(1)); EXPECT_FALSE(dpf::verify(g.proof0, g.proof1)); } TEST(VdpfAdversarial, MacDetectsValueAndTagCorruption) { const auto key = dpf::sample_mac_key(); auto [a, b] = dpf::mac_share_value(dpf::fp61{20}, key); EXPECT_TRUE(dpf::mac_verify(a, b, key)); const auto scaled = dpf::mac_scale(a, dpf::fp61{2}); const auto scaled_b = dpf::mac_scale(b, dpf::fp61{2}); EXPECT_TRUE(dpf::mac_verify(scaled, scaled_b, key)); EXPECT_EQ((scaled.value + scaled_b.value).raw(), (dpf::fp61{20} * dpf::fp61{2}).raw()); a.value = a.value + dpf::fp61{1}; EXPECT_FALSE(dpf::mac_verify(a, b, key)); a.value = a.value - dpf::fp61{1}; a.tag = a.tag + dpf::fp61{1}; EXPECT_FALSE(dpf::mac_verify(a, b, key)); a.tag = a.tag - dpf::fp61{1}; std::array, 1> left{a}; std::array, 2> right{b, b}; std::array coeffs{dpf::fp61{1}}; EXPECT_FALSE(dpf::mac_verify_batch(left, right, coeffs, key)); }