/// @file vdpf_regression_test.cpp /// @brief Holistic VDPF regressions from DCF/Grotto proof bring-up failures. /// @details Patterns covered: /// - path-memo re-fold cancel (blocked parks / native spine) /// - off-path vs on-path blocked proves (CS level vs checkpoint index) /// - domain-tag survival past `hash_node` level masking /// - keygen `make_cs` level must match fold level /// - warm interval memoizer must not skip upper proof folds /// - multi-endpoint signed/unsigned prefix aggregates /// - endpoint-equal-alpha prefix stability across RNG draws #include #include #include #include #include #include #include "dpf.hpp" #include "grotto/prefix_parity.hpp" #include "grotto/offset_horner.hpp" namespace { using Input = std::uint8_t; bool tokens_equal(const dpf::proof_token & a, const dpf::proof_token & b) { return dpf::detail::vdpf::proof_equal(a, b); } } // namespace // --- Domain separation / CS tagging --------------------------------------- TEST(VdpfRegression, HashNodeRetainsHighLevelBits) { // fold_spine_tag is bit 15; masking to 8 bits would drop it and make // blocked CS verify against native folds. constexpr std::size_t tagged = dpf::detail::blocked::fold_spine_tag | std::size_t{3}; const simde__m128i seed = simde_mm_set_epi64x(0x1111, 0x2222); const auto h_native = dpf::detail::vdpf::hash_node(3, 0x2a, seed); const auto h_tagged = dpf::detail::vdpf::hash_node(tagged, 0x2a, seed); EXPECT_NE(std::memcmp(&h_native, &h_tagged, sizeof(h_native)), 0); } TEST(VdpfRegression, BlockedKeygenCsUsesSpineTag) { auto [k0, k1] = dpf::make_dpf(Input{0x2a}, dpf::block_width<4>(dpf::lt(std::uint64_t{1})), dpf::verifiable{}); auto [n0, n1] = dpf::make_dpf(Input{0x2a}, dpf::lt(std::uint64_t{1}), dpf::verifiable{}); // Public CS arrays must diverge: blocked keygen tags make_cs levels. EXPECT_NE(std::memcmp(k0.correction_seeds().data(), n0.correction_seeds().data(), sizeof(decltype(k0)::correction_seeds_array)), 0); (void)k1; (void)n1; } // --- Path-memo idempotence (re-fold cancel) -------------------------------- TEST(VdpfRegression, BlockedEvalShareProveIsIdempotentOnWarmPath) { // Re-folding parked siblings on a warm path XORs them out of the token. auto [k0, k1] = dpf::make_dpf(Input{0x2a}, dpf::block_width<4>(dpf::lt(std::uint64_t{1})), dpf::verifiable{}); const Input x{0x11}; // off-path, parks right children dpf::proof_token a{}, b{}; dpf::detail::vdpf::init_proof(a, k0); dpf::detail::vdpf::init_proof(b, k1); auto p0 = dpf::make_basic_path_memoizer(k0); auto p1 = dpf::make_basic_path_memoizer(k1); dpf::detail::blocked::eval_share(k0, x, p0, &a); dpf::detail::blocked::eval_share(k1, x, p1, &b); EXPECT_TRUE(dpf::verify(a, b)); const auto once0 = a; const auto once1 = b; dpf::detail::blocked::eval_share(k0, x, p0, &a); dpf::detail::blocked::eval_share(k1, x, p1, &b); EXPECT_TRUE(tokens_equal(a, once0)); EXPECT_TRUE(tokens_equal(b, once1)); EXPECT_TRUE(dpf::verify(a, b)); } TEST(VdpfRegression, NativePathProveIsIdempotentOnWarmPath) { auto [k0, k1] = dpf::make_dpf(Input{0x2a}, dpf::lt(std::uint64_t{1}), dpf::verifiable{}); const Input x{0x11}; dpf::proof_token a{}, b{}; dpf::detail::vdpf::init_proof(a, k0); dpf::detail::vdpf::init_proof(b, k1); auto p0 = dpf::make_basic_path_memoizer(k0); auto p1 = dpf::make_basic_path_memoizer(k1); dpf::detail::ensure_level(k0, x, p0, decltype(k0)::depth, &a); dpf::detail::ensure_level(k1, x, p1, decltype(k1)::depth, &b); const auto once0 = a; const auto once1 = b; dpf::detail::ensure_level(k0, x, p0, decltype(k0)::depth, &a); dpf::detail::ensure_level(k1, x, p1, decltype(k1)::depth, &b); EXPECT_TRUE(tokens_equal(a, once0)); EXPECT_TRUE(tokens_equal(b, once1)); EXPECT_TRUE(dpf::verify(a, b)); } TEST(VdpfRegression, PublicProveOnWarmPathMatchesColdNonZero) { auto [k0, k1] = dpf::make_dpf(Input{0x2a}, std::uint64_t{9}, dpf::verifiable{}); const Input x{0x2a}; auto path0 = dpf::make_basic_path_memoizer(k0); auto path1 = dpf::make_basic_path_memoizer(k1); dpf::proof_token cold0{}, cold1{}; (void)*dpf::eval_point(k0, x, dpf::prove(cold0)); (void)*dpf::eval_point(k1, x, dpf::prove(cold1)); EXPECT_TRUE(dpf::verify(cold0, cold1)); EXPECT_FALSE(tokens_equal(cold0, dpf::detail::vdpf::zero_proof())); (void)*dpf::eval_point(k0, x, path0); (void)*dpf::eval_point(k1, x, path1); dpf::proof_token warm0{}, warm1{}; (void)*dpf::eval_point(k0, x, dpf::prove(warm0), path0); (void)*dpf::eval_point(k1, x, dpf::prove(warm1), path1); EXPECT_TRUE(dpf::verify(warm0, warm1)); EXPECT_TRUE(tokens_equal(warm0, cold0)); EXPECT_TRUE(tokens_equal(warm1, cold1)); EXPECT_FALSE(dpf::verify(dpf::detail::vdpf::zero_proof(), dpf::detail::vdpf::zero_proof())); } TEST(VdpfRegression, BlockedMultiEndpointSignedPrefixKeepsSharedParks) { // Two endpoints that share a left turn: without resume-aware park folds // the second walk cancels the first's parked contribution. auto [k0, k1] = dpf::make_dpf(Input{0x40}, dpf::block_width<4>(dpf::gt(std::uint64_t{1})), dpf::verifiable{}); // Both 0x10 and 0x18 start with MSB bits that park the same early sibling // under a typical block schedule on uint8. const std::array ends{Input{0x10}, Input{0x18}}; dpf::proof_token multi0{}, multi1{}; auto s0 = grotto::signed_prefix_parities(k0, ends, dpf::prove(multi0)); auto s1 = grotto::signed_prefix_parities(k1, ends, dpf::prove(multi1)); EXPECT_TRUE(dpf::verify(multi0, multi1)); for (std::size_t i = 0; i < ends.size(); ++i) EXPECT_EQ((s0[i] + s1[i]) & k0.cmp().mask, (dpf::eval_point(dpf::cmp, k0, ends[i]).raw() + dpf::eval_point(dpf::cmp, k1, ends[i]).raw()) & k0.cmp().mask); // Reference: fold e0 then e1 on one path without re-init (same as API). dpf::proof_token ref0{}, ref1{}; dpf::detail::vdpf::init_proof(ref0, k0); dpf::detail::vdpf::init_proof(ref1, k1); auto p0 = dpf::make_basic_path_memoizer(k0); auto p1 = dpf::make_basic_path_memoizer(k1); dpf::detail::blocked::eval_share(k0, ends[0], p0, &ref0); dpf::detail::blocked::eval_share(k0, ends[1], p0, &ref0); dpf::detail::blocked::eval_share(k1, ends[0], p1, &ref1); dpf::detail::blocked::eval_share(k1, ends[1], p1, &ref1); dpf::detail::vdpf::fold_output_binding(ref0, k0); dpf::detail::vdpf::fold_output_binding(ref1, k1); EXPECT_TRUE(tokens_equal(multi0, ref0)); EXPECT_TRUE(tokens_equal(multi1, ref1)); // Replaying only the tip endpoint on the warm path is idempotent. dpf::proof_token path0{}, path1{}; dpf::detail::vdpf::init_proof(path0, k0); dpf::detail::vdpf::init_proof(path1, k1); auto q0 = dpf::make_basic_path_memoizer(k0); auto q1 = dpf::make_basic_path_memoizer(k1); dpf::detail::blocked::eval_share(k0, ends[0], q0, &path0); dpf::detail::blocked::eval_share(k0, ends[1], q0, &path0); dpf::detail::blocked::eval_share(k1, ends[0], q1, &path1); dpf::detail::blocked::eval_share(k1, ends[1], q1, &path1); const auto path_once0 = path0; const auto path_once1 = path1; dpf::detail::blocked::eval_share(k0, ends[1], q0, &path0); dpf::detail::blocked::eval_share(k1, ends[1], q1, &path1); EXPECT_TRUE(tokens_equal(path0, path_once0)); EXPECT_TRUE(tokens_equal(path1, path_once1)); } // --- Off-path / on-path / all-ones blocked point --------------------------- TEST(VdpfRegression, BlockedPointProveOffPathOnPathAndAllOnes) { auto [k0, k1] = dpf::make_dpf(Input{0x2a}, dpf::block_width<4>(dpf::lt(std::uint64_t{1})), dpf::verifiable{}); const Input queries[] = { Input{0x2a}, // on-path Input{0x11}, // off-path with parks Input{0xff}, // never parks (all right) Input{0x00}, // parks every level }; for (Input x : queries) { dpf::proof_token a{}, b{}; dpf::eval_point(dpf::cmp, k0, x, dpf::prove(a)); dpf::eval_point(dpf::cmp, k1, x, dpf::prove(b)); EXPECT_TRUE(dpf::verify(a, b)) << "query=" << int(x); } } TEST(VdpfRegression, BlockedTokenDiffersFromNativeForSameAlpha) { const Input alpha{0x2a}; const Input x{0x11}; auto [b0, b1] = dpf::make_dpf(alpha, dpf::block_width<4>(dpf::lt(std::uint64_t{1})), dpf::verifiable{}); auto [n0, n1] = dpf::make_dpf(alpha, dpf::lt(std::uint64_t{1}), dpf::verifiable{}); dpf::proof_token bp0{}, bp1{}, np0{}, np1{}; dpf::eval_point(dpf::cmp, b0, x, dpf::prove(bp0)); dpf::eval_point(dpf::cmp, b1, x, dpf::prove(bp1)); dpf::eval_point(dpf::cmp, n0, x, dpf::prove(np0)); dpf::eval_point(dpf::cmp, n1, x, dpf::prove(np1)); EXPECT_TRUE(dpf::verify(bp0, bp1)); EXPECT_TRUE(dpf::verify(np0, np1)); EXPECT_FALSE(tokens_equal(bp0, np0)); } // --- Warm interval memoizer ------------------------------------------------ TEST(VdpfRegression, CmpIntervalProveIgnoresWarmMemoizer) { auto [k0, k1] = dpf::make_dpf(Input{0x20}, dpf::lt(std::uint64_t{1}), dpf::verifiable{}); using Key = std::decay_t; const Input lo{0x10}; const Input hi{0x28}; dpf::proof_token cold0{}, cold1{}; dpf::prove_cmp_interval(k0, lo, hi, dpf::prove(cold0)); dpf::prove_cmp_interval(k1, lo, hi, dpf::prove(cold1)); EXPECT_TRUE(dpf::verify(cold0, cold1)); // Warm a full-tree memo on a wider interval, then prove on [lo,hi] // through the same memo. Proving must clear/revisit upper levels. constexpr std::size_t stop = Key::cmp_depth == 0 ? Key::depth : Key::cmp_depth; HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") dpf::detail::incr::cmp_full_interval_memo memo0{256}; dpf::detail::incr::cmp_full_interval_memo memo1{256}; HEDLEY_PRAGMA(GCC diagnostic pop) auto warm_buf0 = dpf::make_output_buffer(dpf::cmp, k0, Input{0x00}, Input{0x3f}); auto warm_buf1 = dpf::make_output_buffer(dpf::cmp, k1, Input{0x00}, Input{0x3f}); dpf::eval_interval(dpf::cmp, k0, Input{0x00}, Input{0x3f}, warm_buf0, memo0); dpf::eval_interval(dpf::cmp, k1, Input{0x00}, Input{0x3f}, warm_buf1, memo1); auto buf0 = dpf::make_output_buffer(dpf::cmp, k0, lo, hi); auto buf1 = dpf::make_output_buffer(dpf::cmp, k1, lo, hi); dpf::proof_token warm0{}, warm1{}; dpf::eval_interval(dpf::cmp, k0, lo, hi, buf0, memo0, dpf::prove(warm0)); dpf::eval_interval(dpf::cmp, k1, lo, hi, buf1, memo1, dpf::prove(warm1)); EXPECT_TRUE(dpf::verify(warm0, warm1)); EXPECT_TRUE(tokens_equal(cold0, warm0)); EXPECT_TRUE(tokens_equal(cold1, warm1)); } // --- Sequence / path-memo aggregate without re-init ------------------------ TEST(VdpfRegression, CmpSequenceProveMatchesIntervalRunCovers) { // prove_cmp_sequence folds contiguous runs as intervals, not point walks. auto [k0, k1] = dpf::make_dpf(Input{0x55}, dpf::gt(std::uint64_t{1}), dpf::verifiable{}); const Input xs[] = {0x10, 0x11, 0x40, 0x41}; dpf::proof_token seq0{}, seq1{}; dpf::prove_cmp_sequence(k0, std::begin(xs), std::end(xs), dpf::prove(seq0)); dpf::prove_cmp_sequence(k1, std::begin(xs), std::end(xs), dpf::prove(seq1)); EXPECT_TRUE(dpf::verify(seq0, seq1)); dpf::proof_token runs0{}, runs1{}; dpf::detail::vdpf::init_proof(runs0, k0); dpf::detail::vdpf::init_proof(runs1, k1); dpf::detail::incr::prove_fold_cmp_interval(k0, Input{0x10}, Input{0x11}, runs0); dpf::detail::incr::prove_fold_cmp_interval(k0, Input{0x40}, Input{0x41}, runs0); dpf::detail::incr::prove_fold_cmp_interval(k1, Input{0x10}, Input{0x11}, runs1); dpf::detail::incr::prove_fold_cmp_interval(k1, Input{0x40}, Input{0x41}, runs1); dpf::detail::vdpf::fold_output_binding(runs0, k0); dpf::detail::vdpf::fold_output_binding(runs1, k1); EXPECT_TRUE(tokens_equal(seq0, runs0)); EXPECT_TRUE(tokens_equal(seq1, runs1)); // eval_sequence(cmp, ..., prove) uses path-memo point folds — a different // transcript from interval-run covers. Both must verify; they need not match. auto buf0 = dpf::make_output_buffer(dpf::cmp, k0, 4); auto buf1 = dpf::make_output_buffer(dpf::cmp, k1, 4); dpf::proof_token e0{}, e1{}; dpf::eval_sequence(dpf::cmp, k0, std::begin(xs), std::end(xs), buf0, dpf::prove(e0)); dpf::eval_sequence(dpf::cmp, k1, std::begin(xs), std::end(xs), buf1, dpf::prove(e1)); EXPECT_TRUE(dpf::verify(e0, e1)); EXPECT_FALSE(tokens_equal(seq0, e0)); } // --- Unsigned prefix vs resumed point folds -------------------------------- TEST(VdpfRegression, UnsignedPrefixTokenMatchesResumedPointFolds) { // Bit payload: unsigned prefix parity is a bit-DPF / XOR gadget. auto [k0, k1] = dpf::make_dpf(Input{0x2a}, dpf::bit{1}, dpf::verifiable{}); const std::array ends{Input{0x10}, Input{0x2b}, Input{0x40}}; dpf::proof_token pref0{}, pref1{}; auto [p0, n0] = grotto::prefix_parities(k0, ends, dpf::prove(pref0)); auto [p1, n1] = grotto::prefix_parities(k1, ends, dpf::prove(pref1)); (void)n0; (void)n1; EXPECT_TRUE(dpf::verify(pref0, pref1)); for (std::size_t i = 0; i < ends.size(); ++i) EXPECT_EQ(p0[i] ^ p1[i], ends[i] > Input{0x2a}); const auto again0 = pref0; const auto again1 = pref1; grotto::prefix_parities(k0, ends, dpf::prove(pref0)); grotto::prefix_parities(k1, ends, dpf::prove(pref1)); EXPECT_TRUE(tokens_equal(pref0, again0)); EXPECT_TRUE(tokens_equal(pref1, again1)); } TEST(VdpfRegression, PrefixParityAtAlphaIsExclusiveAndStable) { // Exclusive of alpha: endpoint == alpha must reconstruct to 0 for every // key. A uint64 payload is the wrong gadget type and used to look "flaky". const Input alpha{0x2a}; const std::array ends{alpha}; for (int t = 0; t < 32; ++t) { auto [k0, k1] = dpf::make_dpf(alpha, dpf::bit{1}); auto [p0, n0] = grotto::prefix_parities(k0, ends); auto [p1, n1] = grotto::prefix_parities(k1, ends); (void)n0; (void)n1; EXPECT_FALSE(p0[0] ^ p1[0]) << "trial " << t; } } // --- Offset Horner / poly multi-power batch -------------------------------- TEST(VdpfRegression, OffsetHornerPerPowerTokensIndependent) { constexpr std::size_t D = 2; const Input center = 12; auto mat = grotto::make_offset_horner_keys(center, dpf::verifiable{}); const std::vector knots{0, 10, 50}; const std::vector> coeff{ {1, 0, 0}, {0, 2, 0}, {7, 1, 3}}; const Input eta = 3; dpf::proof_token t0[D + 1]{}, t1[D + 1]{}; const auto v0 = grotto::offset_horner_eval<0, D, Input, true>( mat, knots, coeff, eta, t0); const auto v1 = grotto::offset_horner_eval<1, D, Input, true>( mat, knots, coeff, eta, t1); EXPECT_EQ(v0 + v1, grotto::offset_horner_clear(center, knots, coeff, eta)); for (std::size_t m = 0; m <= D; ++m) EXPECT_TRUE(dpf::verify(t0[m], t1[m])); // Tamper power 1 only; other powers still verify. t0[1][0] = simde_mm_xor_si128(t0[1][0], simde_mm_set1_epi8(0x7e)); EXPECT_FALSE(dpf::verify(t0[1], t1[1])); EXPECT_TRUE(dpf::verify(t0[0], t1[0])); EXPECT_TRUE(dpf::verify(t0[2], t1[2])); } TEST(VdpfRegression, SignedPrefixBlockedProveValuesMatchDense) { auto [b0, b1] = dpf::make_dpf(Input{0x30}, dpf::block_width<4>(dpf::gt(std::uint64_t{1})), dpf::verifiable{}); auto [d0, d1] = dpf::make_dpf(Input{0x30}, dpf::gt(std::uint64_t{1}), dpf::verifiable{}); const std::array ends{Input{0x10}, Input{0x20}, Input{0x50}}; dpf::proof_token bp0{}, bp1{}; auto bs0 = grotto::signed_prefix_parities(b0, ends, dpf::prove(bp0)); auto bs1 = grotto::signed_prefix_parities(b1, ends, dpf::prove(bp1)); EXPECT_TRUE(dpf::verify(bp0, bp1)); auto ds0 = grotto::signed_prefix_parities(d0, ends); auto ds1 = grotto::signed_prefix_parities(d1, ends); for (std::size_t i = 0; i < ends.size(); ++i) { EXPECT_EQ((bs0[i] + bs1[i]) & b0.cmp().mask, (ds0[i] + ds1[i]) & d0.cmp().mask); } }