#include #include "dpf.hpp" #include "grotto/offset_horner.hpp" #include "grotto/lut_union.hpp" #include "grotto/offset_jet.hpp" #include "grotto/offset_repr.hpp" #include "grotto/offset_twist.hpp" #include #include #include #include #include #include #include #include namespace { template struct has_member_alpha : std::false_type { }; template struct has_member_alpha().alpha)>> : std::true_type { }; template struct has_member_center : std::false_type { }; template struct has_member_center().center)>> : std::true_type { }; template struct has_member_beta : std::false_type { }; template struct has_member_beta().beta)>> : std::true_type { }; template struct has_both_dcf_halves : std::false_type { }; template struct has_both_dcf_halves().key_a), decltype(std::declval().key_b)>> : std::true_type { }; bool tokens_equal(const dpf::proof_token & a, const dpf::proof_token & b) { return std::memcmp(a.data(), b.data(), sizeof(dpf::proof_token)) == 0; } void xor_first_byte(void * p) { auto * bytes = static_cast(p); bytes[0] = static_cast(bytes[0] ^ 0x1u); } template Out group_neg_one() { return -Out{1}; } template void expect_point_near_modulus() { const auto beta = group_neg_one(); const std::uint8_t alpha = 0x2a; auto [k0, k1] = dpf::make_dpf(alpha, beta); const Out on = dpf::reconstruct(*dpf::eval_point(k0, alpha), *dpf::eval_point(k1, alpha)); EXPECT_EQ(on, beta); EXPECT_NE(on, Out{}); const Out off = dpf::reconstruct(*dpf::eval_point(k0, std::uint8_t{0}), *dpf::eval_point(k1, std::uint8_t{0})); EXPECT_EQ(off, Out{}); } template void expect_comparison_keeps_negative_delta() { const auto beta = group_neg_one(); const std::uint8_t alpha = 10; auto [k0, k1] = dpf::make_dpf(alpha, dpf::lt(beta)); const Out hot = dpf::reconstruct( dpf::eval_point(dpf::cmp, k0, std::uint8_t{0}), dpf::eval_point(dpf::cmp, k1, std::uint8_t{0})); EXPECT_EQ(hot, beta); EXPECT_NE(hot, Out{}); const Out cold = dpf::reconstruct( dpf::eval_point(dpf::cmp, k0, std::uint8_t{11}), dpf::eval_point(dpf::cmp, k1, std::uint8_t{11})); EXPECT_EQ(cold, Out{}); } template void expect_proof_binds_leaf_and_warm_path() { const std::uint8_t alpha = 0x2a; const Out beta = Out{9}; auto [k0, k1] = dpf::make_dpf(alpha, beta, dpf::verifiable{}); using key_t = std::decay_t; dpf::proof_token cold0{}; dpf::proof_token cold1{}; (void)*dpf::eval_point(k0, alpha, dpf::prove(cold0)); (void)*dpf::eval_point(k1, alpha, dpf::prove(cold1)); EXPECT_TRUE(dpf::verify(cold0, cold1)); EXPECT_FALSE(dpf::verify(dpf::proof_token{}, dpf::proof_token{})); dpf::basic_path_memoizer memo; (void)*dpf::eval_point(k0, alpha, memo); dpf::proof_token warm{}; (void)*dpf::eval_point(k0, alpha, dpf::prove(warm), memo); EXPECT_TRUE(tokens_equal(warm, cold0)); auto & leaves = const_cast &>(k0.leaves()); xor_first_byte(&std::get<0>(leaves).get()); dpf::proof_token tampered{}; (void)*dpf::eval_point(k0, alpha, dpf::prove(tampered), memo); EXPECT_FALSE(dpf::verify(tampered, cold1)); EXPECT_FALSE(tokens_equal(tampered, cold0)); } template void expect_from_seed_reads_past_first_word() { unsigned char lo[16]{}; unsigned char hi[16]{}; hi[8] = 1; EXPECT_NE(Seeded::from_seed(lo, sizeof(lo)), Seeded::from_seed(hi, sizeof(hi))); } template void expect_from_seed_reads_past_16_bytes() { unsigned char lo[32]{}; unsigned char hi[32]{}; hi[24] = 1; EXPECT_NE(Seeded::from_seed(lo, sizeof(lo)), Seeded::from_seed(hi, sizeof(hi))); } bool scalar_below_order(const dpf::p256_scalar & s) { for (int i = 3; i >= 0; --i) { const auto limb = s.limb(static_cast(i)); const auto bound = dpf::p256_scalar::order[i]; if (limb < bound) return true; if (limb > bound) return false; } return false; } struct IcPad { simde__m128i block() { return dpf::uniform_sample(); } std::uint8_t bit() { return 0; } }; } // namespace TEST(ClassSweep, ModularLeafAndComparisonKeepTheField) { expect_point_near_modulus(); expect_point_near_modulus(); expect_point_near_modulus(); expect_point_near_modulus(); expect_point_near_modulus(); expect_point_near_modulus(); expect_point_near_modulus(); expect_point_near_modulus(); expect_point_near_modulus(); expect_point_near_modulus(); expect_point_near_modulus(); expect_comparison_keeps_negative_delta(); expect_comparison_keeps_negative_delta(); expect_comparison_keeps_negative_delta(); expect_comparison_keeps_negative_delta(); expect_comparison_keeps_negative_delta(); expect_comparison_keeps_negative_delta(); expect_comparison_keeps_negative_delta(); expect_comparison_keeps_negative_delta(); expect_comparison_keeps_negative_delta(); expect_comparison_keeps_negative_delta(); expect_comparison_keeps_negative_delta(); } TEST(ClassSweep, SimdLeafAddReducesInTheField) { alignas(16) std::uint64_t left[2] = {dpf::fp61_mod - 1, dpf::field64::mod - 3}; alignas(16) std::uint64_t right[2] = {4, 5}; simde__m128i a{}; simde__m128i b{}; std::memcpy(&a, left, sizeof(left)); std::memcpy(&b, right, sizeof(right)); alignas(16) std::uint64_t fp_out[2]{}; const auto fp_sum = dpf::leaf_arithmetic::add_t{}(a, b); std::memcpy(fp_out, &fp_sum, sizeof(fp_out)); EXPECT_EQ(fp_out[0], dpf::fp61::reduce((dpf::fp61_mod - 1) + 4)); EXPECT_EQ(fp_out[1], dpf::fp61::reduce((dpf::field64::mod - 3) + 5)); alignas(16) std::uint64_t f64_left[2] = {dpf::field64::mod - 1, dpf::field64::mod - 4}; alignas(16) std::uint64_t f64_right[2] = {2, 6}; std::memcpy(&a, f64_left, sizeof(f64_left)); std::memcpy(&b, f64_right, sizeof(f64_right)); alignas(16) std::uint64_t f64_out[2]{}; const auto f64_sum = dpf::leaf_arithmetic::add_t{}(a, b); std::memcpy(f64_out, &f64_sum, sizeof(f64_out)); EXPECT_EQ(f64_out[0], 1u); EXPECT_EQ(f64_out[1], 2u); const auto wide = (static_cast(dpf::field128::mod_hi) << 64) | dpf::field128::mod_lo; const dpf::field128 near{wide - 1}; const dpf::field128 step{3}; simde__m128i na{}; simde__m128i nb{}; std::memcpy(&na, &near, sizeof(na)); std::memcpy(&nb, &step, sizeof(nb)); const auto f128_sum = dpf::leaf_arithmetic::add_t{}(na, nb); dpf::field128 got{}; std::memcpy(&got, &f128_sum, sizeof(got)); EXPECT_EQ(got, near + step); EXPECT_EQ(got, dpf::field128{2}); } TEST(ClassSweep, FromSeedConsumesBytesPastTheOldWindow) { expect_from_seed_reads_past_first_word(); expect_from_seed_reads_past_first_word(); expect_from_seed_reads_past_first_word(); expect_from_seed_reads_past_first_word(); expect_from_seed_reads_past_first_word(); expect_from_seed_reads_past_16_bytes(); expect_from_seed_reads_past_16_bytes(); } TEST(ClassSweep, RejectionSampleStaysInTheScalarField) { for (int i = 0; i < 32; ++i) { EXPECT_TRUE(scalar_below_order(dpf::uniform_sample())); const auto s = -dpf::p256_scalar{1}; EXPECT_EQ(-(-s), s); EXPECT_EQ(s, dpf::p256_scalar{1} - dpf::p256_scalar{2}); } } TEST(ClassSweep, MalformedCurveEncodingsAreRejected) { for (unsigned prefix : {0x00u, 0x01u, 0x04u, 0x05u}) { unsigned char bad[33]{}; bad[0] = static_cast(prefix); bad[32] = 1; EXPECT_THROW(dpf::p256::from_compressed(bad), std::invalid_argument) << prefix; } unsigned char off_curve[33]{}; off_curve[0] = 0x02; off_curve[32] = 1; EXPECT_THROW(dpf::p256::from_compressed(off_curve), std::invalid_argument); } TEST(ClassSweep, ExtractableCodomainIsTheSketchGroupOnly) { static_assert(dpf::extractable_codomain_ok_v); static_assert(dpf::extractable_codomain_ok_v>); static_assert(!dpf::extractable_codomain_ok_v); static_assert(!dpf::extractable_codomain_ok_v); static_assert(!dpf::extractable_codomain_ok_v); static_assert(!dpf::extractable_codomain_ok_v); static_assert(!dpf::extractable_codomain_ok_v); static_assert(!dpf::extractable_codomain_ok_v); static_assert(!dpf::extractable_codomain_ok_v); } TEST(ClassSweep, FloatAndDoubleLeavesUseTheXorGroup) { const float xf = dpf::leaf_group_add(1.0f, 2.0f); const float ieee_f = 1.0f + 2.0f; EXPECT_NE(xf, ieee_f); std::uint32_t fb = 0; std::memcpy(&fb, &xf, sizeof(fb)); EXPECT_EQ(fb, 0x3f800000u ^ 0x40000000u); const double xd = dpf::leaf_group_add(1.0, 2.0); EXPECT_NE(xd, 1.0 + 2.0); std::uint64_t db = 0; std::memcpy(&db, &xd, sizeof(db)); EXPECT_EQ(db, 0x3ff0000000000000ull ^ 0x4000000000000000ull); simde__m128i a = simde_mm_set1_epi32(static_cast(0x3f800000)); simde__m128i b = simde_mm_set1_epi32(static_cast(0x40000000)); const auto packed = dpf::leaf_arithmetic::add_t{}(a, b); const auto expect = simde_mm_xor_si128(a, b); EXPECT_EQ(std::memcmp(&packed, &expect, sizeof(packed)), 0); } TEST(ClassSweep, ProofsBindEveryEvalEntry) { expect_proof_binds_leaf_and_warm_path(); expect_proof_binds_leaf_and_warm_path(); expect_proof_binds_leaf_and_warm_path(); expect_proof_binds_leaf_and_warm_path(); const std::uint8_t alpha = 12; auto [c0, c1] = dpf::make_dpf(alpha, dpf::lt(std::uint64_t{9}), dpf::verifiable{}); dpf::proof_token before{}; dpf::proof_token after{}; (void)dpf::eval_point(dpf::cmp, c0, std::uint8_t{3}, dpf::prove(before)); auto & words = const_cast &>(c0.value_cw()); xor_first_byte(&words[0]); (void)dpf::eval_point(dpf::cmp, c0, std::uint8_t{3}, dpf::prove(after)); EXPECT_FALSE(tokens_equal(before, after)); dpf::proof_token other{}; (void)dpf::eval_point(dpf::cmp, c1, std::uint8_t{3}, dpf::prove(other)); EXPECT_FALSE(dpf::verify(after, other)); auto [p0, p1] = dpf::make_dpf(std::uint8_t{4}, std::uint32_t{3}, dpf::verifiable{}); const std::uint8_t from = 1; const std::uint8_t to = 6; dpf::proof_token i0{}; dpf::proof_token i1{}; dpf::prove_interval(p0, from, to, dpf::prove(i0)); dpf::prove_interval(p1, from, to, dpf::prove(i1)); EXPECT_TRUE(dpf::verify(i0, i1)); EXPECT_FALSE(dpf::verify(dpf::proof_token{}, dpf::proof_token{})); dpf::proof_token full0{}; dpf::proof_token full1{}; dpf::prove_full(p0, dpf::prove(full0)); dpf::prove_full(p1, dpf::prove(full1)); EXPECT_TRUE(dpf::verify(full0, full1)); std::array seq{2, 3, 4, 9}; dpf::proof_token s0{}; dpf::proof_token s1{}; dpf::prove_sequence(p0, seq.begin(), seq.end(), dpf::prove(s0)); dpf::prove_sequence(p1, seq.begin(), seq.end(), dpf::prove(s1)); EXPECT_TRUE(dpf::verify(s0, s1)); dpf::proof_token s0_again{}; dpf::prove_sequence(p0, seq.begin(), seq.end(), dpf::prove(s0_again)); EXPECT_TRUE(tokens_equal(s0, s0_again)); // Packed leaves may touch slots next to [from, to]. The token does not use the weights. std::vector weights(256, 1u); dpf::proof_token dot{}; (void)dpf::eval_inner_product(p0, from, to, weights, dpf::prove(dot)); EXPECT_TRUE(tokens_equal(dot, i0)); dpf::proof_token a0{}; dpf::proof_token a1{}; dpf::proof_token b0{}; dpf::proof_token b1{}; (void)*dpf::eval_point(p0, std::uint8_t{4}, dpf::prove(a0)); (void)*dpf::eval_point(p1, std::uint8_t{4}, dpf::prove(b0)); (void)*dpf::eval_point(p0, std::uint8_t{5}, dpf::prove(a1)); (void)*dpf::eval_point(p1, std::uint8_t{5}, dpf::prove(b1)); std::array left{a0, a1}; std::array right{b0, b1}; EXPECT_TRUE(dpf::verify_batch(left, right)); // A swap of equal second halves is a no-op. Flip one byte of each half // so a batch that folds only token[0] still accepts the second-half flip. auto flipped_lo = left; xor_first_byte(&flipped_lo[0][0]); EXPECT_FALSE(dpf::verify_batch(flipped_lo, right)); auto flipped_hi = left; xor_first_byte(&flipped_hi[1][1]); EXPECT_FALSE(dpf::verify_batch(flipped_hi, right)); } TEST(ClassSweep, OneComparisonHalfIsNotThePredicate) { const std::uint8_t thresh = 40; const std::uint64_t if_true = 19; auto keys = dpf::make_dpf3_cmp(thresh, if_true, std::uint64_t{0}); auto & k1 = std::get<0>(keys); auto & k2 = std::get<1>(keys); auto & k3 = std::get<2>(keys); static_assert(!has_both_dcf_halves>::value); static_assert(!has_both_dcf_halves>::value); const auto full1 = dpf::eval_full(k1); ASSERT_EQ(full1.size(), 256u); int hidden = 0; for (unsigned x = 0; x < 256; ++x) { const auto q = static_cast(x); dpf::basic_path_memoizer> memo; const auto s1 = dpf::eval_dpf3_cmp(k1, q, memo); const auto s1_again = dpf::eval_dpf3_cmp(k1, q, memo); const auto s2 = dpf::eval_dpf3_cmp(k2, q); const auto s3 = dpf::eval_dpf3_cmp(k3, q); EXPECT_EQ(s1, s1_again); EXPECT_EQ(s1, s3); EXPECT_EQ(s1, full1[x]); const auto opened = dpf::reconstruct_cmp_halves(s1, s2); const dpf::fp61 want = x < thresh ? dpf::fp61{if_true} : dpf::fp61{}; EXPECT_EQ(opened, want) << x; if (dpf::fp61{s1} != want && dpf::fp61{s2} != want) ++hidden; } EXPECT_GT(hidden, 200); } TEST(ClassSweep, OpenedResultsDoNotCarryTheSecretPoint) { static_assert(!has_member_alpha>::value); static_assert(!has_member_beta>::value); static_assert(!has_member_alpha::value); static_assert(!has_member_center>::value); static_assert(!has_member_center>::value); static_assert(!has_member_center>::value); static_assert(!has_member_center>::value); static_assert(!has_member_center>::value); static_assert(!has_member_center>::value); static_assert(!has_member_center>::value); static_assert(!has_member_center>::value); std::vector none; auto empty = dpf::geneval_ic(std::uint8_t{1}, std::uint8_t{2}, none.begin(), none.end(), dpf::ds_randomness), IcPad>{ &dpf::uniform_sample, {}}, dpf::ic(std::uint8_t{0}, std::uint8_t{4}, std::uint32_t{1}, std::uint32_t{0})); EXPECT_FALSE(dpf::verify(empty.proof0, empty.proof1)); } TEST(ClassSweep, ConstrainedComparisonAbortsUnlessAdjacent) { const std::uint64_t samples[][2] = { {0, 0}, {0, 2}, {5, 8}, {100, 0}, {std::numeric_limits::max(), 0}, {20, 20}, }; for (const auto & pair : samples) EXPECT_THROW(dpf::local_ccmp(pair[0], pair[1]), std::invalid_argument) << pair[0] << "," << pair[1]; EXPECT_NO_THROW(dpf::local_ccmp(4, 5)); EXPECT_NO_THROW(dpf::local_ccmp(5, 4)); EXPECT_NO_THROW(dpf::local_ccmp(0, 1)); EXPECT_NO_THROW(dpf::local_ccmp( std::numeric_limits::max(), std::numeric_limits::max() - 1)); } TEST(ClassSweep, SignedJetMatchesClearForEveryDegreeAtLeastTwo) { const std::int8_t centers[] = {-40, -7, -1, 3}; const int etas[] = {-12, 0, 5, 18}; for (std::size_t degree = 2; degree <= 3; ++degree) { std::vector coeff(degree + 1, 1); coeff[2] = 3; const std::vector knots{std::numeric_limits::min()}; for (std::int8_t center : centers) { const auto mat = grotto::make_offset_jet_keys(center, degree); for (int eta : etas) { const auto e = static_cast(eta); const auto s0 = grotto::offset_jet_eval<0>(mat, knots, coeff, e); const auto s1 = grotto::offset_jet_eval<1>(mat, knots, coeff, e); const auto clear = grotto::offset_jet_clear( center, knots, coeff, e); EXPECT_EQ(s0 + s1, clear) << "degree=" << degree << " center=" << int(center) << " eta=" << eta; } } } } TEST(ClassSweep, DyadicShiftMatchesClearAcrossDegrees) { const std::uint8_t centers[] = {1, 4, 200}; const int etas[] = {0, 3, 39, 100}; for (std::size_t degree = 0; degree <= 2; ++degree) { std::vector coeff(degree + 1, 1); if (degree >= 1) coeff[1] = 3; const std::vector knots{0}; for (std::uint8_t center : centers) { const auto mat = grotto::make_offset_twist_keys( center, degree, grotto::twist_half); for (int eta : etas) { const auto e = static_cast(eta); const auto s0 = grotto::offset_twist_eval<0>(mat, knots, coeff, e); const auto s1 = grotto::offset_twist_eval<1>(mat, knots, coeff, e); const auto clear = grotto::offset_twist_clear( center, grotto::twist_half, knots, coeff, e); EXPECT_EQ(s0 + s1, clear) << "degree=" << degree << " center=" << int(center) << " eta=" << eta; EXPECT_NE(s0, clear); EXPECT_NE(s1, clear); } } } } TEST(ClassSweep, HornerSharesMatchClearAndHideTheCenter) { constexpr std::size_t D = 2; const std::uint8_t center = 9; const auto mat = grotto::make_offset_horner_keys(center); const std::vector knots{0, 40}; const std::vector> coeff{ {1, 2, 0}, {4, 0, 1}, }; for (std::uint8_t eta : {std::uint8_t{0}, std::uint8_t{3}, std::uint8_t{70}}) { const auto s0 = grotto::offset_horner_eval<0, D>(mat, knots, coeff, eta); const auto s1 = grotto::offset_horner_eval<1, D>(mat, knots, coeff, eta); const auto clear = grotto::offset_horner_clear(center, knots, coeff, eta); EXPECT_EQ(s0 + s1, clear) << int(eta); } }