libdpf/test/tests/class_sweep_test.cpp

536 lines
20 KiB
C++
Raw Normal View History

#include <gtest/gtest.h>
#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 <array>
#include <cstdint>
#include <cstring>
#include <limits>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <vector>
namespace
{
template <typename T, typename = void>
struct has_member_alpha : std::false_type
{ };
template <typename T>
struct has_member_alpha<T, std::void_t<decltype(std::declval<T>().alpha)>>
: std::true_type
{ };
template <typename T, typename = void>
struct has_member_center : std::false_type
{ };
template <typename T>
struct has_member_center<T, std::void_t<decltype(std::declval<T>().center)>>
: std::true_type
{ };
template <typename T, typename = void>
struct has_member_beta : std::false_type
{ };
template <typename T>
struct has_member_beta<T, std::void_t<decltype(std::declval<T>().beta)>>
: std::true_type
{ };
template <typename T, typename = void>
struct has_both_dcf_halves : std::false_type
{ };
template <typename T>
struct has_both_dcf_halves<T, std::void_t<
decltype(std::declval<T>().key_a), decltype(std::declval<T>().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<unsigned char *>(p);
bytes[0] = static_cast<unsigned char>(bytes[0] ^ 0x1u);
}
template <typename Out>
Out group_neg_one()
{
return -Out{1};
}
template <typename Out>
void expect_point_near_modulus()
{
const auto beta = group_neg_one<Out>();
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 <typename Out>
void expect_comparison_keeps_negative_delta()
{
const auto beta = group_neg_one<Out>();
const std::uint8_t alpha = 10;
auto [k0, k1] = dpf::make_dpf(alpha, dpf::lt(beta));
const Out hot = dpf::reconstruct(
dpf::eval_point<Out>(dpf::cmp, k0, std::uint8_t{0}),
dpf::eval_point<Out>(dpf::cmp, k1, std::uint8_t{0}));
EXPECT_EQ(hot, beta);
EXPECT_NE(hot, Out{});
const Out cold = dpf::reconstruct(
dpf::eval_point<Out>(dpf::cmp, k0, std::uint8_t{11}),
dpf::eval_point<Out>(dpf::cmp, k1, std::uint8_t{11}));
EXPECT_EQ(cold, Out{});
}
template <typename Out>
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<decltype(k0)>;
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<key_t> 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<std::decay_t<decltype(k0.leaves())> &>(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 <typename Seeded>
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 <typename Seeded>
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<std::size_t>(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<simde__m128i>(); }
std::uint8_t bit() { return 0; }
};
} // namespace
TEST(ClassSweep, ModularLeafAndComparisonKeepTheField)
{
expect_point_near_modulus<dpf::fp61>();
expect_point_near_modulus<dpf::field64>();
expect_point_near_modulus<dpf::field128>();
expect_point_near_modulus<dpf::p256_scalar>();
expect_point_near_modulus<dpf::gf2>();
expect_point_near_modulus<dpf::gf22>();
expect_point_near_modulus<dpf::gf24>();
expect_point_near_modulus<dpf::gf28>();
expect_point_near_modulus<dpf::gf216>();
expect_point_near_modulus<dpf::gf232>();
expect_point_near_modulus<dpf::gf264>();
expect_comparison_keeps_negative_delta<dpf::fp61>();
expect_comparison_keeps_negative_delta<dpf::field64>();
expect_comparison_keeps_negative_delta<dpf::field128>();
expect_comparison_keeps_negative_delta<dpf::p256_scalar>();
expect_comparison_keeps_negative_delta<dpf::gf2>();
expect_comparison_keeps_negative_delta<dpf::gf22>();
expect_comparison_keeps_negative_delta<dpf::gf24>();
expect_comparison_keeps_negative_delta<dpf::gf28>();
expect_comparison_keeps_negative_delta<dpf::gf216>();
expect_comparison_keeps_negative_delta<dpf::gf232>();
expect_comparison_keeps_negative_delta<dpf::gf264>();
}
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<dpf::fp61, simde__m128i>{}(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<dpf::field64, simde__m128i>{}(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<unsigned __int128>(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<dpf::field128, simde__m128i>{}(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<dpf::fp61>();
expect_from_seed_reads_past_first_word<dpf::field64>();
expect_from_seed_reads_past_first_word<dpf::field128>();
expect_from_seed_reads_past_first_word<dpf::p256_scalar>();
expect_from_seed_reads_past_first_word<dpf::p256>();
expect_from_seed_reads_past_16_bytes<dpf::p256_scalar>();
expect_from_seed_reads_past_16_bytes<dpf::p256>();
}
TEST(ClassSweep, RejectionSampleStaysInTheScalarField)
{
for (int i = 0; i < 32; ++i)
{
EXPECT_TRUE(scalar_below_order(dpf::uniform_sample<dpf::p256_scalar>()));
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<unsigned char>(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<dpf::fp61>);
static_assert(dpf::extractable_codomain_ok_v<dpf::xor_wrapper<dpf::fp61>>);
static_assert(!dpf::extractable_codomain_ok_v<dpf::field64>);
static_assert(!dpf::extractable_codomain_ok_v<dpf::field128>);
static_assert(!dpf::extractable_codomain_ok_v<dpf::p256>);
static_assert(!dpf::extractable_codomain_ok_v<dpf::p256_scalar>);
static_assert(!dpf::extractable_codomain_ok_v<std::uint64_t>);
static_assert(!dpf::extractable_codomain_ok_v<float>);
static_assert(!dpf::extractable_codomain_ok_v<double>);
}
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<int>(0x3f800000));
simde__m128i b = simde_mm_set1_epi32(static_cast<int>(0x40000000));
const auto packed = dpf::leaf_arithmetic::add_t<float, simde__m128i>{}(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<std::uint32_t>();
expect_proof_binds_leaf_and_warm_path<std::uint64_t>();
expect_proof_binds_leaf_and_warm_path<dpf::fp61>();
expect_proof_binds_leaf_and_warm_path<dpf::field64>();
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<std::decay_t<decltype(c0.value_cw())> &>(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<std::uint8_t, 4> 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<std::uint32_t> 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<dpf::proof_token, 2> left{a0, a1};
std::array<dpf::proof_token, 2> 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<std::decay_t<decltype(k1)>>::value);
static_assert(!has_both_dcf_halves<std::decay_t<decltype(k2)>>::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<std::uint8_t>(x);
dpf::basic_path_memoizer<std::decay_t<decltype(k1.dpf_key)>> 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<dpf::geneval_result<std::uint32_t, simde__m128i>>::value);
static_assert(!has_member_beta<dpf::geneval_result<std::uint32_t, simde__m128i>>::value);
static_assert(!has_member_alpha<dpf::geneval_cmp_result>::value);
static_assert(!has_member_center<grotto::geneval_offset_horner_result<2, std::uint8_t>>::value);
static_assert(!has_member_center<grotto::geneval_lut_union_result<std::uint8_t>>::value);
static_assert(!has_member_center<grotto::geneval_offset_horner_result<3, std::int8_t>>::value);
static_assert(!has_member_center<grotto::offset_horner_keys<std::uint8_t, 2, false>>::value);
static_assert(!has_member_center<grotto::offset_poly_keys<std::uint8_t>>::value);
static_assert(!has_member_center<grotto::offset_jet_keys<std::uint8_t>>::value);
static_assert(!has_member_center<grotto::offset_repr_keys<std::uint8_t>>::value);
static_assert(!has_member_center<grotto::offset_twist_keys<std::uint8_t>>::value);
std::vector<std::uint8_t> none;
auto empty = dpf::geneval_ic(std::uint8_t{1}, std::uint8_t{2},
none.begin(), none.end(),
dpf::ds_randomness<decltype(&dpf::uniform_sample<simde__m128i>), IcPad>{
&dpf::uniform_sample<simde__m128i>, {}},
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<std::uint64_t>::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<std::uint64_t>::max(),
std::numeric_limits<std::uint64_t>::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<std::uint64_t> coeff(degree + 1, 1);
coeff[2] = 3;
const std::vector<std::int8_t> knots{std::numeric_limits<std::int8_t>::min()};
for (std::int8_t center : centers)
{
const auto mat = grotto::make_offset_jet_keys<std::int8_t>(center, degree);
for (int eta : etas)
{
const auto e = static_cast<std::int8_t>(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<std::int8_t>(
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<std::uint64_t> coeff(degree + 1, 1);
if (degree >= 1)
coeff[1] = 3;
const std::vector<std::uint8_t> knots{0};
for (std::uint8_t center : centers)
{
const auto mat = grotto::make_offset_twist_keys<std::uint8_t>(
center, degree, grotto::twist_half);
for (int eta : etas)
{
const auto e = static_cast<std::uint8_t>(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<std::uint8_t, D>(center);
const std::vector<std::uint8_t> knots{0, 40};
const std::vector<std::array<std::uint64_t, D + 1>> 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<D>(center, knots, coeff, eta);
EXPECT_EQ(s0 + s1, clear) << int(eta);
}
}