libdpf/test/tests/vdpf_regression_test.cpp
Ryan Henry 0d22946a0e Checkpoint the party/runtime stack before share-program and malicious-mode work.
Ship the TLS mesh, composer, Beaver/Yao/leaf MPC, prep/online paths, apps, and docs so the tree is pushable before elevating share_expr, security_mode, and prep resume.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-28 05:59:19 -06:00

399 lines
16 KiB
C++

/// @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 <gtest/gtest.h>
#include <tuple>
#include <array>
#include <cstdint>
#include <cstring>
#include <vector>
#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<Input, 2> 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<decltype(k0)>;
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<Key, stop> memo0{256};
dpf::detail::incr::cmp_full_interval_memo<Key, stop> 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<Input, 3> 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<Input, 1> 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<Input, D>(center, dpf::verifiable{});
const std::vector<Input> knots{0, 10, 50};
const std::vector<std::array<std::uint64_t, D + 1>> 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<D>(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<Input, 3> 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);
}
}