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>
This commit is contained in:
Ryan Henry 2026-09-28 05:59:19 -06:00
parent 695f8e84f7
commit 0d22946a0e
1835 changed files with 170291 additions and 2849 deletions

View file

@ -2,23 +2,23 @@
int main()
{
system("./bin/dpf_key_test");
system("./bin/wildcard_test");
(void)std::system("./bin/dpf_key_test");
(void)std::system("./bin/wildcard_test");
system("./bin/eval_point_test");
system("./bin/eval_interval_test");
system("./bin/eval_full_test");
system("./bin/eval_sequence_test");
(void)std::system("./bin/eval_point_test");
(void)std::system("./bin/eval_interval_test");
(void)std::system("./bin/eval_full_test");
(void)std::system("./bin/eval_sequence_test");
system("./bin/eval_point_multi_test");
system("./bin/eval_interval_multi_test");
system("./bin/eval_full_multi_test");
system("./bin/eval_sequence_multi_test");
(void)std::system("./bin/eval_point_multi_test");
(void)std::system("./bin/eval_interval_multi_test");
(void)std::system("./bin/eval_full_multi_test");
(void)std::system("./bin/eval_sequence_multi_test");
system("./bin/advice_bit_iterable_test");
system("./bin/parallel_bit_iterable_test");
system("./bin/setbit_index_iterable_test");
system("./bin/keyword2_test");
(void)std::system("./bin/advice_bit_iterable_test");
(void)std::system("./bin/parallel_bit_iterable_test");
(void)std::system("./bin/setbit_index_iterable_test");
(void)std::system("./bin/keyword2_test");
return 0;
}

View file

@ -0,0 +1,241 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <vector>
#include "dpf/arith_garble.hpp"
namespace
{
using dpf::arith_garble::circuit;
using dpf::arith_garble::wire;
std::vector<std::uint16_t> plain_outs(const circuit & c,
const std::uint16_t * in)
{
auto all = dpf::arith_garble::eval_plain(c, in);
std::vector<std::uint16_t> out;
for (auto id : c.outputs())
out.push_back(all[id]);
return out;
}
void expect_match(const circuit & c, const std::vector<std::uint16_t> & in)
{
auto got = dpf::arith_garble::eval_pair(c, in.data());
auto want = plain_outs(c, in.data());
ASSERT_EQ(got.opened, want);
ASSERT_EQ(got.mask.size(), want.size());
for (std::size_t i = 0; i < want.size(); ++i)
{
EXPECT_EQ(got.opened[i],
dpf::arith_garble::open_shares(got.modulus[i], got.mask[i], got.color[i]));
}
}
} // namespace
TEST(ArithGarble, ProjectionRowsAreModulusMinusOne)
{
circuit c;
auto x = c.input(5);
std::vector<std::uint16_t> sq{0, 1, 4, 4, 1};
c.out(c.project(x, 5, sq));
auto got = dpf::arith_garble::eval_pair(c, std::vector<std::uint16_t>{3}.data());
EXPECT_EQ(got.ciphertext_rows, 4u);
EXPECT_EQ(got.opened[0], 4u);
}
TEST(ArithGarble, AddScaleAndPublicShift)
{
circuit c;
auto x = c.input(7);
auto y = c.input(7);
auto s = c.add(x, y);
auto t = c.scale(s, 3);
c.out(c.add_const(t, 4));
for (std::uint16_t a = 0; a < 7; ++a)
for (std::uint16_t b = 0; b < 7; ++b)
expect_match(c, {a, b});
}
TEST(ArithGarble, ThresholdCostsFanInRows)
{
circuit c;
std::vector<wire> bits;
for (int i = 0; i < 3; ++i)
bits.push_back(c.input(4));
c.out(c.threshold(bits, 3));
auto got = dpf::arith_garble::eval_pair(
c, std::vector<std::uint16_t>{1, 1, 1}.data());
EXPECT_EQ(got.ciphertext_rows, 3u);
EXPECT_EQ(got.opened[0], 1u);
expect_match(c, {1, 1, 0});
expect_match(c, {0, 0, 0});
}
TEST(ArithGarble, FanInAndOfBits)
{
circuit c;
std::vector<wire> bits;
for (int i = 0; i < 4; ++i)
bits.push_back(c.input(2));
c.out(c.fanin_and(bits));
expect_match(c, {1, 1, 1, 1});
expect_match(c, {1, 1, 0, 1});
expect_match(c, {0, 0, 0, 0});
}
TEST(ArithGarble, PrimeProduct)
{
for (std::uint16_t p : {2, 3, 5, 7})
{
circuit c;
auto x = c.input(p);
auto y = c.input(p);
c.out(c.mul(x, y));
for (std::uint16_t a = 0; a < p; ++a)
for (std::uint16_t b = 0; b < p; ++b)
expect_match(c, {a, b});
}
}
TEST(ArithGarble, ChainedProduct)
{
circuit c;
auto x = c.input(5);
auto y = c.input(5);
auto z = c.input(5);
c.out(c.mul(c.add(x, y), z));
expect_match(c, {2, 4, 3});
expect_match(c, {0, 1, 4});
expect_match(c, {4, 4, 0});
}
TEST(ArithGarble, EveryResidueOfAProjectionAndAScale)
{
circuit c;
auto x = c.input(11);
std::vector<std::uint16_t> cube(11);
for (std::uint16_t i = 0; i < 11; ++i)
cube[i] = static_cast<std::uint16_t>((i * i * i) % 11);
auto y = c.project(x, 11, cube);
c.out(y);
c.out(c.scale(y, 10));
c.out(c.add_const(x, 18));
for (std::uint16_t a = 0; a < 11; ++a)
{
expect_match(c, {a});
auto got = dpf::arith_garble::eval_pair(c, &a);
EXPECT_EQ(got.ciphertext_rows, 10u);
}
}
TEST(ArithGarble, FreeGatesSendNoRows)
{
circuit c;
auto x = c.input(9);
auto y = c.input(9);
c.out(c.add_const(c.scale(c.add(x, y), 5), 4));
auto got = dpf::arith_garble::eval_pair(c, std::vector<std::uint16_t>{8, 8}.data());
EXPECT_EQ(got.ciphertext_rows, 0u);
EXPECT_EQ(got.opened[0], static_cast<std::uint16_t>((5 * (8 + 8) + 4) % 9));
}
TEST(ArithGarble, ThresholdEveryWeight)
{
circuit c;
std::vector<wire> bits;
for (int i = 0; i < 5; ++i)
bits.push_back(c.input(6));
for (std::uint16_t t = 0; t <= 5; ++t)
c.out(c.threshold(bits, t));
auto one = dpf::arith_garble::eval_pair(
c, std::vector<std::uint16_t>{1, 1, 1, 1, 1}.data());
EXPECT_EQ(one.ciphertext_rows, 5u * 6u);
for (unsigned mask = 0; mask < 32; ++mask)
{
std::vector<std::uint16_t> in(5);
for (int i = 0; i < 5; ++i)
in[i] = static_cast<std::uint16_t>((mask >> i) & 1u);
expect_match(c, in);
}
}
TEST(ArithGarble, FanInAndEveryPattern)
{
circuit c;
std::vector<wire> bits;
for (int i = 0; i < 4; ++i)
bits.push_back(c.input(2));
c.out(c.fanin_and(bits));
auto rows = dpf::arith_garble::eval_pair(
c, std::vector<std::uint16_t>{1, 1, 1, 1}.data());
EXPECT_EQ(rows.ciphertext_rows, 8u);
for (unsigned mask = 0; mask < 16; ++mask)
{
std::vector<std::uint16_t> in(4);
for (int i = 0; i < 4; ++i)
in[i] = static_cast<std::uint16_t>((mask >> i) & 1u);
expect_match(c, in);
}
}
TEST(ArithGarble, PrimeElevenAndBitScale)
{
circuit mul;
auto x = mul.input(11);
auto y = mul.input(11);
mul.out(mul.mul(x, y));
for (std::uint16_t a = 0; a < 11; ++a)
for (std::uint16_t b = 0; b < 11; ++b)
expect_match(mul, {a, b});
circuit pass;
auto word = pass.input(5);
auto bit = pass.input(2);
pass.out(pass.bit_scale(word, bit));
for (std::uint16_t w = 0; w < 5; ++w)
for (std::uint16_t b = 0; b < 2; ++b)
expect_match(pass, {w, b});
}
TEST(ArithGarble, TwoEvalutionsOpenTheSameValue)
{
circuit c;
auto x = c.input(5);
auto y = c.input(5);
c.out(c.mul(x, y));
const std::uint16_t in[2] = {3, 4};
auto a = dpf::arith_garble::eval_pair(c, in);
auto b = dpf::arith_garble::eval_pair(c, in);
EXPECT_EQ(a.opened, b.opened);
EXPECT_EQ(a.opened[0], static_cast<std::uint16_t>((3 * 4) % 5));
}
TEST(ArithGarble, RejectsBadModuliTablesAndInputs)
{
circuit c;
EXPECT_THROW(c.input(1), std::invalid_argument);
EXPECT_THROW(c.input(129), std::invalid_argument);
auto x = c.input(8);
auto y = c.input(7);
EXPECT_THROW(c.add(x, y), std::invalid_argument);
EXPECT_THROW(c.scale(x, 2), std::invalid_argument);
EXPECT_THROW(c.scale(x, 0), std::invalid_argument);
EXPECT_THROW(c.project(x, 3, {0, 1}), std::invalid_argument);
EXPECT_THROW(c.project(x, 3, std::vector<std::uint16_t>(8, 3)), std::invalid_argument);
auto bit = c.input(2);
EXPECT_THROW(c.bit_scale(x, x), std::invalid_argument);
EXPECT_THROW(c.fanin_and({x}), std::invalid_argument);
EXPECT_THROW(c.threshold({}, 0), std::invalid_argument);
circuit bare;
bare.input(3);
EXPECT_THROW(dpf::arith_garble::eval_pair(bare, std::vector<std::uint16_t>{0}.data()),
std::invalid_argument);
circuit one;
one.out(one.input(4));
EXPECT_THROW(expect_match(one, {4}), std::invalid_argument);
(void)bit;
}

View file

@ -0,0 +1,87 @@
#include <gtest/gtest.h>
#include <cctype>
#include <string>
#include <vector>
#include "cases.hpp"
#include "registry.hpp"
#include "spawn.hpp"
namespace
{
class PartyFlowTest : public ::testing::TestWithParam<std::string>
{
};
TEST_P(PartyFlowTest, Trio)
{
dpf::party::register_all_flows();
const auto & name = GetParam();
const auto * f = dpf::party::find_flow(name);
ASSERT_NE(f, nullptr) << name;
// IKNP replaces the dealer. Spawning p2 makes that role call a keygen
// that refuses `role::p2` (`iknp point/comparison keygen has no dealer`).
const bool pair = dpf::party::flow_is_pair(*f);
auto r = pair ? dpf::party::spawn_pair_flow(name)
: dpf::party::spawn_trio_flow(name);
EXPECT_EQ(r.rc[0], 0) << "p0 failed for " << name;
EXPECT_EQ(r.rc[1], 0) << "p1 failed for " << name;
if (!pair)
EXPECT_EQ(r.rc[2], 0) << "p2 failed for " << name;
else
EXPECT_EQ(r.rc[2], 0) << "pair spawn must not report a dealer";
}
std::vector<std::string> all_flow_names()
{
dpf::party::register_all_flows();
std::vector<std::string> names;
for (const auto * f : dpf::party::select_flows({}))
names.emplace_back(f->name);
return names;
}
TEST(PartyFlow, PairFlowsAreTheIknpOnes)
{
dpf::party::register_all_flows();
int pairs = 0;
int trios = 0;
for (const auto * f : dpf::party::select_flows({}))
{
ASSERT_NE(f, nullptr);
ASSERT_NE(f->name, nullptr);
const std::string name = f->name;
const bool iknp = name.rfind("iknp_", 0) == 0
|| dpf::party::flow_has_tag(*f, "iknp");
EXPECT_EQ(dpf::party::flow_is_pair(*f), iknp) << name;
if (iknp)
{
++pairs;
EXPECT_EQ(name.rfind("iknp_", 0), 0u) << name;
}
else
{
++trios;
}
}
EXPECT_GE(pairs, 5);
EXPECT_GT(trios, pairs);
}
INSTANTIATE_TEST_SUITE_P(
AllFlows,
PartyFlowTest,
::testing::ValuesIn(all_flow_names()),
[](const auto & info) {
std::string n = info.param;
for (char & c : n)
{
if (!(std::isalnum(static_cast<unsigned char>(c)) || c == '_'))
c = '_';
}
return n;
});
} // namespace

View file

@ -5,8 +5,11 @@
#include <tuple>
#include <vector>
#include <cstring>
#include "dpf/beaver.hpp"
#include "dpf/buffered_prg.hpp"
#include "dpf/doerner_shelat.hpp"
#include "dpf/modint.hpp"
namespace
@ -303,6 +306,37 @@ TEST(Beaver, InnerProductMatchesTheSumAndIsOneCross)
EXPECT_EQ(fused.open(again), expect);
}
TEST(Beaver, ScheduleObjectiveRoundsKeepsOneRound)
{
session64 latency;
latency.set_schedule_objective(dpf::beavers::schedule_objective::rounds);
auto sgn = latency.input();
auto x = latency.input();
auto a0 = latency.input();
auto a1 = latency.input();
auto lin = latency(sgn * (a1 * x + a0));
EXPECT_EQ(latency.round_of(lin), 1);
session64 prep;
prep.set_schedule_objective(dpf::beavers::schedule_objective::prep);
auto ps = prep.input();
auto px = prep.input();
auto pa0 = prep.input();
auto pa1 = prep.input();
auto plin = prep(ps * (pa1 * px + pa0));
EXPECT_EQ(prep.round_of(plin), 2);
EXPECT_LT(prep.preprocessing_count(), latency.preprocessing_count());
Counter rng;
latency.sample(rng);
latency.bind(sgn, u64{3}, rng);
latency.bind(x, u64{2}, rng);
latency.bind(a0, u64{4}, rng);
latency.bind(a1, u64{5}, rng);
latency.evaluate();
EXPECT_EQ(latency.open(lin), 3u * (5u * 2u + 4u));
}
TEST(Beaver, InnerProductPeelsASharedFactor)
{
session64 s;
@ -632,16 +666,16 @@ TEST(Beaver, InnerProductRejectsABadShape)
auto x = s.input();
auto y = s.input();
auto z = s.input();
EXPECT_THROW((void)[&] {
EXPECT_THROW([&] {
return s.dot(std::initializer_list<wire64>{}, std::initializer_list<wire64>{});
}(), std::invalid_argument);
EXPECT_THROW((void)[&] { return s.dot({x}, {y, z}); }(), std::invalid_argument);
EXPECT_THROW([&] { return s.dot({x}, {y, z}); }(), std::invalid_argument);
session64 other;
auto w = other.input();
EXPECT_THROW((void)[&] { return s.dot({x}, {w}); }(), std::invalid_argument);
EXPECT_THROW([&] { return s.dot({x}, {w}); }(), std::invalid_argument);
auto dotted = s.dot({x}, {y});
EXPECT_THROW((void)[&] { return s.dot_cross(dotted); }(), std::logic_error);
EXPECT_THROW((void)[&] { return s.dot_cross(x); }(), std::invalid_argument);
EXPECT_THROW([&] { return s.dot_cross(dotted); }(), std::logic_error);
EXPECT_THROW([&] { return s.dot_cross(x); }(), std::invalid_argument);
}
TEST(Beaver, ScaleSharesScalarBlind)
@ -1531,7 +1565,7 @@ TEST(Beaver, FactoredProductIsVisibleAndTheSexticTermIsNot)
EXPECT_EQ(s.preprocessing_count(), prep);
EXPECT_EQ(s.monomial({{x, 1u}, {z, 1u}}).open(),
s.lambda(x).open() * s.lambda(z).open());
EXPECT_THROW((void)[&] { return s.monomial({{x, 2u}, {z, 2u}}); }(),
EXPECT_THROW([&] { return s.monomial({{x, 2u}, {z, 2u}}); }(),
std::logic_error);
s.bind(x, u64{6}, rng);
s.bind(z, u64{7}, rng);
@ -1909,8 +1943,8 @@ TEST(Beaver, HighPowersAndTheExpansionLimit)
session64 s;
auto x = s.input();
EXPECT_THROW((void)[&] { return pow(x, 17u); }(), std::invalid_argument);
EXPECT_THROW((void)[&] { return pow(x, 10) * pow(x, 7); }(), std::invalid_argument);
EXPECT_THROW([&] { return pow(x, 17u); }(), std::invalid_argument);
EXPECT_THROW([&] { return pow(x, 10) * pow(x, 7); }(), std::invalid_argument);
std::vector<wire64> wide;
wide.reserve(12);
@ -1929,7 +1963,7 @@ TEST(Beaver, HighPowersAndTheExpansionLimit)
EXPECT_EQ(s.open(all), 1u);
auto thirteenth = s.input();
EXPECT_THROW((void)[&] { return s(expr12 * thirteenth); }(), std::invalid_argument);
EXPECT_THROW([&] { return s(expr12 * thirteenth); }(), std::invalid_argument);
}
TEST(Beaver, ScheduledPolynomialRejectsEarlyUse)
@ -1938,8 +1972,8 @@ TEST(Beaver, ScheduledPolynomialRejectsEarlyUse)
session64 b;
auto x = a.input();
auto y = b.input();
EXPECT_THROW((void)[&] { return x + y; }(), std::invalid_argument);
EXPECT_THROW((void)[&] { return x * y; }(), std::invalid_argument);
EXPECT_THROW([&] { return x + y; }(), std::invalid_argument);
EXPECT_THROW([&] { return x * y; }(), std::invalid_argument);
auto z = a(x + pow(x, 2));
EXPECT_THROW(a.open(z), std::logic_error);
EXPECT_THROW(a.evaluate(), std::logic_error);
@ -1959,15 +1993,15 @@ TEST(Beaver, RejectsBadUse)
dpf::beavers::session<u64> b;
auto x = a.input();
auto y = b.input();
EXPECT_THROW((void)[&] { return a.product(x, y); }(), std::invalid_argument);
EXPECT_THROW((void)[&] { return x * y; }(), std::invalid_argument);
EXPECT_THROW([&] { return a.product(x, y); }(), std::invalid_argument);
EXPECT_THROW([&] { return x * y; }(), std::invalid_argument);
auto bit = a.bit();
EXPECT_THROW(a.bind(bit, u64{2}), std::invalid_argument);
auto z = a.product(x, x);
EXPECT_THROW(a.evaluate(), std::logic_error);
a.sample();
EXPECT_THROW(a.evaluate(), std::logic_error);
EXPECT_THROW((void)[&] { return a.bit_mul(x, x); }(), std::invalid_argument);
EXPECT_THROW([&] { return a.bit_mul(x, x); }(), std::invalid_argument);
(void)z;
}
@ -1993,3 +2027,178 @@ TEST(Beaver, ProductExtremes)
EXPECT_EQ(s.open(z), want) << a << " * " << b;
}
}
TEST(BeaverAuth, AuthBeaver2HonestAndTamper)
{
auto key = dpf::sample_mac_key<u64>();
auto t = dpf::beavers::sample_auth_beaver2<u64>(key);
EXPECT_TRUE(t.verify(key));
EXPECT_EQ(t.ab.open(), t.a.open() * t.b.open());
auto bad = t;
bad.ab.tag.p0 ^= 1ull;
EXPECT_FALSE(bad.verify(key));
}
TEST(BeaverAuth, AuthBeaverMulMatchesProduct)
{
auto key = dpf::sample_mac_key<u64>();
auto bev = dpf::beavers::sample_auth_beaver2<u64>(key);
const u64 x = 7, y = 11;
auto xs = dpf::beavers::auth_share(x, key);
auto ys = dpf::beavers::auth_share(y, key);
auto [z0, z1] = dpf::beavers::auth_beaver_mul(
xs.party(0), xs.party(1), ys.party(0), ys.party(1), bev, key);
EXPECT_EQ(z0.value + z1.value, x * y);
EXPECT_TRUE(dpf::mac_verify(z0, z1, key));
z0.tag ^= 1ull;
EXPECT_FALSE(dpf::mac_verify(z0, z1, key));
}
TEST(BeaverAuth, SessionAby2ValuesVerify)
{
auto key = dpf::sample_mac_key<u64>();
session64 s;
s.set_mac_key(key);
auto x = s.input();
auto y = s.input();
auto z = s(x * y);
Counter rng;
s.sample(rng);
s.bind(x, u64{6}, rng);
s.bind(y, u64{7}, rng);
s.evaluate();
EXPECT_EQ(s.open(z), 42u);
EXPECT_TRUE(s.lambda_auth(x).verify(key));
EXPECT_TRUE(s.value_auth(x).verify(key));
EXPECT_TRUE(s.value_auth(z).verify(key));
EXPECT_TRUE(s.verify_delta(x));
EXPECT_TRUE(s.verify_delta(y));
EXPECT_TRUE(s.verify_all());
auto tampered = s.value_auth(z);
tampered.tag.p0 ^= 1ull;
EXPECT_FALSE(tampered.verify(key));
}
TEST(BeaverAuth, PartyTapeCarriesTagsAndOpeningsCheck)
{
auto key = dpf::sample_mac_key<u64>();
session64 dealer;
dealer.set_mac_key(key);
auto x = dealer.input();
auto y = dealer.input();
auto z = dealer(x * y);
Counter rng;
dealer.sample(rng);
auto tape0 = dealer.export_party(0);
auto tape1 = dealer.export_party(1);
EXPECT_TRUE(tape0.has_mac);
EXPECT_TRUE(tape1.has_mac);
EXPECT_EQ(tape0.lambda_tag.size(), tape0.lambda.size());
EXPECT_EQ(tape0.lambda[0] + tape1.lambda[0], dealer.lambda(x).open());
EXPECT_EQ(tape0.lambda_tag[0] + tape1.lambda_tag[0],
dealer.lambda_auth(x).tag.open());
dealer.bind(x, u64{3}, rng);
dealer.bind(y, u64{5}, rng);
dealer.evaluate();
EXPECT_EQ(dealer.open(z), 15u);
EXPECT_TRUE(dealer.verify_all());
auto honest = dealer.delta_auth(x);
dpf::beavers::auth_opening<u64> a{honest.value.p0, honest.tag.p0};
dpf::beavers::auth_opening<u64> b{honest.value.p1, honest.tag.p1};
EXPECT_TRUE(dpf::beavers::verify_auth_opening(a, b, key));
b.value ^= 1ull;
EXPECT_FALSE(dpf::beavers::verify_auth_opening(a, b, key));
// Party views: authenticated input shares + λ tags reconstruct δ.
auto xv = dealer.value_auth(x);
dpf::beavers::auth_opening<u64> ox0{
xv.party(0).value + tape0.lambda[0],
xv.party(0).tag + tape0.lambda_tag[0]};
dpf::beavers::auth_opening<u64> ox1{
xv.party(1).value + tape1.lambda[0],
xv.party(1).tag + tape1.lambda_tag[0]};
EXPECT_TRUE(dpf::beavers::verify_auth_opening(ox0, ox1, key));
EXPECT_EQ(ox0.value + ox1.value, dealer.delta(x));
}
TEST(Beaver, OracleAuthTagsReplayFromTheSeed)
{
using block = dpf::prg::aes128::block_type;
const block seed = simde_mm_set_epi64x(0x51, 0x52);
auto key = dpf::sample_mac_key<u64>();
dpf::beavers::oracle<u64> left(seed, 4);
dpf::beavers::oracle<u64> right(seed, 4);
auto a = dpf::beavers::sample_auth_beaver2(key, left, 3);
auto b = dpf::beavers::sample_auth_beaver2(key, right, 3);
EXPECT_EQ(a.a, b.a);
EXPECT_EQ(a.b, b.b);
EXPECT_EQ(a.ab, b.ab);
EXPECT_EQ(a.out, b.out);
EXPECT_TRUE(a.verify(key));
EXPECT_EQ(a.ab.open(), a.a.open() * a.b.open());
dpf::beavers::session<u64> session;
session.set_mac_key(key);
auto x = session.input();
auto y = session.input();
auto z = session(x * y);
session.pin(z);
session.sample_from(left, 3);
EXPECT_TRUE(session.lambda_auth(x).verify(key));
EXPECT_TRUE(session.monomial_auth({{x, 1u}, {y, 1u}}).verify(key));
EXPECT_EQ(session.lambda(x), a.a.value);
}
TEST(Beaver, OracleShapesOpen)
{
using block = dpf::prg::aes128::block_type;
const block seed = simde_mm_set_epi64x(0x71, 0x72);
dpf::beavers::oracle<u64> src(seed, 8);
auto p3 = dpf::beavers::sample_beaver3(src, 1);
EXPECT_EQ(p3.abc.open(), p3.a.open() * p3.b.open() * p3.c.open());
auto sq = dpf::beavers::sample_square(src, 2);
EXPECT_EQ(sq.x2.open(), sq.x.open() * sq.x.open());
auto sc = dpf::beavers::sample_scale(2, src, 4);
EXPECT_EQ(sc.cross[1].open(), sc.scalar.open() * sc.lanes[1].open());
auto fresh = dpf::beavers::sample_fresh<3, u64>(src, 5);
EXPECT_EQ(fresh.subset[(1u << 3) - 2].open(),
fresh.in[0].open() * fresh.in[1].open() * fresh.in[2].open());
}
TEST(Beaver, PrgPadReplaysDsGadgets)
{
using block = dpf::prg::aes128::block_type;
const block seed = simde_mm_set_epi64x(0x81, 0x82);
dpf::prg_pad_rng<> left(seed);
dpf::prg_pad_rng<> right(seed);
const auto a = dpf::detail::ds_sample_bit_and(left);
const auto b = dpf::detail::ds_sample_bit_and(right);
EXPECT_EQ(a.a0, b.a0);
EXPECT_EQ(a.c1, b.c1);
const auto abit = static_cast<std::uint8_t>(a.a0 ^ a.a1);
const auto bbit = static_cast<std::uint8_t>(a.b0 ^ a.b1);
const auto cbit = static_cast<std::uint8_t>(a.c0 ^ a.c1);
EXPECT_EQ(cbit, static_cast<std::uint8_t>(abit & bbit));
dpf::prg_pad_rng<> cleft(seed);
dpf::prg_pad_rng<> cright(seed);
const auto cw0 = dpf::detail::ds_sample_cw(cleft);
const auto cw1 = dpf::detail::ds_sample_cw(cright);
EXPECT_EQ(std::memcmp(&cw0.p0.rand, &cw1.p0.rand, sizeof(cw0.p0.rand)), 0);
EXPECT_EQ(std::memcmp(&cw0.p1.gamma, &cw1.p1.gamma, sizeof(cw0.p1.gamma)), 0);
const auto prod = dpf::detail::ds_xor(
(cw0.p1.bit & 1u) ? cw0.p0.rand : simde_mm_setzero_si128(),
(cw0.p0.bit & 1u) ? cw0.p1.rand : simde_mm_setzero_si128());
const auto got = dpf::detail::ds_xor(cw0.p0.gamma, cw0.p1.gamma);
EXPECT_EQ(std::memcmp(&got, &prod, sizeof(got)), 0);
dpf::detail::urandom_pad_rng pad;
const auto shares = dpf::beavers::sample_bit_arith(pad);
EXPECT_EQ(shares.add0 + shares.add1,
static_cast<std::uint64_t>(shares.xor0 ^ shares.xor1));
}

View file

@ -0,0 +1,45 @@
#include <gtest/gtest.h>
#include <vector>
#include "dpf/bench_cells.hpp"
#include "dpf/party_runner.hpp"
namespace
{
void drive(const char * name, dpf::bench::kind id, int parties)
{
dpf::app::run_config cfg;
cfg.kind = dpf::net::transport::async_memory;
cfg.n_lanes = 1;
std::vector<dpf::protocol::plan> plans;
for (int p = 0; p < parties; ++p)
plans.push_back(dpf::bench::plan_for(static_cast<std::size_t>(p), id));
ASSERT_GT(plans[0].rounds(), 0u);
std::vector<dpf::app::party_values> values(plans.size());
auto result = dpf::app::run_parties(plans, values, {}, cfg, nullptr,
&dpf::bench::run_cell);
EXPECT_GT(result.party0_wall_ns, 0u);
ASSERT_FALSE(result.wire.empty());
EXPECT_GT(result.wire[0].payload_out, 0u);
(void)name;
}
} // namespace
TEST(BenchCells, ProjectionCrossesTheMesh)
{
drive("arith_proj_m5", dpf::bench::kind::proj5, 2);
}
TEST(BenchCells, ShuffleCrossesTheRing)
{
drive("shuffle_n16", dpf::bench::kind::shuf16, 3);
}
TEST(BenchCells, FluteAndStackCrossTheMesh)
{
drive("flute_d2", dpf::bench::kind::flute2, 2);
drive("yao_if_4_2", dpf::bench::kind::yao_if4, 2);
}

View file

@ -0,0 +1,393 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <cstring>
#include "dpf.hpp"
#include "simde/simde/x86/avx2.h"
namespace
{
template <unsigned M, unsigned LaneBits = 8>
struct scalar_acc
{
using mod = dpf::bitmore_mod<M, LaneBits>;
unsigned acc = 0;
unsigned bound = 0;
void add(unsigned x, unsigned addend_max)
{
x &= mod::slot_max;
if (addend_max > mod::slot_max)
addend_max = mod::slot_max;
for (;;)
{
if (bound + addend_max <= mod::slot_max)
break;
if (bound > mod::partial_bound)
{
acc = mod::partial_reduce_slot(acc);
bound = mod::partial_bound;
continue;
}
if (addend_max > mod::partial_bound)
{
x = mod::partial_reduce_slot(x);
addend_max = mod::partial_bound;
continue;
}
if (bound > mod::stable_bound)
{
acc = mod::partial_reduce_slot(acc);
bound = mod::stable_bound;
continue;
}
if (addend_max > mod::stable_bound)
{
x = mod::partial_reduce_slot(x);
addend_max = mod::stable_bound;
continue;
}
break;
}
acc += x;
bound += addend_max;
}
void insert(unsigned bit)
{
while (bound > (mod::slot_max >> 1))
{
acc = mod::partial_reduce_slot(acc);
bound = bound > mod::partial_bound ? mod::partial_bound : mod::stable_bound;
}
acc = ((acc << 1) | (bit & 1u)) & mod::slot_max;
bound = bound * 2u + 1u;
}
};
template <unsigned M, typename Reg>
void expect_bytes()
{
using mod = dpf::bitmore_mod<M>;
unsigned span = mod::resume_bound + 1u;
unsigned k = 0;
while ((span << 1) <= 256u)
{
span <<= 1;
++k;
}
EXPECT_EQ(mod::shift_budget, k);
EXPECT_EQ(mod::add_budget, 255u - mod::resume_bound);
EXPECT_LE((mod::resume_bound + 1u) * (1u << k) - 1u, 255u);
EXPECT_GT(span << 1, 256u);
constexpr int n = static_cast<int>(sizeof(Reg));
for (unsigned base = 0; base < 256u; ++base)
{
alignas(32) unsigned char in[32]{};
alignas(32) unsigned char out[32]{};
for (int i = 0; i < n; ++i)
in[i] = static_cast<unsigned char>((base + static_cast<unsigned>(i) * 13u) & 255u);
Reg x;
std::memcpy(&x, in, sizeof(Reg));
auto partial = mod::partial_reduce(x);
std::memcpy(out, &partial, sizeof(Reg));
for (int i = 0; i < n; ++i)
{
const unsigned expect = mod::partial_reduce_byte(in[i]);
if (out[i] != expect || expect > mod::partial_bound || expect % M != in[i] % M)
{
ADD_FAILURE() << "partial M=" << M << " in=" << static_cast<int>(in[i])
<< " got=" << static_cast<int>(out[i]) << " expect=" << expect;
return;
}
}
auto full = mod::full_reduce(x);
std::memcpy(out, &full, sizeof(Reg));
for (int i = 0; i < n; ++i)
{
if (out[i] != in[i] % M)
{
ADD_FAILURE() << "full M=" << M << " in=" << static_cast<int>(in[i])
<< " got=" << static_cast<int>(out[i]);
return;
}
}
}
}
template <unsigned M, typename Reg>
void expect_accumulator()
{
constexpr int n = static_cast<int>(sizeof(Reg));
dpf::bitmore_accumulator<M, Reg> acc;
scalar_acc<M> slots[32]{};
unsigned math[32]{};
for (int step = 0; step < 96; ++step)
{
alignas(32) unsigned char bytes[32]{};
Reg packed;
if (step % 3 == 0)
{
for (int i = 0; i < n; ++i)
{
bytes[i] = static_cast<unsigned char>(((step * 17 + i * 3) & 1u) | (i & 0xf0));
slots[i].insert(bytes[i]);
math[i] = (math[i] * 2u + (bytes[i] & 1u)) % M;
}
std::memcpy(&packed, bytes, sizeof(Reg));
acc.insert_bit(packed);
}
else if (step % 3 == 1)
{
for (int i = 0; i < n; ++i)
{
bytes[i] = static_cast<unsigned char>((step + i) % M);
slots[i].add(bytes[i], M - 1u);
math[i] = (math[i] + bytes[i]) % M;
}
std::memcpy(&packed, bytes, sizeof(Reg));
acc.add(packed, M - 1u);
}
else
{
for (int i = 0; i < n; ++i)
{
bytes[i] = static_cast<unsigned char>(200u + (i & 15u));
slots[i].add(bytes[i], 255u);
math[i] = (math[i] + bytes[i]) % M;
}
std::memcpy(&packed, bytes, sizeof(Reg));
acc.add(packed, 255u);
}
if (acc.bound() != slots[0].bound || acc.bound() > 255u)
{
ADD_FAILURE() << "bound M=" << M << " step=" << step
<< " simd=" << acc.bound() << " scalar=" << slots[0].bound;
return;
}
alignas(32) unsigned char raw[32]{};
const auto value = acc.value();
std::memcpy(raw, &value, sizeof(Reg));
for (int i = 0; i < n; ++i)
{
if (raw[i] != slots[i].acc || raw[i] > acc.bound() || raw[i] % M != math[i])
{
ADD_FAILURE() << "lane M=" << M << " step=" << step << " i=" << i
<< " got=" << static_cast<int>(raw[i])
<< " expect=" << slots[i].acc
<< " math=" << math[i] << " bound=" << acc.bound();
return;
}
}
}
alignas(32) unsigned char red[32]{};
const auto reduced = acc.reduced();
std::memcpy(red, &reduced, sizeof(Reg));
for (int i = 0; i < n; ++i)
{
if (red[i] != math[i])
{
ADD_FAILURE() << "reduced M=" << M << " i=" << i
<< " got=" << static_cast<int>(red[i])
<< " expect=" << math[i];
return;
}
}
}
template <unsigned M>
void expect_all()
{
expect_bytes<M, simde__m128i>();
expect_bytes<M, simde__m256i>();
if constexpr (M <= 128u)
{
expect_accumulator<M, simde__m128i>();
expect_accumulator<M, simde__m256i>();
}
if constexpr (M < 255u)
expect_all<M + 1u>();
}
} // namespace
template <unsigned M, typename Reg>
void expect_epi16_slots()
{
using mod = dpf::bitmore_mod<M, 16>;
constexpr int lanes = static_cast<int>(sizeof(Reg) / 2u);
unsigned span = mod::resume_bound + 1u;
unsigned k = 0;
const unsigned half = (mod::slot_max + 1u) >> 1;
while (span <= half)
{
span *= 2u;
++k;
}
EXPECT_EQ(mod::shift_budget, k);
EXPECT_EQ(mod::add_budget, mod::slot_max - mod::resume_bound);
for (unsigned base = 0; base < 65536u; base += static_cast<unsigned>(lanes))
{
alignas(32) std::uint16_t in[16]{};
alignas(32) std::uint16_t out[16]{};
for (int i = 0; i < lanes; ++i)
in[i] = static_cast<std::uint16_t>(base + static_cast<unsigned>(i));
Reg x;
std::memcpy(&x, in, sizeof(Reg));
auto partial = mod::partial_reduce(x);
std::memcpy(out, &partial, sizeof(Reg));
for (int i = 0; i < lanes; ++i)
{
const unsigned expect = mod::partial_reduce_slot(in[i]);
if (out[i] != expect || expect > mod::partial_bound || expect % M != in[i] % M)
{
ADD_FAILURE() << "partial16 M=" << M << " in=" << in[i]
<< " got=" << out[i] << " expect=" << expect;
return;
}
}
auto full = mod::full_reduce(x);
std::memcpy(out, &full, sizeof(Reg));
for (int i = 0; i < lanes; ++i)
{
if (out[i] != in[i] % M)
{
ADD_FAILURE() << "full16 M=" << M << " in=" << in[i] << " got=" << out[i];
return;
}
}
}
}
template <unsigned M, typename Reg>
void expect_epi16_accumulator()
{
constexpr int lanes = static_cast<int>(sizeof(Reg) / 2u);
dpf::bitmore_accumulator<M, Reg, 16> acc;
scalar_acc<M, 16> slots[16]{};
unsigned math[16]{};
for (int step = 0; step < 64; ++step)
{
alignas(32) std::uint16_t words[16]{};
Reg packed;
if (step % 3 == 0)
{
for (int i = 0; i < lanes; ++i)
{
words[i] = static_cast<std::uint16_t>(((step * 17 + i * 3) & 1) | ((i * 0x1111) & 0xfff0));
slots[i].insert(words[i]);
math[i] = (math[i] * 2u + (words[i] & 1u)) % M;
}
std::memcpy(&packed, words, sizeof(Reg));
acc.insert_bit(packed);
}
else if (step % 3 == 1)
{
for (int i = 0; i < lanes; ++i)
{
words[i] = static_cast<std::uint16_t>((step * 100 + i * 17) % M);
slots[i].add(words[i], M - 1u);
math[i] = (math[i] + words[i]) % M;
}
std::memcpy(&packed, words, sizeof(Reg));
acc.add(packed, M - 1u);
}
else
{
for (int i = 0; i < lanes; ++i)
{
words[i] = static_cast<std::uint16_t>(40000u + static_cast<unsigned>(i) * 97u);
slots[i].add(words[i], 65535u);
math[i] = (math[i] + words[i]) % M;
}
std::memcpy(&packed, words, sizeof(Reg));
acc.add(packed, 65535u);
}
if (acc.bound() != slots[0].bound || acc.bound() > 65535u)
{
ADD_FAILURE() << "bound16 M=" << M << " step=" << step
<< " simd=" << acc.bound() << " scalar=" << slots[0].bound;
return;
}
alignas(32) std::uint16_t raw[16]{};
const auto value = acc.value();
std::memcpy(raw, &value, sizeof(Reg));
for (int i = 0; i < lanes; ++i)
{
if (raw[i] != slots[i].acc || raw[i] > acc.bound() || raw[i] % M != math[i])
{
ADD_FAILURE() << "lane16 M=" << M << " step=" << step << " i=" << i
<< " got=" << raw[i] << " expect=" << slots[i].acc
<< " math=" << math[i] << " bound=" << acc.bound();
return;
}
}
}
alignas(32) std::uint16_t red[16]{};
const auto reduced = acc.reduced();
std::memcpy(red, &reduced, sizeof(Reg));
for (int i = 0; i < lanes; ++i)
{
if (red[i] != math[i])
{
ADD_FAILURE() << "reduced16 M=" << M << " i=" << i
<< " got=" << red[i] << " expect=" << math[i];
return;
}
}
}
template <unsigned M>
void expect_wide()
{
expect_epi16_slots<M, simde__m128i>();
expect_epi16_slots<M, simde__m256i>();
if constexpr (M <= 32768u)
{
expect_epi16_accumulator<M, simde__m128i>();
expect_epi16_accumulator<M, simde__m256i>();
}
}
TEST(BitmoreMod, BytesAndBudget)
{
expect_all<2>();
}
TEST(BitmoreMod, Epi16)
{
expect_wide<2>();
expect_wide<3>();
expect_wide<15>();
expect_wide<127>();
expect_wide<128>();
expect_wide<255>();
expect_wide<256>();
expect_wide<257>();
expect_wide<4095>();
expect_wide<4096>();
expect_wide<4097>();
expect_wide<16384>();
expect_wide<16385>();
expect_wide<32765>();
expect_wide<32767>();
expect_wide<32768>();
expect_wide<32769>();
expect_wide<40000>();
expect_wide<61440>();
expect_wide<65535>();
}

View file

@ -0,0 +1,109 @@
#include <gtest/gtest.h>
#include <array>
#include <cstdint>
#include <cstring>
#include <utility>
#include <vector>
#include "dpf.hpp"
namespace
{
template <std::size_t N>
dpf::blob<N> make_blob_pattern(unsigned char seed)
{
dpf::blob<N> b{};
for (std::size_t i = 0; i < N; ++i)
b.bytes[i] = static_cast<unsigned char>(seed + static_cast<unsigned char>(i));
return b;
}
template <std::size_t N, typename InputT>
void expect_point_opens(InputT alpha, const dpf::blob<N> & beta)
{
auto [k0, k1] = dpf::make_dpf(alpha, beta);
auto open = [&](InputT x) {
return dpf::reconstruct(*dpf::eval_point(k0, x),
*dpf::eval_point(k1, x));
};
const auto s0 = *dpf::eval_point(k0, alpha);
const auto s1 = *dpf::eval_point(k1, alpha);
EXPECT_EQ(dpf::reconstruct(s0, s1), beta);
EXPECT_EQ(open(alpha), beta);
const dpf::blob<N> zero{};
const std::vector<InputT> offs = {
InputT{0},
InputT{1},
static_cast<InputT>(alpha + 1),
static_cast<InputT>(alpha ^ InputT{1}),
};
for (InputT x : offs)
{
if (x == alpha)
continue;
EXPECT_EQ(open(x), zero) << "off-point";
}
}
} // namespace
TEST(BlobLeaf, PointEvalUint32VariousN)
{
const std::uint32_t alpha = 0x00ab12cdu;
expect_point_opens<1>(alpha, make_blob_pattern<1>(0x11));
expect_point_opens<16>(alpha, make_blob_pattern<16>(0x22));
expect_point_opens<100>(alpha, make_blob_pattern<100>(0x33));
expect_point_opens<1000>(alpha, make_blob_pattern<1000>(0x44));
}
TEST(BlobLeaf, PointEvalUint128)
{
using input_t = simde_uint128;
const input_t alpha = (input_t{1} << 100) | input_t{0x55aau};
expect_point_opens<16>(alpha, make_blob_pattern<16>(0x7a));
expect_point_opens<100>(alpha, make_blob_pattern<100>(0x8b));
}
TEST(BlobLeaf, FullDomainMatchesPointShareForShare)
{
using input_t = std::uint8_t;
using blob_t = dpf::blob<100>;
const input_t alpha = 17;
const blob_t beta = make_blob_pattern<100>(0x9c);
auto [k0, k1] = dpf::make_dpf(alpha, beta);
auto full0 = dpf::eval_full(k0);
auto full1 = dpf::eval_full(k1);
auto & it0 = full0.second;
auto & it1 = full1.second;
auto a = std::begin(it0);
auto b = std::begin(it1);
for (std::size_t i = 0; a != std::end(it0) && b != std::end(it1);
++a, ++b, ++i)
{
const auto p0 = *dpf::eval_point(k0, static_cast<input_t>(i));
const auto p1 = *dpf::eval_point(k1, static_cast<input_t>(i));
EXPECT_EQ(dpf::reconstruct(*a, *b), dpf::reconstruct(p0, p1)) << i;
EXPECT_EQ(dpf::detail_walk::group_value(*a),
dpf::detail_walk::group_value(p0)) << "party0 @" << i;
EXPECT_EQ(dpf::detail_walk::group_value(*b),
dpf::detail_walk::group_value(p1)) << "party1 @" << i;
}
EXPECT_EQ(std::size_t(std::distance(std::begin(it0), std::end(it0))), 256u);
}
TEST(BlobLeaf, Domain128FullEvalThrows)
{
using input_t = simde_uint128;
using blob_t = dpf::blob<16>;
const input_t alpha = input_t{1} << 90;
const blob_t beta = make_blob_pattern<16>(1);
auto [k0, k1] = dpf::make_dpf(alpha, beta);
(void)k1;
EXPECT_THROW((void)dpf::eval_full(k0), std::exception);
}

View file

@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <tuple>
#include "dpf.hpp"
#include "dpf/blocked_dcf.hpp"
@ -422,14 +423,18 @@ TEST(BlockedDcf, WideCheckpointFrontierMatchesDense)
TEST(BlockedDcf, PathRecipesStayOnThePerLevelChannel)
{
const uint8_t alpha = 0x3C;
EXPECT_THROW(dpf::make_dpf(alpha, dpf::block_width<4>(dpf::lcp(uint64_t{1}))),
EXPECT_THROW(
dpf::make_dpf(alpha, dpf::block_width<4>(dpf::lcp(uint64_t{1}))),
std::invalid_argument);
EXPECT_THROW(dpf::make_dpf(alpha, dpf::block_width<4>(dpf::break_bit(uint64_t{3}))),
EXPECT_THROW(
dpf::make_dpf(alpha, dpf::block_width<4>(dpf::break_bit(uint64_t{3}))),
std::invalid_argument);
EXPECT_THROW(dpf::make_dpf(alpha,
EXPECT_THROW(
dpf::make_dpf(alpha,
dpf::block_width<4>(dpf::prefix_with_length<4>(uint64_t{1}))),
std::invalid_argument);
EXPECT_THROW(dpf::make_dpf(alpha, dpf::block_width<2>(dpf::path_paint(
EXPECT_THROW(
dpf::make_dpf(alpha, dpf::block_width<2>(dpf::path_paint(
[](std::size_t matched, uint64_t, bool) {
return static_cast<uint64_t>(matched);
}))),
@ -459,32 +464,34 @@ TEST(BlockedDcf, GrottoPrefixSegmentAndHorner)
const uint16_t center = 30;
auto mat = grotto::make_offset_horner_keys<uint16_t, 1>(center);
uint64_t payload[2];
payload[0] = 1;
payload[1] = center;
using bare_pair = decltype(dpf::make_dpf(center, dpf::gt(uint64_t{0})));
using pair = decltype(dpf::make_dpf(center,
dpf::block_width<4>(dpf::gt(uint64_t{0}))));
std::vector<bare_pair> bare_keys{
dpf::make_dpf(center, dpf::gt(payload[0])),
dpf::make_dpf(center, dpf::gt(payload[1]))};
std::vector<pair> keys{
dpf::make_dpf(center, dpf::block_width<4>(dpf::gt(payload[0]))),
dpf::make_dpf(center, dpf::block_width<4>(dpf::gt(payload[1])))};
std::vector<uint16_t> knots{0, 10, 40};
std::vector<std::array<uint64_t, 2>> coeff{
{1, 0},
{2, 3},
{4, 1}};
const uint16_t eta = 0;
const auto b0s = grotto::offset_horner_coefficient_share<0, 1>(
bare_keys, mat.wrap_share, knots, coeff, eta);
const auto b1s = grotto::offset_horner_coefficient_share<1, 1>(
bare_keys, mat.wrap_share, knots, coeff, eta);
const auto c0 = grotto::offset_horner_coefficient_share<0, 1>(
keys, mat.wrap_share, knots, coeff, eta);
const auto c1 = grotto::offset_horner_coefficient_share<1, 1>(
keys, mat.wrap_share, knots, coeff, eta);
for (std::size_t m = 0; m < 2; ++m)
EXPECT_EQ(b0s[m] + b1s[m], c0[m] + c1[m]);
const auto p0 = grotto::offset_horner_eval<0, 1>(mat, knots, coeff, eta);
const auto p1 = grotto::offset_horner_eval<1, 1>(mat, knots, coeff, eta);
EXPECT_EQ(p0 + p1, grotto::offset_horner_clear<1>(center, knots, coeff, eta));
}
TEST(BlockedDcf, VerifiableUint8DomainMatchesThePredicate)
{
const uint8_t alpha = 0x2a;
auto [k0, k1] = dpf::make_dpf(alpha,
dpf::block_width<4>(dpf::lt(uint64_t{1})), dpf::verifiable{});
for (int x = 0; x < 256; ++x)
{
const auto q = static_cast<uint8_t>(x);
dpf::proof_token a{}, b{};
const uint64_t got = recon(dpf::eval_point(dpf::cmp, k0, q, dpf::prove(a)),
dpf::eval_point(dpf::cmp, k1, q, dpf::prove(b)))
& k0.cmp().mask;
EXPECT_EQ(got, q < alpha ? 1u : 0u) << x;
EXPECT_TRUE(dpf::verify(a, b)) << x;
const uint64_t swapped = recon(dpf::eval_point(dpf::cmp, k1, q),
dpf::eval_point(dpf::cmp, k0, q))
& k0.cmp().mask;
EXPECT_EQ(swapped, got) << x;
}
}

View file

@ -0,0 +1,128 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <vector>
#include "dpf.hpp"
namespace
{
using input_t = std::uint8_t; // 256-point domain
constexpr std::size_t kDomain = 256;
} // namespace
// eval_full_add_into(buf, key, fold): the fold sees every written share once,
// in the same pass that adds it into the buffer (Express / Prio audit hook).
TEST(CallerFold, FullAddIntoFoldSeesEveryShare)
{
const input_t alpha = 42;
const std::uint64_t beta = 0xdeadbeefull;
auto [k0, k1] = dpf::make_dpf(alpha, beta);
std::vector<std::uint64_t> buf0(kDomain, 0), buf1(kDomain, 0);
std::uint64_t fold0 = 0, fold1 = 0;
std::size_t calls0 = 0, calls1 = 0;
dpf::eval_full_add_into(buf0, k0,
[&](std::size_t, std::uint64_t g) { fold0 += g; ++calls0; });
dpf::eval_full_add_into(buf1, k1,
[&](std::size_t, std::uint64_t g) { fold1 += g; ++calls1; });
EXPECT_EQ(calls0, kDomain);
EXPECT_EQ(calls1, kDomain);
// The buffer opens to a point function at alpha.
for (std::size_t i = 0; i < kDomain; ++i)
{
const std::uint64_t got = buf0[i] - buf1[i];
EXPECT_EQ(got, i == alpha ? beta : 0ull) << "i=" << i;
}
// The fold accumulated exactly the same shares: sum reconstructs to beta.
EXPECT_EQ(static_cast<std::uint64_t>(fold0 - fold1), beta);
}
// eval_full_fold allocates its own buffer and folds each share.
TEST(CallerFold, FullFoldReconstructsAudit)
{
const input_t alpha = 200;
const std::uint64_t beta = 7;
auto [k0, k1] = dpf::make_dpf(alpha, beta);
// Weighted fold: sum_i (i+1) * share_i. Reconstructs to (alpha+1)*beta.
std::uint64_t w0 = 0, w1 = 0;
(void)dpf::eval_full_fold(k0,
[&](std::size_t i, std::uint64_t g) { w0 += (i + 1) * g; });
(void)dpf::eval_full_fold(k1,
[&](std::size_t i, std::uint64_t g) { w1 += (i + 1) * g; });
EXPECT_EQ(static_cast<std::uint64_t>(w0 - w1),
static_cast<std::uint64_t>((alpha + 1) * beta));
}
// eval_point_fold calls the fold exactly once with the written share.
TEST(CallerFold, PointFoldCalledOnce)
{
const input_t alpha = 5;
const std::uint64_t beta = 99;
auto [k0, k1] = dpf::make_dpf(alpha, beta);
std::uint64_t seen0 = 0, seen1 = 0;
int calls0 = 0, calls1 = 0;
auto o0 = dpf::eval_point_fold(k0, alpha,
[&](std::size_t, std::uint64_t g) { seen0 = g; ++calls0; });
auto o1 = dpf::eval_point_fold(k1, alpha,
[&](std::size_t, std::uint64_t g) { seen1 = g; ++calls1; });
EXPECT_EQ(calls0, 1);
EXPECT_EQ(calls1, 1);
// What the fold saw equals what eval_point returned.
EXPECT_EQ(seen0, static_cast<std::uint64_t>((*o0).raw()));
EXPECT_EQ(seen1, static_cast<std::uint64_t>((*o1).raw()));
EXPECT_EQ(static_cast<std::uint64_t>(seen0 - seen1), beta);
}
// The fold is generic over the leaf group: a blob<N> row folds by XOR.
TEST(CallerFold, FoldOverBlobLeaf)
{
using blob_t = dpf::blob<24>;
const input_t alpha = 17;
blob_t beta{};
for (std::size_t i = 0; i < blob_t::size; ++i)
beta.bytes[i] = static_cast<unsigned char>(0x30 + i);
auto [k0, k1] = dpf::make_dpf(alpha, beta);
std::vector<blob_t> buf0(kDomain), buf1(kDomain);
blob_t x0{}, x1{};
std::size_t calls = 0;
dpf::eval_full_add_into(buf0, k0,
[&](std::size_t, const blob_t & g) { x0 = x0 ^ g; ++calls; });
dpf::eval_full_add_into(buf1, k1,
[&](std::size_t, const blob_t & g) { x1 = x1 ^ g; });
EXPECT_EQ(calls, kDomain);
// XOR of all shares reconstructs to beta (only alpha is nonzero).
blob_t recon{};
for (std::size_t i = 0; i < blob_t::size; ++i)
recon.bytes[i] = static_cast<unsigned char>(x0.bytes[i] ^ x1.bytes[i]);
EXPECT_EQ(recon, beta);
}
// eval_sequence_xor: keyword PIR without materializing the bit vector.
TEST(CallerFold, SequenceXorMatchesRecordAtAlpha)
{
const input_t alpha = 123;
// Point function with a bit payload (1 at alpha).
auto [k0, k1] = dpf::make_dpf(alpha, dpf::bit::one);
std::vector<std::uint64_t> records(kDomain);
for (std::size_t i = 0; i < kDomain; ++i)
records[i] = 0x1000ull * (i + 1) + 7;
const std::uint64_t acc0 = dpf::eval_sequence_xor(k0, records);
const std::uint64_t acc1 = dpf::eval_sequence_xor(k1, records);
EXPECT_EQ(acc0 ^ acc1, records[alpha]);
}

597
test/tests/carry_test.cpp Normal file
View file

@ -0,0 +1,597 @@
#include <gtest/gtest.h>
#include <tuple>
#include <array>
#include <cstdint>
#include <vector>
#include "dpf.hpp"
#include "grotto/carry_plan.hpp"
#include "grotto/carry.hpp"
#include "grotto/prefix_parity.hpp"
using grotto::carry_mode;
using grotto::sign_knowledge;
TEST(CarryPlan, TruncateReduceOnlyLowLt)
{
const auto r = grotto::plan_carry_in(8, 3);
EXPECT_TRUE(r.use_low_lt);
EXPECT_FALSE(r.use_msb_lt);
EXPECT_FALSE(r.use_share_msb_and);
EXPECT_FALSE(r.use_biased_wrap);
EXPECT_EQ(r.out_n, 5u);
}
TEST(CarryPlan, SameRingKnownSignIsAnd)
{
const auto pos = grotto::plan_carry_out(8, 2, sign_knowledge::nonnegative);
EXPECT_TRUE(pos.use_share_msb_and);
EXPECT_FALSE(pos.and_is_nor);
EXPECT_FALSE(pos.use_msb_lt);
EXPECT_EQ(pos.and_unit, std::int64_t{1} << 6);
const auto neg = grotto::plan_carry_out(8, 2, sign_knowledge::negative);
EXPECT_TRUE(neg.use_share_msb_and);
EXPECT_TRUE(neg.and_is_nor);
EXPECT_EQ(neg.and_unit, -(std::int64_t{1} << 6));
}
TEST(CarryPlan, SameRingSecretSignNeedsBoth)
{
const auto r = grotto::plan_carry_out(8, 2, sign_knowledge::unknown);
EXPECT_TRUE(r.use_msb_lt);
EXPECT_TRUE(r.use_share_msb_and);
}
TEST(CarryPlan, FusedReducedRingDropsSign)
{
const auto r = grotto::plan_carry_fused(8, 3, 5, sign_knowledge::unknown);
EXPECT_TRUE(r.use_low_lt);
EXPECT_FALSE(r.use_msb_lt);
EXPECT_FALSE(r.use_share_msb_and);
}
TEST(CarryPlan, ExtendIsBiasedWrap)
{
const auto r = grotto::plan_carry(grotto::carry_request{
8, 0, 16, carry_mode::extend, sign_knowledge::nonnegative});
EXPECT_TRUE(r.use_biased_wrap);
EXPECT_FALSE(r.use_share_msb_and);
EXPECT_EQ(r.wrap_payload, std::int64_t{1} << 8);
}
TEST(CarryPlan, WindowPublicCarryZeroDropsEq)
{
grotto::carry_request req{};
req.n = 16;
req.s = 4;
req.out_n = 4;
req.mode = carry_mode::window;
req.incoming_carry_public = true;
req.incoming_carry_value = 0;
const auto r = grotto::plan_carry(req);
EXPECT_TRUE(r.use_window_overflow);
EXPECT_FALSE(r.use_window_eq);
EXPECT_FALSE(r.use_window_product);
}
TEST(CarryPlan, WindowSecretCarryNeedsProduct)
{
grotto::carry_request req{};
req.n = 16;
req.s = 4;
req.out_n = 4;
req.mode = carry_mode::window;
req.incoming_carry_public = false;
const auto r = grotto::plan_carry(req);
EXPECT_TRUE(r.use_window_overflow);
EXPECT_TRUE(r.use_window_eq);
EXPECT_TRUE(r.use_window_product);
}
TEST(CarryClear, TruncateReduceMatchesHighPlusCarry)
{
for (std::uint64_t x0 = 0; x0 < 64; ++x0)
{
for (std::uint64_t x1 = 0; x1 < 64; ++x1)
{
const auto got = grotto::carry_in_clear(x0, x1, 6, 2);
const auto x = (x0 + x1) & 63u;
EXPECT_EQ(got, x >> 2);
}
}
}
TEST(CarryClear, CarryOutKnownLeavesOnlySmallResidual)
{
constexpr unsigned n = 6;
constexpr unsigned s = 2;
std::size_t big = 0;
for (std::uint64_t x0 = 0; x0 < (1u << n); ++x0)
{
for (std::uint64_t x1 = 0; x1 < (1u << n); ++x1)
{
const auto x = (x0 + x1) & grotto::carry_mask(n);
const auto true_asr = grotto::carry_asr(x, n, s);
const auto fixed = grotto::carry_out_clear(x0, x1, n, s,
sign_knowledge::nonnegative);
// Known nonnegative assumes msb(x)==0; skip negatives.
if ((x >> (n - 1)) & 1u)
continue;
std::int64_t d = static_cast<std::int64_t>(true_asr)
- static_cast<std::int64_t>(fixed);
if (d > 32)
d -= 64;
if (d < -32)
d += 64;
if (std::abs(d) > 1)
++big;
EXPECT_LE(std::abs(d), 1);
}
}
EXPECT_EQ(big, 0u);
}
TEST(CarryClear, AndDisagreesWithExtensionWrap)
{
// 4-bit shares: share-MSB AND is not the extension wrap bit.
std::size_t mismatch = 0;
for (std::uint64_t x0 = 0; x0 < 16; ++x0)
{
for (std::uint64_t x1 = 0; x1 < 16; ++x1)
{
const auto wrap = ((x0 + x1) >= 16u) ? 1u : 0u;
const auto s0 = (x0 >> 3) & 1u;
const auto s1 = (x1 >> 3) & 1u;
const auto x = (x0 + x1) & 15u;
const auto m = (x >> 3) & 1u;
const auto grotto = (m == 0) ? (s0 & s1) : ((1u - s0) & (1u - s1));
if (grotto != wrap)
++mismatch;
}
}
EXPECT_EQ(mismatch, 112u);
}
TEST(CarryClear, ExtendPreservesSignedValue)
{
EXPECT_EQ(grotto::carry_extend_clear(0x05, 0x00, 8, 16), 0x0005u);
EXPECT_EQ(grotto::carry_extend_clear(0x80, 0x00, 8, 16), 0xff80u);
// 0x7f + 0x01 = 0x80 in 8 bits, which is negative and sign-extends.
EXPECT_EQ(grotto::carry_extend_clear(0x7f, 0x01, 8, 16), 0xff80u);
EXPECT_EQ(grotto::carry_extend_clear(0x40, 0x01, 8, 16), 0x0041u);
}
TEST(CarryClear, WindowOutgoingCarry)
{
const auto w = grotto::carry_window_clear(0x0f, 0x01, 4, 0);
EXPECT_EQ(w.digit, 0u);
EXPECT_EQ(w.carry_out, 1u);
const auto w2 = grotto::carry_window_clear(0x0e, 0x00, 4, 1);
EXPECT_EQ(w2.digit, 0x0fu);
EXPECT_EQ(w2.carry_out, 0u);
const auto w3 = grotto::carry_window_clear(0x0f, 0x00, 4, 1);
EXPECT_EQ(w3.digit, 0u);
EXPECT_EQ(w3.carry_out, 1u);
}
TEST(CarryClear, FusedMatchesAsr)
{
for (std::uint64_t x0 = 0; x0 < 64; ++x0)
for (std::uint64_t x1 = 0; x1 < 64; ++x1)
EXPECT_EQ(grotto::carry_fused_clear(x0, x1, 6, 2),
grotto::carry_asr((x0 + x1) & 63u, 6, 2));
}
TEST(CarryKeys, TruncateReduceOnline)
{
auto keys = grotto::make_carry_in_keys(8, 3);
for (int trial = 0; trial < 32; ++trial)
{
const std::uint64_t x0 = dpf::uniform_sample<std::uint8_t>();
const std::uint64_t x1 = dpf::uniform_sample<std::uint8_t>();
const std::uint64_t x = (x0 + x1) & 0xffu;
const std::uint64_t opened = (x + keys.rin) & 0xffu;
dpf::proof_token pi0{}, pi1{};
const auto y0 = grotto::eval_carry_in(keys, 0, opened);
const auto y1 = grotto::eval_carry_in(keys, 1, opened);
const auto got = (y0.value + y1.value) & 0x1fu;
EXPECT_EQ(got, grotto::carry_in_clear(x0, x1, 8, 3));
(void)pi0;
(void)pi1;
}
}
TEST(CarryKeys, TruncateReduceVerifiableProof)
{
grotto::carry_auth auth{};
auth.verifiable = true;
auto keys = grotto::make_carry_in_keys(8, 3, auth);
const std::uint64_t x0 = 0x11;
const std::uint64_t x1 = 0x22;
const std::uint64_t opened = ((x0 + x1) + keys.rin) & 0xffu;
dpf::proof_token pi0{}, pi1{};
const auto y0 = grotto::eval_carry_in(keys, 0, opened, &pi0);
const auto y1 = grotto::eval_carry_in(keys, 1, opened, &pi1);
EXPECT_EQ((y0.value + y1.value) & 0x1fu, grotto::carry_in_clear(x0, x1, 8, 3));
EXPECT_TRUE(dpf::verify(pi0, pi1));
// Flip every correction seed so some on-path level mixes the tamper.
for (auto & cs : const_cast<typename std::decay_t<decltype(keys.low_lt_v->first)>::correction_seeds_array &>(
keys.low_lt_v->first.correction_seeds()))
{
cs[0] = simde_mm_xor_si128(cs[0], simde_mm_set1_epi8(1));
}
dpf::proof_token q0{}, q1{};
auto ignore0 = grotto::eval_carry_in(keys, 0, opened, &q0);
auto ignore1 = grotto::eval_carry_in(keys, 1, opened, &q1);
(void)ignore0;
(void)ignore1;
EXPECT_FALSE(dpf::verify(q0, q1));
}
TEST(CarryKeys, ProveCarryKeysBatch)
{
grotto::carry_auth auth{};
auth.verifiable = true;
auto keys = grotto::make_carry_in_keys(8, 3, auth);
const std::uint64_t opened = 0x3cu;
dpf::proof_token t0[4]{}, t1[4]{};
const auto n0 = grotto::prove_carry_keys(keys, 0, opened, t0, 4);
const auto n1 = grotto::prove_carry_keys(keys, 1, opened, t1, 4);
EXPECT_EQ(n0, n1);
EXPECT_GE(n0, 1u);
EXPECT_TRUE(dpf::verify_batch(
std::vector<dpf::proof_token>(t0, t0 + n0),
std::vector<dpf::proof_token>(t1, t1 + n1)));
}
TEST(CarryKeys, OutputMacDetectsFlip)
{
grotto::carry_auth auth{};
auth.verifiable = true;
auth.output_mac = true;
auto keys = grotto::make_carry_in_keys(8, 3, auth);
ASSERT_TRUE(keys.has_mac);
const std::uint64_t opened = 0x3cu;
dpf::proof_token t0[4]{}, t1[4]{};
const auto n0 = grotto::prove_carry_keys(keys, 0, opened, t0, 4);
const auto n1 = grotto::prove_carry_keys(keys, 1, opened, t1, 4);
ASSERT_EQ(n0, n1);
ASSERT_GE(n0, 1u);
const std::uint64_t y0 = 3;
const std::uint64_t y1 = 4;
auto [m0, m1] = grotto::mac_carry_result(keys, y0, y1);
EXPECT_TRUE(dpf::mac_verify(m0, m1, keys.mac, t0[0], t1[0]));
EXPECT_FALSE(dpf::mac_verify(m0, m1, keys.mac, dpf::detail::vdpf::zero_proof(),
t1[0]));
m0.value ^= 1u;
EXPECT_FALSE(dpf::mac_verify(m0, m1, keys.mac, t0[0], t1[0]));
}
TEST(CarryCmp, VerifiableComparisonPathProof)
{
using Input = std::uint8_t;
auto [k0, k1] = dpf::make_dpf(Input{10}, dpf::lt(std::uint64_t{1}),
dpf::verifiable{});
dpf::proof_token pi0{}, pi1{};
const auto y0 = dpf::eval_point(dpf::cmp, k0, Input{3}, dpf::prove(pi0));
const auto y1 = dpf::eval_point(dpf::cmp, k1, Input{3}, dpf::prove(pi1));
EXPECT_EQ(dpf::reconstruct(y0, y1) & 1u, 1u);
EXPECT_TRUE(dpf::verify(pi0, pi1));
for (auto & cs : const_cast<typename std::decay_t<decltype(k0)>::correction_seeds_array &>(
k0.correction_seeds()))
{
cs[0] = simde_mm_xor_si128(cs[0], simde_mm_set1_epi8(0x5a));
}
dpf::proof_token q0{}, q1{};
dpf::eval_point(dpf::cmp, k0, Input{3}, dpf::prove(q0));
dpf::eval_point(dpf::cmp, k1, Input{3}, dpf::prove(q1));
EXPECT_FALSE(dpf::verify(q0, q1));
}
TEST(OutputMac, BatchVerify)
{
auto key = dpf::sample_mac_key<std::uint64_t>();
std::vector<dpf::mac_share<std::uint64_t>> left, right;
std::vector<std::uint64_t> coeffs;
for (std::uint64_t i = 0; i < 4; ++i)
{
auto [a, b] = dpf::mac_share_value(i * 3u + 1u, key);
left.push_back(a);
right.push_back(b);
coeffs.push_back(i + 1u);
}
EXPECT_TRUE(dpf::mac_verify_batch(left, right, coeffs, key));
left[1].tag ^= 1u;
EXPECT_FALSE(dpf::mac_verify_batch(left, right, coeffs, key));
}
TEST(CarryKeys, KnownSignCarryOutOnlineMatchesClear)
{
constexpr unsigned n = 8;
constexpr unsigned s = 3;
auto keys = grotto::make_carry_out_keys(n, s, sign_knowledge::nonnegative);
ASSERT_TRUE(keys.has_and_beaver);
ASSERT_TRUE(keys.recipe.use_share_msb_and);
std::size_t checked = 0;
for (std::uint64_t x0 = 0; x0 < 256; x0 += 17)
{
for (std::uint64_t x1 = 0; x1 < 256; x1 += 19)
{
const std::uint64_t x = (x0 + x1) & 0xffu;
if ((x >> (n - 1)) & 1u)
continue; // nonnegative assumption
const std::uint64_t s0 = (x0 >> (n - 1)) & 1u;
const std::uint64_t s1 = (x1 >> (n - 1)) & 1u;
const auto y0 = grotto::eval_carry_out_known(keys, 0, x0, s0, s1);
const auto y1 = grotto::eval_carry_out_known(keys, 1, x1, s1, s0);
const auto got = (y0.value + y1.value) & 0xffu;
const auto expect = grotto::carry_out_clear(x0, x1, n, s,
sign_knowledge::nonnegative);
EXPECT_EQ(got, expect) << "x0=" << x0 << " x1=" << x1;
++checked;
}
}
EXPECT_GT(checked, 50u);
}
TEST(CarryKeys, NegativeSignCarryOutOnlineMatchesClear)
{
constexpr unsigned n = 8;
constexpr unsigned s = 3;
auto keys = grotto::make_carry_out_keys(n, s, sign_knowledge::negative);
ASSERT_TRUE(keys.recipe.and_is_nor);
std::size_t checked = 0;
for (std::uint64_t x0 = 0; x0 < 256; x0 += 17)
{
for (std::uint64_t x1 = 0; x1 < 256; x1 += 19)
{
const std::uint64_t x = (x0 + x1) & 0xffu;
if (((x >> (n - 1)) & 1u) == 0u)
continue; // negative assumption
const std::uint64_t s0 = (x0 >> (n - 1)) & 1u;
const std::uint64_t s1 = (x1 >> (n - 1)) & 1u;
const auto y0 = grotto::eval_carry_out_known(keys, 0, x0, s0, s1);
const auto y1 = grotto::eval_carry_out_known(keys, 1, x1, s1, s0);
const auto got = (y0.value + y1.value) & 0xffu;
const auto expect = grotto::carry_out_clear(x0, x1, n, s,
sign_knowledge::negative);
EXPECT_EQ(got, expect) << "x0=" << x0 << " x1=" << x1;
++checked;
}
}
EXPECT_GT(checked, 50u);
}
TEST(CarryKeys, WindowSecretBuildsEqAndOverflow)
{
grotto::carry_request req{};
req.n = 16;
req.s = 4;
req.out_n = 4;
req.mode = carry_mode::window;
req.incoming_carry_public = false;
auto keys = grotto::make_carry_keys(req);
EXPECT_TRUE(keys.window_overflow.has_value() || keys.window_overflow_v.has_value());
EXPECT_TRUE(keys.window_eq.has_value() || keys.window_eq_v.has_value());
EXPECT_TRUE(keys.has_and_beaver);
}
TEST(CarryKeys, WindowPublicZeroOmitsEq)
{
grotto::carry_request req{};
req.n = 16;
req.s = 4;
req.out_n = 4;
req.mode = carry_mode::window;
req.incoming_carry_public = true;
req.incoming_carry_value = 0;
auto keys = grotto::make_carry_keys(req);
EXPECT_TRUE(keys.window_overflow.has_value() || keys.window_overflow_v.has_value());
EXPECT_FALSE(keys.window_eq.has_value());
EXPECT_FALSE(keys.window_eq_v.has_value());
EXPECT_FALSE(keys.has_and_beaver);
}
TEST(CarryKeys, FusedSameRingPlansBothThenOnlineLowOnlyWhenReduced)
{
auto reduced = grotto::make_carry_fused_keys(8, 3, 5);
EXPECT_TRUE(reduced.recipe.use_low_lt);
EXPECT_FALSE(reduced.recipe.use_share_msb_and);
EXPECT_TRUE(reduced.low_lt.has_value() || reduced.low_lt_v.has_value());
auto same = grotto::make_carry_fused_keys(8, 3, 8, sign_knowledge::nonnegative);
EXPECT_TRUE(same.recipe.use_low_lt);
EXPECT_TRUE(same.recipe.use_share_msb_and);
EXPECT_TRUE(same.has_and_beaver);
}
TEST(PrefixParity, VerifiableTokensMatchAndTamperRejects)
{
using Input = std::uint8_t;
auto [k0, k1] = dpf::make_dpf(Input{0x2a}, dpf::bit{1}, dpf::verifiable{});
// Exclusive of alpha; include alpha itself (must reconstruct to 0).
const std::array<Input, 3> ends{Input{0x10}, Input{0x2a}, Input{0x40}};
dpf::proof_token pi0{}, pi1{};
auto [p0, n0] = grotto::prefix_parities(k0, ends, dpf::prove(pi0));
auto [p1, n1] = grotto::prefix_parities(k1, ends, dpf::prove(pi1));
(void)n0;
(void)n1;
for (std::size_t i = 0; i < ends.size(); ++i)
EXPECT_EQ(p0[i] ^ p1[i], ends[i] > Input{0x2a});
EXPECT_TRUE(dpf::verify(pi0, pi1));
pi0[0] = simde_mm_xor_si128(pi0[0], simde_mm_set1_epi8(0x3c));
EXPECT_FALSE(dpf::verify(pi0, pi1));
}
TEST(CarryClear, EvalCarryClearDispatchMatchesHelpers)
{
const auto tr = grotto::plan_carry_in(8, 3);
EXPECT_EQ(grotto::eval_carry_clear(tr, 0xab, 0x11),
grotto::carry_in_clear(0xab, 0x11, 8, 3));
const auto ext = grotto::plan_carry(grotto::carry_request{
8, 0, 16, carry_mode::extend});
EXPECT_EQ(grotto::eval_carry_clear(ext, 0x80, 0),
grotto::carry_extend_clear(0x80, 0, 8, 16));
const auto fuse = grotto::plan_carry_fused(8, 2, 8);
EXPECT_EQ(grotto::eval_carry_clear(fuse, 0x11, 0x22),
grotto::carry_fused_clear(0x11, 0x22, 8, 2));
}
TEST(CarryKeys, BeaverAndOfPrivateBitsIsOneWhenBothSet)
{
// Direct check of the AND helper via known-sign path with unit=1, s=0
// is awkward; instead use n=2,s=1 so unit=2 and check the product bit.
auto keys = grotto::make_carry_out_keys(4, 1, sign_knowledge::nonnegative);
// Both MSBs set: x0=0b1xxx, x1=0b1xxx with nonnegative sum.
// 0b1000 + 0b1000 = 0b0000 mod 16, msb of sum is 0 — OK for nonnegative.
const std::uint64_t x0 = 0b1000;
const std::uint64_t x1 = 0b1000;
const auto y0 = grotto::eval_carry_out_known(keys, 0, x0, 1, 1);
const auto y1 = grotto::eval_carry_out_known(keys, 1, x1, 1, 1);
const auto got = (y0.value + y1.value) & 0xfu;
const auto expect = grotto::carry_out_clear(x0, x1, 4, 1,
sign_knowledge::nonnegative);
EXPECT_EQ(got, expect);
// AND must be 1, so correction is +2^{3}=+8 on top of local ASRs.
const auto base = (grotto::carry_asr(x0, 4, 1) + grotto::carry_asr(x1, 4, 1)) & 0xfu;
EXPECT_EQ(got, (base + 8u) & 0xfu);
}
TEST(CarryKeys, UnknownSignMatchesClear)
{
constexpr unsigned n = 8;
constexpr unsigned s = 3;
auto keys = grotto::make_carry_out_keys(n, s, sign_knowledge::unknown);
ASSERT_TRUE(keys.recipe.use_msb_lt);
ASSERT_TRUE(keys.recipe.use_share_msb_and);
std::size_t checked = 0;
for (std::uint64_t x0 = 0; x0 < 256; x0 += 17)
{
for (std::uint64_t x1 = 0; x1 < 256; x1 += 19)
{
const std::uint64_t x = (x0 + x1) & 0xffu;
const std::uint64_t msb_high = (x >> (n - 1)) & 1u;
const std::uint64_t s0 = (x0 >> (n - 1)) & 1u;
const std::uint64_t s1 = (x1 >> (n - 1)) & 1u;
const auto y0 = grotto::eval_carry_out_unknown(keys, 0, x0, s0, s1, msb_high);
const auto y1 = grotto::eval_carry_out_unknown(keys, 1, x1, s1, s0, msb_high);
const auto got = (y0.value + y1.value) & 0xffu;
const auto expect = grotto::carry_out_clear(x0, x1, n, s,
sign_knowledge::unknown);
EXPECT_EQ(got, expect) << "x0=" << x0 << " x1=" << x1;
++checked;
}
}
EXPECT_GT(checked, 50u);
}
TEST(CarryKeys, ExtendMatchesClear)
{
auto keys = grotto::make_carry_keys(grotto::carry_request{
8, 0, 16, carry_mode::extend});
ASSERT_TRUE(keys.recipe.use_biased_wrap);
for (std::uint64_t x0 = 0; x0 < 256; x0 += 13)
{
for (std::uint64_t x1 = 0; x1 < 256; x1 += 17)
{
const std::uint64_t x = (x0 + x1) & 0xffu;
const std::uint64_t msb_high = (x >> 7) & 1u;
const std::uint64_t opened = (x + keys.rin) & 0xffu;
const auto y0 = grotto::eval_carry_extend(keys, 0, opened, msb_high);
const auto y1 = grotto::eval_carry_extend(keys, 1, opened, msb_high);
const auto got = (y0.value + y1.value) & 0xffffu;
EXPECT_EQ(got, grotto::carry_extend_clear(x0, x1, 8, 16))
<< "x0=" << x0 << " x1=" << x1;
}
}
}
TEST(CarryKeys, WindowPublicMatchesClear)
{
grotto::carry_request req{};
req.n = 16;
req.s = 4;
req.out_n = 4;
req.mode = carry_mode::window;
req.incoming_carry_public = true;
req.incoming_carry_value = 1;
auto keys = grotto::make_carry_keys(req);
for (std::uint64_t p0 = 0; p0 < 16; ++p0)
{
for (std::uint64_t p1 = 0; p1 < 16; ++p1)
{
const std::uint64_t sum = p0 + p1 + 1u;
const auto y0 = grotto::eval_carry_window(keys, 0, sum);
const auto y1 = grotto::eval_carry_window(keys, 1, sum);
const auto got = y0.value + y1.value;
const auto w = grotto::carry_window_clear(p0, p1, 4, 1);
EXPECT_EQ(got, (w.carry_out << 4) | w.digit);
}
}
}
TEST(CarryKeys, FusedReducedMatchesTruncate)
{
auto keys = grotto::make_carry_fused_keys(8, 3, 5);
ASSERT_TRUE(keys.recipe.use_low_lt);
ASSERT_FALSE(keys.recipe.use_share_msb_and);
for (int trial = 0; trial < 32; ++trial)
{
const std::uint64_t x0 = dpf::uniform_sample<std::uint8_t>();
const std::uint64_t x1 = dpf::uniform_sample<std::uint8_t>();
const std::uint64_t opened = ((x0 + x1) + keys.rin) & 0xffu;
const auto y0 = grotto::eval_carry_fused(keys, 0, opened, x0);
const auto y1 = grotto::eval_carry_fused(keys, 1, opened, x1);
const auto got = (y0.value + y1.value) & 0x1fu;
EXPECT_EQ(got, grotto::carry_in_clear(x0, x1, 8, 3));
}
}
TEST(CarryKeys, FusedSameRingKnownSignMatchesClear)
{
constexpr unsigned n = 8;
constexpr unsigned s = 2;
auto keys = grotto::make_carry_fused_keys(n, s, n, sign_knowledge::nonnegative);
ASSERT_TRUE(keys.recipe.use_share_msb_and);
std::size_t checked = 0;
for (std::uint64_t x0 = 0; x0 < 256; x0 += 21)
{
for (std::uint64_t x1 = 0; x1 < 256; x1 += 23)
{
const std::uint64_t x = (x0 + x1) & 0xffu;
if ((x >> (n - 1)) & 1u)
continue;
const std::uint64_t s0 = (x0 >> (n - 1)) & 1u;
const std::uint64_t s1 = (x1 >> (n - 1)) & 1u;
const std::uint64_t opened = (x + keys.rin) & 0xffu;
const auto y0 = grotto::eval_carry_fused(keys, 0, opened, x0, s0, s1);
const auto y1 = grotto::eval_carry_fused(keys, 1, opened, x1, s1, s0);
const auto got = (y0.value + y1.value) & 0xffu;
EXPECT_EQ(got, grotto::carry_out_clear(x0, x1, n, s,
sign_knowledge::nonnegative))
<< "x0=" << x0 << " x1=" << x1;
++checked;
}
}
EXPECT_GT(checked, 20u);
}
TEST(CarryKeys, RecipeMakeCarryKeysFinalizesBlinds)
{
const auto recipe = grotto::plan_carry_in(8, 3);
auto keys = grotto::make_carry_keys(recipe);
const std::uint64_t high = grotto::carry_mask(5);
const std::uint64_t y_hi = ((std::uint64_t{0} - keys.rin) >> 3) & high;
EXPECT_EQ((keys.rout0 + keys.rout1) & high, y_hi);
}

View file

@ -0,0 +1,535 @@
#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);
}
}

View file

@ -0,0 +1,151 @@
#include <gtest/gtest.h>
#include <tuple>
#include "grotto/closed_form.hpp"
#include <cmath>
#include <cstdint>
namespace
{
std::int64_t raw_of(long double x, unsigned k)
{
return std::llround(std::ldexp(x, static_cast<int>(k)));
}
long double truth(grotto::closed which, long double x)
{
switch (which)
{
case grotto::closed::atanh: return std::atanh(x);
case grotto::closed::asinh: return std::asinh(x);
case grotto::closed::acosh: return std::acosh(x);
case grotto::closed::atan: return std::atan(x);
case grotto::closed::acot: return 1.57079632679489661923L - std::atan(x);
case grotto::closed::asec: return std::acos(1.0L / x);
case grotto::closed::acsc: return std::asin(1.0L / x);
case grotto::closed::asech: return std::acosh(1.0L / x);
case grotto::closed::acsch: return std::asinh(1.0L / x);
case grotto::closed::acoth: return std::atanh(1.0L / x);
case grotto::closed::selu:
return 1.0507009873554804934L * (x > 0 ? x : 1.6732632423543772848L * std::expm1(x));
case grotto::closed::elu:
case grotto::closed::celu:
return x > 0 ? x : std::expm1(x);
case grotto::closed::softsign: return x / (1.0L + std::fabsl(x));
case grotto::closed::tanhshrink: return x - std::tanh(x);
case grotto::closed::logistic: return std::log(x / (1.0L - x));
case grotto::closed::exponential: return -std::log(1.0L - x);
case grotto::closed::laplace:
return x <= 0.5L ? std::log(2.0L * x) : -std::log(2.0L * (1.0L - x));
case grotto::closed::cauchy: return std::tan(3.14159265358979323846L * (x - 0.5L));
case grotto::closed::sinc: return x == 0 ? 1.0L : std::sin(x) / x;
case grotto::closed::cbrt: return std::cbrt(x);
case grotto::closed::qtrt: return std::sqrt(std::sqrt(x));
case grotto::closed::icbrt: return 1.0L / std::cbrt(x);
case grotto::closed::iqtrt: return 1.0L / std::sqrt(std::sqrt(x));
case grotto::closed::pow_m01: return std::exp(-0.1L * std::log(x));
case grotto::closed::pow_p15: return x * std::sqrt(x);
case grotto::closed::pow_m3: return 1.0L / (x * x * x);
}
return 0;
}
void expect_ulps(grotto::closed which, unsigned k, long double x, long double ulps)
{
const std::int64_t raw = raw_of(x, k);
std::int64_t got = 0;
ASSERT_NO_THROW(got = grotto::eval_closed(which, k, raw)) << static_cast<int>(which) << " x=" << static_cast<double>(x);
const long double xr = std::ldexp(static_cast<long double>(raw), -static_cast<int>(k));
const long double want = truth(which, xr) * std::ldexp(1.0L, static_cast<int>(k));
EXPECT_LE(std::fabsl(static_cast<long double>(got) - want), ulps)
<< static_cast<int>(which) << " k=" << k << " x=" << static_cast<double>(x)
<< " got=" << got << " want=" << static_cast<double>(want);
}
} // namespace
TEST(ClosedForm, InverseHyperbolicsAndTrig)
{
for (unsigned k : {16u, 32u})
{
for (long double x : {-0.6L, -0.2L, 0.2L, 0.6L})
expect_ulps(grotto::closed::atanh, k, x, 8.0L);
for (long double x : {-2.0L, -0.5L, 0.3L, 1.5L, 4.0L})
expect_ulps(grotto::closed::asinh, k, x, 16.0L);
for (long double x : {1.0L, 1.5L, 3.0L})
expect_ulps(grotto::closed::acosh, k, x, 16.0L);
for (long double x : {-2.0L, -0.4L, 0.0L, 0.5L, 1.0L, 3.0L})
expect_ulps(grotto::closed::atan, k, x, 8.0L);
for (long double x : {-1.5L, 0.4L, 2.0L})
expect_ulps(grotto::closed::acot, k, x, 8.0L);
for (long double x : {-2.0L, -1.2L, 1.2L, 3.0L})
{
expect_ulps(grotto::closed::asec, k, x, 8.0L);
expect_ulps(grotto::closed::acsc, k, x, 8.0L);
}
for (long double x : {0.2L, 0.5L, 1.0L})
expect_ulps(grotto::closed::asech, k, x, 16.0L);
for (long double x : {-1.5L, -0.4L, 0.4L, 2.0L})
expect_ulps(grotto::closed::acsch, k, x, 16.0L);
for (long double x : {-2.0L, 1.4L, 3.0L})
expect_ulps(grotto::closed::acoth, k, x, 12.0L);
}
}
TEST(ClosedForm, ActivationsQuantilesAndSinc)
{
for (unsigned k : {16u, 32u})
{
for (long double x : {-1.5L, -0.2L, 0.0L, 0.4L, 2.0L})
{
expect_ulps(grotto::closed::elu, k, x, 8.0L);
expect_ulps(grotto::closed::celu, k, x, 8.0L);
expect_ulps(grotto::closed::selu, k, x, 16.0L);
expect_ulps(grotto::closed::softsign, k, x, 4.0L);
expect_ulps(grotto::closed::tanhshrink, k, x, 8.0L);
expect_ulps(grotto::closed::sinc, k, x, 1.0L);
}
for (long double p : {0.1L, 0.3L, 0.5L, 0.8L})
{
expect_ulps(grotto::closed::logistic, k, p, 12.0L);
expect_ulps(grotto::closed::exponential, k, p, 12.0L);
expect_ulps(grotto::closed::laplace, k, p, 12.0L);
}
for (long double p : {0.2L, 0.4L, 0.6L, 0.8L})
expect_ulps(grotto::closed::cauchy, k, p, 16.0L);
}
}
TEST(ClosedForm, ExtraPowers)
{
for (unsigned k : {16u, 32u})
{
for (long double x : {-8.0L, -1.0L, 0.5L, 1.0L, 2.0L, 8.0L})
{
expect_ulps(grotto::closed::cbrt, k, x, 24.0L);
expect_ulps(grotto::closed::icbrt, k, x, 1.0L);
expect_ulps(grotto::closed::pow_m3, k, x, 16.0L);
}
for (long double x : {0.25L, 0.5L, 1.0L, 2.0L, 9.0L, 81.0L})
{
expect_ulps(grotto::closed::qtrt, k, x, 24.0L);
expect_ulps(grotto::closed::iqtrt, k, x, 32.0L);
expect_ulps(grotto::closed::pow_m01, k, x, 24.0L);
expect_ulps(grotto::closed::pow_p15, k, x, 1.0L);
}
expect_ulps(grotto::closed::icbrt, k, std::ldexp(1.0L, -8), 1.0L);
expect_ulps(grotto::closed::icbrt, k, 100.0L, 1.0L);
}
}
TEST(ClosedForm, RejectsPolesAndBadPrecision)
{
EXPECT_THROW(grotto::eval_closed(grotto::closed::atanh, 16, 1 << 16), std::domain_error);
EXPECT_THROW(grotto::eval_closed(grotto::closed::acosh, 16, 0), std::domain_error);
EXPECT_THROW(grotto::eval_closed(grotto::closed::qtrt, 16, -4), std::domain_error);
EXPECT_THROW(grotto::eval_closed(grotto::closed::pow_m3, 16, 0), std::domain_error);
EXPECT_THROW(grotto::eval_closed(grotto::closed::logistic, 16, 0), std::domain_error);
EXPECT_THROW(grotto::eval_closed(grotto::closed::sinc, 7, 1), std::invalid_argument);
}

252
test/tests/cohort_test.cpp Normal file
View file

@ -0,0 +1,252 @@
#include <gtest/gtest.h>
#include "dpf.hpp"
#include <cstdint>
#include <vector>
namespace
{
using in_type = std::uint8_t;
using out_type = std::uint64_t;
template <typename T>
out_type raw_of(const T & v)
{
if constexpr (dpf::is_secret_share_v<std::decay_t<T>>)
return static_cast<out_type>(v.raw());
else
return static_cast<out_type>(v);
}
out_type open(out_type a, out_type b)
{
return static_cast<out_type>(a - b);
}
template <typename Key>
std::vector<out_type> interval_alone(const Key & key, in_type from, in_type to)
{
auto buf = dpf::make_output_buffer_for_interval(key, from, to);
dpf::eval_interval(key, from, to, buf);
std::vector<out_type> v;
v.reserve(buf.size());
for (std::size_t i = 0; i < buf.size(); ++i)
v.push_back(raw_of(buf[i]));
return v;
}
} // namespace
TEST(Cohort, SamePointGenAndPointEval)
{
const in_type alpha = 7;
const std::vector<out_type> beta{1, 2, 3, 4, 5};
auto [c0, c1] = dpf::make_dpf_cohort(alpha, beta.begin(), beta.end());
ASSERT_EQ(c0.size(), beta.size());
std::vector<out_type> y0, y1;
c0.eval_point(alpha, y0);
c1.eval_point(alpha, y1);
ASSERT_EQ(y0.size(), beta.size());
for (std::size_t k = 0; k < beta.size(); ++k)
{
EXPECT_EQ(open(y0[k], y1[k]), beta[k]);
EXPECT_EQ(y0[k], raw_of(*dpf::eval_point(c0.keys()[k], alpha)));
EXPECT_EQ(y1[k], raw_of(*dpf::eval_point(c1.keys()[k], alpha)));
}
for (std::size_t k = 0; k < beta.size(); ++k)
{
const in_type other = static_cast<in_type>(alpha + 1 + k);
std::vector<out_type> z0, z1;
c0.eval_point(other, z0);
c1.eval_point(other, z1);
EXPECT_EQ(open(z0[k], z1[k]), 0u);
EXPECT_EQ(z0[k], raw_of(*dpf::eval_point(c0.keys()[k], other)));
}
}
TEST(Cohort, IntervalInterleavesLeaves)
{
const in_type alpha = 9;
const std::vector<out_type> beta{4, 8, 15, 16};
auto [c0, c1] = dpf::make_dpf_cohort(alpha, beta.begin(), beta.end());
const in_type from = 0, to = 15;
std::vector<out_type> y0, y1;
c0.eval_interval(from, to, y0);
c1.eval_interval(from, to, y1);
using key0 = std::decay_t<decltype(c0.keys()[0])>;
constexpr std::size_t opl = key0::outputs_per_leaf;
const std::size_t n = beta.size();
const auto alone0 = interval_alone(c0.keys()[0], from, to);
ASSERT_EQ(y0.size(), alone0.size() * n);
ASSERT_EQ(alone0.size() % opl, 0u);
for (std::size_t k = 0; k < n; ++k)
{
const auto a0 = interval_alone(c0.keys()[k], from, to);
const auto a1 = interval_alone(c1.keys()[k], from, to);
ASSERT_EQ(a0.size(), alone0.size());
for (std::size_t i = 0; i < a0.size(); ++i)
{
const std::size_t node = i / opl;
const std::size_t lane = i % opl;
const std::size_t slot = (node * n + k) * opl + lane;
EXPECT_EQ(y0[slot], a0[i]);
EXPECT_EQ(y1[slot], a1[i]);
const in_type x = static_cast<in_type>(from + i);
const out_type opened = open(a0[i], a1[i]);
if (x == alpha)
EXPECT_EQ(opened, beta[k]);
else
EXPECT_EQ(opened, 0u);
}
}
}
TEST(Cohort, SequenceRecipeAndInnerProduct)
{
const in_type alpha = 5;
const std::vector<out_type> beta{3, 9, 1};
auto [c0, c1] = dpf::make_dpf_cohort(alpha, beta.begin(), beta.end());
const std::vector<in_type> points{1, 5, 5, 20, 40};
const std::size_t n = beta.size();
std::vector<out_type> y0, y1;
c0.eval_sequence(points.begin(), points.end(), y0);
c1.eval_sequence(points.begin(), points.end(), y1);
ASSERT_EQ(y0.size(), points.size() * n);
using key0 = std::decay_t<decltype(c0.keys()[0])>;
auto recipe = dpf::make_sequence_recipe<key0>(
points.begin(), points.end());
std::vector<out_type> r0, r1;
c0.eval_sequence(recipe, r0);
c1.eval_sequence(recipe, r1);
for (std::size_t q = 0; q < points.size(); ++q)
{
for (std::size_t k = 0; k < n; ++k)
{
const std::size_t slot = dpf::cohort_index(q, k, n);
const auto e0 = raw_of(*dpf::eval_point(c0.keys()[k], points[q]));
const auto e1 = raw_of(*dpf::eval_point(c1.keys()[k], points[q]));
EXPECT_EQ(y0[slot], e0);
EXPECT_EQ(y1[slot], e1);
EXPECT_EQ(r0[slot], e0);
EXPECT_EQ(r1[slot], e1);
if (points[q] == alpha)
EXPECT_EQ(open(e0, e1), beta[k]);
else
EXPECT_EQ(open(e0, e1), 0u);
}
}
std::vector<out_type> w(points.size());
for (std::size_t q = 0; q < w.size(); ++q)
w[q] = q + 3;
const auto s0 = c0.eval_sequence_inner_product(recipe, w);
const auto s1 = c1.eval_sequence_inner_product(points.begin(), points.end(), w);
ASSERT_EQ(s0.size(), n);
for (std::size_t k = 0; k < n; ++k)
{
out_type acc0 = 0, acc1 = 0;
for (std::size_t q = 0; q < points.size(); ++q)
{
acc0 = static_cast<out_type>(acc0
+ y0[dpf::cohort_index(q, k, n)] * w[q]);
acc1 = static_cast<out_type>(acc1
+ y1[dpf::cohort_index(q, k, n)] * w[q]);
}
EXPECT_EQ(s0[k], acc0);
EXPECT_EQ(s1[k], acc1);
EXPECT_EQ(open(s0[k], s1[k]), beta[k] * (w[1] + w[2]));
}
}
TEST(Cohort, IntervalInnerProduct)
{
const in_type alpha = 4;
const std::vector<out_type> beta{6, 7};
auto [c0, c1] = dpf::make_dpf_cohort(alpha, beta.begin(), beta.end());
const in_type from = 0, to = 15;
const auto alone = interval_alone(c0.keys()[0], from, to);
std::vector<out_type> w(alone.size());
for (std::size_t i = 0; i < w.size(); ++i)
w[i] = (i * 5u) + 1u;
const auto a0 = c0.eval_interval_inner_product(from, to, w);
const auto a1 = c1.eval_interval_inner_product(from, to, w);
for (std::size_t k = 0; k < beta.size(); ++k)
{
const auto b0 = interval_alone(c0.keys()[k], from, to);
const auto b1 = interval_alone(c1.keys()[k], from, to);
out_type e0 = 0;
out_type e1 = 0;
for (std::size_t i = 0; i < w.size(); ++i)
{
e0 = static_cast<out_type>(e0 + b0[i] * w[i]);
e1 = static_cast<out_type>(e1 + b1[i] * w[i]);
}
EXPECT_EQ(a0[k], e0);
EXPECT_EQ(a1[k], e1);
// Same weights against one-key batched leaf inner product.
auto m0 = dpf::make_basic_interval_memoizer(c0.keys()[k], from, to);
auto m1 = dpf::make_basic_interval_memoizer(c1.keys()[k], from, to);
const auto ip0 = dpf::eval_inner_product(c0.keys()[k], from, to, w, m0);
const auto ip1 = dpf::eval_inner_product(c1.keys()[k], from, to, w, m1);
EXPECT_EQ(raw_of(ip0), a0[k]);
EXPECT_EQ(raw_of(ip1), a1[k]);
EXPECT_EQ(open(a0[k], a1[k]), beta[k] * w[alpha - from]);
}
}
TEST(Cohort, InterleaveLeavesMatchesSequenceLayout)
{
// Sequence cohort layout is `out[q * n + k]`, the same order
// `interleave_leaves` writes for whole leaf values.
const in_type alpha = 6;
const std::vector<out_type> beta{2, 3, 5};
auto [c0, c1] = dpf::make_dpf_cohort(alpha, beta.begin(), beta.end());
(void)c1;
const std::vector<in_type> pts{1, 6, 10, 20};
const std::size_t n = beta.size();
std::vector<out_type> cohort;
c0.eval_sequence(pts.begin(), pts.end(), cohort);
std::vector<std::vector<out_type>> per_key(n);
std::vector<const out_type *> ptrs(n);
for (std::size_t k = 0; k < n; ++k)
{
per_key[k].resize(pts.size());
for (std::size_t q = 0; q < pts.size(); ++q)
per_key[k][q] = raw_of(*dpf::eval_point(c0.keys()[k], pts[q]));
ptrs[k] = per_key[k].data();
}
std::vector<out_type> interleaved(pts.size() * n);
dpf::interleave_leaves<out_type>(
interleaved.data(), ptrs.data(), n, pts.size());
ASSERT_EQ(interleaved.size(), cohort.size());
for (std::size_t i = 0; i < interleaved.size(); ++i)
EXPECT_EQ(interleaved[i], cohort[i]) << "slot " << i;
}
TEST(Cohort, HalfTreeSamePoint)
{
using ht = dpf::prg::aes128_ccr;
const in_type alpha = 3;
const std::vector<out_type> beta{11, 13, 17, 19};
auto [c0, c1] = dpf::make_dpf_cohort<ht, dpf::prg::aes128>(
alpha, beta.begin(), beta.end());
std::vector<out_type> y0, y1;
c0.eval_point(alpha, y0);
c1.eval_point(alpha, y1);
for (std::size_t k = 0; k < beta.size(); ++k)
{
EXPECT_EQ(open(y0[k], y1[k]), beta[k]);
EXPECT_EQ(y0[k], raw_of(*dpf::eval_point(c0.keys()[k], alpha)));
}
}

1899
test/tests/compose_test.cpp Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <tuple>
#include "dpf.hpp"
#include "grotto/constant_lut.hpp"
@ -258,10 +259,8 @@ bool dispatch_segments(const grotto::constant_lut<std::int8_t> & lut, std::int8_
TEST(ConstantLut, RejectsFractionalBitsPastTheRawWidth)
{
EXPECT_THROW(grotto::make_exact_constant_lut<std::int8_t>(exact_constant::signum, 9),
std::invalid_argument);
EXPECT_THROW(grotto::make_exact_constant_lut<std::int64_t>(exact_constant::clz, 65),
std::invalid_argument);
EXPECT_THROW(grotto::make_exact_constant_lut<std::int8_t>(exact_constant::signum, 9), std::invalid_argument);
EXPECT_THROW(grotto::make_exact_constant_lut<std::int64_t>(exact_constant::clz, 65), std::invalid_argument);
}
TEST(ConstantLut, PaperPartCountsOnInt64With16FractionalBits)
@ -714,7 +713,6 @@ TEST(ConstantLut, ClippedQuotientIntervalAndThreshold)
EXPECT_THROW(grotto::make_clipped_quotient_lut<std::int16_t>(0, -1, 1), std::invalid_argument);
EXPECT_THROW(grotto::make_clipped_quotient_lut<std::int16_t>(1, 4, -4), std::invalid_argument);
EXPECT_THROW(grotto::make_clipped_quotient_lut<std::int32_t>(1, -100000, 100000),
std::invalid_argument);
EXPECT_THROW(grotto::make_clipped_quotient_lut<std::int32_t>(1, -100000, 100000), std::invalid_argument);
EXPECT_THROW(grotto::make_interval_lut<std::int8_t>(2, -2), std::invalid_argument);
}

View file

@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <tuple>
#include "dpf.hpp"
#include "grotto/prefix_parity.hpp"

View file

@ -109,10 +109,8 @@ TEST(CornerGaps, SaturatedUint64LeafCount)
using key_t = dpf::utils::dpf_type_t<dpf::prg::aes128, dpf::prg::aes128, in_t, out_t>;
EXPECT_EQ((dpf::utils::get_nodes_in_interval<key_t>(in_t{1}, ~in_t{0})),
std::numeric_limits<std::size_t>::max());
EXPECT_THROW((dpf::utils::get_nodes_in_interval<key_t>(in_t{0}, ~in_t{0})),
std::length_error);
EXPECT_THROW((dpf::utils::get_nodes_in_interval<key_t>(in_t{5}, in_t{4})),
std::length_error);
EXPECT_THROW((dpf::utils::get_nodes_in_interval<key_t>(in_t{0}, ~in_t{0})), std::length_error);
EXPECT_THROW((dpf::utils::get_nodes_in_interval<key_t>(in_t{5}, in_t{4})), std::length_error);
}
TEST(CornerGaps, MemoizerRejectsALargerInterval)
@ -121,8 +119,7 @@ TEST(CornerGaps, MemoizerRejectsALargerInterval)
using key_t = std::decay_t<decltype(k0)>;
auto memo = dpf::make_basic_interval_memoizer<key_t>(uint8_t{0}, uint8_t{10});
auto buf = dpf::make_output_buffer_for_interval<key_t>(uint8_t{0}, uint8_t{100});
EXPECT_THROW(dpf::eval_interval(k0, uint8_t{0}, uint8_t{100}, buf, memo),
std::length_error);
EXPECT_THROW(dpf::eval_interval(k0, uint8_t{0}, uint8_t{100}, buf, memo), std::length_error);
(void)k1;
}
@ -387,7 +384,6 @@ TEST(CornerGaps, PrgRejectsUint32Seam)
HEDLEY_PRAGMA(GCC diagnostic push)
HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes")
EXPECT_THROW((dpf::randomness::detail::lane_codec<dpf::prg::aes128, simde__m128i>::fill(
seed, static_cast<std::uint64_t>(UINT32_MAX) - 1u, out, 4)),
std::invalid_argument);
seed, static_cast<std::uint64_t>(UINT32_MAX) - 1u, out, 4)), std::invalid_argument);
HEDLEY_PRAGMA(GCC diagnostic pop)
}

View file

@ -0,0 +1,611 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <cstdint>
#include <iterator>
#include <limits>
#include <stdexcept>
#include <tuple>
#include <type_traits>
#include <utility>
#include <vector>
#include "dpf.hpp"
namespace
{
template <typename A, typename B>
auto recon(const A & a, const B & b)
{
if constexpr (dpf::is_secret_share_v<std::decay_t<A>>
&& dpf::is_secret_share_v<std::decay_t<B>>)
return dpf::reconstruct(a, b);
else
{
using T = std::common_type_t<std::decay_t<A>, std::decay_t<B>>;
if constexpr (std::is_integral_v<T> && std::is_unsigned_v<T>)
return static_cast<T>(a - b);
else
return a - b;
}
}
template <typename Key0, typename Key1, typename InputT>
void assign_input_local(Key0 & k0, Key1 & k1, InputT alpha)
{
using input_type = InputT;
const input_type a0 = static_cast<input_type>(0x12);
const input_type a1 = static_cast<input_type>(alpha - a0);
const auto sh0 = k0.offset_x.compute_and_get_share(a0);
const auto sh1 = k1.offset_x.compute_and_get_share(a1);
k0.offset_x.reconstruct(sh1);
k1.offset_x.reconstruct(sh0);
}
template <typename Def0, typename Def1, typename Eager0, typename Eager1>
void expect_recon_equal(Def0 && deferred0, Def1 && deferred1,
Eager0 && eager0, Eager1 && eager1)
{
auto it_a = std::begin(deferred0);
auto it_b = std::begin(deferred1);
auto it_c = std::begin(eager0);
auto it_d = std::begin(eager1);
const auto end_a = std::end(deferred0);
std::size_t n = 0;
while (it_a != end_a)
{
ASSERT_NE(it_b, std::end(deferred1)) << "at index " << n;
ASSERT_NE(it_c, std::end(eager0)) << "at index " << n;
ASSERT_NE(it_d, std::end(eager1)) << "at index " << n;
EXPECT_EQ(recon(*it_a, *it_b), recon(*it_c, *it_d)) << "at index " << n;
++it_a;
++it_b;
++it_c;
++it_d;
++n;
}
EXPECT_EQ(it_b, std::end(deferred1));
EXPECT_EQ(it_c, std::end(eager0));
EXPECT_EQ(it_d, std::end(eager1));
EXPECT_GT(n, std::size_t{0});
}
template <typename InputT>
std::size_t inclusive_span(InputT from, InputT to)
{
constexpr auto bits = dpf::utils::bitlength_of_v<InputT>;
constexpr auto to_int = dpf::utils::to_integral_type<InputT>{};
auto span = to_int(to) - to_int(from);
if constexpr (bits < dpf::utils::bitlength_of_v<decltype(span)>)
span &= (decltype(span){1} << bits) - 1;
return static_cast<std::size_t>(span) + 1;
}
template <typename View0, typename View1, typename InputT, typename OutputT>
void expect_point_mass(View0 && v0, View1 && v1, InputT from, InputT to,
InputT alpha, OutputT beta)
{
auto it0 = std::begin(v0);
auto it1 = std::begin(v1);
bool saw = false;
InputT x = from;
for (std::size_t i = 0; i < inclusive_span(from, to); ++i)
{
ASSERT_NE(it0, std::end(v0));
ASSERT_NE(it1, std::end(v1));
const auto y = recon(*it0, *it1);
if (x == alpha)
{
EXPECT_EQ(y, beta) << "x=" << +x;
saw = true;
}
else
{
EXPECT_EQ(y, OutputT{0}) << "x=" << +x;
}
++it0;
++it1;
++x;
}
EXPECT_EQ(it0, std::end(v0));
EXPECT_EQ(it1, std::end(v1));
bool expect_hit = false;
if constexpr (std::is_unsigned_v<InputT>)
{
if (from <= to)
expect_hit = (alpha >= from && alpha <= to);
else
expect_hit = (alpha >= from) || (alpha <= to);
}
else
{
expect_hit = (alpha >= from && alpha <= to);
}
EXPECT_EQ(saw, expect_hit);
}
template <typename Key0, typename Key1, typename InputT, typename OutputT>
void compare_deferred_interval(Key0 & d0, Key1 & d1, InputT from, InputT to,
InputT alpha, OutputT beta)
{
auto buf0 = dpf::make_output_buffer_for_full(d0);
auto buf1 = dpf::make_output_buffer_for_full(d1);
auto deferred0 = dpf::defer_eval_interval(d0, from, to, buf0);
auto deferred1 = dpf::defer_eval_interval(d1, from, to, buf1);
assign_input_local(d0, d1, alpha);
auto eager_buf0 = dpf::make_output_buffer_for_interval(d0, from, to);
auto eager_buf1 = dpf::make_output_buffer_for_interval(d1, from, to);
auto eager_memo0 = dpf::make_basic_full_memoizer(d0);
auto eager_memo1 = dpf::make_basic_full_memoizer(d1);
auto eager0 = dpf::eval_interval(d0, from, to, eager_buf0, eager_memo0);
auto eager1 = dpf::eval_interval(d1, from, to, eager_buf1, eager_memo1);
auto view0 = deferred0.get();
auto view1 = deferred1.get();
expect_recon_equal(view0, view1, eager0, eager1);
expect_point_mass(view0, view1, from, to, alpha, beta);
}
} // namespace
// ---------------------------------------------------------------------------
// Happy paths already covered lightly; keep regressions + expand corners.
// ---------------------------------------------------------------------------
TEST(DeferEvalTest, IntervalMatchesEagerAfterAssignUint8)
{
using input_type = std::uint8_t;
using output_type = std::uint32_t;
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
output_type{7});
compare_deferred_interval(d0, d1, input_type{0x10}, input_type{0x40},
input_type{0x2A}, output_type{7});
}
TEST(DeferEvalTest, FullMatchesEagerAfterAssignUint8)
{
using input_type = std::uint8_t;
using output_type = std::uint64_t;
constexpr output_type beta{0x1111};
constexpr input_type alpha{0x33};
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{}, beta);
auto buf0 = dpf::make_output_buffer_for_full(d0);
auto buf1 = dpf::make_output_buffer_for_full(d1);
auto deferred0 = dpf::defer_eval_full(d0, buf0);
auto deferred1 = dpf::defer_eval_full(d1, buf1);
assign_input_local(d0, d1, alpha);
auto eager_buf0 = dpf::make_output_buffer_for_full(d0);
auto eager_buf1 = dpf::make_output_buffer_for_full(d1);
auto eager0 = dpf::eval_full(d0, eager_buf0);
auto eager1 = dpf::eval_full(d1, eager_buf1);
auto view0 = deferred0.get();
auto view1 = deferred1.get();
expect_recon_equal(view0, view1, eager0, eager1);
expect_point_mass(view0, view1,
std::numeric_limits<input_type>::min(),
std::numeric_limits<input_type>::max(), alpha, beta);
}
TEST(DeferEvalTest, IntervalMatchesEagerAfterAssignSigned)
{
using input_type = std::int8_t;
using output_type = std::uint32_t;
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
output_type{9});
compare_deferred_interval(d0, d1, input_type{-40}, input_type{10},
input_type{-20}, output_type{9});
}
TEST(DeferEvalTest, MultiOutputLeafMatchesEager)
{
using input_type = std::uint8_t;
using output_type = std::uint64_t;
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
output_type{0xABCDEF0123456789ull});
ASSERT_GT(decltype(d0)::outputs_per_leaf, std::size_t{1});
compare_deferred_interval(d0, d1, input_type{0x70}, input_type{0x8F},
input_type{0x7E}, output_type{0xABCDEF0123456789ull});
}
// ---------------------------------------------------------------------------
// Assert / misuse corners
// ---------------------------------------------------------------------------
TEST(DeferEvalTest, GetBeforeAssignThrows)
{
using input_type = std::uint8_t;
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
std::uint32_t{1});
(void)d1;
auto buf = dpf::make_output_buffer_for_full(d0);
auto deferred = dpf::defer_eval_interval(d0, input_type{0}, input_type{3},
buf);
EXPECT_THROW(static_cast<void>(deferred.get()), std::runtime_error);
EXPECT_THROW(static_cast<void>(deferred.begin()), std::runtime_error);
}
TEST(DeferEvalTest, DeferAfterAssignThrows)
{
using input_type = std::uint8_t;
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
std::uint32_t{1});
assign_input_local(d0, d1, input_type{9});
auto buf = dpf::make_output_buffer_for_full(d0);
EXPECT_THROW(
static_cast<void>(dpf::defer_eval_interval(d0, input_type{0},
input_type{3}, buf)),
std::runtime_error);
EXPECT_THROW(static_cast<void>(dpf::defer_eval_full(d0, buf)),
std::runtime_error);
}
TEST(DeferEvalTest, EagerEvalBeforeAssignThrows)
{
using input_type = std::uint8_t;
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
std::uint32_t{1});
(void)d1;
EXPECT_THROW(static_cast<void>(dpf::eval_point(d0, input_type{0})),
std::runtime_error);
EXPECT_THROW(static_cast<void>(dpf::eval_interval(d0, input_type{0},
input_type{1})),
std::runtime_error);
EXPECT_THROW(static_cast<void>(dpf::eval_full(d0)), std::runtime_error);
}
TEST(DeferEvalTest, DeferTraverseIntervalRequiresAssignedInput)
{
using input_type = std::uint8_t;
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
std::uint32_t{1});
auto memo = dpf::make_basic_full_memoizer(d0);
EXPECT_THROW(
dpf::defer_traverse_interval(d0, input_type{0}, input_type{10}, memo),
std::runtime_error);
assign_input_local(d0, d1, input_type{4});
EXPECT_NO_THROW(
dpf::defer_traverse_interval(d0, input_type{0}, input_type{10}, memo));
}
TEST(DeferEvalTest, DeferTraverseFullAllowsUnassignedInput)
{
using input_type = std::uint8_t;
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
std::uint32_t{3});
auto memo0 = dpf::make_basic_full_memoizer(d0);
auto memo1 = dpf::make_basic_full_memoizer(d1);
EXPECT_NO_THROW(dpf::defer_traverse_full(d0, memo0));
EXPECT_NO_THROW(dpf::defer_traverse_full(d1, memo1));
assign_input_local(d0, d1, input_type{0x55});
EXPECT_EQ(recon(*dpf::eval_point(d0, input_type{0x55}),
*dpf::eval_point(d1, input_type{0x55})),
std::uint32_t{3});
}
// ---------------------------------------------------------------------------
// Boundary / length / caching
// ---------------------------------------------------------------------------
TEST(DeferEvalTest, SinglePointInterval)
{
using input_type = std::uint8_t;
using output_type = std::uint32_t;
constexpr input_type alpha{0x77};
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
output_type{42});
compare_deferred_interval(d0, d1, alpha, alpha, alpha, output_type{42});
}
TEST(DeferEvalTest, SpikeAtFromAndToBoundaries)
{
using input_type = std::uint8_t;
using output_type = std::uint32_t;
{
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
output_type{5});
compare_deferred_interval(d0, d1, input_type{0x20}, input_type{0x30},
input_type{0x20}, output_type{5});
}
{
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
output_type{6});
compare_deferred_interval(d0, d1, input_type{0x20}, input_type{0x30},
input_type{0x30}, output_type{6});
}
}
TEST(DeferEvalTest, SpikeOutsideIntervalIsZero)
{
using input_type = std::uint8_t;
using output_type = std::uint32_t;
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
output_type{99});
compare_deferred_interval(d0, d1, input_type{0x10}, input_type{0x20},
input_type{0x80}, output_type{99});
}
TEST(DeferEvalTest, ViewLengthMatchesInclusiveSpan)
{
using input_type = std::uint8_t;
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
std::uint32_t{1});
constexpr input_type from{5};
constexpr input_type to{12};
auto buf0 = dpf::make_output_buffer_for_full(d0);
auto buf1 = dpf::make_output_buffer_for_full(d1);
auto deferred0 = dpf::defer_eval_interval(d0, from, to, buf0);
auto deferred1 = dpf::defer_eval_interval(d1, from, to, buf1);
assign_input_local(d0, d1, input_type{7});
auto view0 = deferred0.get();
auto view1 = deferred1.get();
EXPECT_EQ(static_cast<std::size_t>(std::distance(std::begin(view0),
std::end(view0))),
inclusive_span(from, to));
EXPECT_EQ(static_cast<std::size_t>(std::distance(std::begin(view1),
std::end(view1))),
inclusive_span(from, to));
}
TEST(DeferEvalTest, GetCachesRotationSecondCallMatches)
{
using input_type = std::uint8_t;
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
std::uint32_t{11});
auto buf0 = dpf::make_output_buffer_for_full(d0);
auto buf1 = dpf::make_output_buffer_for_full(d1);
auto deferred0 = dpf::defer_eval_interval(d0, input_type{1}, input_type{20},
buf0);
auto deferred1 = dpf::defer_eval_interval(d1, input_type{1}, input_type{20},
buf1);
assign_input_local(d0, d1, input_type{9});
auto a0 = deferred0.get();
auto a1 = deferred1.get();
auto b0 = deferred0.get();
auto b1 = deferred1.get();
expect_recon_equal(a0, a1, b0, b1);
}
TEST(DeferEvalTest, BeginEndOnDeferredObject)
{
using input_type = std::uint8_t;
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
std::uint32_t{2});
auto buf0 = dpf::make_output_buffer_for_full(d0);
auto buf1 = dpf::make_output_buffer_for_full(d1);
auto deferred0 = dpf::defer_eval_interval(d0, input_type{0}, input_type{4},
buf0);
auto deferred1 = dpf::defer_eval_interval(d1, input_type{0}, input_type{4},
buf1);
assign_input_local(d0, d1, input_type{2});
std::size_t n = 0;
auto it0 = deferred0.begin();
auto it1 = deferred1.begin();
for (; it0 != deferred0.end(); ++it0, ++it1, ++n)
(void)recon(*it0, *it1);
EXPECT_EQ(it1, deferred1.end());
EXPECT_EQ(n, inclusive_span(input_type{0}, input_type{4}));
}
TEST(DeferEvalTest, DeferEvalIntervalMinMaxMatchesDeferFull)
{
using input_type = std::uint8_t;
using output_type = std::uint32_t;
constexpr output_type beta{13};
constexpr input_type alpha{0x01};
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{}, beta);
auto buf_i0 = dpf::make_output_buffer_for_full(d0);
auto buf_i1 = dpf::make_output_buffer_for_full(d1);
auto buf_f0 = dpf::make_output_buffer_for_full(d0);
auto buf_f1 = dpf::make_output_buffer_for_full(d1);
auto as_interval0 = dpf::defer_eval_interval(d0,
std::numeric_limits<input_type>::min(),
std::numeric_limits<input_type>::max(), buf_i0);
auto as_interval1 = dpf::defer_eval_interval(d1,
std::numeric_limits<input_type>::min(),
std::numeric_limits<input_type>::max(), buf_i1);
auto as_full0 = dpf::defer_eval_full(d0, buf_f0);
auto as_full1 = dpf::defer_eval_full(d1, buf_f1);
assign_input_local(d0, d1, alpha);
expect_recon_equal(as_interval0.get(), as_interval1.get(),
as_full0.get(), as_full1.get());
}
// ---------------------------------------------------------------------------
// Wrapping intervals and offset stress
// ---------------------------------------------------------------------------
TEST(DeferEvalTest, WrappingLogicalIntervalUint8)
{
using input_type = std::uint8_t;
using output_type = std::uint32_t;
// [200, 10] wraps across 0.
constexpr input_type from{200};
constexpr input_type to{10};
ASSERT_GT(from, to);
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
output_type{8});
compare_deferred_interval(d0, d1, from, to, input_type{250},
output_type{8});
}
TEST(DeferEvalTest, WrappingIntervalSpikeInLowHalf)
{
using input_type = std::uint8_t;
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
std::uint32_t{4});
compare_deferred_interval(d0, d1, input_type{200}, input_type{10},
input_type{5}, std::uint32_t{4});
}
TEST(DeferEvalTest, ManyRandomOffsetsMatchEager)
{
using input_type = std::uint8_t;
using output_type = std::uint32_t;
constexpr input_type from{30};
constexpr input_type to{90};
constexpr output_type beta{77};
// Sweep alphas; each keygen draws a fresh mask so offsets differ.
for (unsigned a = 0; a < 256; a += 17)
{
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{}, beta);
compare_deferred_interval(d0, d1, from, to,
static_cast<input_type>(a), beta);
}
}
TEST(DeferEvalTest, SignedFullDomain)
{
using input_type = std::int8_t;
using output_type = std::uint32_t;
constexpr output_type beta{21};
constexpr input_type alpha{-128};
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{}, beta);
auto buf0 = dpf::make_output_buffer_for_full(d0);
auto buf1 = dpf::make_output_buffer_for_full(d1);
auto deferred0 = dpf::defer_eval_full(d0, buf0);
auto deferred1 = dpf::defer_eval_full(d1, buf1);
assign_input_local(d0, d1, alpha);
auto eager_buf0 = dpf::make_output_buffer_for_full(d0);
auto eager_buf1 = dpf::make_output_buffer_for_full(d1);
auto eager0 = dpf::eval_full(d0, eager_buf0);
auto eager1 = dpf::eval_full(d1, eager_buf1);
expect_recon_equal(deferred0.get(), deferred1.get(), eager0, eager1);
}
TEST(DeferEvalTest, SignedSpanCrossingZero)
{
using input_type = std::int8_t;
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
std::uint32_t{3});
compare_deferred_interval(d0, d1, input_type{-5}, input_type{5},
input_type{0}, std::uint32_t{3});
}
TEST(DeferEvalTest, UnalignedMultiOutputLeafInterval)
{
using input_type = std::uint8_t;
using output_type = std::uint64_t;
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
output_type{0x55});
ASSERT_GT(decltype(d0)::outputs_per_leaf, std::size_t{1});
// Odd endpoints: not leaf-aligned when opl == 2.
compare_deferred_interval(d0, d1, input_type{0x11}, input_type{0x2A},
input_type{0x1F}, output_type{0x55});
}
// ---------------------------------------------------------------------------
// Multi-output keys
// ---------------------------------------------------------------------------
TEST(DeferEvalTest, TwoOutputSlotsIndependent)
{
using input_type = std::uint8_t;
using out0 = std::uint32_t;
using out1 = std::uint32_t;
constexpr input_type alpha{0x44};
constexpr out0 beta0{100};
constexpr out1 beta1{200};
constexpr input_type from{0x40};
constexpr input_type to{0x50};
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{}, beta0,
beta1);
auto bufs0 = dpf::make_output_buffer_for_full<0, 1>(d0);
auto bufs1 = dpf::make_output_buffer_for_full<0, 1>(d1);
auto def0 = dpf::defer_eval_interval<0, 1>(d0, from, to, bufs0);
auto def1 = dpf::defer_eval_interval<0, 1>(d1, from, to, bufs1);
assign_input_local(d0, d1, alpha);
auto eager_bufs0 = dpf::make_output_buffer_for_interval<0, 1>(d0, from, to);
auto eager_bufs1 = dpf::make_output_buffer_for_interval<0, 1>(d1, from, to);
auto memo0 = dpf::make_basic_full_memoizer(d0);
auto memo1 = dpf::make_basic_full_memoizer(d1);
auto eager0 = dpf::eval_interval<0, 1>(d0, from, to, eager_bufs0, memo0);
auto eager1 = dpf::eval_interval<0, 1>(d1, from, to, eager_bufs1, memo1);
expect_recon_equal(std::get<0>(def0).get(), std::get<0>(def1).get(),
std::get<0>(eager0), std::get<0>(eager1));
expect_recon_equal(std::get<1>(def0).get(), std::get<1>(def1).get(),
std::get<1>(eager0), std::get<1>(eager1));
expect_point_mass(std::get<0>(def0).get(), std::get<0>(def1).get(),
from, to, alpha, beta0);
expect_point_mass(std::get<1>(def0).get(), std::get<1>(def1).get(),
from, to, alpha, beta1);
}
TEST(DeferEvalTest, SelectSecondOutputOnly)
{
using input_type = std::uint8_t;
constexpr input_type alpha{0x08};
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
std::uint32_t{1}, std::uint32_t{99});
auto buf0 = dpf::make_output_buffer_for_full<1>(d0);
auto buf1 = dpf::make_output_buffer_for_full<1>(d1);
auto def0 = dpf::defer_eval_interval<1>(d0, input_type{0}, input_type{15},
buf0);
auto def1 = dpf::defer_eval_interval<1>(d1, input_type{0}, input_type{15},
buf1);
assign_input_local(d0, d1, alpha);
auto eager_buf0 = dpf::make_output_buffer_for_interval<1>(d0, input_type{0},
input_type{15});
auto eager_buf1 = dpf::make_output_buffer_for_interval<1>(d1, input_type{0},
input_type{15});
auto memo0 = dpf::make_basic_full_memoizer(d0);
auto memo1 = dpf::make_basic_full_memoizer(d1);
auto eager0 = dpf::eval_interval<1>(d0, input_type{0}, input_type{15},
eager_buf0, memo0);
auto eager1 = dpf::eval_interval<1>(d1, input_type{0}, input_type{15},
eager_buf1, memo1);
expect_recon_equal(def0.get(), def1.get(), eager0, eager1);
expect_point_mass(def0.get(), def1.get(), input_type{0}, input_type{15},
alpha, std::uint32_t{99});
}
// ---------------------------------------------------------------------------
// Metadata on the deferred object
// ---------------------------------------------------------------------------
TEST(DeferEvalTest, DeferredStoresFromToAndKey)
{
using input_type = std::uint8_t;
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
std::uint32_t{1});
(void)d1;
auto buf = dpf::make_output_buffer_for_full(d0);
auto deferred = dpf::defer_eval_interval(d0, input_type{3}, input_type{9},
buf);
EXPECT_EQ(deferred.from(), input_type{3});
EXPECT_EQ(deferred.to(), input_type{9});
EXPECT_EQ(&deferred.dpf(), &d0);
}
TEST(DeferEvalTest, DomainMinMaxSpike)
{
using input_type = std::uint8_t;
{
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
std::uint32_t{1});
compare_deferred_interval(d0, d1, input_type{0}, input_type{255},
input_type{0}, std::uint32_t{1});
}
{
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<input_type>{},
std::uint32_t{2});
compare_deferred_interval(d0, d1, input_type{0}, input_type{255},
input_type{255}, std::uint32_t{2});
}
}

View file

@ -0,0 +1,156 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <cstring>
#include <limits>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <vector>
#include "dpf.hpp"
namespace
{
// Full AES-128 block payload: Boyle packing leaves lg(outputs_per_leaf) = 0,
// so a 128-bit domain walks depth 128 (Express `domainSize` = 128).
using input_t = simde_uint128;
using output_t = simde_uint128;
using dpf_key_t = dpf::utils::dpf_type_t<dpf::prg::aes128, dpf::prg::aes128,
input_t, output_t>;
output_t make_payload()
{
output_t y{};
auto * bytes = reinterpret_cast<unsigned char *>(&y);
for (std::size_t i = 0; i < sizeof(y); ++i)
bytes[i] = static_cast<unsigned char>(0xa0 + i);
return y;
}
input_t make_alpha()
{
// High bit set, plus a distinctive low pattern.
return (input_t{1} << 127) | (input_t{0x0123456789abcdefull} << 64)
| input_t{0xfedcba9876543210ull};
}
} // namespace
TEST(Domain128Point, DepthIs128ForFullBlockPayload)
{
static_assert(dpf::utils::bitlength_of_v<input_t> == 128);
static_assert(dpf_key_t::lg_outputs_per_leaf == 0);
static_assert(dpf_key_t::outputs_per_leaf == 1);
static_assert(dpf_key_t::depth == 128);
EXPECT_EQ(dpf_key_t::depth, 128u);
EXPECT_EQ(dpf::utils::bitlength_of_v<input_t>, 128u);
}
TEST(Domain128Point, PointEvalShares)
{
const input_t alpha = make_alpha();
const output_t beta = make_payload();
const output_t zero{};
auto [k0, k1] = dpf::make_dpf(alpha, beta);
EXPECT_EQ(k0.correction_words().size(), 128u);
EXPECT_EQ(k1.correction_words().size(), 128u);
EXPECT_EQ(std::decay_t<decltype(k0)>::depth, 128u);
auto open = [&](input_t x) {
return dpf::reconstruct(*dpf::eval_point(k0, x),
*dpf::eval_point(k1, x));
};
// Programmed point: both parties' shares open to the payload.
{
const auto s0 = *dpf::eval_point(k0, alpha);
const auto s1 = *dpf::eval_point(k1, alpha);
EXPECT_EQ(dpf::reconstruct(s0, s1), beta);
EXPECT_EQ(open(alpha), beta);
}
const std::vector<input_t> off_points = {
input_t{0},
input_t{1},
alpha + 1,
alpha - 1,
input_t{1} << 127,
(input_t{1} << 127) | input_t{1},
alpha ^ (input_t{1} << 64),
alpha ^ input_t{1},
~input_t{0},
};
for (const input_t x : off_points)
{
if (x == alpha)
continue;
const auto s0 = *dpf::eval_point(k0, x);
const auto s1 = *dpf::eval_point(k1, x);
EXPECT_EQ(dpf::reconstruct(s0, s1), zero) << "off-point open";
}
// Every individual bit of alpha must be on the path.
for (int i = 0; i < 128; ++i)
{
const input_t flipped = alpha ^ (input_t{1} << i);
const auto s0 = *dpf::eval_point(k0, flipped);
const auto s1 = *dpf::eval_point(k1, flipped);
EXPECT_EQ(dpf::reconstruct(s0, s1), zero) << "bit " << i;
}
}
TEST(Domain128Point, Uint128ClassInput)
{
using in_t = uint128_t;
using out_t = simde_uint128;
using kt = dpf::utils::dpf_type_t<dpf::prg::aes128, dpf::prg::aes128,
in_t, out_t>;
static_assert(kt::depth == 128);
static_assert(dpf::utils::bitlength_of_v<in_t> == 128);
// uint128_t(upper, lower): bit 127 set.
const in_t alpha{std::uint64_t{1} << 63, 0};
out_t beta{};
std::memset(&beta, 0x5c, sizeof(beta));
auto [k0, k1] = dpf::make_dpf(alpha, beta);
EXPECT_EQ(k0.correction_words().size(), 128u);
const auto s0 = *dpf::eval_point(k0, alpha);
const auto s1 = *dpf::eval_point(k1, alpha);
EXPECT_EQ(dpf::reconstruct(s0, s1), beta);
const in_t neigh = alpha + in_t{1};
EXPECT_EQ(dpf::reconstruct(*dpf::eval_point(k0, neigh),
*dpf::eval_point(k1, neigh)), out_t{});
}
TEST(Domain128Point, FullDomainExpansionThrows)
{
const input_t alpha = make_alpha();
const output_t beta = make_payload();
auto [k0, k1] = dpf::make_dpf(alpha, beta);
(void)k1;
EXPECT_THROW(
(void)dpf::make_output_buffer_for_full(k0),
std::length_error);
EXPECT_THROW(
(void)dpf::eval_full(k0),
std::length_error);
EXPECT_THROW(
(void)dpf::eval_interval(k0,
std::numeric_limits<input_t>::min(),
std::numeric_limits<input_t>::max()),
std::length_error);
EXPECT_THROW(
(void)dpf::make_basic_full_memoizer(k0),
std::length_error);
}

757
test/tests/dpf3_test.cpp Normal file
View file

@ -0,0 +1,757 @@
#include <gtest/gtest.h>
#include <tuple>
#include <cstdint>
#include <cstring>
#include <stdexcept>
#include <vector>
#include "dpf.hpp"
#include "dpf/dpf3_ds.hpp"
#include "dpf/prg_aes_ccr.hpp"
namespace
{
using dpf::fp61;
fp61 open3(fp61 a, fp61 b, fp61 c)
{
return dpf::shamir3::reconstruct(
dpf::shamir3::share{1, a}, dpf::shamir3::share{2, b},
dpf::shamir3::share{3, c});
}
/// F_DPF3CMP: complementary DCF halves (party 1 = k0, party 2 = k1).
fp61 open_cmp(std::uint64_t k0_half, std::uint64_t k1_half)
{
return dpf::reconstruct_cmp_halves(k0_half, k1_half);
}
template <typename K1, typename K2, typename K3, typename Input>
void expect_point(const K1 & k1, const K2 & k2, const K3 & k3, Input alpha,
fp61 beta)
{
for (unsigned x = 0; x < 256; ++x)
{
const fp61 got = open3(dpf::eval_point(k1, static_cast<Input>(x)),
dpf::eval_point(k2, static_cast<Input>(x)),
dpf::eval_point(k3, static_cast<Input>(x)));
if (static_cast<Input>(x) == alpha)
EXPECT_EQ(got, beta) << "x=" << x;
else
EXPECT_EQ(got.raw(), 0u) << "x=" << x;
}
}
} // namespace
TEST(Shamir3, ShareAndReconstruct)
{
const fp61 secret{123456789u};
const auto s = dpf::shamir3::share_secret(secret);
EXPECT_EQ(dpf::shamir3::reconstruct(s[0], s[1]), secret);
EXPECT_EQ(dpf::shamir3::reconstruct(s[0], s[2]), secret);
EXPECT_EQ(dpf::shamir3::reconstruct(s[1], s[2]), secret);
EXPECT_EQ(dpf::shamir3::reconstruct(s[0], s[1], s[2]), secret);
}
TEST(Shamir3, PartyScaleRoundTrip)
{
const fp61 v{0x123456789abcdefull & ((1ull << 61) - 1)};
const auto s = dpf::shamir3::share_secret(v);
for (const auto & sh : s)
{
const fp61 uns = dpf::shamir3::unscale(sh);
const fp61 back = dpf::shamir3::party_scale(
dpf::shamir3::share{sh.party, uns});
EXPECT_EQ(back, sh.value);
}
}
TEST(Dpf3, PointFullDomain)
{
using Input = std::uint8_t;
const Input alpha = 42;
const fp61 beta{99};
auto [k1, k2, k3] = dpf::make_dpf3(alpha, beta);
expect_point(k1, k2, k3, alpha, beta);
// Single key is not the clear point function.
EXPECT_NE(dpf::eval_point(k1, alpha), beta);
EXPECT_NE(dpf::eval_point(k2, alpha), beta);
EXPECT_NE(dpf::eval_point(k3, alpha), beta);
}
TEST(Dpf3, HalfTree)
{
using Input = std::uint8_t;
using Interior = dpf::prg::aes128_ccr;
using Exterior = dpf::prg::aes128;
const Input alpha = 7;
const fp61 beta{5};
auto [k1, k2, k3] = dpf::make_dpf3<Interior, Exterior>(alpha, beta);
expect_point(k1, k2, k3, alpha, beta);
}
TEST(Dpf3, VerifiableProof)
{
using Input = std::uint8_t;
const Input alpha = 11;
const fp61 beta{3};
auto [k1, k2, k3] = dpf::make_dpf3(alpha, beta, dpf::verifiable{});
auto p1 = dpf::prove_dpf3(k1, alpha);
auto p2 = dpf::prove_dpf3(k2, alpha);
auto p3 = dpf::prove_dpf3(k3, alpha);
EXPECT_TRUE(dpf::verify_dpf3(p1, p2, p3));
using arr = typename std::decay_t<decltype(k1.a.dpf_key)>::correction_seeds_array;
for (auto & cs : const_cast<arr &>(k1.a.dpf_key.correction_seeds()))
cs[0] = simde_mm_xor_si128(cs[0], simde_mm_set1_epi8(1));
auto bad = dpf::prove_dpf3(k1, alpha);
EXPECT_FALSE(dpf::verify_dpf3(bad, p2, p3));
}
TEST(Dpf3, ExtractableWeightOne)
{
using Input = std::uint8_t;
const Input alpha = 20;
const fp61 beta{7};
auto [k1, k2, k3] = dpf::make_dpf3(alpha, beta, dpf::extractable{});
EXPECT_TRUE(k1.extractable);
std::vector<fp61> s1(256), s2(256), s3(256), rs(256);
for (unsigned x = 0; x < 256; ++x)
{
s1[x] = dpf::eval_point(k1, static_cast<Input>(x));
s2[x] = dpf::eval_point(k2, static_cast<Input>(x));
s3[x] = dpf::eval_point(k3, static_cast<Input>(x));
rs[x] = dpf::uniform_sample<fp61>();
}
EXPECT_TRUE(dpf::sketch_verify3(k1, k2, k3, s1, s2, s3, rs));
s1[21] = s1[21] + beta;
s2[21] = s2[21] + beta;
s3[21] = s3[21] + beta;
EXPECT_FALSE(dpf::sketch_verify3(k1, k2, k3, s1, s2, s3, rs));
}
TEST(Dpf3, ExtractableGateRejectsPlainKeys)
{
using Input = std::uint8_t;
auto [k1, k2, k3] = dpf::make_dpf3(Input{1}, fp61{1});
std::vector<fp61> z(1, fp61{});
std::vector<fp61> rs(1, fp61{1});
EXPECT_THROW(dpf::sketch_verify3(k1, k2, k3, z, z, z, rs),
std::invalid_argument);
}
TEST(Dpf3, VerifiableExtractableProofAndSketch)
{
using Input = std::uint8_t;
const Input alpha = 33;
const fp61 beta{6};
auto [k1, k2, k3] =
dpf::make_dpf3(alpha, beta, dpf::verifiable{}, dpf::extractable{});
std::vector<fp61> s1(256), s2(256), s3(256), rs(256);
for (unsigned x = 0; x < 256; ++x)
{
s1[x] = dpf::eval_point(k1, static_cast<Input>(x));
s2[x] = dpf::eval_point(k2, static_cast<Input>(x));
s3[x] = dpf::eval_point(k3, static_cast<Input>(x));
rs[x] = dpf::uniform_sample<fp61>();
}
EXPECT_TRUE(dpf::verify_dpf3(k1, k2, k3, dpf::prove_dpf3(k1, alpha),
dpf::prove_dpf3(k2, alpha), dpf::prove_dpf3(k3, alpha), s1, s2, s3,
rs));
}
TEST(Dpf3, UpdatableInPlaceKeepsAlpha)
{
using Input = std::uint8_t;
const Input alpha = 9;
auto [k1, k2, k3] = dpf::make_dpf3(alpha, fp61{5}, dpf::updatable{});
EXPECT_TRUE(k1.updatable && k2.updatable && k3.updatable);
expect_point(k1, k2, k3, alpha, fp61{5});
dpf::update_payload(k1, k2, k3, alpha, fp61{8});
expect_point(k1, k2, k3, alpha, fp61{8});
dpf::update_payload(k1, k2, k3, alpha, fp61{0});
expect_point(k1, k2, k3, alpha, fp61{0});
dpf::update_payload(k1, k2, k3, alpha, fp61{100});
expect_point(k1, k2, k3, alpha, fp61{100});
}
TEST(Dpf3, NonUpdatableRejectsUpdate)
{
using Input = std::uint8_t;
auto [k1, k2, k3] = dpf::make_dpf3(Input{3}, fp61{1});
EXPECT_FALSE(k1.updatable);
EXPECT_THROW(dpf::update_payload(k1, k2, k3, Input{3}, fp61{2}), std::invalid_argument);
}
TEST(Dpf3, RemakeFreshTrees)
{
using Input = std::uint8_t;
const Input alpha = 15;
auto [k1, k2, k3] = dpf::remake_dpf3(alpha, fp61{44});
expect_point(k1, k2, k3, alpha, fp61{44});
auto [v1, v2, v3] = dpf::remake_dpf3(alpha, fp61{2}, dpf::verifiable{});
EXPECT_TRUE(v1.verifiable);
expect_point(v1, v2, v3, alpha, fp61{2});
}
TEST(Dpf3, VerifiableUpdatableProofSurvivesUpdate)
{
using Input = std::uint8_t;
const Input alpha = 33;
auto [k1, k2, k3] =
dpf::make_dpf3(alpha, fp61{6}, dpf::verifiable{}, dpf::updatable{});
EXPECT_TRUE(dpf::verify_dpf3(dpf::prove_dpf3(k1, alpha),
dpf::prove_dpf3(k2, alpha), dpf::prove_dpf3(k3, alpha)));
dpf::update_payload(k1, k2, k3, alpha, fp61{90});
expect_point(k1, k2, k3, alpha, fp61{90});
EXPECT_TRUE(dpf::verify_dpf3(dpf::prove_dpf3(k1, alpha),
dpf::prove_dpf3(k2, alpha), dpf::prove_dpf3(k3, alpha)));
}
TEST(Dpf3, MultipointFullDomain)
{
using Input = std::uint8_t;
const std::vector<Input> alphas{1, 2, 9, 40};
const std::vector<fp61> betas{fp61{7}, fp61{11}, fp61{3}, fp61{4}};
auto [k1, k2, k3] = dpf::make_multipoint3(alphas, betas);
for (unsigned x = 0; x < 256; ++x)
{
fp61 want{};
for (std::size_t i = 0; i < alphas.size(); ++i)
if (alphas[i] == static_cast<Input>(x))
want = betas[i];
const fp61 got = open3(
dpf::eval_multipoint(k1, static_cast<Input>(x)),
dpf::eval_multipoint(k2, static_cast<Input>(x)),
dpf::eval_multipoint(k3, static_cast<Input>(x)));
EXPECT_EQ(got, want) << "x=" << x;
}
}
TEST(Dpf3, MultipointUpdatableInPlace)
{
using Input = std::uint8_t;
const std::vector<Input> alphas{1, 2, 9, 40};
const std::vector<fp61> betas{fp61{7}, fp61{11}, fp61{3}, fp61{4}};
auto [k1, k2, k3] = dpf::make_multipoint3(alphas, betas, dpf::updatable{});
EXPECT_TRUE(k1.updatable);
const auto sigma = k1.sigma;
const auto m = k1.bucket_count;
const std::vector<fp61> betas2{fp61{1}, fp61{2}, fp61{3}, fp61{4}};
dpf::update_payload(k1, k2, k3, alphas, betas2);
EXPECT_EQ(0, std::memcmp(&k1.sigma, &sigma, sizeof(sigma)));
EXPECT_EQ(k1.bucket_count, m);
for (unsigned x = 0; x < 256; ++x)
{
fp61 want{};
for (std::size_t i = 0; i < alphas.size(); ++i)
if (alphas[i] == static_cast<Input>(x))
want = betas2[i];
const fp61 got = open3(
dpf::eval_multipoint(k1, static_cast<Input>(x)),
dpf::eval_multipoint(k2, static_cast<Input>(x)),
dpf::eval_multipoint(k3, static_cast<Input>(x)));
EXPECT_EQ(got, want) << "x=" << x;
}
}
TEST(Dpf3, MultipointNonUpdatableRejects)
{
using Input = std::uint8_t;
const std::vector<Input> alphas{3, 5};
const std::vector<fp61> betas{fp61{1}, fp61{1}};
auto [k1, k2, k3] = dpf::make_multipoint3(alphas, betas);
EXPECT_THROW(dpf::update_payload(k1, k2, k3, alphas, betas), std::invalid_argument);
}
TEST(Dpf3, MultipointSeedFlip)
{
using Input = std::uint8_t;
const std::vector<Input> alphas{3, 5, 7};
const std::vector<fp61> betas{fp61{1}, fp61{1}, fp61{1}};
auto [k1, k2, k3] = dpf::make_multipoint3(alphas, betas, dpf::verifiable{});
ASSERT_FALSE(k1.buckets.empty());
auto flip = [](auto & plus) {
using arr =
typename std::decay_t<decltype(plus.dpf_key)>::correction_seeds_array;
for (auto & cs : const_cast<arr &>(plus.dpf_key.correction_seeds()))
cs[0] = simde_mm_xor_si128(cs[0], simde_mm_set1_epi8(1));
};
for (auto & bucket : k1.buckets)
{
flip(bucket.a);
flip(bucket.b);
}
bool rejected = false;
for (std::size_t i = 0; i < k1.buckets.size(); ++i)
{
auto p1 = dpf::prove_dpf3(k1.buckets[i], Input{0});
auto p2 = dpf::prove_dpf3(k2.buckets[i], Input{0});
auto p3 = dpf::prove_dpf3(k3.buckets[i], Input{0});
if (!dpf::verify_dpf3(p1, p2, p3))
rejected = true;
}
EXPECT_TRUE(rejected);
}
TEST(Dpf3, ComparisonFullDomain)
{
using Input = std::uint8_t;
const Input thresh = 100;
const uint64_t beta = 17;
auto [k1, k2, k3] = dpf::make_dpf3_cmp(thresh, beta);
for (unsigned x = 0; x < 256; ++x)
{
const bool hot = static_cast<Input>(x) < thresh;
const auto s1 = dpf::eval_point(k1, static_cast<Input>(x));
const auto s2 = dpf::eval_point(k2, static_cast<Input>(x));
const auto s3 = dpf::eval_point(k3, static_cast<Input>(x));
EXPECT_EQ(open_cmp(s1, s2).raw(), hot ? beta : 0u) << "x=" << x;
EXPECT_EQ(open_cmp(s3, s2).raw(), hot ? beta : 0u) << "x=" << x;
EXPECT_EQ(s1, s3) << "x=" << x; // both hold k0
if (hot)
EXPECT_NE(s1, beta);
}
}
TEST(Dpf3, BlockedComparison)
{
using Input = std::uint8_t;
auto [k1, k2, k3] = dpf::make_dpf3_cmp_blocked<4>(Input{50}, 9u);
EXPECT_EQ(open_cmp(dpf::eval_point(k1, Input{10}), dpf::eval_point(k2, Input{10}))
.raw(),
9u);
EXPECT_EQ(open_cmp(dpf::eval_point(k1, Input{200}),
dpf::eval_point(k2, Input{200}))
.raw(),
0u);
}
TEST(Dpf3, IntervalFullDomain)
{
using Input = std::uint8_t;
const Input r = 10, p = 20, q = 40;
const uint64_t beta = 5;
auto [k1, k2, k3] = dpf::make_dpf3_ic(r, p, q, beta);
auto two = dpf::make_dpf(r, dpf::ic(p, q, beta));
for (unsigned x = 0; x < 256; ++x)
{
const auto want = dpf::reconstruct(
dpf::eval_point(dpf::ic, two.first, static_cast<Input>(x)),
dpf::eval_point(dpf::ic, two.second, static_cast<Input>(x)));
const fp61 got = open_cmp(
dpf::eval_point(k1, static_cast<Input>(x)),
dpf::eval_point(k2, static_cast<Input>(x)));
EXPECT_EQ(got.raw(), static_cast<uint64_t>(want)) << "x=" << x;
}
}
TEST(Dpf3, ComparisonPayloadUpdateInPlace)
{
using Input = std::uint8_t;
const Input thresh = 80;
auto [k1, k2, k3] = dpf::make_dpf3_cmp(thresh, 3u);
dpf::update_payload_cmp(k1, k2, k3, 3u, 11u);
for (unsigned x = 0; x < 256; ++x)
{
const bool hot = static_cast<Input>(x) < thresh;
const fp61 got = open_cmp(
dpf::eval_point(k1, static_cast<Input>(x)),
dpf::eval_point(k2, static_cast<Input>(x)));
EXPECT_EQ(got.raw(), hot ? 11u : 0u) << "x=" << x;
}
dpf::update_payload_cmp(k1, k2, k3, 11u, 0u);
for (unsigned x = 0; x < 256; ++x)
{
const fp61 got = open_cmp(
dpf::eval_point(k1, static_cast<Input>(x)),
dpf::eval_point(k2, static_cast<Input>(x)));
EXPECT_EQ(got.raw(), 0u) << "x=" << x;
}
}
TEST(Dpf3, ComparisonRemake)
{
using Input = std::uint8_t;
auto [k1, k2, k3] = dpf::remake_dpf3_cmp(Input{50}, 7u, 0u);
EXPECT_EQ(open_cmp(dpf::eval_point(k1, Input{10}), dpf::eval_point(k2, Input{10}))
.raw(),
7u);
}
TEST(Dpf3, DoernerShelatMatchesDealer)
{
using Input = std::uint8_t;
const Input alpha = 0x2a;
const Input x0 = 0x13;
const Input x1 = static_cast<Input>(alpha ^ x0);
const fp61 beta{77};
auto [d1, d2, d3] = dpf::make_dpf3(alpha, beta);
auto [s1, s2, s3] = dpf::make_dpf3_doerner_shelat(x0, x1, beta);
expect_point(d1, d2, d3, alpha, beta);
expect_point(s1, s2, s3, alpha, beta);
EXPECT_EQ(dpf::detail::dpf3_impl::open_xor_point(x0, x1), alpha);
}
TEST(Dpf3, DoernerShelatVerifiable)
{
using Input = std::uint8_t;
const Input alpha = 19;
const Input x0 = 7;
const Input x1 = static_cast<Input>(alpha ^ x0);
auto [k1, k2, k3] =
dpf::make_dpf3_doerner_shelat(x0, x1, fp61{4}, dpf::verifiable{});
EXPECT_TRUE(k1.verifiable);
expect_point(k1, k2, k3, alpha, fp61{4});
EXPECT_TRUE(dpf::verify_dpf3(dpf::prove_dpf3(k1, alpha),
dpf::prove_dpf3(k2, alpha), dpf::prove_dpf3(k3, alpha)));
}
TEST(Dpf3, DoernerShelatUpdatable)
{
using Input = std::uint8_t;
const Input alpha = 8;
const Input x0 = 1;
const Input x1 = static_cast<Input>(alpha ^ x0);
auto [k1, k2, k3] =
dpf::make_dpf3_doerner_shelat(x0, x1, fp61{2}, dpf::updatable{});
EXPECT_TRUE(k1.updatable);
EXPECT_FALSE(k1.verifiable);
dpf::update_payload(k1, k2, k3, alpha, fp61{55});
expect_point(k1, k2, k3, alpha, fp61{55});
}
TEST(Dpf3, DoernerShelatVerifiableUpdatableTagParity)
{
using Input = std::uint8_t;
const Input alpha = 14;
const Input x0 = 2;
const Input x1 = static_cast<Input>(alpha ^ x0);
auto [k1, k2, k3] = dpf::make_dpf3_doerner_shelat(x0, x1, fp61{3},
dpf::verifiable{}, dpf::updatable{});
EXPECT_TRUE(k1.verifiable && k1.updatable);
EXPECT_TRUE(dpf::verify_dpf3(dpf::prove_dpf3(k1, alpha),
dpf::prove_dpf3(k2, alpha), dpf::prove_dpf3(k3, alpha)));
dpf::update_payload(k1, k2, k3, alpha, fp61{11});
expect_point(k1, k2, k3, alpha, fp61{11});
}
TEST(Dpf3, DoernerShelatExtractable)
{
using Input = std::uint8_t;
const Input alpha = 12;
const Input x0 = 3;
const Input x1 = static_cast<Input>(alpha ^ x0);
auto [k1, k2, k3] =
dpf::make_dpf3_doerner_shelat(x0, x1, fp61{1}, dpf::extractable{});
EXPECT_TRUE(k1.extractable && k2.extractable && k3.extractable);
expect_point(k1, k2, k3, alpha, fp61{1});
std::vector<fp61> s1(256), s2(256), s3(256), rs(256);
for (unsigned x = 0; x < 256; ++x)
{
s1[x] = dpf::eval_point(k1, static_cast<Input>(x));
s2[x] = dpf::eval_point(k2, static_cast<Input>(x));
s3[x] = dpf::eval_point(k3, static_cast<Input>(x));
rs[x] = dpf::uniform_sample<fp61>();
}
EXPECT_TRUE(dpf::sketch_verify3(k1, k2, k3, s1, s2, s3, rs));
}
TEST(Dpf3, RemakeTagParity)
{
using Input = std::uint8_t;
auto [u1, u2, u3] = dpf::remake_dpf3(Input{1}, fp61{2}, dpf::updatable{});
EXPECT_TRUE(u1.updatable);
expect_point(u1, u2, u3, Input{1}, fp61{2});
auto [e1, e2, e3] =
dpf::remake_dpf3(Input{2}, fp61{3}, dpf::verifiable{}, dpf::extractable{});
EXPECT_TRUE(e1.verifiable && e1.extractable);
expect_point(e1, e2, e3, Input{2}, fp61{3});
}
TEST(Dpf3, MultipointVerifiableUpdatable)
{
using Input = std::uint8_t;
const std::vector<Input> alphas{4, 8, 16};
const std::vector<fp61> betas{fp61{1}, fp61{2}, fp61{3}};
auto [k1, k2, k3] = dpf::make_multipoint3(alphas, betas, dpf::verifiable{},
dpf::updatable{});
EXPECT_TRUE(k1.verifiable && k1.updatable);
const std::vector<fp61> betas2{fp61{9}, fp61{8}, fp61{7}};
dpf::update_payload(k1, k2, k3, alphas, betas2);
for (std::size_t i = 0; i < alphas.size(); ++i)
{
const fp61 got = open3(dpf::eval_multipoint(k1, alphas[i]),
dpf::eval_multipoint(k2, alphas[i]),
dpf::eval_multipoint(k3, alphas[i]));
EXPECT_EQ(got, betas2[i]);
}
}
TEST(Dpf3, AsSharePartyIndex)
{
using Input = std::uint8_t;
auto [k1, k2, k3] = dpf::make_dpf3(Input{0}, fp61{1});
const fp61 y{};
EXPECT_EQ(dpf::as_share(k1, y).party, 1);
EXPECT_EQ(dpf::as_share(k2, y).party, 2);
EXPECT_EQ(dpf::as_share(k3, y).party, 3);
}
TEST(Shamir3, InconsistentSharesThrow)
{
const auto s = dpf::shamir3::share_secret(fp61{42});
auto bad = s[2];
bad.value = bad.value + fp61{1};
EXPECT_THROW(dpf::shamir3::reconstruct(s[0], s[1], bad), std::runtime_error);
}
TEST(Dpf3, ComparisonPredicatesLeqGtGeq)
{
using Input = std::uint8_t;
const Input thresh = 100;
const uint64_t beta = 4;
auto check = [&](auto keys, auto pred) {
auto [k1, k2, k3] = keys;
for (unsigned x = 0; x < 256; ++x)
{
const bool hot = pred(static_cast<Input>(x));
const fp61 got = open_cmp(dpf::eval_point(k1, static_cast<Input>(x)),
dpf::eval_point(k2, static_cast<Input>(x)));
EXPECT_EQ(got.raw(), hot ? beta : 0u) << "x=" << x;
}
};
check(dpf::make_dpf3_cmp(thresh, dpf::leq(beta)),
[&](Input x) { return x <= thresh; });
check(dpf::make_dpf3_cmp(thresh, dpf::gt(beta)),
[&](Input x) { return x > thresh; });
check(dpf::make_dpf3_cmp(thresh, dpf::geq(beta)),
[&](Input x) { return x >= thresh; });
}
TEST(Dpf3, ComparisonUpdateFp61NotUint64Wrap)
{
// Historical bug: `(0 - 11) & ~0ull` is not −11 mod p. Updating 11 → 0
// must clear the hot lane, not leave a wraparound residue.
using Input = std::uint8_t;
const Input thresh = 60;
auto [k1, k2, k3] = dpf::make_dpf3_cmp(thresh, 11u);
dpf::update_payload_cmp(k1, k2, k3, 11u, 0u);
for (unsigned x = 0; x < 256; ++x)
{
const fp61 got = open_cmp(dpf::eval_point(k1, static_cast<Input>(x)),
dpf::eval_point(k2, static_cast<Input>(x)));
EXPECT_EQ(got.raw(), 0u) << "x=" << x;
}
}
TEST(Dpf3, PublicPiIdenticalAfterUpdate)
{
using Input = std::uint8_t;
const Input alpha = 21;
auto [k1, k2, k3] = dpf::make_dpf3(alpha, fp61{3}, dpf::updatable{});
EXPECT_EQ(k1.a.offset, k2.a.offset);
EXPECT_EQ(k2.a.offset, k3.a.offset);
EXPECT_EQ(k1.b.offset, k2.b.offset);
EXPECT_EQ(k2.b.offset, k3.b.offset);
dpf::update_payload(k1, k2, k3, alpha, fp61{70});
EXPECT_EQ(k1.a.offset, k2.a.offset);
EXPECT_EQ(k2.a.offset, k3.a.offset);
EXPECT_EQ(k1.b.offset, k2.b.offset);
EXPECT_EQ(k2.b.offset, k3.b.offset);
expect_point(k1, k2, k3, alpha, fp61{70});
}
TEST(Dpf3, ProveRejectsClearedVerifiableFlag)
{
using Input = std::uint8_t;
const Input alpha = 5;
auto [k1, k2, k3] = dpf::make_dpf3(alpha, fp61{1}, dpf::verifiable{});
k1.verifiable = false; // outer flag gate (inners remain V)
EXPECT_THROW(dpf::prove_dpf3(k1, alpha), std::invalid_argument);
(void)k2;
(void)k3;
}
TEST(Dpf3, VerifyExtractableRejectsNonExtractable)
{
using Input = std::uint8_t;
const Input alpha = 8;
auto [k1, k2, k3] = dpf::make_dpf3(alpha, fp61{2}, dpf::verifiable{});
std::vector<fp61> s1(8), s2(8), s3(8), rs(8);
for (unsigned x = 0; x < 8; ++x)
{
s1[x] = dpf::eval_point(k1, static_cast<Input>(x));
s2[x] = dpf::eval_point(k2, static_cast<Input>(x));
s3[x] = dpf::eval_point(k3, static_cast<Input>(x));
rs[x] = fp61{1};
}
EXPECT_THROW(
dpf::verify_dpf3(k1, k2, k3, dpf::prove_dpf3(k1, alpha),
dpf::prove_dpf3(k2, alpha), dpf::prove_dpf3(k3, alpha), s1, s2, s3,
rs),
std::invalid_argument);
}
TEST(Dpf3, IntervalBoundaryRelativeToR)
{
// Interval is relative to public shift `r`; x=r+p and x=r+q−1 are the
// classic edges that mis-scored in the IC example (absolute vs relative).
using Input = std::uint8_t;
const Input r = 10, p = 20, q = 40;
const uint64_t beta = 5;
auto [k1, k2, k3] = dpf::make_dpf3_ic(r, p, q, beta);
auto two = dpf::make_dpf(r, dpf::ic(p, q, beta));
for (Input x : {Input{29}, Input{30}, Input{35}, Input{49}, Input{50}})
{
const auto want = dpf::reconstruct(
dpf::eval_point(dpf::ic, two.first, x),
dpf::eval_point(dpf::ic, two.second, x));
const fp61 got = open_cmp(dpf::eval_point(k1, x), dpf::eval_point(k2, x));
EXPECT_EQ(got.raw(), static_cast<uint64_t>(want)) << "x=" << unsigned(x);
}
}
TEST(Dpf3, MultipointRemakeFreshSigma)
{
using Input = std::uint8_t;
const std::vector<Input> alphas{2, 4, 8};
const std::vector<fp61> betas{fp61{1}, fp61{2}, fp61{3}};
auto [a1, a2, a3] = dpf::make_multipoint3(alphas, betas, dpf::updatable{});
const auto sigma0 = a1.sigma;
auto [b1, b2, b3] = dpf::remake_multipoint3(alphas, betas);
EXPECT_NE(0, std::memcmp(&b1.sigma, &sigma0, sizeof(sigma0)));
for (std::size_t i = 0; i < alphas.size(); ++i)
{
EXPECT_EQ(open3(dpf::eval_multipoint(b1, alphas[i]),
dpf::eval_multipoint(b2, alphas[i]),
dpf::eval_multipoint(b3, alphas[i])),
betas[i]);
}
(void)a2;
(void)a3;
}
TEST(Dpf3, HalfTreeUpdatableUpdate)
{
using Input = std::uint8_t;
using Interior = dpf::prg::aes128_ccr;
using Exterior = dpf::prg::aes128;
const Input alpha = 55;
auto [k1, k2, k3] =
dpf::make_dpf3<Interior, Exterior>(alpha, fp61{8}, dpf::updatable{});
dpf::update_payload(k1, k2, k3, alpha, fp61{12});
expect_point(k1, k2, k3, alpha, fp61{12});
}
TEST(Dpf3, UpdatePreservesCorrectionSeeds)
{
using Input = std::uint8_t;
const Input alpha = 17;
auto [k1, k2, k3] =
dpf::make_dpf3(alpha, fp61{4}, dpf::verifiable{}, dpf::updatable{});
const auto seeds = k1.a.dpf_key.correction_seeds();
const auto root = k1.a.dpf_key.root();
dpf::update_payload(k1, k2, k3, alpha, fp61{19});
EXPECT_EQ(0, std::memcmp(seeds.data(), k1.a.dpf_key.correction_seeds().data(),
sizeof(seeds)));
EXPECT_EQ(0, std::memcmp(&root, &k1.a.dpf_key.root(), sizeof(root)));
expect_point(k1, k2, k3, alpha, fp61{19});
auto [r1, r2, r3] =
dpf::remake_dpf3(alpha, fp61{19}, dpf::verifiable{}, dpf::updatable{});
EXPECT_NE(0, std::memcmp(seeds.data(), r1.a.dpf_key.correction_seeds().data(),
sizeof(seeds)));
(void)r2;
(void)r3;
}
TEST(Dpf3, DoernerShelatVuProofAfterUpdate)
{
using Input = std::uint8_t;
const Input alpha = 22;
const Input x0 = 6;
const Input x1 = static_cast<Input>(alpha ^ x0);
auto [k1, k2, k3] = dpf::make_dpf3_doerner_shelat(x0, x1, fp61{5},
dpf::verifiable{}, dpf::updatable{});
EXPECT_TRUE(dpf::verify_dpf3(dpf::prove_dpf3(k1, alpha),
dpf::prove_dpf3(k2, alpha), dpf::prove_dpf3(k3, alpha)));
dpf::update_payload(k1, k2, k3, alpha, fp61{41});
expect_point(k1, k2, k3, alpha, fp61{41});
EXPECT_TRUE(dpf::verify_dpf3(dpf::prove_dpf3(k1, alpha),
dpf::prove_dpf3(k2, alpha), dpf::prove_dpf3(k3, alpha)));
}
TEST(Dpf3, MultipointVuProofAfterUpdate)
{
using Input = std::uint8_t;
const std::vector<Input> alphas{4, 8, 16};
const std::vector<fp61> betas{fp61{1}, fp61{2}, fp61{3}};
auto [k1, k2, k3] = dpf::make_multipoint3(alphas, betas, dpf::verifiable{},
dpf::updatable{});
const auto sigma = k1.sigma;
const std::vector<fp61> betas2{fp61{9}, fp61{8}, fp61{7}};
dpf::update_payload(k1, k2, k3, alphas, betas2);
EXPECT_EQ(0, std::memcmp(&k1.sigma, &sigma, sizeof(sigma)));
bool any = false;
for (std::size_t i = 0; i < k1.buckets.size(); ++i)
{
any = true;
EXPECT_TRUE(dpf::verify_dpf3(dpf::prove_dpf3(k1.buckets[i], Input{0}),
dpf::prove_dpf3(k2.buckets[i], Input{0}),
dpf::prove_dpf3(k3.buckets[i], Input{0})));
}
EXPECT_TRUE(any);
}
TEST(Dpf3, BlockedComparisonFullDomain)
{
using Input = std::uint8_t;
const Input thresh = 50;
const uint64_t beta = 9;
auto [k1, k2, k3] = dpf::make_dpf3_cmp_blocked<4>(thresh, beta);
for (unsigned x = 0; x < 256; ++x)
{
const bool hot = static_cast<Input>(x) < thresh;
const fp61 got = open_cmp(dpf::eval_point(k1, static_cast<Input>(x)),
dpf::eval_point(k2, static_cast<Input>(x)));
EXPECT_EQ(got.raw(), hot ? beta : 0u) << "x=" << x;
}
}
TEST(Dpf3, TagsAnyOrderIncludingExtractableUpdatable)
{
using Input = std::uint8_t;
const Input alpha = 17;
auto [k1, k2, k3] = dpf::make_dpf3(alpha, fp61{4},
dpf::extractable{}, dpf::updatable{}, dpf::verifiable{});
EXPECT_TRUE(k1.verifiable && k1.extractable && k1.updatable);
EXPECT_TRUE(k2.extractable && k3.updatable);
expect_point(k1, k2, k3, alpha, fp61{4});
dpf::update_payload(k1, k2, k3, alpha, fp61{11});
expect_point(k1, k2, k3, alpha, fp61{11});
}
TEST(Dpf3, ComparisonSpecWrappersMatchPlainPredicate)
{
using Input = std::uint8_t;
const Input thresh = 40;
const uint64_t beta = 3;
auto check = [&](auto keys) {
auto [k1, k2, k3] = keys;
for (unsigned x = 0; x < 256; ++x)
{
const bool hot = static_cast<Input>(x) > thresh;
const fp61 got = open_cmp(dpf::eval_point(k1, static_cast<Input>(x)),
dpf::eval_point(k2, static_cast<Input>(x)));
EXPECT_EQ(got.raw(), hot ? beta : 0u) << "x=" << x;
}
};
check(dpf::make_dpf3_cmp(thresh, dpf::idcf(dpf::gt(beta))));
check(dpf::make_dpf3_cmp(thresh, dpf::block_width<4>(dpf::gt(beta))));
}

107
test/tests/dwt_lut_test.cpp Normal file
View file

@ -0,0 +1,107 @@
#include <gtest/gtest.h>
#include "grotto/dwt_lut.hpp"
#include <cmath>
#include <cstdint>
#include <vector>
namespace
{
grotto::dwt_lut make(grotto::dwt_family family, const std::vector<double> & samples,
unsigned fractional_bits, unsigned depth)
{
if (family == grotto::dwt_family::haar)
return grotto::make_haar_dwt_lut(samples, fractional_bits, depth);
return grotto::make_bior53_dwt_lut(samples, fractional_bits, depth);
}
} // namespace
TEST(DwtLut, HaarIsQuantizedBlockMean)
{
const std::vector<double> samples{
0.0, 0.5, -1.25, 3.0, 4.0, 0.25, -0.5, 2.0};
const auto lut = grotto::make_haar_dwt_lut(samples, 8, 1);
ASSERT_EQ(lut.coeff.size(), 4u);
EXPECT_EQ(lut.coeff[0], 64);
EXPECT_EQ(lut.coeff[1], 224);
EXPECT_EQ(lut.coeff[2], 544);
EXPECT_EQ(lut.coeff[3], 192);
EXPECT_EQ(lut(0), 64);
EXPECT_EQ(lut(1), 64);
EXPECT_EQ(lut(7), 192);
}
TEST(DwtLut, HaarNegativeFloor)
{
const std::vector<double> samples{-1.1, 0.2, -0.3, -4.0};
const auto lut = grotto::make_haar_dwt_lut(samples, 10, 1);
ASSERT_EQ(lut.coeff.size(), 2u);
EXPECT_EQ(lut.coeff[0], -461);
EXPECT_EQ(lut.coeff[1], -2202);
EXPECT_EQ(lut(0), -461);
EXPECT_EQ(lut(3), -2202);
}
TEST(DwtLut, Bior53MatchesSmoothExtension)
{
const std::vector<double> samples{
0.0, 0.5, -1.25, 3.0, 4.0, 0.25, -0.5, 2.0};
const auto one = make(grotto::dwt_family::bior53, samples, 8, 1);
const std::vector<std::int64_t> expect1{-513, 144, -288, 2064, -449, 2304};
EXPECT_EQ(one.coeff, expect1);
const auto two = make(grotto::dwt_family::bior53, samples, 8, 2);
const std::vector<std::int64_t> expect2{-3649, -753, 912, 319, 10112};
EXPECT_EQ(two.coeff, expect2);
const std::int64_t at[8] = {228, 190, 153, 116, 79, 691, 1303, 1915};
for (std::uint64_t raw = 0; raw < 8; ++raw)
EXPECT_EQ(two(raw), at[raw]) << raw;
}
TEST(DwtLut, Bior53FloorDivOnNegatives)
{
const std::vector<double> samples{-1.1, 0.2, -0.3, -4.0};
const auto lut = grotto::make_bior53_dwt_lut(samples, 10, 1);
const std::vector<std::int64_t> expect{-7578, -1793, -154, -15770};
EXPECT_EQ(lut.coeff, expect);
EXPECT_EQ(lut(0), -77);
EXPECT_EQ(lut(1), -3981);
EXPECT_EQ(lut(2), -7885);
EXPECT_EQ(lut(3), -5837);
}
TEST(DwtLut, SigmoidGridAgreesWithHaarMean)
{
auto samples = grotto::sample_dwt_signal(6, 4, [](double x) {
return 1.0 / (1.0 + std::exp(-(x - 2.0)));
});
const auto haar = grotto::make_haar_dwt_lut(samples, 4, 2);
ASSERT_EQ(haar.coeff.size(), 16u);
for (std::uint64_t bin = 0; bin < 16; ++bin)
{
double sum = 0;
for (unsigned k = 0; k < 4; ++k)
sum += samples[bin * 4 + k];
const auto q = static_cast<std::int64_t>(std::floor(sum / 4.0 * 16.0));
EXPECT_EQ(haar.coeff[bin], q) << bin;
EXPECT_EQ(haar(bin * 4), q);
}
const auto bior = grotto::make_bior53_dwt_lut(samples, 4, 2);
EXPECT_EQ(bior(32), 8);
EXPECT_EQ(bior(48), 12);
EXPECT_EQ(haar(32), 8);
}
TEST(DwtLut, RejectsBadShape)
{
const std::vector<double> samples{0.0, 1.0, 2.0};
EXPECT_THROW(grotto::make_haar_dwt_lut(samples, 4, 1), std::invalid_argument);
const std::vector<double> four{0.0, 1.0, 2.0, 3.0};
EXPECT_THROW(grotto::make_haar_dwt_lut(four, 4, 0), std::invalid_argument);
EXPECT_THROW(grotto::make_bior53_dwt_lut(four, 4, 3), std::invalid_argument);
auto lut = grotto::make_haar_dwt_lut(four, 4, 1);
EXPECT_THROW(lut(4), std::out_of_range);
}

View file

@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <tuple>
#include "grotto/dyadic_lut.hpp"
@ -181,8 +182,7 @@ TEST(DyadicLut, MostSignificantBits)
EXPECT_EQ(lut(x), bit) << raw << " i=" << i;
}
}
EXPECT_THROW(grotto::make_msb_lut<std::int16_t>(grotto::msb_bit_limit),
std::invalid_argument);
EXPECT_THROW(grotto::make_msb_lut<std::int16_t>(grotto::msb_bit_limit), std::invalid_argument);
const auto scaled = grotto::make_msb_lut<std::int16_t>(0, 4);
EXPECT_EQ(scaled(std::int16_t{-1}), 16);
EXPECT_EQ(scaled(std::int16_t{1}), 0);

View file

@ -90,6 +90,7 @@ TEST(EasyLut, FewPiecesAtEveryPrecision)
EXPECT_EQ((grotto::make_squared_relu_lut<std::int16_t>(0).parts()), 2u);
EXPECT_EQ((grotto::make_leaky_relu_lut<std::int16_t>(0).parts()), 1u);
EXPECT_EQ((grotto::make_leaky_relu_lut<std::int16_t>(3).parts()), 2u);
EXPECT_EQ((grotto::make_leaky_relu_hundredth_lut<std::int16_t>().parts()), 2u);
for (unsigned fractional_bits : {0u, 2u, 4u})
{
@ -144,6 +145,10 @@ TEST(EasyLut, ExactLinesMatchOnInt16)
return raw >= 0 ? raw : round_div(raw, 4);
});
}
const auto hundredth = grotto::make_leaky_relu_hundredth_lut<std::int16_t>();
expect_all<std::int16_t>(hundredth, [](std::int64_t raw) {
return raw >= 0 ? raw : round_div(raw, 100);
});
}
TEST(EasyLut, ClipIsTheGeneralProgram)

View file

@ -14,7 +14,13 @@ auto recon(const A & a, const B & b)
&& dpf::is_secret_share_v<std::decay_t<B>>)
return dpf::reconstruct(a, b);
else
return a - b;
{
using T = std::common_type_t<std::decay_t<A>, std::decay_t<B>>;
if constexpr (std::is_integral_v<T> && std::is_unsigned_v<T>)
return static_cast<T>(a - b);
else
return a - b;
}
}
} // namespace
@ -222,6 +228,13 @@ using Types = testing::Types
test_type<uint16_t, dpf::bitstring<150>>,
test_type<uint16_t, dpf::xor_wrapper<int64_t>>,
test_type<uint16_t, dpf::xor_wrapper<uint64_t>>,
test_type<uint16_t, dpf::gf2>,
test_type<uint16_t, dpf::gf22>,
test_type<uint16_t, dpf::gf24>,
test_type<uint16_t, dpf::gf28>,
test_type<uint16_t, dpf::gf216>,
test_type<uint16_t, dpf::gf232>,
test_type<uint16_t, dpf::gf264>,
// custom types
test_type<custom_input_type, uint64_t>,

View file

@ -16,7 +16,13 @@ auto recon(const A & a, const B & b)
&& dpf::is_secret_share_v<std::decay_t<B>>)
return dpf::reconstruct(a, b);
else
return a - b;
{
using T = std::common_type_t<std::decay_t<A>, std::decay_t<B>>;
if constexpr (std::is_integral_v<T> && std::is_unsigned_v<T>)
return static_cast<T>(a - b);
else
return a - b;
}
}
} // namespace
@ -199,6 +205,13 @@ using Types = testing::Types
test_type<uint16_t, dpf::bitstring<150>>,
test_type<uint16_t, dpf::xor_wrapper<int64_t>>,
test_type<uint16_t, dpf::xor_wrapper<uint64_t>>,
test_type<uint16_t, dpf::gf2>,
test_type<uint16_t, dpf::gf22>,
test_type<uint16_t, dpf::gf24>,
test_type<uint16_t, dpf::gf28>,
test_type<uint16_t, dpf::gf216>,
test_type<uint16_t, dpf::gf232>,
test_type<uint16_t, dpf::gf264>,
// custom types
test_type<custom_input_type, uint64_t>,

View file

@ -0,0 +1,695 @@
#include <gtest/gtest.h>
#include <array>
#include <cstdint>
#include <stdexcept>
#include <tuple>
#include <vector>
#include "dpf.hpp"
namespace
{
using In = std::uint8_t;
template <typename A, typename B>
auto open(const A & a, const B & b)
{
if constexpr (dpf::is_secret_share_v<A>)
return dpf::reconstruct(a, b);
else if constexpr (dpf::utils::is_xor_wrapper_v<A>)
return static_cast<A>(a ^ b);
else
return static_cast<A>(a - b);
}
} // namespace
TEST(InnerProduct, ScalarIntervalMatchesPoints)
{
const In alpha = 42;
const std::uint64_t beta = 7;
auto [k0, k1] = dpf::make_dpf(alpha, beta);
const In from = 40, to = 50;
std::vector<std::uint64_t> w(to - from + 1);
std::uint64_t expect = 0;
for (std::size_t i = 0; i < w.size(); ++i)
{
w[i] = i + 1;
const In x = static_cast<In>(from + i);
const auto y = open(*dpf::eval_point(k0, x), *dpf::eval_point(k1, x));
expect += static_cast<std::uint64_t>(y) * w[i];
}
EXPECT_EQ(open(
dpf::eval_inner_product(dpf::paired, k0, from, to, w),
dpf::eval_inner_product(dpf::paired, k1, from, to, w)),
expect);
EXPECT_EQ(expect, beta * w[alpha - from]);
}
TEST(InnerProduct, FullDomainOnlyAlpha)
{
auto [k0, k1] = dpf::make_dpf(In{7}, std::uint64_t{9});
std::vector<std::uint64_t> w(256);
for (unsigned i = 0; i < w.size(); ++i)
w[i] = (i * 3u) & 15u;
EXPECT_EQ(open(
dpf::eval_full_inner_product(dpf::paired, k0, w),
dpf::eval_full_inner_product(dpf::paired, k1, w)),
std::uint64_t{9} * w[7]);
}
TEST(InnerProduct, TwoOutputsSameLeaf)
{
auto [k0, k1] = dpf::make_dpf(In{9}, std::uint32_t{3}, std::uint32_t{5});
std::vector<std::array<std::uint32_t, 2>> rows;
std::uint64_t expect = 0;
for (In x = 8;; ++x)
{
const std::uint32_t w0 = 1, w1 = x;
rows.push_back({w0, w1});
const auto y0 = open(*dpf::eval_point<0>(k0, x), *dpf::eval_point<0>(k1, x));
const auto y1 = open(*dpf::eval_point<1>(k0, x), *dpf::eval_point<1>(k1, x));
expect += static_cast<std::uint64_t>(y0) * w0
+ static_cast<std::uint64_t>(y1) * w1;
if (x == 10)
break;
}
EXPECT_EQ(open(
dpf::eval_inner_product<0, 1>(dpf::paired, k0, In{8}, In{10}, rows),
dpf::eval_inner_product<0, 1>(dpf::paired, k1, In{8}, In{10}, rows)),
expect);
}
TEST(InnerProduct, TupleRowMatchesArray)
{
auto [k0, k1] = dpf::make_dpf(In{4}, std::uint16_t{2}, std::uint16_t{6});
const std::vector<In> pts{1, 4, 8};
std::vector<std::tuple<std::uint32_t, std::uint32_t>> tuples{
{1u, 1u}, {3u, 5u}, {7u, 9u}};
std::vector<std::array<std::uint32_t, 2>> arrays{{1u, 1u}, {3u, 5u}, {7u, 9u}};
const auto t0 = dpf::eval_sequence_inner_product<0, 1>(k0, pts.begin(), pts.end(), tuples);
const auto t1 = dpf::eval_sequence_inner_product<0, 1>(k1, pts.begin(), pts.end(), tuples);
const auto a0 = dpf::eval_sequence_inner_product<0, 1>(k0, pts.begin(), pts.end(), arrays);
const auto a1 = dpf::eval_sequence_inner_product<0, 1>(k1, pts.begin(), pts.end(), arrays);
EXPECT_EQ(open(t0, t1), open(a0, a1));
}
TEST(InnerProduct, AncestorAndLeaf)
{
auto [k0, k1] = dpf::make_dpf(In{0x2a}, dpf::at<4>(std::uint8_t{5}), std::uint8_t{9});
const std::vector<In> pts{0x10, 0x2a, 0x2b, 0x30};
const std::vector<std::array<std::uint32_t, 2>> rows{{1, 0}, {1, 1}, {2, 4}, {8, 1}};
std::uint64_t expect = 0;
for (std::size_t i = 0; i < pts.size(); ++i)
{
const auto y0 = open(*dpf::eval_point(dpf::out<0>, k0, pts[i]),
*dpf::eval_point(dpf::out<0>, k1, pts[i]));
const auto y1 = open(*dpf::eval_point(dpf::out<1>, k0, pts[i]),
*dpf::eval_point(dpf::out<1>, k1, pts[i]));
expect += static_cast<std::uint64_t>(y0) * rows[i][0]
+ static_cast<std::uint64_t>(y1) * rows[i][1];
if (pts[i] == In{0x2a} || pts[i] == In{0x2b})
EXPECT_EQ(y0, std::uint8_t{5});
else
EXPECT_EQ(y0, std::uint8_t{0});
}
EXPECT_EQ(open(
dpf::eval_sequence_inner_product<0, 1>(k0, pts.begin(), pts.end(), rows),
dpf::eval_sequence_inner_product<0, 1>(k1, pts.begin(), pts.end(), rows)),
expect);
const auto recipe = dpf::make_sequence_recipe<decltype(k0)>(pts.begin(), pts.end());
EXPECT_EQ(open(
dpf::eval_sequence_inner_product<0, 1>(k0, recipe, pts.begin(), pts.end(), rows),
dpf::eval_sequence_inner_product<0, 1>(k1, recipe, pts.begin(), pts.end(), rows)),
expect);
}
TEST(InnerProduct, WrapInterval)
{
auto [k0, k1] = dpf::make_dpf(In{255}, std::uint32_t{4});
const In from = 250, to = 2;
std::vector<std::uint32_t> w;
std::uint64_t expect = 0;
auto push = [&](In x) {
w.push_back(static_cast<std::uint32_t>(w.size() + 1));
const auto y = open(*dpf::eval_point(k0, x), *dpf::eval_point(k1, x));
expect += static_cast<std::uint64_t>(y) * w.back();
};
for (unsigned x = from; x < 256; ++x)
push(static_cast<In>(x));
for (unsigned x = 0; x <= to; ++x)
push(static_cast<In>(x));
EXPECT_EQ(w.size(), 9u);
EXPECT_EQ(open(
dpf::eval_inner_product(dpf::paired, k0, from, to, w),
dpf::eval_inner_product(dpf::paired, k1, from, to, w)),
expect);
EXPECT_EQ(expect, std::uint64_t{4} * w[5]); // 255 is the 6th point, index 5
}
TEST(InnerProduct, XorWeights)
{
using X = dpf::xor_wrapper<std::uint32_t>;
auto [k0, k1] = dpf::make_dpf(In{3}, X{0x0fu});
const std::vector<In> pts{1, 3, 4};
const std::vector<X> w{X{0xffu}, X{0xf0u}, X{0x0fu}};
// Only x=3 is hot: 0x0f AND 0xf0.
EXPECT_EQ(open(
dpf::eval_sequence_inner_product<0>(k0, pts.begin(), pts.end(), w),
dpf::eval_sequence_inner_product<0>(k1, pts.begin(), pts.end(), w)),
X{0x0fu & 0xf0u});
}
TEST(InnerProduct, EmptySequenceIsZero)
{
auto [k0, k1] = dpf::make_dpf(In{1}, std::uint64_t{3});
const std::vector<In> pts;
const std::vector<std::uint64_t> w;
EXPECT_EQ(open(
dpf::eval_sequence_inner_product<0>(k0, pts.begin(), pts.end(), w),
dpf::eval_sequence_inner_product<0>(k1, pts.begin(), pts.end(), w)),
std::uint64_t{0});
}
TEST(InnerProduct, UnsortedSequenceThrows)
{
auto [k0, k1] = dpf::make_dpf(In{1}, std::uint64_t{3});
(void)k1;
const std::vector<In> pts{3, 1};
const std::vector<std::uint64_t> w{1, 1};
EXPECT_THROW(
dpf::eval_sequence_inner_product<0>(k0, pts.begin(), pts.end(), w),
std::runtime_error);
}
TEST(InnerProduct, ColumnsThreeMomentsOneWalk)
{
const In alpha = 42;
const std::uint64_t beta = 7;
auto [k0, k1] = dpf::make_dpf(alpha, beta);
const std::vector<In> pts{40, 41, 42, 50};
std::vector<std::uint64_t> r(pts.size());
std::vector<std::uint64_t> r2(pts.size());
std::vector<std::uint64_t> ones(pts.size(), 1);
std::uint64_t expect_sum = 0, expect_dot = 0, expect_sq = 0;
for (std::size_t i = 0; i < pts.size(); ++i)
{
r[i] = 3 + i * 5;
r2[i] = r[i] * r[i];
const auto y = open(*dpf::eval_point(k0, pts[i]), *dpf::eval_point(k1, pts[i]));
expect_sum += static_cast<std::uint64_t>(y);
expect_dot += static_cast<std::uint64_t>(y) * r[i];
expect_sq += static_cast<std::uint64_t>(y) * r2[i];
}
const auto streams = std::tie(ones, r, r2);
const auto s0 = dpf::eval_sequence_inner_product<0>(
dpf::columns, k0, pts.begin(), pts.end(), streams);
const auto s1 = dpf::eval_sequence_inner_product<0>(
dpf::columns, k1, pts.begin(), pts.end(), streams);
EXPECT_EQ(open(std::get<0>(s0), std::get<0>(s1)), expect_sum);
EXPECT_EQ(open(std::get<1>(s0), std::get<1>(s1)), expect_dot);
EXPECT_EQ(open(std::get<2>(s0), std::get<2>(s1)), expect_sq);
EXPECT_EQ(expect_sum, beta);
EXPECT_EQ(expect_dot, beta * r[2]);
EXPECT_EQ(expect_sq, beta * r2[2]);
}
TEST(InnerProduct, ColumnsCallableStreamsAndSideVisit)
{
auto [k0, k1] = dpf::make_dpf(In{9}, std::uint32_t{4});
const std::vector<In> pts{1, 9, 12};
auto ones = [](std::size_t) { return std::uint32_t{1}; };
auto scale = [](std::size_t i) { return std::uint32_t(i + 2); };
std::size_t visits = 0;
std::uint32_t seen = 0;
const auto s0 = dpf::eval_sequence_inner_product<0>(
dpf::columns, k0, pts.begin(), pts.end(), std::tie(ones, scale),
dpf::project([](auto share) { return share + share; }),
dpf::also([&](std::size_t, In x, auto share) {
++visits;
if (x == In{9})
seen = share.raw();
}));
const auto s1 = dpf::eval_sequence_inner_product<0>(
dpf::columns, k1, pts.begin(), pts.end(), std::tie(ones, scale),
dpf::project([](auto share) { return share + share; }));
EXPECT_EQ(visits, pts.size());
const auto hot = *dpf::eval_point(k0, In{9});
EXPECT_EQ(seen, hot.raw());
// project doubles the share, so the opened moments are 2 * beta * weight.
EXPECT_EQ(open(std::get<0>(s0), std::get<0>(s1)), std::uint64_t{8});
EXPECT_EQ(open(std::get<1>(s0), std::get<1>(s1)), std::uint64_t{8} * 3u);
}
TEST(InnerProduct, ColumnsIntervalMatchesSequence)
{
auto [k0, k1] = dpf::make_dpf(In{4}, std::uint16_t{6});
const In from = 2, to = 6;
std::vector<std::uint16_t> w;
for (In x = from;; ++x)
{
w.push_back(static_cast<std::uint16_t>(x));
if (x == to)
break;
}
const auto a0 = dpf::eval_inner_product<0>(dpf::columns, k0, from, to, std::tie(w));
const auto a1 = dpf::eval_inner_product<0>(dpf::columns, k1, from, to, std::tie(w));
std::vector<In> pts;
for (In x = from;; ++x)
{
pts.push_back(x);
if (x == to)
break;
}
const auto b0 = dpf::eval_sequence_inner_product<0>(
dpf::columns, k0, pts.begin(), pts.end(), std::tie(w));
const auto b1 = dpf::eval_sequence_inner_product<0>(
dpf::columns, k1, pts.begin(), pts.end(), std::tie(w));
EXPECT_EQ(open(std::get<0>(a0), std::get<0>(a1)),
open(std::get<0>(b0), std::get<0>(b1)));
EXPECT_EQ(open(std::get<0>(a0), std::get<0>(a1)), std::uint16_t{6} * 4);
}
TEST(InnerProduct, RecipeLengthMismatchThrows)
{
auto [k0, k1] = dpf::make_dpf(In{1}, std::uint64_t{3});
(void)k1;
const std::vector<In> pts{1, 2};
const std::vector<In> shorter{1};
const std::vector<std::uint64_t> w{1, 1};
const auto recipe = dpf::make_sequence_recipe<decltype(k0)>(pts.begin(), pts.end());
EXPECT_THROW(
dpf::eval_sequence_inner_product<0>(k0, recipe, shorter.begin(), shorter.end(), w),
std::invalid_argument);
}
TEST(InnerProduct, ColumnsUnsortedAndRecipeMismatchThrow)
{
auto [k0, k1] = dpf::make_dpf(In{1}, std::uint64_t{3});
(void)k1;
const std::vector<In> unsorted{3, 1};
const std::vector<std::uint64_t> w{1, 1};
EXPECT_THROW(
dpf::eval_sequence_inner_product<0>(
dpf::columns, k0, unsorted.begin(), unsorted.end(), std::tie(w)),
std::runtime_error);
const std::vector<In> pts{1, 2};
const std::vector<In> shorter{1};
const auto recipe = dpf::make_sequence_recipe<decltype(k0)>(pts.begin(), pts.end());
EXPECT_THROW(
dpf::eval_sequence_inner_product<0>(
dpf::columns, k0, recipe, shorter.begin(), shorter.end(), std::tie(w)),
std::invalid_argument);
}
TEST(InnerProduct, BatchedLeafWalkMatchesPaired)
{
// Plain `eval_inner_product` (memoized leaf walk) and `dpf::paired` both
// compute sum_x DPF(x)*w[x] when there is one output and `[from, to]` is
// leaf-aligned, so the covering-leaf weight layout matches the clipped
// domain points.
const In alpha = 36;
const std::uint64_t beta = 11;
auto [k0, k1] = dpf::make_dpf(alpha, beta);
const In from = 30, to = 45; // even..odd => full covering leaves
ASSERT_EQ(decltype(k0)::outputs_per_leaf, 2u);
std::vector<std::uint64_t> w(to - from + 1);
for (std::size_t i = 0; i < w.size(); ++i)
w[i] = (i * 7u) + 3u;
auto m0 = dpf::make_basic_interval_memoizer(k0, from, to);
auto m1 = dpf::make_basic_interval_memoizer(k1, from, to);
const auto leaf0 = dpf::eval_inner_product(k0, from, to, w, m0);
const auto leaf1 = dpf::eval_inner_product(k1, from, to, w, m1);
const auto pair0 = dpf::eval_inner_product(dpf::paired, k0, from, to, w);
const auto pair1 = dpf::eval_inner_product(dpf::paired, k1, from, to, w);
EXPECT_EQ(open(leaf0, leaf1), open(pair0, pair1));
EXPECT_EQ(open(leaf0, leaf1), beta * w[alpha - from]);
}
TEST(InnerProduct, ColumnsOneStreamMatchesPaired)
{
// Transposed (`columns`) with a single weight stream is the same scalar
// product as `paired` on that stream.
auto [k0, k1] = dpf::make_dpf(In{12}, std::uint32_t{5});
const In from = 8, to = 20;
std::vector<std::uint32_t> w;
for (In x = from;; ++x)
{
w.push_back(static_cast<std::uint32_t>(x + 1));
if (x == to)
break;
}
const auto p0 = dpf::eval_inner_product(dpf::paired, k0, from, to, w);
const auto p1 = dpf::eval_inner_product(dpf::paired, k1, from, to, w);
const auto c0 = dpf::eval_inner_product<0>(dpf::columns, k0, from, to, std::tie(w));
const auto c1 = dpf::eval_inner_product<0>(dpf::columns, k1, from, to, std::tie(w));
EXPECT_EQ(open(p0, p1), open(std::get<0>(c0), std::get<0>(c1)));
EXPECT_EQ(open(p0, p1), std::uint32_t{5} * w[12 - 8]);
}
TEST(InnerProduct, LengthOneOddAndUnaligned)
{
auto [k0, k1] = dpf::make_dpf(In{41}, std::uint64_t{9});
// Single domain point.
{
const In x = 41;
std::vector<std::uint64_t> w{4};
EXPECT_EQ(open(
dpf::eval_inner_product(dpf::paired, k0, x, x, w),
dpf::eval_inner_product(dpf::paired, k1, x, x, w)),
std::uint64_t{9} * 4u);
const auto c0 = dpf::eval_inner_product<0>(
dpf::columns, k0, x, x, std::tie(w));
const auto c1 = dpf::eval_inner_product<0>(
dpf::columns, k1, x, x, std::tie(w));
EXPECT_EQ(open(std::get<0>(c0), std::get<0>(c1)), std::uint64_t{9} * 4u);
}
// Odd length, not leaf-aligned (uint64 packs two lanes per leaf).
// `paired` / `columns` weight by clipped domain points; the batched leaf
// walk weights the covering leaves (see file brief / cohort docs).
{
const In from = 39, to = 45; // 7 points; covering leaf also holds 38
std::vector<std::uint64_t> w(to - from + 1);
std::uint64_t expect = 0;
for (std::size_t i = 0; i < w.size(); ++i)
{
w[i] = i + 2;
const In x = static_cast<In>(from + i);
const auto y = open(*dpf::eval_point(k0, x), *dpf::eval_point(k1, x));
expect += static_cast<std::uint64_t>(y) * w[i];
}
EXPECT_EQ(open(
dpf::eval_inner_product(dpf::paired, k0, from, to, w),
dpf::eval_inner_product(dpf::paired, k1, from, to, w)),
expect);
const auto c0 = dpf::eval_inner_product<0>(
dpf::columns, k0, from, to, std::tie(w));
const auto c1 = dpf::eval_inner_product<0>(
dpf::columns, k1, from, to, std::tie(w));
EXPECT_EQ(open(std::get<0>(c0), std::get<0>(c1)), expect);
EXPECT_EQ(expect, std::uint64_t{9} * w[41 - 39]);
// Covering-leaf weights: pad the missing start lane (x=38) with 0 so
// the batched walk agrees with the clipped paired result.
constexpr std::size_t opl = decltype(k0)::outputs_per_leaf;
ASSERT_EQ(opl, 2u);
std::vector<std::uint64_t> cover(w.size() + 1, 0);
for (std::size_t i = 0; i < w.size(); ++i)
cover[i + 1] = w[i];
auto m0 = dpf::make_basic_interval_memoizer(k0, from, to);
auto m1 = dpf::make_basic_interval_memoizer(k1, from, to);
EXPECT_EQ(open(
dpf::eval_inner_product(k0, from, to, cover, m0),
dpf::eval_inner_product(k1, from, to, cover, m1)),
expect);
}
}
TEST(InnerProduct, EmptyIntervalWeightsStillZero)
{
// Empty point list already covered; empty closed interval is impossible,
// but a sequence of length 0 for columns must stay zero.
auto [k0, k1] = dpf::make_dpf(In{1}, std::uint64_t{3});
const std::vector<In> pts;
const std::vector<std::uint64_t> w;
const auto c0 = dpf::eval_sequence_inner_product<0>(
dpf::columns, k0, pts.begin(), pts.end(), std::tie(w));
const auto c1 = dpf::eval_sequence_inner_product<0>(
dpf::columns, k1, pts.begin(), pts.end(), std::tie(w));
EXPECT_EQ(open(std::get<0>(c0), std::get<0>(c1)), std::uint64_t{0});
}
TEST(InnerProduct, ColumnsFullAndAlsoThenProject)
{
auto [k0, k1] = dpf::make_dpf(In{7}, std::uint16_t{4});
std::vector<std::uint16_t> ones(256, 1);
std::vector<std::uint16_t> r(256);
for (unsigned i = 0; i < 256; ++i)
r[i] = static_cast<std::uint16_t>((i * 3u) & 15u);
const auto f0 = dpf::eval_full_inner_product<0>(
dpf::columns, k0, std::tie(ones, r));
const auto f1 = dpf::eval_full_inner_product<0>(
dpf::columns, k1, std::tie(ones, r));
EXPECT_EQ(open(std::get<0>(f0), std::get<0>(f1)), std::uint16_t{4});
EXPECT_EQ(open(std::get<1>(f0), std::get<1>(f1)), std::uint16_t{4} * r[7]);
const std::vector<In> pts{3, 7, 9};
std::size_t visits = 0;
const auto s0 = dpf::eval_sequence_inner_product<0>(
dpf::columns, k0, pts.begin(), pts.end(), std::tie(ones),
dpf::also([&](std::size_t, In, auto) { ++visits; }),
dpf::project([](auto share) { return share; }));
const auto s1 = dpf::eval_sequence_inner_product<0>(
dpf::columns, k1, pts.begin(), pts.end(), std::tie(ones));
EXPECT_EQ(visits, pts.size());
EXPECT_EQ(open(std::get<0>(s0), std::get<0>(s1)), std::uint16_t{4});
}
TEST(InnerProduct, PrepareMemoizerThenInnerProduct)
{
// uint64 packs two lanes per leaf. [10, 31] fills those leaves, so a
// weight per domain point is also a weight per covering lane.
auto [k0, k1] = dpf::make_dpf(In{20}, std::uint64_t{6});
const In from = 10, to = 31;
ASSERT_EQ(decltype(k0)::outputs_per_leaf, 2u);
ASSERT_EQ(static_cast<unsigned>(to - from + 1) % 2u, 0u);
std::vector<std::uint64_t> w(to - from + 1, 2);
auto cold0 = dpf::eval_inner_product(k0, from, to, w,
dpf::make_basic_interval_memoizer(k0, from, to));
auto cold1 = dpf::eval_inner_product(k1, from, to, w,
dpf::make_basic_interval_memoizer(k1, from, to));
auto m0 = dpf::make_basic_interval_memoizer(k0, from, to);
auto m1 = dpf::make_basic_interval_memoizer(k1, from, to);
dpf::eval_prepare_interval(k0, from, to, m0);
dpf::eval_prepare_interval(k1, from, to, m1);
const auto warm0 = dpf::eval_inner_product(k0, from, to, w, m0);
const auto warm1 = dpf::eval_inner_product(k1, from, to, w, m1);
EXPECT_EQ(warm0, cold0);
EXPECT_EQ(warm1, cold1);
EXPECT_EQ(open(warm0, warm1), std::uint64_t{6} * 2u);
// A second pass on the warm memoizer must not rebuild a different share.
EXPECT_EQ(dpf::eval_inner_product(k0, from, to, w, m0), warm0);
EXPECT_EQ(dpf::eval_inner_product(k1, from, to, w, m1), warm1);
}
TEST(InnerProduct, IntervalMatchesPointReconstruction)
{
// Long interval: reopen against point-by-point reconstruction for both
// paired and columns (two streams).
const In alpha = 100;
const std::uint64_t beta = 13;
auto [k0, k1] = dpf::make_dpf(alpha, beta);
const In from = 80, to = 140;
const std::size_t n = static_cast<std::size_t>(to - from + 1);
std::vector<std::uint64_t> ones(n, 1);
std::vector<std::uint64_t> scale(n);
std::uint64_t expect_sum = 0, expect_dot = 0;
for (std::size_t i = 0; i < n; ++i)
{
scale[i] = i + 1;
const In x = static_cast<In>(from + i);
const auto y = open(*dpf::eval_point(k0, x), *dpf::eval_point(k1, x));
expect_sum += static_cast<std::uint64_t>(y);
expect_dot += static_cast<std::uint64_t>(y) * scale[i];
}
EXPECT_EQ(open(
dpf::eval_inner_product(dpf::paired, k0, from, to, scale),
dpf::eval_inner_product(dpf::paired, k1, from, to, scale)),
expect_dot);
const auto c0 = dpf::eval_inner_product<0>(
dpf::columns, k0, from, to, std::tie(ones, scale));
const auto c1 = dpf::eval_inner_product<0>(
dpf::columns, k1, from, to, std::tie(ones, scale));
EXPECT_EQ(open(std::get<0>(c0), std::get<0>(c1)), expect_sum);
EXPECT_EQ(open(std::get<1>(c0), std::get<1>(c1)), expect_dot);
EXPECT_EQ(expect_sum, beta);
EXPECT_EQ(expect_dot, beta * scale[alpha - from]);
}
TEST(InnerProduct, ShortCoveringWeightsThrow)
{
auto [k0, k1] = dpf::make_dpf(In{20}, std::uint64_t{6});
(void)k1;
// [10, 30] is 21 points and 22 covering lanes (the leaf of 30 also holds 31).
const In from = 10, to = 30;
std::vector<std::uint64_t> clipped(to - from + 1, 2);
auto m0 = dpf::make_basic_interval_memoizer(k0, from, to);
EXPECT_THROW(dpf::eval_inner_product(k0, from, to, clipped, m0),
std::invalid_argument);
dpf::eval_prepare_interval(k0, from, to, m0);
EXPECT_THROW(dpf::eval_inner_product(k0, from, to, clipped, m0),
std::invalid_argument);
std::vector<std::uint64_t> cover(clipped.size() + 1, 2);
EXPECT_EQ(open(
dpf::eval_inner_product(k0, from, to, cover, m0),
dpf::eval_inner_product(k1, from, to, cover,
dpf::make_basic_interval_memoizer(k1, from, to))),
std::uint64_t{6} * 2u);
}
TEST(InnerProduct, OneLaneShortStillThrows)
{
auto [k0, k1] = dpf::make_dpf(In{4}, std::uint64_t{1});
(void)k1;
const In from = 0, to = 2; // 3 points, 4 covering lanes
std::vector<std::uint64_t> almost(3, 1);
auto memo = dpf::make_basic_interval_memoizer(k0, from, to);
EXPECT_THROW(dpf::eval_inner_product(k0, from, to, almost, memo),
std::invalid_argument);
almost.push_back(0);
EXPECT_NO_THROW(dpf::eval_inner_product(k0, from, to, almost, memo));
}
TEST(InnerProduct, MemoizerSmallerThanIntervalThrows)
{
auto [k0, k1] = dpf::make_dpf(In{8}, std::uint64_t{3});
(void)k1;
auto small = dpf::make_basic_interval_memoizer(k0, In{0}, In{1});
std::vector<std::uint64_t> w(64, 1);
EXPECT_THROW(dpf::eval_inner_product(k0, In{0}, In{30}, w, small),
std::length_error);
EXPECT_THROW(dpf::eval_prepare_interval(k0, In{0}, In{30}, small),
std::length_error);
}
TEST(InnerProduct, PairedAndColumnsRejectShortWeights)
{
auto [k0, k1] = dpf::make_dpf(In{5}, std::uint64_t{2});
(void)k1;
const In from = 1, to = 8;
std::vector<std::uint64_t> short_w(3, 1);
EXPECT_THROW(dpf::eval_inner_product(dpf::paired, k0, from, to, short_w),
std::invalid_argument);
EXPECT_THROW(dpf::eval_inner_product<0>(dpf::columns, k0, from, to,
std::tie(short_w)), std::invalid_argument);
const std::vector<In> pts{1, 5, 8};
const std::vector<std::uint64_t> one_row{1};
EXPECT_THROW(dpf::eval_sequence_inner_product<0>(
k0, pts.begin(), pts.end(), one_row), std::invalid_argument);
const std::vector<std::uint64_t> one{1};
EXPECT_THROW(dpf::eval_sequence_inner_product<0>(
dpf::columns, k0, pts.begin(), pts.end(), std::tie(one)),
std::invalid_argument);
}
TEST(InnerProduct, PrepareOnAWrapStillMatchesPoints)
{
auto [k0, k1] = dpf::make_dpf(In{1}, std::uint32_t{9});
const In from = 250, to = 4;
std::vector<std::uint32_t> w;
std::uint64_t expect = 0;
for (unsigned x = from; x < 256; ++x)
{
w.push_back(static_cast<std::uint32_t>(w.size() + 1));
const auto y = open(*dpf::eval_point(k0, static_cast<In>(x)),
*dpf::eval_point(k1, static_cast<In>(x)));
expect += static_cast<std::uint64_t>(y) * w.back();
}
for (unsigned x = 0; x <= to; ++x)
{
w.push_back(static_cast<std::uint32_t>(w.size() + 1));
const auto y = open(*dpf::eval_point(k0, static_cast<In>(x)),
*dpf::eval_point(k1, static_cast<In>(x)));
expect += static_cast<std::uint64_t>(y) * w.back();
}
auto m0 = dpf::make_basic_interval_memoizer(k0, from, to);
auto m1 = dpf::make_basic_interval_memoizer(k1, from, to);
dpf::eval_prepare_interval(k0, from, to, m0);
dpf::eval_prepare_interval(k1, from, to, m1);
EXPECT_EQ(open(
dpf::eval_inner_product(dpf::paired, k0, from, to, w),
dpf::eval_inner_product(dpf::paired, k1, from, to, w)),
expect);
// A wrap is two leaf segments. The memoizer keeps one, so prepare must
// leave the paired product unchanged and must not throw.
EXPECT_EQ(open(
dpf::eval_inner_product(dpf::paired, k0, from, to, w),
dpf::eval_inner_product(dpf::paired, k1, from, to, w)),
expect);
std::vector<std::uint32_t> too_short(w.size() - 1, 1);
EXPECT_THROW(
dpf::eval_inner_product(dpf::paired, k0, from, to, too_short),
std::invalid_argument);
(void)m0;
(void)m1;
}
TEST(InnerProduct, BatchedWalkMatchesIntervalBuffer)
{
auto [k0, k1] = dpf::make_dpf(In{20}, std::uint64_t{6});
const In from = 10, to = 30;
auto [buf0, it0] = dpf::eval_interval(k0, from, to);
auto [buf1, it1] = dpf::eval_interval(k1, from, to);
(void)it0;
(void)it1;
ASSERT_GT(buf0.size(), static_cast<std::size_t>(to - from + 1));
std::vector<std::uint64_t> w(buf0.size());
for (std::size_t i = 0; i < w.size(); ++i)
w[i] = (i * 3u) + 1u;
std::uint64_t e0 = 0, e1 = 0, opened = 0;
for (std::size_t i = 0; i < w.size(); ++i)
{
e0 += static_cast<std::uint64_t>(buf0[i].value) * w[i];
e1 += static_cast<std::uint64_t>(buf1[i].value) * w[i];
opened += static_cast<std::uint64_t>(open(buf0[i], buf1[i])) * w[i];
}
auto m0 = dpf::make_basic_interval_memoizer(k0, from, to);
auto m1 = dpf::make_basic_interval_memoizer(k1, from, to);
dpf::eval_prepare_interval(k0, from, to, m0);
dpf::eval_prepare_interval(k1, from, to, m1);
const auto ip0 = dpf::eval_inner_product(k0, from, to, w, m0);
const auto ip1 = dpf::eval_inner_product(k1, from, to, w, m1);
EXPECT_EQ(ip0, e0);
EXPECT_EQ(ip1, e1);
EXPECT_EQ(open(ip0, ip1), opened);
std::size_t hot = w.size();
for (std::size_t i = 0; i < w.size(); ++i)
{
if (open(buf0[i], buf1[i]) != 0)
{
EXPECT_EQ(hot, w.size());
hot = i;
}
}
ASSERT_LT(hot, w.size());
EXPECT_EQ(opened, std::uint64_t{6} * w[hot]);
}
TEST(InnerProduct, FullDomainBatchedRejectsAShortVector)
{
auto [k0, k1] = dpf::make_dpf(In{3}, std::uint64_t{1});
(void)k1;
std::vector<std::uint64_t> w(255, 1);
auto memo = dpf::make_basic_full_memoizer(k0);
EXPECT_THROW(dpf::eval_full_inner_product(k0, w, memo),
std::invalid_argument);
w.push_back(1);
EXPECT_EQ(open(
dpf::eval_full_inner_product(k0, w, memo),
dpf::eval_full_inner_product(k1, w,
dpf::make_basic_full_memoizer(k1))),
std::uint64_t{1} * w[3]);
}
TEST(InnerProduct, TwoOutputRowsRejectAShortList)
{
auto [k0, k1] = dpf::make_dpf(In{4}, std::uint16_t{2}, std::uint16_t{6});
(void)k1;
const std::vector<In> pts{1, 4, 8};
std::vector<std::array<std::uint32_t, 2>> rows{{1u, 1u}};
EXPECT_THROW((dpf::eval_sequence_inner_product<0, 1>(
k0, pts.begin(), pts.end(), rows)), std::invalid_argument);
rows.push_back({3u, 5u});
rows.push_back({7u, 9u});
EXPECT_NO_THROW((dpf::eval_sequence_inner_product<0, 1>(
k0, pts.begin(), pts.end(), rows)));
}

View file

@ -16,7 +16,13 @@ auto recon(const A & a, const B & b)
&& dpf::is_secret_share_v<std::decay_t<B>>)
return dpf::reconstruct(a, b);
else
return a - b;
{
using T = std::common_type_t<std::decay_t<A>, std::decay_t<B>>;
if constexpr (std::is_integral_v<T> && std::is_unsigned_v<T>)
return static_cast<T>(a - b);
else
return a - b;
}
}
} // namespace
@ -289,6 +295,13 @@ using Types = testing::Types
test_type<uint16_t, dpf::bitstring<150>>,
test_type<uint16_t, dpf::xor_wrapper<int64_t>>,
test_type<uint16_t, dpf::xor_wrapper<uint64_t>>,
test_type<uint16_t, dpf::gf2>,
test_type<uint16_t, dpf::gf22>,
test_type<uint16_t, dpf::gf24>,
test_type<uint16_t, dpf::gf28>,
test_type<uint16_t, dpf::gf216>,
test_type<uint16_t, dpf::gf232>,
test_type<uint16_t, dpf::gf264>,
// custom types
test_type<custom_input_type, uint64_t>,

View file

@ -17,7 +17,13 @@ auto recon(const A & a, const B & b)
&& dpf::is_secret_share_v<std::decay_t<B>>)
return dpf::reconstruct(a, b);
else
return a - b;
{
using T = std::common_type_t<std::decay_t<A>, std::decay_t<B>>;
if constexpr (std::is_integral_v<T> && std::is_unsigned_v<T>)
return static_cast<T>(a - b);
else
return a - b;
}
}
} // namespace
@ -265,6 +271,13 @@ using Types = testing::Types
test_type<uint16_t, dpf::bitstring<150>>,
test_type<uint16_t, dpf::xor_wrapper<int64_t>>,
test_type<uint16_t, dpf::xor_wrapper<uint64_t>>,
test_type<uint16_t, dpf::gf2>,
test_type<uint16_t, dpf::gf22>,
test_type<uint16_t, dpf::gf24>,
test_type<uint16_t, dpf::gf28>,
test_type<uint16_t, dpf::gf216>,
test_type<uint16_t, dpf::gf232>,
test_type<uint16_t, dpf::gf264>,
// custom types
test_type<custom_input_type, uint64_t>,

View file

@ -14,7 +14,13 @@ auto recon(const A & a, const B & b)
&& dpf::is_secret_share_v<std::decay_t<B>>)
return dpf::reconstruct(a, b);
else
return a - b;
{
using T = std::common_type_t<std::decay_t<A>, std::decay_t<B>>;
if constexpr (std::is_integral_v<T> && std::is_unsigned_v<T>)
return static_cast<T>(a - b);
else
return a - b;
}
}
} // namespace
@ -128,7 +134,11 @@ TYPED_TEST_P(EvalPointMultiTest, Basic)
for (auto [x, y0, y1, y2, y3] : this->params)
{
auto [dpf0, dpf1] = dpf::make_dpf(x, y0, y1, y2, y3);
auto _keys = dpf::make_dpf(x, y0, y1, y2, y3);
auto & dpf0 = std::get<0>(_keys);
auto & dpf1 = std::get<1>(_keys);
this->assert_wrapper(x, y0, y1, y2, y3,
[&dpf0](input_type cur)
@ -152,7 +162,11 @@ TYPED_TEST_P(EvalPointMultiTest, BasicPathMemoizer)
for (auto [x, y0, y1, y2, y3] : this->params)
{
auto [dpf0, dpf1] = dpf::make_dpf(x, y0, y1, y2, y3);
auto _keys = dpf::make_dpf(x, y0, y1, y2, y3);
auto & dpf0 = std::get<0>(_keys);
auto & dpf1 = std::get<1>(_keys);
this->assert_wrapper(x, y0, y1, y2, y3,
[&dpf0, &memo0](input_type cur)
@ -176,7 +190,11 @@ TYPED_TEST_P(EvalPointMultiTest, NonmemoizingPathMemoizer)
for (auto [x, y0, y1, y2, y3] : this->params)
{
auto [dpf0, dpf1] = dpf::make_dpf(x, y0, y1, y2, y3);
auto _keys = dpf::make_dpf(x, y0, y1, y2, y3);
auto & dpf0 = std::get<0>(_keys);
auto & dpf1 = std::get<1>(_keys);
this->assert_wrapper(x, y0, y1, y2, y3,
[&dpf0, &memo0](input_type cur)
@ -222,6 +240,13 @@ using Types = testing::Types
test_type<uint16_t, dpf::bitstring<150>>,
test_type<uint16_t, dpf::xor_wrapper<int64_t>>,
test_type<uint16_t, dpf::xor_wrapper<uint64_t>>,
test_type<uint16_t, dpf::gf2>,
test_type<uint16_t, dpf::gf22>,
test_type<uint16_t, dpf::gf24>,
test_type<uint16_t, dpf::gf28>,
test_type<uint16_t, dpf::gf216>,
test_type<uint16_t, dpf::gf232>,
test_type<uint16_t, dpf::gf264>,
// custom types
test_type<custom_input_type, uint64_t>,

View file

@ -16,7 +16,13 @@ auto recon(const A & a, const B & b)
&& dpf::is_secret_share_v<std::decay_t<B>>)
return dpf::reconstruct(a, b);
else
return a - b;
{
using T = std::common_type_t<std::decay_t<A>, std::decay_t<B>>;
if constexpr (std::is_integral_v<T> && std::is_unsigned_v<T>)
return static_cast<T>(a - b);
else
return a - b;
}
}
} // namespace
@ -110,7 +116,11 @@ TYPED_TEST_P(EvalPointTest, Basic)
for (auto [x, y] : this->params)
{
auto [dpf0, dpf1] = dpf::make_dpf(x, y);
auto _keys = dpf::make_dpf(x, y);
auto & dpf0 = std::get<0>(_keys);
auto & dpf1 = std::get<1>(_keys);
this->assert_wrapper(x, y,
[&dpf0](input_type cur)
@ -131,7 +141,11 @@ TYPED_TEST_P(EvalPointTest, BasicPathMemoizer)
for (auto [x, y] : this->params)
{
auto [dpf0, dpf1] = dpf::make_dpf(x, y);
auto _keys = dpf::make_dpf(x, y);
auto & dpf0 = std::get<0>(_keys);
auto & dpf1 = std::get<1>(_keys);
using key0_t = std::decay_t<decltype(dpf0)>;
using key1_t = std::decay_t<decltype(dpf1)>;
auto memo0 = dpf::make_basic_path_memoizer<key0_t>(),
@ -156,7 +170,11 @@ TYPED_TEST_P(EvalPointTest, NonmemoizingPathMemoizer)
for (auto [x, y] : this->params)
{
auto [dpf0, dpf1] = dpf::make_dpf(x, y);
auto _keys = dpf::make_dpf(x, y);
auto & dpf0 = std::get<0>(_keys);
auto & dpf1 = std::get<1>(_keys);
using key0_t = std::decay_t<decltype(dpf0)>;
using key1_t = std::decay_t<decltype(dpf1)>;
auto memo0 = dpf::make_nonmemoizing_path_memoizer<key0_t>(),
@ -206,6 +224,13 @@ using Types = testing::Types
test_type<uint16_t, dpf::bitstring<150>>,
test_type<uint16_t, dpf::xor_wrapper<int64_t>>,
test_type<uint16_t, dpf::xor_wrapper<uint64_t>>,
test_type<uint16_t, dpf::gf2>,
test_type<uint16_t, dpf::gf22>,
test_type<uint16_t, dpf::gf24>,
test_type<uint16_t, dpf::gf28>,
test_type<uint16_t, dpf::gf216>,
test_type<uint16_t, dpf::gf232>,
test_type<uint16_t, dpf::gf264>,
// custom types
test_type<custom_input_type, uint64_t>,

View file

@ -16,7 +16,13 @@ auto recon(const A & a, const B & b)
&& dpf::is_secret_share_v<std::decay_t<B>>)
return dpf::reconstruct(a, b);
else
return a - b;
{
using T = std::common_type_t<std::decay_t<A>, std::decay_t<B>>;
if constexpr (std::is_integral_v<T> && std::is_unsigned_v<T>)
return static_cast<T>(a - b);
else
return a - b;
}
}
} // namespace
@ -702,6 +708,13 @@ using Types = testing::Types
test_type<uint16_t, dpf::bitstring<150>>,
test_type<uint16_t, dpf::xor_wrapper<int64_t>>,
test_type<uint16_t, dpf::xor_wrapper<uint64_t>>,
test_type<uint16_t, dpf::gf2>,
test_type<uint16_t, dpf::gf22>,
test_type<uint16_t, dpf::gf24>,
test_type<uint16_t, dpf::gf28>,
test_type<uint16_t, dpf::gf216>,
test_type<uint16_t, dpf::gf232>,
test_type<uint16_t, dpf::gf264>,
// custom types
test_type<custom_input_type, uint64_t>,

View file

@ -18,7 +18,13 @@ auto recon(const A & a, const B & b)
&& dpf::is_secret_share_v<std::decay_t<B>>)
return dpf::reconstruct(a, b);
else
return a - b;
{
using T = std::common_type_t<std::decay_t<A>, std::decay_t<B>>;
if constexpr (std::is_integral_v<T> && std::is_unsigned_v<T>)
return static_cast<T>(a - b);
else
return a - b;
}
}
} // namespace
@ -711,6 +717,13 @@ using Types = testing::Types
test_type<uint16_t, dpf::bitstring<150>>,
test_type<uint16_t, dpf::xor_wrapper<int64_t>>,
test_type<uint16_t, dpf::xor_wrapper<uint64_t>>,
test_type<uint16_t, dpf::gf2>,
test_type<uint16_t, dpf::gf22>,
test_type<uint16_t, dpf::gf24>,
test_type<uint16_t, dpf::gf28>,
test_type<uint16_t, dpf::gf216>,
test_type<uint16_t, dpf::gf232>,
test_type<uint16_t, dpf::gf264>,
// custom types
test_type<custom_input_type, uint64_t>,

View file

@ -0,0 +1,138 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <stdexcept>
#include <vector>
#include "dpf.hpp"
namespace
{
auto make_unit16(std::uint16_t alpha)
{
return dpf::make_dpf(alpha, dpf::idpf_ones<16>());
}
} // namespace
TEST(EvalUntil, SpineMatchesEvalPoint)
{
const std::uint16_t alpha = 0xBEEF;
auto [k0, k1] = make_unit16(alpha);
dpf::idpf_eval_ctx ctx0(k0);
dpf::idpf_eval_ctx ctx1(k1);
EXPECT_EQ(ctx0.level(), 0u);
EXPECT_EQ(ctx0.node_count(), 1u);
// Empty prefixes on a fresh context leave the root in place.
auto empty = dpf::eval_until(ctx0, 0, std::vector<std::uint16_t>{});
EXPECT_TRUE(empty.empty());
EXPECT_EQ(ctx0.node_count(), 1u);
std::uint16_t prefix = 0;
for (std::size_t level = 1; level <= 16; ++level)
{
const auto bit = static_cast<std::uint16_t>(
(alpha >> (16 - level)) & 1u);
prefix = static_cast<std::uint16_t>((prefix << 1) | bit);
auto s0 = dpf::eval_until(ctx0, level,
std::vector<std::uint16_t>{prefix});
auto s1 = dpf::eval_until(ctx1, level,
std::vector<std::uint16_t>{prefix});
ASSERT_EQ(s0.size(), 1u);
ASSERT_EQ(s1.size(), 1u);
EXPECT_EQ(ctx0.node_count(), 1u);
EXPECT_LE(ctx0.node_count(), level);
const auto domain = static_cast<std::uint16_t>(
prefix << (16 - level));
// Dispatch eval_point through a level switch for the matching out<I,N>.
std::uint64_t ref = 0;
switch (level)
{
#define LIBDPF_CHECK_LEVEL(L) \
case L: \
ref = dpf::reconstruct( \
*dpf::eval_point(dpf::out<L - 1, L>, k0, domain), \
*dpf::eval_point(dpf::out<L - 1, L>, k1, domain)); \
break
LIBDPF_CHECK_LEVEL(1); LIBDPF_CHECK_LEVEL(2); LIBDPF_CHECK_LEVEL(3);
LIBDPF_CHECK_LEVEL(4); LIBDPF_CHECK_LEVEL(5); LIBDPF_CHECK_LEVEL(6);
LIBDPF_CHECK_LEVEL(7); LIBDPF_CHECK_LEVEL(8); LIBDPF_CHECK_LEVEL(9);
LIBDPF_CHECK_LEVEL(10); LIBDPF_CHECK_LEVEL(11); LIBDPF_CHECK_LEVEL(12);
LIBDPF_CHECK_LEVEL(13); LIBDPF_CHECK_LEVEL(14); LIBDPF_CHECK_LEVEL(15);
LIBDPF_CHECK_LEVEL(16);
#undef LIBDPF_CHECK_LEVEL
default:
FAIL() << "bad level";
}
EXPECT_EQ(dpf::reconstruct(s0[0], s1[0]), ref);
EXPECT_EQ(ref, 1u);
}
}
TEST(EvalUntil, RejectsNonExtendingPrefix)
{
auto [k0, k1] = make_unit16(0x0001);
(void)k1;
dpf::idpf_eval_ctx ctx(k0);
dpf::eval_until(ctx, 1, std::vector<std::uint16_t>{0});
EXPECT_THROW(
dpf::eval_until(ctx, 2, std::vector<std::uint16_t>{3}),
std::invalid_argument);
}
TEST(EvalUntil, BothChildrenStayLinear)
{
auto [k0, k1] = make_unit16(0xF00D);
dpf::idpf_eval_ctx ctx0(k0);
dpf::idpf_eval_ctx ctx1(k1);
auto s0 = dpf::eval_until(ctx0, 1, std::vector<std::uint16_t>{0, 1});
auto s1 = dpf::eval_until(ctx1, 1, std::vector<std::uint16_t>{0, 1});
EXPECT_EQ(ctx0.node_count(), 2u);
EXPECT_EQ(dpf::reconstruct(s0[0], s1[0]), 0u);
EXPECT_EQ(dpf::reconstruct(s0[1], s1[1]), 1u);
}
// Seam with the older full-prefix walk: at depth 1, eval_until on {0,1}
// opens the same counts as eval_prefixes(out<0,1>).
TEST(EvalUntil, MatchesEvalPrefixesAtDepthOne)
{
const std::uint16_t alpha = 0x8000;
auto [k0, k1] = make_unit16(alpha);
dpf::idpf_eval_ctx ctx0(k0);
dpf::idpf_eval_ctx ctx1(k1);
auto u0 = dpf::eval_until(ctx0, 1, std::vector<std::uint16_t>{0, 1});
auto u1 = dpf::eval_until(ctx1, 1, std::vector<std::uint16_t>{0, 1});
auto [b0, it0] = dpf::eval_prefixes(dpf::out<0, 1>, k0);
auto [b1, it1] = dpf::eval_prefixes(dpf::out<0, 1>, k1);
(void)b0;
(void)b1;
auto p0 = std::begin(it0);
auto p1 = std::begin(it1);
EXPECT_EQ(dpf::reconstruct(u0[0], u1[0]), dpf::reconstruct(*p0, *p1));
++p0;
++p1;
EXPECT_EQ(dpf::reconstruct(u0[1], u1[1]), dpf::reconstruct(*p0, *p1));
}
TEST(EvalUntil, RetainThenContinue)
{
auto [k0, k1] = make_unit16(0xC000);
dpf::idpf_eval_ctx ctx0(k0);
dpf::idpf_eval_ctx ctx1(k1);
dpf::eval_until(ctx0, 1, std::vector<std::uint16_t>{0, 1});
dpf::eval_until(ctx1, 1, std::vector<std::uint16_t>{0, 1});
ctx0.retain(1);
ctx1.retain(1);
EXPECT_EQ(ctx0.node_count(), 1u);
auto s0 = dpf::eval_until(ctx0, 2, std::vector<std::uint16_t>{2, 3});
auto s1 = dpf::eval_until(ctx1, 2, std::vector<std::uint16_t>{2, 3});
// 0xC000 = 1100… so length-2 prefix is 3 (bits 11).
EXPECT_EQ(dpf::reconstruct(s0[0], s1[0]), 0u);
EXPECT_EQ(dpf::reconstruct(s0[1], s1[1]), 1u);
}

View file

@ -0,0 +1,164 @@
#include <gtest/gtest.h>
#include <tuple>
#include "grotto/exact_steps.hpp"
#include <cmath>
#include <cstdint>
#include <limits>
namespace
{
int ref_bit_width(std::uint16_t bits)
{
if (bits == 0)
return 0;
return 32 - __builtin_clz(static_cast<unsigned>(bits));
}
int ref_countl_one(std::uint16_t bits)
{
int n = 0;
for (int b = 15; b >= 0; --b)
{
if (((bits >> b) & 1u) == 0)
break;
++n;
}
return n;
}
std::uint16_t ref_bit_floor(std::uint16_t bits)
{
if (bits == 0)
return 0;
return std::uint16_t{1} << (ref_bit_width(bits) - 1);
}
std::uint32_t ref_bit_ceil(std::uint16_t bits)
{
if (bits <= 1)
return 1;
if ((bits & (bits - 1u)) == 0)
return bits;
return std::uint32_t{1} << ref_bit_width(bits);
}
int ref_ceil_log(std::int16_t raw, unsigned k, int group)
{
const long double x = std::ldexp(static_cast<long double>(raw < 0 ? -raw : raw), -static_cast<int>(k));
return static_cast<int>(std::ceil(std::log2(static_cast<double>(x)) / group - 1e-15));
}
int ref_logstar(std::int16_t raw, unsigned k)
{
long double x = std::ldexp(static_cast<long double>(raw), -static_cast<int>(k));
int n = 0;
while (x > 1.0L)
{
x = std::log2(static_cast<double>(x));
++n;
if (n > 8)
break;
}
return n;
}
int ref_length(std::int16_t raw, unsigned k, int base)
{
const long double x = std::ldexp(static_cast<long double>(raw < 0 ? -raw : raw), -static_cast<int>(k));
const auto n = static_cast<std::int64_t>(std::floor(static_cast<double>(x)));
if (n <= 0)
return 1;
int digits = 0;
auto m = n;
while (m > 0)
{
m /= base;
++digits;
}
return digits;
}
} // namespace
TEST(ExactSteps, WordBitsMatchTheUnsignedPattern)
{
for (int raw = -32768; raw <= 32767; ++raw)
{
const auto bits = static_cast<std::uint16_t>(raw);
const auto value = static_cast<std::int64_t>(static_cast<std::int16_t>(raw));
EXPECT_EQ(grotto::eval_bit_width<std::int16_t>(value, 0), ref_bit_width(bits));
EXPECT_EQ(grotto::eval_countl_one<std::int16_t>(value, 0), ref_countl_one(bits));
EXPECT_EQ(grotto::eval_has_single_bit<std::int16_t>(value, 0),
((bits != 0 && (bits & (bits - 1u)) == 0) ? 1 : 0));
EXPECT_EQ(grotto::eval_bit_floor<std::int16_t>(value), ref_bit_floor(bits));
EXPECT_EQ(grotto::eval_bit_ceil<std::int16_t>(value), ref_bit_ceil(bits));
}
}
TEST(ExactSteps, IntegerLogsAndLogstar)
{
for (unsigned k : {0u, 4u})
{
for (int raw = 1; raw <= 32767; raw += (k == 0 ? 17 : 3))
{
const auto value = static_cast<std::int64_t>(raw);
EXPECT_EQ(grotto::eval_ilog16<std::int16_t>(value, k) >> k, ref_ceil_log(static_cast<std::int16_t>(raw), k, 4))
<< raw << " k=" << k;
EXPECT_EQ(grotto::eval_ilog256<std::int16_t>(value, k) >> k, ref_ceil_log(static_cast<std::int16_t>(raw), k, 8))
<< raw;
EXPECT_EQ(grotto::eval_logstar<std::int16_t>(value, k) >> k, ref_logstar(static_cast<std::int16_t>(raw), k))
<< raw;
}
}
EXPECT_EQ(grotto::eval_ilog16<std::int16_t>(0, 0), grotto::ilog_of_zero);
EXPECT_EQ(grotto::eval_logstar<std::int16_t>(-4, 0), grotto::ilog_of_zero);
EXPECT_EQ(grotto::eval_logstar<std::int16_t>(1, 0), 0);
EXPECT_EQ(grotto::eval_logstar<std::int16_t>(2, 0), 1);
EXPECT_EQ(grotto::eval_logstar<std::int16_t>(4, 0), 2);
EXPECT_EQ(grotto::eval_logstar<std::int16_t>(16, 0), 3);
}
TEST(ExactSteps, DecimalFloorCeilAndLengths)
{
for (unsigned k : {0u, 4u})
{
for (int raw = -4000; raw <= 4000; raw += 3)
{
const long double x = std::ldexp(static_cast<long double>(raw), -static_cast<int>(k));
const auto floor_raw = static_cast<std::int64_t>(std::floor(static_cast<double>(x)) * std::ldexp(1.0, static_cast<int>(k)));
const auto ceil_raw = static_cast<std::int64_t>(std::ceil(static_cast<double>(x)) * std::ldexp(1.0, static_cast<int>(k)));
EXPECT_EQ(grotto::eval_dec_floor(raw, k), floor_raw) << raw;
EXPECT_EQ(grotto::eval_dec_ceil(raw, k), ceil_raw) << raw;
const auto width = static_cast<std::int16_t>(raw);
EXPECT_EQ(grotto::eval_dec_width(raw, k) >> k, ref_length(width, k, 10));
EXPECT_EQ(grotto::eval_oct_width(raw, k) >> k, ref_length(width, k, 8));
EXPECT_EQ(grotto::eval_b64_width(raw, k) >> k, ref_length(width, k, 64));
const auto digits = std::llabs(static_cast<long long>(std::floor(std::fabs(static_cast<double>(x)))));
EXPECT_EQ(grotto::eval_has_single_digit(raw, k) >> k, digits <= 9 ? 1 : 0);
}
}
}
TEST(ExactSteps, AngleScaleIsTheLinearFactor)
{
const unsigned k = 16;
const std::int64_t deg = 180ll << k;
const auto rad = grotto::eval_deg2rad(deg, k);
const long double want = 180.0L * 3.14159265358979323846L / 180.0L * std::ldexp(1.0L, k);
EXPECT_LE(std::fabsl(static_cast<long double>(rad) - want), 64.0L);
const auto back = grotto::eval_rad2deg(rad, k);
EXPECT_LE(std::llabs(back - deg), 4096);
}
TEST(ExactSteps, StepLutAgreesWithTheScalarOnInt8)
{
const auto lut = grotto::make_exact_step_lut<std::int8_t>(0, [](std::int64_t raw) {
return grotto::eval_bit_width<std::int8_t>(raw, 0);
});
for (int raw = -128; raw <= 127; ++raw)
EXPECT_EQ(lut(static_cast<std::int8_t>(raw)), grotto::eval_bit_width<std::int8_t>(raw, 0));
EXPECT_THROW(grotto::make_exact_step_lut<std::int32_t>(0, [](std::int64_t) { return 0; }), std::invalid_argument);
}

View file

@ -0,0 +1,299 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <string>
#include <thread>
#include <unistd.h>
#include <vector>
#include "dpf.hpp"
#include "dpf/app_flow.hpp"
#include "dpf/app_plans.hpp"
#include "dpf/beaver.hpp"
#include "dpf/buffered_prg.hpp"
#include "dpf/doerner_shelat.hpp"
#include "dpf/experiment.hpp"
#include "dpf/prg_aes.hpp"
#include "dpf/random.hpp"
namespace
{
std::string tmp_dir(const char * tag)
{
const std::string dir =
std::string("/tmp/libdpf_ex_") + tag + "_" + std::to_string(::getpid());
(void)::system(("rm -rf " + dir + " && mkdir -p " + dir).c_str());
return dir;
}
std::size_t count_lines(const std::string & path)
{
std::ifstream in(path);
std::size_t n = 0;
std::string line;
while (std::getline(in, line))
++n;
return n;
}
TEST(Experiment, ReplayMatchesMakeDpfRoots)
{
dpf::experiment::master_seed master{};
std::uint8_t alpha = 7;
using node_t = dpf::prg::aes128::block_type;
node_t root0a{}, root0b{}, root1a{}, root1b{};
{
dpf::experiment ex("dpf");
master = ex.seed();
auto [k0, k1] = dpf::make_dpf(alpha, std::uint64_t{1});
root0a = k0.root();
root1a = k1.root();
}
{
auto ex = dpf::experiment::replay("dpf", master);
auto [k0, k1] = dpf::make_dpf(alpha, std::uint64_t{1});
root0b = k0.root();
root1b = k1.root();
}
EXPECT_EQ(std::memcmp(&root0a, &root0b, sizeof(root0a)), 0);
EXPECT_EQ(std::memcmp(&root1a, &root1b, sizeof(root1a)), 0);
}
TEST(Experiment, NestedRaiiRestoresOuterHook)
{
dpf::experiment outer("outer");
const auto outer_seed = outer.seed();
const auto a = dpf::uniform_sample<std::uint64_t>();
{
dpf::experiment inner("inner");
EXPECT_NE(inner.seed(), outer_seed);
(void)dpf::uniform_sample<std::uint64_t>();
}
// Outer stream continues where it left off (inner restored the hook).
const auto b = dpf::uniform_sample<std::uint64_t>();
auto again = dpf::experiment::replay("outer", outer_seed);
EXPECT_EQ(dpf::uniform_sample<std::uint64_t>(), a);
EXPECT_EQ(dpf::uniform_sample<std::uint64_t>(), b);
}
TEST(Experiment, NoteSeedFromConstructors)
{
dpf::experiment ex("ctors");
EXPECT_TRUE(ex.has_seed_named("master"));
{
dpf::beavers::oracle<std::uint64_t> o(16);
EXPECT_TRUE(ex.has_seed_named("beavers::oracle"));
EXPECT_TRUE(ex.has_seed_named("lane_table"));
(void)o;
}
{
dpf::randomness::buffered_prg<dpf::prg::aes128, std::uint64_t> prg(8);
EXPECT_TRUE(ex.has_seed_named("buffered_prg"));
(void)prg;
}
{
dpf::prg_pad_rng<> pad;
EXPECT_TRUE(ex.has_seed_named("prg_pad_rng"));
(void)pad;
}
{
dpf::pseudorandom_root_sampler<dpf::prg::aes128> roots;
EXPECT_TRUE(ex.has_seed_named("pseudorandom_root_sampler"));
(void)roots;
}
ex.note_seed("custom", std::uint32_t{0xdeadbeefu});
EXPECT_TRUE(ex.has_seed_named("custom"));
EXPECT_GE(ex.seed_count(), 6u);
}
TEST(Experiment, RandomBytesCounterTracksUniformFill)
{
dpf::reset_random_bytes_count();
EXPECT_EQ(dpf::random_bytes_count(), 0u);
dpf::experiment ex("bytes");
// ctor resets after drawing the master.
EXPECT_EQ(dpf::random_bytes_count(), 0u);
(void)dpf::uniform_sample<std::uint64_t>();
EXPECT_EQ(dpf::random_bytes_count(), 8u);
(void)dpf::uniform_sample<std::uint32_t>();
EXPECT_EQ(dpf::random_bytes_count(), 12u);
}
TEST(Experiment, IngestPlanCriticalPathAndEdges)
{
dpf::experiment ex("plan");
auto p = dpf::protocol::fss_point_plan(0, 8, 16);
ex.ingest_plan(p);
EXPECT_EQ(ex.interactive_rounds(), 8u);
EXPECT_EQ(ex.dag_depth(), p.waves());
EXPECT_EQ(ex.plan_bytes_out(), 128u);
EXPECT_EQ(ex.plan_edge_bytes(dpf::protocol::edge_channel::peer), 128u);
EXPECT_EQ(ex.plan_edge_bytes(dpf::protocol::edge_channel::dealer), 0u);
EXPECT_GT(ex.critical_path_length(), 0u);
}
TEST(Experiment, MeasurePlanProbeAndTiming)
{
auto plan = dpf::protocol::mailbox_write_fused_plan(0);
auto ex = dpf::app::measure_plan("mailbox", plan);
EXPECT_EQ(ex.interactive_rounds(), 8u);
EXPECT_EQ(ex.rounds().size(), 8u);
std::size_t sum_out = 0, sum_in = 0;
for (const auto & r : ex.rounds())
{
EXPECT_EQ(r.channel, dpf::protocol::edge_channel::peer);
sum_out += r.bytes_out;
sum_in += r.bytes_in;
}
EXPECT_EQ(sum_out, ex.bytes_out());
EXPECT_EQ(sum_in, ex.bytes_in());
EXPECT_EQ(ex.edge_bytes_out(dpf::protocol::edge_channel::peer),
ex.bytes_out());
EXPECT_GT(ex.wall_ns(), 0u);
EXPECT_GT(ex.prg_evals(), 0u); // walk kernels expand
EXPECT_TRUE(ex.has_seed_named("master"));
}
TEST(Experiment, MeasureClientServersTwoRounds)
{
auto plan = dpf::protocol::n_server_pir_plan(0, 2, 64, 8);
const auto slots = plan.slot_bytes_all();
ASSERT_EQ(slots.size(), 2u);
auto ex = dpf::app::measure_plan("pir2", plan);
EXPECT_EQ(ex.interactive_rounds(), 2u);
ASSERT_EQ(ex.rounds().size(), 2u);
EXPECT_EQ(ex.rounds()[0].bytes_out, slots[0]);
EXPECT_EQ(ex.rounds()[1].bytes_out, slots[1]);
EXPECT_EQ(ex.plan_bytes_out(), slots[0] + slots[1]);
}
TEST(Experiment, WriteCsvAllFilesAndAppend)
{
const auto dir = tmp_dir("csv");
auto plan = dpf::protocol::fss_cmp_plan(0);
auto ex = dpf::app::measure_plan("cmp", plan);
ex.set_run_id(1);
ex.write_csv(dir);
EXPECT_EQ(count_lines(dir + "/summary.csv"), 2u); // header + row
EXPECT_GE(count_lines(dir + "/rounds.csv"), 2u);
EXPECT_GE(count_lines(dir + "/edges.csv"), 2u);
EXPECT_GE(count_lines(dir + "/seeds.csv"), 2u);
EXPECT_GE(count_lines(dir + "/critical_path.csv"), 2u);
ex.set_run_id(2);
ex.write_csv(dir);
EXPECT_EQ(count_lines(dir + "/summary.csv"), 3u); // append
std::ifstream seeds(dir + "/seeds.csv");
std::string line;
bool saw_master = false;
while (std::getline(seeds, line))
if (line.find("master") != std::string::npos)
saw_master = true;
EXPECT_TRUE(saw_master);
(void)::system(("rm -rf " + dir).c_str());
}
TEST(Experiment, RunMeasuredEnvCsv)
{
const auto dir = tmp_dir("env");
ASSERT_EQ(::setenv("DPF_EXPERIMENT_DIR", dir.c_str(), 1), 0);
const int rc = dpf::app::run_measured("env_csv",
dpf::protocol::keyword_pir_compose_plan(0, 8), 2);
EXPECT_EQ(rc, 0);
EXPECT_GE(count_lines(dir + "/summary.csv"), 2u);
ASSERT_EQ(::unsetenv("DPF_EXPERIMENT_DIR"), 0);
(void)::system(("rm -rf " + dir).c_str());
}
TEST(Experiment, ProbeAbsentDriveStillWorks)
{
// exercise_plan without experiment must not require clocks/probes.
auto plan = dpf::protocol::range_count_plan(0);
const auto cost = dpf::app::exercise_plan(plan);
EXPECT_EQ(cost.rounds, 8u);
EXPECT_EQ(cost.bytes, 128u);
}
TEST(Experiment, UninstallRestoresSystemEntropy)
{
dpf::experiment::master_seed master{};
{
dpf::experiment ex("tmp");
master = ex.seed();
(void)dpf::uniform_sample<std::uint64_t>();
}
// After destroy, draws are fresh system entropy (not the replay stream).
// Just ensure we can sample without a hook and without throwing.
const auto x = dpf::uniform_sample<std::uint64_t>();
const auto y = dpf::uniform_sample<std::uint64_t>();
(void)x;
(void)y;
auto replayed = dpf::experiment::replay("tmp", master);
// Replay still works after a window of system entropy.
(void)dpf::uniform_sample<std::uint64_t>();
}
TEST(Experiment, ThreadsDoNotCrossStreams)
{
constexpr int n = 32;
std::vector<std::uint64_t> a(n), b(n);
dpf::experiment::master_seed sa{}, sb{};
std::thread t0([&] {
dpf::experiment ex("A", "p0");
sa = ex.seed();
for (int i = 0; i < n; ++i)
a[i] = dpf::uniform_sample<std::uint64_t>();
});
std::thread t1([&] {
dpf::experiment ex("B", "p1");
sb = ex.seed();
for (int i = 0; i < n; ++i)
b[i] = dpf::uniform_sample<std::uint64_t>();
});
t0.join();
t1.join();
EXPECT_NE(sa, sb);
auto ra = dpf::experiment::replay("A", sa);
for (int i = 0; i < n; ++i)
EXPECT_EQ(dpf::uniform_sample<std::uint64_t>(), a[i]);
auto rb = dpf::experiment::replay("B", sb);
for (int i = 0; i < n; ++i)
EXPECT_EQ(dpf::uniform_sample<std::uint64_t>(), b[i]);
}
TEST(Experiment, MoveTransfersActiveContext)
{
dpf::experiment::master_seed master{};
std::uint64_t first = 0;
{
dpf::experiment ex("move");
master = ex.seed();
first = dpf::uniform_sample<std::uint64_t>();
dpf::experiment moved = std::move(ex);
EXPECT_EQ(moved.seed(), master);
const auto second = dpf::uniform_sample<std::uint64_t>();
auto again = dpf::experiment::replay("move", master);
EXPECT_EQ(dpf::uniform_sample<std::uint64_t>(), first);
EXPECT_EQ(dpf::uniform_sample<std::uint64_t>(), second);
}
}
TEST(Experiment, BeginEndTimingWithoutProbe)
{
dpf::experiment ex("timing");
ex.begin_timing();
volatile std::uint64_t sink = 0;
for (int i = 0; i < 1000; ++i)
sink += dpf::uniform_sample<std::uint64_t>();
(void)sink;
ex.end_timing();
EXPECT_GT(ex.wall_ns(), 0u);
EXPECT_GE(ex.random_bytes(), 8000u);
}
} // namespace

View file

@ -0,0 +1,62 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <vector>
#include "dpf.hpp"
// Extractable full-domain expansion must match point eval share-for-share,
// including both lanes of the packed leaf that holds the programmed point.
TEST(ExtractableFull, Fp61SharesMatchPointEval)
{
constexpr std::uint8_t alpha = 9;
const dpf::fp61 message{42};
auto [k0, k1] = dpf::make_dpf(alpha, message, dpf::extractable{});
auto [buf0, it0] = dpf::eval_full(k0);
auto [buf1, it1] = dpf::eval_full(k1);
auto a0 = std::begin(it0);
auto a1 = std::begin(it1);
for (unsigned i = 0; i < 256; ++i, ++a0, ++a1)
{
const auto x = static_cast<std::uint8_t>(i);
const auto p0 = (*dpf::eval_point(k0, x)).raw();
const auto p1 = (*dpf::eval_point(k1, x)).raw();
EXPECT_EQ((*a0).raw().raw(), p0.raw()) << "party0 at " << i;
EXPECT_EQ((*a1).raw().raw(), p1.raw()) << "party1 at " << i;
}
EXPECT_EQ(a0, std::end(it0));
EXPECT_EQ(a1, std::end(it1));
EXPECT_EQ(dpf::reconstruct(*a0 = *std::begin(it0), *std::begin(it1)),
dpf::fp61{0}); // silence unused if reconstruct needs the type
(void)buf0;
(void)buf1;
}
TEST(ExtractableFull, Fp61OpenedMailbox)
{
constexpr std::uint8_t alpha = 9;
const dpf::fp61 message{42};
auto [k0, k1] = dpf::make_dpf(alpha, message, dpf::extractable{});
auto [buf0, it0] = dpf::eval_full(k0);
auto [buf1, it1] = dpf::eval_full(k1);
std::vector<dpf::fp61> y0, y1;
for (auto it = std::begin(it0); it != std::end(it0); ++it)
y0.push_back((*it).raw());
for (auto it = std::begin(it1); it != std::end(it1); ++it)
y1.push_back((*it).raw());
EXPECT_EQ(y0[alpha] - y1[alpha], message);
EXPECT_EQ((y0[0] - y1[0]).raw(), 0u);
EXPECT_EQ((y0[8] - y1[8]).raw(), 0u);
EXPECT_EQ((y0[10] - y1[10]).raw(), 0u);
std::vector<dpf::fp61> challenge(256);
for (std::size_t i = 0; i < challenge.size(); ++i)
challenge[i] = dpf::fp61{static_cast<std::uint64_t>(i + 1)};
EXPECT_TRUE(dpf::sketch_verify(dpf::sketch_fold(y0, challenge),
dpf::sketch_fold(y1, challenge)));
}

View file

@ -0,0 +1,215 @@
#include <gtest/gtest.h>
#include <tuple>
#include "dpf.hpp"
#include <cstdint>
#include <cstring>
#include <stdexcept>
#include <type_traits>
namespace
{
template <typename Key0, typename Key1, typename In>
auto open_at(const Key0 &k0, const Key1 &k1, In x)
{
const auto y0 = *dpf::eval_point(k0, x);
const auto y1 = *dpf::eval_point(k1, x);
return dpf::reconstruct(y0, y1);
}
template <typename Out, typename In>
void expect_point_payload(In alpha, Out beta)
{
auto [k0, k1] = dpf::make_dpf(alpha, beta);
const In last = static_cast<In>(16);
for (In x = 0; x < last; ++x)
{
const Out got = open_at(k0, k1, x);
EXPECT_EQ(got, x == alpha ? beta : Out{}) << static_cast<unsigned>(x);
}
const Out on = open_at(k0, k1, alpha);
EXPECT_EQ(on, beta);
}
} // namespace
TEST(Field64, ArithmeticMatchesThePrime)
{
constexpr auto p = dpf::field64::mod;
EXPECT_EQ((dpf::field64{p - 1} + dpf::field64{p - 1}).raw(), p - 2);
EXPECT_EQ((dpf::field64{p - 1} + dpf::field64{1}).raw(), 0u);
EXPECT_EQ((dpf::field64{1} - dpf::field64{2}).raw(), p - 1);
EXPECT_EQ((-dpf::field64{0}).raw(), 0u);
EXPECT_EQ((-dpf::field64{1}).raw(), p - 1);
EXPECT_EQ(dpf::field64{-1}.raw(), p - 1);
EXPECT_EQ((dpf::field64{p - 1} * dpf::field64{p - 1}).raw(), 1u);
EXPECT_EQ((dpf::field64{0x0123456789abcdefull} * dpf::field64{0xfedcba9876543210ull}).raw(),
0xcfaeafd136c7bbaeull);
EXPECT_EQ((dpf::field64{std::uint64_t{1} << 32} * dpf::field64{std::uint64_t{1} << 32}).raw(),
0xffffffffull);
const dpf::field64 a{1000};
const dpf::field64 b{p - 5};
const dpf::field64 c{17};
EXPECT_EQ((a + b) * c, a * c + b * c);
EXPECT_TRUE(std::is_trivially_copyable_v<dpf::field64>);
EXPECT_TRUE(std::is_standard_layout_v<dpf::field64>);
}
TEST(Field128, ArithmeticMatchesThePrime)
{
const dpf::field128 almost{static_cast<unsigned __int128>(
(static_cast<unsigned __int128>(dpf::field128::mod_hi) << 64)
| dpf::field128::mod_lo) - 1};
EXPECT_EQ(almost + dpf::field128{1}, dpf::field128{0});
EXPECT_EQ(almost * almost, dpf::field128{1});
EXPECT_EQ(-almost, dpf::field128{1});
EXPECT_EQ(dpf::field128{-1}, almost);
EXPECT_EQ(dpf::field128{1} - dpf::field128{2}, almost);
EXPECT_TRUE(std::is_trivially_copyable_v<dpf::field128>);
EXPECT_TRUE(std::is_standard_layout_v<dpf::field128>);
}
TEST(Field128, WideProduct)
{
const unsigned __int128 a =
(static_cast<unsigned __int128>(0x123456789abcdef0ull) << 64)
| 0x123456789abcdefull;
const unsigned __int128 b =
(static_cast<unsigned __int128>(0xfedcba9876543210ull) << 64)
| 0xfedcba9876543210ull;
const auto got = dpf::field128{a} * dpf::field128{b};
EXPECT_EQ(got.hi(), 0xb0a8b1cbf73350c9ull);
EXPECT_EQ(got.lo(), 0xf0123456789abe97ull);
}
TEST(P256, GeneratorAndDoubling)
{
const auto g = dpf::p256::generator();
EXPECT_EQ(dpf::p256{1}, g);
EXPECT_EQ(dpf::p256{0}, dpf::p256{});
EXPECT_TRUE(dpf::p256{}.is_identity());
EXPECT_EQ(g + dpf::p256{}, g);
EXPECT_EQ(g - g, dpf::p256{});
EXPECT_EQ(-dpf::p256{1}, dpf::p256{-1});
const auto two = g + g;
EXPECT_EQ(two, dpf::p256{2});
EXPECT_EQ(two - g, g);
const unsigned char expect[] = {
0x03,
0x7c, 0xf2, 0x7b, 0x18, 0x8d, 0x03, 0x4f, 0x7e,
0x8a, 0x52, 0x38, 0x03, 0x04, 0xb5, 0x1a, 0xc3,
0xc0, 0x89, 0x69, 0xe2, 0x77, 0xf2, 0x1b, 0x35,
0xa6, 0x0b, 0x48, 0xfc, 0x47, 0x66, 0x99, 0x78,
};
EXPECT_EQ(std::memcmp(two.bytes(), expect, 33), 0);
EXPECT_EQ(dpf::p256::from_compressed(expect), two);
const unsigned char bad[33] = {0x04};
EXPECT_THROW(dpf::p256::from_compressed(bad), std::invalid_argument);
EXPECT_TRUE(std::is_trivially_copyable_v<dpf::p256>);
EXPECT_TRUE(std::is_standard_layout_v<dpf::p256>);
}
template <typename Out, typename Spec>
void expect_cmp(std::uint8_t alpha, Out beta, Spec spec, bool leq)
{
auto [k0, k1] = dpf::make_dpf(alpha, spec);
for (int x = 0; x < 24; ++x)
{
const auto q = static_cast<std::uint8_t>(x);
const auto y0 = dpf::eval_point<Out>(dpf::cmp, k0, q);
const auto y1 = dpf::eval_point<Out>(dpf::cmp, k1, q);
const bool hot = leq ? x <= static_cast<int>(alpha)
: x < static_cast<int>(alpha);
EXPECT_EQ(dpf::reconstruct(y0, y1), hot ? beta : Out{}) << x;
}
}
TEST(FieldOutput, ComparisonPayload)
{
const auto f = dpf::field64{5};
expect_cmp(std::uint8_t{10}, f, dpf::lt(f), false);
expect_cmp(std::uint8_t{10}, f, dpf::leq(f), true);
const auto w = dpf::field128{9};
expect_cmp(std::uint8_t{10}, w, dpf::lt(w), false);
expect_cmp(std::uint8_t{4}, dpf::p256{1}, dpf::lt(dpf::p256{1}), false);
}
TEST(FieldOutput, IdcfPrefixUsesTheSameGroup)
{
const auto beta = dpf::field64{3};
const std::uint8_t alpha = 0x2a;
auto [k0, k1] = dpf::make_dpf(alpha, dpf::idcf(dpf::lt(beta)));
auto [a0, a1] = dpf::make_dpf(alpha, dpf::lt_at<4>(beta));
for (int x = 0; x < 32; ++x)
{
const auto q = static_cast<std::uint8_t>(x);
const auto full0 = dpf::eval_point<dpf::field64>(dpf::cmp, k0, q);
const auto full1 = dpf::eval_point<dpf::field64>(dpf::cmp, k1, q);
EXPECT_EQ(dpf::reconstruct(full0, full1),
x < static_cast<int>(alpha) ? beta : dpf::field64{});
const auto p0 = dpf::eval_point<4, dpf::field64>(dpf::cmp_prefix<4>, k0, q);
const auto p1 = dpf::eval_point<4, dpf::field64>(dpf::cmp_prefix<4>, k1, q);
const auto n0 = dpf::eval_point<dpf::field64>(dpf::cmp, a0, q);
const auto n1 = dpf::eval_point<dpf::field64>(dpf::cmp, a1, q);
EXPECT_EQ(dpf::reconstruct(p0, p1), dpf::reconstruct(n0, n1)) << x;
}
}
TEST(FieldOutput, PointPayloadRoundTrip)
{
expect_point_payload<dpf::field64>(std::uint8_t{0x2a}, dpf::field64{99});
expect_point_payload<dpf::field128>(std::uint8_t{0x11}, dpf::field128{7});
expect_point_payload<dpf::p256>(std::uint8_t{0x2a}, dpf::p256{1});
}
TEST(FieldOutput, Fp61PayloadNearModulus)
{
const dpf::fp61 beta{dpf::fp61_mod - 1};
expect_point_payload<dpf::fp61>(std::uint8_t{0x2a}, beta);
expect_point_payload<dpf::fp61>(std::uint8_t{0x05}, dpf::fp61{dpf::fp61_mod - 7});
}
TEST(FieldOutput, Fp61NegativeComparisonDelta)
{
// δ = −1 ≡ p−1 must survive comparison (not collapse to 0 via a 61-bit mask).
expect_cmp(std::uint8_t{10}, dpf::fp61{dpf::fp61_mod - 1},
dpf::lt(dpf::fp61{dpf::fp61_mod - 1}), false);
expect_cmp(std::uint8_t{10}, dpf::fp61{dpf::fp61_mod - 1},
dpf::leq(dpf::fp61{dpf::fp61_mod - 1}), true);
}
TEST(FieldOutput, P256MalformedCompressedRejected)
{
unsigned char bad[33] = {0x02};
bad[32] = 1; // x = 1 is not on P-256
EXPECT_THROW(dpf::p256::from_compressed(bad), std::invalid_argument);
dpf::p256 bogus{};
unsigned char raw[sizeof(dpf::p256)]{};
std::memcpy(raw, &bogus, sizeof(raw));
std::memcpy(raw, bad, 33);
std::memcpy(&bogus, raw, sizeof(raw));
EXPECT_THROW(bogus + dpf::p256{}, std::invalid_argument);
EXPECT_THROW(-bogus, std::invalid_argument);
}
TEST(FieldOutput, P256ScalarWideComparisonPayload)
{
const dpf::p256_scalar beta{17};
expect_cmp(std::uint8_t{10}, beta, dpf::lt(beta), false);
expect_cmp(std::uint8_t{10}, beta, dpf::leq(beta), true);
}
TEST(FieldOutput, Field128NoncanonicalNegationAndEquality)
{
dpf::field128 a{};
const std::uint64_t w[2] = {
dpf::field128::mod_lo, dpf::field128::mod_hi};
std::memcpy(&a, w, sizeof(w));
EXPECT_EQ(dpf::field128::canonicalize(a), dpf::field128{});
EXPECT_EQ(-a, dpf::field128{});
EXPECT_EQ(a, dpf::field128{});
}

View file

@ -0,0 +1,310 @@
#include <gtest/gtest.h>
#include <condition_variable>
#include <cstdint>
#include <exception>
#include <limits>
#include <mutex>
#include <thread>
#include <type_traits>
#include <utility>
#include "grotto.hpp"
namespace
{
struct bus
{
std::mutex mu;
std::condition_variable cv;
std::uint64_t word[2]{};
std::uint64_t deliver[2]{};
int epoch = 0;
int arrived = 0;
int sent0 = 0;
std::uint64_t exchange(int me, std::uint64_t mine)
{
std::unique_lock<std::mutex> lock(mu);
const int seen = epoch;
word[me] = mine;
if (me == 0)
++sent0;
if (++arrived == 2)
{
deliver[0] = word[1];
deliver[1] = word[0];
arrived = 0;
++epoch;
cv.notify_all();
}
else
cv.wait(lock, [&] { return epoch != seen; });
return deliver[me];
}
};
template <typename T>
std::pair<T, T> split_word(T secret)
{
using U = std::make_unsigned_t<T>;
const U mask = static_cast<U>(~U{0});
const U bits = static_cast<U>(secret);
const U share = static_cast<U>(dpf::uniform_sample<U>() & mask);
const U other = static_cast<U>(bits - share);
return {static_cast<T>(share), static_cast<T>(other)};
}
template <typename Fixed>
simde_uint128 raw_bits(Fixed value)
{
std::uint64_t limb[4] = {};
grotto::detail::store_raw_limbs(value.integral_representation(), limb);
return simde_uint128{limb[0]} | (simde_uint128{limb[1]} << 64);
}
template <unsigned I, unsigned F, unsigned Lf, typename L, unsigned Rf, typename R>
void expect_product(std::pair<L, L> lhs, std::pair<R, R> rhs)
{
using shape = grotto::fixed_mul_beaver_shape<I, F, Lf, L, Rf, R>;
ASSERT_TRUE(shape::fits);
static const auto prep = grotto::make_fixed_mul_beaver_prep<I, F, Lf, L, Rf, R>();
const auto triple = grotto::sample_fixed_mul_beaver_triple(shape::plan::multiply_bits);
bus link;
using result = typename shape::plan::result_type;
result y0{};
result y1{};
std::exception_ptr e0;
std::exception_ptr e1;
std::thread t0([&] {
try
{
y0 = grotto::eval_fixed_mul_beaver<I, F, Lf, L, Rf, R>(
prep, triple, 0, lhs.first, rhs.first,
[&](std::uint64_t mine) { return link.exchange(0, mine); });
}
catch (...)
{
e0 = std::current_exception();
}
});
std::thread t1([&] {
try
{
y1 = grotto::eval_fixed_mul_beaver<I, F, Lf, L, Rf, R>(
prep, triple, 1, lhs.second, rhs.second,
[&](std::uint64_t mine) { return link.exchange(1, mine); });
}
catch (...)
{
e1 = std::current_exception();
}
});
t0.join();
t1.join();
if (e0)
std::rethrow_exception(e0);
if (e1)
std::rethrow_exception(e1);
EXPECT_EQ(link.sent0, static_cast<int>(shape::messages));
const auto clear = grotto::fixed_mul<I, F>(
grotto::make_fixed_from_integral_type<Lf, L>(
static_cast<L>(static_cast<std::make_unsigned_t<L>>(lhs.first)
+ static_cast<std::make_unsigned_t<L>>(lhs.second))),
grotto::make_fixed_from_integral_type<Rf, R>(
static_cast<R>(static_cast<std::make_unsigned_t<R>>(rhs.first)
+ static_cast<std::make_unsigned_t<R>>(rhs.second))));
const unsigned storage = shape::storage_bits;
const simde_uint128 mask = storage >= 128u
? ~simde_uint128{0}
: (simde_uint128{1} << storage) - 1;
const auto got = (raw_bits(y0) + raw_bits(y1)) & mask;
const auto want = raw_bits(clear) & mask;
EXPECT_EQ(static_cast<std::uint64_t>(got), static_cast<std::uint64_t>(want));
EXPECT_EQ(static_cast<std::uint64_t>(got >> 64),
static_cast<std::uint64_t>(want >> 64));
}
template <typename T>
T reconstruct(std::pair<T, T> shares)
{
using U = std::make_unsigned_t<T>;
return static_cast<T>(static_cast<U>(shares.first) + static_cast<U>(shares.second));
}
} // namespace
TEST(FixedMulBeaver, EmptyWindowIsZeroAndSilent)
{
using shape = grotto::fixed_mul_beaver_shape<0, 4, 0, std::int32_t, 0, std::int32_t>;
EXPECT_TRUE(shape::fits);
EXPECT_FALSE(shape::active);
EXPECT_EQ(shape::messages, 0u);
auto prep = grotto::make_fixed_mul_beaver_prep<0, 4, 0, std::int32_t, 0, std::int32_t>();
grotto::fixed_mul_beaver_triple triple;
int calls = 0;
auto y = grotto::eval_fixed_mul_beaver<0, 4, 0, std::int32_t, 0, std::int32_t>(
prep, triple, 0, 7, 9, [&](std::uint64_t) {
++calls;
return std::uint64_t{0};
});
EXPECT_EQ(calls, 0);
EXPECT_EQ(y.integral_representation(), 0);
}
TEST(FixedMulBeaver, WideSignedWindowDoesNotFit)
{
using shape = grotto::fixed_mul_beaver_shape<16, 64, 16, std::int32_t, 16, std::int32_t>;
EXPECT_FALSE(shape::fits);
EXPECT_TRUE(shape::result_lift);
EXPECT_GT(shape::plan::out_bits, 64u);
}
TEST(FixedMulBeaver, TruncationIsOnlyTheBeaverOpening)
{
using L = std::uint32_t;
using shape = grotto::fixed_mul_beaver_shape<16, 0, 0, L, 0, L>;
EXPECT_TRUE(shape::fits);
EXPECT_FALSE(shape::lhs_lift);
EXPECT_FALSE(shape::shift_right);
EXPECT_EQ(shape::messages, 2u);
auto prep = grotto::make_fixed_mul_beaver_prep<16, 0, 0, L, 0, L>();
for (int i = 0; i < 8; ++i)
{
const auto lhs = split_word(i == 0 ? L{7} : dpf::uniform_sample<L>());
const auto rhs = split_word(i == 0 ? L{9} : dpf::uniform_sample<L>());
const auto triple = grotto::sample_fixed_mul_beaver_triple(16u);
bus link;
auto y0 = grotto::fixedpoint<0, std::uint16_t>{};
auto y1 = y0;
std::thread t0([&] {
y0 = grotto::eval_fixed_mul_beaver<16, 0, 0, L, 0, L>(
prep, triple, 0, lhs.first, rhs.first,
[&](std::uint64_t mine) { return link.exchange(0, mine); });
});
std::thread t1([&] {
y1 = grotto::eval_fixed_mul_beaver<16, 0, 0, L, 0, L>(
prep, triple, 1, lhs.second, rhs.second,
[&](std::uint64_t mine) { return link.exchange(1, mine); });
});
t0.join();
t1.join();
EXPECT_EQ(link.sent0, 2);
const auto clear = grotto::fixed_mul<16, 0>(
grotto::make_fixed_from_integral_type<0, L>(reconstruct(lhs)),
grotto::make_fixed_from_integral_type<0, L>(reconstruct(rhs)));
const auto got = static_cast<std::uint16_t>(
static_cast<std::uint16_t>(y0.integral_representation())
+ static_cast<std::uint16_t>(y1.integral_representation()));
EXPECT_EQ(got, clear.integral_representation());
}
}
TEST(FixedMulBeaver, NegativeProductFloorsIntoTheWindow)
{
using Q = std::int32_t;
const auto lhs = split_word(Q{-3});
const auto rhs = split_word(Q{1});
expect_product<8, 4, 4, Q, 4, Q>(lhs, rhs);
const auto neg = split_word(Q{-2});
expect_product<8, 4, 4, Q, 4, Q>(lhs, neg);
}
TEST(FixedMulBeaver, SignedLiftAndRightShift)
{
using Q = std::int32_t;
using shape = grotto::fixed_mul_beaver_shape<16, 16, 16, Q, 16, Q>;
EXPECT_TRUE(shape::lhs_lift);
EXPECT_TRUE(shape::shift_right);
EXPECT_FALSE(shape::product_lift);
EXPECT_EQ(shape::plan::multiply_bits, 48u);
EXPECT_EQ(shape::messages, 5u);
auto prep = grotto::make_fixed_mul_beaver_prep<16, 16, 16, Q, 16, Q>();
const Q samples[][2] = {
{0, 0},
{1 << 16, 2 << 16},
{-(3 << 16), 1 << 16},
{-(3 << 16), -(2 << 16)},
{std::numeric_limits<Q>::max(), -1},
{std::numeric_limits<Q>::min(), 1},
};
for (const auto & sample : samples)
{
const auto triple = grotto::sample_fixed_mul_beaver_triple(48u);
const auto lhs = split_word(sample[0]);
const auto rhs = split_word(sample[1]);
bus link;
using result = typename shape::plan::result_type;
result y0{};
result y1{};
std::thread t0([&] {
y0 = grotto::eval_fixed_mul_beaver<16, 16, 16, Q, 16, Q>(
prep, triple, 0, lhs.first, rhs.first,
[&](std::uint64_t mine) { return link.exchange(0, mine); });
});
std::thread t1([&] {
y1 = grotto::eval_fixed_mul_beaver<16, 16, 16, Q, 16, Q>(
prep, triple, 1, lhs.second, rhs.second,
[&](std::uint64_t mine) { return link.exchange(1, mine); });
});
t0.join();
t1.join();
EXPECT_EQ(link.sent0, 5);
const auto clear = grotto::fixed_mul<16, 16>(
grotto::make_fixed_from_integral_type<16, Q>(reconstruct(lhs)),
grotto::make_fixed_from_integral_type<16, Q>(reconstruct(rhs)));
const auto got = static_cast<std::uint32_t>(y0.integral_representation())
+ static_cast<std::uint32_t>(y1.integral_representation());
EXPECT_EQ(got, static_cast<std::uint32_t>(clear.integral_representation()))
<< "lhs=" << sample[0] << " rhs=" << sample[1];
}
}
TEST(FixedMulBeaver, LeftShiftAndUnsignedLift)
{
using L = std::uint16_t;
using shape = grotto::fixed_mul_beaver_shape<4, 8, 0, L, 0, L>;
EXPECT_TRUE(shape::shift_left);
EXPECT_TRUE(shape::fits);
const auto lhs = split_word(L{3});
const auto rhs = split_word(L{5});
expect_product<4, 8, 0, L, 0, L>(lhs, rhs);
}
TEST(FixedMulBeaver, NarrowProductIsSignExtendedBeforeTheWindow)
{
using Q = std::int8_t;
using shape = grotto::fixed_mul_beaver_shape<16, 8, 4, Q, 4, Q>;
EXPECT_TRUE(shape::fits);
EXPECT_TRUE(shape::lhs_lift);
EXPECT_TRUE(shape::product_lift);
EXPECT_TRUE(shape::result_lift);
EXPECT_FALSE(shape::shift_right);
EXPECT_EQ(shape::messages, 6u);
const Q samples[] = {0, 1, -1, -3, 7, -8, 12, std::numeric_limits<Q>::max(),
std::numeric_limits<Q>::min()};
for (Q a : samples)
for (Q b : samples)
expect_product<16, 8, 4, Q, 4, Q>(split_word(a), split_word(b));
}
TEST(FixedMulBeaver, Int64WindowUsesA96BitProduct)
{
using Q = std::int64_t;
using shape = grotto::fixed_mul_beaver_shape<32, 32, 32, Q, 32, Q>;
ASSERT_TRUE(shape::fits);
EXPECT_EQ(shape::plan::multiply_bits, 96u);
EXPECT_EQ(shape::plan::modulus_bits, 96u);
EXPECT_TRUE(shape::lhs_lift);
EXPECT_EQ(shape::messages, 8u);
const Q samples[][2] = {
{0, 0},
{Q{3} << 32, Q{5} << 32},
{-(Q{3} << 32), Q{1} << 32},
{-(Q{4} << 32), -(Q{2} << 32)},
};
for (const auto & sample : samples)
expect_product<32, 32, 32, Q, 32, Q>(split_word(sample[0]), split_word(sample[1]));
}

180
test/tests/flute_test.cpp Normal file
View file

@ -0,0 +1,180 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <vector>
#include "dpf/flute.hpp"
TEST(Flute, EveryTwoBitFunction)
{
const unsigned delta = 2;
std::uint8_t rows[4];
for (unsigned fn = 0; fn < 16; ++fn)
{
for (unsigned j = 0; j < 4; ++j)
rows[j] = static_cast<std::uint8_t>((fn >> j) & 1u);
for (unsigned x = 0; x < 4; ++x)
{
std::uint8_t bits[2] = {
static_cast<std::uint8_t>(x & 1u),
static_cast<std::uint8_t>((x >> 1) & 1u),
};
auto want = dpf::flute::eval_plain(delta, 1, rows, bits);
auto pair = dpf::flute::eval_pair(delta, 1, rows, bits);
auto trio = dpf::flute::eval_trio(delta, 1, rows, bits);
EXPECT_EQ(pair.opened, want);
EXPECT_EQ(trio.opened, want);
EXPECT_EQ(pair.online_bits, 2u);
EXPECT_EQ(trio.online_bits, 3u);
EXPECT_EQ(pair.mask.size(), 2u);
EXPECT_EQ(trio.mask.size(), 3u);
}
}
}
TEST(Flute, TwoOutputBits)
{
// LSB of the 3-bit index, and its majority with the other two bits.
const unsigned delta = 3;
const unsigned rows = 8;
std::vector<std::uint8_t> columns(2 * rows);
for (unsigned j = 0; j < rows; ++j)
{
columns[j] = static_cast<std::uint8_t>(j & 1u);
const unsigned ones = (j & 1u) + ((j >> 1) & 1u) + ((j >> 2) & 1u);
columns[rows + j] = static_cast<std::uint8_t>(ones >= 2);
}
for (unsigned x = 0; x < rows; ++x)
{
std::uint8_t bits[3] = {
static_cast<std::uint8_t>(x & 1u),
static_cast<std::uint8_t>((x >> 1) & 1u),
static_cast<std::uint8_t>((x >> 2) & 1u),
};
auto want = dpf::flute::eval_plain(delta, 2, columns.data(), bits);
auto got = dpf::flute::eval_pair(delta, 2, columns.data(), bits);
EXPECT_EQ(got.opened, want);
EXPECT_EQ(got.online_bits, 4u);
std::uint8_t lam0 = 0;
std::uint8_t lam1 = 0;
lam0 = static_cast<std::uint8_t>(got.mask[0][0] ^ got.mask[0][1]);
lam1 = static_cast<std::uint8_t>(got.mask[1][0] ^ got.mask[1][1]);
(void)lam0;
(void)lam1;
for (unsigned w = 0; w < 2; ++w)
{
const std::uint8_t lam = static_cast<std::uint8_t>(
got.mask[0][w] ^ got.mask[1][w]);
EXPECT_EQ(static_cast<std::uint8_t>(got.masked[w] ^ lam), want[w]);
}
}
}
namespace
{
void expect_lut(unsigned delta, unsigned n_out, const std::vector<std::uint8_t> & columns,
const std::uint8_t * bits)
{
auto want = dpf::flute::eval_plain(delta, n_out, columns.data(), bits);
auto pair = dpf::flute::eval_pair(delta, n_out, columns.data(), bits);
auto trio = dpf::flute::eval_trio(delta, n_out, columns.data(), bits);
EXPECT_EQ(pair.opened, want);
EXPECT_EQ(trio.opened, want);
EXPECT_EQ(pair.online_bits, static_cast<std::size_t>(2 * n_out));
EXPECT_EQ(trio.online_bits, static_cast<std::size_t>(3 * n_out));
for (unsigned w = 0; w < n_out; ++w)
{
const std::uint8_t two = static_cast<std::uint8_t>(
pair.masked[w] ^ pair.mask[0][w] ^ pair.mask[1][w]);
const std::uint8_t three = static_cast<std::uint8_t>(
trio.masked[w] ^ trio.mask[0][w] ^ trio.mask[1][w] ^ trio.mask[2][w]);
EXPECT_EQ(two, want[w]);
EXPECT_EQ(three, want[w]);
}
}
std::uint32_t xorshift(std::uint32_t & s)
{
s ^= s << 13;
s ^= s >> 17;
s ^= s << 5;
return s;
}
} // namespace
TEST(Flute, EveryFunctionThroughThreeBits)
{
for (unsigned delta = 1; delta <= 3; ++delta)
{
const unsigned rows = 1u << delta;
const unsigned nfn = 1u << rows;
std::vector<std::uint8_t> column(rows);
std::vector<std::uint8_t> bits(delta);
for (unsigned fn = 0; fn < nfn; ++fn)
{
for (unsigned j = 0; j < rows; ++j)
column[j] = static_cast<std::uint8_t>((fn >> j) & 1u);
for (unsigned x = 0; x < rows; ++x)
{
for (unsigned i = 0; i < delta; ++i)
bits[i] = static_cast<std::uint8_t>((x >> i) & 1u);
expect_lut(delta, 1, column, bits.data());
}
}
}
}
TEST(Flute, FourBitSampleAndEightBitSpecials)
{
const unsigned delta = 4;
const unsigned rows = 16;
std::uint32_t rng = 0xC0FFEEu;
std::vector<std::uint8_t> columns(2 * rows);
std::vector<std::uint8_t> bits(delta);
for (unsigned sample = 0; sample < 24; ++sample)
{
for (auto & cell : columns)
cell = static_cast<std::uint8_t>(xorshift(rng) & 1u);
for (unsigned x = 0; x < rows; ++x)
{
for (unsigned i = 0; i < delta; ++i)
bits[i] = static_cast<std::uint8_t>((x >> i) & 1u);
expect_lut(delta, 2, columns, bits.data());
}
}
const unsigned wide = 8;
const unsigned wrows = 256;
std::vector<std::uint8_t> table(4 * wrows, 0);
for (unsigned j = 0; j < wrows; ++j)
{
unsigned ones = 0;
for (unsigned i = 0; i < wide; ++i)
ones += (j >> i) & 1u;
table[j] = static_cast<std::uint8_t>(ones & 1u);
table[wrows + j] = static_cast<std::uint8_t>(ones == wide);
table[2 * wrows + j] = static_cast<std::uint8_t>((j >> 7) & 1u);
table[3 * wrows + j] = static_cast<std::uint8_t>(j == 0 || j == 255);
}
const unsigned points[] = {0u, 1u, 2u, 17u, 128u, 170u, 254u, 255u};
std::vector<std::uint8_t> wbits(wide);
for (unsigned x : points)
{
for (unsigned i = 0; i < wide; ++i)
wbits[i] = static_cast<std::uint8_t>((x >> i) & 1u);
expect_lut(wide, 4, table, wbits.data());
}
}
TEST(Flute, RejectsABadIndex)
{
std::uint8_t column[2] = {0, 1};
std::uint8_t bit = 1;
EXPECT_THROW(dpf::flute::eval_plain(0, 1, column, &bit), std::invalid_argument);
EXPECT_THROW(dpf::flute::eval_pair(9, 1, column, &bit), std::invalid_argument);
bit = 2;
EXPECT_THROW(dpf::flute::eval_trio(1, 1, column, &bit), std::invalid_argument);
EXPECT_THROW(dpf::flute::eval_pair(1, 0, column, &bit), std::invalid_argument);
}

View file

@ -1,10 +1,14 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <cstring>
#include <limits>
#include <sstream>
#include "simde/simde/x86/avx2.h"
#include "dpf/fp61.hpp"
#include "dpf/leaf_arithmetic.hpp"
namespace
{
@ -55,3 +59,26 @@ TEST(Fp61, StreamPrintsTheReducedValue)
os << fp61{fp61_mod + 4};
EXPECT_EQ(os.str(), "4");
}
TEST(Fp61, FromSeedMersenneFoldsWideWords)
{
std::uint64_t words[2] = {fp61_mod - 1, 1};
const auto a = fp61::from_seed(words, sizeof(words));
// (p-1) + 8·1 (since 2^64 ≡ 8), then reduce — not a 61-bit mask of the lo word.
EXPECT_NE(a.raw(), 0u);
EXPECT_EQ(a.raw(), fp61::reduce((fp61_mod - 1) + 8));
}
TEST(Fp61, LeafAddReducesNearModulus)
{
alignas(16) std::uint64_t left[2] = {fp61_mod - 1, fp61_mod - 2};
alignas(16) std::uint64_t right[2] = {3, 5};
alignas(16) std::uint64_t out[2]{};
simde__m128i a{}, b{};
std::memcpy(&a, left, sizeof(left));
std::memcpy(&b, right, sizeof(right));
const auto sum = dpf::leaf_arithmetic::add_t<fp61, simde__m128i>{}(a, b);
std::memcpy(out, &sum, sizeof(out));
EXPECT_EQ(out[0], 2u);
EXPECT_EQ(out[1], 3u);
}

View file

@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <tuple>
#include "dpf.hpp"
@ -349,6 +350,10 @@ TEST(Geneval, FullDomainUint8)
}
EXPECT_EQ(recon(g.party0[alpha], g.party1[alpha]), y);
EXPECT_EQ(recon(g.party0[0], g.party1[0]), out_t{0});
EXPECT_TRUE(dpf::verify(g.proof0, g.proof1));
EXPECT_NE(recon(g.party1[alpha], g.party0[alpha]), y);
g.proof0[0] = simde_mm_xor_si128(g.proof0[0], simde_mm_set1_epi8(1));
EXPECT_FALSE(dpf::verify(g.proof0, g.proof1));
}
TEST(Geneval, EmptySequence)
@ -796,10 +801,10 @@ TEST(Geneval, FullMatchesWholeIntervalAndRejectsHugeDomain)
EXPECT_EQ(recon(full.party0[255], full.party1[255]), y);
EXPECT_EQ(recon(full.party0[0], full.party1[0]), out_t{0});
EXPECT_THROW((dpf::geneval_full(uint32_t{1}, uint32_t{2}, rng<uint32_t>(),
uint32_t{1})), std::length_error);
EXPECT_THROW((dpf::geneval_interval(in_t{5}, in_t{1}, in_t{4}, in_t{3},
rng<in_t>(), y)), std::invalid_argument);
EXPECT_THROW(dpf::geneval_full(uint32_t{1}, uint32_t{2}, rng<uint32_t>(),
uint32_t{1}), std::length_error);
EXPECT_THROW(dpf::geneval_interval(in_t{5}, in_t{1}, in_t{4}, in_t{3},
rng<in_t>(), y), std::invalid_argument);
}
TEST(Geneval, SequencePermutationKeepsWordsAndDuplicates)
@ -981,8 +986,8 @@ TEST(Geneval, ArithShareOverflowAndWrappingInterval)
in_t from = 250;
in_t to = 10;
EXPECT_THROW((dpf::geneval_interval(dpf::arith_input, x0, x1, from, to,
rng<in_t>(), y)), std::invalid_argument);
EXPECT_THROW(dpf::geneval_interval(dpf::arith_input, x0, x1, from, to,
rng<in_t>(), y), std::invalid_argument);
from = 250;
to = 255;
@ -1043,8 +1048,8 @@ TEST(Geneval, SignedRegressionsFromTheCornerPass)
out_t y = -7;
// An inverted signed range must not wrap the long way around.
EXPECT_THROW((dpf::geneval_interval(in_t{2}, in_t{1}, in_t{4}, in_t{-3},
rng<in_t>(), y)), std::invalid_argument);
EXPECT_THROW(dpf::geneval_interval(in_t{2}, in_t{1}, in_t{4}, in_t{-3},
rng<in_t>(), y), std::invalid_argument);
// [INT_MIN, INT_MAX] is numeric order; full is bit-pattern order.
// The words are the same trie. Each input's share matches either way.
@ -1402,3 +1407,37 @@ TEST(Geneval, ArithDoernerShelatAndCmpMatchDealer)
EXPECT_EQ(opened, ends[i] > secret ? beta : 0u) << int(ends[i]);
}
}
TEST(Geneval, ArithCarryLeavesXorShares)
{
// 250 + 13 = 7 (mod 256). The carry must not collapse the sharing to (7, 0).
auto split_u8 = [](std::uint64_t seed) {
Pad pad;
pad.n = seed;
dpf::detail::local_cw_protocol<Pad> proto{pad};
std::uint8_t x0 = 250;
std::uint8_t x1 = 13;
proto.encode_walk_shares(x0, x1, true);
// Seven carries. Each is one bit-Beaver from `sample_beaver2` on
// `xor_wrapper<uint8_t>`: `sample_fresh<2>` draws 7 ring elements
// and each element is 8 pad bits.
EXPECT_EQ(pad.n, seed + 7u * 7u * 8u);
EXPECT_EQ(static_cast<std::uint8_t>(x0 ^ x1), 7);
EXPECT_NE(x1, 0);
EXPECT_NE(x0, 7);
};
split_u8(1);
split_u8(50);
Pad pad;
dpf::detail::local_cw_protocol<Pad> proto{pad};
const std::int8_t secret = -20;
std::int8_t a0 = 100;
std::int8_t a1 = static_cast<std::int8_t>(secret - a0);
std::int8_t encoded = secret;
dpf::utils::flip_msb_if_signed_integral(encoded);
proto.encode_walk_shares(a0, a1, true);
EXPECT_EQ(static_cast<std::uint8_t>(a0) ^ static_cast<std::uint8_t>(a1),
static_cast<std::uint8_t>(encoded));
EXPECT_NE(a1, 0);
}

View file

@ -0,0 +1,536 @@
/// @file gf2_adversarial_test.cpp
/// @brief Adversarial checks for GF(2^k) output types.
/// @details Full domains, lane boundaries, poisoned high bits, independent
/// polynomial inverses, leaf scaling, shares, multi-output keys,
/// comparisons, and proof tampering.
#include <gtest/gtest.h>
#include "dpf.hpp"
#include <cstdint>
#include <cstring>
#include <iterator>
#include <vector>
namespace
{
using u128 = unsigned __int128;
u128 poly_mul(u128 a, u128 b)
{
unsigned __int128 p = 0;
for (int i = 0; i < 128 && b != 0; ++i)
{
if ((b & 1) != 0)
p ^= a;
a <<= 1;
b >>= 1;
}
return p;
}
u128 poly_quot_rem(u128 & rem, u128 den, int)
{
unsigned __int128 q = 0;
int den_bit = -1;
for (int i = 127; i >= 0; --i)
{
if (((den >> i) & 1) != 0)
{
den_bit = i;
break;
}
}
if (den_bit < 0)
return 0;
for (;;)
{
int bit = -1;
for (int i = 127; i >= 0; --i)
{
if (((rem >> i) & 1) != 0)
{
bit = i;
break;
}
}
if (bit < den_bit)
break;
const int sh = bit - den_bit;
q ^= u128{1} << sh;
rem ^= den << sh;
}
return q;
}
u128 poly_inv(u128 a, u128 mod)
{
u128 r0 = mod;
u128 r1 = a;
u128 s0 = 0;
u128 s1 = 1;
while (r1 != 0)
{
const u128 q = poly_quot_rem(r0, r1, 0);
const u128 nr = r0;
u128 ns = s0 ^ poly_mul(q, s1);
int mod_bit = -1;
for (int i = 127; i >= 0; --i)
{
if (((mod >> i) & 1) != 0)
{
mod_bit = i;
break;
}
}
while (mod_bit >= 0)
{
int bit = -1;
for (int i = 127; i >= 0; --i)
{
if (((ns >> i) & 1) != 0)
{
bit = i;
break;
}
}
if (bit < mod_bit)
break;
ns ^= mod << (bit - mod_bit);
}
r0 = r1;
s0 = s1;
r1 = nr;
s1 = ns;
}
return r0 == 1 ? s0 : 0;
}
template <typename F>
u128 modulus_of()
{
constexpr unsigned bits = F::bits;
if constexpr (bits == 1)
return 0x3;
else if constexpr (bits == 2)
return 0x7;
else if constexpr (bits == 4)
return 0x13;
else if constexpr (bits == 8)
return 0x11b;
else if constexpr (bits == 16)
return 0x1002d;
else if constexpr (bits == 32)
return 0x190200001ull;
else
return (u128{1} << 64) | (u128{1} << 63)
| (u128{1} << 62) | (u128{1} << 53) | 1;
}
template <typename F>
F mask_elem(u128 v)
{
using word = typename F::integral_type;
if constexpr (F::bits >= sizeof(word) * 8u)
return F{static_cast<word>(v)};
else
{
const word m = static_cast<word>((word{1} << F::bits) - word{1});
return F{static_cast<word>(static_cast<word>(v) & m)};
}
}
std::uint64_t rng_state = 0x123456789abcdefull;
std::uint64_t next_rng()
{
rng_state = rng_state * 6364136223846793005ull + 1ull;
return rng_state;
}
template <typename F>
F random_elem()
{
return mask_elem<F>(next_rng());
}
template <typename Key0, typename Key1, typename In, typename F>
F open_at(const Key0 & k0, const Key1 & k1, In x)
{
const auto y0 = *dpf::eval_point(k0, x);
const auto y1 = *dpf::eval_point(k1, x);
const F fwd = dpf::reconstruct(y0, y1);
const F rev = dpf::reconstruct(y1, y0);
EXPECT_EQ(fwd, rev);
return fwd;
}
template <typename F>
void expect_full_domain(F beta)
{
using In = std::uint8_t;
for (int alpha_i : {0, 1, 31, 32, 127, 128, 254, 255})
{
const In alpha = static_cast<In>(alpha_i);
auto [k0, k1] = dpf::make_dpf(alpha, beta);
for (int x = 0; x < 256; ++x)
{
const In q = static_cast<In>(x);
const F got = open_at<decltype(k0), decltype(k1), In, F>(k0, k1, q);
EXPECT_EQ(got, q == alpha ? beta : F{}) << +q << " alpha " << +alpha;
}
}
}
template <typename F, typename It0, typename It1>
F open_written(It0 it0, It1 it1)
{
using V0 = typename std::iterator_traits<It0>::value_type;
using V1 = typename std::iterator_traits<It1>::value_type;
V0 a = *it0;
V1 b = *it1;
if constexpr (dpf::is_secret_share_v<V0>)
return dpf::reconstruct(a, b);
else
{
const auto lane = [](auto v) {
return F{static_cast<typename F::integral_type>(static_cast<unsigned>(v))};
};
return lane(a) + lane(b);
}
}
template <typename F>
void expect_interval_and_sequence(F beta)
{
using In = std::uint8_t;
const In alpha = 0x2a;
auto [k0, k1] = dpf::make_dpf(alpha, beta);
auto [b0, i0] = dpf::eval_interval(k0, In{0}, In{255});
auto [b1, i1] = dpf::eval_interval(k1, In{0}, In{255});
auto p0 = std::begin(i0);
auto p1 = std::begin(i1);
for (int x = 0; x < 256; ++x, ++p0, ++p1)
{
EXPECT_EQ(open_written<F>(p0, p1),
static_cast<In>(x) == alpha ? beta : F{}) << x;
}
const In pts[] = {0, 1, 41, 42, 43, 127, 128, 255};
auto [s0, is0] = dpf::eval_sequence(k0, std::begin(pts), std::end(pts));
auto [s1, is1] = dpf::eval_sequence(k1, std::begin(pts), std::end(pts));
auto q0 = std::begin(is0);
auto q1 = std::begin(is1);
for (In q : pts)
{
EXPECT_EQ(open_written<F>(q0, q1),
q == alpha ? beta : F{}) << +q;
++q0;
++q1;
}
}
template <typename F>
void expect_comparison(F beta)
{
using In = std::uint8_t;
const In alpha = 10;
auto [lt0, lt1] = dpf::make_dpf(alpha, dpf::lt(beta));
auto [le0, le1] = dpf::make_dpf(alpha, dpf::leq(beta));
for (int x = 0; x < 256; ++x)
{
const In q = static_cast<In>(x);
const auto a0 = dpf::eval_point<F>(dpf::cmp, lt0, q);
const auto a1 = dpf::eval_point<F>(dpf::cmp, lt1, q);
const auto b0 = dpf::eval_point<F>(dpf::cmp, le0, q);
const auto b1 = dpf::eval_point<F>(dpf::cmp, le1, q);
EXPECT_EQ(dpf::reconstruct(a0, a1), x < 10 ? beta : F{}) << x;
EXPECT_EQ(dpf::reconstruct(b0, b1), x <= 10 ? beta : F{}) << x;
}
}
template <typename Node, typename F>
void expect_scale(Node node, F k)
{
const auto scaled = dpf::multiply_leaf(node, k);
unsigned char src[sizeof(Node)];
unsigned char got[sizeof(Node)];
std::memcpy(src, &node, sizeof(src));
std::memcpy(got, &scaled, sizeof(got));
if constexpr (F::bits >= 8)
{
constexpr std::size_t lanes = sizeof(Node) / sizeof(typename F::integral_type);
for (std::size_t i = 0; i < lanes; ++i)
{
typename F::integral_type lane{};
std::memcpy(&lane, src + i * sizeof(lane), sizeof(lane));
typename F::integral_type out{};
std::memcpy(&out, got + i * sizeof(out), sizeof(out));
EXPECT_EQ(F{out}, F{lane} * k);
}
}
else
{
constexpr unsigned w = F::bits;
constexpr unsigned mask = (1u << w) - 1u;
constexpr unsigned per = 8u / w;
for (std::size_t i = 0; i < sizeof(Node); ++i)
{
unsigned expect = 0;
for (unsigned lane = 0; lane < per; ++lane)
{
const auto a = F{(src[i] >> (lane * w)) & mask};
expect |= static_cast<unsigned>((a * k).raw()) << (lane * w);
}
EXPECT_EQ(got[i], static_cast<unsigned char>(expect));
}
}
}
template <typename F>
void expect_algebra()
{
const auto mod = modulus_of<F>();
const F one{1};
EXPECT_EQ(one + one, F{});
EXPECT_EQ(-one, one);
EXPECT_EQ(F{-7}, F{7});
EXPECT_EQ(one * one, one);
EXPECT_EQ(F{} * random_elem<F>(), F{});
const unsigned lim = F::bits <= 8 ? (1u << F::bits) : 0u;
if (lim != 0)
{
for (unsigned a = 1; a < lim; ++a)
{
const auto inv = poly_inv(a, mod);
ASSERT_NE(inv, 0u) << a;
EXPECT_EQ(F{static_cast<typename F::integral_type>(a)}
* mask_elem<F>(inv), one) << a;
}
for (unsigned a = 0; a < lim; ++a)
{
for (unsigned b = 0; b < lim; ++b)
{
const F prod = F{static_cast<typename F::integral_type>(a)}
* F{static_cast<typename F::integral_type>(b)};
if (a != 0 && b != 0)
EXPECT_NE(prod, F{}) << a << " " << b;
}
}
}
else
{
for (int n = 0; n < 48; ++n)
{
const F a = random_elem<F>();
if (a == F{})
continue;
const auto inv = poly_inv(a.raw(), mod);
ASSERT_NE(inv, 0u);
EXPECT_EQ(a * mask_elem<F>(inv), one);
}
}
for (int n = 0; n < 32; ++n)
{
const F a = random_elem<F>();
const F b = random_elem<F>();
const F c = random_elem<F>();
EXPECT_EQ((a + b) * c, a * c + b * c);
EXPECT_EQ((a * b) * c, a * (b * c));
EXPECT_EQ(a + a, F{});
}
unsigned char poison[sizeof(F)];
std::memset(poison, 0xff, sizeof(poison));
F poisoned{};
std::memcpy(&poisoned, poison, sizeof(poisoned));
EXPECT_EQ(poisoned.raw(), mask_elem<F>(~u128{0}).raw());
EXPECT_EQ(poisoned + F{}, mask_elem<F>(~u128{0}));
unsigned char lo[16]{};
unsigned char hi[16]{};
lo[0] = 0x15;
hi[0] = 0x15;
if (sizeof(typename F::integral_type) < 16)
hi[sizeof(typename F::integral_type)] = 0x5a;
EXPECT_EQ(F::from_seed(lo, sizeof(lo)), F::from_seed(hi, sizeof(hi)));
}
template <typename F>
void expect_leaf_ops()
{
alignas(32) unsigned char bytes[32];
for (int i = 0; i < 32; ++i)
bytes[i] = static_cast<unsigned char>(0xA5 ^ i);
simde__m128i n128;
simde__m256i n256;
std::memcpy(&n128, bytes, sizeof(n128));
std::memcpy(&n256, bytes, sizeof(n256));
const F k = F::bits == 1 ? F{1} : F{2};
expect_scale(n128, k);
expect_scale(n128, F{});
expect_scale(n128, F{1});
expect_scale(n256, k);
const auto sum = dpf::add_leaf<F>(n128, n128);
unsigned char z[sizeof(n128)];
std::memcpy(z, &sum, sizeof(z));
for (unsigned char b : z)
EXPECT_EQ(b, 0);
unsigned char d[sizeof(n128)];
const auto sub = dpf::subtract_leaf<F>(n128, n128);
std::memcpy(d, &sub, sizeof(d));
for (unsigned char b : d)
EXPECT_EQ(b, 0);
}
template <typename F>
void expect_shares_and_prefix(F beta)
{
const auto add = dpf::make_additive_shares(beta);
EXPECT_EQ(dpf::reconstruct(add.first, add.second), beta);
EXPECT_EQ(dpf::reconstruct(add.second, add.first), beta);
const auto sub = dpf::make_subtractive_shares(beta);
EXPECT_EQ(dpf::reconstruct(sub.first, sub.second), beta);
EXPECT_EQ(dpf::reconstruct(sub.second, sub.first), beta);
auto drawn = dpf::additively_share(beta);
EXPECT_EQ(dpf::reconstruct(drawn.first, drawn.second), beta);
}
template <typename F>
void expect_verifiable(F beta)
{
using In = std::uint8_t;
const In alpha = 0x11;
auto [k0, k1] = dpf::make_dpf(alpha, beta, dpf::verifiable{});
for (int x = 0; x < 256; ++x)
{
const In q = static_cast<In>(x);
dpf::proof_token a{};
dpf::proof_token b{};
const auto y0 = *dpf::eval_point(k0, q, dpf::prove(a));
const auto y1 = *dpf::eval_point(k1, q, dpf::prove(b));
EXPECT_TRUE(dpf::verify(a, b)) << +q;
EXPECT_EQ(dpf::reconstruct(y0, y1), q == alpha ? beta : F{});
}
auto & leaves = const_cast<std::decay_t<decltype(k0.leaves())> &>(k0.leaves());
auto * raw = reinterpret_cast<unsigned char *>(&std::get<0>(leaves).get());
raw[0] = static_cast<unsigned char>(raw[0] ^ 0x1u);
dpf::proof_token tampered{};
dpf::proof_token honest{};
(void)*dpf::eval_point(k0, alpha, dpf::prove(tampered));
(void)*dpf::eval_point(k1, alpha, dpf::prove(honest));
EXPECT_FALSE(dpf::verify(tampered, honest));
}
template <typename F>
void run_all(F beta)
{
expect_algebra<F>();
expect_full_domain(F{});
expect_full_domain(F{1});
expect_full_domain(beta);
expect_interval_and_sequence(beta);
expect_comparison(beta);
expect_leaf_ops<F>();
expect_shares_and_prefix(beta);
}
} // namespace
TEST(Gf2Adversarial, AlgebraDomainAndLeaves)
{
run_all(dpf::gf2{1});
run_all(dpf::gf22{3});
run_all(dpf::gf24{0xa});
run_all(dpf::gf28{0x1b});
run_all(dpf::gf216{0x2d});
run_all(dpf::gf232{0x90200001u});
run_all(dpf::gf264{0x11});
}
TEST(Gf2Adversarial, ProofsRejectAFlippedLeaf)
{
expect_verifiable(dpf::gf2{1});
expect_verifiable(dpf::gf24{0xf});
expect_verifiable(dpf::gf28{0xff});
expect_verifiable(dpf::gf264{~std::uint64_t{0}});
}
TEST(Gf2Adversarial, PrefixLongerThanTheLane)
{
using In = std::uint8_t;
const In alpha = 0x2a;
auto [k0, k1] = dpf::make_dpf(alpha,
dpf::at<8>(dpf::gf28{0x1b}), dpf::gf28{0x5a});
const auto p0 = *dpf::eval_point<0>(k0, alpha);
const auto p1 = *dpf::eval_point<0>(k1, alpha);
const auto l0 = *dpf::eval_point<1>(k0, alpha);
const auto l1 = *dpf::eval_point<1>(k1, alpha);
EXPECT_EQ(dpf::reconstruct(p0, p1), dpf::gf28{0x1b});
EXPECT_EQ(dpf::reconstruct(l0, l1), dpf::gf28{0x5a});
const auto off0 = *dpf::eval_point<1>(k0, In{0});
const auto off1 = *dpf::eval_point<1>(k1, In{0});
EXPECT_EQ(dpf::reconstruct(off0, off1), dpf::gf28{});
}
TEST(Gf2Adversarial, MultiOutputDoesNotBleed)
{
using In = std::uint8_t;
const In alpha = 0x2a;
const dpf::gf28 lane{0x1b};
const std::uint8_t wide = 9;
auto [k0, k1] = dpf::make_dpf(alpha, lane, wide);
for (int x = 0; x < 64; ++x)
{
const In q = static_cast<In>(x);
const auto a0 = *dpf::eval_point<0>(k0, q);
const auto a1 = *dpf::eval_point<0>(k1, q);
const auto b0 = *dpf::eval_point<1>(k0, q);
const auto b1 = *dpf::eval_point<1>(k1, q);
EXPECT_EQ(dpf::reconstruct(a0, a1), q == alpha ? lane : dpf::gf28{});
EXPECT_EQ(dpf::reconstruct(b0, b1), q == alpha ? wide : std::uint8_t{0});
}
}
TEST(Gf2Adversarial, NakedLeafMasksHighBits)
{
const auto leaf = dpf::make_naked_leaf<simde__m128i>(std::uint8_t{3}, dpf::gf24{0xF5});
EXPECT_EQ((dpf::extract_leaf<simde__m128i, dpf::gf24>(leaf, std::uint8_t{3})),
dpf::gf24{0x5});
EXPECT_EQ((dpf::extract_leaf<simde__m128i, dpf::gf24>(leaf, std::uint8_t{2})),
dpf::gf24{});
EXPECT_EQ((dpf::extract_leaf<simde__m128i, dpf::gf24>(leaf, std::uint8_t{4})),
dpf::gf24{});
EXPECT_EQ((dpf::extract_leaf<simde__m128i, dpf::gf24>(leaf, std::uint8_t{31})),
dpf::gf24{});
}
TEST(Gf2Adversarial, UnassignedWildcardThrows)
{
auto [w0, w1] = dpf::make_dpf(std::uint8_t{3}, dpf::wildcard_value<dpf::gf28>{});
EXPECT_THROW((void)*dpf::eval_point(w0, std::uint8_t{3}), std::runtime_error);
EXPECT_THROW((void)*dpf::eval_point(w1, std::uint8_t{3}), std::runtime_error);
auto [p0, p1] = dpf::make_dpf(std::uint8_t{1}, dpf::wildcard_value<dpf::gf2>{});
EXPECT_THROW((void)*dpf::eval_point(p0, std::uint8_t{1}), std::runtime_error);
(void)p1;
}
TEST(Gf2Adversarial, FromSeedDropsBytesPastTheWord)
{
unsigned char narrow[16]{};
unsigned char wide[16];
std::memset(wide, 0xa5, sizeof(wide));
narrow[0] = 0x15;
wide[0] = 0x15;
EXPECT_EQ(dpf::gf24::from_seed(narrow, 16), dpf::gf24{0x5});
EXPECT_EQ(dpf::gf24::from_seed(wide, 16), dpf::gf24{0x5});
narrow[0] = 0xab;
wide[0] = 0xab;
EXPECT_EQ(dpf::gf28::from_seed(narrow, 1), dpf::gf28::from_seed(wide, 16));
}

306
test/tests/gf2_test.cpp Normal file
View file

@ -0,0 +1,306 @@
#include <gtest/gtest.h>
#include "dpf.hpp"
#include <array>
#include <cstdint>
#include <cstring>
#include <stdexcept>
#include <type_traits>
#include <utility>
namespace
{
template <typename F>
F pow_elem(F base, unsigned long long exp)
{
F r{1};
while (exp != 0)
{
if ((exp & 1ull) != 0)
r = r * base;
exp >>= 1;
if (exp != 0)
base = base * base;
}
return r;
}
template <typename F>
void expect_field_laws()
{
constexpr unsigned bits = F::bits;
EXPECT_EQ(F{0} + F{1}, F{1});
EXPECT_EQ(F{1} + F{1}, F{0});
EXPECT_EQ(-F{1}, F{1});
EXPECT_EQ(F{-1}, F{1});
EXPECT_EQ(F{1} * F{1}, F{1});
EXPECT_EQ(F{0} * F{5}, F{0});
EXPECT_TRUE(std::is_trivially_copyable_v<F>);
EXPECT_TRUE(std::is_standard_layout_v<F>);
EXPECT_TRUE(dpf::utils::has_characteristic_two_v<F>);
EXPECT_EQ(dpf::utils::bitlength_of_v<F>, bits);
EXPECT_EQ(dpf::utils::make_default_v<F>, F{1});
if constexpr (bits > 1)
{
const F x{2};
F xpow{1};
for (unsigned i = 0; i < bits; ++i)
xpow = xpow * x;
if constexpr (bits == 2 || bits == 4)
EXPECT_EQ(xpow, F{0x3});
else if constexpr (bits == 8)
EXPECT_EQ(xpow, F{0x1b});
else if constexpr (bits == 16)
EXPECT_EQ(xpow, F{0x2d});
else if constexpr (bits == 32)
EXPECT_EQ(xpow.raw(), 0x90200001u);
else
{
const auto tail = static_cast<typename F::integral_type>(
(typename F::integral_type{1} << 63)
| (typename F::integral_type{1} << 62)
| (typename F::integral_type{1} << 53)
| typename F::integral_type{1});
EXPECT_EQ(xpow.raw(), tail);
}
}
const F a{0x13};
const F b{0x2a};
const F c{0x7};
EXPECT_EQ((a + b) * c, a * c + b * c);
EXPECT_EQ((a * b) * c, a * (b * c));
if constexpr (bits <= 32)
{
if (a != F{0})
EXPECT_EQ(a * pow_elem(a, (1ull << bits) - 2ull), F{1});
}
if constexpr (bits <= 8)
{
const unsigned lim = 1u << bits;
for (unsigned i = 1; i < lim; ++i)
EXPECT_EQ(F{i} * pow_elem(F{i}, (1ull << bits) - 2ull), F{1}) << i;
}
}
template <typename Key0, typename Key1, typename In>
auto open_at(const Key0 & k0, const Key1 & k1, In x)
{
const auto y0 = *dpf::eval_point(k0, x);
const auto y1 = *dpf::eval_point(k1, x);
return dpf::reconstruct(y0, y1);
}
template <typename Out, typename In>
void expect_point_payload(In alpha, Out beta)
{
auto [k0, k1] = dpf::make_dpf(alpha, beta);
for (int x = 0; x < 32; ++x)
{
const In q = static_cast<In>(x);
const Out got = open_at(k0, k1, q);
EXPECT_EQ(got, q == alpha ? beta : Out{}) << static_cast<unsigned>(x);
}
EXPECT_EQ(open_at(k0, k1, alpha), beta);
}
template <typename Out, typename Spec>
void expect_cmp(std::uint8_t alpha, Out beta, Spec spec, bool leq)
{
auto [k0, k1] = dpf::make_dpf(alpha, spec);
for (int x = 0; x < 24; ++x)
{
const auto q = static_cast<std::uint8_t>(x);
const auto y0 = dpf::eval_point<Out>(dpf::cmp, k0, q);
const auto y1 = dpf::eval_point<Out>(dpf::cmp, k1, q);
const bool hot = leq ? x <= static_cast<int>(alpha)
: x < static_cast<int>(alpha);
EXPECT_EQ(dpf::reconstruct(y0, y1), hot ? beta : Out{}) << x;
}
}
} // namespace
TEST(Gf2, FieldLaws)
{
expect_field_laws<dpf::gf2>();
expect_field_laws<dpf::gf22>();
expect_field_laws<dpf::gf24>();
expect_field_laws<dpf::gf28>();
expect_field_laws<dpf::gf216>();
expect_field_laws<dpf::gf232>();
expect_field_laws<dpf::gf264>();
}
TEST(Gf2, SubByteAdditionIsXor)
{
EXPECT_EQ(dpf::gf22{3} + dpf::gf22{1}, dpf::gf22{2});
EXPECT_EQ(dpf::gf24{0xf} + dpf::gf24{1}, dpf::gf24{0xe});
EXPECT_EQ(dpf::gf24{2} * dpf::gf24{2}, dpf::gf24{4});
EXPECT_EQ(dpf::gf22{2} * dpf::gf22{2}, dpf::gf22{3});
}
TEST(Gf2, LeafScaleIsPerLane)
{
alignas(16) unsigned char bytes[16];
for (int i = 0; i < 16; ++i)
bytes[i] = 0xe4;
simde__m128i node;
std::memcpy(&node, bytes, sizeof(node));
const auto scaled = dpf::multiply_leaf(node, dpf::gf24{2});
unsigned char out[16];
std::memcpy(out, &scaled, sizeof(out));
const auto lo = static_cast<unsigned>((dpf::gf24{0x4} * dpf::gf24{2}).raw());
const auto hi = static_cast<unsigned>((dpf::gf24{0xe} * dpf::gf24{2}).raw());
const auto expect = static_cast<unsigned char>(lo | (hi << 4));
for (unsigned char b : out)
EXPECT_EQ(b, expect);
const auto summed = dpf::add_leaf<dpf::gf24>(node, node);
unsigned char z[16];
std::memcpy(z, &summed, sizeof(z));
for (unsigned char b : z)
EXPECT_EQ(b, 0);
}
TEST(Gf2, ShufbScalarVectorMatchesFieldMul)
{
alignas(32) unsigned char bytes[33];
for (int i = 0; i < 33; ++i)
bytes[i] = static_cast<unsigned char>(i * 17 + 3);
const unsigned scalars[] = {0u, 1u, 2u, 0x1bu, 0x80u, 0xffu, 0x2du, 0x8000u, 0xffffu};
for (unsigned s : scalars)
{
if (s > 0xffu)
continue;
simde__m128i n128;
simde__m256i n256;
std::memcpy(&n128, bytes, 16);
std::memcpy(&n256, bytes, 32);
const auto a128 = dpf::multiply_leaf(n128, dpf::gf28{s});
const auto a256 = dpf::multiply_leaf(n256, dpf::gf28{s});
unsigned char o128[16];
unsigned char o256[32];
std::memcpy(o128, &a128, 16);
std::memcpy(o256, &a256, 32);
for (int i = 0; i < 16; ++i)
EXPECT_EQ(o128[i], (dpf::gf28{bytes[i]} * dpf::gf28{s}).raw()) << s << " " << i;
for (int i = 0; i < 32; ++i)
EXPECT_EQ(o256[i], (dpf::gf28{bytes[i]} * dpf::gf28{s}).raw()) << s << " " << i;
unsigned char tail[33];
std::memcpy(tail, bytes, 33);
dpf::gf2_detail::scale_gf28(tail, bytes, 33, static_cast<std::uint8_t>(s));
for (int i = 0; i < 33; ++i)
EXPECT_EQ(tail[i], (dpf::gf28{bytes[i]} * dpf::gf28{s}).raw()) << "tail " << i;
}
alignas(32) unsigned char lanes[32];
for (int i = 0; i < 16; ++i)
{
const auto lane = static_cast<std::uint16_t>(i * 19 + 1);
lanes[2 * i] = static_cast<unsigned char>(lane);
lanes[2 * i + 1] = static_cast<unsigned char>(lane >> 8);
}
for (unsigned s : scalars)
{
simde__m256i node;
std::memcpy(&node, lanes, 32);
const auto scaled = dpf::multiply_leaf(node, dpf::gf216{s});
unsigned char got[32];
std::memcpy(got, &scaled, 32);
for (int i = 0; i < 16; ++i)
{
const auto lane = static_cast<std::uint16_t>(lanes[2 * i] | (lanes[2 * i + 1] << 8));
const auto prod = (dpf::gf216{lane} * dpf::gf216{s}).raw();
EXPECT_EQ(got[2 * i], static_cast<unsigned char>(prod)) << s << " " << i;
EXPECT_EQ(got[2 * i + 1], static_cast<unsigned char>(prod >> 8)) << s << " " << i;
}
}
}
TEST(Gf2, PointPayload)
{
expect_point_payload<dpf::gf2>(std::uint8_t{0x2a}, dpf::gf2{1});
expect_point_payload<dpf::gf22>(std::uint8_t{0x11}, dpf::gf22{3});
expect_point_payload<dpf::gf24>(std::uint8_t{0x05}, dpf::gf24{0xa});
expect_point_payload<dpf::gf28>(std::uint8_t{0x2a}, dpf::gf28{0x1b});
expect_point_payload<dpf::gf216>(std::uint8_t{0x07}, dpf::gf216{0x2d});
expect_point_payload<dpf::gf232>(std::uint8_t{0x13}, dpf::gf232{0x90200001u});
expect_point_payload<dpf::gf264>(std::uint8_t{0x04}, dpf::gf264{0x11});
}
TEST(Gf2, ComparisonPayload)
{
expect_cmp(std::uint8_t{10}, dpf::gf24{0x7}, dpf::lt(dpf::gf24{0x7}), false);
expect_cmp(std::uint8_t{10}, dpf::gf28{0x1b}, dpf::leq(dpf::gf28{0x1b}), true);
expect_cmp(std::uint8_t{4}, dpf::gf264{0x53}, dpf::lt(dpf::gf264{0x53}), false);
}
template <typename F>
void expect_inv_exhaustive()
{
EXPECT_THROW(dpf::detail::shamir_field<F>::inv(F{0}), std::invalid_argument);
const unsigned long long n = 1ull << F::bits;
for (unsigned long long i = 1; i < n; ++i)
{
const F a{static_cast<typename F::integral_type>(i)};
const F b = dpf::detail::shamir_field<F>::inv(a);
EXPECT_EQ(a * b, F{1}) << i;
}
}
template <typename F>
void expect_shamir23(F secret, F slope)
{
const auto shares = dpf::shamir::deal<F, 2, 3>(secret, std::array<F, 1>{{slope}});
EXPECT_EQ((dpf::shamir::reconstruct(std::get<0>(shares), std::get<1>(shares))), secret);
EXPECT_EQ((dpf::shamir::reconstruct(std::get<1>(shares), std::get<2>(shares))), secret);
EXPECT_EQ((dpf::shamir::reconstruct(std::get<2>(shares), std::get<0>(shares))), secret);
EXPECT_EQ((dpf::shamir::reconstruct(
std::get<0>(shares), std::get<1>(shares), std::get<2>(shares))), secret);
}
TEST(Gf2, InverseAndShamir)
{
expect_inv_exhaustive<dpf::gf2>();
expect_inv_exhaustive<dpf::gf22>();
expect_inv_exhaustive<dpf::gf24>();
expect_inv_exhaustive<dpf::gf28>();
const dpf::gf216 wide[] = {dpf::gf216{1}, dpf::gf216{2}, dpf::gf216{0x2d},
dpf::gf216{0xffff}};
for (auto a : wide)
EXPECT_EQ(a * dpf::detail::shamir_field<dpf::gf216>::inv(a), dpf::gf216{1});
const dpf::gf232 mid[] = {dpf::gf232{1}, dpf::gf232{2}, dpf::gf232{0x190200001u}};
for (auto a : mid)
EXPECT_EQ(a * dpf::detail::shamir_field<dpf::gf232>::inv(a), dpf::gf232{1});
const dpf::gf264 big[] = {dpf::gf264{1}, dpf::gf264{2}, dpf::gf264{~std::uint64_t{0}}};
for (auto a : big)
EXPECT_EQ(a * dpf::detail::shamir_field<dpf::gf264>::inv(a), dpf::gf264{1});
expect_shamir23(dpf::gf22{1}, dpf::gf22{2});
expect_shamir23(dpf::gf28{0x1b}, dpf::gf28{0x5a});
expect_shamir23(dpf::gf264{0x11}, dpf::gf264{0x53});
const auto shares = dpf::shamir::deal<dpf::gf28, 3, 5>(
dpf::gf28{0x1b}, std::array<dpf::gf28, 2>{{dpf::gf28{2}, dpf::gf28{9}}});
EXPECT_EQ((dpf::shamir::reconstruct(
std::get<0>(shares), std::get<2>(shares), std::get<4>(shares))),
dpf::gf28{0x1b});
// Point 2 is 0 in GF(2), so that party would hold the secret.
const auto leaked = dpf::shamir::deal<dpf::gf2, 2, 3>(
dpf::gf2{1}, std::array<dpf::gf2, 1>{{dpf::gf2{1}}});
EXPECT_EQ(std::get<1>(leaked).raw(), dpf::gf2{1}.raw());
EXPECT_THROW((dpf::shamir::reconstruct(std::get<0>(leaked), std::get<1>(leaked))),
std::invalid_argument);
const auto one = dpf::shamir::deal<dpf::gf2, 1, 1>(dpf::gf2{1}, std::array<dpf::gf2, 0>{});
EXPECT_EQ(dpf::shamir::reconstruct(std::get<0>(one)), dpf::gf2{1});
}

View file

@ -0,0 +1,487 @@
#include <gtest/gtest.h>
#include <array>
#include <cstdint>
#include <cstring>
#include <type_traits>
#include <utility>
#include "dpf.hpp"
namespace
{
using block_t = dpf::prg::aes128::block_type;
constexpr std::size_t kBits = 8;
static block_t g_roots[2];
static int g_ri = 0;
block_t take_root()
{
return g_roots[g_ri++];
}
void reset_roots(block_t r0, block_t r1)
{
g_roots[0] = r0;
g_roots[1] = r1;
g_ri = 0;
}
std::uint8_t sibling_at(std::uint8_t alpha, std::size_t level)
{
return static_cast<std::uint8_t>(alpha ^ (1u << (kBits - 1 - level)));
}
template <std::size_t I, std::size_t N, typename Keys>
auto grow_rest(Keys keys, std::uint8_t alpha,
const std::array<std::uint64_t, N> & betas)
{
if constexpr (I >= N)
return keys;
else
{
auto next = dpf::extend(keys.first, keys.second, alpha,
dpf::at<I + 1>(betas[I]));
return grow_rest<I + 1, N>(std::move(next), alpha, betas);
}
}
template <std::size_t N>
auto grow_incrementally(std::uint8_t alpha,
const std::array<std::uint64_t, N> & betas, block_t r0, block_t r1)
{
reset_roots(r0, r1);
auto keys = dpf::make_dpf(alpha,
dpf::root_sampler_t<dpf::prg::aes128>{take_root},
dpf::at<1>(betas[0]));
return grow_rest<1, N>(std::move(keys), alpha, betas);
}
} // namespace
TEST(GrowingIdpf, PrefixesOpenToOwnPayload)
{
const std::uint8_t alpha = 0xB2;
const std::array<std::uint64_t, kBits> betas = {
0x11ull, 0x2222ull, 0x333333ull, 0x44444444ull,
0x5555555555ull, 0x666666666666ull, 0x77777777777777ull,
0x8888888888888888ull};
block_t r0 = dpf::uniform_sample<block_t>();
block_t r1 = dpf::uniform_sample<block_t>();
auto [k0, k1] = grow_incrementally(alpha, betas, r0, r1);
using KT = std::decay_t<decltype(k0)>;
EXPECT_EQ(KT::depth, kBits - 1);
EXPECT_EQ(KT::num_outputs, kBits);
auto open = [&](auto out_ic, std::uint8_t x) {
return dpf::reconstruct(
*dpf::eval_point(out_ic, k0, x),
*dpf::eval_point(out_ic, k1, x));
};
[&]<std::size_t... L>(std::index_sequence<L...>) {
(([&] {
EXPECT_EQ(open(dpf::out<L>, alpha), betas[L]) << "prefix " << L;
EXPECT_EQ(open(dpf::out<L>, sibling_at(alpha, L)), 0ull)
<< "sibling at prefix " << L;
}()), ...);
}(std::make_index_sequence<kBits>{});
}
TEST(GrowingIdpf, ExtendMatchesFullySpecified)
{
const std::uint8_t alpha = 0x6D;
const std::array<std::uint64_t, kBits> betas = {
1001, 2002, 3003, 4004, 5005, 6006, 7007, 8008};
block_t r0 = dpf::uniform_sample<block_t>();
block_t r1 = dpf::uniform_sample<block_t>();
auto [k0, k1] = grow_incrementally(alpha, betas, r0, r1);
reset_roots(r0, r1);
auto [ref0, ref1] = dpf::make_dpf(alpha,
dpf::root_sampler_t<dpf::prg::aes128>{take_root},
dpf::at<1>(betas[0]), dpf::at<2>(betas[1]), dpf::at<3>(betas[2]),
dpf::at<4>(betas[3]), dpf::at<5>(betas[4]), dpf::at<6>(betas[5]),
dpf::at<7>(betas[6]), dpf::at<8>(betas[7]));
using KT = std::decay_t<decltype(k0)>;
static_assert(std::is_same_v<KT, std::decay_t<decltype(ref0)>>);
EXPECT_EQ(std::memcmp(k0.correction_words().data(),
ref0.correction_words().data(),
sizeof(typename KT::correction_words_array)), 0);
EXPECT_EQ(std::memcmp(k0.correction_advice().data(),
ref0.correction_advice().data(),
sizeof(typename KT::correction_advice_array)), 0);
EXPECT_EQ(std::memcmp(&k0.root(), &ref0.root(), sizeof(block_t)), 0);
EXPECT_EQ(std::memcmp(&k1.root(), &ref1.root(), sizeof(block_t)), 0);
[&]<std::size_t... L>(std::index_sequence<L...>) {
(([&] {
EXPECT_EQ(std::memcmp(&k0.template leaf<L>(),
&ref0.template leaf<L>(), sizeof(k0.template leaf<L>())), 0)
<< "leaf0 " << L;
EXPECT_EQ(std::memcmp(&k1.template leaf<L>(),
&ref1.template leaf<L>(), sizeof(k1.template leaf<L>())), 0)
<< "leaf1 " << L;
EXPECT_EQ(dpf::reconstruct(
*dpf::eval_point(dpf::out<L>, k0, alpha),
*dpf::eval_point(dpf::out<L>, k1, alpha)),
betas[L])
<< "open " << L;
}()), ...);
}(std::make_index_sequence<kBits>{});
}
TEST(GrowingIdpf, AddOutputWildcardOnExistingLevel)
{
const std::uint8_t alpha = 0xA5;
block_t r0 = dpf::uniform_sample<block_t>();
block_t r1 = dpf::uniform_sample<block_t>();
reset_roots(r0, r1);
auto [k0, k1] = dpf::make_dpf(alpha,
dpf::root_sampler_t<dpf::prg::aes128>{take_root},
dpf::at<2>(std::uint64_t{42}));
// at<2>(u64) → level 1. at<3>(u32) → level 1 (lg_opl=2), new group.
auto [a0, a1] = dpf::add_output(k0, k1, alpha,
dpf::at<3>(dpf::wildcard_value<std::uint32_t>{}));
using AT = std::decay_t<decltype(a0)>;
EXPECT_EQ(AT::depth, 1u);
EXPECT_EQ(AT::num_outputs, 2u);
EXPECT_TRUE(AT::wildcard_bits[1]);
auto o0 = dpf::eval_point(dpf::out<0>, a0, alpha);
auto o1 = dpf::eval_point(dpf::out<0>, a1, alpha);
EXPECT_EQ(dpf::reconstruct(*o0, *o1), 42ull);
}
TEST(GrowingIdpf, DeepestPrefixMatchesClassicDpf)
{
const std::uint8_t alpha = 0xC3;
const std::array<std::uint64_t, kBits> betas = {
1, 2, 4, 8, 16, 32, 64, 128};
block_t r0 = dpf::uniform_sample<block_t>();
block_t r1 = dpf::uniform_sample<block_t>();
auto [k0, k1] = grow_incrementally(alpha, betas, r0, r1);
auto recon = [&](std::uint8_t x) {
return dpf::reconstruct(
*dpf::eval_point(dpf::out<kBits - 1>, k0, x),
*dpf::eval_point(dpf::out<kBits - 1>, k1, x));
};
auto [c0, c1] = dpf::make_dpf(alpha, betas.back());
for (std::uint32_t x = 0; x < 256; ++x)
{
const auto want = dpf::reconstruct(
*dpf::eval_point(c0, static_cast<std::uint8_t>(x)),
*dpf::eval_point(c1, static_cast<std::uint8_t>(x)));
EXPECT_EQ(recon(static_cast<std::uint8_t>(x)),
static_cast<std::uint64_t>(want))
<< "x=" << x;
}
}
TEST(GrowingIdpf, ChaChaPrgOpensThePrefix)
{
using prg = dpf::prg::chacha20;
using block = prg::block_type;
static block roots[2];
static int ri = 0;
auto take = +[]() -> block { return roots[ri++]; };
const std::uint8_t alpha = 0x3C;
const std::uint64_t beta = 0x1234;
roots[0] = dpf::uniform_sample<block>();
roots[1] = dpf::uniform_sample<block>();
ri = 0;
auto [k0, k1] = dpf::make_dpf<prg, prg>(alpha,
dpf::root_sampler_t<prg>{take}, dpf::at<1>(beta));
const auto opened = dpf::reconstruct(
*dpf::eval_point(dpf::out<0>, k0, alpha),
*dpf::eval_point(dpf::out<0>, k1, alpha));
EXPECT_EQ(opened, beta);
}
namespace
{
struct GrowPad
{
simde__m128i block() { return dpf::uniform_sample<simde__m128i>(); }
std::uint8_t bit()
{
return static_cast<std::uint8_t>(
dpf::uniform_sample<unsigned char>() & 1u);
}
};
} // namespace
TEST(GrowingIdpf, MemoizerExtendMatchesRewalk)
{
const std::uint8_t alpha = 0xB2;
block_t r0 = dpf::uniform_sample<block_t>();
block_t r1 = dpf::uniform_sample<block_t>();
reset_roots(r0, r1);
auto [k0, k1] = dpf::make_dpf(alpha,
dpf::root_sampler_t<dpf::prg::aes128>{take_root},
dpf::at<1>(std::uint64_t{7}));
auto [d0, d1] = dpf::extend(k0, k1, alpha, dpf::at<2>(std::uint64_t{9}));
auto m0 = dpf::make_basic_path_memoizer(k0);
auto m1 = dpf::make_basic_path_memoizer(k1);
(void)dpf::eval_point(dpf::out<0>, k0, alpha, m0);
(void)dpf::eval_point(dpf::out<0>, k1, alpha, m1);
auto [m_k0, m_k1] =
dpf::extend(k0, k1, m0, m1, alpha, dpf::at<2>(std::uint64_t{9}));
EXPECT_EQ(0, std::memcmp(d0.correction_words().data(),
m_k0.correction_words().data(),
sizeof(d0.correction_words())));
EXPECT_EQ(dpf::reconstruct(*dpf::eval_point(dpf::out<0>, m_k0, alpha),
*dpf::eval_point(dpf::out<0>, m_k1, alpha)),
7ull);
EXPECT_EQ(dpf::reconstruct(*dpf::eval_point(dpf::out<1>, m_k0, alpha),
*dpf::eval_point(dpf::out<1>, m_k1, alpha)),
9ull);
(void)d1;
}
TEST(GrowingIdpf, ExtendDsMatchesDealer)
{
const std::uint8_t alpha = 0xB2;
block_t r0 = dpf::uniform_sample<block_t>();
block_t r1 = dpf::uniform_sample<block_t>();
reset_roots(r0, r1);
auto [k0, k1] = dpf::make_dpf(alpha,
dpf::root_sampler_t<dpf::prg::aes128>{take_root},
dpf::at<1>(std::uint64_t{7}));
auto [d0, d1] = dpf::extend(k0, k1, alpha, dpf::at<2>(std::uint64_t{9}));
auto m0 = dpf::make_basic_path_memoizer(k0);
auto m1 = dpf::make_basic_path_memoizer(k1);
(void)dpf::eval_point(dpf::out<0>, k0, alpha, m0);
(void)dpf::eval_point(dpf::out<0>, k1, alpha, m1);
GrowPad pads{};
dpf::local_cw_protocol<GrowPad> proto{pads};
auto [s0, s1] = dpf::extend_ds(k0, k1, m0, m1, alpha, std::uint8_t{0}, proto,
dpf::at<2>(std::uint64_t{9}));
EXPECT_EQ(0, std::memcmp(d0.correction_words().data(),
s0.correction_words().data(),
sizeof(d0.correction_words())));
EXPECT_EQ(dpf::reconstruct(*dpf::eval_point(dpf::out<1>, s0, alpha),
*dpf::eval_point(dpf::out<1>, s1, alpha)),
9ull);
(void)d1;
}
TEST(GrowingIdpf, AddOutputDsWildcard)
{
const std::uint8_t alpha = 0xA5;
block_t r0 = dpf::uniform_sample<block_t>();
block_t r1 = dpf::uniform_sample<block_t>();
reset_roots(r0, r1);
auto [k0, k1] = dpf::make_dpf(alpha,
dpf::root_sampler_t<dpf::prg::aes128>{take_root},
dpf::at<2>(std::uint64_t{42}));
auto m0 = dpf::make_basic_path_memoizer(k0);
auto m1 = dpf::make_basic_path_memoizer(k1);
(void)dpf::eval_point(dpf::out<0>, k0, alpha, m0);
(void)dpf::eval_point(dpf::out<0>, k1, alpha, m1);
GrowPad pads{};
dpf::local_cw_protocol<GrowPad> proto{pads};
auto [a0, a1] = dpf::add_output_ds(k0, k1, m0, m1, alpha, std::uint8_t{0},
proto, dpf::at<3>(dpf::wildcard_value<std::uint32_t>{}));
EXPECT_EQ(std::decay_t<decltype(a0)>::num_outputs, 2u);
EXPECT_EQ(dpf::reconstruct(*dpf::eval_point(dpf::out<0>, a0, alpha),
*dpf::eval_point(dpf::out<0>, a1, alpha)),
42ull);
}
TEST(GrowingIdpf, ShallowMemoizerThrows)
{
const std::uint8_t alpha = 0x10;
auto [k0, k1] = dpf::make_dpf(alpha, dpf::at<2>(std::uint64_t{1}));
using bare = dpf::unwrap_party_key_t<std::decay_t<decltype(k0)>>;
auto m0 = dpf::make_basic_path_memoizer(k0);
auto m1 = dpf::make_basic_path_memoizer(k1);
const bare & b0 = k0;
const bare & b1 = k1;
dpf::detail::ensure_level(b0, alpha, m0, 0);
dpf::detail::ensure_level(b1, alpha, m1, 0);
EXPECT_THROW(
(void)dpf::extend(k0, k1, m0, m1, alpha, dpf::at<3>(std::uint64_t{2})),
std::invalid_argument);
}
TEST(GrowingIdpfDeath, MismatchedKeyPairThrows)
{
const std::uint8_t alpha = 0x55;
// depth >= 1 so correction-word memcmp runs
auto [k0, _] = dpf::make_dpf(alpha, dpf::at<2>(std::uint64_t{1}));
auto [__, k1] = dpf::make_dpf(alpha, dpf::at<2>(std::uint64_t{1}));
EXPECT_THROW(
(void)dpf::extend(k0, k1, alpha, dpf::at<3>(std::uint64_t{2})),
std::invalid_argument);
}
TEST(GrowingIdpfDeath, WrongPathBitThrows)
{
const std::uint8_t alpha = 0x80; // MSB = 1
auto [k0, k1] = dpf::make_dpf(alpha, dpf::at<1>(std::uint64_t{1}));
// depth 0; programmed bit is 1; flip it
EXPECT_THROW(
(void)dpf::extend(k0, k1, /*bit=*/false, alpha,
dpf::at<2>(std::uint64_t{2})),
std::invalid_argument);
}
TEST(GrowingIdpfDeath, SpecNotOnNewDepthThrows)
{
const std::uint8_t alpha = 0x11;
auto [k0, k1] = dpf::make_dpf(alpha, dpf::at<1>(std::uint64_t{1}));
// Deepens by one via at<2>, but also plants at<1> on the old level.
EXPECT_THROW(
(void)dpf::extend(k0, k1, alpha, dpf::at<2>(std::uint64_t{9}),
dpf::at<1>(std::uint64_t{8})),
std::invalid_argument);
}
TEST(GrowingIdpf, ExtendSaturatesEightBitDomain)
{
const std::uint8_t alpha = 0x01;
const std::array<std::uint64_t, 8> betas = {1, 2, 3, 4, 5, 6, 7, 8};
block_t r0 = dpf::uniform_sample<block_t>();
block_t r1 = dpf::uniform_sample<block_t>();
auto [k0, k1] = grow_incrementally(alpha, betas, r0, r1);
using KT = std::decay_t<decltype(k0)>;
EXPECT_EQ(KT::depth, 7u);
EXPECT_EQ(KT::depth + 1, dpf::utils::bitlength_of_v<std::uint8_t>);
EXPECT_EQ(dpf::reconstruct(*dpf::eval_point(dpf::out<7>, k0, alpha),
*dpf::eval_point(dpf::out<7>, k1, alpha)),
8ull);
}
TEST(GrowingIdpfDeath, MemoizerOnSiblingCorruptsNewSlot)
{
const std::uint8_t alpha = 0xB2;
const std::uint8_t sibling = static_cast<std::uint8_t>(alpha ^ 0x80);
// depth 1 so the frontier is past the root
auto [k0, k1] = dpf::make_dpf(alpha, dpf::at<2>(std::uint64_t{7}));
auto [good0, good1] =
dpf::extend(k0, k1, alpha, dpf::at<3>(std::uint64_t{9}));
auto m0 = dpf::make_basic_path_memoizer(k0);
auto m1 = dpf::make_basic_path_memoizer(k1);
(void)dpf::eval_point(dpf::out<0>, k0, sibling, m0);
(void)dpf::eval_point(dpf::out<0>, k1, sibling, m1);
auto [bad0, bad1] =
dpf::extend(k0, k1, m0, m1, alpha, dpf::at<3>(std::uint64_t{9}));
EXPECT_NE(0, std::memcmp(good0.correction_words().data(),
bad0.correction_words().data(),
sizeof(good0.correction_words())));
const auto got = dpf::reconstruct(*dpf::eval_point(dpf::out<1>, bad0, alpha),
*dpf::eval_point(dpf::out<1>, bad1, alpha));
EXPECT_NE(got, 9ull);
(void)good1;
}
TEST(GrowingIdpfDeath, DsWrongSharesDisagreeWithDealer)
{
const std::uint8_t alpha = 0xB2;
auto [k0, k1] = dpf::make_dpf(alpha, dpf::at<2>(std::uint64_t{7}));
auto [good0, good1] =
dpf::extend(k0, k1, alpha, dpf::at<3>(std::uint64_t{9}));
auto m0 = dpf::make_basic_path_memoizer(k0);
auto m1 = dpf::make_basic_path_memoizer(k1);
(void)dpf::eval_point(dpf::out<0>, k0, alpha, m0);
(void)dpf::eval_point(dpf::out<0>, k1, alpha, m1);
GrowPad pads{};
dpf::local_cw_protocol<GrowPad> proto{pads};
// Flip the bit used at the extend level (depth 1 → second MSB).
const std::uint8_t wrong = static_cast<std::uint8_t>(alpha ^ 0x40);
auto [bad0, bad1] = dpf::extend_ds(k0, k1, m0, m1, wrong, std::uint8_t{0},
proto, dpf::at<3>(std::uint64_t{9}));
EXPECT_NE(0, std::memcmp(good0.correction_words().data(),
bad0.correction_words().data(),
sizeof(good0.correction_words())));
(void)good1;
(void)bad1;
}
TEST(GrowingIdpf, ExhaustiveDomainAfterMemoGrow)
{
const std::uint8_t alpha = 0x5A;
auto [k0, k1] = dpf::make_dpf(alpha, dpf::at<1>(std::uint64_t{3}));
auto m0 = dpf::make_basic_path_memoizer(k0);
auto m1 = dpf::make_basic_path_memoizer(k1);
(void)dpf::eval_point(dpf::out<0>, k0, alpha, m0);
(void)dpf::eval_point(dpf::out<0>, k1, alpha, m1);
auto [g0, g1] =
dpf::extend(k0, k1, m0, m1, alpha, dpf::at<2>(std::uint64_t{11}));
auto [d0, d1] =
dpf::extend(k0, k1, alpha, dpf::at<2>(std::uint64_t{11}));
for (unsigned x = 0; x < 256; ++x)
{
const auto xa = static_cast<std::uint8_t>(x);
EXPECT_EQ(dpf::reconstruct(*dpf::eval_point(dpf::out<0>, g0, xa),
*dpf::eval_point(dpf::out<0>, g1, xa)),
dpf::reconstruct(*dpf::eval_point(dpf::out<0>, d0, xa),
*dpf::eval_point(dpf::out<0>, d1, xa)))
<< "out0 x=" << x;
EXPECT_EQ(dpf::reconstruct(*dpf::eval_point(dpf::out<1>, g0, xa),
*dpf::eval_point(dpf::out<1>, g1, xa)),
dpf::reconstruct(*dpf::eval_point(dpf::out<1>, d0, xa),
*dpf::eval_point(dpf::out<1>, d1, xa)))
<< "out1 x=" << x;
}
}
TEST(GrowingIdpf, DsAndDealerAgreeFullDomain)
{
const std::uint8_t alpha = 0x3C;
auto [k0, k1] = dpf::make_dpf(alpha, dpf::at<1>(std::uint64_t{4}));
auto [d0, d1] =
dpf::extend(k0, k1, alpha, dpf::at<2>(std::uint64_t{8}));
auto m0 = dpf::make_basic_path_memoizer(k0);
auto m1 = dpf::make_basic_path_memoizer(k1);
(void)dpf::eval_point(dpf::out<0>, k0, alpha, m0);
(void)dpf::eval_point(dpf::out<0>, k1, alpha, m1);
GrowPad pads{};
dpf::local_cw_protocol<GrowPad> proto{pads};
auto [s0, s1] = dpf::extend_ds(k0, k1, m0, m1, alpha, std::uint8_t{0}, proto,
dpf::at<2>(std::uint64_t{8}));
EXPECT_EQ(0, std::memcmp(d0.correction_words().data(),
s0.correction_words().data(),
sizeof(d0.correction_words())));
EXPECT_EQ(0, std::memcmp(d0.correction_advice().data(),
s0.correction_advice().data(),
sizeof(d0.correction_advice())));
for (unsigned x = 0; x < 256; ++x)
{
const auto xa = static_cast<std::uint8_t>(x);
EXPECT_EQ(dpf::reconstruct(*dpf::eval_point(dpf::out<1>, s0, xa),
*dpf::eval_point(dpf::out<1>, s1, xa)),
dpf::reconstruct(*dpf::eval_point(dpf::out<1>, d0, xa),
*dpf::eval_point(dpf::out<1>, d1, xa)))
<< "x=" << x;
}
}
TEST(GrowingIdpf, EmptyAddOutputThrows)
{
const std::uint8_t alpha = 0x01;
auto [k0, k1] = dpf::make_dpf(alpha, dpf::at<1>(std::uint64_t{1}));
EXPECT_THROW((void)dpf::add_output(k0, k1, alpha), std::invalid_argument);
}

View file

@ -320,3 +320,36 @@ TEST(HalfTree, IncrementalPlacementSmoke)
EXPECT_EQ(got, q == alpha ? uint8_t{7} : uint8_t{0}) << i;
}
}
TEST(HalfTree, VerifiableFullDomainDetectsSeedTamper)
{
using in_t = std::uint8_t;
using out_t = std::uint16_t;
const in_t alpha = 0;
const out_t beta = 0x1111;
auto [k0, k1] = dpf::make_dpf<ht_prg, leaf_prg>(alpha, beta, dpf::verifiable{});
EXPECT_TRUE(std::decay_t<decltype(k0)>::tree::is_half_tree);
for (int i = 0; i < 256; ++i)
{
const in_t q = static_cast<in_t>(i);
dpf::proof_token a{}, b{};
const out_t got = recon(*dpf::eval_point(k0, q, dpf::prove(a)),
*dpf::eval_point(k1, q, dpf::prove(b)));
EXPECT_EQ(got, q == alpha ? beta : out_t{}) << i;
EXPECT_TRUE(dpf::verify(a, b)) << i;
}
for (auto & cs : const_cast<typename std::decay_t<decltype(k0)>::correction_seeds_array &>(
k0.correction_seeds()))
cs[0] = simde_mm_xor_si128(cs[0], simde_mm_set1_epi8(1));
int rejected = 0;
for (int i = 0; i < 256; ++i)
{
const in_t q = static_cast<in_t>(i);
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))
++rejected;
}
EXPECT_GT(rejected, 0);
}

View file

@ -45,6 +45,13 @@ static std::tuple
param_type<uint16_t, dpf::bitstring<150>>,
param_type<uint16_t, dpf::xor_wrapper<int64_t>>,
param_type<uint16_t, dpf::xor_wrapper<uint64_t>>,
param_type<uint16_t, dpf::gf2>,
param_type<uint16_t, dpf::gf22>,
param_type<uint16_t, dpf::gf24>,
param_type<uint16_t, dpf::gf28>,
param_type<uint16_t, dpf::gf216>,
param_type<uint16_t, dpf::gf232>,
param_type<uint16_t, dpf::gf264>,
// custom types
param_type<custom_input_type, uint64_t>,
@ -555,6 +562,62 @@ static std::tuple
std::make_tuple(uint16_t(0xFFFF), dpf::xor_wrapper<uint64_t>(uint64_t(0xAAAAAAAAAAAAAAAA))),
std::make_tuple(uint16_t(0xFFFF), dpf::xor_wrapper<uint64_t>(uint64_t(0xFFFFFFFFFFFFFFFF)))
},
{
std::make_tuple(uint16_t(0x0000), dpf::gf2{1}),
std::make_tuple(uint16_t(0x5555), dpf::gf2{1}),
std::make_tuple(uint16_t(0x7FFF), dpf::gf2{1}),
std::make_tuple(uint16_t(0x8000), dpf::gf2{1}),
std::make_tuple(uint16_t(0xAAAA), dpf::gf2{1}),
std::make_tuple(uint16_t(0xFFFF), dpf::gf2{1})
},
{
std::make_tuple(uint16_t(0x0000), dpf::gf22{1}),
std::make_tuple(uint16_t(0x5555), dpf::gf22{2}),
std::make_tuple(uint16_t(0x7FFF), dpf::gf22{3}),
std::make_tuple(uint16_t(0x8000), dpf::gf22{1}),
std::make_tuple(uint16_t(0xAAAA), dpf::gf22{2}),
std::make_tuple(uint16_t(0xFFFF), dpf::gf22{3})
},
{
std::make_tuple(uint16_t(0x0000), dpf::gf24{1}),
std::make_tuple(uint16_t(0x5555), dpf::gf24{0x6}),
std::make_tuple(uint16_t(0x7FFF), dpf::gf24{0xa}),
std::make_tuple(uint16_t(0x8000), dpf::gf24{0xf}),
std::make_tuple(uint16_t(0xAAAA), dpf::gf24{0x2}),
std::make_tuple(uint16_t(0xFFFF), dpf::gf24{0xd})
},
{
std::make_tuple(uint16_t(0x0000), dpf::gf28{1}),
std::make_tuple(uint16_t(0x5555), dpf::gf28{0x1b}),
std::make_tuple(uint16_t(0x7FFF), dpf::gf28{0x80}),
std::make_tuple(uint16_t(0x8000), dpf::gf28{0xff}),
std::make_tuple(uint16_t(0xAAAA), dpf::gf28{0x02}),
std::make_tuple(uint16_t(0xFFFF), dpf::gf28{0x5a})
},
{
std::make_tuple(uint16_t(0x0000), dpf::gf216{1}),
std::make_tuple(uint16_t(0x5555), dpf::gf216{0x2d}),
std::make_tuple(uint16_t(0x7FFF), dpf::gf216{0x8000}),
std::make_tuple(uint16_t(0x8000), dpf::gf216{0xffff}),
std::make_tuple(uint16_t(0xAAAA), dpf::gf216{0x0002}),
std::make_tuple(uint16_t(0xFFFF), dpf::gf216{0x1234})
},
{
std::make_tuple(uint16_t(0x0000), dpf::gf232{1}),
std::make_tuple(uint16_t(0x5555), dpf::gf232{0x90200001u}),
std::make_tuple(uint16_t(0x7FFF), dpf::gf232{0x80000000u}),
std::make_tuple(uint16_t(0x8000), dpf::gf232{0xffffffffu}),
std::make_tuple(uint16_t(0xAAAA), dpf::gf232{0x00000002u}),
std::make_tuple(uint16_t(0xFFFF), dpf::gf232{0x13579bdfu})
},
{
std::make_tuple(uint16_t(0x0000), dpf::gf264{1}),
std::make_tuple(uint16_t(0x5555), dpf::gf264{0x11}),
std::make_tuple(uint16_t(0x7FFF), dpf::gf264{0x8000000000000000ull}),
std::make_tuple(uint16_t(0x8000), dpf::gf264{~std::uint64_t{0}}),
std::make_tuple(uint16_t(0xAAAA), dpf::gf264{0x2}),
std::make_tuple(uint16_t(0xFFFF), dpf::gf264{0x123456789abcdefull})
},
{
std::make_tuple(custom_input_type(0x0000), uint64_t(0x0000000000000001)),
std::make_tuple(custom_input_type(0x0000), uint64_t(0x5555555555555555)),

View file

@ -59,6 +59,13 @@ static std::tuple
param_type<uint16_t, dpf::bitstring<150>>,
param_type<uint16_t, dpf::xor_wrapper<int64_t>>,
param_type<uint16_t, dpf::xor_wrapper<uint64_t>>,
param_type<uint16_t, dpf::gf2>,
param_type<uint16_t, dpf::gf22>,
param_type<uint16_t, dpf::gf24>,
param_type<uint16_t, dpf::gf28>,
param_type<uint16_t, dpf::gf216>,
param_type<uint16_t, dpf::gf232>,
param_type<uint16_t, dpf::gf264>,
// custom types
param_type<custom_input_type, uint64_t>,
@ -591,6 +598,188 @@ static std::tuple
dpf::xor_wrapper<uint64_t>(uint64_t(0xAAAAAAAAAAAAAAAA)),
dpf::xor_wrapper<uint64_t>(uint64_t(0xFFFFFFFFFFFFFFFF)))
},
{
std::make_tuple(uint16_t(0x0000), dpf::gf2{1},
dpf::gf2{1},
dpf::gf2{1},
dpf::gf2{1}),
std::make_tuple(uint16_t(0x5555), dpf::gf2{1},
dpf::gf2{1},
dpf::gf2{1},
dpf::gf2{1}),
std::make_tuple(uint16_t(0x7FFF), dpf::gf2{1},
dpf::gf2{1},
dpf::gf2{1},
dpf::gf2{1}),
std::make_tuple(uint16_t(0x8000), dpf::gf2{1},
dpf::gf2{1},
dpf::gf2{1},
dpf::gf2{1}),
std::make_tuple(uint16_t(0xAAAA), dpf::gf2{1},
dpf::gf2{1},
dpf::gf2{1},
dpf::gf2{1}),
std::make_tuple(uint16_t(0xFFFF), dpf::gf2{1},
dpf::gf2{1},
dpf::gf2{1},
dpf::gf2{1}),
},
{
std::make_tuple(uint16_t(0x0000), dpf::gf22{1},
dpf::gf22{2},
dpf::gf22{3},
dpf::gf22{1}),
std::make_tuple(uint16_t(0x5555), dpf::gf22{2},
dpf::gf22{3},
dpf::gf22{1},
dpf::gf22{2}),
std::make_tuple(uint16_t(0x7FFF), dpf::gf22{3},
dpf::gf22{1},
dpf::gf22{2},
dpf::gf22{3}),
std::make_tuple(uint16_t(0x8000), dpf::gf22{1},
dpf::gf22{2},
dpf::gf22{3},
dpf::gf22{1}),
std::make_tuple(uint16_t(0xAAAA), dpf::gf22{2},
dpf::gf22{3},
dpf::gf22{1},
dpf::gf22{2}),
std::make_tuple(uint16_t(0xFFFF), dpf::gf22{3},
dpf::gf22{1},
dpf::gf22{2},
dpf::gf22{3}),
},
{
std::make_tuple(uint16_t(0x0000), dpf::gf24{1},
dpf::gf24{0x6},
dpf::gf24{0xa},
dpf::gf24{0xf}),
std::make_tuple(uint16_t(0x5555), dpf::gf24{0x6},
dpf::gf24{0xa},
dpf::gf24{0xf},
dpf::gf24{0x2}),
std::make_tuple(uint16_t(0x7FFF), dpf::gf24{0xa},
dpf::gf24{0xf},
dpf::gf24{0x2},
dpf::gf24{0xd}),
std::make_tuple(uint16_t(0x8000), dpf::gf24{0xf},
dpf::gf24{0x2},
dpf::gf24{0xd},
dpf::gf24{1}),
std::make_tuple(uint16_t(0xAAAA), dpf::gf24{0x2},
dpf::gf24{0xd},
dpf::gf24{1},
dpf::gf24{0x6}),
std::make_tuple(uint16_t(0xFFFF), dpf::gf24{0xd},
dpf::gf24{1},
dpf::gf24{0x6},
dpf::gf24{0xa}),
},
{
std::make_tuple(uint16_t(0x0000), dpf::gf28{1},
dpf::gf28{0x1b},
dpf::gf28{0x80},
dpf::gf28{0xff}),
std::make_tuple(uint16_t(0x5555), dpf::gf28{0x1b},
dpf::gf28{0x80},
dpf::gf28{0xff},
dpf::gf28{0x02}),
std::make_tuple(uint16_t(0x7FFF), dpf::gf28{0x80},
dpf::gf28{0xff},
dpf::gf28{0x02},
dpf::gf28{0x5a}),
std::make_tuple(uint16_t(0x8000), dpf::gf28{0xff},
dpf::gf28{0x02},
dpf::gf28{0x5a},
dpf::gf28{1}),
std::make_tuple(uint16_t(0xAAAA), dpf::gf28{0x02},
dpf::gf28{0x5a},
dpf::gf28{1},
dpf::gf28{0x1b}),
std::make_tuple(uint16_t(0xFFFF), dpf::gf28{0x5a},
dpf::gf28{1},
dpf::gf28{0x1b},
dpf::gf28{0x80}),
},
{
std::make_tuple(uint16_t(0x0000), dpf::gf216{1},
dpf::gf216{0x2d},
dpf::gf216{0x8000},
dpf::gf216{0xffff}),
std::make_tuple(uint16_t(0x5555), dpf::gf216{0x2d},
dpf::gf216{0x8000},
dpf::gf216{0xffff},
dpf::gf216{0x0002}),
std::make_tuple(uint16_t(0x7FFF), dpf::gf216{0x8000},
dpf::gf216{0xffff},
dpf::gf216{0x0002},
dpf::gf216{0x1234}),
std::make_tuple(uint16_t(0x8000), dpf::gf216{0xffff},
dpf::gf216{0x0002},
dpf::gf216{0x1234},
dpf::gf216{1}),
std::make_tuple(uint16_t(0xAAAA), dpf::gf216{0x0002},
dpf::gf216{0x1234},
dpf::gf216{1},
dpf::gf216{0x2d}),
std::make_tuple(uint16_t(0xFFFF), dpf::gf216{0x1234},
dpf::gf216{1},
dpf::gf216{0x2d},
dpf::gf216{0x8000}),
},
{
std::make_tuple(uint16_t(0x0000), dpf::gf232{1},
dpf::gf232{0x90200001u},
dpf::gf232{0x80000000u},
dpf::gf232{0xffffffffu}),
std::make_tuple(uint16_t(0x5555), dpf::gf232{0x90200001u},
dpf::gf232{0x80000000u},
dpf::gf232{0xffffffffu},
dpf::gf232{0x00000002u}),
std::make_tuple(uint16_t(0x7FFF), dpf::gf232{0x80000000u},
dpf::gf232{0xffffffffu},
dpf::gf232{0x00000002u},
dpf::gf232{0x13579bdfu}),
std::make_tuple(uint16_t(0x8000), dpf::gf232{0xffffffffu},
dpf::gf232{0x00000002u},
dpf::gf232{0x13579bdfu},
dpf::gf232{1}),
std::make_tuple(uint16_t(0xAAAA), dpf::gf232{0x00000002u},
dpf::gf232{0x13579bdfu},
dpf::gf232{1},
dpf::gf232{0x90200001u}),
std::make_tuple(uint16_t(0xFFFF), dpf::gf232{0x13579bdfu},
dpf::gf232{1},
dpf::gf232{0x90200001u},
dpf::gf232{0x80000000u}),
},
{
std::make_tuple(uint16_t(0x0000), dpf::gf264{1},
dpf::gf264{0x11},
dpf::gf264{0x8000000000000000ull},
dpf::gf264{~std::uint64_t{0}}),
std::make_tuple(uint16_t(0x5555), dpf::gf264{0x11},
dpf::gf264{0x8000000000000000ull},
dpf::gf264{~std::uint64_t{0}},
dpf::gf264{0x2}),
std::make_tuple(uint16_t(0x7FFF), dpf::gf264{0x8000000000000000ull},
dpf::gf264{~std::uint64_t{0}},
dpf::gf264{0x2},
dpf::gf264{0x123456789abcdefull}),
std::make_tuple(uint16_t(0x8000), dpf::gf264{~std::uint64_t{0}},
dpf::gf264{0x2},
dpf::gf264{0x123456789abcdefull},
dpf::gf264{1}),
std::make_tuple(uint16_t(0xAAAA), dpf::gf264{0x2},
dpf::gf264{0x123456789abcdefull},
dpf::gf264{1},
dpf::gf264{0x11}),
std::make_tuple(uint16_t(0xFFFF), dpf::gf264{0x123456789abcdefull},
dpf::gf264{1},
dpf::gf264{0x11},
dpf::gf264{0x8000000000000000ull}),
},
{
std::make_tuple(custom_input_type(0x0000), uint64_t(0x0000000000000001),
uint64_t(0x5555555555555555),

View file

@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <tuple>
#include "dpf.hpp"
@ -113,8 +114,8 @@ TEST(Ic, MemoizerAgrees)
{
const uint8_t r = 40, p = 7, q = 90;
auto keys = dpf::make_dpf(r, dpf::ic(p, q, uint32_t{11}, uint32_t{2}));
dpf::basic_path_memoizer<decltype(keys.first.key)> memo0;
dpf::basic_path_memoizer<decltype(keys.second.key)> memo1;
dpf::basic_path_memoizer<decltype(keys.first.dpf_key)> memo0;
dpf::basic_path_memoizer<decltype(keys.second.dpf_key)> memo1;
for (int x = 0; x < 256; ++x)
{
const auto a = dpf::eval_point(dpf::ic, keys.first, static_cast<uint8_t>(x), memo0);
@ -131,7 +132,7 @@ TEST(Ic, IntervalAndSequenceBuffers)
auto keys = dpf::make_dpf(r, dpf::ic(p, q, uint16_t{9}));
auto buf0 = dpf::make_output_buffer(dpf::ic, keys.first, uint8_t{3}, uint8_t{18});
auto buf1 = dpf::make_output_buffer(dpf::ic, keys.second, uint8_t{3}, uint8_t{18});
dpf::basic_path_memoizer<decltype(keys.first.key)> memo;
dpf::basic_path_memoizer<decltype(keys.first.dpf_key)> memo;
dpf::eval_interval(dpf::ic, keys.first, uint8_t{3}, uint8_t{18}, buf0, memo);
dpf::eval_interval(dpf::ic, keys.second, uint8_t{3}, uint8_t{18}, buf1);
for (std::size_t i = 0; i < buf0.size(); ++i)
@ -226,6 +227,7 @@ TEST(Ic, Geneval)
rngs, dpf::ic(p, q, beta));
ASSERT_EQ(opened.party0.size(), 7u);
ASSERT_EQ(opened.live_levels, 8u);
EXPECT_TRUE(dpf::verify(opened.proof0, opened.proof1));
const uint8_t r = static_cast<uint8_t>(r0 ^ r1);
for (std::size_t i = 0; i < 7; ++i)
{
@ -239,8 +241,7 @@ TEST(Ic, Geneval)
TEST(Ic, RejectsWrappedBounds)
{
EXPECT_THROW(dpf::make_dpf(uint8_t{1}, dpf::ic(uint8_t{9}, uint8_t{2}, uint32_t{1})),
std::invalid_argument);
EXPECT_THROW(dpf::make_dpf(uint8_t{1}, dpf::ic(uint8_t{9}, uint8_t{2}, uint32_t{1})), std::invalid_argument);
}
TEST(Ic, BitPayload)
@ -334,14 +335,14 @@ TEST(Ic, AdditiveGeneval)
std::begin(none), std::begin(none), ic_rng(), dpf::ic(p, q, beta));
EXPECT_TRUE(empty.party0.empty());
EXPECT_EQ(empty.live_levels, 0u);
// Empty-range tokens stay zero and must not verify as a matching pair.
EXPECT_FALSE(dpf::verify(empty.proof0, empty.proof1));
}
TEST(Ic, IntervalRejectsDescendingEndpoints)
{
auto keys = dpf::make_dpf(uint8_t{4}, dpf::ic(uint8_t{1}, uint8_t{6}, uint32_t{1}));
auto buf = dpf::make_output_buffer(dpf::ic, keys.first, 1);
EXPECT_THROW(dpf::eval_interval(dpf::ic, keys.first, uint8_t{9}, uint8_t{2}, buf),
std::invalid_argument);
EXPECT_THROW(dpf::make_output_buffer(dpf::ic, keys.first, uint8_t{9}, uint8_t{2}),
std::invalid_argument);
EXPECT_THROW(dpf::eval_interval(dpf::ic, keys.first, uint8_t{9}, uint8_t{2}, buf), std::invalid_argument);
EXPECT_THROW(dpf::make_output_buffer(dpf::ic, keys.first, uint8_t{9}, uint8_t{2}), std::invalid_argument);
}

View file

@ -0,0 +1,90 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <cstdint>
#include <functional>
#include <utility>
#include <vector>
#include "dpf.hpp"
namespace
{
using key0_t = decltype(dpf::make_dpf(std::uint16_t{0},
dpf::idpf_ones<16>()).first);
using key1_t = decltype(dpf::make_dpf(std::uint16_t{0},
dpf::idpf_ones<16>()).second);
void split_keys(const std::vector<std::uint16_t> & values,
std::vector<key0_t> & k0, std::vector<key1_t> & k1)
{
k0.clear();
k1.clear();
for (auto v : values)
{
auto [a, b] = dpf::make_dpf(v, dpf::idpf_ones<16>());
k0.push_back(std::move(a));
k1.push_back(std::move(b));
}
}
} // namespace
TEST(IdpfAgg, MaxAndKthAgainstPlaintext)
{
const std::vector<std::uint16_t> values{3, 100, 7, 100, 42};
std::vector<key0_t> k0;
std::vector<key1_t> k1;
split_keys(values, k0, k1);
EXPECT_EQ(dpf::idpf_agg_max(k0, k1), 100u);
EXPECT_EQ(dpf::idpf_agg_kth(k0, k1, 1), 100u);
EXPECT_EQ(dpf::idpf_agg_kth(k0, k1, values.size()), 3u);
auto sorted = values;
std::sort(sorted.begin(), sorted.end(), std::greater<>{});
EXPECT_EQ(dpf::idpf_agg_kth(k0, k1, 3), sorted[2]);
}
TEST(IdpfAgg, AllEqual)
{
const std::vector<std::uint16_t> values{0x00AA, 0x00AA, 0x00AA};
std::vector<key0_t> k0;
std::vector<key1_t> k1;
split_keys(values, k0, k1);
EXPECT_EQ(dpf::idpf_agg_max(k0, k1), 0x00AAu);
EXPECT_EQ(dpf::idpf_agg_kth(k0, k1, 1), 0x00AAu);
EXPECT_EQ(dpf::idpf_agg_kth(k0, k1, 3), 0x00AAu);
}
TEST(IdpfAgg, KEqualsOneAndN)
{
const std::vector<std::uint16_t> values{1, 2, 3, 4};
std::vector<key0_t> k0;
std::vector<key1_t> k1;
split_keys(values, k0, k1);
EXPECT_EQ(dpf::idpf_agg_kth(k0, k1, 1), 4u);
EXPECT_EQ(dpf::idpf_agg_kth(k0, k1, 4), 1u);
}
TEST(IdpfAgg, Duplicates)
{
const std::vector<std::uint16_t> values{9, 9, 1, 9, 5};
std::vector<key0_t> k0;
std::vector<key1_t> k1;
split_keys(values, k0, k1);
EXPECT_EQ(dpf::idpf_agg_max(k0, k1), 9u);
EXPECT_EQ(dpf::idpf_agg_kth(k0, k1, 2), 9u);
EXPECT_EQ(dpf::idpf_agg_kth(k0, k1, 4), 5u);
}
// Seam: max equals kth(k=1); both agree with a plaintext sort.
TEST(IdpfAgg, MaxEqualsKthOne)
{
const std::vector<std::uint16_t> values{4, 90, 4, 15};
std::vector<key0_t> k0;
std::vector<key1_t> k1;
split_keys(values, k0, k1);
EXPECT_EQ(dpf::idpf_agg_max(k0, k1), dpf::idpf_agg_kth(k0, k1, 1));
}

View file

@ -0,0 +1,497 @@
#include <gtest/gtest.h>
#include <array>
#include <cstdint>
#include <cstring>
#include <exception>
#include <filesystem>
#include <stdexcept>
#include <string>
#include <thread>
#include <unistd.h>
#include <utility>
#include <vector>
#include "dpf.hpp"
#include "dpf/iknp.hpp"
#include "dpf/prg_aes_ccr.hpp"
#include "flow_util.hpp"
#include "iknp_deal.hpp"
namespace
{
using dpf::net::role;
using dpf::net::trio;
template <typename Fn0, typename Fn1>
void run_pair(Fn0 && fn0, Fn1 && fn1)
{
const auto dir = std::filesystem::temp_directory_path()
/ ("libdpf_iknp_" + std::to_string(::getpid()));
std::filesystem::create_directories(dir);
std::exception_ptr ep0;
std::exception_ptr ep1;
std::thread t0([&] {
try
{
auto net = trio::connect_pair(role::p0, dir.string());
fn0(net);
}
catch (...)
{
ep0 = std::current_exception();
}
});
std::thread t1([&] {
try
{
auto net = trio::connect_pair(role::p1, dir.string());
fn1(net);
}
catch (...)
{
ep1 = std::current_exception();
}
});
t0.join();
t1.join();
std::filesystem::remove_all(dir);
if (ep0)
std::rethrow_exception(ep0);
if (ep1)
std::rethrow_exception(ep1);
}
simde__m128i xor_block(simde__m128i a, simde__m128i b)
{
return simde_mm_xor_si128(a, b);
}
TEST(iknp, pads_match_dealer_relations)
{
constexpr std::size_t nblock = 4;
constexpr std::size_t nbit = 5;
constexpr std::size_t nb2a = 3;
constexpr std::size_t ncw = 2;
dpf::iknp::material m0, m1;
run_pair(
[&](trio & net) {
m0 = dpf::iknp::sample(net.to(role::p1), 0, nblock, nbit, nb2a, ncw);
},
[&](trio & net) {
m1 = dpf::iknp::sample(net.to(role::p0), 1, nblock, nbit, nb2a, ncw);
});
ASSERT_EQ(m0.blocks.size(), nblock);
ASSERT_EQ(m1.blocks.size(), nblock);
for (std::size_t i = 0; i < nblock; ++i)
{
const auto a = static_cast<std::uint8_t>(m0.blocks[i].a ^ m1.blocks[i].a);
const auto b = xor_block(m0.blocks[i].b, m1.blocks[i].b);
const auto c = xor_block(m0.blocks[i].c, m1.blocks[i].c);
const auto expect = (a & 1u) ? b : simde_mm_setzero_si128();
EXPECT_EQ(std::memcmp(&c, &expect, sizeof(c)), 0);
}
for (std::size_t i = 0; i < nbit; ++i)
{
const auto a = static_cast<std::uint8_t>(m0.bits[i].a ^ m1.bits[i].a);
const auto b = static_cast<std::uint8_t>(m0.bits[i].b ^ m1.bits[i].b);
const auto c = static_cast<std::uint8_t>(m0.bits[i].c ^ m1.bits[i].c);
EXPECT_EQ(c, static_cast<std::uint8_t>((a & b) & 1u));
}
for (std::size_t i = 0; i < nb2a; ++i)
{
const auto bit = static_cast<std::uint64_t>(m0.b2a[i].r ^ m1.b2a[i].r);
EXPECT_EQ(m0.b2a[i].add + m1.b2a[i].add, bit);
}
// gamma0 ⊕ gamma1 = (bit1·rand0) ⊕ (bit0·rand1) (XOR shares; neither
// party learns the peer pad bit).
for (std::size_t i = 0; i < ncw; ++i)
{
const auto prod = xor_block(
(m1.cws[i].bit & 1u) ? m0.cws[i].rand : simde_mm_setzero_si128(),
(m0.cws[i].bit & 1u) ? m1.cws[i].rand : simde_mm_setzero_si128());
const auto got = xor_block(m0.cws[i].gamma, m1.cws[i].gamma);
EXPECT_EQ(std::memcmp(&got, &prod, sizeof(prod)), 0);
}
}
template <typename Point>
void expect_point(Point alpha, Point x0, Point x1, std::uint64_t beta, bool additive)
{
using namespace dpf::party;
run_pair(
[&](trio & net) {
dist_with_point_key_iknp<dpf::prg::aes128, dpf::prg::aes128, true>(
net, role::p0, x0, x1, beta,
[&](const auto & key) {
const auto y = util::share_bits(*dpf::eval_point(key, alpha));
const auto open = util::open_subtractive(net, role::p0, y);
EXPECT_EQ(open, beta);
const Point other = static_cast<Point>(
static_cast<unsigned>(alpha) ^ 1u);
const auto z = util::share_bits(*dpf::eval_point(key, other));
const auto oz = util::open_subtractive(net, role::p0, z);
EXPECT_EQ(oz, 0u);
},
[](const auto &) {}, false, additive);
EXPECT_TRUE(net.dealer_inbox_done());
},
[&](trio & net) {
dist_with_point_key_iknp<dpf::prg::aes128, dpf::prg::aes128, true>(
net, role::p1, x0, x1, beta,
[](const auto &) {},
[&](const auto & key) {
const auto y = util::share_bits(*dpf::eval_point(key, alpha));
(void)util::open_subtractive(net, role::p1, y);
const Point other = static_cast<Point>(
static_cast<unsigned>(alpha) ^ 1u);
const auto z = util::share_bits(*dpf::eval_point(key, other));
(void)util::open_subtractive(net, role::p1, z);
}, false, additive);
EXPECT_TRUE(net.dealer_inbox_done());
});
}
TEST(iknp, point_reveal)
{
const std::uint8_t alpha = 0x2a;
expect_point<std::uint8_t>(alpha, 0x10, static_cast<std::uint8_t>(alpha ^ 0x10),
7, false);
}
TEST(iknp, point_additive)
{
const std::uint8_t alpha = 0x2a;
const std::uint8_t x0 = 0x10;
const std::uint8_t x1 = static_cast<std::uint8_t>(alpha - x0);
expect_point<std::uint8_t>(alpha, x0, x1, 11, true);
}
TEST(iknp, half_tree_reveal)
{
using namespace dpf::party;
using Ht = dpf::prg::aes128_ccr;
const std::uint8_t alpha = 7;
const std::uint8_t x0 = 0x25;
const std::uint8_t x1 = static_cast<std::uint8_t>(alpha ^ x0);
const std::uint64_t beta = 9;
run_pair(
[&](trio & net) {
dist_with_point_key_iknp<Ht, Ht, true>(net, role::p0, x0, x1, beta,
[&](const auto & key) {
const auto open = util::open_subtractive(net, role::p0,
util::share_bits(*dpf::eval_point(key, alpha)));
EXPECT_EQ(open, beta);
},
[](const auto &) {});
EXPECT_TRUE(net.dealer_inbox_done());
},
[&](trio & net) {
dist_with_point_key_iknp<Ht, Ht, true>(net, role::p1, x0, x1, beta,
[](const auto &) {},
[&](const auto & key) {
(void)util::open_subtractive(net, role::p1,
util::share_bits(*dpf::eval_point(key, alpha)));
});
EXPECT_TRUE(net.dealer_inbox_done());
});
}
TEST(iknp, comparison_reveal_and_oblivious)
{
using namespace dpf::party;
const std::uint8_t alpha = 0x40;
const std::uint8_t x0 = 0x14;
const std::uint8_t x1 = static_cast<std::uint8_t>(alpha ^ x0);
const std::uint64_t beta = 9;
auto check = [&](bool reveal) {
run_pair(
[&](trio & net) {
auto on = [&](const auto & key) {
const auto mask = key.cmp().mask;
const auto below = util::open_additive(net, role::p0,
util::share_bits(dpf::eval_point(dpf::cmp, key,
static_cast<std::uint8_t>(alpha - 1)))) & mask;
const auto at = util::open_additive(net, role::p0,
util::share_bits(dpf::eval_point(dpf::cmp, key, alpha)))
& mask;
EXPECT_EQ(below, beta);
EXPECT_EQ(at, 0u);
};
if (reveal)
dist_with_cmp_key_iknp<dpf::prg::aes128, dpf::prg::aes128, true>(
net, role::p0, x0, x1, dpf::lt(beta), on, [](const auto &) {});
else
dist_with_cmp_key_iknp(net, role::p0, x0, x1, dpf::lt(beta),
on, [](const auto &) {});
EXPECT_TRUE(net.dealer_inbox_done());
},
[&](trio & net) {
auto on = [&](const auto & key) {
const auto mask = key.cmp().mask;
(void)(util::open_additive(net, role::p1,
util::share_bits(dpf::eval_point(dpf::cmp, key,
static_cast<std::uint8_t>(alpha - 1)))) & mask);
(void)(util::open_additive(net, role::p1,
util::share_bits(dpf::eval_point(dpf::cmp, key, alpha)))
& mask);
};
if (reveal)
dist_with_cmp_key_iknp<dpf::prg::aes128, dpf::prg::aes128, true>(
net, role::p1, x0, x1, dpf::lt(beta), [](const auto &) {}, on);
else
dist_with_cmp_key_iknp(net, role::p1, x0, x1, dpf::lt(beta),
[](const auto &) {}, on);
EXPECT_TRUE(net.dealer_inbox_done());
});
};
check(true);
check(false);
}
TEST(iknp, interval_shared_and_reveal)
{
using namespace dpf::party;
const std::uint8_t r = 40, p = 7, q = 90;
const std::uint8_t r0 = 0x13;
const std::uint8_t r1 = static_cast<std::uint8_t>(r ^ r0);
const std::uint32_t if_true = 11, if_false = 2;
auto check = [&](bool reveal) {
run_pair(
[&](trio & net) {
auto on = [&](const auto & key) {
const auto gmask = key.group_mask;
const auto nmask = key.input_mask;
for (unsigned x : {0u, 7u, 40u, 90u, 200u})
{
const auto mine = util::share_bits(
dpf::eval_point(dpf::ic, key, static_cast<std::uint8_t>(x)));
const auto peer = net.exchange_with(role::p1, mine);
const auto w = (x - static_cast<unsigned>(r)) & nmask;
const bool inside = w >= p && w <= q;
const auto want = (inside ? if_true : if_false) & gmask;
EXPECT_EQ((mine + peer) & gmask, want);
}
};
if (reveal)
dist_with_ic_key_iknp<dpf::prg::aes128, dpf::prg::aes128, true>(
net, role::p0, r0, r1, dpf::ic(p, q, if_true, if_false),
on, [](const auto &) {});
else
dist_with_ic_key_iknp(net, role::p0, r0, r1,
dpf::ic(p, q, if_true, if_false), on, [](const auto &) {});
EXPECT_TRUE(net.dealer_inbox_done());
},
[&](trio & net) {
auto on = [&](const auto & key) {
for (unsigned x : {0u, 7u, 40u, 90u, 200u})
{
const auto mine = util::share_bits(
dpf::eval_point(dpf::ic, key, static_cast<std::uint8_t>(x)));
(void)net.exchange_with(role::p0, mine);
}
};
if (reveal)
dist_with_ic_key_iknp<dpf::prg::aes128, dpf::prg::aes128, true>(
net, role::p1, r0, r1, dpf::ic(p, q, if_true, if_false),
[](const auto &) {}, on);
else
dist_with_ic_key_iknp(net, role::p1, r0, r1,
dpf::ic(p, q, if_true, if_false), [](const auto &) {}, on);
EXPECT_TRUE(net.dealer_inbox_done());
});
};
check(false);
check(true);
}
template <typename Key, typename Share>
void assign_wildcard(Key & key, Share my_share, trio & net, role self)
{
const role peer = self == role::p0 ? role::p1 : role::p0;
auto & wrap = std::get<0>(key.leaf_nodes);
if (wrap.is_ready())
wrap.begin_update();
auto blinded = wrap.compute_and_get_blinded_output_share(my_share);
auto peer_blinded = net.exchange_with(peer, blinded);
auto leaf = wrap.compute_and_get_leaf_share(peer_blinded);
auto peer_leaf = net.exchange_with(peer, leaf);
wrap.reconstruct_correction_word(peer_leaf);
}
TEST(iknp, wildcard_assign)
{
using namespace dpf::party::util;
const std::uint8_t alpha = 0xAA;
const std::uint8_t x0 = 0x31;
const std::uint8_t x1 = static_cast<std::uint8_t>(alpha ^ x0);
const std::uint32_t y = 0xAAAAAAAAu;
const std::uint32_t y0 = 0x12345678u;
const std::uint32_t y1 = y - y0;
using out_t = dpf::wildcard_value<std::uint32_t>;
run_pair(
[&](trio & net) {
dpf::party::dist_with_point_key_iknp(net, role::p0, x0, x1, out_t{},
[&](auto key) {
assign_wildcard(key, y0, net, role::p0);
const auto on = open_subtractive(net, role::p0,
share_bits(*dpf::eval_point(key, alpha)));
const auto off = open_subtractive(net, role::p0,
share_bits(*dpf::eval_point(key,
static_cast<std::uint8_t>(alpha ^ 1u))));
EXPECT_EQ(on, y);
EXPECT_EQ(off, 0u);
},
[](auto) {});
EXPECT_TRUE(net.dealer_inbox_done());
},
[&](trio & net) {
dpf::party::dist_with_point_key_iknp(net, role::p1, x0, x1, out_t{},
[](auto) {},
[&](auto key) {
assign_wildcard(key, y1, net, role::p1);
(void)open_subtractive(net, role::p1,
share_bits(*dpf::eval_point(key, alpha)));
(void)open_subtractive(net, role::p1,
share_bits(*dpf::eval_point(key,
static_cast<std::uint8_t>(alpha ^ 1u))));
});
EXPECT_TRUE(net.dealer_inbox_done());
});
}
TEST(iknp, extractable_sketch)
{
const std::uint8_t alpha = 0x2a;
const std::uint8_t x0 = 0x10;
const std::uint8_t x1 = static_cast<std::uint8_t>(alpha ^ x0);
const dpf::fp61 beta{7};
run_pair(
[&](trio & net) {
dpf::party::dist_with_extractable_point_key_iknp(
net, role::p0, x0, x1, beta,
[&](const auto & key) {
const std::array<dpf::fp61, 1> rs{dpf::fp61{3}};
dpf::sketch_share local{};
auto sk = dpf::sketch(local, rs);
(void)*dpf::eval_point(key, alpha, sk);
auto theirs = net.exchange_with(role::p1, local,
dpf::net::msg::sketch_share);
EXPECT_TRUE(dpf::sketch_verify(local, theirs));
},
[](const auto &) {});
EXPECT_TRUE(net.dealer_inbox_done());
},
[&](trio & net) {
dpf::party::dist_with_extractable_point_key_iknp(
net, role::p1, x0, x1, beta,
[](const auto &) {},
[&](const auto & key) {
const std::array<dpf::fp61, 1> rs{dpf::fp61{3}};
dpf::sketch_share local{};
auto sk = dpf::sketch(local, rs);
(void)*dpf::eval_point(key, alpha, sk);
auto theirs = net.exchange_with(role::p0, local,
dpf::net::msg::sketch_share);
EXPECT_TRUE(dpf::sketch_verify(theirs, local));
});
EXPECT_TRUE(net.dealer_inbox_done());
});
}
TEST(iknp, oblivious_point_hash)
{
using namespace dpf::party;
// Default (non-reveal) point keygen hashes the shared prefix, so this
// exercises the per-level AND tape as well as the correction words.
const std::uint8_t alpha = 0x2a;
const std::uint8_t x0 = 0x10;
const std::uint8_t x1 = static_cast<std::uint8_t>(alpha ^ x0);
const std::uint64_t beta = 4;
run_pair(
[&](trio & net) {
dist_with_point_key_iknp(net, role::p0, x0, x1, beta,
[&](const auto & key) {
const auto on = util::open_subtractive(net, role::p0,
util::share_bits(*dpf::eval_point(key, alpha)));
const auto off = util::open_subtractive(net, role::p0,
util::share_bits(*dpf::eval_point(key,
static_cast<std::uint8_t>(alpha ^ 1u))));
EXPECT_EQ(on, beta);
EXPECT_EQ(off, 0u);
},
[](const auto &) {});
EXPECT_TRUE(net.dealer_inbox_done());
},
[&](trio & net) {
dist_with_point_key_iknp(net, role::p1, x0, x1, beta,
[](const auto &) {},
[&](const auto & key) {
(void)util::open_subtractive(net, role::p1,
util::share_bits(*dpf::eval_point(key, alpha)));
(void)util::open_subtractive(net, role::p1,
util::share_bits(*dpf::eval_point(key,
static_cast<std::uint8_t>(alpha ^ 1u))));
});
EXPECT_TRUE(net.dealer_inbox_done());
});
}
TEST(iknp, transfer_labels_follows_choice_and_reuses_base)
{
constexpr std::size_t n = 7;
std::vector<simde__m128i> m0(n), m1(n);
std::vector<std::uint8_t> choices(n);
for (std::size_t i = 0; i < n; ++i)
{
m0[i] = dpf::uniform_sample<simde__m128i>();
m1[i] = dpf::uniform_sample<simde__m128i>();
choices[i] = static_cast<std::uint8_t>(i & 1u);
}
std::vector<simde__m128i> again0(2), again1(2);
again0[0] = dpf::uniform_sample<simde__m128i>();
again0[1] = dpf::uniform_sample<simde__m128i>();
again1[0] = dpf::uniform_sample<simde__m128i>();
again1[1] = dpf::uniform_sample<simde__m128i>();
const std::uint8_t again_choice[2] = {1, 0};
std::vector<simde__m128i> got, got2;
run_pair(
[&](trio & net) {
dpf::iknp::detail::role_state st;
std::vector<simde__m128i> unused;
dpf::iknp::transfer_labels(net.to(role::p1), 0, true, st, m0, m1,
{}, unused);
EXPECT_TRUE(unused.empty());
std::vector<simde__m128i> empty_out;
dpf::iknp::transfer_labels(net.to(role::p1), 0, true, st, {}, {},
{}, empty_out);
dpf::iknp::transfer_labels(net.to(role::p1), 0, true, st, again0,
again1, {}, unused);
},
[&](trio & net) {
dpf::iknp::detail::role_state st;
dpf::iknp::transfer_labels(net.to(role::p0), 1, false, st, {}, {},
choices, got);
std::vector<std::uint8_t> none;
std::vector<simde__m128i> empty_out;
dpf::iknp::transfer_labels(net.to(role::p0), 1, false, st, {}, {},
none, empty_out);
EXPECT_TRUE(empty_out.empty());
dpf::iknp::transfer_labels(net.to(role::p0), 1, false, st, {}, {},
std::vector<std::uint8_t>(again_choice, again_choice + 2), got2);
});
ASSERT_EQ(got.size(), n);
for (std::size_t i = 0; i < n; ++i)
{
const auto & expect = choices[i] ? m1[i] : m0[i];
EXPECT_EQ(std::memcmp(&got[i], &expect, sizeof(expect)), 0) << i;
}
ASSERT_EQ(got2.size(), 2u);
EXPECT_EQ(std::memcmp(&got2[0], &again1[0], sizeof(again1[0])), 0);
EXPECT_EQ(std::memcmp(&got2[1], &again0[1], sizeof(again0[1])), 0);
}
} // namespace

View file

@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <tuple>
#include "dpf.hpp"
#include "grotto/fixedpoint.hpp"
@ -1895,3 +1896,315 @@ TEST_F(IncrementalDpfTest, SequenceRecipeAtPrefixSlot)
}
}
// ---------------------------------------------------------------------------
// Regressions for bugs found under ASan/UBSan:
// * eq(..., if_false) must absorb on every eval surface, not only point
// * path memoizer resume past a full-width cmp depth must not shift by nbits
// ---------------------------------------------------------------------------
TEST_F(IncrementalDpfTest, EqIfFalseAbsorbsOnEveryEvalSurface)
{
const uint8_t alpha = 5;
const uint8_t yt = 7;
const uint8_t yf = 3;
auto [k0, k1] = dpf::make_dpf(alpha, dpf::eq(yt, yf));
using KT = std::decay_t<decltype(k0)>;
EXPECT_TRUE(KT::is_multilevel);
EXPECT_EQ(std::get<0>(k0.public_addends), yf);
constexpr auto opl = KT::template outputs_per_leaf_of<0>;
auto expect_at = [&](uint8_t q) {
return (q == alpha) ? yt : yf;
};
for (unsigned q = 0; q < 8u; ++q)
{
EXPECT_EQ(recon(*dpf::eval_point(k0, uint8_t(q)),
*dpf::eval_point(k1, uint8_t(q))),
expect_at(uint8_t(q)))
<< "point q=" << q;
}
auto [f0, itf0] = dpf::eval_full(k0);
auto [f1, itf1] = dpf::eval_full(k1);
(void)itf0;
(void)itf1;
for (unsigned q = 0; q < 8u; ++q)
{
EXPECT_EQ(recon(f0[q], f1[q]), expect_at(uint8_t(q)))
<< "full q=" << q;
}
// Leaf-aligned interval so buffer index == lane.
constexpr uint8_t from = 0;
constexpr uint8_t to = 15; // one full packing leaf when opl==16
auto [iv0, ii0] = dpf::eval_interval(dpf::out<0>, k0, from, to);
auto [iv1, ii1] = dpf::eval_interval(dpf::out<0>, k1, from, to);
(void)ii0;
(void)ii1;
ASSERT_EQ(iv0.size(), static_cast<std::size_t>(to - from + 1));
for (unsigned q = from; q <= to; ++q)
{
const uint8_t want = (q < 8u) ? expect_at(uint8_t(q)) : yf;
EXPECT_EQ(recon(iv0[q - from], iv1[q - from]), want)
<< "interval q=" << q;
}
std::array<uint8_t, 8> pts{{7, 0, 5, 2, 1, 6, 3, 4}};
auto s0 = dpf::make_output_buffer_for(k0, pts.size());
auto s1 = dpf::make_output_buffer_for(k1, pts.size());
dpf::eval_sequence(dpf::out<0>, k0, pts.begin(), pts.end(), s0);
dpf::eval_sequence(dpf::out<0>, k1, pts.begin(), pts.end(), s1);
for (std::size_t i = 0; i < pts.size(); ++i)
{
auto e0 = dpf::eval_point(dpf::out<0>, k0, pts[i]);
auto e1 = dpf::eval_point(dpf::out<0>, k1, pts[i]);
EXPECT_EQ(recon(s0[i * opl + e0.offset], s1[i * opl + e1.offset]),
expect_at(pts[i]))
<< "sequence i=" << i;
}
std::array<uint8_t, 8> sorted{{0, 1, 2, 3, 4, 5, 6, 7}};
auto b0 = dpf::eval_sequence_breadth_first(dpf::out<0>, k0, sorted.begin(),
sorted.end());
auto b1 = dpf::eval_sequence_breadth_first(dpf::out<0>, k1, sorted.begin(),
sorted.end());
for (std::size_t i = 0; i < sorted.size(); ++i)
{
EXPECT_EQ(recon(b0[i], b1[i]), expect_at(sorted[i]))
<< "breadth i=" << i;
}
// Weights must cover every packing lane the interval exterior materialises.
std::vector<uint64_t> w(iv0.size(), 1);
uint64_t expect_ip = 0;
for (std::size_t i = 0; i < iv0.size(); ++i)
expect_ip += static_cast<uint64_t>(recon(iv0[i], iv1[i])) * w[i];
const auto ip0 = dpf::eval_inner_product(dpf::out<0>, k0, from, to, w);
const auto ip1 = dpf::eval_inner_product(dpf::out<0>, k1, from, to, w);
EXPECT_EQ(static_cast<uint8_t>(recon(ip0, ip1)),
static_cast<uint8_t>(expect_ip));
}
TEST_F(IncrementalDpfTest, EqAtIfFalseWithPackedIntervalAndXor)
{
const uint32_t alpha = 0x00c0ffeeu;
auto [k0, k1] = dpf::make_dpf(alpha,
dpf::eq_at<16>(uint16_t{99}, uint16_t{7}),
dpf::eq(dpf::xor_wrapper<uint32_t>{0x00ff00ffu},
dpf::xor_wrapper<uint32_t>{0x00001111u}));
using KT = std::decay_t<decltype(k0)>;
constexpr auto opl = KT::template outputs_per_leaf_of<0>;
const uint16_t lane = static_cast<uint16_t>(alpha >> 16);
// Align to a packing leaf so buffer index math is exact.
const uint16_t from = static_cast<uint16_t>(lane & ~(opl - 1u));
const uint16_t to = static_cast<uint16_t>(from + opl - 1u);
auto [buf0, iit0] = dpf::eval_interval(dpf::out<0, 16>, k0, from, to);
auto [buf1, iit1] = dpf::eval_interval(dpf::out<0, 16>, k1, from, to);
(void)iit0;
(void)iit1;
for (uint16_t q = from; q <= to; ++q)
{
const std::size_t idx = static_cast<std::size_t>(q - from);
const uint16_t want = (q == lane) ? uint16_t{99} : uint16_t{7};
EXPECT_EQ(recon(buf0[idx], buf1[idx]), want) << "eq_at lane=" << q;
const uint32_t full = static_cast<uint32_t>(q) << 16;
EXPECT_EQ(recon(*dpf::eval_point(dpf::out<0, 16>, k0, full),
*dpf::eval_point(dpf::out<0, 16>, k1, full)),
want);
}
// Spot-check the full-width xor_wrapper eq (avoid 2^32 full-domain walk).
const auto on = dpf::xor_wrapper<uint32_t>{0x00ff00ffu};
const auto off = dpf::xor_wrapper<uint32_t>{0x00001111u};
EXPECT_EQ(recon(*dpf::eval_point(dpf::out<1>, k0, alpha),
*dpf::eval_point(dpf::out<1>, k1, alpha)),
on);
EXPECT_EQ(recon(*dpf::eval_point(dpf::out<1>, k0, alpha ^ 1u),
*dpf::eval_point(dpf::out<1>, k1, alpha ^ 1u)),
off);
EXPECT_EQ(recon(*dpf::eval_point(dpf::out<1>, k0, 0u),
*dpf::eval_point(dpf::out<1>, k1, 0u)),
off);
// Leaf-aligned interval around α for the xor slot.
using KT1 = std::decay_t<decltype(k0)>;
constexpr auto opl1 = KT1::template outputs_per_leaf_of<1>;
const uint32_t xfrom = alpha & ~(opl1 - 1u);
const uint32_t xto = xfrom + static_cast<uint32_t>(opl1 - 1u);
auto [xf0, xi0] = dpf::eval_interval(dpf::out<1>, k0, xfrom, xto);
auto [xf1, xi1] = dpf::eval_interval(dpf::out<1>, k1, xfrom, xto);
(void)xi0;
(void)xi1;
for (uint32_t q = xfrom; q <= xto; ++q)
{
const auto want = (q == alpha) ? on : off;
EXPECT_EQ(recon(xf0[q - xfrom], xf1[q - xfrom]), want) << "xor q=" << q;
}
}
TEST_F(IncrementalDpfTest, EqIfFalseDealerMatchesDoernerShelatSurfaces)
{
const uint8_t alpha = 0x2au;
const uint8_t x0s = 0x11u;
const uint8_t x1s = static_cast<uint8_t>(alpha ^ x0s);
auto dealer = dpf::make_dpf(alpha, dpf::eq(uint8_t{9}, uint8_t{4}));
auto ds = dpf::make_dpf_doerner_shelat(x0s, x1s,
dpf::eq(uint8_t{9}, uint8_t{4}));
for (unsigned q = 0; q < 256u; q += 17u)
{
const uint8_t qq = static_cast<uint8_t>(q);
EXPECT_EQ(recon(*dpf::eval_point(dealer.first, qq),
*dpf::eval_point(dealer.second, qq)),
recon(*dpf::eval_point(ds.first, qq),
*dpf::eval_point(ds.second, qq)));
}
auto [fa0, ia0] = dpf::eval_full(dealer.first);
auto [fa1, ia1] = dpf::eval_full(dealer.second);
auto [fb0, ib0] = dpf::eval_full(ds.first);
auto [fb1, ib1] = dpf::eval_full(ds.second);
(void)ia0;
(void)ia1;
(void)ib0;
(void)ib1;
for (unsigned q = 0; q < 256u; q += 17u)
EXPECT_EQ(recon(fa0[q], fa1[q]), recon(fb0[q], fb1[q]));
}
TEST_F(IncrementalDpfTest, PathMemoizerResumePastFullWidthCmpDepth)
{
const uint32_t alpha = 0x00abcdefu;
auto [k0, k1] = dpf::make_dpf(alpha, uint32_t{7}, dpf::lt(uint64_t{3}));
auto path0 = dpf::make_basic_path_memoizer(k0);
auto path1 = dpf::make_basic_path_memoizer(k1);
const uint64_t mask = k0.cmp().mask;
EXPECT_EQ(dpf::reconstruct(dpf::eval_point(dpf::cmp, k0, alpha, path0),
dpf::eval_point(dpf::cmp, k1, alpha, path1))
& mask,
0u);
EXPECT_EQ(dpf::reconstruct(
dpf::eval_point(dpf::cmp, k0, alpha - 1u, path0),
dpf::eval_point(dpf::cmp, k1, alpha - 1u, path1))
& mask,
3u);
EXPECT_EQ(recon(*dpf::eval_point(k0, alpha, path0),
*dpf::eval_point(k1, alpha, path1)),
7u);
EXPECT_EQ(recon(*dpf::eval_point(k0, alpha ^ 1u, path0),
*dpf::eval_point(k1, alpha ^ 1u, path1)),
0u);
auto p0 = dpf::make_basic_path_memoizer(k0);
auto p1 = dpf::make_basic_path_memoizer(k1);
EXPECT_EQ(recon(*dpf::eval_point(k0, alpha, p0),
*dpf::eval_point(k1, alpha, p1)),
7u);
EXPECT_EQ(dpf::reconstruct(dpf::eval_point(dpf::cmp, k0, alpha, p0),
dpf::eval_point(dpf::cmp, k1, alpha, p1))
& mask,
0u);
EXPECT_EQ(recon(*dpf::eval_point(k0, alpha ^ 2u, p0),
*dpf::eval_point(k1, alpha ^ 2u, p1)),
0u);
}
TEST_F(IncrementalDpfTest, PathMemoizerResumeCmpOnlyFullWidth)
{
const uint32_t alpha = 0x80000001u;
auto [k0, k1] = dpf::make_dpf(alpha, dpf::geq(uint64_t{5}, uint64_t{1}));
auto path0 = dpf::make_basic_path_memoizer(k0);
auto path1 = dpf::make_basic_path_memoizer(k1);
const uint64_t mask = k0.cmp().mask;
auto rq = [&](uint32_t q) {
return dpf::reconstruct(dpf::eval_point(dpf::cmp, k0, q, path0),
dpf::eval_point(dpf::cmp, k1, q, path1))
& mask;
};
EXPECT_EQ(rq(alpha), 5u);
EXPECT_EQ(rq(alpha), 5u);
EXPECT_EQ(rq(alpha - 1u), 1u);
EXPECT_EQ(rq(alpha + 1u), 5u);
}
TEST_F(IncrementalDpfTest, EqMixedWithBlockedCmpSharesMemoizer)
{
const uint8_t alpha = 0x5au;
auto [k0, k1] = dpf::make_dpf(alpha, dpf::eq(uint8_t{11}, uint8_t{2}),
dpf::block_width<3>(dpf::lt(uint64_t{9}, uint64_t{1})));
auto path0 = dpf::make_basic_path_memoizer(k0);
auto path1 = dpf::make_basic_path_memoizer(k1);
EXPECT_EQ(recon(*dpf::eval_point(k0, alpha, path0),
*dpf::eval_point(k1, alpha, path1)),
11u);
const uint64_t mask = k0.cmp().mask;
EXPECT_EQ(dpf::reconstruct(
dpf::eval_point(dpf::cmp, k0, uint8_t(alpha - 1), path0),
dpf::eval_point(dpf::cmp, k1, uint8_t(alpha - 1), path1))
& mask,
9u);
EXPECT_EQ(dpf::reconstruct(dpf::eval_point(dpf::cmp, k0, alpha, path0),
dpf::eval_point(dpf::cmp, k1, alpha, path1))
& mask,
1u);
auto [f0, i0] = dpf::eval_full(k0);
auto [f1, i1] = dpf::eval_full(k1);
(void)i0;
(void)i1;
EXPECT_EQ(recon(f0[alpha], f1[alpha]), 11u);
EXPECT_EQ(recon(f0[uint8_t(alpha ^ 1)], f1[uint8_t(alpha ^ 1)]), 2u);
}
TEST_F(IncrementalDpfTest, IntervalMemoizerReuseAcrossEqAndShallowAt)
{
const uint32_t alpha = 0x00a1b2c3u;
auto [k0, k1] = dpf::make_dpf(alpha, dpf::eq(uint32_t{8}, uint32_t{1}),
dpf::at<8>(uint8_t{42}));
using KT = std::decay_t<decltype(k0)>;
constexpr auto opl0 = KT::template outputs_per_leaf_of<0>;
const uint32_t efrom = alpha & ~(opl0 - 1u);
const uint32_t eto = efrom + static_cast<uint32_t>(opl0 - 1u);
auto [f0, fi0] = dpf::eval_interval(dpf::out<0>, k0, efrom, eto);
auto [f1, fi1] = dpf::eval_interval(dpf::out<0>, k1, efrom, eto);
(void)fi0;
(void)fi1;
for (uint32_t q = efrom; q <= eto; ++q)
{
const uint32_t want = (q == alpha) ? 8u : 1u;
EXPECT_EQ(recon(f0[q - efrom], f1[q - efrom]), want) << "eq q=" << q;
}
const uint8_t top = static_cast<uint8_t>(alpha >> 24);
auto [ait0, ai0] = dpf::eval_interval(dpf::out<1, 8>, k0, uint8_t{0},
uint8_t{255});
auto [ait1, ai1] = dpf::eval_interval(dpf::out<1, 8>, k1, uint8_t{0},
uint8_t{255});
(void)ai0;
(void)ai1;
EXPECT_EQ(recon(ait0[top], ait1[top]), 42u);
EXPECT_EQ(recon(ait0[uint8_t(top ^ 1)], ait1[uint8_t(top ^ 1)]), 0u);
// Re-walk a different eq window after the shallow interval.
const uint32_t from = alpha - 3u;
const uint32_t to = alpha + 3u;
const uint32_t afrom = from & ~(opl0 - 1u);
const uint32_t ato = (to + opl0 - 1u) & ~(opl0 - 1u);
const uint32_t ato_inclusive = ato + static_cast<uint32_t>(opl0 - 1u);
auto [eit0, ee0] = dpf::eval_interval(dpf::out<0>, k0, afrom,
ato_inclusive);
auto [eit1, ee1] = dpf::eval_interval(dpf::out<0>, k1, afrom,
ato_inclusive);
(void)ee0;
(void)ee1;
for (uint32_t q = from; q <= to; ++q)
{
const uint32_t want = (q == alpha) ? 8u : 1u;
EXPECT_EQ(recon(eit0[q - afrom], eit1[q - afrom]), want) << "q=" << q;
}
}

View file

@ -0,0 +1,311 @@
#include <gtest/gtest.h>
#include "dpf.hpp"
#include <array>
#include <cstdint>
#include <cstring>
#include <type_traits>
#include <vector>
namespace
{
template <typename LaneT>
using storage = dpf::leaf_storage_t<LaneT>;
template <typename LaneT>
constexpr unsigned lane_bits()
{
if constexpr (dpf::utils::is_packed_subbyte_v<LaneT>)
return static_cast<unsigned>(dpf::utils::packed_lane_bits_v<LaneT>);
else
return static_cast<unsigned>(sizeof(LaneT) * 8u);
}
template <typename LaneT>
constexpr std::uint64_t lane_mask()
{
if constexpr (dpf::utils::is_packed_subbyte_v<LaneT>)
return (std::uint64_t{1} << dpf::utils::packed_lane_bits_v<LaneT>) - 1u;
else if constexpr (sizeof(LaneT) >= 8)
return ~std::uint64_t{0};
else
return (std::uint64_t{1} << (sizeof(LaneT) * 8u)) - 1u;
}
template <typename LaneT>
std::uint64_t get_lane(const storage<LaneT> * buf, std::size_t i)
{
if constexpr (dpf::utils::is_packed_subbyte_v<LaneT>)
{
constexpr unsigned w = dpf::utils::packed_lane_bits_v<LaneT>;
const std::size_t bit = i * w;
const std::size_t word = bit / 64u;
const unsigned shift = static_cast<unsigned>(bit % 64u);
std::uint64_t v = buf[word] >> shift;
if (shift + w > 64u)
v |= buf[word + 1u] << (64u - shift);
return v & lane_mask<LaneT>();
}
else if constexpr (std::is_class_v<LaneT>)
{
return static_cast<std::uint64_t>(
static_cast<typename LaneT::integral_type>(buf[i])) & lane_mask<LaneT>();
}
else
{
return static_cast<std::uint64_t>(buf[i]) & lane_mask<LaneT>();
}
}
template <typename LaneT>
void set_lane(storage<LaneT> * buf, std::size_t i, std::uint64_t val)
{
val &= lane_mask<LaneT>();
if constexpr (dpf::utils::is_packed_subbyte_v<LaneT>)
{
constexpr unsigned w = dpf::utils::packed_lane_bits_v<LaneT>;
const std::size_t bit = i * w;
const std::size_t word = bit / 64u;
const unsigned shift = static_cast<unsigned>(bit % 64u);
const std::uint64_t mask = lane_mask<LaneT>();
buf[word] = (buf[word] & ~(mask << shift)) | (val << shift);
if (shift + w > 64u)
{
const unsigned lo = 64u - shift;
buf[word + 1u] = (buf[word + 1u] & ~(mask >> lo)) | (val >> lo);
}
}
else
{
buf[i] = static_cast<storage<LaneT>>(val);
}
}
template <typename LaneT>
std::vector<storage<LaneT>> make_key(std::size_t nleaves, std::size_t key_id)
{
std::vector<storage<LaneT>> v(dpf::leaf_storage_words<LaneT>(nleaves),
storage<LaneT>{0});
for (std::size_t i = 0; i < nleaves; ++i)
{
// Distinct pattern per (key, leaf); fits every width's mask.
const std::uint64_t val
= (key_id * 131u + i * 17u + 3u) & lane_mask<LaneT>();
set_lane<LaneT>(v.data(), i, val);
}
return v;
}
template <typename LaneT>
void check_roundtrip(std::size_t nkeys, std::size_t nleaves)
{
std::vector<std::vector<storage<LaneT>>> owned;
owned.reserve(nkeys);
std::vector<const storage<LaneT> *> in_ptrs;
std::vector<storage<LaneT> *> out_ptrs;
in_ptrs.reserve(nkeys);
out_ptrs.reserve(nkeys);
for (std::size_t k = 0; k < nkeys; ++k)
{
owned.push_back(make_key<LaneT>(nleaves, k));
in_ptrs.push_back(owned.back().data());
}
std::vector<storage<LaneT>> interleaved(
dpf::leaf_storage_words<LaneT>(nkeys * nleaves), storage<LaneT>{0});
dpf::interleave_leaves<LaneT>(interleaved.data(), in_ptrs.data(), nkeys,
nleaves);
for (std::size_t i = 0; i < nleaves; ++i)
{
for (std::size_t k = 0; k < nkeys; ++k)
{
const std::size_t g = dpf::cohort_index(i, k, nkeys);
EXPECT_EQ(get_lane<LaneT>(interleaved.data(), g),
get_lane<LaneT>(owned[k].data(), i))
<< "LaneT bits=" << lane_bits<LaneT>() << " nkeys=" << nkeys
<< " nleaves=" << nleaves << " i=" << i << " k=" << k;
}
}
std::vector<std::vector<storage<LaneT>>> round;
round.reserve(nkeys);
for (std::size_t k = 0; k < nkeys; ++k)
{
round.emplace_back(dpf::leaf_storage_words<LaneT>(nleaves),
storage<LaneT>{0});
out_ptrs.push_back(round.back().data());
}
dpf::deinterleave_leaves<LaneT>(out_ptrs.data(), interleaved.data(), nkeys,
nleaves);
for (std::size_t k = 0; k < nkeys; ++k)
{
for (std::size_t i = 0; i < nleaves; ++i)
{
EXPECT_EQ(get_lane<LaneT>(round[k].data(), i),
get_lane<LaneT>(owned[k].data(), i))
<< "deinterleave bits=" << lane_bits<LaneT>()
<< " nkeys=" << nkeys << " i=" << i << " k=" << k;
}
}
}
} // namespace
template <typename LaneT>
class InterleaveLeavesTest : public ::testing::Test
{
};
using InterleaveTypes = ::testing::Types<
std::uint64_t,
std::uint32_t,
std::uint16_t,
std::uint8_t,
dpf::nyble,
dpf::twobit,
dpf::bit,
dpf::gf2,
dpf::gf22,
dpf::gf24,
dpf::gf28,
dpf::gf216,
dpf::gf232,
dpf::gf264>;
TYPED_TEST_SUITE(InterleaveLeavesTest, InterleaveTypes);
TYPED_TEST(InterleaveLeavesTest, KeyCountsAndOddLength)
{
// Length not a multiple of pack width (8 for bits-in-byte, 32 for
// 2-bit-in-u64, 16 for 4-bit-in-u64, etc.): 13 is odd for all of those.
constexpr std::size_t nleaves = 13;
for (std::size_t nkeys : {std::size_t{1}, std::size_t{3}, std::size_t{5},
std::size_t{16}})
check_roundtrip<TypeParam>(nkeys, nleaves);
}
TYPED_TEST(InterleaveLeavesTest, LongerNotMultipleOfWord)
{
// 70 leaves: not a multiple of 64 (bit word) or 32 (twobit word).
check_roundtrip<TypeParam>(5, 70);
check_roundtrip<TypeParam>(8, 70);
}
TEST(InterleaveLeaves, BitPackingIsKeyMajorWithinLeaf)
{
// Three keys, five 1-bit leaves. Logical order of bits in the output
// stream is (i0,k0),(i0,k1),(i0,k2),(i1,k0),... — not a byte index of
// i*m+k into a uint8_t array.
constexpr std::size_t nkeys = 3;
constexpr std::size_t nleaves = 5;
std::array<std::uint64_t, 1> k0{0}, k1{0}, k2{0};
// key0: 1 0 1 1 0
// key1: 0 1 1 0 1
// key2: 1 1 0 0 1
k0[0] = 0b01101ull;
k1[0] = 0b10110ull;
k2[0] = 0b10011ull;
const std::uint64_t * keys[3] = {k0.data(), k1.data(), k2.data()};
std::uint64_t out = 0;
dpf::interleave_leaves<dpf::bit>(&out, keys, nkeys, nleaves);
// Expected 15 bits, low-first: for i=0..4, bits of keys 0,1,2
// i0: 1,0,1 | i1: 0,1,1 | i2: 1,1,0 | i3: 1,0,0 | i4: 0,1,1
// bit0..14 = 1,0,1,0,1,1,1,1,0,1,0,0,0,1,1
const std::uint64_t expect = 0b110001011110101ull;
EXPECT_EQ(out & ((1ull << 15) - 1ull), expect);
}
TEST(InterleaveLeaves, InnerProductModintFromBits)
{
// Same three keys as above. Leaf integers (key 0 in the low bit):
// i0 = 0b101 = 5, i1 = 0b110 = 6, i2 = 0b011 = 3, i3 = 0b001 = 1, i4 = 0b110 = 6.
constexpr std::size_t nkeys = 3;
constexpr std::size_t nleaves = 5;
std::array<std::uint64_t, 1> k0{0b01101ull}, k1{0b10110ull}, k2{0b10011ull};
const std::uint64_t * keys[3] = {k0.data(), k1.data(), k2.data()};
std::uint64_t bits = 0;
dpf::interleave_leaves<dpf::bit>(&bits, keys, nkeys, nleaves);
const std::uint64_t w[5] = {1, 2, 3, 4, 5};
const auto got = dpf::interleaved_bits_inner_product<3>(&bits, nleaves, nkeys, w);
// 5*1 + 6*2 + 3*3 + 1*4 + 6*5 = 5+12+9+4+30 = 60 ≡ 60-7*8 = 4 (mod 8)
EXPECT_EQ(static_cast<unsigned>(static_cast<dpf::modint<3>::integral_type>(got)), 4u);
std::array<dpf::modint<3>, 5> mw{1, 2, 3, 4, 5};
const auto got_m = dpf::interleaved_bits_inner_product<3>(&bits, nleaves, nkeys, mw);
EXPECT_EQ(static_cast<unsigned>(static_cast<dpf::modint<3>::integral_type>(got_m)), 4u);
}
TEST(InterleaveLeaves, InnerProductModint128)
{
constexpr std::size_t nkeys = 128;
constexpr std::size_t nleaves = 3;
std::vector<std::uint64_t> key_bits(nkeys * 2, 0);
std::vector<const std::uint64_t *> ptrs(nkeys);
for (std::size_t k = 0; k < nkeys; ++k)
{
// Leaf 0: bit k set. Leaf 1: only key 0. Leaf 2: zero.
if (k == 0)
key_bits[k * 2] = 0b011ull;
else
key_bits[k * 2] = 0b001ull;
ptrs[k] = key_bits.data() + k * 2;
}
std::vector<std::uint64_t> bits(dpf::leaf_storage_words<dpf::bit>(nkeys * nleaves));
dpf::interleave_leaves<dpf::bit>(bits.data(), ptrs.data(), nkeys, nleaves);
const std::uint64_t w[3] = {1, 3, 9};
const auto got = dpf::interleaved_bits_inner_product<128>(
bits.data(), nleaves, nkeys, w);
using limb = dpf::modint<128>::integral_type;
const limb all = ~limb{0};
const limb expect = all * limb{1} + limb{1} * limb{3};
EXPECT_EQ(static_cast<limb>(got), expect);
}
TEST(InterleaveLeaves, InnerProductLowBitsOfWideGroup)
{
// Eight 1-bit keys, read as modint<4>: only keys 0..3.
constexpr std::size_t nkeys = 8;
constexpr std::size_t nleaves = 2;
std::array<std::uint64_t, 8> raw{};
const std::uint64_t * ptrs[8];
for (std::size_t k = 0; k < nkeys; ++k)
{
raw[k] = (k < 4) ? 0b11ull : 0b01ull; // both leaves set for low keys
ptrs[k] = &raw[k];
}
std::uint64_t bits[4]{};
dpf::interleave_leaves<dpf::bit>(bits, ptrs, nkeys, nleaves);
const std::uint64_t w[2] = {1, 1};
const auto got = dpf::interleaved_bits_inner_product<4>(bits, nleaves, nkeys, w);
// Each leaf's low 4 bits are 0b1111 = 15. 15+15 = 30 ≡ 14 (mod 16).
EXPECT_EQ(static_cast<unsigned>(static_cast<dpf::modint<4>::integral_type>(got)), 14u);
}
TYPED_TEST(InterleaveLeavesTest, EmptyAndSingleLeaf)
{
check_roundtrip<TypeParam>(0, 0);
check_roundtrip<TypeParam>(3, 0);
check_roundtrip<TypeParam>(0, 5);
check_roundtrip<TypeParam>(1, 1);
check_roundtrip<TypeParam>(4, 1);
}
TEST(InterleaveLeaves, InnerProductEmptyIsZero)
{
const std::uint64_t w[1] = {9};
EXPECT_EQ(static_cast<unsigned>(static_cast<dpf::modint<3>::integral_type>(
dpf::interleaved_bits_inner_product<3>(
static_cast<const std::uint64_t *>(nullptr), 0, 3, w))),
0u);
std::uint64_t bits = 0;
EXPECT_EQ(static_cast<unsigned>(static_cast<dpf::modint<3>::integral_type>(
dpf::interleaved_bits_inner_product<3>(&bits, 5, 0, w))),
0u);
}

View file

@ -0,0 +1,68 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <type_traits>
#include <vector>
#include "dpf.hpp"
TEST(ItDpf3, OnPointAndOffPoint)
{
constexpr std::uint8_t alpha = 42;
constexpr std::uint64_t beta = 7;
auto [k0, k1, k2] = dpf::make_it_dpf3(alpha, beta);
EXPECT_FALSE((std::is_same_v<decltype(k0),
decltype(std::get<0>(dpf::make_dpf3(alpha, dpf::fp61{1})))>));
const auto on = dpf::eval_it_dpf3(k0, alpha) + dpf::eval_it_dpf3(k1, alpha)
+ dpf::eval_it_dpf3(k2, alpha);
EXPECT_EQ(on, beta);
const auto off = dpf::eval_it_dpf3(k0, std::uint8_t{41})
+ dpf::eval_it_dpf3(k1, std::uint8_t{41})
+ dpf::eval_it_dpf3(k2, std::uint8_t{41});
EXPECT_EQ(off, 0u);
}
TEST(ItDpf3, InnerProductIsTableEntry)
{
constexpr std::uint8_t index = 17;
std::vector<std::uint64_t> table(256);
for (std::size_t i = 0; i < table.size(); ++i)
table[i] = i * i + 3;
auto [k0, k1, k2] = dpf::make_it_dpf3(index, 1);
const auto s0 = dpf::eval_it_dpf3_inner_product(k0, table);
const auto s1 = dpf::eval_it_dpf3_inner_product(k1, table);
const auto s2 = dpf::eval_it_dpf3_inner_product(k2, table);
EXPECT_EQ(s0 + s1 + s2, table[index]);
}
// Seam: same PIR shape as make_dpf3 + shamir reconstruct, different key type
// and three-way sum instead of any-two Lagrange.
TEST(ItDpf3, PirDotDistinctFromShamirDpf3)
{
constexpr std::uint8_t index = 42;
std::vector<std::uint64_t> table(256);
for (std::size_t i = 0; i < table.size(); ++i)
table[i] = i + 1;
auto [i0, i1, i2] = dpf::make_it_dpf3(index, 1);
const auto it_opened =
dpf::eval_it_dpf3_inner_product(i0, table)
+ dpf::eval_it_dpf3_inner_product(i1, table)
+ dpf::eval_it_dpf3_inner_product(i2, table);
EXPECT_EQ(it_opened, table[index]);
std::vector<dpf::fp61> ftable(256);
for (std::size_t i = 0; i < ftable.size(); ++i)
ftable[i] = dpf::fp61{static_cast<std::uint64_t>(i + 1)};
auto [d1, d2, d3] = dpf::make_dpf3(index, dpf::fp61{1});
const auto s1 = dpf::eval_full_inner_product(d1, ftable);
const auto s2 = dpf::eval_full_inner_product(d2, ftable);
const auto opened = dpf::shamir3::reconstruct(
dpf::as_share(d1, s1), dpf::as_share(d2, s2));
EXPECT_EQ(opened.raw(), table[index]);
EXPECT_FALSE((std::is_same_v<decltype(i0), decltype(d1)>));
}

View file

@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <tuple>
#include <algorithm>
#include <cstdint>

View file

@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <tuple>
#include "dpf.hpp"
@ -164,8 +165,8 @@ void expect_interval(In alpha, Lane y, In from, In to)
EXPECT_EQ(p1, std::end(b.second));
}
template <typename Key>
void expect_live_words(const Key & key, const auto & g)
template <typename Key, typename GenevalResult>
void expect_live_words(const Key & key, const GenevalResult & g)
{
ASSERT_LE(g.live_levels, g.correction_words.size());
for (std::size_t i = 0; i < g.live_levels; ++i)
@ -326,8 +327,7 @@ TEST(LaneBlast, Uint8TwobitEveryInputAndEveryShape)
EXPECT_EQ(f0, std::end(fi0));
const in_t unsorted[] = {255, 40, 0};
EXPECT_THROW(dpf::make_sequence_recipe(k0, std::begin(unsorted), std::end(unsorted)),
std::runtime_error);
EXPECT_THROW(dpf::make_sequence_recipe(k0, std::begin(unsorted), std::end(unsorted)), std::runtime_error);
const in_t seq[] = {0, 40, 40, 41, 104, 255};
auto recipe = dpf::make_sequence_recipe(k0, std::begin(seq), std::end(seq));
auto s0 = dpf::eval_sequence(k0, recipe);

View file

@ -0,0 +1,149 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <utility>
#include <vector>
#include "dpf.hpp"
namespace
{
// Full-block payload ⇒ outputs_per_leaf = 1, so a single public F matches the
// leaf correction word share-for-share.
using input_t = std::uint8_t;
using output_t = simde_uint128;
constexpr std::size_t n = 256;
output_t leaf_cw_as_output(const dpf::utils::dpf_type_t<dpf::prg::aes128,
dpf::prg::aes128, input_t, output_t> & key)
{
using exterior = typename std::decay_t<decltype(key)>::exterior_node;
return dpf::extract_leaf<exterior, output_t>(key.template leaf<0>(), 0);
}
// party_key wrappers from make_dpf
template <typename Key>
output_t leaf_cw_as_output(const Key & key)
{
using exterior = typename Key::exterior_node;
return dpf::extract_leaf<exterior, output_t>(key.template leaf<0>(), 0);
}
output_t make_payload(std::uint64_t lo)
{
return output_t{lo};
}
} // namespace
TEST(LeafLater, MatchesCorrectedImmediately)
{
const input_t alpha = 42;
const output_t beta = make_payload(7);
auto [k0, k1] = dpf::make_dpf(alpha, beta);
// Public leaf CW (same on both keys); applying it recovers the immediate path.
const output_t F = leaf_cw_as_output(k0);
EXPECT_EQ(F, leaf_cw_as_output(k1));
auto full0 = dpf::eval_full(k0);
auto full1 = dpf::eval_full(k1);
std::vector<output_t> buf0(n), buf1(n);
std::vector<std::uint8_t> c0(n), c1(n);
dpf::eval_full(buf0, c0, k0, dpf::leaf_later{});
dpf::eval_full(buf1, c1, k1, dpf::leaf_later{});
dpf::apply_leaf_correction(buf0, c0, F);
dpf::apply_leaf_correction(buf1, c1, F);
auto it0 = std::begin(full0.second);
auto it1 = std::begin(full1.second);
for (std::size_t i = 0; i < n; ++i, ++it0, ++it1)
{
const auto imm0 = dpf::detail_walk::group_value(*it0);
const auto imm1 = dpf::detail_walk::group_value(*it1);
EXPECT_EQ(buf0[i], imm0) << "party0 @" << i;
EXPECT_EQ(buf1[i], imm1) << "party1 @" << i;
EXPECT_EQ(buf0[i] - buf1[i], imm0 - imm1) << i;
const auto got = buf0[i] - buf1[i];
if (i == alpha)
EXPECT_EQ(got, beta);
else
EXPECT_EQ(got, output_t{0});
}
}
TEST(LeafLater, ApplyAfterRotateMatchesCorrectThenRotate)
{
const input_t alpha = 10;
const output_t beta = make_payload(99);
const std::size_t s = 32;
auto [k0, k1] = dpf::make_dpf(alpha, beta);
const output_t F = leaf_cw_as_output(k0);
auto full0 = dpf::eval_full(k0);
auto full1 = dpf::eval_full(k1);
std::vector<output_t> early0(n), early1(n);
{
auto a = std::begin(full0.second);
auto b = std::begin(full1.second);
for (std::size_t i = 0; i < n; ++i, ++a, ++b)
{
early0[i] = dpf::detail_walk::group_value(*a);
early1[i] = dpf::detail_walk::group_value(*b);
}
}
early0 = dpf::cyclic_shift(early0, s);
early1 = dpf::cyclic_shift(early1, s);
std::vector<output_t> late0(n), late1(n);
std::vector<std::uint8_t> c0(n), c1(n);
dpf::eval_full(late0, c0, k0, dpf::leaf_later{});
dpf::eval_full(late1, c1, k1, dpf::leaf_later{});
dpf::cyclic_shift_pair(late0, c0, s);
dpf::cyclic_shift_pair(late1, c1, s);
dpf::apply_leaf_correction(late0, c0, F);
dpf::apply_leaf_correction(late1, c1, F);
for (std::size_t i = 0; i < n; ++i)
{
EXPECT_EQ(late0[i], early0[i]) << i;
EXPECT_EQ(late1[i], early1[i]) << i;
}
}
TEST(LeafLater, DuoramShapedUpdate)
{
// Unit at r, rotate by i*−r, then apply a public F built from the leaf CW
// of a key keyed at the online message (same as correcting immediately for
// that message). Neighbours stay unchanged.
const input_t r = 10;
const input_t i_star = 42;
const std::size_t shift = static_cast<std::size_t>(i_star - r);
const output_t message = make_payload(7);
auto [k0, k1] = dpf::make_dpf(r, message);
const output_t F = leaf_cw_as_output(k0);
std::vector<output_t> mem0(n), mem1(n);
mem0[i_star] = make_payload(100);
std::vector<output_t> v0(n), v1(n);
std::vector<std::uint8_t> t0(n), t1(n);
dpf::eval_full_add_into(v0, t0, k0, dpf::leaf_later{}, dpf::rotate{shift});
dpf::eval_full_add_into(v1, t1, k1, dpf::leaf_later{}, dpf::rotate{shift});
dpf::apply_leaf_correction(v0, t0, F);
dpf::apply_leaf_correction(v1, t1, F);
for (std::size_t i = 0; i < n; ++i)
{
mem0[i] = mem0[i] + v0[i];
mem1[i] = mem1[i] + v1[i];
}
EXPECT_EQ(mem0[i_star] - mem1[i_star], make_payload(100) + message);
EXPECT_EQ(mem0[r] - mem1[r], output_t{0});
EXPECT_EQ(mem0[0] - mem1[0], output_t{0});
}

744
test/tests/log_test.cpp Normal file
View file

@ -0,0 +1,744 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <map>
#include <mutex>
#include <sstream>
#include <string>
#include <thread>
#include <vector>
#include <unistd.h>
#include "dpf/app_flow.hpp"
#include "dpf/experiment.hpp"
#include "dpf/launch.hpp"
#include "dpf/log.hpp"
#include "dpf/prg_aes.hpp"
#include "dpf/prg_chacha.hpp"
#include "dpf/prg_count.hpp"
#include "dpf/run_config.hpp"
#include "dpf/run_log.hpp"
#include "dpf/yao.hpp"
namespace
{
std::string temp_path(const char * tag)
{
return "/tmp/libdpf_log_test_" + std::to_string(::getpid()) + "_" + tag + ".log";
}
std::vector<std::string> read_lines(const std::string & path)
{
std::ifstream in(path);
std::vector<std::string> out;
std::string line;
while (std::getline(in, line))
out.push_back(line);
return out;
}
bool has(const std::string & line, const std::string & needle)
{
return line.find(needle) != std::string::npos;
}
std::vector<std::string> with_event(const std::vector<std::string> & lines,
const std::string & ev)
{
std::vector<std::string> out;
for (const auto & l : lines)
if (has(l, " ev=" + ev + " ") || (l.size() >= ev.size() + 4
&& l.compare(l.size() - ev.size() - 4, std::string::npos, " ev=" + ev) == 0))
out.push_back(l);
return out;
}
/// Route the log to a fresh file for one test; silence it afterwards.
struct file_log
{
std::string path;
explicit file_log(const char * tag, dpf::log::level l = dpf::log::level::debug,
dpf::log::seed_policy seeds = dpf::log::seed_policy::full)
: path(temp_path(tag))
{
std::remove(path.c_str());
dpf::log::settings s;
s.threshold = l;
s.sinks = "file:" + path;
s.seeds = seeds;
dpf::log::configure(s);
}
~file_log()
{
dpf::log::settings s;
s.sinks = "none";
dpf::log::configure(s);
std::remove(path.c_str());
}
std::vector<std::string> lines() const { return read_lines(path); }
};
} // namespace
// Runs first: nothing has configured the log yet in this process.
TEST(Log, SilentUntilConfigured)
{
EXPECT_FALSE(dpf::log::enabled(dpf::log::level::error));
EXPECT_FALSE(dpf::log::enabled(dpf::log::level::info));
int evaluated = 0;
DPF_LOG(error, "never").kv("x", ++evaluated);
EXPECT_EQ(evaluated, 0);
}
TEST(Log, LevelAndSeedPolicyNames)
{
using dpf::log::level;
EXPECT_EQ(dpf::log::parse_level("silent"), level::silent);
EXPECT_EQ(dpf::log::parse_level("warn"), level::warning);
EXPECT_EQ(dpf::log::parse_level("3"), level::info);
EXPECT_EQ(dpf::log::parse_level("absurd"), level::trace);
EXPECT_THROW(dpf::log::parse_level("loud"), std::invalid_argument);
EXPECT_EQ(dpf::log::parse_seed_policy("hash"), dpf::log::seed_policy::hash);
EXPECT_EQ(dpf::log::parse_seed_policy("off"), dpf::log::seed_policy::off);
EXPECT_THROW(dpf::log::parse_seed_policy("maybe"), std::invalid_argument);
dpf::log::settings bad;
bad.sinks = "stderr,carrier-pigeon";
EXPECT_THROW(dpf::log::configure(bad), std::invalid_argument);
EXPECT_FALSE(dpf::log::enabled(dpf::log::level::error));
}
TEST(Log, RecordFormatAndThreshold)
{
std::string path;
{
file_log log("format", dpf::log::level::info);
path = log.path;
DPF_LOG(info, "demo").kv("plain", "abc").kv("spaced", "a b")
.kv("quote", "say \"hi\"").kv("eq", "k=v").kv("n", std::size_t{42})
.kv("neg", -3).kv("flag", true).kv("empty", "").hex("h", "\x01\xff", 2);
DPF_LOG(debug, "hidden").kv("x", 1);
DPF_LOG(warning, "shown").kv("x", 2);
const auto lines = log.lines();
ASSERT_EQ(lines.size(), 2u);
const auto & l = lines[0];
EXPECT_EQ(l.rfind("ts=", 0), 0u);
EXPECT_TRUE(has(l, "Z lvl=info inv=" + dpf::log::invocation_id() + " pid="));
EXPECT_EQ(dpf::log::invocation_id().size(), 16u);
EXPECT_TRUE(has(l, " ev=demo plain=abc spaced=\"a b\" quote=\"say \\\"hi\\\"\" "
"eq=\"k=v\" n=42 neg=-3 flag=1 empty=\"\" h=01ff"));
EXPECT_TRUE(has(lines[1], "lvl=warning"));
EXPECT_TRUE(has(lines[1], "ev=shown x=2"));
}
EXPECT_FALSE(dpf::log::enabled(dpf::log::level::error));
}
TEST(Log, RoleScopeTagsAndRestores)
{
file_log log("role");
DPF_LOG(info, "outside");
{
const dpf::log::role_scope a("p1");
DPF_LOG(info, "inside");
{
const dpf::log::role_scope b("dealer");
DPF_LOG(info, "nested");
}
DPF_LOG(info, "back");
}
const auto lines = log.lines();
ASSERT_EQ(lines.size(), 4u);
EXPECT_FALSE(has(lines[0], " role="));
EXPECT_TRUE(has(lines[1], " role=p1 ev=inside"));
EXPECT_TRUE(has(lines[2], " role=dealer ev=nested"));
EXPECT_TRUE(has(lines[3], " role=p1 ev=back"));
}
TEST(Log, SeedPolicies)
{
const std::uint8_t seed[4] = {0xde, 0xad, 0xbe, 0xef};
{
file_log log("seed_full", dpf::log::level::info, dpf::log::seed_policy::full);
DPF_LOG(info, "s").seed("value", seed, sizeof(seed));
EXPECT_TRUE(has(log.lines().at(0), "value=deadbeef"));
}
{
file_log log("seed_hash", dpf::log::level::info, dpf::log::seed_policy::hash);
DPF_LOG(info, "s").seed("value", seed, sizeof(seed));
const auto l = log.lines().at(0);
EXPECT_TRUE(has(l, "value=sha256:"));
EXPECT_FALSE(has(l, "deadbeef"));
const auto pos = l.find("sha256:") + 7;
EXPECT_EQ(l.substr(pos).size(), 16u);
}
{
file_log log("seed_off", dpf::log::level::info, dpf::log::seed_policy::off);
DPF_LOG(info, "s").seed("value", seed, sizeof(seed));
EXPECT_TRUE(has(log.lines().at(0), "value=withheld"));
}
const auto abc = dpf::log::detail::sha256("",
reinterpret_cast<const std::uint8_t *>("abc"), 3);
std::string hex;
dpf::log::detail::append_hex(hex, abc.data(), abc.size());
EXPECT_EQ(hex, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
}
TEST(Log, ConcurrentRecordsStayWholeLines)
{
file_log log("threads", dpf::log::level::info);
constexpr int threads = 8;
constexpr int each = 400;
std::vector<std::thread> ts;
for (int t = 0; t < threads; ++t)
ts.emplace_back([t] {
const dpf::log::role_scope role("p" + std::to_string(t));
for (int i = 0; i < each; ++i)
DPF_LOG(info, "tick").kv("thread", t).kv("i", i)
.kv("pad", std::string(64, static_cast<char>('a' + t)));
});
for (auto & t : ts)
t.join();
const auto lines = log.lines();
ASSERT_EQ(lines.size(), static_cast<std::size_t>(threads * each));
std::map<int, int> per;
for (const auto & l : lines)
{
ASSERT_EQ(l.rfind("ts=", 0), 0u) << l;
const auto p = l.find(" thread=");
ASSERT_NE(p, std::string::npos) << l;
const int t = std::stoi(l.substr(p + 8));
EXPECT_TRUE(has(l, " role=p" + std::to_string(t) + " ")) << l;
EXPECT_TRUE(has(l, "pad=" + std::string(64, static_cast<char>('a' + t)))) << l;
++per[t];
}
for (int t = 0; t < threads; ++t)
EXPECT_EQ(per[t], each);
}
TEST(Log, RunConfigKeysAndDescribe)
{
dpf::app::run_config cfg;
cfg.set("log_level", "debug");
cfg.set("log", "file:/tmp/x.log,stderr");
cfg.set("log_seeds", "hash");
cfg.set("cpu2", "3");
EXPECT_EQ(cfg.log_level, dpf::log::level::debug);
EXPECT_EQ(cfg.log_sinks, "file:/tmp/x.log,stderr");
EXPECT_EQ(cfg.log_seeds, dpf::log::seed_policy::hash);
EXPECT_EQ(cfg.cpu[2], 3);
EXPECT_THROW(cfg.set("log_level", "chatty"), std::invalid_argument);
EXPECT_THROW(cfg.set("cpu0", "3abc"), std::invalid_argument);
std::map<std::string, std::string> kv;
for (const auto & p : cfg.describe())
kv[p.first] = p.second;
for (const char * k : {"log_level", "log", "log_seeds", "cpu2", "compact",
"keepalive_idle", "connect_ms", "accept_ms", "join_ms", "handshake_ms"})
EXPECT_EQ(kv.count(k), 1u) << k;
EXPECT_EQ(kv["log_level"], "debug");
EXPECT_EQ(kv["cpu2"], "3");
::setenv("DPF_LOG_LEVEL", "warning", 1);
::setenv("DPF_LOG_SEEDS", "off", 1);
const auto env = dpf::app::run_config::from_env();
::unsetenv("DPF_LOG_LEVEL");
::unsetenv("DPF_LOG_SEEDS");
EXPECT_EQ(env.log_level, dpf::log::level::warning);
EXPECT_EQ(env.log_seeds, dpf::log::seed_policy::off);
}
TEST(Log, BannerRecordsProvenance)
{
const std::string path = temp_path("banner");
std::remove(path.c_str());
::setenv("DPF_TEST_MARKER", "banner-check", 1);
::setenv("DPF_TEST_SEED", "0123456789abcdef", 1);
dpf::app::run_config cfg;
cfg.log_sinks = "file:" + path;
cfg.log_seeds = dpf::log::seed_policy::hash;
dpf::app::start_logging(cfg);
const auto lines = read_lines(path);
::unsetenv("DPF_TEST_MARKER");
::unsetenv("DPF_TEST_SEED");
for (const char * ev : {"start", "build", "host", "argv", "env", "log", "config"})
EXPECT_FALSE(with_event(lines, ev).empty()) << ev;
const auto build = with_event(lines, "build").at(0);
EXPECT_TRUE(has(build, " rev=")) << build;
EXPECT_TRUE(has(build, " entropy=")) << build;
EXPECT_TRUE(has(with_event(lines, "start").at(0), " utc=")) << lines[0];
bool marker = false;
bool seed_hidden = false;
for (const auto & l : with_event(lines, "env"))
{
marker = marker || has(l, "name=DPF_TEST_MARKER value=banner-check");
seed_hidden = seed_hidden
|| (has(l, "name=DPF_TEST_SEED value=sha256:") && !has(l, "0123456789abcdef"));
}
EXPECT_TRUE(marker);
EXPECT_TRUE(seed_hidden);
const auto config = with_event(lines, "config").at(0);
EXPECT_TRUE(has(config, " transport=async")) << config;
EXPECT_TRUE(has(config, " log_seeds=hash")) << config;
dpf::log::settings off;
off.sinks = "none";
dpf::log::configure(off);
std::remove(path.c_str());
}
TEST(Log, LinkUpNamesEndpointsAndAuthentication)
{
for (const char * encryption : {"off", "on"})
{
file_log log("links", dpf::log::level::info);
dpf::protocol::composer c0(0);
dpf::protocol::composer c1(1);
auto x0 = c0.input(dpf::protocol::domain::a, 8);
auto x1 = c1.input(dpf::protocol::domain::a, 8);
auto o0 = c0.exchange(x0);
(void)c1.exchange(x1);
auto p0 = c0.schedule();
auto p1 = c1.schedule();
dpf::app::party_values v0(p0.nodes().size()), v1(p1.nodes().size());
const std::uint64_t a = 11, b = 31;
v0[x0.id].assign(8, 0);
v1[x1.id].assign(8, 0);
std::memcpy(v0[x0.id].data(), &a, 8);
std::memcpy(v1[x1.id].data(), &b, 8);
dpf::app::run_config cfg;
cfg.kind = dpf::net::transport::mux;
cfg.set("encryption", encryption);
const bool tls = std::strcmp(encryption, "on") == 0;
const auto r = dpf::run_two_party(p0, p1, v0, v1, {}, cfg);
std::uint64_t open = 0;
std::memcpy(&open, v0[o0.id].data(), 8);
EXPECT_EQ(open, 42u) << encryption;
ASSERT_EQ(r.party_wall_ns.size(), 2u);
const auto lines = log.lines();
const auto ups = with_event(lines, "link.up");
ASSERT_EQ(ups.size(), 2u) << encryption;
bool saw_accept = false, saw_connect = false;
for (const auto & l : ups)
{
EXPECT_TRUE(has(l, " transport=mux")) << l;
EXPECT_TRUE(has(l, " local=127.0.0.1:")) << l;
EXPECT_TRUE(has(l, " remote=127.0.0.1:")) << l;
if (tls)
{
EXPECT_TRUE(has(l, " encryption=TLSv1.3/")) << l;
EXPECT_TRUE(has(l, " peer_key=")) << l;
EXPECT_TRUE(has(l, " auth=")) << l;
}
else
EXPECT_TRUE(has(l, " auth=none encryption=none")) << l;
EXPECT_TRUE(has(l, " sndbuf=")) << l;
EXPECT_TRUE(has(l, " rtt_us=")) << l;
saw_accept = saw_accept || (has(l, " role=p0 ") && has(l, " how=accept peer=p1"));
saw_connect = saw_connect || (has(l, " role=p1 ") && has(l, " how=connect peer=p0"));
}
EXPECT_TRUE(saw_accept) << encryption;
EXPECT_TRUE(saw_connect) << encryption;
EXPECT_FALSE(with_event(lines, "listen").empty());
EXPECT_EQ(with_event(lines, "plan").size(), 2u);
const auto done = with_event(lines, "party.done");
ASSERT_EQ(done.size(), 2u);
for (const auto & l : done)
{
EXPECT_TRUE(has(l, " wall_ns=")) << l;
EXPECT_TRUE(has(l, " wire_out=")) << l;
}
EXPECT_EQ(with_event(lines, "parties.done").size(), 1u);
EXPECT_TRUE(with_event(lines, "link.plaintext").empty());
}
}
TEST(Log, NodeArgumentsNameTheBadField)
{
const char * bad_party[] = {"node", "--party=zero", "--peers=a:1,b:2"};
try
{
(void)dpf::app::parse_node_args(3, const_cast<char **>(bad_party),
dpf::app::run_config{});
FAIL() << "expected a parse error";
}
catch (const std::invalid_argument & e)
{
EXPECT_TRUE(has(e.what(), "--party needs a number")) << e.what();
}
const char * bad_port[] = {"node", "--party=0", "--peers=a:1,b:70000"};
EXPECT_THROW((void)dpf::app::parse_node_args(3, const_cast<char **>(bad_port),
dpf::app::run_config{}),
std::invalid_argument);
const char * memory[] = {"node", "--party=1", "--peers=a:1,b:2", "--transport=async"};
const auto a = dpf::app::parse_node_args(4, const_cast<char **>(memory),
dpf::app::run_config{});
EXPECT_EQ(a.cfg.kind, dpf::net::transport::mux);
EXPECT_EQ(a.replaced_transport, "async");
}
TEST(SymCount, PurposeAndPrimitiveCells)
{
using dpf::prg::primitive;
using dpf::prg::purpose;
dpf::prg::reset_eval_count();
auto seed = simde_mm_set_epi64x(3, 4);
auto blk = dpf::prg::aes128::eval(seed, 0);
blk = dpf::prg::aes128::eval(blk, 1);
{
const dpf::prg::purpose_scope h(purpose::hash);
(void)dpf::prg::aes128::eval01(blk);
{
const dpf::prg::purpose_scope inner(purpose::harness);
(void)dpf::prg::aes128::eval(blk, 7);
}
(void)dpf::prg::aes128::eval(blk, 8);
}
(void)dpf::prg::chacha<20>::eval(seed, 0);
EXPECT_EQ(dpf::prg::count(purpose::expand, primitive::aes128), 2u);
EXPECT_EQ(dpf::prg::count(purpose::hash, primitive::aes128), 3u);
EXPECT_EQ(dpf::prg::count(purpose::harness, primitive::aes128), 1u);
EXPECT_EQ(dpf::prg::count(purpose::expand, primitive::chacha), 1u);
EXPECT_EQ(dpf::prg::eval_count(), 6u);
const auto snap = dpf::prg::snapshot();
std::uint64_t all = 0;
for (auto n : snap)
all += n;
EXPECT_EQ(all, 7u);
dpf::prg::reset_eval_count();
EXPECT_EQ(dpf::prg::eval_count(), 0u);
}
TEST(SymCount, GarblingHashesCountAsHash)
{
using dpf::prg::primitive;
using dpf::prg::purpose;
dpf::prg::reset_eval_count();
const auto x = simde_mm_set_epi64x(9, 10);
(void)dpf::yao::detail::cr_hash(x, 5);
EXPECT_EQ(dpf::prg::count(purpose::hash, primitive::aes128), 1u);
EXPECT_EQ(dpf::prg::count(purpose::expand, primitive::aes128), 0u);
dpf::prg::reset_eval_count();
}
TEST(SymCount, ExperimentSeedStreamIsNotProtocolWork)
{
using dpf::prg::primitive;
using dpf::prg::purpose;
dpf::experiment ex("stream");
ex.begin_timing();
std::array<std::uint8_t, 1600> buf{};
dpf::uniform_fill(buf);
ex.end_timing();
EXPECT_EQ(ex.random_bytes(), 1600u);
EXPECT_EQ(ex.prg_evals(), 0u);
const auto & sym = ex.sym_counts();
EXPECT_EQ(sym[static_cast<std::size_t>(purpose::harness) * dpf::prg::primitive_count
+ static_cast<std::size_t>(primitive::aes128)],
100u);
}
TEST(Experiment, SeedSourceIsLoggedAndWritten)
{
const std::string dir = "/tmp/libdpf_log_test_csv_" + std::to_string(::getpid());
(void)::system(("rm -rf '" + dir + "'").c_str());
file_log log("seed_source", dpf::log::level::info);
dpf::experiment::master_seed master{};
{
dpf::experiment ex("fresh_one");
master = ex.seed();
EXPECT_FALSE(ex.seed_provided());
ex.begin_timing();
(void)dpf::prg::aes128::eval(simde_mm_set_epi64x(1, 2), 0);
ex.end_timing();
ex.write_csv(dir);
}
{
auto again = dpf::experiment::replay("fresh_one", master);
EXPECT_TRUE(again.seed_provided());
again.set_run_id(1);
again.write_csv(dir);
}
const auto seeds = with_event(log.lines(), "seed");
ASSERT_EQ(seeds.size(), 2u);
std::string hex;
dpf::log::detail::append_hex(hex, master.data(), master.size());
EXPECT_TRUE(has(seeds[0], " source=fresh")) << seeds[0];
EXPECT_TRUE(has(seeds[0], " value=" + hex)) << seeds[0];
EXPECT_TRUE(has(seeds[1], " source=provided")) << seeds[1];
EXPECT_TRUE(has(seeds[1], " value=" + hex)) << seeds[1];
const auto runs = read_lines(dir + "/runs.csv");
ASSERT_EQ(runs.size(), 3u);
EXPECT_EQ(runs[0], "name,party,run_id,invocation,seed_source,written_utc");
EXPECT_TRUE(has(runs[1], "fresh_one,p0,0," + dpf::log::invocation_id() + ",fresh,"));
EXPECT_TRUE(has(runs[2], "fresh_one,p0,1," + dpf::log::invocation_id() + ",provided,"));
const auto sym = read_lines(dir + "/sym.csv");
ASSERT_GE(sym.size(), 2u);
EXPECT_EQ(sym[0], "name,party,run_id,purpose,primitive,blocks,invocation");
EXPECT_EQ(sym[1], "fresh_one,p0,0,expand,aes128,1," + dpf::log::invocation_id());
(void)::system(("rm -rf '" + dir + "'").c_str());
}
namespace
{
constexpr std::uint32_t k_draw = 94201;
struct chain
{
dpf::protocol::plan plan;
dpf::protocol::node x;
dpf::protocol::node out;
};
chain make_chain(std::size_t party, int steps)
{
dpf::protocol::composer c(party);
chain ch;
ch.x = c.input(dpf::protocol::domain::a, 8);
auto cur = ch.x;
for (int i = 0; i < steps; ++i)
cur = c.compute(k_draw, {c.exchange(cur)}, dpf::protocol::domain::a, 8);
ch.out = cur;
ch.plan = c.schedule();
return ch;
}
struct draw_log
{
std::mutex mu;
std::vector<std::uint64_t> values;
std::size_t hooked = 0;
};
/// Adds 1 to its input, draws one word, and runs `aes` AES blocks.
dpf::protocol::kernel_fn draw_kernel(draw_log & log, int aes)
{
return [&log, aes](std::uint32_t, const std::vector<dpf::protocol::node> &,
const std::vector<dpf::protocol::block_span> & inputs,
dpf::protocol::block_span output, std::size_t lanes) {
const bool hooked = dpf::detail::uniform_bytes_hook != nullptr;
const auto r = dpf::uniform_sample<std::uint64_t>();
{
std::lock_guard<std::mutex> lock(log.mu);
log.values.push_back(r);
log.hooked += hooked ? 1 : 0;
}
auto blk = simde_mm_set_epi64x(1, 2);
for (int i = 0; i < aes; ++i)
blk = dpf::prg::aes128::eval(blk, static_cast<psnip_uint32_t>(i));
asm volatile("" : "+x"(blk));
for (std::size_t l = 0; l < lanes; ++l)
{
std::uint64_t v = 0;
std::memcpy(&v, inputs[0].at(l), 8);
++v;
std::memcpy(output.at(l), &v, 8);
}
};
}
struct chain_run
{
std::vector<dpf::protocol::plan> plans;
std::vector<dpf::app::party_values> inputs;
};
chain_run two_party_chain(int steps)
{
chain_run r;
for (std::size_t p = 0; p < 2; ++p)
{
auto ch = make_chain(p, steps);
dpf::app::party_values v(ch.plan.nodes().size());
v[ch.x.id].assign(8, 0);
const std::uint64_t in = p == 0 ? 5 : 6;
std::memcpy(v[ch.x.id].data(), &in, 8);
r.plans.push_back(ch.plan);
r.inputs.push_back(v);
}
return r;
}
std::vector<std::uint64_t> sorted_draws(draw_log & log)
{
std::lock_guard<std::mutex> lock(log.mu);
auto v = log.values;
log.values.clear();
log.hooked = 0;
std::sort(v.begin(), v.end());
return v;
}
void expect_replay(dpf::net::transport kind, std::size_t pool, std::size_t trials)
{
const auto run = two_party_chain(3);
draw_log log;
std::map<std::uint32_t, dpf::protocol::kernel_fn> k{{k_draw, draw_kernel(log, 0)}};
dpf::app::run_config cfg;
cfg.kind = kind;
cfg.compute_threads = pool;
cfg.trials = trials;
dpf::experiment first("replay");
(void)dpf::app::exercise_parties(run.plans, run.inputs, k, &first, cfg);
const std::size_t hooked = log.hooked;
const auto a = sorted_draws(log);
auto again = dpf::experiment::replay("replay", first.seed());
(void)dpf::app::exercise_parties(run.plans, run.inputs, k, &again, cfg);
const auto b = sorted_draws(log);
ASSERT_EQ(a.size(), 6u * trials) << dpf::net::transport_name(kind) << " pool " << pool;
EXPECT_EQ(hooked, a.size()) << dpf::net::transport_name(kind) << " pool " << pool;
EXPECT_EQ(a, b) << dpf::net::transport_name(kind) << " pool " << pool;
EXPECT_TRUE(first.has_seed_named("p0/master"));
EXPECT_TRUE(first.has_seed_named("p1/master"));
}
} // namespace
TEST(Replay, RecordedMasterReplaysEveryParty)
{
expect_replay(dpf::net::transport::async_memory, 0, 1);
expect_replay(dpf::net::transport::memory_sink, 0, 1);
expect_replay(dpf::net::transport::mux, 0, 3);
}
TEST(Replay, ComputePoolKernelsDrawFromTheirPartysStream)
{
expect_replay(dpf::net::transport::async_memory, 2, 1);
}
TEST(Replay, PartyStreamsDifferAndDependOnlyOnTheMaster)
{
const dpf::experiment::master_seed m{};
auto a = dpf::experiment::replay("x", m);
const auto p0 = a.derive_party(0).seed();
const auto p1 = a.derive_party(1).seed();
auto b = dpf::experiment::replay("other name", m);
EXPECT_NE(p0, p1);
EXPECT_EQ(b.derive_party(1).seed(), p1);
EXPECT_EQ(b.derive_party(1).seed_origin(), dpf::experiment::origin::derived);
}
TEST(Counting, ComputePoolWorkIsChargedToItsParty)
{
const auto run = two_party_chain(3);
draw_log log;
std::map<std::uint32_t, dpf::protocol::kernel_fn> k{{k_draw, draw_kernel(log, 20000)}};
for (std::size_t pool : {std::size_t{0}, std::size_t{2}})
{
dpf::app::run_config cfg;
cfg.kind = dpf::net::transport::async_memory;
cfg.compute_threads = pool;
dpf::experiment ex("pool");
(void)dpf::app::exercise_parties(run.plans, run.inputs, k, &ex, cfg);
EXPECT_EQ(ex.prg_evals(), 3u * 20000u) << "pool " << pool;
EXPECT_EQ(ex.random_bytes(), 3u * 8u) << "pool " << pool;
EXPECT_GT(ex.cpu_ns(), ex.wall_ns() / 4) << "pool " << pool;
(void)sorted_draws(log);
}
}
TEST(Bytes, ReceivedBytesMatchSentOnASymmetricChain)
{
const auto run = two_party_chain(3);
draw_log log;
std::map<std::uint32_t, dpf::protocol::kernel_fn> k{{k_draw, draw_kernel(log, 0)}};
dpf::app::run_config cfg;
cfg.kind = dpf::net::transport::mux;
dpf::experiment ex("bytes");
(void)dpf::app::exercise_parties(run.plans, run.inputs, k, &ex, cfg);
EXPECT_EQ(ex.bytes_out(), ex.plan_bytes_out());
EXPECT_EQ(ex.bytes_in(), ex.bytes_out());
EXPECT_EQ(ex.edge_bytes_in(dpf::protocol::edge_channel::peer),
ex.edge_bytes_out(dpf::protocol::edge_channel::peer));
ASSERT_EQ(ex.rounds().size(), 3u);
for (const auto & r : ex.rounds())
EXPECT_EQ(r.bytes_in, 8u) << "round " << r.round;
}
TEST(Trials, EveryPartyIsTimedAndMediansReachSummary)
{
const auto run = two_party_chain(2);
draw_log log;
std::map<std::uint32_t, dpf::protocol::kernel_fn> k{{k_draw, draw_kernel(log, 0)}};
dpf::app::run_config cfg;
cfg.kind = dpf::net::transport::async_memory;
cfg.warmup = 1;
cfg.trials = 3;
dpf::experiment ex("trials");
(void)dpf::app::exercise_parties(run.plans, run.inputs, k, &ex, cfg);
EXPECT_EQ(ex.trials().size(), 3u);
EXPECT_GE(ex.slowest_median_ns(), ex.median_trial_ns());
const std::string dir = "/tmp/libdpf_log_test_trials_" + std::to_string(::getpid());
(void)::system(("rm -rf '" + dir + "'").c_str());
ex.write_csv(dir);
const auto trials = read_lines(dir + "/trials.csv");
ASSERT_EQ(trials.size(), 1u + 3u * 2u);
EXPECT_EQ(trials[0], "name,party,run_id,trial,wall_ns,invocation");
EXPECT_EQ(trials[1].rfind("trials,p0,0,0,", 0), 0u) << trials[1];
EXPECT_EQ(trials[2].rfind("trials,p1,0,0,", 0), 0u) << trials[2];
const auto summary = read_lines(dir + "/summary.csv");
ASSERT_EQ(summary.size(), 2u);
EXPECT_TRUE(has(summary[0], ",median_ns,slowest_median_ns,trials,invocation"));
EXPECT_TRUE(has(summary[1], "," + std::to_string(ex.median_trial_ns()) + ","
+ std::to_string(ex.slowest_median_ns()) + ",3," + dpf::log::invocation_id()));
(void)::system(("rm -rf '" + dir + "'").c_str());
}
TEST(Csv, ChangedHeaderMovesTheOldFileAside)
{
const std::string dir = "/tmp/libdpf_log_test_rotate_" + std::to_string(::getpid());
(void)::system(("rm -rf '" + dir + "' && mkdir -p '" + dir + "'").c_str());
{
std::ofstream old(dir + "/summary.csv");
old << "name,party,run_id,wall_ns\nold,p0,0,1\n";
}
file_log log("rotate", dpf::log::level::warning);
dpf::experiment ex("rotate");
ex.write_csv(dir);
const auto now = read_lines(dir + "/summary.csv");
ASSERT_EQ(now.size(), 2u);
EXPECT_TRUE(has(now[0], ",invocation"));
std::size_t moved = 0;
for (const auto & e : std::filesystem::directory_iterator(dir))
if (e.path().filename().string().rfind("summary.before-", 0) == 0)
{
++moved;
EXPECT_EQ(read_lines(e.path().string()).at(1), "old,p0,0,1");
}
EXPECT_EQ(moved, 1u);
EXPECT_EQ(with_event(log.lines(), "csv.moved").size(), 1u);
(void)::system(("rm -rf '" + dir + "'").c_str());
}
TEST(Gate, AllPartiesStartTogetherAndAFailureReleasesTheRest)
{
{
dpf::app::detail::start_gate gate(3);
std::atomic<int> through{0};
std::vector<std::thread> ts;
for (int i = 0; i < 3; ++i)
ts.emplace_back([&] { through += gate.arrive_and_wait() ? 1 : 0; });
for (auto & t : ts)
t.join();
EXPECT_EQ(through.load(), 3);
}
{
dpf::app::detail::start_gate gate(3);
std::atomic<int> through{0};
std::vector<std::thread> ts;
for (int i = 0; i < 2; ++i)
ts.emplace_back([&] { through += gate.arrive_and_wait() ? 1 : 0; });
std::this_thread::sleep_for(std::chrono::milliseconds(20));
gate.fail();
for (auto & t : ts)
t.join();
EXPECT_EQ(through.load(), 0);
}
}

View file

@ -0,0 +1,472 @@
#include <gtest/gtest.h>
#include "dpf/compose.hpp"
#include "dpf/verifiable.hpp"
#include "grotto/lut_union.hpp"
#include "simde/simde/x86/sse2.h"
#include <algorithm>
#include <cstdint>
#include <limits>
#include <stdexcept>
#include <vector>
namespace
{
using grotto::offset_horner_group_add;
using grotto::offset_horner_group_sub;
grotto::piecewise_lut<uint8_t> lut_a()
{
grotto::piecewise_lut<uint8_t> lut;
lut.knots = {0, 10, 50};
lut.coeff = {{1, 0}, {0, 2}, {7, 1}};
return lut;
}
grotto::piecewise_lut<uint8_t> lut_b()
{
grotto::piecewise_lut<uint8_t> lut;
lut.knots = {0, 4, 12, 80};
lut.coeff = {{3, 0, 0}, {1, 0, 1}, {9, 2, 0}, {4, 1, 0}};
return lut;
}
grotto::piecewise_lut<uint8_t> lut_c()
{
grotto::piecewise_lut<uint8_t> lut;
lut.knots = {0, 7, 90};
lut.coeff = {{8, 1, 0}, {2, 0, 3}, {1, 1, 1}};
return lut;
}
std::vector<uint64_t> open_union(const grotto::offset_poly_keys<uint8_t> & mat,
const grotto::lut_union_plan<uint8_t> & plan)
{
const auto s0 = grotto::lut_union_eval<0>(mat, plan);
const auto s1 = grotto::lut_union_eval<1>(mat, plan);
EXPECT_EQ(s0.size(), s1.size());
std::vector<uint64_t> out(s0.size());
for (std::size_t i = 0; i < s0.size(); ++i)
out[i] = s0[i] + s1[i];
return out;
}
template <typename InputT>
void expect_partition(const grotto::lut_union_plan<InputT> & plan)
{
if (plan.knots.empty())
{
ADD_FAILURE() << "empty union";
return;
}
for (const auto & func : plan.funcs)
{
std::vector<int> seen(plan.knots.size(), 0);
for (const auto & span : func)
{
auto mark = [&](std::size_t j) {
if (j >= seen.size())
{
ADD_FAILURE() << "span index " << j;
return;
}
seen[j] += 1;
};
if (span.begin <= span.end)
{
for (std::size_t j = span.begin; j < span.end; ++j)
mark(j);
}
else
{
for (std::size_t j = span.begin; j < plan.knots.size(); ++j)
mark(j);
for (std::size_t j = 0; j < span.end; ++j)
mark(j);
}
}
for (int bit : seen)
EXPECT_EQ(bit, 1);
}
}
} // namespace
TEST(LutUnion, WideDomainMapsTheWrapAcrossTheFront)
{
grotto::piecewise_lut<uint64_t> a;
a.knots = {10, 30};
a.coeff = {{1}, {2}};
grotto::piecewise_lut<uint64_t> b;
b.knots = {0, 20};
b.coeff = {{3}, {4}};
const auto plan = grotto::make_lut_union_plan(
std::vector<grotto::piecewise_lut<uint64_t>>{a, b}, uint64_t{0});
ASSERT_EQ(plan.knots, (std::vector<uint64_t>{0, 10, 20, 30}));
EXPECT_EQ(plan.comparisons, 1u);
EXPECT_EQ(plan.prefix_walks, 1u);
EXPECT_EQ(plan.depth, 64u);
EXPECT_EQ(plan.geneval_rounds(), plan.depth);
EXPECT_EQ(plan.degree, 0u);
ASSERT_EQ(plan.funcs[0].size(), 2u);
EXPECT_EQ(plan.funcs[0][0].begin, 1u);
EXPECT_EQ(plan.funcs[0][0].end, 3u);
EXPECT_EQ(plan.funcs[0][1].begin, 3u);
EXPECT_EQ(plan.funcs[0][1].end, 1u);
ASSERT_EQ(plan.funcs[1].size(), 2u);
EXPECT_EQ(plan.funcs[1][0].begin, 0u);
EXPECT_EQ(plan.funcs[1][0].end, 2u);
EXPECT_EQ(plan.funcs[1][1].begin, 2u);
EXPECT_EQ(plan.funcs[1][1].end, 0u);
expect_partition(plan);
}
TEST(LutUnion, OneWalkMatchesEachLutOnItsOwnKnots)
{
const auto a = lut_a();
const auto b = lut_b();
const uint8_t center = 12;
const auto plan = grotto::make_lut_union_plan({a, b}, uint8_t{3});
EXPECT_GT(plan.endpoints(), a.knots.size());
EXPECT_EQ(plan.degree, 2u);
EXPECT_EQ(plan.lanes, 3u);
EXPECT_EQ(plan.depth, 8u);
EXPECT_EQ(plan.comparisons, 1u);
EXPECT_EQ(plan.prefix_walks, 1u);
const auto mat = grotto::make_offset_poly_keys<uint8_t>(center, plan.degree);
for (int eta = 0; eta < 256; eta += 17)
{
const auto e = static_cast<uint8_t>(eta);
const auto here = grotto::make_lut_union_plan({a, b}, e);
const auto got = open_union(mat, here);
ASSERT_EQ(got.size(), 2u);
EXPECT_EQ(got[0], grotto::offset_poly_clear<uint8_t>(center, a.knots, a.coeff, e))
<< eta;
EXPECT_EQ(got[1], grotto::offset_poly_clear<uint8_t>(center, b.knots, b.coeff, e))
<< eta;
}
}
TEST(LutUnion, GenevalPlanIsOneComparisonHoweverManyLuts)
{
const uint8_t eta = 3;
const auto few = grotto::make_lut_union_plan({lut_a(), lut_b()}, eta);
const auto many = grotto::make_lut_union_plan({lut_a(), lut_b(), lut_c()}, eta);
EXPECT_GT(many.endpoints(), few.endpoints());
EXPECT_EQ(few.geneval_rounds(), many.geneval_rounds());
EXPECT_EQ(few.funcs.size(), 2u);
EXPECT_EQ(many.funcs.size(), 3u);
dpf::protocol::composer c0(0);
grotto::schedule_lut_union(c0, few);
dpf::protocol::composer c1(0);
grotto::schedule_lut_union(c1, many);
const auto p0 = c0.default_plan();
const auto p1 = c1.default_plan();
EXPECT_EQ(p0.rounds(), few.depth);
EXPECT_EQ(p1.rounds(), many.depth);
EXPECT_EQ(p0.rounds(), p1.rounds());
EXPECT_EQ(p0.slot_bytes(0), grotto::lut_union_slot_bytes(few.lanes));
EXPECT_EQ(p1.slot_bytes(0), grotto::lut_union_slot_bytes(many.lanes));
}
TEST(LutUnion, GenevalXorAndAdditiveSharesMatchTheClearLuts)
{
const uint8_t center = 12;
const uint8_t eta = 3;
const auto a = lut_a();
const auto b = lut_b();
const auto plan = grotto::make_lut_union_plan({a, b}, eta);
const uint8_t share = 0x3c;
const uint8_t other = static_cast<uint8_t>(center ^ share);
const auto xor_got = grotto::geneval_lut_union(share, other, plan);
EXPECT_EQ(xor_got.eta, eta);
ASSERT_EQ(xor_got.value0.size(), 2u);
EXPECT_EQ(xor_got.value0[0] + xor_got.value1[0],
grotto::offset_poly_clear<uint8_t>(center, a.knots, a.coeff, eta));
EXPECT_EQ(xor_got.value0[1] + xor_got.value1[1],
grotto::offset_poly_clear<uint8_t>(center, b.knots, b.coeff, eta));
const uint8_t c0 = 5;
const uint8_t c1 = offset_horner_group_sub(center, c0);
const auto add_got = grotto::geneval_lut_union(dpf::arith_input, c0, c1, plan);
EXPECT_EQ(add_got.value0[0] + add_got.value1[0], xor_got.value0[0] + xor_got.value1[0]);
EXPECT_EQ(add_got.value0[1] + add_got.value1[1], xor_got.value0[1] + xor_got.value1[1]);
}
TEST(LutUnion, EasyAndConstantLutsShareTheUnion)
{
const auto relu = grotto::piecewise_from_easy(grotto::make_relu_lut<int8_t>());
const auto clip = grotto::piecewise_from_easy(
grotto::make_clip_lut<int8_t>(0, -2, 3));
grotto::constant_lut<int8_t> sign;
sign.bounds = {std::numeric_limits<int8_t>::min(), 0};
sign.values = {-1, 1};
const auto step = grotto::piecewise_from_constant(sign);
const int8_t center = -20;
const int8_t eta = 15;
const auto plan = grotto::make_lut_union_plan(
std::vector<grotto::piecewise_lut<int8_t>>{relu, clip, step}, eta);
EXPECT_EQ(plan.comparisons, 1u);
EXPECT_EQ(plan.prefix_walks, 1u);
EXPECT_GT(plan.endpoints(), relu.knots.size());
const auto mat = grotto::make_offset_poly_keys<int8_t>(center, plan.degree);
const auto s0 = grotto::lut_union_eval<0>(mat, plan);
const auto s1 = grotto::lut_union_eval<1>(mat, plan);
const int8_t wrapped = offset_horner_group_add(center, eta);
const uint64_t opened[3] = {s0[0] + s1[0], s0[1] + s1[1], s0[2] + s1[2]};
EXPECT_EQ(opened[0], static_cast<uint64_t>(grotto::make_relu_lut<int8_t>()(wrapped)));
EXPECT_EQ(opened[1], static_cast<uint64_t>(grotto::make_clip_lut<int8_t>(0, -2, 3)(wrapped)));
EXPECT_EQ(opened[2], static_cast<uint64_t>(sign(wrapped)));
EXPECT_EQ(opened[0], grotto::offset_poly_clear<int8_t>(center, relu.knots, relu.coeff, eta));
}
TEST(LutUnion, RejectsARoundingDenominatorAndANarrowKey)
{
EXPECT_THROW(grotto::piecewise_from_easy(grotto::make_leaky_relu_lut<int8_t>(1)),
std::invalid_argument);
EXPECT_THROW(grotto::make_lut_union_plan(
std::vector<grotto::piecewise_lut<uint8_t>>{}, uint8_t{0}),
std::invalid_argument);
const auto plan = grotto::make_lut_union_plan({lut_b()}, uint8_t{1});
const auto narrow = grotto::make_offset_poly_keys<uint8_t>(4, 0);
EXPECT_THROW(grotto::lut_union_eval<0>(narrow, plan), std::invalid_argument);
grotto::piecewise_lut<uint8_t> unsorted;
unsorted.knots = {0, 5, 3};
unsorted.coeff = {{1}, {1}, {1}};
EXPECT_THROW(grotto::make_lut_union_plan({unsorted}, uint8_t{0}), std::invalid_argument);
grotto::piecewise_lut<uint8_t> ragged;
ragged.knots = {0, 1};
ragged.coeff = {{1}, {1, 2}};
EXPECT_THROW(grotto::make_lut_union_plan({ragged}, uint8_t{0}), std::invalid_argument);
grotto::piecewise_lut<uint8_t> empty_row;
empty_row.knots = {0};
empty_row.coeff = {{}};
EXPECT_THROW(grotto::make_lut_union_plan({empty_row}, uint8_t{0}), std::invalid_argument);
grotto::piecewise_lut<uint8_t> too_wide;
too_wide.knots = {0};
too_wide.coeff = {std::vector<uint64_t>(grotto::offset_poly_max_degree + 2, 1)};
EXPECT_THROW(grotto::make_lut_union_plan({too_wide}, uint8_t{0}), std::invalid_argument);
grotto::easy_lut<int8_t> broken;
broken.bounds = {0};
broken.c0 = {0};
EXPECT_THROW(grotto::piecewise_from_easy(broken), std::invalid_argument);
grotto::constant_lut<int8_t> bare;
bare.bounds = {std::numeric_limits<int8_t>::min()};
EXPECT_THROW(grotto::piecewise_from_constant(bare), std::invalid_argument);
dpf::protocol::composer composer(0);
EXPECT_THROW(grotto::schedule_lut_union(composer, grotto::lut_union_plan<uint8_t>{}),
std::invalid_argument);
}
TEST(LutUnion, CoarsePieceCoversSeveralUnionKnots)
{
const auto plan = grotto::make_lut_union_plan({lut_a(), lut_b()}, uint8_t{0});
expect_partition(plan);
ASSERT_EQ(plan.knots, (std::vector<uint8_t>{0, 4, 10, 12, 50, 80}));
const auto & first = plan.funcs[0][0];
EXPECT_EQ(first.end - first.begin, 2u);
EXPECT_EQ(first.kappa, 0);
EXPECT_EQ(first.coeff, (std::vector<uint64_t>{1, 0}));
const uint8_t center = 200;
const uint8_t eta = 100;
const auto carried = grotto::make_lut_union_plan({lut_a(), lut_b()}, eta);
expect_partition(carried);
EXPECT_NE(std::find(carried.knots.begin(), carried.knots.end(), uint8_t{156}),
carried.knots.end());
const auto mat = grotto::make_offset_poly_keys<uint8_t>(center, carried.degree);
const auto got = open_union(mat, carried);
EXPECT_EQ(got[0], grotto::offset_poly_clear<uint8_t>(center, lut_a().knots, lut_a().coeff, eta));
EXPECT_EQ(got[1], grotto::offset_poly_clear<uint8_t>(center, lut_b().knots, lut_b().coeff, eta));
}
TEST(LutUnion, EveryEtaMatchesASeparatePolynomialEval)
{
const auto a = lut_a();
const auto b = lut_b();
const uint8_t center = 12;
const auto probe = grotto::make_lut_union_plan({a, b}, uint8_t{0});
const auto mat = grotto::make_offset_poly_keys<uint8_t>(center, probe.degree);
const auto wide = grotto::make_offset_poly_keys<uint8_t>(center, probe.degree + 2);
const auto alone_a = grotto::make_offset_poly_keys<uint8_t>(center, 1);
const auto alone_b = grotto::make_offset_poly_keys<uint8_t>(center, 2);
for (int eta = 0; eta < 256; ++eta)
{
const auto e = static_cast<uint8_t>(eta);
const auto plan = grotto::make_lut_union_plan({a, b}, e);
expect_partition(plan);
const auto got = open_union(mat, plan);
const auto again = open_union(mat, plan);
EXPECT_EQ(got, again);
EXPECT_EQ(got, open_union(wide, plan));
const uint64_t a_alone = grotto::offset_poly_eval<0>(alone_a, a.knots, a.coeff, e)
+ grotto::offset_poly_eval<1>(alone_a, a.knots, a.coeff, e);
const uint64_t b_alone = grotto::offset_poly_eval<0>(alone_b, b.knots, b.coeff, e)
+ grotto::offset_poly_eval<1>(alone_b, b.knots, b.coeff, e);
EXPECT_EQ(got[0], a_alone) << eta;
EXPECT_EQ(got[1], b_alone) << eta;
}
}
TEST(LutUnion, OnePieceLutCoversTheWholeUnion)
{
grotto::piecewise_lut<uint8_t> whole;
whole.knots = {0};
whole.coeff = {{5, 1}};
const auto plan = grotto::make_lut_union_plan({whole, lut_a()}, uint8_t{0});
expect_partition(plan);
ASSERT_EQ(plan.funcs[0].size(), 1u);
EXPECT_EQ(plan.funcs[0][0].begin, 0u);
EXPECT_EQ(plan.funcs[0][0].end, plan.knots.size());
// A nonzero eta inserts the carry cut, so the one public knot becomes two
// refined pieces. They still partition the union.
const uint8_t eta = 9;
const auto split = grotto::make_lut_union_plan({whole, lut_a()}, eta);
expect_partition(split);
EXPECT_GT(split.funcs[0].size(), 1u);
const uint8_t center = 40;
const auto mat = grotto::make_offset_poly_keys<uint8_t>(center, split.degree);
const auto got = open_union(mat, split);
EXPECT_EQ(got[0], grotto::offset_poly_clear<uint8_t>(center, whole.knots, whole.coeff, eta));
EXPECT_EQ(got[1], grotto::offset_poly_clear<uint8_t>(center, lut_a().knots, lut_a().coeff, eta));
}
TEST(LutUnion, IdenticalKnotsStayASingleCopy)
{
const auto a = lut_a();
const auto plan = grotto::make_lut_union_plan({a, a}, uint8_t{0});
EXPECT_EQ(plan.knots, a.knots);
EXPECT_EQ(plan.funcs.size(), 2u);
EXPECT_EQ(plan.prefix_walks, 1u);
expect_partition(plan);
}
TEST(LutUnion, VerifiableTokensCoverEveryLane)
{
const auto a = lut_a();
const auto b = lut_b();
const uint8_t center = 12;
const uint8_t eta = 5;
const auto plan = grotto::make_lut_union_plan({a, b}, eta);
const auto mat = grotto::make_offset_poly_keys<uint8_t>(center, plan.degree, dpf::verifiable{});
std::vector<dpf::proof_token> tok0(plan.lanes), tok1(plan.lanes);
const auto s0 = grotto::lut_union_eval<0>(mat, plan, tok0.data());
const auto s1 = grotto::lut_union_eval<1>(mat, plan, tok1.data());
EXPECT_EQ(s0[0] + s1[0], grotto::offset_poly_clear<uint8_t>(center, a.knots, a.coeff, eta));
EXPECT_EQ(s0[1] + s1[1], grotto::offset_poly_clear<uint8_t>(center, b.knots, b.coeff, eta));
for (std::size_t m = 0; m < plan.lanes; ++m)
EXPECT_TRUE(dpf::verify(tok0[m], tok1[m])) << m;
tok0[0][0] = simde_mm_xor_si128(tok0[0][0], simde_mm_set1_epi8(1));
EXPECT_FALSE(dpf::verify(tok0[0], tok1[0]));
EXPECT_TRUE(dpf::verify(tok0[1], tok1[1]));
}
TEST(LutUnion, ScheduleOnAnExistingSeedStaysOneWalk)
{
const auto few = grotto::make_lut_union_plan({lut_a()}, uint8_t{1});
const auto many = grotto::make_lut_union_plan({lut_a(), lut_b(), lut_c()}, uint8_t{1});
EXPECT_EQ(few.lanes, 2u);
EXPECT_EQ(grotto::lut_union_slot_bytes(1), 16u);
EXPECT_EQ(grotto::lut_union_slot_bytes(few.lanes), 16u);
EXPECT_EQ(grotto::lut_union_slot_bytes(many.lanes), 24u);
dpf::protocol::composer composer(0);
auto seed = composer.input(dpf::protocol::domain::fss, 16);
grotto::schedule_lut_union(composer, seed, many);
const auto scheduled = composer.default_plan();
ASSERT_EQ(scheduled.rounds(), many.depth);
for (std::uint16_t round = 0; round < scheduled.rounds(); ++round)
EXPECT_EQ(scheduled.slot_bytes(round), grotto::lut_union_slot_bytes(many.lanes));
}
TEST(LutUnion, SignedDomainAndRealTables)
{
const auto relu = grotto::piecewise_from_easy(grotto::make_relu_lut<int8_t>());
const auto abs_lut = grotto::piecewise_from_easy(grotto::make_abs_lut<int8_t>());
const auto square = grotto::piecewise_from_easy(grotto::make_squared_relu_lut<int8_t>(0));
for (const auto & row : relu.coeff)
EXPECT_EQ(row.size(), 2u);
for (const auto & row : abs_lut.coeff)
EXPECT_EQ(row.size(), 2u);
for (const auto & row : square.coeff)
EXPECT_EQ(row.size(), 3u);
const int8_t center = -20;
const auto probe = grotto::make_lut_union_plan(
std::vector<grotto::piecewise_lut<int8_t>>{relu, abs_lut, square}, int8_t{0});
EXPECT_EQ(probe.degree, 2u);
EXPECT_EQ(probe.depth, 8u);
const auto mat = grotto::make_offset_poly_keys<int8_t>(center, probe.degree);
const auto relu_f = grotto::make_relu_lut<int8_t>();
const auto abs_f = grotto::make_abs_lut<int8_t>();
const auto sq_f = grotto::make_squared_relu_lut<int8_t>(0);
for (int eta = -128; eta < 128; eta += 7)
{
const auto e = static_cast<int8_t>(eta);
const auto plan = grotto::make_lut_union_plan(
std::vector<grotto::piecewise_lut<int8_t>>{relu, abs_lut, square}, e);
expect_partition(plan);
const auto s0 = grotto::lut_union_eval<0>(mat, plan);
const auto s1 = grotto::lut_union_eval<1>(mat, plan);
const int8_t wrapped = offset_horner_group_add(center, e);
EXPECT_EQ(s0[0] + s1[0], static_cast<uint64_t>(relu_f(wrapped))) << eta;
EXPECT_EQ(s0[1] + s1[1], static_cast<uint64_t>(abs_f(wrapped))) << eta;
EXPECT_EQ(s0[2] + s1[2], static_cast<uint64_t>(sq_f(wrapped))) << eta;
}
const int8_t share = 3;
const int8_t other = static_cast<int8_t>(center ^ share);
const auto plan = grotto::make_lut_union_plan(
std::vector<grotto::piecewise_lut<int8_t>>{relu, abs_lut, square}, int8_t{-3});
const auto got = grotto::geneval_lut_union(share, other, plan);
const int8_t wrapped = offset_horner_group_add(center, int8_t{-3});
EXPECT_EQ(got.value0[0] + got.value1[0], static_cast<uint64_t>(relu_f(wrapped)));
EXPECT_EQ(got.value0[1] + got.value1[1], static_cast<uint64_t>(abs_f(wrapped)));
EXPECT_EQ(got.value0[2] + got.value1[2], static_cast<uint64_t>(sq_f(wrapped)));
}
TEST(LutUnion, SixteenBitDomainKeepsOneComparison)
{
grotto::piecewise_lut<uint16_t> left;
left.knots = {0, 1000};
left.coeff = {{3, 1}, {8, 0}};
grotto::piecewise_lut<uint16_t> right;
right.knots = {0, 400, 2000};
right.coeff = {{1, 0, 2}, {9, 1, 0}, {4, 0, 1}};
const uint16_t center = 50;
const uint16_t eta = 40000;
const auto plan = grotto::make_lut_union_plan({left, right}, eta);
EXPECT_EQ(plan.depth, 16u);
EXPECT_EQ(plan.geneval_rounds(), 16u);
EXPECT_EQ(plan.comparisons, 1u);
expect_partition(plan);
const auto mat = grotto::make_offset_poly_keys<uint16_t>(center, plan.degree);
const auto s0 = grotto::lut_union_eval<0>(mat, plan);
const auto s1 = grotto::lut_union_eval<1>(mat, plan);
EXPECT_EQ(s0[0] + s1[0], grotto::offset_poly_clear<uint16_t>(center, left.knots, left.coeff, eta));
EXPECT_EQ(s0[1] + s1[1], grotto::offset_poly_clear<uint16_t>(center, right.knots, right.coeff, eta));
dpf::protocol::composer composer(0);
grotto::schedule_lut_union(composer, plan);
EXPECT_EQ(composer.default_plan().rounds(), 16u);
}

View file

@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <tuple>
#include <array>
#include <cstdint>
@ -36,7 +37,7 @@ TEST(Multipoint, PlainPointsAndZeros)
EXPECT_FALSE(decltype(k0)::is_verifiable);
EXPECT_TRUE(decltype(k0)::is_multipoint);
EXPECT_EQ(k0.bucket_count, k1.bucket_count);
EXPECT_EQ(k0.bucket_domain, k1.bucket_domain);
EXPECT_TRUE(k0.bucket_domain == k1.bucket_domain);
EXPECT_GT(k0.bucket_count, alphas.size());
expect_points(k0, k1, alphas, betas);
@ -93,8 +94,14 @@ TEST(Multipoint, TamperedBucketRejects)
const std::vector<std::uint64_t> betas{1, 1, 1, 1};
auto [k0, k1] = dpf::make_multipoint(alphas, betas, dpf::verifiable{});
auto & cs = const_cast<dpf::cs_block &>(k0.buckets[0].correction_seeds()[0]);
cs[0] = simde_mm_xor_si128(cs[0], simde_mm_set1_epi8(1));
// A single level-0 seed is invisible when that party's root control bit is 0.
ASSERT_FALSE(k0.buckets.empty());
for (auto & bucket : k0.buckets)
{
using arr = typename std::decay_t<decltype(bucket)>::correction_seeds_array;
for (auto & cs : const_cast<arr &>(bucket.correction_seeds()))
cs[0] = simde_mm_xor_si128(cs[0], simde_mm_set1_epi8(1));
}
dpf::proof_token a0{}, a1{};
dpf::audit_multipoint(k0, dpf::prove(a0));
@ -152,29 +159,92 @@ TEST(Multipoint, RejectsAnEmptyListAndALengthMismatch)
{
const std::vector<std::uint8_t> alphas{1, 2};
const std::vector<std::uint64_t> betas{1};
EXPECT_THROW(
{
EXPECT_THROW({
auto keys = dpf::make_multipoint(alphas, betas);
(void)keys;
},
std::invalid_argument);
EXPECT_THROW(
{
}, std::invalid_argument);
EXPECT_THROW({
auto keys = dpf::make_multipoint(
std::vector<std::uint8_t>{}, std::vector<std::uint64_t>{});
(void)keys;
},
std::invalid_argument);
}, std::invalid_argument);
}
TEST(Multipoint, FullDomainProofsRejectABucketWordFlip)
{
using Input = std::uint8_t;
const std::vector<Input> alphas{1, 9, 40};
const std::vector<std::uint64_t> betas{7, 11, 3};
auto [k0, k1] = dpf::make_multipoint(alphas, betas, dpf::verifiable{});
int mismatches = 0;
for (int x = 0; x < 256; ++x)
{
const Input q = static_cast<Input>(x);
dpf::proof_token a{}, b{};
const auto y = dpf::reconstruct(
dpf::eval_multipoint(k0, q, dpf::prove(a)),
dpf::eval_multipoint(k1, q, dpf::prove(b)));
std::uint64_t want = 0;
for (std::size_t i = 0; i < alphas.size(); ++i)
if (alphas[i] == q)
want = betas[i];
EXPECT_EQ(y, want) << int(q);
EXPECT_TRUE(dpf::verify(a, b)) << int(q);
}
for (auto & word : const_cast<typename std::decay_t<decltype(k0.buckets[0])>::correction_words_array &>(
k0.buckets[0].correction_words()))
word = simde_mm_xor_si128(word, simde_mm_set1_epi8(0x3c));
for (auto & cs : const_cast<typename std::decay_t<decltype(k0.buckets[0])>::correction_seeds_array &>(
k0.buckets[0].correction_seeds()))
cs[0] = simde_mm_xor_si128(cs[0], simde_mm_set1_epi8(1));
for (int x = 0; x < 256; ++x)
{
const Input q = static_cast<Input>(x);
const auto y = dpf::reconstruct(dpf::eval_multipoint(k0, q),
dpf::eval_multipoint(k1, q));
std::uint64_t want = 0;
for (std::size_t i = 0; i < alphas.size(); ++i)
if (alphas[i] == q)
want = betas[i];
if (y != want)
++mismatches;
}
EXPECT_GT(mismatches, 0);
dpf::proof_token a0{}, a1{};
dpf::audit_multipoint(k0, dpf::prove(a0));
dpf::audit_multipoint(k1, dpf::prove(a1));
EXPECT_FALSE(dpf::verify(a0, a1));
}
TEST(Multipoint, WideDomain)
{
using Input = std::uint64_t;
const std::vector<Input> alphas{
0x100000001ull,
0x8000000000000001ull,
42ull,
0xfffffffffffffffeull};
const std::vector<std::uint64_t> betas{9, 8, 7, 6};
auto [k0, k1] = dpf::make_multipoint(alphas, betas);
expect_points(k0, k1, alphas, betas);
EXPECT_EQ(dpf::reconstruct(dpf::eval_multipoint(k0, Input{1}),
dpf::eval_multipoint(k1, Input{1})),
0u);
auto [v0, v1] = dpf::make_multipoint(alphas, betas, dpf::verifiable{});
dpf::proof_token p0{}, p1{};
const auto y0 = dpf::eval_multipoint(v0, alphas[1], dpf::prove(p0));
const auto y1 = dpf::eval_multipoint(v1, alphas[1], dpf::prove(p1));
EXPECT_EQ(dpf::reconstruct(y0, y1), betas[1]);
EXPECT_TRUE(dpf::verify(p0, p1));
}
TEST(Multipoint, RejectsDuplicates)
{
const std::vector<std::uint8_t> alphas{1, 2, 1, 4};
const std::vector<std::uint64_t> betas{1, 2, 3, 4};
EXPECT_THROW(
{
EXPECT_THROW({
auto keys = dpf::make_multipoint(alphas, betas);
(void)keys;
},
std::invalid_argument);
}, std::invalid_argument);
}

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <tuple>
#include "grotto/nmod.hpp"
@ -164,8 +165,7 @@ TEST(Nmod, RejectsAZeroReciprocalAndAHugeQuotient)
EXPECT_THROW(grotto::nmod(1, 0, 0, 0, 4), std::invalid_argument);
EXPECT_THROW(grotto::nmod(1, 0, 1, 0, 64), std::invalid_argument);
EXPECT_THROW(grotto::nmod_pow2(1, 0, 128, 4), std::overflow_error);
EXPECT_THROW(grotto::nmod(INT64_MAX, 0, u128{1} << 80, 0, 4),
std::overflow_error);
EXPECT_THROW(grotto::nmod(INT64_MAX, 0, u128{1} << 80, 0, 4), std::overflow_error);
}
TEST(Nmod, ScalePastTheProductWindow)

View file

@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <tuple>
#include "dpf.hpp"
#include "grotto/offset_horner.hpp"
@ -445,23 +446,17 @@ TEST(OffsetHorner, WrapSharesHideThePayload)
}
}
TEST(OffsetHorner, PowerKeysAreIndependentComparisons)
TEST(OffsetHorner, PowersShareOneSeedSpine)
{
constexpr std::size_t D = 3;
const uint16_t center = 1000;
auto mat = grotto::make_offset_horner_keys<uint16_t, D>(center);
const auto & k0 = std::get<0>(mat.keys[0]);
const auto & k1 = std::get<0>(mat.keys[1]);
const auto & k0 = std::get<0>(mat.keys);
const auto & k1 = std::get<1>(mat.keys);
EXPECT_NE(std::memcmp(&k0.root(), &k1.root(), sizeof(k0.root())), 0);
bool cw_differs = false;
const std::size_t depth = std::remove_reference_t<decltype(k0)>::depth;
for (std::size_t level = 0; level < depth; ++level)
{
if (k0.value_cw(level) != k1.value_cw(level))
cw_differs = true;
}
EXPECT_TRUE(cw_differs);
EXPECT_EQ(mat.keys.size(), D + 1);
EXPECT_GT(depth, 0u);
EXPECT_EQ(k0.value_cw(0), k1.value_cw(0));
}
TEST(OffsetHorner, DegreeZeroMatchesSignRespectingDot)
@ -744,7 +739,6 @@ TEST(OffsetHorner, GenevalXorSharesMatchTheDealerPoint)
EXPECT_EQ(grotto::geneval_offset_horner_center(center, uint8_t{0}), center);
const auto got = grotto::geneval_offset_horner<D>(share, other, eta, knots, coeff);
EXPECT_EQ(got.center, center);
EXPECT_EQ(got.eta, eta);
EXPECT_EQ(got.value0 + got.value1, gold<D>(center, eta, knots, coeff));
EXPECT_EQ(got.value0 + got.value1, 30u);
@ -769,7 +763,6 @@ TEST(OffsetHorner, GenevalFromAdditiveSharesOfXAndR)
const uint8_t eta = offset_horner_group_sub(x, r);
const uint8_t center = offset_horner_group_add(r, r);
EXPECT_EQ(got.eta, eta);
EXPECT_EQ(got.center, center);
EXPECT_EQ(got.value0 + got.value1, gold<D>(center, eta, knots, coeff));
EXPECT_EQ(got.value0 + got.value1, 88u);
const uint8_t wrapped = offset_horner_group_add(center, eta);
@ -796,7 +789,6 @@ TEST(OffsetHorner, GenevalSignedSharesUseGenevalConvention)
const int8_t eta = -3;
EXPECT_EQ(grotto::geneval_offset_horner_center(share, other), center);
const auto got = grotto::geneval_offset_horner<D>(share, other, eta, knots, coeff);
EXPECT_EQ(got.center, center);
EXPECT_EQ(got.value0 + got.value1, gold<D>(center, eta, knots, coeff));
const int8_t x = 40;
@ -809,7 +801,6 @@ TEST(OffsetHorner, GenevalSignedSharesUseGenevalConvention)
knots, coeff);
const int8_t expect_center = offset_horner_group_add(r, r);
const int8_t expect_eta = offset_horner_group_sub(x, r);
EXPECT_EQ(from_mask.center, expect_center);
EXPECT_EQ(from_mask.eta, expect_eta);
EXPECT_EQ(from_mask.value0 + from_mask.value1,
gold<D>(expect_center, expect_eta, knots, coeff));
@ -891,7 +882,6 @@ void expect_geneval(T center, T eta, const std::vector<T> & knots,
const T other = static_cast<T>(center ^ share);
const auto g = grotto::geneval_offset_horner<Degree>(share, other, eta, knots, coeff);
const uint64_t want = gold<Degree>(center, eta, knots, coeff);
EXPECT_EQ(g.center, center) << where;
EXPECT_EQ(g.value0 + g.value1, want) << where;
uint64_t summed = 0;
for (std::size_t k = 0; k <= Degree; ++k)
@ -1244,14 +1234,17 @@ template <typename T>
bool addition_leaves_domain(T center, T eta)
{
constexpr unsigned bits = dpf::utils::bitlength_of_v<T>;
if (bits > 62)
if constexpr (bits > 62)
return false;
const int64_t sum = math_of(center) + math_of(eta);
const int64_t mod = int64_t{1} << bits;
if constexpr (std::is_signed_v<T>)
return sum >= (mod >> 1) || sum < -(mod >> 1);
else
return sum >= mod;
{
const int64_t sum = math_of(center) + math_of(eta);
const int64_t mod = int64_t{1} << bits;
if constexpr (std::is_signed_v<T>)
return sum >= (mod >> 1) || sum < -(mod >> 1);
else
return sum >= mod;
}
}
template <typename T>
@ -1399,10 +1392,10 @@ void exercise_big_domain()
const T r1 = offset_horner_group_sub(r, r0);
const auto got = grotto::geneval_offset_horner<D>(x0, x1, r0, r1, knots, coeff);
const T sum = offset_horner_group_add(x, r);
EXPECT_EQ(got.center, offset_horner_group_add(r, r));
const T expect_center = offset_horner_group_add(r, r);
EXPECT_EQ(got.eta, offset_horner_group_sub(x, r));
EXPECT_EQ(offset_horner_group_add(got.center, got.eta), sum);
EXPECT_EQ(got.value0 + got.value1, gold<D>(got.center, got.eta, knots, coeff));
EXPECT_EQ(offset_horner_group_add(expect_center, got.eta), sum);
EXPECT_EQ(got.value0 + got.value1, gold<D>(expect_center, got.eta, knots, coeff));
if (::testing::Test::HasFailure())
return;
}

View file

@ -0,0 +1,192 @@
#include <gtest/gtest.h>
#include <tuple>
#include "grotto/offset_jet.hpp"
#include "dpf/verifiable.hpp"
#include <cstdint>
#include <stdexcept>
#include <vector>
namespace
{
uint64_t binom_ref(std::int64_t n, unsigned k)
{
if (k == 0)
return 1;
if (n >= 0)
{
unsigned __int128 acc = 1;
for (unsigned i = 1; i <= k; ++i)
acc = acc * static_cast<unsigned __int128>(n - static_cast<std::int64_t>(k - i)) / i;
return static_cast<uint64_t>(acc);
}
const uint64_t mag = binom_ref(-n + static_cast<std::int64_t>(k) - 1, k);
return (k & 1u) ? static_cast<uint64_t>(0) - mag : mag;
}
} // namespace
TEST(OffsetJet, BinomMatchesReference)
{
for (uint64_t n = 0; n < 40; ++n)
for (unsigned k = 0; k <= 16; ++k)
EXPECT_EQ(grotto::offset_jet_binom(n, k), binom_ref(static_cast<std::int64_t>(n), k))
<< "n=" << n << " k=" << k;
for (std::int64_t n = -20; n < 0; ++n)
for (unsigned k = 0; k <= 10; ++k)
EXPECT_EQ(grotto::offset_jet_binom(n, k), binom_ref(n, k))
<< "n=" << n << " k=" << k;
// Chu–Vandermonde sanity for a large center and a public kappa.
const uint64_t c = (uint64_t{1} << 40) + 17;
const std::int64_t kappa = -3;
for (unsigned k = 0; k <= 8; ++k)
{
uint64_t lhs = grotto::offset_jet_binom(
static_cast<std::int64_t>(c) + kappa, k);
uint64_t rhs = 0;
for (unsigned j = 0; j <= k; ++j)
rhs += grotto::offset_jet_binom(c, j)
* grotto::offset_jet_binom(kappa, k - j);
EXPECT_EQ(lhs, rhs) << "k=" << k;
}
}
TEST(OffsetJet, PublicCoefficientsMatchTheWrappedPoint)
{
const std::size_t degree = 3;
const uint8_t center = 9;
const auto mat = grotto::make_offset_jet_keys<uint8_t>(center, degree);
// f(x) = 2 + 3 C(x,1) + C(x,3)
const std::vector<uint64_t> coeff{2, 3, 0, 1};
const std::vector<uint8_t> knots{0};
for (int eta = 0; eta < 256; eta += 19)
{
const auto e = static_cast<uint8_t>(eta);
const uint64_t s0 = grotto::offset_jet_eval<0>(mat, knots, coeff, e);
const uint64_t s1 = grotto::offset_jet_eval<1>(mat, knots, coeff, e);
const uint64_t clear = grotto::offset_jet_clear<uint8_t>(center, knots, coeff, e);
EXPECT_EQ(s0 + s1, clear);
const uint8_t wrapped = static_cast<uint8_t>(center + e);
uint64_t expect = 0;
for (std::size_t k = 0; k <= degree; ++k)
expect += coeff[k] * grotto::offset_jet_binom(static_cast<uint64_t>(wrapped),
static_cast<unsigned>(k));
EXPECT_EQ(clear, expect);
}
}
TEST(OffsetJet, CarrySplitStillEvaluates)
{
const uint8_t center = 200;
const uint8_t eta = 100;
const auto mat = grotto::make_offset_jet_keys<uint8_t>(center, 1);
const std::vector<uint64_t> coeff{5, 1};
const std::vector<uint8_t> knots{0};
const uint64_t got = grotto::offset_jet_eval<0>(mat, knots, coeff, eta)
+ grotto::offset_jet_eval<1>(mat, knots, coeff, eta);
// wrapped = 200+100 = 44; f = 5 + C(44,1) = 5+44
EXPECT_EQ(got, uint64_t{5} + 44);
}
TEST(OffsetJet, DifferenceAndPrefixFromOneJet)
{
const std::size_t degree = 3;
const uint8_t center = 12;
const uint8_t eta = 3;
// Need degree+1 for prefix of a degree-2 polynomial; use degree 3 key.
const auto mat = grotto::make_offset_jet_keys<uint8_t>(center, degree);
const std::vector<uint64_t> poly{4, 2, 1}; // 4 + 2 C(x,1) + C(x,2)
std::vector<uint64_t> coeff = poly;
coeff.push_back(0); // pad to degree 3
const std::vector<uint8_t> knots{0};
const auto j0 = grotto::offset_jet_shares<0>(mat, knots, eta);
const auto j1 = grotto::offset_jet_shares<1>(mat, knots, eta);
std::vector<uint64_t> jet(degree + 1);
for (std::size_t k = 0; k <= degree; ++k)
jet[k] = j0[k] + j1[k];
const uint8_t x = static_cast<uint8_t>(center + eta);
const uint64_t value = grotto::offset_jet_dot(coeff, jet);
uint64_t expect_v = 0;
for (std::size_t k = 0; k < poly.size(); ++k)
expect_v += poly[k] * grotto::offset_jet_binom(static_cast<uint64_t>(x),
static_cast<unsigned>(k));
EXPECT_EQ(value, expect_v);
const auto dcoeff = grotto::offset_jet_difference_coeff(coeff);
const uint64_t diff = grotto::offset_jet_dot(dcoeff, jet);
const uint64_t fx1 = expect_v
- poly[0] * 0 // recompute f(x+1) - f(x)
+ 0;
uint64_t expect_fx1 = 0;
for (std::size_t k = 0; k < poly.size(); ++k)
expect_fx1 += poly[k] * grotto::offset_jet_binom(
static_cast<uint64_t>(static_cast<uint8_t>(x + 1)),
static_cast<unsigned>(k));
EXPECT_EQ(diff, static_cast<uint64_t>(expect_fx1 - expect_v));
(void)fx1;
// Prefix needs degree+1: key degree 3, poly degree <= 2.
const auto pcoeff = grotto::offset_jet_prefix_coeff(poly);
ASSERT_EQ(pcoeff.size(), degree + 1u);
const uint64_t prefix = grotto::offset_jet_dot(pcoeff, jet);
uint64_t expect_p = 0;
for (uint8_t i = 0; i < x; ++i)
{
for (std::size_t k = 0; k < poly.size(); ++k)
expect_p += poly[k] * grotto::offset_jet_binom(static_cast<uint64_t>(i),
static_cast<unsigned>(k));
}
EXPECT_EQ(prefix, expect_p);
}
TEST(OffsetJet, VerifiableProofs)
{
const std::size_t degree = 2;
const auto mat = grotto::make_offset_jet_keys<uint8_t>(2, degree, dpf::verifiable{});
const std::vector<uint64_t> coeff{1, 0, 3};
const std::vector<uint8_t> knots{0};
const uint8_t eta = 5;
std::vector<dpf::proof_token> a(degree + 1), b(degree + 1);
const uint64_t s0 = grotto::offset_jet_eval<0>(mat, knots, coeff, eta, a.data());
const uint64_t s1 = grotto::offset_jet_eval<1>(mat, knots, coeff, eta, b.data());
EXPECT_EQ(s0 + s1, grotto::offset_jet_clear<uint8_t>(2, knots, coeff, eta));
for (std::size_t m = 0; m <= degree; ++m)
EXPECT_TRUE(dpf::verify(a[m], b[m]));
}
TEST(OffsetJet, RejectsOversizeDegree)
{
EXPECT_THROW(grotto::make_offset_jet_keys<uint8_t>(1, grotto::offset_jet_max_degree + 1), std::invalid_argument);
}
TEST(OffsetJet, NegativeCenterDegreeTwoPlus)
{
const std::size_t degree = 3;
const int8_t center = -7;
const auto mat = grotto::make_offset_jet_keys<int8_t>(center, degree);
// f(x) = 1 + C(x,1) + 3 C(x,2) + C(x,3)
const std::vector<uint64_t> coeff{1, 1, 3, 1};
const std::vector<int8_t> knots{static_cast<int8_t>(-128)};
for (int eta = -20; eta <= 20; eta += 5)
{
const auto e = static_cast<int8_t>(eta);
const uint64_t s0 = grotto::offset_jet_eval<0>(mat, knots, coeff, e);
const uint64_t s1 = grotto::offset_jet_eval<1>(mat, knots, coeff, e);
const uint64_t clear = grotto::offset_jet_clear<int8_t>(center, knots, coeff, e);
EXPECT_EQ(s0 + s1, clear) << "eta=" << eta;
const int8_t wrapped = grotto::offset_horner_group_add(center, e);
uint64_t expect = 0;
for (std::size_t k = 0; k <= degree; ++k)
expect += coeff[k] * grotto::offset_jet_binom(
static_cast<std::int64_t>(wrapped), static_cast<unsigned>(k));
EXPECT_EQ(clear, expect) << "eta=" << eta;
}
}

View file

@ -0,0 +1,158 @@
#include <gtest/gtest.h>
#include <tuple>
#include "grotto/offset_poly.hpp"
#include "dpf/verifiable.hpp"
#include <cstdint>
#include <stdexcept>
#include <vector>
namespace
{
uint64_t wrapped_pow(uint8_t center, uint8_t eta, std::size_t degree)
{
const uint64_t point = static_cast<uint64_t>(static_cast<uint8_t>(center + eta));
uint64_t acc = 0;
uint64_t pow = 1;
std::vector<uint64_t> coeff(degree + 1, 0);
coeff[0] = 3;
coeff[degree] = 1;
for (uint64_t c : coeff)
{
acc += c * pow;
pow *= point;
}
return acc;
}
} // namespace
TEST(OffsetPoly, PublicCoefficientsMatchTheWrappedPolynomial)
{
const std::size_t degree = 4;
const auto mat = grotto::make_offset_poly_keys<uint8_t>(2, degree);
std::vector<uint64_t> coeff(degree + 1, 0);
coeff[0] = 3;
coeff[degree] = 1;
const std::vector<uint8_t> knots{0};
for (int eta = 0; eta < 256; eta += 17)
{
const auto e = static_cast<uint8_t>(eta);
const uint64_t s0 = grotto::offset_poly_eval<0>(mat, knots, {coeff}, e);
const uint64_t s1 = grotto::offset_poly_eval<1>(mat, knots, {coeff}, e);
const uint64_t clear = grotto::offset_poly_clear<uint8_t>(2, knots, {coeff}, e);
EXPECT_EQ(s0 + s1, clear);
EXPECT_EQ(clear, wrapped_pow(2, e, degree));
}
}
TEST(OffsetPoly, CarrySplitStillEvaluatesTheWrappedPoint)
{
const auto mat = grotto::make_offset_poly_keys<uint8_t>(200, 1);
const std::vector<uint64_t> coeff{5, 1};
const std::vector<uint8_t> knots{0};
const uint8_t eta = 100;
const uint64_t got = grotto::offset_poly_eval<0>(mat, knots, {coeff}, eta)
+ grotto::offset_poly_eval<1>(mat, knots, {coeff}, eta);
EXPECT_EQ(got, uint64_t{5} + 44);
}
TEST(OffsetPoly, SharedCoefficientsUseOneBeaverDot)
{
const std::size_t degree = 2;
const uint8_t center = 9;
const uint8_t eta = 4;
const auto mat = grotto::make_offset_poly_keys<uint8_t>(center, degree);
const std::vector<uint64_t> coeff{4, 0, 2};
const std::vector<uint8_t> knots{0};
const std::vector<uint64_t> share0{1, 7, 9};
std::vector<uint64_t> share1(degree + 1);
for (std::size_t i = 0; i < coeff.size(); ++i)
share1[i] = coeff[i] - share0[i];
const auto kappas = grotto::offset_poly_kappas<uint8_t>(knots, degree, eta);
const auto p0 = grotto::offset_poly_power_shares<0>(mat, knots, eta);
const auto p1 = grotto::offset_poly_power_shares<1>(mat, knots, eta);
ASSERT_EQ(p0.size(), kappas.size());
std::vector<uint64_t> q0, q1, y0, y1;
for (std::size_t piece = 0; piece < kappas.size(); ++piece)
{
const auto a0 = grotto::offset_poly_shift_share(share0, kappas[piece]);
const auto a1 = grotto::offset_poly_shift_share(share1, kappas[piece]);
for (std::size_t m = 0; m <= degree; ++m)
{
q0.push_back(a0[m]);
q1.push_back(a1[m]);
y0.push_back(p0[piece][m]);
y1.push_back(p1[piece][m]);
}
}
auto triple = dpf::beavers::sample_dot<uint64_t>(q0.size());
std::vector<uint64_t> opened_d(q0.size()), opened_e(y0.size());
for (std::size_t i = 0; i < q0.size(); ++i)
{
// Open only masked differences q-a and power-b (F_Poly).
opened_d[i] = (q0[i] - triple.x[i].p0) + (q1[i] - triple.x[i].p1);
opened_e[i] = (y0[i] - triple.y[i].p0) + (y1[i] - triple.y[i].p1);
}
const uint64_t v0 = grotto::offset_poly_beaver_share(0, q0, y0, opened_d, opened_e, triple);
const uint64_t v1 = grotto::offset_poly_beaver_share(1, q1, y1, opened_d, opened_e, triple);
const uint64_t clear = grotto::offset_poly_clear<uint8_t>(center, knots, {coeff}, eta);
EXPECT_EQ(v0 + v1, clear);
const uint64_t point = static_cast<uint8_t>(center + eta);
EXPECT_EQ(clear, 4 + 2 * point * point);
}
TEST(OffsetPoly, VerifiableTokensRejectATamperedProof)
{
const std::size_t degree = 2;
const auto mat = grotto::make_offset_poly_keys<uint8_t>(2, degree, dpf::verifiable{});
const std::vector<uint64_t> coeff{1, 0, 3};
const std::vector<uint8_t> knots{0};
const uint8_t eta = 5;
std::vector<dpf::proof_token> a(degree + 1), b(degree + 1);
const uint64_t s0 = grotto::offset_poly_eval<0>(mat, knots, {coeff}, eta, a.data());
const uint64_t s1 = grotto::offset_poly_eval<1>(mat, knots, {coeff}, eta, b.data());
EXPECT_EQ(s0 + s1, grotto::offset_poly_clear<uint8_t>(2, knots, {coeff}, eta));
for (std::size_t m = 0; m <= degree; ++m)
EXPECT_TRUE(dpf::verify(a[m], b[m])) << m;
a[0][0] = simde_mm_xor_si128(a[0][0], simde_mm_set1_epi8(1));
EXPECT_FALSE(dpf::verify(a[0], b[0]));
EXPECT_TRUE(dpf::verify(a[1], b[1]));
}
TEST(OffsetPoly, HornerAndPolyAgreeOnTheSameKnots)
{
constexpr std::size_t D = 3;
const uint8_t center = 17;
const uint8_t eta = 9;
const std::vector<uint8_t> knots{0, 40, 100};
std::vector<std::array<uint64_t, D + 1>> horner_coeff{
{2, 0, 1, 0},
{0, 3, 0, 1},
{5, 1, 0, 0},
};
std::vector<std::vector<uint64_t>> poly_coeff(horner_coeff.size());
for (std::size_t i = 0; i < horner_coeff.size(); ++i)
poly_coeff[i].assign(horner_coeff[i].begin(), horner_coeff[i].end());
auto hmat = grotto::make_offset_horner_keys<uint8_t, D>(center);
auto pmat = grotto::make_offset_poly_keys<uint8_t>(center, D);
const uint64_t h = grotto::offset_horner_eval<0, D>(hmat, knots, horner_coeff, eta)
+ grotto::offset_horner_eval<1, D>(hmat, knots, horner_coeff, eta);
const uint64_t p = grotto::offset_poly_eval<0>(pmat, knots, poly_coeff, eta)
+ grotto::offset_poly_eval<1>(pmat, knots, poly_coeff, eta);
EXPECT_EQ(h, p);
EXPECT_EQ(h, grotto::offset_horner_clear<D>(center, knots, horner_coeff, eta));
EXPECT_EQ(p, grotto::offset_poly_clear(center, knots, poly_coeff, eta));
}
TEST(OffsetPoly, RejectsADegreePastTheCap)
{
EXPECT_THROW(grotto::make_offset_poly_keys<uint8_t>(1, grotto::offset_poly_max_degree + 1), std::invalid_argument);
EXPECT_THROW(grotto::make_offset_poly_keys<uint8_t>(1, grotto::offset_poly_max_degree + 1, dpf::verifiable{}), std::invalid_argument);
}

View file

@ -0,0 +1,172 @@
#include <gtest/gtest.h>
#include <tuple>
#include "grotto/offset_repr.hpp"
#include "dpf/verifiable.hpp"
#include <cstdint>
#include <stdexcept>
#include <vector>
namespace
{
uint64_t fib_ref(uint64_t n)
{
if (n == 0)
return 0;
uint64_t a = 0, b = 1;
for (uint64_t i = 1; i < n; ++i)
{
const uint64_t c = a + b;
a = b;
b = c;
}
return b;
}
std::vector<uint64_t> matvec2(
const std::vector<std::vector<uint64_t>> & M,
const std::vector<uint64_t> & v)
{
return {M[0][0] * v[0] + M[0][1] * v[1], M[1][0] * v[0] + M[1][1] * v[1]};
}
} // namespace
TEST(OffsetRepr, FibonacciStateMatchesReference)
{
for (uint64_t n = 0; n < 90; ++n)
{
const auto S = grotto::offset_repr_fibonacci_state(n);
ASSERT_EQ(S.size(), 2u);
EXPECT_EQ(S[1], fib_ref(n)) << "F_" << n;
EXPECT_EQ(S[0], fib_ref(n + 1)) << "F_" << (n + 1);
}
}
TEST(OffsetRepr, MatrixPowAdvancesFibonacci)
{
const auto M = grotto::offset_repr_fibonacci_matrix();
const auto S0 = grotto::offset_repr_fibonacci_state(0);
for (std::int64_t k = 0; k < 40; ++k)
{
const auto Mk = grotto::offset_repr_matrix_pow(M, k);
const auto advanced = matvec2(Mk, S0);
const auto expect = grotto::offset_repr_fibonacci_state(static_cast<uint64_t>(k));
EXPECT_EQ(advanced, expect) << "k=" << k;
}
// Negative: M^{-k} S_k = S_0.
for (std::int64_t k = 1; k < 20; ++k)
{
const auto Sk = grotto::offset_repr_fibonacci_state(static_cast<uint64_t>(k));
const auto Minv = grotto::offset_repr_matrix_pow(M, -k);
const auto back = matvec2(Minv, Sk);
EXPECT_EQ(back, S0) << "back from k=" << k;
}
}
TEST(OffsetRepr, GeometricIsScalarPow)
{
const uint64_t lambda = 3;
const auto M = grotto::offset_repr_geometric_matrix(lambda);
uint64_t expect = 1;
for (std::int64_t e = 0; e < 20; ++e)
{
const auto P = grotto::offset_repr_matrix_pow(M, e);
EXPECT_EQ(P[0][0], expect);
expect *= lambda;
}
const auto Minv = grotto::offset_repr_matrix_pow(M, -1);
EXPECT_EQ(Minv[0][0] * lambda, uint64_t{1});
}
TEST(OffsetRepr, Crc32JumpMatchesNaive)
{
constexpr std::uint32_t poly = 0xEDB88320u;
auto step1 = [](std::uint32_t s) {
return (s >> 1) ^ (poly & (0u - (s & 1u)));
};
for (std::uint32_t seed : {0u, 1u, 0xFFFFFFFFu, 0x12345678u})
{
for (unsigned steps = 0; steps < 200; ++steps)
{
std::uint32_t naive = seed;
for (unsigned i = 0; i < steps; ++i)
naive = step1(naive);
EXPECT_EQ(grotto::offset_repr_crc32_jump(seed, steps), naive)
<< "seed=" << seed << " steps=" << steps;
}
}
// Doubling path for large jumps.
const std::uint32_t seed = 0xA5A5A5A5u;
std::uint32_t naive = seed;
for (unsigned i = 0; i < 1000; ++i)
naive = step1(naive);
EXPECT_EQ(grotto::offset_repr_crc32_jump(seed, 1000), naive);
}
TEST(OffsetRepr, KeyedFibonacciMatchesClear)
{
const uint8_t center = 10;
const auto state = grotto::offset_repr_fibonacci_state(center);
const auto M = grotto::offset_repr_fibonacci_matrix();
const auto mat = grotto::make_offset_repr_keys<uint8_t>(center, state);
const std::vector<uint8_t> knots{0};
for (int eta = 0; eta < 256; eta += 17)
{
const auto e = static_cast<uint8_t>(eta);
const auto s0 = grotto::offset_repr_eval<0>(mat, M, knots, e);
const auto s1 = grotto::offset_repr_eval<1>(mat, M, knots, e);
const auto clear = grotto::offset_repr_clear(center, state, M, knots, e);
ASSERT_EQ(s0.size(), 2u);
ASSERT_EQ(s1.size(), 2u);
ASSERT_EQ(clear.size(), 2u);
EXPECT_EQ(s0[0] + s1[0], clear[0]);
EXPECT_EQ(s0[1] + s1[1], clear[1]);
const uint8_t wrapped = static_cast<uint8_t>(center + e);
const auto expect = grotto::offset_repr_fibonacci_state(wrapped);
EXPECT_EQ(clear, expect) << "eta=" << eta;
}
}
TEST(OffsetRepr, CarrySplitStillAdvances)
{
const uint8_t center = 200;
const uint8_t eta = 100; // wraps: 44
const auto state = grotto::offset_repr_fibonacci_state(center);
const auto M = grotto::offset_repr_fibonacci_matrix();
const auto mat = grotto::make_offset_repr_keys<uint8_t>(center, state);
const std::vector<uint8_t> knots{0};
const auto got0 = grotto::offset_repr_eval<0>(mat, M, knots, eta);
const auto got1 = grotto::offset_repr_eval<1>(mat, M, knots, eta);
const auto expect = grotto::offset_repr_fibonacci_state(44);
EXPECT_EQ(got0[0] + got1[0], expect[0]);
EXPECT_EQ(got0[1] + got1[1], expect[1]);
}
TEST(OffsetRepr, VerifiableProofs)
{
const uint8_t center = 7;
const auto state = grotto::offset_repr_fibonacci_state(center);
const auto M = grotto::offset_repr_fibonacci_matrix();
const auto mat = grotto::make_offset_repr_keys<uint8_t>(center, state, dpf::verifiable{});
const std::vector<uint8_t> knots{0};
const uint8_t eta = 5;
std::vector<dpf::proof_token> a(2), b(2);
const auto s0 = grotto::offset_repr_eval<0>(mat, M, knots, eta, a.data());
const auto s1 = grotto::offset_repr_eval<1>(mat, M, knots, eta, b.data());
const auto clear = grotto::offset_repr_clear(center, state, M, knots, eta);
EXPECT_EQ(s0[0] + s1[0], clear[0]);
EXPECT_EQ(s0[1] + s1[1], clear[1]);
EXPECT_TRUE(dpf::verify(a[0], b[0]));
EXPECT_TRUE(dpf::verify(a[1], b[1]));
}
TEST(OffsetRepr, RejectsBadDim)
{
EXPECT_THROW(grotto::make_offset_repr_keys<uint8_t>(1, {}), std::invalid_argument);
EXPECT_THROW(grotto::make_offset_repr_keys<uint8_t>(1,
std::vector<uint64_t>(grotto::offset_repr_max_dim + 1, 0)), std::invalid_argument);
}

View file

@ -0,0 +1,153 @@
#include <gtest/gtest.h>
#include <tuple>
#include "grotto/offset_twist.hpp"
#include "dpf/verifiable.hpp"
#include <cstdint>
#include <stdexcept>
#include <vector>
namespace
{
uint64_t pow_u64(uint64_t base, uint64_t exp)
{
uint64_t acc = 1;
while (exp != 0)
{
if (exp & 1u)
acc *= base;
base *= base;
exp >>= 1;
}
return acc;
}
uint64_t twisted_poly(uint64_t x, uint64_t lambda, const std::vector<uint64_t> & coeff)
{
uint64_t acc = 0;
uint64_t pow = 1;
for (uint64_t c : coeff)
{
acc += c * pow;
pow *= x;
}
return acc * pow_u64(lambda, x);
}
uint64_t twisted_half(uint64_t x, const std::vector<uint64_t> & coeff)
{
uint64_t acc = 0;
uint64_t pow = 1;
for (uint64_t c : coeff)
{
acc += c * pow;
pow *= x;
}
if (x >= 64)
return 0;
return acc >> static_cast<unsigned>(x);
}
} // namespace
TEST(OffsetTwist, OddLambdaMatchesClear)
{
const uint8_t center = 9;
const uint64_t lambda = 3;
const std::size_t degree = 2;
const auto mat = grotto::make_offset_twist_keys<uint8_t>(center, degree, lambda);
// f(x) = (2 + 5x + x^2) * 3^x
const std::vector<uint64_t> coeff{2, 5, 1};
const std::vector<uint8_t> knots{0};
for (int eta = 0; eta < 256; eta += 19)
{
const auto e = static_cast<uint8_t>(eta);
const uint64_t s0 = grotto::offset_twist_eval<0>(mat, knots, coeff, e);
const uint64_t s1 = grotto::offset_twist_eval<1>(mat, knots, coeff, e);
const uint64_t clear = grotto::offset_twist_clear(
center, lambda, knots, coeff, e);
EXPECT_EQ(s0 + s1, clear) << "eta=" << eta;
const uint8_t wrapped = static_cast<uint8_t>(center + e);
EXPECT_EQ(clear, twisted_poly(wrapped, lambda, coeff)) << "eta=" << eta;
}
}
TEST(OffsetTwist, CarrySplitStillTwists)
{
const uint8_t center = 200;
const uint8_t eta = 100; // wraps to 44
const uint64_t lambda = 5;
const auto mat = grotto::make_offset_twist_keys<uint8_t>(center, 1, lambda);
const std::vector<uint64_t> coeff{1, 2}; // (1 + 2x) 5^x
const std::vector<uint8_t> knots{0};
const uint64_t got = grotto::offset_twist_eval<0>(mat, knots, coeff, eta)
+ grotto::offset_twist_eval<1>(mat, knots, coeff, eta);
EXPECT_EQ(got, twisted_poly(44, lambda, coeff));
}
TEST(OffsetTwist, HalfShiftsOnShares)
{
const uint8_t center = 4;
const std::size_t degree = 2;
const auto mat = grotto::make_offset_twist_keys<uint8_t>(center, degree, grotto::twist_half);
// Dyadic path returns shares of the shifted value; untwisted sum stays shared.
const std::vector<uint64_t> coeff{7, 3, 1};
const std::vector<uint8_t> knots{0};
for (int eta = 0; eta < 40; eta += 3)
{
const auto e = static_cast<uint8_t>(eta);
const uint64_t s0 = grotto::offset_twist_eval<0>(mat, knots, coeff, e);
const uint64_t s1 = grotto::offset_twist_eval<1>(mat, knots, coeff, e);
const uint8_t wrapped = static_cast<uint8_t>(center + e);
const uint64_t clear = grotto::offset_twist_clear(
center, grotto::twist_half, knots, coeff, e);
EXPECT_EQ(s0 + s1, clear) << "eta=" << eta;
EXPECT_EQ(clear, twisted_half(wrapped, coeff)) << "eta=" << eta;
}
}
TEST(OffsetTwist, ArithmeticoGeometricClosedForm)
{
const uint64_t lambda = 3;
for (uint64_t n = 1; n < 30; ++n)
{
uint64_t naive = 0;
for (uint64_t k = 1; k <= n; ++k)
naive += k * pow_u64(lambda, k);
EXPECT_EQ(grotto::offset_twist_arithmetico_geometric(n, lambda), naive)
<< "n=" << n;
}
}
TEST(OffsetTwist, VerifiableProofs)
{
const uint8_t center = 6;
const uint64_t lambda = 7;
const std::size_t degree = 2;
const auto mat = grotto::make_offset_twist_keys<uint8_t>(
center, degree, lambda, dpf::verifiable{});
const std::vector<uint64_t> coeff{1, 0, 4};
const std::vector<uint8_t> knots{0};
const uint8_t eta = 3;
std::vector<dpf::proof_token> a(degree + 1), b(degree + 1);
const uint64_t s0 = grotto::offset_twist_eval<0>(mat, knots, coeff, eta, a.data());
const uint64_t s1 = grotto::offset_twist_eval<1>(mat, knots, coeff, eta, b.data());
EXPECT_EQ(s0 + s1,
grotto::offset_twist_clear(center, lambda, knots, coeff, eta));
for (std::size_t m = 0; m <= degree; ++m)
EXPECT_TRUE(dpf::verify(a[m], b[m]));
}
TEST(OffsetTwist, RejectsEvenLambda)
{
EXPECT_THROW(grotto::make_offset_twist_keys<uint8_t>(1, 1, uint64_t{2}), std::invalid_argument);
}
TEST(OffsetTwist, RejectsOversizeDegree)
{
EXPECT_THROW(grotto::make_offset_twist_keys<uint8_t>(
1, grotto::offset_twist_max_degree + 1, uint64_t{3}), std::invalid_argument);
}

View file

@ -0,0 +1,316 @@
/// @file opt_in_malicious_test.cpp
/// @brief Opt-in verifiable / extractable / MAC checks stay off by default.
#include <gtest/gtest.h>
#include <tuple>
#include <array>
#include <cstdint>
#include <cstring>
#include <vector>
#include "dpf.hpp"
#include "dpf/beaver.hpp"
#include "grotto/carry.hpp"
namespace
{
using Input = std::uint8_t;
using u64 = std::uint64_t;
struct Counter
{
int draws = 0;
u64 operator()()
{
++draws;
return 0x9e3779b97f4a7c15ull * static_cast<u64>(draws);
}
};
} // namespace
TEST(OptInProfile, SemiHonestKeyHasNoProofOrSketch)
{
auto [k0, k1] = dpf::make_dpf_profile<dpf::semi_honest>(Input{0x2a}, u64{7});
EXPECT_FALSE(decltype(k0)::is_verifiable);
EXPECT_FALSE(decltype(k0)::is_extractable);
EXPECT_FALSE(decltype(k1)::is_verifiable);
EXPECT_FALSE(decltype(k1)::is_extractable);
auto plain = dpf::make_dpf(Input{0x2a}, u64{7});
EXPECT_FALSE(decltype(plain.first)::is_verifiable);
EXPECT_FALSE(decltype(plain.first)::is_extractable);
}
TEST(OptInProfile, CheckedKeyOptsIn)
{
auto [k0, k1] = dpf::make_dpf_profile<dpf::checked>(Input{0x2a}, dpf::fp61{7});
EXPECT_TRUE(decltype(k0)::is_verifiable);
EXPECT_TRUE(decltype(k0)::is_extractable);
}
TEST(OptInProfile, FlagsAreIndependentOfTheOutputMacBit)
{
using ver_only = dpf::auth_profile<true, false, false>;
using ext_only = dpf::auth_profile<false, true, false>;
using mac_only = dpf::auth_profile<false, false, true>;
static_assert(dpf::checked::output_mac);
static_assert(!dpf::semi_honest::output_mac);
static_assert(std::tuple_size_v<decltype(dpf::key_tags_tuple<dpf::checked>())> == 2);
static_assert(std::tuple_size_v<decltype(dpf::key_tags_tuple<mac_only>())> == 0);
static_assert(std::tuple_size_v<decltype(dpf::key_tags<ver_only>())> == 1);
auto ver = dpf::make_dpf_profile<ver_only>(Input{1}, u64{4});
EXPECT_TRUE(decltype(ver.first)::is_verifiable);
EXPECT_FALSE(decltype(ver.first)::is_extractable);
auto ext = dpf::make_dpf_profile<ext_only>(Input{1}, dpf::fp61{4});
EXPECT_FALSE(decltype(ext.first)::is_verifiable);
EXPECT_TRUE(decltype(ext.first)::is_extractable);
auto mac = dpf::make_dpf_profile<mac_only>(Input{1}, u64{4});
EXPECT_FALSE(decltype(mac.first)::is_verifiable);
EXPECT_FALSE(decltype(mac.first)::is_extractable);
}
TEST(OptInProfile, CheckedUint8DomainProofSketchAndSeedFlip)
{
const Input alpha = 0x2a;
const dpf::fp61 beta{9};
auto [k0, k1] = dpf::make_dpf_profile<dpf::checked>(alpha, beta);
std::array<dpf::fp61, 256> s0{}, s1{}, r{};
for (int x = 0; x < 256; ++x)
{
const Input q = static_cast<Input>(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));
s0[static_cast<std::size_t>(x)] = y0.raw();
s1[static_cast<std::size_t>(x)] = y1.raw();
EXPECT_TRUE(dpf::verify(a, b)) << x;
EXPECT_EQ(dpf::reconstruct(y0, y1), x == alpha ? beta : dpf::fp61{0});
r[static_cast<std::size_t>(x)] = dpf::fp61{static_cast<std::uint64_t>(3 + x)};
}
EXPECT_TRUE(dpf::sketch_verify(dpf::sketch_fold(s0, r), dpf::sketch_fold(s1, r)));
auto forged = s0;
forged[0] = forged[0] + beta;
EXPECT_FALSE(dpf::sketch_verify(dpf::sketch_fold(forged, r), dpf::sketch_fold(s1, r)));
auto bad = k0;
using arr = typename decltype(k0)::correction_seeds_array;
for (auto & cs : const_cast<arr &>(bad.correction_seeds()))
cs[0] = simde_mm_xor_si128(cs[0], simde_mm_set1_epi8(1));
int failed = 0;
for (int x = 0; x < 256; ++x)
{
dpf::proof_token a{}, b{};
(void)*dpf::eval_point(bad, static_cast<Input>(x), dpf::prove(a));
(void)*dpf::eval_point(k1, static_cast<Input>(x), dpf::prove(b));
if (!dpf::verify(a, b))
++failed;
}
EXPECT_GT(failed, 0);
}
TEST(OptInMac, DefaultSessionHasNoTagVectors)
{
dpf::beavers::session<u64> s;
auto x = s.input();
auto y = s.input();
auto z = s(x * y);
Counter rng;
s.sample(rng);
auto tape = s.export_party(0);
EXPECT_FALSE(tape.has_mac);
EXPECT_TRUE(tape.lambda_tag.empty());
EXPECT_TRUE(tape.dot_cross_tag.empty());
(void)z;
}
TEST(OptInMac, HonestHornerOpeningsVerify)
{
auto key = dpf::sample_mac_key<u64>();
dpf::beavers::session<u64> s;
s.set_mac_key(key);
auto x = s.input();
auto z = s.horner(x, {u64{1}, u64{2}, u64{3}});
Counter rng;
s.sample(rng);
s.bind(x, u64{5}, rng);
s.evaluate();
EXPECT_EQ(s.open(z), 1u + 2u * 5u + 3u * 25u);
EXPECT_TRUE(s.verify_delta(x));
EXPECT_TRUE(s.verify_all());
}
TEST(OptInMac, FlippedTagRejects)
{
auto key = dpf::sample_mac_key<u64>();
dpf::beavers::session<u64> s;
s.set_mac_key(key);
auto x = s.input();
auto y = s.input();
auto z = s(x * y);
Counter rng;
s.sample(rng);
s.bind(x, u64{3}, rng);
s.bind(y, u64{5}, rng);
s.evaluate();
EXPECT_EQ(s.open(z), 15u);
auto a = s.delta_auth(x);
dpf::beavers::auth_opening<u64> o0{a.value.p0, a.tag.p0};
dpf::beavers::auth_opening<u64> o1{a.value.p1, a.tag.p1};
EXPECT_TRUE(dpf::beavers::verify_auth_opening(o0, o1, key));
o0.tag ^= 1ull;
EXPECT_FALSE(dpf::beavers::verify_auth_opening(o0, o1, key));
}
TEST(OptInExtractable, NoteSketchNoOpOnPlainKey)
{
using key_t = decltype(dpf::make_dpf(Input{1}, u64{1}).first);
dpf::sketch_share sk{};
const std::array<u64, 1> ys{9};
const std::array<dpf::fp61, 1> rs{dpf::fp61{2}};
dpf::note_sketch<key_t>(sk, ys, rs);
EXPECT_EQ(sk.z1, dpf::fp61{0});
EXPECT_EQ(sk.z2, dpf::fp61{0});
EXPECT_EQ(sk.z3, dpf::fp61{0});
}
TEST(OptInExtractable, HonestSketchAcceptsTamperRejects)
{
const Input alpha = 0x11;
const dpf::fp61 beta{42};
auto [k0, k1] = dpf::make_dpf(alpha, beta, dpf::extractable{});
EXPECT_TRUE(decltype(k0)::is_extractable);
EXPECT_FALSE(decltype(k0)::is_verifiable);
std::array<Input, 4> pts{0x10, 0x11, 0x12, 0x13};
std::array<dpf::fp61, 4> r{
dpf::fp61{3}, dpf::fp61{5}, dpf::fp61{7}, dpf::fp61{11}};
std::array<dpf::fp61, 4> s0{}, s1{};
for (std::size_t i = 0; i < pts.size(); ++i)
{
s0[i] = (*dpf::eval_point(k0, pts[i])).raw();
s1[i] = (*dpf::eval_point(k1, pts[i])).raw();
}
dpf::sketch_share sk0{}, sk1{};
dpf::note_sketch<decltype(k0)>(sk0, s0, r);
dpf::note_sketch<decltype(k1)>(sk1, s1, r);
EXPECT_TRUE(dpf::sketch_verify(sk0, sk1));
s0[0] = s0[0] + beta;
dpf::note_sketch<decltype(k0)>(sk0, s0, r);
EXPECT_FALSE(dpf::sketch_verify(sk0, sk1));
}
TEST(OptInMemoProve, HonestIdempotentAndTamper)
{
const Input alpha = 0x2a;
auto [k0, k1] = dpf::make_dpf(alpha,
dpf::block_width<4>(dpf::lt(u64{1})), dpf::verifiable{});
using KT0 = decltype(k0);
using KT1 = decltype(k1);
const Input from = 0x20;
const Input to = 0x2f;
const auto nbits = static_cast<std::size_t>(k0.cmp().nbits);
using integral = typename KT0::integral_type;
const auto a = static_cast<integral>(from);
const auto b = static_cast<integral>(to);
const auto excl = dpf::detail::incr::cmp_exclusive_end(b);
const auto count = dpf::detail::incr::cmp_inclusive_count(a, b);
constexpr std::size_t stop =
KT0::cmp_depth == 0 ? KT0::depth : KT0::cmp_depth;
const std::size_t levels = KT0::cmp_h;
dpf::detail::incr::cmp_full_interval_memo<KT0, stop> memo0{count};
dpf::detail::incr::cmp_full_interval_memo<KT1, stop> memo1{count};
dpf::detail::incr::eval_cmp_interval_impl_interior(k0, a, excl, nbits, memo0,
levels, nullptr);
dpf::detail::incr::eval_cmp_interval_impl_interior(k1, a, excl, nbits, memo1,
levels, nullptr);
const integral lane = static_cast<integral>(alpha);
dpf::proof_token pi0{}, pi1{};
dpf::detail::vdpf::init_proof(pi0, k0);
dpf::detail::vdpf::init_proof(pi1, k1);
dpf::basic_path_memoizer<KT0> path0{};
dpf::basic_path_memoizer<KT1> path1{};
dpf::detail::blocked::eval_share_memo(k0, lane, a, excl, memo0, &pi0,
&path0);
dpf::detail::blocked::eval_share_memo(k1, lane, a, excl, memo1, &pi1,
&path1);
EXPECT_TRUE(dpf::verify(pi0, pi1));
const auto snap0 = pi0;
dpf::detail::blocked::eval_share_memo(k0, lane, a, excl, memo0, &pi0,
&path0);
EXPECT_TRUE(dpf::detail::vdpf::proof_equal(pi0, snap0));
// CW tamper on party 0.
auto k0_bad = k0;
for (auto & cs : const_cast<typename KT0::correction_seeds_array &>(
k0_bad.correction_seeds()))
{
cs[0] = simde_mm_xor_si128(cs[0], simde_mm_set1_epi8(0x5a));
}
dpf::detail::incr::cmp_full_interval_memo<KT0, stop> memo_bad{count};
dpf::detail::incr::eval_cmp_interval_impl_interior(k0_bad, a, excl, nbits,
memo_bad, levels, nullptr);
dpf::proof_token q0{}, q1{};
dpf::detail::vdpf::init_proof(q0, k0_bad);
dpf::detail::vdpf::init_proof(q1, k1);
dpf::basic_path_memoizer<KT0> pb0{};
dpf::basic_path_memoizer<KT1> pb1{};
dpf::detail::blocked::eval_share_memo(k0_bad, lane, a, excl, memo_bad,
&q0, &pb0);
dpf::detail::blocked::eval_share_memo(k1, lane, a, excl, memo1, &q1,
&pb1);
EXPECT_FALSE(dpf::verify(q0, q1));
}
TEST(OptInMemoProve, BfsIntervalProveUnchanged)
{
const Input alpha = 0x2a;
auto [k0, k1] = dpf::make_dpf(alpha,
dpf::block_width<4>(dpf::lt(u64{1})), dpf::verifiable{});
const Input from = 0x00;
const Input to = 0x3f;
dpf::proof_token a{}, b{};
dpf::prove_cmp_interval(k0, from, to, dpf::prove(a));
dpf::prove_cmp_interval(k1, from, to, dpf::prove(b));
EXPECT_TRUE(dpf::verify(a, b));
// Re-proving the same interval yields the same BFS transcript.
dpf::proof_token a2{}, b2{};
dpf::prove_cmp_interval(k0, from, to, dpf::prove(a2));
dpf::prove_cmp_interval(k1, from, to, dpf::prove(b2));
EXPECT_TRUE(dpf::detail::vdpf::proof_equal(a, a2));
EXPECT_TRUE(dpf::detail::vdpf::proof_equal(b, b2));
// Interval eval with prove still verifies (memo walk does not fold again).
auto buf0 = dpf::eval_interval(dpf::cmp, k0, from, to, dpf::prove(a2));
auto buf1 = dpf::eval_interval(dpf::cmp, k1, from, to, dpf::prove(b2));
EXPECT_TRUE(dpf::verify(a2, b2));
EXPECT_EQ(buf0.size(), buf1.size());
}
TEST(OptInCarry, VerifiableAndMacRoundTrip)
{
grotto::carry_auth auth{};
auth.verifiable = true;
auth.output_mac = true;
auto keys = grotto::make_carry_in_keys(8, 3, auth);
ASSERT_TRUE(keys.has_mac);
ASSERT_TRUE(keys.low_lt_v.has_value());
const std::uint64_t opened = 0x3cu;
dpf::proof_token t0[4]{}, t1[4]{};
const auto n0 = grotto::prove_carry_keys(keys, 0, opened, t0, 4);
const auto n1 = grotto::prove_carry_keys(keys, 1, opened, t1, 4);
EXPECT_EQ(n0, n1);
EXPECT_TRUE(dpf::verify_batch(
std::vector<dpf::proof_token>(t0, t0 + n0),
std::vector<dpf::proof_token>(t1, t1 + n1)));
const auto y0 = grotto::eval_carry_in(keys, 0, opened);
const auto y1 = grotto::eval_carry_in(keys, 1, opened);
auto [m0, m1] = grotto::mac_carry_result(keys, y0.value, y1.value);
EXPECT_TRUE(dpf::mac_verify(m0, m1, keys.mac, t0[0], t1[0]));
}

View file

@ -0,0 +1,94 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <vector>
#include "dpf.hpp"
namespace
{
std::vector<dpf::fp61> random_challenges(std::size_t n)
{
std::vector<dpf::fp61> rs(n);
for (auto & r : rs)
r = dpf::uniform_sample<dpf::fp61>();
return rs;
}
} // namespace
TEST(PathSketch, HonestIdpfAccepts)
{
using Input = std::uint8_t;
const Input alpha = 0xA0; // prefix 1010_0000 → length-3 prefix 101
auto [k0, k1] = dpf::make_dpf(alpha,
dpf::idpf(std::uint64_t{5}, std::uint64_t{5}, std::uint64_t{5}));
const auto rs = random_challenges(dpf::path_sketch_challenge_count(3));
EXPECT_TRUE((dpf::verify_idpf_path<3>(k0, k1, rs)));
}
TEST(PathSketch, HonestUnitWeightAccepts)
{
using Input = std::uint8_t;
auto [k0, k1] = dpf::make_dpf(Input{0b10100101},
dpf::idpf(std::uint64_t{1}, std::uint64_t{1}, std::uint64_t{1},
std::uint64_t{1}));
const auto rs = random_challenges(dpf::path_sketch_challenge_count(4));
EXPECT_TRUE((dpf::verify_idpf_path<4>(k0, k1, rs)));
}
TEST(PathSketch, LevelSketchRejectsTwoHot)
{
// Manufacture a two-hot vector and feed sketch_fold directly.
std::vector<dpf::fp61> y0 = {dpf::fp61{3}, dpf::fp61{0}, dpf::fp61{0},
dpf::fp61{0}};
std::vector<dpf::fp61> y1 = {dpf::fp61{0}, dpf::fp61{0}, dpf::fp61{2},
dpf::fp61{0}};
// Opened values: 3, 0, -2, 0 — two nonzeros.
std::vector<dpf::fp61> r = {dpf::fp61{9}, dpf::fp61{4}, dpf::fp61{7},
dpf::fp61{2}};
const auto s0 = dpf::sketch_fold(y0, r);
const auto s1 = dpf::sketch_fold(y1, r);
EXPECT_FALSE(dpf::sketch_verify(s0, s1));
}
TEST(PathSketch, ParentGapRejectsBrokenRelation)
{
using Input = std::uint8_t;
auto [k0, k1] = dpf::make_dpf(Input{0xA0},
dpf::idpf(std::uint64_t{5}, std::uint64_t{5}, std::uint64_t{5}));
auto gamma = random_challenges(2);
// Honest parent gap opens to 0.
const auto g0 = dpf::sketch_path_parent(dpf::out<0, 1>, dpf::out<1, 2>,
k0, gamma.data(), gamma.size());
const auto g1 = dpf::sketch_path_parent(dpf::out<0, 1>, dpf::out<1, 2>,
k1, gamma.data(), gamma.size());
EXPECT_TRUE(dpf::sketch_path_verify_parent(g0, g1));
// Bias party 0's gap — must reject.
EXPECT_FALSE(dpf::sketch_path_verify_parent(g0 + dpf::fp61{1}, g1));
}
TEST(PathSketch, ChallengeCountFormula)
{
// Depth 1: only level sketch at N=1 → 2 challenges.
EXPECT_EQ(dpf::path_sketch_challenge_count(1), 2u);
// Depth 2: level1 (2) + parent(2) + level2 (4) = 8.
EXPECT_EQ(dpf::path_sketch_challenge_count(2), 8u);
// Depth 3: 8 + parent@2 (4) + level3 (8) = 20.
EXPECT_EQ(dpf::path_sketch_challenge_count(3), 20u);
}
TEST(PathSketch, MasticClientsVerify)
{
auto [a0, a1] = dpf::make_dpf(std::uint8_t{0xA0},
dpf::idpf(std::uint64_t{5}, std::uint64_t{5}, std::uint64_t{5}));
auto [b0, b1] = dpf::make_dpf(std::uint8_t{0xB0},
dpf::idpf(std::uint64_t{3}, std::uint64_t{3}, std::uint64_t{3}));
const auto rs = random_challenges(dpf::path_sketch_challenge_count(3));
EXPECT_TRUE((dpf::verify_idpf_path<3>(a0, a1, rs)));
EXPECT_TRUE((dpf::verify_idpf_path<3>(b0, b1, rs)));
}

331
test/tests/pprf_test.cpp Normal file
View file

@ -0,0 +1,331 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <stdexcept>
#include <vector>
#include "dpf.hpp"
namespace
{
template <typename Block>
bool eq_block(const Block & a, const Block & b)
{
return std::memcmp(&a, &b, sizeof(Block)) == 0;
}
template <typename InputT>
void check_pprf_spread(InputT alpha)
{
auto master = dpf::make_pprf_master<InputT>();
auto punctured = dpf::puncture(master, alpha, /*program_alpha=*/true);
const auto master_at_alpha = dpf::pprf_eval(master, alpha);
EXPECT_TRUE(eq_block(master_at_alpha, *punctured.programmed));
EXPECT_TRUE(eq_block(dpf::pprf_eval(punctured, alpha), master_at_alpha));
std::vector<InputT> points = {
InputT{0},
InputT{1},
static_cast<InputT>(alpha + 1),
static_cast<InputT>(alpha ^ InputT{1}),
};
if constexpr (dpf::utils::bitlength_of_v<InputT> > 8)
{
points.push_back(static_cast<InputT>(
InputT{1} << (dpf::utils::bitlength_of_v<InputT> / 2)));
points.push_back(static_cast<InputT>(alpha ^ (InputT{1} << 7)));
}
for (InputT x : points)
{
if (x == alpha)
continue;
EXPECT_TRUE(eq_block(dpf::pprf_eval(master, x),
dpf::pprf_eval(punctured, x)));
}
auto bare = dpf::puncture(master, alpha, /*program_alpha=*/false);
EXPECT_FALSE(bare.programmed.has_value());
EXPECT_THROW((void)dpf::pprf_eval(bare, alpha), std::invalid_argument);
for (InputT x : points)
{
if (x == alpha)
continue;
EXPECT_TRUE(eq_block(dpf::pprf_eval(master, x),
dpf::pprf_eval(bare, x)));
}
EXPECT_EQ(punctured.siblings.size(), dpf::utils::bitlength_of_v<InputT>);
}
bool node_has_seed(const dpf::pprf_copath<std::uint8_t> & copath,
const dpf::prg::aes128::block_type & seed)
{
for (const auto & node : copath.nodes)
{
if (eq_block(node.seed, seed))
return true;
}
return false;
}
} // namespace
TEST(Pprf, PathBitHelpersAreConstexpr)
{
using input_t = std::uint8_t;
static_assert(dpf::detail::pprf_impl::path_bit<input_t>(0b1010'0000u, 0));
static_assert(!dpf::detail::pprf_impl::path_bit<input_t>(0b0010'0000u, 0));
static_assert(dpf::detail::pprf_impl::path_prefix<input_t>(0b1010'1100u, 4)
== input_t{0b1010u});
static_assert(dpf::detail::pprf_impl::prefix_matches<input_t>(
0b1010'1100u, 4, input_t{0b1010u}));
static_assert(!dpf::detail::pprf_impl::prefix_matches<input_t>(
0b1010'1100u, 4, input_t{0b1011u}));
SUCCEED();
}
TEST(Pprf, Uint32)
{
check_pprf_spread<std::uint32_t>(0x00c0ffeeu);
}
TEST(Pprf, Uint128)
{
using input_t = simde_uint128;
const input_t alpha = (input_t{1} << 120) | input_t{0xdeadbeefull};
check_pprf_spread<input_t>(alpha);
}
TEST(PprfCopath, OneHiddenAgreesWithOnePoint)
{
using input_t = std::uint8_t;
constexpr std::size_t n = dpf::utils::bitlength_of_v<input_t>;
auto master = dpf::make_pprf_master<input_t>();
const input_t alpha = 0x2au;
auto one = dpf::puncture(master, alpha, /*program_alpha=*/true);
const input_t hidden[] = {alpha};
auto copath = dpf::puncture(master, std::begin(hidden), std::end(hidden),
/*program_hidden=*/true);
ASSERT_EQ(copath.nodes.size(), n);
ASSERT_EQ(copath.programmed.size(), 1u);
EXPECT_TRUE(eq_block(copath.programmed[0], *one.programmed));
for (std::size_t i = 0; i < n; ++i)
{
EXPECT_EQ(copath.nodes[i].level, i + 1);
EXPECT_TRUE(eq_block(copath.nodes[i].seed, one.siblings[i]));
EXPECT_EQ(copath.nodes[i].prefix,
dpf::detail::pprf_impl::path_prefix(
static_cast<input_t>(
alpha ^ static_cast<input_t>(input_t{1} << (n - 1 - i))),
i + 1));
}
for (unsigned x = 0; x < 256; ++x)
{
const auto xi = static_cast<input_t>(x);
EXPECT_TRUE(eq_block(dpf::pprf_eval(copath, xi), dpf::pprf_eval(one, xi)));
if (xi != alpha)
EXPECT_TRUE(
eq_block(dpf::pprf_eval(copath, xi), dpf::pprf_eval(master, xi)));
}
auto bare = dpf::puncture(master, std::begin(hidden), std::end(hidden),
/*program_hidden=*/false);
EXPECT_TRUE(bare.programmed.empty());
EXPECT_THROW((void)dpf::pprf_eval(bare, alpha), std::invalid_argument);
for (unsigned x = 0; x < 256; ++x)
{
const auto xi = static_cast<input_t>(x);
if (xi == alpha)
continue;
EXPECT_TRUE(
eq_block(dpf::pprf_eval(bare, xi), dpf::pprf_eval(master, xi)));
}
}
TEST(PprfCopath, AdjacentAndSeparatedSets)
{
using input_t = std::uint8_t;
constexpr std::size_t n = dpf::utils::bitlength_of_v<input_t>;
auto master = dpf::make_pprf_master<input_t>();
const input_t adjacent[] = {2, 3};
auto adj = dpf::puncture(master, std::begin(adjacent), std::end(adjacent));
// Share a 7-bit prefix: one sibling per shared level, none at the leaves.
EXPECT_EQ(adj.nodes.size(), n - 1);
EXPECT_LE(adj.nodes.size(), n * adj.hidden.size());
EXPECT_LT(adj.nodes.size(), 2 * n);
const input_t separated[] = {2, 5};
auto sep = dpf::puncture(master, std::begin(separated), std::end(separated));
EXPECT_LE(sep.nodes.size(), n * sep.hidden.size());
EXPECT_GT(sep.nodes.size(), adj.nodes.size());
for (unsigned x = 0; x < 256; ++x)
{
const auto xi = static_cast<input_t>(x);
const bool in_adj = (xi == 2 || xi == 3);
const bool in_sep = (xi == 2 || xi == 5);
if (in_adj)
EXPECT_THROW((void)dpf::pprf_eval(adj, xi), std::invalid_argument);
else
EXPECT_TRUE(
eq_block(dpf::pprf_eval(adj, xi), dpf::pprf_eval(master, xi)));
if (in_sep)
EXPECT_THROW((void)dpf::pprf_eval(sep, xi), std::invalid_argument);
else
EXPECT_TRUE(
eq_block(dpf::pprf_eval(sep, xi), dpf::pprf_eval(master, xi)));
}
}
TEST(PprfCopath, HiddenLeafNotPublishedWhenSiblingHidden)
{
using input_t = std::uint8_t;
auto master = dpf::make_pprf_master<input_t>();
const input_t leaf = 2;
const auto leaf_seed = dpf::pprf_eval(master, leaf);
const input_t alone[] = {leaf};
auto solo = dpf::puncture(master, std::begin(alone), std::end(alone),
/*program_hidden=*/true);
EXPECT_TRUE(node_has_seed(solo, dpf::pprf_eval(master, static_cast<input_t>(3))));
const input_t both[] = {2, 3};
auto pair = dpf::puncture(master, std::begin(both), std::end(both),
/*program_hidden=*/true);
EXPECT_FALSE(node_has_seed(pair, leaf_seed));
EXPECT_FALSE(node_has_seed(pair, dpf::pprf_eval(master, static_cast<input_t>(3))));
ASSERT_EQ(pair.programmed.size(), 2u);
EXPECT_TRUE(eq_block(pair.programmed[0], leaf_seed));
EXPECT_TRUE(eq_block(pair.programmed[1],
dpf::pprf_eval(master, static_cast<input_t>(3))));
EXPECT_TRUE(eq_block(dpf::pprf_eval(pair, leaf), leaf_seed));
EXPECT_TRUE(eq_block(dpf::pprf_eval(pair, static_cast<input_t>(3)),
dpf::pprf_eval(master, static_cast<input_t>(3))));
}
TEST(PprfCopath, EmptyAndDuplicates)
{
using input_t = std::uint8_t;
auto master = dpf::make_pprf_master<input_t>();
const std::vector<input_t> none{};
auto empty = dpf::puncture(master, none.begin(), none.end());
ASSERT_EQ(empty.nodes.size(), 1u);
EXPECT_EQ(empty.nodes[0].level, 0u);
EXPECT_EQ(empty.nodes[0].prefix, input_t{});
EXPECT_TRUE(eq_block(empty.nodes[0].seed, master.root));
EXPECT_TRUE(empty.hidden.empty());
for (unsigned x = 0; x < 256; ++x)
{
const auto xi = static_cast<input_t>(x);
EXPECT_TRUE(
eq_block(dpf::pprf_eval(empty, xi), dpf::pprf_eval(master, xi)));
}
const input_t dups[] = {5, 2, 5, 2, 5};
auto once = dpf::puncture(master, std::begin(dups), std::end(dups));
const input_t unique[] = {2, 5};
auto clean = dpf::puncture(master, std::begin(unique), std::end(unique));
ASSERT_EQ(once.hidden.size(), 2u);
EXPECT_EQ(once.hidden, clean.hidden);
ASSERT_EQ(once.nodes.size(), clean.nodes.size());
for (std::size_t i = 0; i < once.nodes.size(); ++i)
{
EXPECT_EQ(once.nodes[i].level, clean.nodes[i].level);
EXPECT_EQ(once.nodes[i].prefix, clean.nodes[i].prefix);
EXPECT_TRUE(eq_block(once.nodes[i].seed, clean.nodes[i].seed));
}
}
TEST(PprfCopath, FullDomainPublishesNothing)
{
using input_t = std::uint8_t;
auto master = dpf::make_pprf_master<input_t>();
std::vector<input_t> all(256);
for (unsigned x = 0; x < 256; ++x)
all[x] = static_cast<input_t>(x);
auto bare = dpf::puncture(master, all.begin(), all.end(),
/*program_hidden=*/false);
EXPECT_TRUE(bare.nodes.empty());
ASSERT_EQ(bare.hidden.size(), 256u);
EXPECT_TRUE(bare.programmed.empty());
for (unsigned x = 0; x < 256; ++x)
EXPECT_THROW((void)dpf::pprf_eval(bare, static_cast<input_t>(x)),
std::invalid_argument);
auto programmed = dpf::puncture(master, all.begin(), all.end(),
/*program_hidden=*/true);
EXPECT_TRUE(programmed.nodes.empty());
ASSERT_EQ(programmed.programmed.size(), 256u);
for (unsigned x = 0; x < 256; ++x)
{
const auto xi = static_cast<input_t>(x);
EXPECT_TRUE(eq_block(dpf::pprf_eval(programmed, xi),
dpf::pprf_eval(master, xi)));
}
}
TEST(PprfCopath, NodePrefixesAreConsistent)
{
using input_t = std::uint8_t;
auto master = dpf::make_pprf_master<input_t>();
const input_t hidden[] = {2, 5, 200};
auto copath = dpf::puncture(master, std::begin(hidden), std::end(hidden));
EXPECT_LE(copath.nodes.size(),
dpf::utils::bitlength_of_v<input_t> * copath.hidden.size());
for (const auto & node : copath.nodes)
{
ASSERT_GE(node.level, 1u);
ASSERT_LE(node.level, dpf::utils::bitlength_of_v<input_t>);
// Right-aligned prefix fits in `level` bits.
if (node.level < dpf::utils::bitlength_of_v<input_t>)
{
EXPECT_LT(static_cast<unsigned>(node.prefix),
1u << node.level);
}
}
for (unsigned x = 0; x < 256; ++x)
{
const auto xi = static_cast<input_t>(x);
if (std::binary_search(std::begin(hidden), std::end(hidden), xi))
continue;
EXPECT_TRUE(
eq_block(dpf::pprf_eval(copath, xi), dpf::pprf_eval(master, xi)));
}
}
TEST(PprfCopath, Uint32SetMatchesMaster)
{
using input_t = std::uint32_t;
auto master = dpf::make_pprf_master<input_t>();
const input_t hidden[] = {0u, 0x00c0ffeeu, 0xffffffffu};
auto copath = dpf::puncture(master, std::begin(hidden), std::end(hidden),
/*program_hidden=*/true);
ASSERT_EQ(copath.programmed.size(), 3u);
for (input_t h : hidden)
EXPECT_TRUE(eq_block(dpf::pprf_eval(copath, h), dpf::pprf_eval(master, h)));
const input_t samples[] = {1u, 2u, 0x00c0ffefu, 0x80000000u, 0xfffffffeu};
for (input_t x : samples)
{
if (std::find(std::begin(hidden), std::end(hidden), x) != std::end(hidden))
continue;
EXPECT_TRUE(
eq_block(dpf::pprf_eval(copath, x), dpf::pprf_eval(master, x)));
}
}

View file

@ -439,3 +439,41 @@ TEST(KPpvc, RejectsASharedHiddenIndex)
EXPECT_EQ(st.copies[0].i, st.copies[1].i);
EXPECT_FALSE(scheme::check_well_formed(pp, com, st));
}
TEST(Ppvc, AuditedReplicaReexpandsFromCopath)
{
using scheme = dpf::ppvc<std::uint8_t, 2, 8>;
using input_t = std::uint8_t;
constexpr input_t live = 1;
constexpr input_t audited = 3;
constexpr input_t pool[] = {0, 1, 2, 3};
const auto pp = scheme::setup_from_seed(seed_block(90, 91));
auto master = dpf::make_pprf_master<input_t>();
std::array<scheme::commitment, 4> commitments{};
std::array<scheme::state, 4> states{};
std::array<block, 4> seeds{};
for (std::size_t i = 0; i < 4; ++i)
{
seeds[i] = dpf::pprf_eval(master, pool[i]);
auto made = scheme::commit_from_seed(pp, seeds[i]);
commitments[i] = std::move(made.first);
states[i] = std::move(made.second);
ASSERT_TRUE(scheme::check_well_formed(pp, commitments[i], states[i]));
}
const input_t hidden[] = {live};
auto copath = dpf::puncture(master, std::begin(hidden), std::end(hidden),
/*program_hidden=*/false);
EXPECT_THROW((void)dpf::pprf_eval(copath, live), std::invalid_argument);
const auto opened = dpf::pprf_eval(copath, audited);
EXPECT_TRUE(same_root(opened, seeds[3]));
auto [com_again, st_again] = scheme::commit_from_seed(pp, opened);
EXPECT_TRUE(scheme::check_well_formed(pp, com_again, st_again));
EXPECT_TRUE(scheme::audit(pp, commitments[3], opened));
EXPECT_EQ(st_again.i, states[3].i);
}

View file

@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <tuple>
#include <cstdint>
#include <cstring>

View file

@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <tuple>
#include <cstdint>
#include <cstring>

View file

@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <tuple>
#include "grotto/principal_lut.hpp"

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
#include "dpf/random.hpp"
#include <tuple>
#include <gtest/gtest.h>
@ -311,7 +312,7 @@ TEST_F(RandomTest, ForkedProcessesDoNotRepeatEntropy)
{
// A buffered stdio read of the device copies the unread buffer into the
// child. Prime the generator, then compare the next draw on each side.
(void)dpf::uniform_sample<std::uint64_t>();
dpf::uniform_sample<std::uint64_t>();
int fds[2];
ASSERT_EQ(::pipe(fds), 0);

View file

@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <tuple>
#include "grotto/range_lut.hpp"
@ -103,16 +104,26 @@ bool near_odd_multiple_of_half_pi(long double x)
TEST(RangeLut, LogarithmsTrackLibmOnEveryPrecision)
{
const grotto::reduced maps[] = {
grotto::reduced::ln, grotto::reduced::lg, grotto::reduced::log10,
};
const long double samples[] = {
0.125L, 0.3L, 0.5L, 0.75L, 1.0L, 1.5L, 2.0L, 3.0L, 7.5L, 16.0L, 24.0L, 100.0L,
};
for (auto which : maps)
for (unsigned k : grotto::principal_precisions)
for (long double x : samples)
expect_ulps(which, k, x, 6.0L);
for (unsigned k : grotto::principal_precisions)
{
for (long double x : samples)
{
expect_ulps(grotto::reduced::ln, k, x, 1.0L);
expect_ulps(grotto::reduced::log10, k, x, 1.0L);
expect_ulps(grotto::reduced::lg, k, x, 2.0L);
}
for (int exp : {-20, 20})
{
const long double x = std::ldexp(1.0L, exp);
if (raw_of(x, k) == 0)
continue;
expect_ulps(grotto::reduced::ln, k, x, 1.0L);
expect_ulps(grotto::reduced::log10, k, x, 1.0L);
}
}
}
TEST(RangeLut, ExponentialsTrackLibmOnEveryPrecision)
@ -120,10 +131,12 @@ TEST(RangeLut, ExponentialsTrackLibmOnEveryPrecision)
const long double samples[] = {
-2.0L, -1.5L, -0.5L, -0.1L, 0.0L, 0.1L, 0.5L, 1.0L, 1.5L, 2.0L,
};
for (auto which : {grotto::reduced::exp, grotto::reduced::exp2})
for (unsigned k : grotto::principal_precisions)
for (long double x : samples)
expect_ulps(which, k, x, 16.0L);
for (unsigned k : grotto::principal_precisions)
for (long double x : samples)
{
expect_ulps(grotto::reduced::exp, k, x, 1.0L);
expect_ulps(grotto::reduced::exp2, k, x, 2.0L);
}
for (unsigned k : grotto::principal_precisions)
{
for (long double x : {-0.9L, -0.25L, 0.0L, 0.25L, 0.9L})
@ -259,7 +272,7 @@ TEST(RangeLut, TanAndSecPolesThrow)
{
try
{
(void)grotto::eval_reduced(which, k, raw);
grotto::eval_reduced(which, k, raw);
}
catch (const std::domain_error &)
{
@ -290,7 +303,7 @@ TEST(RangeLut, Exp10RejectsAnIntegerPowerPast18)
{
try
{
(void)grotto::eval_reduced(grotto::reduced::exp10, k, raw_of(static_cast<long double>(n), k));
grotto::eval_reduced(grotto::reduced::exp10, k, raw_of(static_cast<long double>(n), k));
}
catch (const std::overflow_error &)
{
@ -302,10 +315,8 @@ TEST(RangeLut, Exp10RejectsAnIntegerPowerPast18)
ASSERT_LE(first_overflow, 19);
EXPECT_NO_THROW(grotto::eval_reduced(grotto::reduced::exp10, k,
raw_of(static_cast<long double>(first_overflow - 1), k)));
EXPECT_THROW(grotto::eval_reduced(grotto::reduced::exp10, k, raw_of(19.0L, k)),
std::overflow_error);
EXPECT_THROW(grotto::eval_reduced(grotto::reduced::exp10, k, raw_of(-19.0L, k)),
std::overflow_error);
EXPECT_THROW(grotto::eval_reduced(grotto::reduced::exp10, k, raw_of(19.0L, k)), std::overflow_error);
EXPECT_THROW(grotto::eval_reduced(grotto::reduced::exp10, k, raw_of(-19.0L, k)), std::overflow_error);
}
TEST(RangeLut, Expm1AndLog1pTrackLibm)
@ -325,9 +336,28 @@ TEST(RangeLut, Expm1AndLog1pTrackLibm)
expect_ulps(grotto::reduced::log1p, k, std::ldexp(1.0L, -static_cast<int>(k)), 2.0L);
expect_ulps(grotto::reduced::log1p, k, -std::ldexp(1.0L, -static_cast<int>(k)), 2.0L);
for (long double x : expm1_samples)
expect_ulps(grotto::reduced::expm1, k, x, 20.0L);
expect_ulps(grotto::reduced::expm1, k, x, 1.0L);
for (long double x : log1p_samples)
expect_ulps(grotto::reduced::log1p, k, x, 8.0L);
expect_ulps(grotto::reduced::log1p, k, x, 1.0L);
}
}
TEST(RangeLut, ReciprocalFamilyStaysInsideOneUlpAfterTheLift)
{
const long double samples[] = {
std::ldexp(1.0L, -12), 0.015625L, 0.125L, 0.5L, 0.75L, 1.0L, 1.5L, 3.0L, 8.0L, 100.0L,
};
for (auto which : {grotto::reduced::inv, grotto::reduced::rsqrt, grotto::reduced::invsq})
{
for (unsigned k : {16u, 32u})
{
for (long double x : samples)
{
if (k == 16 && x < std::ldexp(1.0L, -10))
continue;
expect_ulps(which, k, x, 1.0L);
}
}
}
}

View file

@ -0,0 +1,128 @@
#include <gtest/gtest.h>
#include "grotto/ring_switch.hpp"
#include "dpf/field128.hpp"
#include "dpf/p256_scalar.hpp"
#include "dpf/verifiable.hpp"
#include <cstdint>
TEST(Residue, Zn64Arithmetic)
{
using Z = grotto::zn64<7>;
EXPECT_EQ((Z{3} + Z{5}).raw(), 1u);
EXPECT_EQ((-Z{3}).raw(), 4u);
EXPECT_EQ((Z{3} - Z{5}).raw(), 5u);
}
TEST(Residue, Zn128Arithmetic)
{
using Z = grotto::zn128<1009, 0>;
EXPECT_EQ((Z{1000} + Z{20}).lo(), 11u);
EXPECT_EQ((-Z{1}).lo(), 1008u);
}
TEST(Residue, CrtFactor)
{
using Z = grotto::zn64<15>;
const Z share{11};
EXPECT_EQ(grotto::ring_switch_factor<3>(share).raw(), 2u);
EXPECT_EQ(grotto::ring_switch_factor<5>(share).raw(), 1u);
}
TEST(RingSwitch, Zn64WrapAndNoWrap)
{
using Z = grotto::zn64<1009>;
const std::uint8_t r = 200;
const std::uint8_t eta_nw = 10; // 200+10 < 256
const std::uint8_t eta_w = 100; // 200+100 >= 256
const std::uint8_t x_nw = static_cast<std::uint8_t>(r + eta_nw);
const std::uint8_t x_w = static_cast<std::uint8_t>(r + eta_w);
auto mat = grotto::make_ring_switch_keys<Z>(r);
const Z s0 = grotto::ring_switch_eval<0>(mat, eta_nw);
const Z s1 = grotto::ring_switch_eval<1>(mat, eta_nw);
EXPECT_EQ(s0 + s1, grotto::ring_switch_clear<Z>(x_nw, r, eta_nw));
EXPECT_EQ((s0 + s1).raw(), static_cast<std::uint64_t>(x_nw) % 1009);
const Z t0 = grotto::ring_switch_eval<0>(mat, eta_w);
const Z t1 = grotto::ring_switch_eval<1>(mat, eta_w);
EXPECT_EQ(t0 + t1, grotto::ring_switch_clear<Z>(x_w, r, eta_w));
EXPECT_EQ((t0 + t1).raw(), static_cast<std::uint64_t>(x_w) % 1009);
}
TEST(RingSwitch, Zn64EtaZero)
{
using Z = grotto::zn64<97>;
const std::uint8_t r = 55;
const std::uint8_t eta = 0;
auto mat = grotto::make_ring_switch_keys<Z>(r);
const Z got = grotto::ring_switch_eval<0>(mat, eta)
+ grotto::ring_switch_eval<1>(mat, eta);
EXPECT_EQ(got.raw(), 55u);
}
TEST(RingSwitch, FullWidthLimb)
{
using Z = grotto::zn64<10007>;
const std::uint64_t r = 0xffff'ffff'ffff'ff00ull;
const std::uint64_t eta = 0x200ull; // wraps
const std::uint64_t x = r + eta; // wraps in uint64
auto mat = grotto::make_ring_switch_keys<Z>(r);
const Z got = grotto::ring_switch_eval<0>(mat, eta)
+ grotto::ring_switch_eval<1>(mat, eta);
EXPECT_EQ(got, grotto::ring_switch_clear<Z>(x, r, eta));
EXPECT_EQ(got.raw(), x % 10007);
}
TEST(RingSwitch, Field128)
{
const std::uint16_t r = 40000;
const std::uint16_t eta = 30000; // wraps
const std::uint16_t x = static_cast<std::uint16_t>(r + eta);
auto mat = grotto::make_ring_switch_keys<dpf::field128>(r);
const auto got = grotto::ring_switch_eval<0>(mat, eta)
+ grotto::ring_switch_eval<1>(mat, eta);
EXPECT_EQ(got, grotto::ring_switch_clear<dpf::field128>(x, r, eta));
EXPECT_EQ(got, dpf::field128{x});
}
TEST(RingSwitch, P256Scalar)
{
const std::uint8_t r = 200;
const std::uint8_t eta = 100;
const std::uint8_t x = static_cast<std::uint8_t>(r + eta);
auto mat = grotto::make_ring_switch_keys<dpf::p256_scalar>(r);
const auto got = grotto::ring_switch_eval<0>(mat, eta)
+ grotto::ring_switch_eval<1>(mat, eta);
EXPECT_EQ(got, grotto::ring_switch_clear<dpf::p256_scalar>(x, r, eta));
EXPECT_EQ(got, dpf::p256_scalar{x});
}
TEST(RingSwitch, Zn128)
{
using Z = grotto::zn128<0x9a57'0000'0000'0001ull, 0>;
const std::uint8_t r = 10;
const std::uint8_t eta = 20;
const std::uint8_t x = 30;
auto mat = grotto::make_ring_switch_keys<Z>(r);
const Z got = grotto::ring_switch_eval<0>(mat, eta)
+ grotto::ring_switch_eval<1>(mat, eta);
EXPECT_EQ(got, grotto::ring_switch_clear<Z>(x, r, eta));
EXPECT_EQ(got.lo(), 30u);
}
TEST(RingSwitch, Verifiable)
{
using Z = grotto::zn64<1009>;
const std::uint8_t r = 17;
const std::uint8_t eta = 200;
const std::uint8_t x = static_cast<std::uint8_t>(r + eta);
auto mat = grotto::make_ring_switch_keys<Z>(r, dpf::verifiable{});
dpf::proof_token a{}, b{};
const Z s0 = grotto::ring_switch_eval<0>(mat, eta, &a);
const Z s1 = grotto::ring_switch_eval<1>(mat, eta, &b);
EXPECT_EQ(s0 + s1, grotto::ring_switch_clear<Z>(x, r, eta));
EXPECT_TRUE(dpf::verify(a, b));
}

View file

@ -1,7 +1,10 @@
#include <gtest/gtest.h>
#include <array>
#include <cstdint>
#include <cstring>
#include <limits>
#include <stdexcept>
#include <type_traits>
#include <utility>
@ -10,6 +13,14 @@
namespace
{
struct dpf3_party_tag
{
static constexpr bool is_dpf3 = true;
static constexpr int party = 3;
};
TEST(SecretShare, LayoutMatchesValueType)
{
using T = std::uint64_t;
@ -140,4 +151,365 @@ TEST(SecretShare, PrgExpandSubtractive)
EXPECT_EQ(dpf::reconstruct(t0, t1), 0u);
}
TEST(SecretShare, Additive3LayoutAndPlaintextSplit)
{
using T = std::uint64_t;
static_assert(dpf::sharing_parties_v<dpf::sharing::additive3> == 3);
static_assert(dpf::sharing_threshold_v<dpf::sharing::additive3> == 3);
static_assert(dpf::is_secret_share_v<dpf::additive3_share<T, 2>>);
static_assert(dpf::share_party_v<dpf::additive3_share<T, 2>> == 2u);
static_assert(dpf::share_scheme_v<dpf::additive3_share<T, 1>>
== dpf::sharing::additive3);
EXPECT_EQ(sizeof(dpf::additive3_share<T, 2>), sizeof(T));
EXPECT_TRUE((std::is_trivially_copyable_v<dpf::additive3_share<T, 2>>));
EXPECT_TRUE((std::is_standard_layout_v<dpf::additive3_share<T, 0>>));
constexpr auto split = dpf::make_additive3_shares(std::uint32_t{9});
static_assert(dpf::reconstruct(
std::get<0>(split), std::get<1>(split), std::get<2>(split)) == 9u);
const std::uint32_t secret = 0xdeadbeefu;
auto [a0, a1, a2] = dpf::make_additive3_shares(secret);
EXPECT_EQ(a1.raw(), 0u);
EXPECT_EQ(a2.raw(), 0u);
EXPECT_EQ(dpf::reconstruct(a2, a0, a1), secret);
}
TEST(SecretShare, Additive3LinearComboAndAbsorb)
{
auto [a0, a1, a2] = dpf::make_additive3_shares(std::uint32_t{10});
auto [b0, b1, b2] = dpf::make_additive3_shares(std::uint32_t{3});
auto c0 = a0 * 2 + b0;
auto c1 = a1 * 2 + b1;
auto c2 = a2 * 2 + b2;
EXPECT_EQ(dpf::reconstruct(c0, c1, c2), 23u);
a0 += std::uint32_t{10};
a1 += std::uint32_t{10};
a2 += std::uint32_t{10};
EXPECT_EQ(dpf::reconstruct(a0, a1, a2), 20u);
const auto raw = std::numeric_limits<std::int32_t>::min();
auto s0 = dpf::additive3_share<std::int32_t, 0>::from_raw(raw);
auto s1 = dpf::additive3_share<std::int32_t, 1>::from_raw(-1);
auto s2 = dpf::additive3_share<std::int32_t, 2>::from_raw(1);
EXPECT_EQ(dpf::reconstruct(s0, s1, s2), raw);
}
TEST(SecretShare, Additive3RandomAndXor)
{
using X = dpf::xor_wrapper<std::uint32_t>;
const X secret{0xA5A5A5A5u};
auto [x0, x1, x2] = dpf::additively_share3(secret);
EXPECT_EQ(dpf::reconstruct(x0, x1, x2), secret);
const std::int32_t n = -42;
auto [a, b, c] = dpf::additively_share3(n);
EXPECT_EQ(dpf::reconstruct(c, b, a), n);
const float f = 1.5f;
auto [f0, f1, f2] = dpf::additively_share3(f);
EXPECT_EQ(dpf::reconstruct(f0, f1, f2), f);
}
TEST(SecretShare, ReplicatedLayoutPlaintextAndPairs)
{
using T = std::uint32_t;
static_assert(dpf::sharing_parties_v<dpf::sharing::replicated> == 3);
static_assert(dpf::sharing_threshold_v<dpf::sharing::replicated> == 2);
static_assert(dpf::share_scheme_v<dpf::replicated_share<T, 2>>
== dpf::sharing::replicated);
EXPECT_EQ(sizeof(dpf::replicated_share<T, 0>), 2 * sizeof(T));
EXPECT_TRUE((std::is_trivially_copyable_v<dpf::replicated_share<T, 1>>));
EXPECT_TRUE((std::is_standard_layout_v<dpf::replicated_share<T, 2>>));
constexpr auto split = dpf::make_replicated_shares(T{11});
static_assert(dpf::reconstruct(std::get<0>(split), std::get<1>(split)) == 11u);
static_assert(dpf::reconstruct(std::get<1>(split), std::get<2>(split)) == 11u);
static_assert(dpf::reconstruct(std::get<2>(split), std::get<0>(split)) == 11u);
auto [r0, r1, r2] = dpf::make_replicated_shares(T{11});
EXPECT_EQ(r0.own, 11u);
EXPECT_EQ(r0.next, 0u);
EXPECT_EQ(r1.own, 0u);
EXPECT_EQ(r1.next, 0u);
EXPECT_EQ(r2.own, 0u);
EXPECT_EQ(r2.next, 11u);
EXPECT_EQ(dpf::reconstruct(r0, r1, r2), 11u);
EXPECT_EQ(r0.as_additive3().raw(), 11u);
EXPECT_EQ(r0.next_additive3().raw(), r1.as_additive3().raw());
}
TEST(SecretShare, ReplicatedLinearComboAbsorbAndAdditive3)
{
auto [a0, a1, a2] = dpf::make_replicated_shares(std::uint32_t{10});
auto [b0, b1, b2] = dpf::make_replicated_shares(std::uint32_t{3});
auto c0 = a0 * 2 - b0;
auto c1 = a1 * 2 - b1;
auto c2 = a2 * 2 - b2;
EXPECT_EQ(dpf::reconstruct(c0, c1), 17u);
EXPECT_EQ(dpf::reconstruct(c1, c2), 17u);
EXPECT_EQ(dpf::reconstruct(c0, c2, c1), 17u);
c0 += std::uint32_t{4};
c1 += std::uint32_t{4};
c2 += std::uint32_t{4};
EXPECT_EQ(c1.own, (a1 * 2 - b1).own);
EXPECT_EQ(c1.next, (a1 * 2 - b1).next);
EXPECT_EQ(dpf::reconstruct(c2, c0), 21u);
auto [d0, d1, d2] = dpf::make_additive3_shares(std::uint32_t{1});
d1 = dpf::additive3_share<std::uint32_t, 1>::from_raw(2u);
d2 = dpf::additive3_share<std::uint32_t, 2>::from_raw(4u);
auto [e0, e1, e2] = dpf::add_replicated(c0, c1, c2, d0, d1, d2);
EXPECT_EQ(e0.next, e1.own);
EXPECT_EQ(e1.next, e2.own);
EXPECT_EQ(e2.next, e0.own);
EXPECT_EQ(dpf::reconstruct(e0, e1), 28u);
EXPECT_EQ(dpf::reconstruct(e1, e2, e0), dpf::reconstruct(e0, e2));
}
TEST(SecretShare, ReplicatedFromComponentsAndRandom)
{
using X = dpf::xor_wrapper<std::uint16_t>;
const X secret{0xBEEFu};
auto [a0, a1, a2] = dpf::additively_share3(secret);
auto [r0, r1, r2] = dpf::make_replicated_shares(a0, a1, a2);
EXPECT_EQ(r0.own, a0.raw());
EXPECT_EQ(r0.next, a1.raw());
EXPECT_EQ(r2.next, a0.raw());
EXPECT_EQ(dpf::reconstruct(r2, r1), secret);
EXPECT_EQ(dpf::reconstruct(r0.as_additive3(), r1.as_additive3(),
r2.as_additive3()), secret);
auto built = dpf::replicated_share<X, 0>::from_additive3(
a0, a1);
EXPECT_EQ(built, r0);
const std::int64_t n = std::numeric_limits<std::int64_t>::min() + 7;
auto [s0, s1, s2] = dpf::share_replicated(n);
EXPECT_EQ(s0.next, s1.own);
EXPECT_EQ(s1.next, s2.own);
EXPECT_EQ(s2.next, s0.own);
EXPECT_EQ(dpf::reconstruct(s0, s1), n);
EXPECT_EQ(dpf::reconstruct(s1, s2), n);
EXPECT_EQ(dpf::reconstruct(s2, s0), n);
EXPECT_EQ(dpf::reconstruct(s2, s1, s0), n);
auto [p0, p1, p2] = dpf::make_replicated_shares(std::uint32_t{8});
dpf::replicated_share<std::uint32_t, 0> slot{};
dpf::assign_share_slot(slot, p0);
EXPECT_EQ(slot, p0);
std::uint32_t word = 0;
dpf::assign_share_slot(word, p0.as_additive3());
EXPECT_EQ(word, 8u);
(void)p1;
(void)p2;
}
TEST(SecretShare, TwoPartyConversionsPreserveSecret)
{
auto a0 = dpf::additive_share<std::int32_t, 0>::from_raw(10);
auto a1 = dpf::additive_share<std::int32_t, 1>::from_raw(5);
auto b0 = dpf::a2b(a0);
auto b1 = dpf::a2b(a1);
EXPECT_EQ(b0.raw(), 10);
EXPECT_EQ(b1.raw(), -5);
EXPECT_EQ(dpf::reconstruct(b0, b1), 15);
EXPECT_EQ(dpf::reconstruct(dpf::b2a(b0), dpf::b2a(b1)), 15);
auto f0 = dpf::a2fss(a0);
auto f1 = dpf::a2fss(a1);
EXPECT_EQ(f1.raw(), -5);
EXPECT_EQ(dpf::reconstruct(dpf::fss2a(f0), dpf::fss2a(f1)), 15);
EXPECT_EQ(dpf::fss2b(f1).raw(), f1.raw());
EXPECT_EQ(dpf::b2fss(b1).raw(), b1.raw());
// Subtractive and FSS share a sign, so party 1 adds the raw words.
auto mixed = b1 + f1;
EXPECT_EQ(mixed.raw(), -10);
// Additive and FSS disagree on party 1, so the FSS word is flipped.
auto flipped = a1 + dpf::fss_share<std::int32_t, 1>::from_raw(4);
EXPECT_EQ(flipped.raw(), 1);
}
TEST(SecretShare, ShamirRoundTripAndReplicatedProduct)
{
const dpf::fp61 secret{20};
const dpf::fp61 slope{3};
auto [s0, s1, s2] = dpf::make_shamir_shares(secret, slope);
EXPECT_EQ((dpf::shamir_share<dpf::fp61, 0>::point), 1u);
EXPECT_EQ(s0.raw(), secret + slope * dpf::fp61{1});
EXPECT_EQ(s1.raw(), secret + slope * dpf::fp61{2});
EXPECT_EQ(s2.raw(), secret + slope * dpf::fp61{3});
EXPECT_EQ(dpf::reconstruct(s0, s1), secret);
EXPECT_EQ(dpf::reconstruct(s1, s2), secret);
EXPECT_EQ(dpf::reconstruct(s2, s0, s1), secret);
s0 += dpf::fp61{4};
s1 += dpf::fp61{4};
s2 += dpf::fp61{4};
EXPECT_EQ(dpf::reconstruct(s2, s0), secret + dpf::fp61{4});
auto [y0, y1, y2] = dpf::s2y(s0, s1);
EXPECT_EQ(dpf::reconstruct(y0, y1, y2), secret + dpf::fp61{4});
auto back = dpf::y2s(y0, y1, y2, slope);
EXPECT_EQ(dpf::reconstruct(std::get<0>(back), std::get<2>(back)),
secret + dpf::fp61{4});
auto [r0, r1, r2] = dpf::s2rss(s0, s2);
EXPECT_EQ(dpf::reconstruct(r0, r1), secret + dpf::fp61{4});
auto shamir_again = dpf::rss2s(r1, r2, slope);
EXPECT_EQ(dpf::reconstruct(std::get<1>(shamir_again), std::get<2>(shamir_again)),
secret + dpf::fp61{4});
auto tagged = dpf::as_shamir_share(dpf3_party_tag{}, dpf::fp61{7});
EXPECT_EQ(decltype(tagged)::party, 2u);
EXPECT_EQ(tagged.raw(), dpf::fp61{7});
auto runtime = dpf::shamir3::runtime_share(s0);
EXPECT_EQ(runtime.party, 1);
EXPECT_EQ(dpf::shamir3::typed_share<0>(runtime), s0);
EXPECT_EQ(dpf::shamir3::reconstruct(runtime,
dpf::shamir3::runtime_share(s1)), secret + dpf::fp61{4});
auto [x0, x1, x2] = dpf::make_replicated_shares(
std::uint32_t{3}, std::uint32_t{5}, std::uint32_t{1});
auto [z0, z1, z2] = dpf::make_replicated_shares(
std::uint32_t{4}, std::uint32_t{2}, std::uint32_t{6});
EXPECT_EQ(dpf::reconstruct(dpf::rss_mul(x0, z0), dpf::rss_mul(x1, z1),
dpf::rss_mul(x2, z2)), 108u);
auto [p0, p1, p2] = dpf::rss_mul(x0, x1, x2, z0, z1, z2);
EXPECT_EQ(p0.next, p1.own);
EXPECT_EQ(dpf::reconstruct(p0, p2), 108u);
auto m0 = dpf::additive3_share<std::uint32_t, 0>::from_raw(9u);
auto m1 = dpf::additive3_share<std::uint32_t, 1>::from_raw(1u);
auto m2 = dpf::additive3_share<std::uint32_t, 2>::from_raw(
static_cast<std::uint32_t>(0u - 9u - 1u));
auto [q0, q1, q2] = dpf::rss_mul(x0, x1, x2, z0, z1, z2, m0, m1, m2);
EXPECT_EQ(dpf::reconstruct(q1, q0), 108u);
EXPECT_NE(q0.own, p0.own);
(void)q2;
auto [h0, h1, h2] = dpf::y2rss(dpf::rss2y(p0), dpf::rss2y(p1), dpf::rss2y(p2));
EXPECT_EQ(h0, p0);
EXPECT_EQ(h1, p1);
EXPECT_EQ(h2, p2);
}
TEST(SecretShare, ShamirKnSpecializesTwoOfThree)
{
using F = dpf::fp61;
static_assert(std::is_same_v<
dpf::shamir::share<F, 0, 2, 3>, dpf::shamir_share<F, 0>>);
static_assert(std::is_same_v<
dpf::shamir::share<F, 2, 2, 3>, dpf::shamir_share<F, 2>>);
static_assert(dpf::shamir::two_of_three::threshold == 2);
static_assert(dpf::shamir::two_of_three::parties == 3);
static_assert(dpf::shamir::two_of_three::degree == 1);
static_assert(dpf::shamir_share<F, 1>::threshold == 2);
static_assert(dpf::shamir_share<F, 1>::parties == 3);
static_assert(!dpf::is_secret_share_v<dpf::shamir::share<F, 0, 3, 5>>);
static_assert(dpf::shamir::is_share_v<dpf::shamir::share<F, 0, 3, 5>>);
static_assert(dpf::shamir::is_share_v<dpf::shamir_share<F, 0>>);
EXPECT_EQ(sizeof(dpf::shamir::share<F, 4, 3, 5>), sizeof(F));
EXPECT_TRUE((std::is_trivially_copyable_v<dpf::shamir::share<F, 4, 3, 5>>));
EXPECT_TRUE((std::is_standard_layout_v<dpf::shamir_share<F, 0>>));
// p(x) = 10 + 2x + 3x^2. Points 1..5 are 15, 26, 43, 66, 95.
const F secret{10};
const std::array<F, 2> coeff{{F{2}, F{3}}};
const F expect[5] = {F{15}, F{26}, F{43}, F{66}, F{95}};
auto dealt = dpf::make_shamir_shares<3, 5>(secret, coeff);
const std::array<F, 5> raw{{
std::get<0>(dealt).raw(), std::get<1>(dealt).raw(),
std::get<2>(dealt).raw(), std::get<3>(dealt).raw(),
std::get<4>(dealt).raw()}};
for (int i = 0; i < 5; ++i)
EXPECT_EQ(raw[static_cast<std::size_t>(i)], expect[i]);
EXPECT_EQ(std::get<4>(dealt).point, 5u);
EXPECT_EQ(std::get<4>(dealt).party, 4u);
for (int i = 0; i < 5; ++i)
for (int j = i + 1; j < 5; ++j)
for (int k = j + 1; k < 5; ++k)
{
const std::array<dpf::shamir::point_share<F>, 3> subset{{
{i + 1, raw[static_cast<std::size_t>(i)]},
{j + 1, raw[static_cast<std::size_t>(j)]},
{k + 1, raw[static_cast<std::size_t>(k)]}}};
const F opened = dpf::shamir::reconstruct<F, 3, 5>(subset);
EXPECT_EQ(opened, secret);
}
EXPECT_EQ(dpf::shamir::reconstruct(
std::get<0>(dealt), std::get<2>(dealt), std::get<4>(dealt)), secret);
auto scaled0 = std::get<0>(dealt) * F{3};
auto scaled1 = std::get<1>(dealt) * F{3};
auto scaled2 = std::get<2>(dealt) * F{3};
EXPECT_EQ(dpf::shamir::reconstruct(scaled0, scaled1, scaled2), secret * F{3});
auto sum0 = std::get<0>(dealt) + std::get<0>(dealt);
auto sum1 = std::get<1>(dealt) + std::get<1>(dealt);
auto sum2 = std::get<2>(dealt) + std::get<2>(dealt);
EXPECT_EQ(dpf::shamir::reconstruct(sum0, sum1, sum2), secret + secret);
std::get<0>(dealt) += F{4};
std::get<1>(dealt) += F{4};
std::get<2>(dealt) += F{4};
EXPECT_EQ(dpf::shamir::reconstruct(
std::get<0>(dealt), std::get<1>(dealt), std::get<2>(dealt)),
secret + F{4});
std::array<dpf::shamir::point_share<F>, 4> extra{{
{1, raw[0]}, {2, raw[1]}, {3, raw[2]}, {4, raw[3]}}};
EXPECT_EQ((dpf::shamir::reconstruct<F, 3, 5>(extra)), secret);
extra[3].value = extra[3].value + F{1};
EXPECT_THROW((dpf::shamir::reconstruct<F, 3, 5>(extra)), std::runtime_error);
const std::array<dpf::shamir::point_share<F>, 2> too_few{{
{1, raw[0]}, {2, raw[1]}}};
EXPECT_THROW((dpf::shamir::reconstruct<F, 3, 5>(too_few)),
std::invalid_argument);
const std::array<dpf::shamir::point_share<F>, 2> dup{{
{1, raw[0]}, {1, raw[0]}}};
EXPECT_THROW((dpf::shamir::reconstruct<F, 2, 5>(dup)), std::invalid_argument);
// Threshold 2 is not tied to three shareholders. p(x) = 8 + 5x.
const auto line = dpf::shamir::deal<F, 2, 4>(F{8}, std::array<F, 1>{{F{5}}});
EXPECT_EQ(std::get<0>(line).raw(), F{13});
EXPECT_EQ(std::get<3>(line).raw(), F{28});
EXPECT_EQ(dpf::shamir::reconstruct(std::get<1>(line), std::get<3>(line)), F{8});
// Threshold 1 copies the secret. Threshold N needs every share.
const auto copied = dpf::shamir::deal<F, 1, 3>(F{9}, std::array<F, 0>{});
EXPECT_EQ(std::get<0>(copied).raw(), F{9});
EXPECT_EQ(std::get<2>(copied).raw(), F{9});
EXPECT_EQ(dpf::shamir::reconstruct(std::get<1>(copied)), F{9});
const auto plain = dpf::shamir::share_secret<F, 1, 3>(F{6});
EXPECT_EQ(std::get<0>(plain).raw(), F{6});
EXPECT_EQ(std::get<2>(plain).raw(), F{6});
const std::array<F, 3> dense{{F{1}, F{0}, F{4}}};
const auto all = dpf::shamir::deal<F, 4, 4>(F{7}, dense);
EXPECT_EQ(dpf::shamir::reconstruct(std::get<0>(all), std::get<1>(all),
std::get<2>(all), std::get<3>(all)), F{7});
const std::array<dpf::shamir::point_share<F>, 3> missing{{
{1, std::get<0>(all).raw()},
{2, std::get<1>(all).raw()},
{3, std::get<2>(all).raw()}}};
EXPECT_THROW((dpf::shamir::reconstruct<F, 4, 4>(missing)),
std::invalid_argument);
const auto rnd = dpf::shamir::share_secret<F, 2, 3>(F{11});
static_assert(std::is_same_v<std::decay_t<decltype(std::get<0>(rnd))>,
dpf::shamir_share<F, 0>>);
EXPECT_EQ(dpf::reconstruct(std::get<0>(rnd), std::get<2>(rnd)), F{11});
const auto wider = dpf::shamir::share_secret<F, 3, 5>(F{12});
EXPECT_EQ(dpf::shamir::reconstruct(
std::get<0>(wider), std::get<2>(wider), std::get<4>(wider)), F{12});
}
} // namespace

1142
test/tests/security_test.cpp Normal file

File diff suppressed because it is too large Load diff

111
test/tests/sfss_test.cpp Normal file
View file

@ -0,0 +1,111 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <tuple>
#include <utility>
#include <vector>
#include "dpf.hpp"
namespace
{
template <typename K0, typename K1, typename InputT>
dpf::fp61 open_sdpf(const K0 & k0, const K1 & k1, InputT x,
const dpf::sfss_ct & ct)
{
return dpf::reconstruct(
dpf::subtractive_share<dpf::fp61, 0>::from_raw(
dpf::eval_sdpf(k0, x, ct)),
dpf::subtractive_share<dpf::fp61, 1>::from_raw(
dpf::eval_sdpf(k1, x, ct)));
}
} // namespace
TEST(Sfss, PointBetaOneOnAndOff)
{
using Input = std::uint8_t;
const Input alpha = 0x2a;
auto [k0, k1, ke, st] = dpf::make_sdpf(alpha);
EXPECT_EQ(ke.beta.raw(), 1u);
const dpf::fp61 m{42};
const auto ct = dpf::sfss_enc(st, ke, m);
EXPECT_EQ(ct.j, 1u);
EXPECT_EQ(st.ctr, 2u);
EXPECT_EQ(open_sdpf(k0, k1, alpha, ct), m);
EXPECT_EQ(open_sdpf(k0, k1, Input{0}, ct), dpf::fp61{0});
EXPECT_EQ(open_sdpf(k0, k1, static_cast<Input>(alpha ^ 1), ct), dpf::fp61{0});
}
TEST(Sfss, WeightedBeta)
{
using Input = std::uint16_t;
const Input alpha = 1000;
const dpf::fp61 beta{7};
auto [k0, k1, ke, st] = dpf::make_sdpf(alpha, beta);
EXPECT_EQ(ke.beta, beta);
const dpf::fp61 m{11};
const auto ct = dpf::sfss_enc(st, ke, m);
EXPECT_EQ(open_sdpf(k0, k1, alpha, ct), beta * m);
EXPECT_EQ(open_sdpf(k0, k1, Input{0}, ct), dpf::fp61{0});
}
TEST(Sfss, ManyStreamMessages)
{
using Input = std::uint8_t;
const Input alpha = 7;
auto [k0, k1, ke, st] = dpf::make_sdpf(alpha, dpf::fp61{3});
for (std::uint64_t i = 0; i < 20; ++i)
{
const dpf::fp61 m{i + 1};
const auto ct = dpf::sfss_enc(st, ke, m);
EXPECT_EQ(ct.j, i + 1);
EXPECT_EQ(open_sdpf(k0, k1, alpha, ct), ke.beta * m) << i;
EXPECT_EQ(open_sdpf(k0, k1, Input{8}, ct), dpf::fp61{0}) << i;
}
}
TEST(Sfss, WindowTelescopes)
{
using Input = std::uint8_t;
const Input alpha = 3;
auto [k0, k1, ke, st] = dpf::make_sdpf(alpha, dpf::fp61{5});
const std::uint64_t j_lo = st.ctr;
dpf::fp61 sum_m{};
dpf::fp61 c_agg{};
constexpr std::size_t window = 8;
for (std::size_t i = 0; i < window; ++i)
{
const dpf::fp61 m{static_cast<std::uint64_t>(i + 2)};
sum_m = sum_m + m;
const auto ct = dpf::sfss_enc_window(st, ke, m);
c_agg = c_agg + ct.c;
}
const std::uint64_t j_hi = j_lo + window - 1;
const auto s0 = dpf::eval_sdpf_window(k0, alpha, c_agg, j_lo, j_hi);
const auto s1 = dpf::eval_sdpf_window(k1, alpha, c_agg, j_lo, j_hi);
EXPECT_EQ(dpf::reconstruct(
dpf::subtractive_share<dpf::fp61, 0>::from_raw(s0),
dpf::subtractive_share<dpf::fp61, 1>::from_raw(s1)),
ke.beta * sum_m);
const auto z0 = dpf::eval_sdpf_window(k0, Input{4}, c_agg, j_lo, j_hi);
const auto z1 = dpf::eval_sdpf_window(k1, Input{4}, c_agg, j_lo, j_hi);
EXPECT_EQ(dpf::reconstruct(
dpf::subtractive_share<dpf::fp61, 0>::from_raw(z0),
dpf::subtractive_share<dpf::fp61, 1>::from_raw(z1)),
dpf::fp61{0});
}
TEST(Sfss, RejectZeroBeta)
{
EXPECT_THROW((void)dpf::make_sdpf(std::uint8_t{1}, dpf::fp61{0}),
std::invalid_argument);
}

View file

@ -0,0 +1,347 @@
/// @file shamir_adversarial_test.cpp
/// @brief Corruption and misuse checks for (K,N) Shamir sharing.
/// @details Exactly K shares accept any values, including a mislabeled point.
/// Each further share is checked against the polynomial of the first
/// K, wherever the bad share sits in that list. A full set of shares
/// of a different secret is consistent and opens that secret. A zero
/// leading coefficient drops the real threshold. (2,3) shamir3
/// rejects a party outside 1..3 and an inconsistent third share.
/// Threshold 1 hides nothing: every share is the secret. When K = N
/// there is no extra share, so a tampered full set is not detected.
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <cstdint>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include "dpf.hpp"
namespace
{
using F = dpf::fp61;
template <typename Tuple, std::size_t... I>
std::array<F, sizeof...(I)> raws(const Tuple & shares, std::index_sequence<I...>)
{
return {{std::get<I>(shares).raw()...}};
}
template <std::size_t N, typename Tuple>
std::array<F, N> raws(const Tuple & shares)
{
return raws(shares, std::make_index_sequence<N>{});
}
template <std::size_t N>
std::array<dpf::shamir::point_share<F>, N> points(const std::array<F, N> & value)
{
std::array<dpf::shamir::point_share<F>, N> out{};
for (std::size_t i = 0; i < N; ++i)
out[i] = {static_cast<int>(i + 1), value[i]};
return out;
}
} // namespace
TEST(ShamirAdversarial, ExactlyKAcceptsALie)
{
const std::array<F, 2> coeff{{F{2}, F{3}}};
const auto dealt = dpf::shamir::deal<F, 3, 5>(F{10}, coeff);
auto bad = std::get<0>(dealt);
bad += F{1};
const F opened = dpf::shamir::reconstruct(bad, std::get<1>(dealt), std::get<2>(dealt));
EXPECT_NE(opened, F{10});
}
TEST(ShamirAdversarial, ExtraShareCatchesALieInAnySlot)
{
const std::array<F, 2> coeff{{F{2}, F{3}}};
const auto dealt = dpf::shamir::deal<F, 3, 5>(F{10}, coeff);
const auto honest = points<5>(raws<5>(dealt));
EXPECT_EQ((dpf::shamir::reconstruct<F, 3, 5>(honest)), F{10});
for (std::size_t slot = 0; slot < 5; ++slot)
{
auto lied = honest;
lied[slot].value = lied[slot].value + F{1};
EXPECT_THROW((dpf::shamir::reconstruct<F, 3, 5>(lied)), std::runtime_error)
<< "slot " << slot;
// The same lie, moved to the front of the argument list.
std::array<dpf::shamir::point_share<F>, 5> front{};
front[0] = lied[slot];
std::size_t n = 1;
for (std::size_t i = 0; i < 5; ++i)
if (i != slot)
front[n++] = honest[i];
EXPECT_THROW((dpf::shamir::reconstruct<F, 3, 5>(front)), std::runtime_error)
<< "front slot " << slot;
}
}
TEST(ShamirAdversarial, ConsistentForgeryOpensTheForgedSecret)
{
const std::array<F, 2> coeff{{F{1}, F{4}}};
const auto forged = dpf::shamir::deal<F, 3, 5>(F{99}, coeff);
const auto held = points<5>(raws<5>(forged));
EXPECT_EQ((dpf::shamir::reconstruct<F, 3, 5>(held)), F{99});
const auto real = dpf::shamir::deal<F, 3, 5>(F{10}, std::array<F, 2>{{F{2}, F{3}}});
auto mixed = points<5>(raws<5>(real));
mixed[4] = held[4];
EXPECT_THROW((dpf::shamir::reconstruct<F, 3, 5>(mixed)), std::runtime_error);
}
TEST(ShamirAdversarial, OrderOfAConsistentSetDoesNotMatter)
{
const std::array<F, 2> coeff{{F{2}, F{3}}};
const auto dealt = dpf::shamir::deal<F, 3, 5>(F{10}, coeff);
const F values[5] = {
std::get<0>(dealt).raw(), std::get<1>(dealt).raw(),
std::get<2>(dealt).raw(), std::get<3>(dealt).raw(),
std::get<4>(dealt).raw()};
int idx[3] = {0, 2, 4};
do
{
const std::array<dpf::shamir::point_share<F>, 3> subset{{
{idx[0] + 1, values[idx[0]]},
{idx[1] + 1, values[idx[1]]},
{idx[2] + 1, values[idx[2]]}}};
EXPECT_EQ((dpf::shamir::reconstruct<F, 3, 5>(subset)), F{10});
}
while (std::next_permutation(idx, idx + 3));
const auto forward = points<5>(raws<5>(dealt));
std::array<dpf::shamir::point_share<F>, 5> backward{};
for (std::size_t i = 0; i < 5; ++i)
backward[i] = forward[4 - i];
EXPECT_EQ((dpf::shamir::reconstruct<F, 3, 5>(backward)), F{10});
}
TEST(ShamirAdversarial, WrongThresholdAndMislabeledPoint)
{
// p(x) = 8 + 5x + x^2. The line through p(1)=14 and p(2)=22 opens 6, not 8.
const auto steep = dpf::shamir::deal<F, 3, 5>(F{8}, std::array<F, 2>{{F{5}, F{1}}});
const std::array<dpf::shamir::point_share<F>, 2> line{{
{1, std::get<0>(steep).raw()}, {2, std::get<1>(steep).raw()}}};
EXPECT_EQ(std::get<0>(steep).raw(), F{14});
EXPECT_EQ(std::get<1>(steep).raw(), F{22});
EXPECT_EQ((dpf::shamir::reconstruct<F, 2, 5>(line)), F{6});
EXPECT_NE((dpf::shamir::reconstruct<F, 2, 5>(line)), F{8});
// A zero leading coefficient is only a (2,5) sharing.
const auto flat = dpf::shamir::deal<F, 3, 5>(F{8}, std::array<F, 2>{{F{5}, F{0}}});
const std::array<dpf::shamir::point_share<F>, 2> still_line{{
{1, std::get<0>(flat).raw()}, {4, std::get<3>(flat).raw()}}};
EXPECT_EQ((dpf::shamir::reconstruct<F, 2, 5>(still_line)), F{8});
auto [s0, s1, s2] = dpf::make_shamir_shares(F{20}, F{3});
const std::array<dpf::shamir::point_share<F>, 2> swapped{{
{1, s1.raw()}, {2, s0.raw()}}};
EXPECT_NE((dpf::shamir::reconstruct<F, 2, 3>(swapped)), F{20});
const std::array<dpf::shamir::point_share<F>, 3> swapped3{{
{1, s1.raw()}, {2, s0.raw()}, {3, s2.raw()}}};
EXPECT_THROW((dpf::shamir::reconstruct<F, 2, 3>(swapped3)), std::runtime_error);
}
TEST(ShamirAdversarial, OneShareDoesNotFixTheSecret)
{
// p(1) = 13 for both (secret, slope) = (10, 3) and (11, 2).
auto [a0, a1, a2] = dpf::make_shamir_shares(F{10}, F{3});
auto [b0, b1, b2] = dpf::make_shamir_shares(F{11}, F{2});
EXPECT_EQ(a0.raw(), b0.raw());
EXPECT_EQ(a0.raw(), F{13});
EXPECT_EQ((dpf::reconstruct(a0, a1)), F{10});
EXPECT_EQ((dpf::reconstruct(b0, b1)), F{11});
EXPECT_NE((dpf::reconstruct(a0, b1)), F{10});
(void)a2;
(void)b2;
}
TEST(ShamirAdversarial, RejectsCountPointsAndOneSidedEdit)
{
const auto dealt = dpf::shamir::deal<F, 3, 5>(F{10}, std::array<F, 2>{{F{2}, F{3}}});
const auto honest = points<5>(raws<5>(dealt));
const std::array<dpf::shamir::point_share<F>, 2> too_few{{honest[0], honest[1]}};
EXPECT_THROW((dpf::shamir::reconstruct<F, 3, 5>(too_few)), std::invalid_argument);
EXPECT_THROW((dpf::shamir::reconstruct<F, 3, 5>(honest.data(), 0)),
std::invalid_argument);
EXPECT_THROW((dpf::shamir::reconstruct<F, 3, 5>(honest.data(), 6)),
std::invalid_argument);
const std::array<dpf::shamir::point_share<F>, 3> dup{{
{1, honest[0].value}, {1, honest[0].value}, {3, honest[2].value}}};
EXPECT_THROW((dpf::shamir::reconstruct<F, 3, 5>(dup)), std::invalid_argument);
const std::array<dpf::shamir::point_share<F>, 3> zero_pt{{
{0, honest[0].value}, {2, honest[1].value}, {3, honest[2].value}}};
EXPECT_THROW((dpf::shamir::reconstruct<F, 3, 5>(zero_pt)), std::invalid_argument);
const std::array<dpf::shamir::point_share<F>, 3> negative{{
{-1, honest[0].value}, {2, honest[1].value}, {3, honest[2].value}}};
EXPECT_THROW((dpf::shamir::reconstruct<F, 3, 5>(negative)), std::invalid_argument);
const std::array<dpf::shamir::point_share<F>, 3> past{{
{1, honest[0].value}, {2, honest[1].value}, {6, honest[2].value}}};
EXPECT_THROW((dpf::shamir::reconstruct<F, 3, 5>(past)), std::invalid_argument);
auto one = std::get<0>(dealt);
one += F{4};
const F sided = dpf::shamir::reconstruct(one, std::get<1>(dealt), std::get<2>(dealt));
EXPECT_NE(sided, F{10});
auto with_extra = honest;
with_extra[0].value = one.raw();
EXPECT_THROW((dpf::shamir::reconstruct<F, 3, 5>(with_extra)), std::runtime_error);
auto all = dealt;
std::get<0>(all) += F{4};
std::get<1>(all) += F{4};
std::get<2>(all) += F{4};
std::get<3>(all) += F{4};
std::get<4>(all) += F{4};
EXPECT_EQ((dpf::shamir::reconstruct<F, 3, 5>(points<5>(raws<5>(all)))), F{14});
}
TEST(ShamirAdversarial, ThresholdOneHidesNothingAndFullThresholdNeedsEveryShare)
{
const auto copied = dpf::shamir::deal<F, 1, 4>(F{4}, std::array<F, 0>{});
EXPECT_EQ(std::get<0>(copied).raw(), F{4});
EXPECT_EQ(std::get<3>(copied).raw(), F{4});
auto lie = std::get<0>(copied);
lie += F{1};
EXPECT_EQ(dpf::shamir::reconstruct(lie), F{5});
auto all = points<4>(raws<4>(copied));
all[2].value = all[2].value + F{1};
EXPECT_THROW((dpf::shamir::reconstruct<F, 1, 4>(all)), std::runtime_error);
const auto quad = dpf::shamir::deal<F, 4, 4>(
F{9}, std::array<F, 3>{{F{1}, F{2}, F{3}}});
const F opened4 = dpf::shamir::reconstruct(std::get<0>(quad), std::get<1>(quad),
std::get<2>(quad), std::get<3>(quad));
EXPECT_EQ(opened4, F{9});
const std::array<dpf::shamir::point_share<F>, 3> missing{{
{1, std::get<0>(quad).raw()},
{2, std::get<1>(quad).raw()},
{3, std::get<2>(quad).raw()}}};
EXPECT_THROW((dpf::shamir::reconstruct<F, 4, 4>(missing)), std::invalid_argument);
// K = N, so the full set is exactly K shares and a lie is not detected.
auto bad = std::get<3>(quad);
bad += F{1};
const F lied = dpf::shamir::reconstruct(std::get<0>(quad), std::get<1>(quad),
std::get<2>(quad), bad);
EXPECT_NE(lied, F{9});
const auto room = dpf::shamir::deal<F, 3, 4>(F{9}, std::array<F, 2>{{F{1}, F{2}}});
auto bad_extra = std::get<3>(room);
bad_extra += F{1};
EXPECT_THROW((dpf::shamir::reconstruct(std::get<0>(room), std::get<1>(room),
std::get<2>(room), bad_extra)), std::runtime_error);
}
TEST(ShamirAdversarial, Shamir3RejectsBadPartiesAndMatchesTheGeneralOpen)
{
const auto s = dpf::shamir3::share_secret(F{42});
const F from_general = dpf::shamir::reconstruct<F, 2, 3>(
std::array<dpf::shamir::point_share<F>, 2>{{
{s[0].party, s[0].value}, {s[2].party, s[2].value}}});
const F from_shamir3 = dpf::shamir3::reconstruct(s[0], s[2]);
EXPECT_EQ(from_shamir3, from_general);
const F from_three = dpf::shamir3::reconstruct(s[0], s[1], s[2]);
EXPECT_EQ(from_three, F{42});
EXPECT_THROW((dpf::shamir3::reconstruct(
dpf::shamir3::share{0, F{1}}, dpf::shamir3::share{1, F{1}})),
std::invalid_argument);
EXPECT_THROW((dpf::shamir3::reconstruct(
dpf::shamir3::share{1, F{1}}, dpf::shamir3::share{4, F{1}})),
std::invalid_argument);
EXPECT_THROW((dpf::shamir3::reconstruct(
dpf::shamir3::share{2, F{1}}, dpf::shamir3::share{2, F{2}})),
std::invalid_argument);
EXPECT_THROW((dpf::shamir3::reconstruct(s[0], s[1], dpf::shamir3::share{4, s[2].value})),
std::invalid_argument);
auto bad = s[2];
bad.value = bad.value + F{1};
EXPECT_THROW((dpf::shamir3::reconstruct(s[0], s[1], bad)), std::runtime_error);
EXPECT_THROW((dpf::shamir::reconstruct<F, 2, 3>(
std::array<dpf::shamir::point_share<F>, 3>{{
{s[0].party, s[0].value},
{s[1].party, s[1].value},
{bad.party, bad.value}}})),
std::runtime_error);
EXPECT_THROW((dpf::shamir3::add(s[0], s[1])), std::invalid_argument);
const auto doubled = dpf::shamir3::add(s[0], s[0]);
EXPECT_EQ(doubled.value, s[0].value + s[0].value);
EXPECT_THROW((dpf::shamir3::typed_share<0>(s[1])), std::invalid_argument);
const auto typed = dpf::shamir3::typed_share<0>(s[0]);
EXPECT_EQ(typed.raw(), s[0].value);
auto [t0, t1, t2] = dpf::make_shamir_shares(F{42}, F{0});
t2 += F{1};
EXPECT_THROW((dpf::reconstruct(t0, t1, t2)), std::runtime_error);
EXPECT_THROW((dpf::shamir::reconstruct(t2, t0, t1)), std::runtime_error);
}
TEST(ShamirAdversarial, ZeroAndFieldEdge)
{
const auto zeros = dpf::shamir::deal<F, 3, 4>(F{0}, std::array<F, 2>{{F{0}, F{0}}});
EXPECT_EQ(std::get<0>(zeros).raw(), F{0});
EXPECT_EQ(std::get<3>(zeros).raw(), F{0});
const F opened0 = dpf::shamir::reconstruct(
std::get<1>(zeros), std::get<2>(zeros), std::get<3>(zeros));
EXPECT_EQ(opened0, F{0});
const F edge{dpf::fp61_mod - 1};
const auto high = dpf::shamir::deal<F, 2, 3>(edge, std::array<F, 1>{{edge}});
const F opened_edge = dpf::reconstruct(std::get<0>(high), std::get<2>(high));
EXPECT_EQ(opened_edge, edge);
static_assert(std::is_same_v<dpf::shamir::share<F, 0, 2, 3>, dpf::shamir_share<F, 0>>);
}
TEST(ShamirAdversarial, Gf2PointsMustFitAndLiesAreXor)
{
// GF(2^8) has room for points 1..5. A one-bit lie is invisible at K=3
// and caught when an honest extra share is present.
using G = dpf::gf28;
const auto dealt = dpf::shamir::deal<G, 3, 5>(
G{0x1b}, std::array<G, 2>{{G{2}, G{9}}});
auto bad = std::get<0>(dealt);
bad += G{1};
const G sided = dpf::shamir::reconstruct(bad, std::get<1>(dealt), std::get<2>(dealt));
EXPECT_NE(sided, G{0x1b});
auto extra = std::get<3>(dealt);
EXPECT_THROW((dpf::shamir::reconstruct(bad, std::get<1>(dealt), std::get<2>(dealt), extra)),
std::runtime_error);
// Adding a share to itself is XOR, so the opened secret is 0, not twice.
const G doubled = dpf::shamir::reconstruct(
std::get<0>(dealt) + std::get<0>(dealt),
std::get<1>(dealt) + std::get<1>(dealt),
std::get<2>(dealt) + std::get<2>(dealt));
EXPECT_EQ(doubled, G{0});
// GF(16) holds points 1..15. Point 16 is 0. Point 17 aliases point 1.
using H = dpf::gf24;
const auto fit = dpf::shamir::deal<H, 2, 15>(H{0xa}, std::array<H, 1>{{H{0x3}}});
EXPECT_EQ((dpf::shamir::reconstruct(std::get<0>(fit), std::get<14>(fit))), H{0xa});
const auto zero_pt = dpf::shamir::deal<H, 2, 16>(H{0xa}, std::array<H, 1>{{H{0x3}}});
// Point 16 is 0 in GF(16), so that share is p(0), the secret.
EXPECT_EQ(std::get<15>(zero_pt).raw(), H{0xa}.raw());
EXPECT_THROW((dpf::shamir::reconstruct(std::get<0>(zero_pt), std::get<15>(zero_pt))),
std::invalid_argument);
const auto alias = dpf::shamir::deal<H, 2, 17>(H{0xa}, std::array<H, 1>{{H{0x3}}});
EXPECT_EQ(std::get<0>(alias).raw(), std::get<16>(alias).raw());
EXPECT_THROW((dpf::shamir::reconstruct(std::get<0>(alias), std::get<16>(alias))),
std::invalid_argument);
// GF(4) holds a (2,3) sharing. Point 4 is 0.
const auto small = dpf::shamir::deal<dpf::gf22, 2, 4>(
dpf::gf22{1}, std::array<dpf::gf22, 1>{{dpf::gf22{2}}});
EXPECT_THROW((dpf::shamir::reconstruct(std::get<0>(small), std::get<3>(small))),
std::invalid_argument);
EXPECT_EQ((dpf::shamir::reconstruct(std::get<0>(small), std::get<2>(small))), dpf::gf22{1});
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,93 @@
#include <gtest/gtest.h>
#include <cstdint>
#include "aes_mmo_ref.hpp"
#include "aes_sbox_bp.hpp"
#include "dpf/doerner_shelat.hpp"
namespace
{
std::uint8_t sbox_circuit(std::uint8_t x)
{
std::uint8_t w[aes_bp::wire_count]{};
for (int i = 0; i < 8; ++i)
w[i] = static_cast<std::uint8_t>((x >> (7 - i)) & 1u);
for (std::size_t oi = 0; oi < aes_bp::op_count; ++oi)
{
const auto kind = aes_bp::ops[oi][0];
const auto dst = aes_bp::ops[oi][1];
const auto a = aes_bp::ops[oi][2];
const auto b = aes_bp::ops[oi][3];
if (kind == 0)
w[dst] = static_cast<std::uint8_t>(w[a] ^ w[b]);
else if (kind == 1)
w[dst] = static_cast<std::uint8_t>(w[a] & w[b]);
else
w[dst] = static_cast<std::uint8_t>(w[a] ^ w[b] ^ 1u);
}
std::uint8_t out = 0;
for (int i = 0; i < 8; ++i)
out = static_cast<std::uint8_t>(
out | (w[aes_bp::out_wire[static_cast<std::size_t>(i)]] << (7 - i)));
return out;
}
} // namespace
TEST(SharedAnd, BoyarPeraltaMatchesAesSbox)
{
int ands = 0;
for (std::size_t oi = 0; oi < aes_bp::op_count; ++oi)
if (aes_bp::ops[oi][0] == 1)
++ands;
EXPECT_EQ(ands, static_cast<int>(aes_bp::and_count));
for (int x = 0; x < 256; ++x)
{
const auto byte = static_cast<std::uint8_t>(x);
EXPECT_EQ(sbox_circuit(byte), dpf::party::aes_ref::sbox_at(byte))
<< "byte " << x;
}
}
TEST(SharedAnd, PublicProductTermIsAddedOnce)
{
// Every XOR-share of a bit AND. The opened masks are `d = x XOR a` and
// `e = y XOR b`. Party 0 alone adds the public `d AND e`. Adding it on
// both parties cancels that term and the product is wrong whenever it
// is 1.
int cancelled = 0;
for (unsigned bits = 0; bits < 256; ++bits)
{
const std::uint8_t a = static_cast<std::uint8_t>(bits & 1u);
const std::uint8_t b = static_cast<std::uint8_t>((bits >> 1) & 1u);
const std::uint8_t x = static_cast<std::uint8_t>((bits >> 2) & 1u);
const std::uint8_t y = static_cast<std::uint8_t>((bits >> 3) & 1u);
const std::uint8_t a0 = static_cast<std::uint8_t>((bits >> 4) & 1u);
const std::uint8_t b0 = static_cast<std::uint8_t>((bits >> 5) & 1u);
const std::uint8_t x0 = static_cast<std::uint8_t>((bits >> 6) & 1u);
const std::uint8_t y0 = static_cast<std::uint8_t>((bits >> 7) & 1u);
const std::uint8_t c = static_cast<std::uint8_t>(a & b);
const std::uint8_t c0 = static_cast<std::uint8_t>((a0 & b0) ^ ((bits * 3u) & 1u));
const std::uint8_t a1 = static_cast<std::uint8_t>(a0 ^ a);
const std::uint8_t b1 = static_cast<std::uint8_t>(b0 ^ b);
const std::uint8_t c1 = static_cast<std::uint8_t>(c0 ^ c);
const std::uint8_t x1 = static_cast<std::uint8_t>(x0 ^ x);
const std::uint8_t y1 = static_cast<std::uint8_t>(y0 ^ y);
const std::uint8_t d = static_cast<std::uint8_t>((x0 ^ a0) ^ (x1 ^ a1));
const std::uint8_t e = static_cast<std::uint8_t>((y0 ^ b0) ^ (y1 ^ b1));
const std::uint8_t z0 = dpf::detail::ds_bit_and_party(d, e, a0, b0, c0, true);
const std::uint8_t z1 = dpf::detail::ds_bit_and_party(d, e, a1, b1, c1, false);
EXPECT_EQ(static_cast<std::uint8_t>(z0 ^ z1), static_cast<std::uint8_t>(x & y));
const std::uint8_t both = static_cast<std::uint8_t>(
dpf::detail::ds_bit_and_party(d, e, a0, b0, c0, true)
^ dpf::detail::ds_bit_and_party(d, e, a1, b1, c1, true));
if ((d & e) != 0)
{
EXPECT_NE(both, static_cast<std::uint8_t>(x & y));
++cancelled;
}
}
EXPECT_GT(cancelled, 0);
}

View file

@ -0,0 +1,346 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <cstring>
#include <numeric>
#include <vector>
#include "dpf/compose.hpp"
#include "dpf/shuffle.hpp"
namespace
{
template <typename T>
std::vector<T> replay(const std::vector<T> & v, const dpf::rss::seed_bundle & bundle,
std::uint64_t index, unsigned passes)
{
const dpf::rss::seed_block seeds[3] = {bundle.k01, bundle.k12, bundle.k20};
auto out = v;
for (unsigned p = 0; p < passes; ++p)
{
out = dpf::shuffle::permute(out,
dpf::shuffle::permutation_from_seed(seeds[p], v.size(), index));
}
return out;
}
template <typename T>
void deal(const std::vector<T> & clear, dpf::shuffle::shuffle_party_view<T> held[3])
{
const std::size_t n = clear.size();
for (unsigned p = 0; p < 3; ++p)
{
held[p].own.assign(n, T{});
held[p].next.assign(n, T{});
}
for (std::size_t i = 0; i < n; ++i)
{
const T a = dpf::uniform_sample<T>();
const T b = dpf::uniform_sample<T>();
const T c = static_cast<T>(clear[i] - a - b);
held[0].own[i] = a;
held[0].next[i] = b;
held[1].own[i] = b;
held[1].next[i] = c;
held[2].own[i] = c;
held[2].next[i] = a;
}
}
template <typename T>
std::vector<T> open_owns(const dpf::shuffle::shuffle_party_view<T> held[3])
{
std::vector<T> out(held[0].own.size());
for (std::size_t i = 0; i < out.size(); ++i)
out[i] = static_cast<T>(held[0].own[i] + held[1].own[i] + held[2].own[i]);
return out;
}
template <typename T>
void run_passes(dpf::shuffle::shuffle_party_view<T> held[3],
const dpf::rss::seed_bundle & bundle, std::uint64_t index, unsigned passes)
{
const unsigned order[3] = {2u, 0u, 1u};
for (unsigned step = 0; step < passes; ++step)
{
const unsigned left = order[step];
const unsigned u = dpf::shuffle::hidden_u_party(left);
const unsigned side = dpf::shuffle::hidden_side_party(left);
auto u_step = dpf::shuffle::shuffle_hidden_pass<T>(
u, dpf::rss::party_seeds::from_bundle(bundle, u), held[u], index, left,
nullptr);
auto s_step = dpf::shuffle::shuffle_hidden_pass<T>(
side, dpf::rss::party_seeds::from_bundle(bundle, side), held[side],
index, left, &u_step.out.data);
auto l_step = dpf::shuffle::shuffle_hidden_pass<T>(
left, dpf::rss::party_seeds::from_bundle(bundle, left), held[left],
index, left, &s_step.out.data);
EXPECT_EQ(u_step.out.data.size(), held[u].own.size());
EXPECT_EQ(s_step.out.to, left);
EXPECT_EQ(held[u].own.size(), l_step.view.own.size());
held[u] = std::move(u_step.view);
held[side] = std::move(s_step.view);
held[left] = std::move(l_step.view);
ASSERT_EQ(held[0].next, held[1].own);
ASSERT_EQ(held[1].next, held[2].own);
ASSERT_EQ(held[2].next, held[0].own);
}
}
} // namespace
TEST(ShuffleHidden, ThreePassesMatchTheSeedPermutations)
{
auto bundle = dpf::rss::sample_seed_bundle();
std::vector<std::uint64_t> v(8);
std::iota(v.begin(), v.end(), 0);
const std::uint64_t index = 4;
auto opened = dpf::shuffle::shuffle_hidden_triple(v, bundle, index);
auto expect = dpf::shuffle::permute(v,
dpf::shuffle::permutation_from_seed(bundle.k01, v.size(), index));
expect = dpf::shuffle::permute(expect,
dpf::shuffle::permutation_from_seed(bundle.k12, v.size(), index));
expect = dpf::shuffle::permute(expect,
dpf::shuffle::permutation_from_seed(bundle.k20, v.size(), index));
EXPECT_EQ(opened, expect);
EXPECT_TRUE(dpf::shuffle::is_permutation_of(v, opened));
}
TEST(ShuffleHidden, LeftOutPartyUsesOnlyItsTwoSeeds)
{
auto bundle = dpf::rss::sample_seed_bundle();
const std::size_t n = 4;
dpf::shuffle::shuffle_party_view<std::uint64_t> in{
std::vector<std::uint64_t>(n, 1),
std::vector<std::uint64_t>(n, 2),
};
auto seeds = dpf::rss::party_seeds::from_bundle(bundle, 2);
std::vector<std::uint64_t> inbound(n, 9);
auto step = dpf::shuffle::shuffle_hidden_pass<std::uint64_t>(
2, seeds, in, 0, 2, &inbound);
EXPECT_FALSE(step.out.sends);
EXPECT_EQ(step.view.own, inbound);
EXPECT_EQ(step.view.next.size(), n);
}
TEST(ShuffleHidden, ReplicationHoldsAfterEachPass)
{
auto bundle = dpf::rss::sample_seed_bundle();
const std::size_t n = 5;
dpf::shuffle::shuffle_party_view<std::uint64_t> held[3];
for (unsigned p = 0; p < 3; ++p)
{
held[p].own.assign(n, 0);
held[p].next.assign(n, 0);
}
for (std::size_t i = 0; i < n; ++i)
{
const auto a = dpf::uniform_sample<std::uint64_t>();
const auto b = dpf::uniform_sample<std::uint64_t>();
held[0].own[i] = a;
held[0].next[i] = b;
held[1].own[i] = b;
held[1].next[i] = static_cast<std::uint64_t>(i - a - b);
held[2].own[i] = held[1].next[i];
held[2].next[i] = a;
}
for (unsigned left : {2u, 0u, 1u})
{
const unsigned u = dpf::shuffle::hidden_u_party(left);
const unsigned side = dpf::shuffle::hidden_side_party(left);
auto u_step = dpf::shuffle::shuffle_hidden_pass<std::uint64_t>(
u, dpf::rss::party_seeds::from_bundle(bundle, u), held[u], 1, left,
nullptr);
auto s_step = dpf::shuffle::shuffle_hidden_pass<std::uint64_t>(
side, dpf::rss::party_seeds::from_bundle(bundle, side), held[side],
1, left, &u_step.out.data);
auto l_step = dpf::shuffle::shuffle_hidden_pass<std::uint64_t>(
left, dpf::rss::party_seeds::from_bundle(bundle, left), held[left],
1, left, &s_step.out.data);
EXPECT_TRUE(u_step.out.sends);
EXPECT_EQ(u_step.out.to, side);
EXPECT_TRUE(s_step.out.sends);
EXPECT_EQ(s_step.out.to, left);
EXPECT_FALSE(l_step.out.sends);
held[u] = std::move(u_step.view);
held[side] = std::move(s_step.view);
held[left] = std::move(l_step.view);
EXPECT_EQ(held[0].next, held[1].own);
EXPECT_EQ(held[1].next, held[2].own);
EXPECT_EQ(held[2].next, held[0].own);
}
}
TEST(ShuffleHidden, ComposerRecordsThreeSends)
{
dpf::protocol::composer c;
auto out = c.shuffle_hidden(8, sizeof(std::uint64_t));
auto plan = c.schedule();
EXPECT_EQ(plan.rounds(), 3u);
std::vector<std::uint32_t> left_out;
for (std::size_t w = 0; w < plan.waves(); ++w)
{
for (auto ex : plan.wave(w).exchanges)
{
EXPECT_EQ(plan.opcode_of(ex.id), dpf::protocol::opcodes::shuffle_send);
left_out.push_back(plan.aux_of(ex.id));
EXPECT_EQ(dpf::protocol::detail::exchange_channel(plan, ex.id),
dpf::protocol::edge_channel::rss_next);
}
}
EXPECT_EQ(left_out, (std::vector<std::uint32_t>{2u, 0u, 1u}));
EXPECT_EQ(plan.value_bytes_of(out.id), 8u * sizeof(std::uint64_t));
std::size_t prev = 0;
bool seen = false;
for (std::size_t w = 0; w < plan.waves(); ++w)
{
if (plan.wave(w).exchanges.empty())
continue;
if (seen)
EXPECT_GT(w, prev);
prev = w;
seen = true;
}
}
TEST(ShuffleHidden, EachPassMatchesOneSeedPermutation)
{
auto bundle = dpf::rss::sample_seed_bundle();
std::vector<std::uint64_t> v(9);
for (std::size_t i = 0; i < v.size(); ++i)
v[i] = 1000u + static_cast<std::uint64_t>(i * 17u);
dpf::shuffle::shuffle_party_view<std::uint64_t> held[3];
deal(v, held);
for (unsigned passes = 1; passes <= 3; ++passes)
{
deal(v, held);
run_passes(held, bundle, 9, passes);
EXPECT_EQ(open_owns(held), replay(v, bundle, 9, passes));
}
}
TEST(ShuffleHidden, LengthsRingsAndHighBits)
{
auto bundle = dpf::rss::sample_seed_bundle();
for (std::size_t n : {0u, 1u, 2u, 3u, 7u, 16u, 64u})
{
std::vector<std::uint64_t> v(n);
for (std::size_t i = 0; i < n; ++i)
v[i] = (i * 0x9E3779B97F4A7C15ull) ^ 0x8000000000000000ull;
auto opened = dpf::shuffle::shuffle_hidden_triple(v, bundle, 3);
EXPECT_EQ(opened, replay(v, bundle, 3, 3));
if (n == 1)
EXPECT_EQ(opened, v);
}
std::vector<std::uint8_t> bytes{0, 255, 1, 128, 7, 7, 255};
EXPECT_EQ(dpf::shuffle::shuffle_hidden_triple(bytes, bundle, 2),
replay(bytes, bundle, 2, 3));
std::vector<std::uint32_t> words{0u, 0xffffffffu, 1u, 0x80000000u};
EXPECT_EQ(dpf::shuffle::shuffle_hidden_triple(words, bundle, 5),
replay(words, bundle, 5, 3));
std::vector<std::uint64_t> same(12, 42);
EXPECT_EQ(dpf::shuffle::shuffle_hidden_triple(same, bundle, 1), same);
}
TEST(ShuffleHidden, ReplayIsStableAndIndexChangesTheOrder)
{
auto bundle = dpf::rss::sample_seed_bundle();
std::vector<std::uint64_t> v(16);
std::iota(v.begin(), v.end(), 0);
auto a = dpf::shuffle::shuffle_hidden_triple(v, bundle, 0);
auto again = dpf::shuffle::shuffle_hidden_triple(v, bundle, 0);
auto other = dpf::shuffle::shuffle_hidden_triple(v, bundle, 99);
EXPECT_EQ(a, again);
EXPECT_EQ(a, replay(v, bundle, 0, 3));
EXPECT_NE(a, other);
}
TEST(ShuffleHidden, UPartyIgnoresInboundAndABadMessageBreaksTheOpen)
{
auto bundle = dpf::rss::sample_seed_bundle();
std::vector<std::uint64_t> v(8);
std::iota(v.begin(), v.end(), 3);
dpf::shuffle::shuffle_party_view<std::uint64_t> held[3];
deal(v, held);
const unsigned left = 2;
const unsigned u = dpf::shuffle::hidden_u_party(left);
const unsigned side = dpf::shuffle::hidden_side_party(left);
std::vector<std::uint64_t> junk(v.size(), 123);
auto ignored = dpf::shuffle::shuffle_hidden_pass<std::uint64_t>(
u, dpf::rss::party_seeds::from_bundle(bundle, u), held[u], 0, left, &junk);
auto clean = dpf::shuffle::shuffle_hidden_pass<std::uint64_t>(
u, dpf::rss::party_seeds::from_bundle(bundle, u), held[u], 0, left, nullptr);
EXPECT_EQ(ignored.view.own, clean.view.own);
EXPECT_EQ(ignored.view.next, clean.view.next);
junk[0] ^= 1u;
auto bad = dpf::shuffle::shuffle_hidden_pass<std::uint64_t>(
side, dpf::rss::party_seeds::from_bundle(bundle, side), held[side], 0,
left, &junk);
auto good = dpf::shuffle::shuffle_hidden_pass<std::uint64_t>(
side, dpf::rss::party_seeds::from_bundle(bundle, side), held[side], 0,
left, &clean.out.data);
EXPECT_NE(bad.view.own, good.view.own);
}
TEST(ShuffleHidden, LeftOutMaskMatchesAndMissesThePermutationSeed)
{
auto bundle = dpf::rss::sample_seed_bundle();
auto party2 = dpf::rss::party_seeds::from_bundle(bundle, 2);
EXPECT_EQ(std::memcmp(&party2.with_prev, &bundle.k12, sizeof(bundle.k12)), 0);
EXPECT_EQ(std::memcmp(&party2.with_next, &bundle.k20, sizeof(bundle.k20)), 0);
EXPECT_NE(std::memcmp(&party2.with_prev, &bundle.k01, sizeof(bundle.k01)), 0);
EXPECT_NE(std::memcmp(&party2.with_next, &bundle.k01, sizeof(bundle.k01)), 0);
const auto pi_known = dpf::shuffle::permutation_from_seed(bundle.k01, 8, 0);
const auto pi_prev = dpf::shuffle::permutation_from_seed(party2.with_prev, 8, 0);
const auto pi_next = dpf::shuffle::permutation_from_seed(party2.with_next, 8, 0);
EXPECT_NE(pi_known, pi_prev);
EXPECT_NE(pi_known, pi_next);
}
TEST(ShuffleHidden, RejectsBadPartiesAndShortMessages)
{
auto bundle = dpf::rss::sample_seed_bundle();
dpf::shuffle::shuffle_party_view<std::uint64_t> in{
{1, 2},
{3, 4},
};
auto seeds = dpf::rss::party_seeds::from_bundle(bundle, 0);
EXPECT_THROW(
dpf::shuffle::shuffle_hidden_pass<std::uint64_t>(1, seeds, in, 0, 2, nullptr),
std::invalid_argument);
EXPECT_THROW(
dpf::shuffle::shuffle_hidden_pass<std::uint64_t>(0, seeds, in, 0, 3, nullptr),
std::invalid_argument);
in.next.pop_back();
EXPECT_THROW(
dpf::shuffle::shuffle_hidden_pass<std::uint64_t>(0, seeds, in, 0, 2, nullptr),
std::invalid_argument);
in.next.push_back(4);
auto side = dpf::rss::party_seeds::from_bundle(bundle, 1);
EXPECT_THROW(
dpf::shuffle::shuffle_hidden_pass<std::uint64_t>(1, side, in, 0, 2, nullptr),
std::invalid_argument);
std::vector<std::uint64_t> short_msg{1};
EXPECT_THROW(dpf::shuffle::shuffle_hidden_pass<std::uint64_t>(
1, side, in, 0, 2, &short_msg),
std::invalid_argument);
EXPECT_THROW(dpf::shuffle::permute(std::vector<std::uint64_t>{1},
std::vector<std::size_t>{}),
std::invalid_argument);
}
TEST(ShuffleHidden, PublicShuffleStillFollowsK01)
{
auto bundle = dpf::rss::sample_seed_bundle();
std::vector<std::uint64_t> v{4, 9, 1, 7, 3};
EXPECT_EQ(dpf::shuffle::shuffle_party_triple(v, bundle, 6),
dpf::shuffle::shuffle_clear(v, bundle, 6));
}
TEST(ShuffleHidden, ComposerRejectsAnEmptyColumn)
{
dpf::protocol::composer c;
EXPECT_THROW(c.shuffle_hidden(0, 8), std::invalid_argument);
EXPECT_THROW(c.shuffle_hidden(4, 0), std::invalid_argument);
}

View file

@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <tuple>
#include "dpf.hpp"
#include "grotto/constant_lut.hpp"

View file

@ -10,6 +10,7 @@
// incremental_test.cpp.
#include <gtest/gtest.h>
#include <tuple>
#include "dpf.hpp"
#include "grotto/fixedpoint.hpp"
@ -242,6 +243,10 @@ void assign_wildcard_leaf_local(Key0 & k0, Key1 & k1, const ShareT & shr0,
{
auto & w0 = std::get<I>(k0.leaf_nodes);
auto & w1 = std::get<I>(k1.leaf_nodes);
if (w0.is_ready())
w0.begin_update();
if (w1.is_ready())
w1.begin_update();
const auto b0 = w0.compute_and_get_blinded_output_share(shr0);
const auto b1 = w1.compute_and_get_blinded_output_share(shr1);
const auto l0 = w0.compute_and_get_leaf_share(b1);
@ -552,7 +557,7 @@ TEST_F(StressScenariosTest, MlWildcardEvalPointThrowsBeforeAssign)
auto [k0, k1] = dpf::make_dpf(x,
dpf::at<8>(uint8_t{7}), dpf::at<12>(wc), uint16_t{1});
// Slot 1 is an unassigned wildcard: evaluating it must throw.
EXPECT_ANY_THROW((void)dpf::eval_point(dpf::out<1, 12>, k0, x));
EXPECT_ANY_THROW(dpf::eval_point(dpf::out<1, 12>, k0, x));
(void)k1;
}
@ -2038,7 +2043,7 @@ TEST_F(StressScenariosTest, MlPackedSmallWildcardAtNonTerminalAssignAll)
EXPECT_NE(KT::meta[0].group_id, KT::meta[8].group_id);
// Unassigned packed wildcards throw; deepest concrete still works.
EXPECT_ANY_THROW((void)dpf::eval_point(dpf::out<0, 12>, k0, x));
EXPECT_ANY_THROW(dpf::eval_point(dpf::out<0, 12>, k0, x));
EXPECT_EQ(recon(*dpf::eval_point(dpf::out<8>, k0, x),
*dpf::eval_point(dpf::out<8>, k1, x)),
uint32_t{999});
@ -2248,8 +2253,8 @@ TEST_F(StressScenariosTest, PrgCounterWrapperAesClassicEvalIncrements)
const auto after_gen = prg::count();
EXPECT_GT(after_gen, before);
(void)dpf::eval_point(k0, x);
(void)dpf::eval_full(k0);
dpf::eval_point(k0, x);
dpf::eval_full(k0);
const auto after_eval = prg::count();
EXPECT_GT(after_eval, after_gen);
@ -2271,8 +2276,8 @@ TEST_F(StressScenariosTest, PrgCounterWrapperLowmcMultilevelEvalIncrements)
const auto bi2 = interior::count();
const auto be2 = exterior::count();
(void)dpf::eval_point(dpf::out<0, 8>, k0, x);
(void)dpf::eval_full(dpf::out<1, 16>, k0);
dpf::eval_point(dpf::out<0, 8>, k0, x);
dpf::eval_full(dpf::out<1, 16>, k0);
EXPECT_GT(interior::count(), bi2);
EXPECT_GT(exterior::count(), be2);
@ -2302,3 +2307,80 @@ TEST_F(StressScenariosTest, PrgLowmcBothSidesPackedInterval)
*dpf::eval_point(dpf::out<4>, k1, x)), uint32_t{55});
}
TEST_F(StressScenariosTest, PairedInnerProductLeafAndAncestor)
{
using In = std::uint8_t;
auto [k0, k1] = dpf::make_dpf(In{9}, std::uint32_t{7}, std::uint32_t{11});
std::vector<std::array<std::uint32_t, 2>> rows;
std::uint64_t expect = 0;
for (In x = 4;; ++x)
{
const auto y0 = recon(*dpf::eval_point<0>(k0, x), *dpf::eval_point<0>(k1, x));
const auto y1 = recon(*dpf::eval_point<1>(k0, x), *dpf::eval_point<1>(k1, x));
const std::uint32_t w0 = x * 3u + 1u;
const std::uint32_t w1 = x * 5u + 2u;
rows.push_back({w0, w1});
expect += static_cast<std::uint64_t>(y0) * w0
+ static_cast<std::uint64_t>(y1) * w1;
if (x == 12)
break;
}
const auto a = dpf::eval_inner_product<0, 1>(dpf::paired, k0, In{4}, In{12}, rows);
const auto b = dpf::eval_inner_product<0, 1>(dpf::paired, k1, In{4}, In{12}, rows);
EXPECT_EQ(recon(a, b), expect);
const std::vector<In> pts{2, 9, 40};
const std::vector<std::array<std::uint64_t, 2>> seq_rows{{1, 2}, {3, 4}, {5, 6}};
std::uint64_t seq_expect = 0;
for (std::size_t i = 0; i < pts.size(); ++i)
{
const auto y0 = recon(*dpf::eval_point<0>(k0, pts[i]),
*dpf::eval_point<0>(k1, pts[i]));
const auto y1 = recon(*dpf::eval_point<1>(k0, pts[i]),
*dpf::eval_point<1>(k1, pts[i]));
seq_expect += static_cast<std::uint64_t>(y0) * seq_rows[i][0]
+ static_cast<std::uint64_t>(y1) * seq_rows[i][1];
}
EXPECT_EQ(recon(
dpf::eval_sequence_inner_product<0, 1>(k0, pts.begin(), pts.end(), seq_rows),
dpf::eval_sequence_inner_product<0, 1>(k1, pts.begin(), pts.end(), seq_rows)),
seq_expect);
const auto recipe = dpf::make_sequence_recipe<decltype(k0)>(pts.begin(), pts.end());
EXPECT_EQ(recon(
dpf::eval_sequence_inner_product<0, 1>(k0, recipe, pts.begin(), pts.end(), seq_rows),
dpf::eval_sequence_inner_product<0, 1>(k1, recipe, pts.begin(), pts.end(), seq_rows)),
seq_expect);
auto [a0, a1] = dpf::make_dpf(In{0x2a}, dpf::at<4>(std::uint8_t{5}), std::uint8_t{9});
const std::vector<In> ap{0x20, 0x2a, 0x2b, 0x30};
const std::vector<std::array<std::uint32_t, 2>> ar{{1, 1}, {2, 3}, {4, 5}, {6, 7}};
std::uint64_t anc = 0;
for (std::size_t i = 0; i < ap.size(); ++i)
{
const auto y0 = recon(*dpf::eval_point(dpf::out<0>, a0, ap[i]),
*dpf::eval_point(dpf::out<0>, a1, ap[i]));
const auto y1 = recon(*dpf::eval_point(dpf::out<1>, a0, ap[i]),
*dpf::eval_point(dpf::out<1>, a1, ap[i]));
anc += static_cast<std::uint64_t>(y0) * ar[i][0]
+ static_cast<std::uint64_t>(y1) * ar[i][1];
}
EXPECT_EQ(recon(
dpf::eval_sequence_inner_product<0, 1>(a0, ap.begin(), ap.end(), ar),
dpf::eval_sequence_inner_product<0, 1>(a1, ap.begin(), ap.end(), ar)),
anc);
std::vector<std::uint64_t> full(256);
std::uint64_t full_expect = 0;
for (unsigned x = 0; x < 256; ++x)
{
full[x] = (x * 3u + 1u) & 0x1fu;
const auto y = recon(*dpf::eval_point(k0, static_cast<In>(x)),
*dpf::eval_point(k1, static_cast<In>(x)));
full_expect += static_cast<std::uint64_t>(y) * full[x];
}
EXPECT_EQ(recon(
dpf::eval_full_inner_product<0>(dpf::paired, k0, full),
dpf::eval_full_inner_product<0>(dpf::paired, k1, full)),
full_expect);
}

View file

@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <tuple>
#include <array>
#include <cstdint>
@ -248,7 +249,7 @@ TEST(TypeTraits, IntegralSelectionAndBuiltinWidths)
EXPECT_TRUE(dpf::utils::has_operators_plus_minus_v<uint32_t>);
EXPECT_TRUE(dpf::utils::has_operators_plus_minus_v<float>);
EXPECT_FALSE(dpf::utils::has_characteristic_two_v<uint32_t>);
EXPECT_FALSE(dpf::utils::has_characteristic_two_v<dpf::bit>);
EXPECT_TRUE(dpf::utils::has_characteristic_two_v<dpf::bit>);
EXPECT_FALSE(dpf::utils::is_xor_wrapper_v<uint32_t>);
EXPECT_FALSE(dpf::is_wildcard_v<uint32_t>);
}
@ -293,6 +294,27 @@ TEST(TypeTraits, BoundaryWidthsAndOutputPacking)
EXPECT_EQ((dpf::outputs_per_leaf_v<uint8_t, simde__m128i>), 16u);
EXPECT_EQ((dpf::outputs_per_leaf_v<uint32_t, simde__m128i>), 4u);
EXPECT_EQ((dpf::outputs_per_leaf_v<dpf::bit, simde__m128i>), 128u);
EXPECT_EQ((dpf::utils::bitlength_of_output_v<dpf::gf2, simde__m128i>), 1u);
EXPECT_EQ((dpf::utils::bitlength_of_output_v<dpf::gf22, simde__m128i>), 2u);
EXPECT_EQ((dpf::utils::bitlength_of_output_v<dpf::gf24, simde__m128i>), 4u);
EXPECT_EQ((dpf::utils::bitlength_of_output_v<dpf::gf28, simde__m128i>), 8u);
EXPECT_EQ((dpf::utils::bitlength_of_output_v<dpf::gf216, simde__m128i>), 16u);
EXPECT_EQ((dpf::utils::bitlength_of_output_v<dpf::gf232, simde__m128i>), 32u);
EXPECT_EQ((dpf::utils::bitlength_of_output_v<dpf::gf264, simde__m128i>), 64u);
EXPECT_EQ((dpf::outputs_per_leaf_v<dpf::gf2, simde__m128i>), 128u);
EXPECT_EQ((dpf::outputs_per_leaf_v<dpf::gf22, simde__m128i>), 64u);
EXPECT_EQ((dpf::outputs_per_leaf_v<dpf::gf24, simde__m128i>), 32u);
EXPECT_EQ((dpf::outputs_per_leaf_v<dpf::gf28, simde__m128i>), 16u);
EXPECT_EQ((dpf::outputs_per_leaf_v<dpf::gf216, simde__m128i>), 8u);
EXPECT_EQ((dpf::outputs_per_leaf_v<dpf::gf232, simde__m128i>), 4u);
EXPECT_EQ((dpf::outputs_per_leaf_v<dpf::gf264, simde__m128i>), 2u);
EXPECT_EQ((dpf::outputs_per_leaf_v<dpf::gf2, simde__m256i>), 256u);
EXPECT_EQ((dpf::outputs_per_leaf_v<dpf::gf264, simde__m256i>), 4u);
EXPECT_TRUE(dpf::utils::is_packed_subbyte_v<dpf::gf2>);
EXPECT_TRUE(dpf::utils::is_packed_subbyte_v<dpf::gf24>);
EXPECT_FALSE(dpf::utils::is_packed_subbyte_v<dpf::gf28>);
EXPECT_TRUE(dpf::utils::has_characteristic_two_v<dpf::gf264>);
EXPECT_EQ((dpf::block_length_of_leaf_v<dpf::gf264, simde__m128i>), 1u);
EXPECT_EQ((dpf::outputs_per_leaf_v<dpf::modint<10>, simde__m128i>), 8u);
EXPECT_EQ((dpf::block_length_of_leaf_v<uint32_t, simde__m128i>), 1u);
EXPECT_EQ((dpf::block_length_of_leaf_v<uint256_t, simde__m128i>), 2u);

View file

@ -0,0 +1,248 @@
/// @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 <gtest/gtest.h>
#include <array>
#include <cstdint>
#include <vector>
#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<long long>(n),
static_cast<long long>(n * 9 + 3));
n += 2;
return v;
}
void fill(void * p, std::size_t nbytes)
{
auto * b = static_cast<unsigned char *>(p);
for (std::size_t i = 0; i < nbytes; ++i)
b[i] = static_cast<unsigned char>(n + i * 17);
n += nbytes;
}
std::uint8_t bit() { return static_cast<std::uint8_t>(n++ & 1u); }
};
dpf::ds_randomness<simde__m128i (*)(), Pad> tape()
{
HEDLEY_PRAGMA(GCC diagnostic push)
HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes")
return {dpf::uniform_sample<simde__m128i>, Pad{}};
HEDLEY_PRAGMA(GCC diagnostic pop)
}
template <typename Y0, typename Y1, typename Want>
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<Want>(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<Input>(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<Ht, Ht>(alpha, beta, dpf::verifiable{});
EXPECT_TRUE(decltype(k0)::tree::is_half_tree);
for (int x = 0; x < 256; ++x)
{
const Input q = static_cast<Input>(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<Input>(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<dpf::fp61, 256> s0{}, s1{}, r{};
for (int x = 0; x < 256; ++x)
{
const Input q = static_cast<Input>(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<std::size_t>(x)] = y0.raw();
s1[static_cast<std::size_t>(x)] = y1.raw();
r[static_cast<std::size_t>(x)] = dpf::fp61{static_cast<std::uint64_t>(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<typename std::decay_t<decltype(k0)>::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<Input>(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<typename std::decay_t<decltype(h0)>::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<Input>(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<Input> 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<Input>(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<Input>(q) == alpha ? y : 0ull;
EXPECT_EQ(static_cast<std::uint64_t>(g.party0[static_cast<std::size_t>(q)]
- g.party1[static_cast<std::size_t>(q)]),
want) << q;
if (g.party0[static_cast<std::size_t>(q)] != g.party1[static_cast<std::size_t>(q)])
{
EXPECT_NE(static_cast<std::uint64_t>(g.party1[static_cast<std::size_t>(q)]
- g.party0[static_cast<std::size_t>(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<dpf::fp61>();
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<dpf::mac_share<dpf::fp61>, 1> left{a};
std::array<dpf::mac_share<dpf::fp61>, 2> right{b, b};
std::array<dpf::fp61, 1> coeffs{dpf::fp61{1}};
EXPECT_FALSE(dpf::mac_verify_batch(left, right, coeffs, key));
}

View file

@ -0,0 +1,399 @@
/// @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);
}
}

View file

@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <tuple>
#include <array>
#include <cstdint>
@ -6,6 +7,9 @@
#include <vector>
#include "dpf.hpp"
#include "grotto/offset_horner.hpp"
#include "grotto/offset_poly.hpp"
#include "grotto/carry.hpp"
using Interior = dpf::prg::aes128;
using Exterior = dpf::prg::aes128;
@ -39,11 +43,13 @@ TEST(Verifiable, TamperedCwRejects)
auto [k0, k1] = dpf::make_dpf<Interior, Exterior>(Input{3},
std::uint64_t{1}, dpf::verifiable{});
// Flip one bit of a public correction word on party 0's view of the
// shared CW array by rebuilding an otherwise-identical key is hard;
// instead flip cs after the fact via const_cast of the seed storage.
auto & cs = const_cast<dpf::cs_block &>(k0.correction_seeds()[0]);
cs[0] = simde_mm_xor_si128(cs[0], simde_mm_set1_epi8(1));
// Flip every correction seed so an on-path level with control bit 1
// mixes the tamper (a single level-0 flip is invisible when t=0 there).
for (auto & cs : const_cast<typename std::decay_t<decltype(k0)>::correction_seeds_array &>(
k0.correction_seeds()))
{
cs[0] = simde_mm_xor_si128(cs[0], simde_mm_set1_epi8(1));
}
dpf::proof_token pi0{}, pi1{};
(void)*dpf::eval_point(k0, Input{3}, dpf::prove(pi0));
@ -69,6 +75,10 @@ TEST(Verifiable, BatchVerify)
EXPECT_TRUE(dpf::verify_batch(left, right));
left[2][0] = simde_mm_xor_si128(left[2][0], simde_mm_set1_epi8(0xff));
EXPECT_FALSE(dpf::verify_batch(left, right));
// Restore and swap second halves of two slots — must still reject.
left[2][0] = simde_mm_xor_si128(left[2][0], simde_mm_set1_epi8(0xff));
std::swap(left[1][1], left[3][1]);
EXPECT_FALSE(dpf::verify_batch(left, right));
right.pop_back();
EXPECT_FALSE(dpf::verify_batch(left, right));
}
@ -175,6 +185,65 @@ TEST(Verifiable, IntervalProve)
EXPECT_TRUE(dpf::verify(a, b));
}
TEST(Verifiable, IntervalEvalProveMatchesProveInterval)
{
using Input = std::uint8_t;
auto [k0, k1] = dpf::make_dpf<Interior, Exterior>(Input{0x20},
std::uint64_t{7}, dpf::verifiable{});
dpf::proof_token p0{}, p1{}, e0{}, e1{};
dpf::prove_interval(k0, Input{0x10}, Input{0x18}, dpf::prove(p0));
dpf::prove_interval(k1, Input{0x10}, Input{0x18}, dpf::prove(p1));
EXPECT_TRUE(dpf::verify(p0, p1));
auto buf0 = dpf::make_output_buffer_for_interval(k0, Input{0x10}, Input{0x18});
auto buf1 = dpf::make_output_buffer_for_interval(k1, Input{0x10}, Input{0x18});
dpf::eval_interval(k0, Input{0x10}, Input{0x18}, buf0, dpf::prove(e0));
dpf::eval_interval(k1, Input{0x10}, Input{0x18}, buf1, dpf::prove(e1));
EXPECT_TRUE(dpf::verify(e0, e1));
EXPECT_TRUE(dpf::detail::vdpf::proof_equal(p0, e0));
EXPECT_TRUE(dpf::detail::vdpf::proof_equal(p1, e1));
// Tamper party 0's token after an honest fold.
e0[0] = simde_mm_xor_si128(e0[0], simde_mm_set1_epi8(1));
EXPECT_FALSE(dpf::verify(e0, e1));
}
TEST(Verifiable, FullProve)
{
using Input = std::uint8_t;
auto [k0, k1] = dpf::make_dpf<Interior, Exterior>(Input{0x55},
std::uint64_t{3}, dpf::verifiable{});
dpf::proof_token a{}, b{};
dpf::prove_full(k0, dpf::prove(a));
dpf::prove_full(k1, dpf::prove(b));
EXPECT_TRUE(dpf::verify(a, b));
a[0] = simde_mm_xor_si128(a[0], simde_mm_set1_epi8(1));
EXPECT_FALSE(dpf::verify(a, b));
}
TEST(Verifiable, SequenceProveIntervalCovers)
{
using Input = std::uint8_t;
auto [k0, k1] = dpf::make_dpf<Interior, Exterior>(Input{0x22},
std::uint64_t{9}, dpf::verifiable{});
// Two runs: [0x10,0x12] and [0x20,0x21], plus an isolated 0x30.
const Input xs[] = {0x10, 0x11, 0x12, 0x20, 0x21, 0x30};
dpf::proof_token a{}, b{};
dpf::prove_sequence(k0, std::begin(xs), std::end(xs), dpf::prove(a));
dpf::prove_sequence(k1, std::begin(xs), std::end(xs), dpf::prove(b));
EXPECT_TRUE(dpf::verify(a, b));
// eval_sequence(..., prove) uses the same interval-run covers.
auto buf0 = dpf::make_output_buffer_for_subsequence(k0, std::begin(xs), std::end(xs));
auto buf1 = dpf::make_output_buffer_for_subsequence(k1, std::begin(xs), std::end(xs));
dpf::proof_token e0{}, e1{};
dpf::eval_sequence(k0, std::begin(xs), std::end(xs), buf0, dpf::prove(e0));
dpf::eval_sequence(k1, std::begin(xs), std::end(xs), buf1, dpf::prove(e1));
EXPECT_TRUE(dpf::verify(e0, e1));
EXPECT_TRUE(dpf::detail::vdpf::proof_equal(a, e0));
EXPECT_TRUE(dpf::detail::vdpf::proof_equal(b, e1));
}
TEST(Verifiable, DoernerShelatProve)
{
using Input = std::uint8_t;
@ -209,3 +278,431 @@ TEST(Verifiable, DoernerShelatProve)
beta);
EXPECT_TRUE(dpf::verify(a, b));
}
TEST(Verifiable, CmpIntervalProve)
{
using Input = std::uint8_t;
auto [k0, k1] = dpf::make_dpf(Input{0x20}, dpf::lt(std::uint64_t{1}),
dpf::verifiable{});
dpf::proof_token a{}, b{};
dpf::prove_cmp_interval(k0, Input{0x10}, Input{0x28}, dpf::prove(a));
dpf::prove_cmp_interval(k1, Input{0x10}, Input{0x28}, dpf::prove(b));
EXPECT_TRUE(dpf::verify(a, b));
auto buf0 = dpf::make_output_buffer(dpf::cmp, k0, Input{0x10}, Input{0x28});
auto buf1 = dpf::make_output_buffer(dpf::cmp, k1, Input{0x10}, Input{0x28});
dpf::proof_token e0{}, e1{};
dpf::eval_interval(dpf::cmp, k0, Input{0x10}, Input{0x28}, buf0, dpf::prove(e0));
dpf::eval_interval(dpf::cmp, k1, Input{0x10}, Input{0x28}, buf1, dpf::prove(e1));
EXPECT_TRUE(dpf::verify(e0, e1));
EXPECT_TRUE(dpf::detail::vdpf::proof_equal(a, e0));
EXPECT_TRUE(dpf::detail::vdpf::proof_equal(b, e1));
e0[0] = simde_mm_xor_si128(e0[0], simde_mm_set1_epi8(1));
EXPECT_FALSE(dpf::verify(e0, e1));
}
TEST(Verifiable, CmpFullAndSequenceProve)
{
using Input = std::uint8_t;
auto [k0, k1] = dpf::make_dpf(Input{0x55}, dpf::gt(std::uint64_t{1}),
dpf::verifiable{});
dpf::proof_token a{}, b{};
dpf::prove_cmp_full(k0, dpf::prove(a));
dpf::prove_cmp_full(k1, dpf::prove(b));
EXPECT_TRUE(dpf::verify(a, b));
const Input xs[] = {0x10, 0x11, 0x20};
dpf::proof_token s0{}, s1{};
dpf::prove_cmp_sequence(k0, std::begin(xs), std::end(xs), dpf::prove(s0));
dpf::prove_cmp_sequence(k1, std::begin(xs), std::end(xs), dpf::prove(s1));
EXPECT_TRUE(dpf::verify(s0, s1));
auto buf0 = dpf::make_output_buffer(dpf::cmp, k0, 3);
auto buf1 = dpf::make_output_buffer(dpf::cmp, k1, 3);
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));
}
TEST(Verifiable, CmpInnerProductProve)
{
using Input = std::uint8_t;
auto [k0, k1] = dpf::make_dpf(Input{0x18}, dpf::lt(std::uint64_t{1}),
dpf::verifiable{});
const std::uint64_t w[] = {1, 2, 3, 4};
dpf::proof_token a{}, b{};
const auto d0 = dpf::eval_inner_product(dpf::cmp, k0, Input{0x10}, Input{0x13},
w, dpf::prove(a));
const auto d1 = dpf::eval_inner_product(dpf::cmp, k1, Input{0x10}, Input{0x13},
w, dpf::prove(b));
EXPECT_TRUE(dpf::verify(a, b));
(void)d0;
(void)d1;
}
TEST(Verifiable, IdcfPointProve)
{
using Input = std::uint8_t;
auto [k0, k1] = dpf::make_dpf(Input{0x2a}, dpf::idcf(dpf::gt(std::uint64_t{1})),
dpf::verifiable{});
dpf::proof_token a{}, b{};
dpf::eval_point(dpf::cmp, k0, Input{0x30}, dpf::prove(a));
dpf::eval_point(dpf::cmp, k1, Input{0x30}, dpf::prove(b));
EXPECT_TRUE(dpf::verify(a, b));
a[0] = simde_mm_xor_si128(a[0], simde_mm_set1_epi8(1));
EXPECT_FALSE(dpf::verify(a, b));
}
TEST(Verifiable, BlockedCmpPointProve)
{
using Input = std::uint8_t;
auto [k0, k1] = dpf::make_dpf(Input{0x2a},
dpf::block_width<4>(dpf::lt(std::uint64_t{1})), dpf::verifiable{});
EXPECT_GT(decltype(k0)::cmp_block, 0u);
dpf::proof_token a{}, b{};
dpf::eval_point(dpf::cmp, k0, Input{0x11}, dpf::prove(a));
dpf::eval_point(dpf::cmp, k1, Input{0x11}, dpf::prove(b));
EXPECT_TRUE(dpf::verify(a, b));
// Native key of the same alpha yields a different token domain.
auto [n0, n1] = dpf::make_dpf(Input{0x2a}, dpf::lt(std::uint64_t{1}),
dpf::verifiable{});
dpf::proof_token c{}, d{};
dpf::eval_point(dpf::cmp, n0, Input{0x11}, dpf::prove(c));
dpf::eval_point(dpf::cmp, n1, Input{0x11}, dpf::prove(d));
EXPECT_TRUE(dpf::verify(c, d));
EXPECT_FALSE(dpf::detail::vdpf::proof_equal(a, c));
a[0] = simde_mm_xor_si128(a[0], simde_mm_set1_epi8(1));
EXPECT_FALSE(dpf::verify(a, b));
}
TEST(Verifiable, BlockedCmpIntervalProve)
{
using Input = std::uint8_t;
auto [k0, k1] = dpf::make_dpf(Input{0x20},
dpf::block_width<4>(dpf::gt(std::uint64_t{1})), dpf::verifiable{});
dpf::proof_token a{}, b{};
dpf::prove_cmp_interval(k0, Input{0x1c}, Input{0x24}, dpf::prove(a));
dpf::prove_cmp_interval(k1, Input{0x1c}, Input{0x24}, dpf::prove(b));
EXPECT_TRUE(dpf::verify(a, b));
}
TEST(Verifiable, GenevalTrieProve)
{
using Input = std::uint8_t;
const Input alpha = 0x2a;
Input x0 = 0x11;
Input x1 = static_cast<Input>(alpha ^ x0);
struct Pad
{
std::uint64_t n = 1;
simde__m128i block()
{
auto v = simde_mm_set_epi64x(static_cast<long long>(n),
static_cast<long long>(n * 9 + 3));
n += 2;
return v;
}
void fill(void * p, std::size_t nbytes)
{
auto * b = static_cast<unsigned char *>(p);
for (std::size_t i = 0; i < nbytes; ++i)
b[i] = static_cast<unsigned char>(n + i * 17);
n += nbytes;
}
std::uint8_t bit() { return static_cast<std::uint8_t>(n++ & 1u); }
};
HEDLEY_PRAGMA(GCC diagnostic push)
HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes")
dpf::ds_randomness<simde__m128i (*)(), Pad> rng{
dpf::uniform_sample<simde__m128i>, Pad{}};
HEDLEY_PRAGMA(GCC diagnostic pop)
const Input qs[] = {0x2a, 0x2b};
auto got = dpf::geneval_point(x0, x1, qs[0], rng, std::uint64_t{7});
EXPECT_TRUE(dpf::verify(got.proof0, got.proof1));
got.proof0[0] = simde_mm_xor_si128(got.proof0[0], simde_mm_set1_epi8(1));
EXPECT_FALSE(dpf::verify(got.proof0, got.proof1));
}
TEST(Verifiable, GenevalCmpProve)
{
using Input = std::uint8_t;
const Input alpha = 0x30;
Input x0 = 0x05;
Input x1 = static_cast<Input>(alpha ^ x0);
struct Pad
{
std::uint64_t n = 1;
simde__m128i block()
{
auto v = simde_mm_set_epi64x(static_cast<long long>(n),
static_cast<long long>(n * 9 + 3));
n += 2;
return v;
}
void fill(void * p, std::size_t nbytes)
{
auto * b = static_cast<unsigned char *>(p);
for (std::size_t i = 0; i < nbytes; ++i)
b[i] = static_cast<unsigned char>(n + i * 17);
n += nbytes;
}
std::uint8_t bit() { return static_cast<std::uint8_t>(n++ & 1u); }
};
HEDLEY_PRAGMA(GCC diagnostic push)
HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes")
dpf::ds_randomness<simde__m128i (*)(), Pad> rng{
dpf::uniform_sample<simde__m128i>, Pad{}};
HEDLEY_PRAGMA(GCC diagnostic pop)
const Input ends[] = {0x10, 0x20, 0x40};
auto got = dpf::geneval_cmp(x0, x1, std::begin(ends), std::end(ends), rng,
dpf::gt(std::uint64_t{1}));
EXPECT_TRUE(dpf::verify(got.proof0, got.proof1));
}
TEST(Verifiable, OffsetHornerProve)
{
using Input = std::uint8_t;
constexpr std::size_t D = 1;
const Input center = 12;
auto mat = grotto::make_offset_horner_keys<Input, D>(center, dpf::verifiable{});
EXPECT_TRUE(decltype(mat)::is_verifiable);
const std::vector<Input> knots{0, 10, 50};
const std::vector<std::array<std::uint64_t, D + 1>> coeff{
{1, 0}, {0, 2}, {7, 1}};
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]));
t0[0][0] = simde_mm_xor_si128(t0[0][0], simde_mm_set1_epi8(1));
EXPECT_FALSE(dpf::verify(t0[0], t1[0]));
}
TEST(Verifiable, PathPaintLcpProve)
{
using Input = std::uint8_t;
auto [k0, k1] = dpf::make_dpf(Input{0x2a}, dpf::lcp(std::uint64_t{1}),
dpf::verifiable{});
dpf::proof_token a{}, b{};
dpf::prove_cmp_interval(k0, Input{0x20}, Input{0x2f}, dpf::prove(a));
dpf::prove_cmp_interval(k1, Input{0x20}, Input{0x2f}, dpf::prove(b));
EXPECT_TRUE(dpf::verify(a, b));
a[0] = simde_mm_xor_si128(a[0], simde_mm_set1_epi8(1));
EXPECT_FALSE(dpf::verify(a, b));
}
TEST(Verifiable, OffsetPolyProve)
{
using Input = std::uint8_t;
const Input center = 12;
auto mat = grotto::make_offset_poly_keys(center, 1, dpf::verifiable{});
EXPECT_TRUE(mat.verifiable);
const std::vector<Input> knots{0, 10, 50};
const std::vector<std::vector<std::uint64_t>> coeff{{1, 0}, {0, 2}, {7, 1}};
const Input eta = 3;
dpf::proof_token t0[2]{}, t1[2]{};
const auto v0 = grotto::offset_poly_eval<0>(mat, knots, coeff, eta, t0);
const auto v1 = grotto::offset_poly_eval<1>(mat, knots, coeff, eta, t1);
EXPECT_EQ(v0 + v1, grotto::offset_poly_clear(center, knots, coeff, eta));
EXPECT_TRUE(dpf::verify(t0[0], t1[0]));
EXPECT_TRUE(dpf::verify(t0[1], t1[1]));
t0[1][0] = simde_mm_xor_si128(t0[1][0], simde_mm_set1_epi8(1));
EXPECT_FALSE(dpf::verify(t0[1], t1[1]));
EXPECT_TRUE(dpf::verify(t0[0], t1[0]));
}
TEST(Verifiable, BlockedCmpIntervalTamperRejects)
{
using Input = std::uint8_t;
auto [k0, k1] = dpf::make_dpf(Input{0x20},
dpf::block_width<4>(dpf::gt(std::uint64_t{1})), dpf::verifiable{});
dpf::proof_token a{}, b{};
dpf::prove_cmp_interval(k0, Input{0x1c}, Input{0x24}, dpf::prove(a));
dpf::prove_cmp_interval(k1, Input{0x1c}, Input{0x24}, dpf::prove(b));
EXPECT_TRUE(dpf::verify(a, b));
for (auto & cs : const_cast<typename std::decay_t<decltype(k0)>::correction_seeds_array &>(
k0.correction_seeds()))
cs[0] = simde_mm_xor_si128(cs[0], simde_mm_set1_epi8(0x11));
dpf::proof_token c{}, d{};
dpf::prove_cmp_interval(k0, Input{0x1c}, Input{0x24}, dpf::prove(c));
dpf::prove_cmp_interval(k1, Input{0x1c}, Input{0x24}, dpf::prove(d));
EXPECT_FALSE(dpf::verify(c, d));
}
TEST(Verifiable, GenevalOffsetHornerProve)
{
using Input = std::uint8_t;
constexpr std::size_t D = 1;
const Input center = 20;
const Input share = 0x3c;
const Input other = static_cast<Input>(center ^ share);
const Input eta = 3;
struct Pad
{
std::uint64_t n = 1;
simde__m128i block()
{
auto v = simde_mm_set_epi64x(static_cast<long long>(n),
static_cast<long long>(n * 9 + 3));
n += 2;
return v;
}
void fill(void * p, std::size_t nbytes)
{
auto * b = static_cast<unsigned char *>(p);
for (std::size_t i = 0; i < nbytes; ++i)
b[i] = static_cast<unsigned char>(n + i * 17);
n += nbytes;
}
std::uint8_t bit() { return static_cast<std::uint8_t>(n++ & 1u); }
};
HEDLEY_PRAGMA(GCC diagnostic push)
HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes")
dpf::ds_randomness<simde__m128i (*)(), Pad> rng{
dpf::uniform_sample<simde__m128i>, Pad{}};
HEDLEY_PRAGMA(GCC diagnostic pop)
const std::vector<Input> knots{0, 15, 40};
const std::vector<std::array<std::uint64_t, D + 1>> coeff{{1, 0}, {2, 1}, {0, 3}};
auto got = grotto::geneval_offset_horner<D>(share, other, eta, knots, coeff,
std::move(rng));
for (std::size_t m = 0; m <= D; ++m)
EXPECT_TRUE(dpf::verify(got.proof0[m], got.proof1[m]));
got.proof0[0][0] = simde_mm_xor_si128(got.proof0[0][0], simde_mm_set1_epi8(1));
EXPECT_FALSE(dpf::verify(got.proof0[0], got.proof1[0]));
}
TEST(Verifiable, ZeroTokenRejects)
{
dpf::proof_token z = dpf::detail::vdpf::zero_proof();
EXPECT_FALSE(dpf::verify(z, z));
using Input = std::uint8_t;
auto [k0, k1] = dpf::make_dpf<Interior, Exterior>(Input{3},
std::uint64_t{1}, dpf::verifiable{});
dpf::proof_token a{}, b{};
(void)*dpf::eval_point(k0, Input{3}, dpf::prove(a));
(void)*dpf::eval_point(k1, Input{3}, dpf::prove(b));
EXPECT_TRUE(dpf::verify(a, b));
EXPECT_FALSE(dpf::verify(a, z));
EXPECT_FALSE(dpf::verify(z, b));
}
TEST(Verifiable, ColdAndWarmProveMatch)
{
using Input = std::uint8_t;
auto [k0, k1] = dpf::make_dpf<Interior, Exterior>(Input{0x2a},
std::uint64_t{7}, dpf::verifiable{});
const Input x{0x11};
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(dpf::detail::vdpf::proof_equal(cold0,
dpf::detail::vdpf::zero_proof()));
auto p0 = dpf::make_basic_path_memoizer(k0);
auto p1 = dpf::make_basic_path_memoizer(k1);
dpf::proof_token warm0{}, warm1{};
(void)*dpf::eval_point(k0, x, dpf::prove(warm0), p0);
(void)*dpf::eval_point(k1, x, dpf::prove(warm1), p1);
// Second prove on the warm memoizer must match the cold token.
dpf::proof_token again0{}, again1{};
(void)*dpf::eval_point(k0, x, dpf::prove(again0), p0);
(void)*dpf::eval_point(k1, x, dpf::prove(again1), p1);
EXPECT_TRUE(dpf::detail::vdpf::proof_equal(cold0, warm0));
EXPECT_TRUE(dpf::detail::vdpf::proof_equal(cold0, again0));
EXPECT_TRUE(dpf::detail::vdpf::proof_equal(cold1, again1));
EXPECT_TRUE(dpf::verify(again0, again1));
}
TEST(Verifiable, LeafTamperRejectsBoundProof)
{
using Input = std::uint8_t;
auto [k0, k1] = dpf::make_dpf<Interior, Exterior>(Input{5},
std::uint64_t{11}, dpf::verifiable{});
auto & leaf = std::get<0>(
const_cast<typename std::decay_t<decltype(k0)>::leaf_wrapper_tuple &>(
k0.leaf_nodes)).raw_leaf();
auto * bytes = reinterpret_cast<unsigned char *>(&leaf);
bytes[0] = static_cast<unsigned char>(bytes[0] ^ 0x5a);
dpf::proof_token a{}, b{};
(void)*dpf::eval_point(k0, Input{5}, dpf::prove(a));
(void)*dpf::eval_point(k1, Input{5}, dpf::prove(b));
EXPECT_FALSE(dpf::verify(a, b));
}
TEST(Verifiable, ValueWordTamperRejectsBoundProof)
{
using Input = std::uint8_t;
auto [k0, k1] = dpf::make_dpf(Input{0x20}, dpf::lt(std::uint64_t{1}),
dpf::verifiable{});
auto & vcw = const_cast<typename std::decay_t<decltype(k0)>::value_cw_array &>(
k0.value_cw());
vcw[0] = static_cast<typename std::decay_t<decltype(k0)>::value_cw_word>(
static_cast<std::uint64_t>(vcw[0]) ^ 1ull);
dpf::proof_token a{}, b{};
dpf::eval_point(dpf::cmp, k0, Input{0x10}, dpf::prove(a));
dpf::eval_point(dpf::cmp, k1, Input{0x10}, dpf::prove(b));
EXPECT_FALSE(dpf::verify(a, b));
}
TEST(Verifiable, MakeCsMatchesHashNodeXor)
{
// Local transcript of party/oblivious_hash.hpp: cs = H(s0) XOR H(s1).
const std::size_t level = 3;
const psnip_uint64_t prefix = 0x2a;
const simde__m128i s0 = simde_mm_set_epi64x(0x1111, 0x2222);
const simde__m128i s1 = simde_mm_set_epi64x(0x3333, 0x4444);
const auto cs = dpf::detail::vdpf::make_cs(level, prefix, s0, s1);
const auto h0 = dpf::detail::vdpf::hash_node(level, prefix, s0);
const auto h1 = dpf::detail::vdpf::hash_node(level, prefix, s1);
dpf::cs_block xor_h{
simde_mm_xor_si128(h0[0], h1[0]),
simde_mm_xor_si128(h0[1], h1[1]),
simde_mm_xor_si128(h0[2], h1[2]),
simde_mm_xor_si128(h0[3], h1[3])};
EXPECT_EQ(std::memcmp(&cs, &xor_h, sizeof(cs)), 0);
constexpr std::size_t tagged =
dpf::detail::blocked::fold_spine_tag | level;
const auto cs_b = dpf::detail::vdpf::make_cs(tagged, prefix, s0, s1);
const auto hb0 = dpf::detail::vdpf::hash_node(tagged, prefix, s0);
const auto hb1 = dpf::detail::vdpf::hash_node(tagged, prefix, s1);
dpf::cs_block xor_hb{
simde_mm_xor_si128(hb0[0], hb1[0]),
simde_mm_xor_si128(hb0[1], hb1[1]),
simde_mm_xor_si128(hb0[2], hb1[2]),
simde_mm_xor_si128(hb0[3], hb1[3])};
EXPECT_EQ(std::memcmp(&cs_b, &xor_hb, sizeof(cs_b)), 0);
EXPECT_NE(std::memcmp(&cs, &cs_b, sizeof(cs)), 0);
}
TEST(Verifiable, InnerProductProve)
{
using Input = std::uint8_t;
auto [k0, k1] = dpf::make_dpf<Interior, Exterior>(Input{0x10},
std::uint64_t{3}, dpf::verifiable{});
std::array<std::uint64_t, 8> w{};
w.fill(1);
dpf::proof_token a{}, b{}, p0{}, p1{};
dpf::eval_inner_product(k0, Input{0x0c}, Input{0x13}, w,
dpf::prove(a));
dpf::eval_inner_product(k1, Input{0x0c}, Input{0x13}, w,
dpf::prove(b));
EXPECT_TRUE(dpf::verify(a, b));
dpf::prove_interval(k0, Input{0x0c}, Input{0x13}, dpf::prove(p0));
dpf::prove_interval(k1, Input{0x0c}, Input{0x13}, dpf::prove(p1));
EXPECT_TRUE(dpf::detail::vdpf::proof_equal(a, p0));
EXPECT_TRUE(dpf::detail::vdpf::proof_equal(b, p1));
}

View file

@ -0,0 +1,211 @@
#include <gtest/gtest.h>
#include <array>
#include <cstdint>
#include <vector>
#include "dpf.hpp"
// Walk helpers fold a small operation into an existing DPF walk. See
// dpf/eval_walk.hpp.
TEST(WalkHelpers, RotatePairedInnerProduct)
{
constexpr std::size_t n = 256;
constexpr std::uint8_t r = 50;
constexpr std::uint8_t a = 42;
const std::size_t s = (n - static_cast<std::size_t>(
static_cast<std::uint8_t>(r - a))) % n;
std::vector<std::uint64_t> table(n);
for (std::size_t i = 0; i < n; ++i) table[i] = i * 7 + 1;
auto [k0, k1] = dpf::make_dpf(r, std::uint64_t{1});
std::vector<std::uint64_t> manual(n);
for (std::size_t i = 0; i < n; ++i) manual[i] = table[(i + s) % n];
const auto ref = dpf::reconstruct(
dpf::eval_full_inner_product(dpf::paired, k0, manual),
dpf::eval_full_inner_product(dpf::paired, k1, manual));
const auto got = dpf::reconstruct(
dpf::eval_full_inner_product(dpf::paired, k0, table, dpf::rotate{s}),
dpf::eval_full_inner_product(dpf::paired, k1, table, dpf::rotate{s}));
EXPECT_EQ(got, ref);
EXPECT_EQ(got, table[(r + s) % n]);
}
TEST(WalkHelpers, FullAddIntoHistogram)
{
constexpr std::size_t n = 256;
std::vector<dpf::field64> h0(n), h1(n);
for (auto bin : std::array<std::uint8_t, 3>{3, 3, 7})
{
auto [k0, k1] = dpf::make_dpf(bin, dpf::field64{1});
dpf::eval_full_add_into(h0, k0);
dpf::eval_full_add_into(h1, k1);
}
EXPECT_EQ((h0[3] - h1[3]).raw(), 2u);
EXPECT_EQ((h0[7] - h1[7]).raw(), 1u);
EXPECT_EQ((h0[0] - h1[0]).raw(), 0u);
}
TEST(WalkHelpers, FullAddIntoRotate)
{
constexpr std::size_t n = 256;
constexpr std::uint8_t r = 10;
constexpr std::size_t shift = 32;
std::vector<std::uint64_t> d0(n, 0), d1(n, 0);
auto [w0, w1] = dpf::make_dpf(r, std::uint64_t{7});
dpf::eval_full_add_into(d0, w0, dpf::rotate{shift});
dpf::eval_full_add_into(d1, w1, dpf::rotate{shift});
const std::size_t hot = (r + shift) % n;
EXPECT_EQ(d0[hot] - d1[hot], 7u);
EXPECT_EQ(d0[r] - d1[r], 0u);
}
TEST(WalkHelpers, FullAddIntoSketch)
{
constexpr std::size_t n = 256;
constexpr std::uint8_t address = 9;
const dpf::fp61 message{42};
auto [k0, k1] = dpf::make_dpf(address, message, dpf::extractable{});
std::vector<dpf::fp61> box0(n), box1(n);
std::vector<dpf::fp61> challenge(n);
for (std::size_t i = 0; i < n; ++i)
challenge[i] = dpf::fp61{static_cast<std::uint64_t>(i + 1)};
dpf::sketch_share s0{}, s1{};
auto sk0 = dpf::sketch(s0, challenge);
auto sk1 = dpf::sketch(s1, challenge);
dpf::eval_full_add_into(box0, k0, sk0);
dpf::eval_full_add_into(box1, k1, sk1);
EXPECT_EQ(box0[address] - box1[address], message);
EXPECT_EQ((box0[0] - box1[0]).raw(), 0u);
EXPECT_TRUE(dpf::sketch_verify(s0, s1));
}
TEST(WalkHelpers, CyclicShift)
{
std::vector<int> v(8);
for (int i = 0; i < 8; ++i) v[i] = i;
auto sh = dpf::cyclic_shift(v, 3);
for (std::size_t i = 0; i < v.size(); ++i)
EXPECT_EQ(sh[i], v[(i + v.size() - 3) % v.size()]);
}
TEST(WalkHelpers, PackBitColumns)
{
constexpr std::size_t n = 256;
constexpr std::uint8_t alpha = 42;
auto [a0, a1] = dpf::make_dpf(alpha, dpf::bit::one);
auto [b0, b1] = dpf::make_dpf(alpha, dpf::bit::one);
const auto p0 = dpf::pack_bit_columns(a0, b0);
const auto p1 = dpf::pack_bit_columns(a1, b1);
for (std::size_t row = 0; row < n; ++row)
{
const std::uint64_t opened = p0[row] ^ p1[row];
EXPECT_EQ(opened, row == alpha ? 0b11u : 0u) << "row " << row;
}
}
TEST(WalkHelpers, ModBitColumns)
{
constexpr std::size_t n = 256;
constexpr std::uint8_t alpha = 42;
auto [a0, a1] = dpf::make_dpf(alpha, dpf::bit::one);
auto [b0, b1] = dpf::make_dpf(alpha, dpf::bit::one);
auto [c0, c1] = dpf::make_dpf(alpha, dpf::bit::one);
auto expect_mod = [&](auto modulus, const auto & k0, const auto & k1, const auto & k2)
{
constexpr unsigned m = decltype(modulus)::value;
const auto packed = dpf::pack_bit_columns(k0, k1, k2);
const auto got = dpf::mod_bit_columns<m>(k0, k1, k2);
ASSERT_EQ(got.size(), n);
for (std::size_t row = 0; row < n; ++row)
EXPECT_EQ(static_cast<unsigned>(got[row]), packed[row] % m) << "row " << row;
};
expect_mod(std::integral_constant<unsigned, 5>{}, a0, b0, c0);
expect_mod(std::integral_constant<unsigned, 128>{}, a0, b0, c0);
expect_mod(std::integral_constant<unsigned, 200>{}, a1, b1, c1);
expect_mod(std::integral_constant<unsigned, 1000>{}, a0, b1, c0);
expect_mod(std::integral_constant<unsigned, 32768>{}, a0, b0, c0);
const auto one = dpf::mod_bit_columns<7>(a0);
const auto bits = dpf::pack_bit_columns(a0);
for (std::size_t row = 0; row < n; ++row)
EXPECT_EQ(static_cast<unsigned>(one[row]), bits[row] % 7u);
}
TEST(WalkHelpers, UnitSignBitLeaf)
{
constexpr std::uint8_t r = 50;
int w0 = 0, w1 = 0;
auto [k0, k1] = dpf::make_dpf(r, dpf::bit::one, dpf::unit_sign{w0, w1});
const int sign = w0 - w1;
EXPECT_TRUE(sign == 1 || sign == -1);
EXPECT_EQ(w0, static_cast<int>(static_cast<bool>((*dpf::eval_point(k0, r)).raw())));
EXPECT_EQ(w1, static_cast<int>(static_cast<bool>((*dpf::eval_point(k1, r)).raw())));
// The key still opens as an ordinary unit bit DPF.
EXPECT_EQ(dpf::reconstruct(*dpf::eval_point(k0, r), *dpf::eval_point(k1, r)),
dpf::bit::one);
EXPECT_EQ(dpf::reconstruct(*dpf::eval_point(k0, std::uint8_t{0}),
*dpf::eval_point(k1, std::uint8_t{0})), dpf::bit::zero);
}
TEST(WalkHelpers, CmpFullInnerProduct)
{
constexpr std::size_t n = 256;
constexpr std::uint8_t threshold = 50;
auto [c0, c1] = dpf::make_dpf(threshold, dpf::gt(std::uint64_t{1}));
std::vector<std::uint64_t> w(n, 0);
w[90] = 8; w[200] = 3; w[30] = 5; // 90,200 are > 50
const auto h0 = dpf::eval_full_inner_product(dpf::cmp, c0, w);
const auto h1 = dpf::eval_full_inner_product(dpf::cmp, c1, w);
EXPECT_EQ(dpf::reconstruct_cmp_halves(h0, h1).raw(), 11u);
}
TEST(WalkHelpers, Dpf3FullAddInto)
{
constexpr std::size_t n = 256;
std::vector<dpf::fp61> l1(n), l2(n), l3(n);
auto [k1, k2, k3] = dpf::make_dpf3(std::uint8_t{5}, dpf::fp61{100});
dpf::eval_full_add_into(l1, k1);
dpf::eval_full_add_into(l2, k2);
dpf::eval_full_add_into(l3, k3);
auto open2 = [](dpf::fp61 a, dpf::fp61 b) {
return dpf::shamir3::reconstruct(dpf::shamir3::share{1, a},
dpf::shamir3::share{2, b});
};
EXPECT_EQ(open2(l1[5], l2[5]), dpf::fp61{100});
EXPECT_EQ(open2(l1[0], l2[0]).raw(), 0u);
}
TEST(WalkHelpers, PrefixesAndPrefixInnerProduct)
{
constexpr std::uint8_t left = 0xA0; // prefix "101"
auto [k0, k1] = dpf::make_dpf(left,
dpf::idpf(std::uint64_t{1}, std::uint64_t{1}, std::uint64_t{1}));
auto [buf0, it0] = dpf::eval_prefixes(dpf::out<0, 1>, k0);
auto [buf1, it1] = dpf::eval_prefixes(dpf::out<0, 1>, k1);
EXPECT_EQ(dpf::reconstruct(buf0[0], buf1[0]), 0u);
EXPECT_EQ(dpf::reconstruct(buf0[1], buf1[1]), 1u);
std::vector<std::uint64_t> vals1{11, 23};
const auto dot1 =
dpf::eval_prefix_inner_product(dpf::out<0, 1>, k0, vals1)
- dpf::eval_prefix_inner_product(dpf::out<0, 1>, k1, vals1);
EXPECT_EQ(dot1, vals1[1]);
std::vector<std::uint64_t> vals3(8);
for (std::size_t i = 0; i < 8; ++i) vals3[i] = 100 + i;
const auto dot3 =
dpf::eval_prefix_inner_product(dpf::out<2, 3>, k0, vals3)
- dpf::eval_prefix_inner_product(dpf::out<2, 3>, k1, vals3);
EXPECT_EQ(dot3, vals3[0b101]);
}

View file

@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <tuple>
#include "dpf.hpp"
#include "grotto/fixedpoint.hpp"

View file

@ -1,4 +1,8 @@
#include <gtest/gtest.h>
#include <tuple>
#include <cstring>
#include <cstdint>
#include "asio.hpp"
#define LIBDPF_HAS_ASIO
@ -591,7 +595,7 @@ TEST(WildcardTest, PackedSmallWildcardsAtNonTerminalAssignAll)
dpf::at<10>(w, w, w, w),
uint16_t{99});
ASSERT_THROW((void)dpf::eval_point(dpf::out<0, 10>, dpf0, x), std::runtime_error);
ASSERT_THROW(dpf::eval_point(dpf::out<0, 10>, dpf0, x), std::runtime_error);
ASSERT_EQ(static_cast<uint16_t>(
dpf::reconstruct(*dpf::eval_point(dpf::out<4>, dpf0, x), *dpf::eval_point(dpf::out<4>, dpf1, x))),
uint16_t{99});
@ -617,3 +621,189 @@ TEST(WildcardTest, PackedSmallWildcardsAtNonTerminalAssignAll)
static_cast<uint16_t>(x ^ (1u << 6))))),
concrete_t{0});
}
namespace
{
template <typename Bits, typename T>
T bits_as(Bits bits)
{
T out{};
std::memcpy(&out, &bits, sizeof(T));
return out;
}
template <typename T>
auto bits_of(T v)
{
using bits_t = std::conditional_t<sizeof(T) == 4, std::uint32_t, std::uint64_t>;
bits_t bits{};
std::memcpy(&bits, &v, sizeof(T));
return bits;
}
/// In-process assign for a wildcard leaf (same messages as asio, no socket).
template <std::size_t I = 0, typename DpfKey0, typename DpfKey1, typename ShareT>
void assign_leaf_local(DpfKey0 & dpf0, DpfKey1 & dpf1, const ShareT & shr0,
const ShareT & shr1)
{
auto & w0 = std::get<I>(dpf0.leaf_nodes);
auto & w1 = std::get<I>(dpf1.leaf_nodes);
if (w0.is_ready())
w0.begin_update();
if (w1.is_ready())
w1.begin_update();
const auto b0 = w0.compute_and_get_blinded_output_share(shr0);
const auto b1 = w1.compute_and_get_blinded_output_share(shr1);
const auto l0 = w0.compute_and_get_leaf_share(b1);
const auto l1 = w1.compute_and_get_leaf_share(b0);
w0.reconstruct_correction_word(l1);
w1.reconstruct_correction_word(l0);
}
} // namespace
TEST(WildcardTest, FullWidthXorDoesNotRevealBeta)
{
using input_type = uint8_t;
using concrete_type = dpf::xints::xint128_t;
using output_type = dpf::wildcard_value<concrete_type>;
input_type x = 0x42;
output_type y;
auto [dpf0, dpf1] = dpf::make_dpf(x, y);
auto & w0 = std::get<0>(dpf0.leaf_nodes);
auto & w1 = std::get<0>(dpf1.leaf_nodes);
// Scale Beaver must be planted even for a single full-width XOR lane.
EXPECT_EQ((dpf::outputs_per_leaf_v<concrete_type,
typename std::decay_t<decltype(dpf0)>::exterior_node>), 1u);
EXPECT_TRUE(dpf::utils::has_characteristic_two_v<concrete_type>);
concrete_type y_exp = concrete_type{0x0123456789ABCDEFull};
concrete_type y_shr0 = concrete_type{0x1111111111111111ull};
concrete_type y_shr1 = y_exp + y_shr0; // XOR group
const auto blinded0 = w0.compute_and_get_blinded_output_share(y_shr0);
const auto blinded1 = w1.compute_and_get_blinded_output_share(y_shr1);
// With a non-trivial output blind, the exchanged value is not the share.
EXPECT_NE(blinded0, y_shr0);
EXPECT_NE(blinded1, y_shr1);
// Finish the assign after the privacy check above (state is already blinded).
const auto l0 = w0.compute_and_get_leaf_share(blinded1);
const auto l1 = w1.compute_and_get_leaf_share(blinded0);
w0.reconstruct_correction_word(l1);
w1.reconstruct_correction_word(l0);
EXPECT_EQ(dpf::reconstruct(*dpf::eval_point(dpf0, x), *dpf::eval_point(dpf1, x)),
y_exp);
EXPECT_EQ(dpf::reconstruct(*dpf::eval_point(dpf0, static_cast<input_type>(x ^ 1)),
*dpf::eval_point(dpf1, static_cast<input_type>(x ^ 1))),
concrete_type{});
}
TEST(WildcardTest, FloatWildcardRoundTrip)
{
using input_type = uint8_t;
using concrete_type = float;
using output_type = dpf::wildcard_value<concrete_type>;
input_type x = 0x55;
output_type y;
auto [dpf0, dpf1] = dpf::make_dpf(x, y);
const concrete_type y_exp = 3.14159265f;
const auto y_bits = bits_of(y_exp);
const std::uint32_t shr0_bits = 0xA5A5A5A5u;
const concrete_type y_shr0 = bits_as<std::uint32_t, concrete_type>(shr0_bits);
const concrete_type y_shr1 =
bits_as<std::uint32_t, concrete_type>(y_bits ^ shr0_bits);
assign_leaf_local(dpf0, dpf1, y_shr0, y_shr1);
const auto got = dpf::reconstruct(*dpf::eval_point(dpf0, x), *dpf::eval_point(dpf1, x));
EXPECT_EQ(bits_of(got), y_bits);
EXPECT_EQ(bits_of(dpf::reconstruct(
*dpf::eval_point(dpf0, static_cast<input_type>(x ^ 1)),
*dpf::eval_point(dpf1, static_cast<input_type>(x ^ 1)))),
0u);
}
TEST(WildcardTest, SecondAssignUpdatesPayload)
{
using input_type = uint8_t;
using concrete_type = uint32_t;
using output_type = dpf::wildcard_value<concrete_type>;
input_type x = 0x11;
auto [dpf0, dpf1] = dpf::make_dpf(x, output_type{});
const concrete_type beta = 0xAAAAAAAAu;
const concrete_type beta2 = 0xBBBBBBBBu;
const concrete_type s0 = 0x12345678u;
assign_leaf_local(dpf0, dpf1, s0, static_cast<concrete_type>(beta - s0));
EXPECT_EQ(dpf::reconstruct(*dpf::eval_point(dpf0, x), *dpf::eval_point(dpf1, x)), beta);
// Second assign installs (beta2 - beta) on top of the ready leaf.
const concrete_type delta = static_cast<concrete_type>(beta2 - beta);
const concrete_type d0 = 0x01010101u;
assign_leaf_local(dpf0, dpf1, d0, static_cast<concrete_type>(delta - d0));
EXPECT_EQ(dpf::reconstruct(*dpf::eval_point(dpf0, x), *dpf::eval_point(dpf1, x)),
beta2);
}
TEST(WildcardTest, AssignWildcardInputOpensPublicShiftNotAlpha)
{
using input_type = uint8_t;
using output_type = uint32_t;
// Wildcard domain: dealer plants a random mask; parties later open (mask - alpha).
auto [dpf0, dpf1] = dpf::make_dpf(dpf::wildcard_value<input_type>{}, output_type{7});
const input_type mask = static_cast<input_type>(
dpf0.offset_x.raw() + dpf1.offset_x.raw());
const input_type alpha = 0xAAu;
const input_type a0 = 0x12u;
const input_type a1 = static_cast<input_type>(alpha - a0);
const auto sh0 = dpf0.offset_x.compute_and_get_share(a0);
const auto sh1 = dpf1.offset_x.compute_and_get_share(a1);
const auto open0 = dpf0.offset_x.reconstruct(sh1);
const auto open1 = dpf1.offset_x.reconstruct(sh0);
EXPECT_EQ(open0, open1);
const input_type want_shift = static_cast<input_type>(mask - alpha);
EXPECT_EQ(open0, want_shift);
EXPECT_NE(open0, alpha); // public value is the shift, not alpha
EXPECT_EQ(static_cast<input_type>(open0 + alpha), mask);
EXPECT_EQ(dpf::reconstruct(*dpf::eval_point(dpf0, alpha), *dpf::eval_point(dpf1, alpha)),
output_type{7});
EXPECT_EQ(dpf::reconstruct(*dpf::eval_point(dpf0, static_cast<input_type>(alpha ^ 1)),
*dpf::eval_point(dpf1, static_cast<input_type>(alpha ^ 1))),
output_type{0});
}
TEST(WildcardTest, UpdatableTagAssignsThenRewrites)
{
const std::uint8_t alpha = 0x2a;
auto [k0, k1] = dpf::make_dpf(alpha, std::uint64_t{7}, dpf::updatable{});
EXPECT_TRUE(k0.is_wildcard(0));
EXPECT_TRUE(k1.is_wildcard(0));
EXPECT_EQ(dpf::reconstruct(*dpf::eval_point(k0, alpha), *dpf::eval_point(k1, alpha)),
std::uint64_t{7});
EXPECT_EQ(dpf::reconstruct(*dpf::eval_point(k0, std::uint8_t{0}),
*dpf::eval_point(k1, std::uint8_t{0})),
std::uint64_t{0});
auto & w0 = std::get<0>(k0.leaf_nodes);
auto & w1 = std::get<0>(k1.leaf_nodes);
w0.begin_update();
w1.begin_update();
// A second assign installs the difference β' − β, not the new absolute payload.
const auto shares = dpf::additively_share(std::uint64_t{9} - std::uint64_t{7});
const auto b0 = w0.compute_and_get_blinded_output_share(shares.first.raw());
const auto b1 = w1.compute_and_get_blinded_output_share(shares.second.raw());
const auto l0 = w0.compute_and_get_leaf_share(b1);
const auto l1 = w1.compute_and_get_leaf_share(b0);
w0.reconstruct_correction_word(l1);
w1.reconstruct_correction_word(l0);
EXPECT_EQ(dpf::reconstruct(*dpf::eval_point(k0, alpha), *dpf::eval_point(k1, alpha)),
std::uint64_t{9});
}

View file

@ -1,4 +1,5 @@
#include <gtest/gtest.h>
#include <tuple>
#include "grotto/window_lut.hpp"
@ -77,6 +78,18 @@ long double reference(grotto::window which, unsigned k, long double x)
return acosl(x);
case grotto::window::probit:
return 0;
case grotto::window::hardelish:
if (x <= -1.0L)
return 0;
if (x >= 1.0L)
return x;
if (x >= 0.0L)
return x * (x + 1.0L) / 2.0L;
return expm1l(x) * (x + 1.0L) / 2.0L;
case grotto::window::lecun_tanh:
return 1.7159L * tanhl(2.0L * x / 3.0L);
case grotto::window::one_minus_sigmoid:
return 1.0L - sigmoid(x);
}
return 0;
}
@ -154,6 +167,51 @@ TEST(WindowLut, ExhaustiveLowPrecision)
expect_close(grotto::window::asin, k, raw);
expect_close(grotto::window::acos, k, raw);
}
const std::int64_t step = k == 8 ? 1 : 17;
for (std::int64_t raw = -2 * one; raw <= 2 * one; raw += step)
expect_close(grotto::window::hardelish, k, raw);
const std::int64_t lecun_span = std::int64_t{20} << k;
for (std::int64_t raw = -lecun_span; raw <= lecun_span; raw += step)
{
expect_close(grotto::window::lecun_tanh, k, raw);
expect_close(grotto::window::one_minus_sigmoid, k, raw);
}
}
}
TEST(WindowLut, HighPrecisionSamples)
{
for (unsigned k : {16u, 24u, 32u})
{
const auto one = std::int64_t{1} << k;
const auto step = std::int64_t{1} << (k - 8);
for (std::int64_t raw = -2 * one; raw <= 2 * one; raw += step)
expect_close(grotto::window::hardelish, k, raw);
const std::int64_t span = std::int64_t{20} << k;
const auto wide = std::int64_t{1} << (k - 6);
for (std::int64_t raw = -span; raw <= span; raw += wide)
{
expect_close(grotto::window::lecun_tanh, k, raw);
expect_close(grotto::window::one_minus_sigmoid, k, raw);
}
expect_close(grotto::window::hardelish, k, -one + 1);
expect_close(grotto::window::hardelish, k, -1);
expect_close(grotto::window::lecun_tanh, k, 1);
expect_close(grotto::window::lecun_tanh, k, span);
}
}
TEST(WindowLut, OneMinusSigmoidComplementsSigmoid)
{
for (unsigned k : {8u, 16u, 32u})
{
const auto one = std::int64_t{1} << k;
for (std::int64_t raw : {std::int64_t{-8} * one, -one, std::int64_t{0}, one, std::int64_t{8} * one})
{
const auto sig = grotto::eval_window(grotto::window::sigmoid, k, raw);
const auto comp = grotto::eval_window(grotto::window::one_minus_sigmoid, k, raw);
EXPECT_EQ(sig + comp, one) << k << " " << raw;
}
}
}

View file

@ -0,0 +1,577 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <cstdint>
#include <iterator>
#include <limits>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <vector>
#include "dpf.hpp"
namespace
{
template <typename A, typename B>
auto recon(const A & a, const B & b)
{
if constexpr (dpf::is_secret_share_v<std::decay_t<A>>
&& dpf::is_secret_share_v<std::decay_t<B>>)
return dpf::reconstruct(a, b);
else
{
using T = std::common_type_t<std::decay_t<A>, std::decay_t<B>>;
if constexpr (std::is_integral_v<T> && std::is_unsigned_v<T>)
return static_cast<T>(a - b);
else
return a - b;
}
}
template <typename Key0, typename Key1, typename InputT>
void assign_input_local(Key0 & k0, Key1 & k1, InputT alpha)
{
const InputT a0 = static_cast<InputT>(0x12);
const InputT a1 = static_cast<InputT>(alpha - a0);
const auto sh0 = k0.offset_x.compute_and_get_share(a0);
const auto sh1 = k1.offset_x.compute_and_get_share(a1);
k0.offset_x.reconstruct(sh1);
k1.offset_x.reconstruct(sh0);
}
template <typename InputT>
std::size_t inclusive_span(InputT from, InputT to)
{
constexpr auto bits = dpf::utils::bitlength_of_v<InputT>;
constexpr auto to_int = dpf::utils::to_integral_type<InputT>{};
auto span = to_int(to) - to_int(from);
if constexpr (bits < dpf::utils::bitlength_of_v<decltype(span)>)
span &= (decltype(span){1} << bits) - 1;
return static_cast<std::size_t>(span) + 1;
}
template <typename InputT, typename Fn>
void for_inclusive_wrap(InputT from, InputT to, Fn && fn)
{
constexpr auto to_int = dpf::utils::to_integral_type<InputT>{};
const auto n = inclusive_span(from, to);
InputT x = from;
for (std::size_t i = 0; i < n; ++i)
{
fn(x);
x = static_cast<InputT>(to_int(x) + 1);
}
}
} // namespace
// ---------------------------------------------------------------------------
// utils: interval_wraps / split_leaf_nodes / get_leafnodes
// ---------------------------------------------------------------------------
TEST(WrapUtils, IntervalWrapsUnsigned)
{
EXPECT_FALSE(dpf::utils::interval_wraps(std::uint8_t{10}, std::uint8_t{20}, 8));
EXPECT_TRUE(dpf::utils::interval_wraps(std::uint8_t{200}, std::uint8_t{10}, 8));
EXPECT_FALSE(dpf::utils::interval_wraps(std::uint8_t{0}, std::uint8_t{255}, 8));
EXPECT_TRUE(dpf::utils::interval_wraps(std::uint8_t{255}, std::uint8_t{0}, 8));
EXPECT_FALSE(dpf::utils::interval_wraps(0, 0, 0));
}
TEST(WrapUtils, IntervalWrapsMaskedLowBits)
{
// Only low 4 bits participate; 0x1F vs 0x02 wraps in nibble space.
EXPECT_TRUE(dpf::utils::interval_wraps(0x1Fu, 0x02u, 4));
EXPECT_FALSE(dpf::utils::interval_wraps(0x12u, 0x1Eu, 4));
}
TEST(WrapUtils, SplitNonWrapSingleSegment)
{
auto segs = dpf::utils::split_leaf_nodes(std::uint8_t{3}, std::uint8_t{10}, 8, false);
ASSERT_EQ(segs.n, 1u);
EXPECT_EQ(segs.seg[0].from_node, 3);
EXPECT_EQ(segs.seg[0].to_node, 10);
EXPECT_EQ(segs.seg[0].count, 7u);
EXPECT_EQ(segs.total, 7u);
}
TEST(WrapUtils, SplitWrapTwoSegments)
{
// depth < bitwidth so domain_end is representable.
auto segs = dpf::utils::split_leaf_nodes(
std::uint16_t{250}, std::uint16_t{4}, 8, true);
ASSERT_EQ(segs.n, 2u);
EXPECT_EQ(segs.seg[0].from_node, 250);
EXPECT_EQ(segs.seg[0].to_node, 256);
EXPECT_EQ(segs.seg[0].count, 6u);
EXPECT_EQ(segs.seg[1].from_node, 0);
EXPECT_EQ(segs.seg[1].to_node, 4);
EXPECT_EQ(segs.seg[1].count, 4u);
EXPECT_EQ(segs.total, 10u);
}
TEST(WrapUtils, SplitWrapShallowDepth)
{
// depth 6 => domain_end 64
auto segs = dpf::utils::split_leaf_nodes(std::uint8_t{60}, std::uint8_t{3}, 6, true);
ASSERT_EQ(segs.n, 2u);
EXPECT_EQ(segs.seg[0].from_node, 60);
EXPECT_EQ(segs.seg[0].to_node, 64);
EXPECT_EQ(segs.seg[0].count, 4u);
EXPECT_EQ(segs.seg[1].from_node, 0);
EXPECT_EQ(segs.seg[1].to_node, 3);
EXPECT_EQ(segs.seg[1].count, 3u);
EXPECT_EQ(segs.total, 7u);
}
TEST(WrapUtils, InputWrapWithFromNodeLeToNodeStillSplits)
{
// Packing can make from_node <= to_node while the input still wraps.
// Collapsing would omit the duplicated shared leaf.
auto segs = dpf::utils::split_leaf_nodes(std::uint8_t{2}, std::uint8_t{5}, 6, true);
ASSERT_EQ(segs.n, 2u);
EXPECT_EQ(segs.seg[0].from_node, 2);
EXPECT_EQ(segs.seg[0].to_node, 64);
EXPECT_EQ(segs.seg[1].from_node, 0);
EXPECT_EQ(segs.seg[1].to_node, 5);
EXPECT_GT(segs.total, 5u - 2u);
}
TEST(WrapUtils, LeafCountAgreesWithSplitTotal)
{
using In = std::uint8_t;
auto [k0, k1] = dpf::make_dpf(In{1}, std::uint32_t{1});
using key_t = std::decay_t<decltype(k0)>;
auto check = [](In from, In to)
{
In ff = from, ft = to;
dpf::utils::flip_msb_if_signed_integral(ff);
dpf::utils::flip_msb_if_signed_integral(ft);
constexpr auto to_int = dpf::utils::to_integral_type<In>{};
using integral = typename key_t::integral_type;
const auto from_i = static_cast<integral>(to_int(ff));
const auto to_i = static_cast<integral>(to_int(ft));
const bool wraps = dpf::utils::interval_wraps(from_i, to_i, 8);
const auto segs = dpf::utils::split_leaf_nodes(
dpf::utils::get_from_node<key_t>(ff),
dpf::utils::get_to_node<key_t>(ft),
static_cast<std::size_t>(key_t::depth), wraps);
EXPECT_EQ((dpf::utils::get_leafnodes_in_output_interval<key_t>(from, to)),
segs.total);
};
check(In{10}, In{20});
check(In{200}, In{10});
check(In{10}, In{9});
check(In{0}, In{255});
(void)k1;
}
TEST(WrapUtils, LeafCountVariesWithAlignmentWhenPacked)
{
using In = std::int8_t;
auto [k0, k1] = dpf::make_dpf(In{0}, std::uint32_t{7});
using key_t = std::decay_t<decltype(k0)>;
ASSERT_GT(key_t::outputs_per_leaf, 1u);
const In from{-40}, to{10};
const auto n0 = dpf::utils::get_leafnodes_in_output_interval<key_t>(from, to);
bool saw_larger = false;
for (int d = 0; d < 64; ++d)
{
const In tfrom = static_cast<In>(from + d);
const In tto = static_cast<In>(to + d);
const auto n = dpf::utils::get_leafnodes_in_output_interval<key_t>(tfrom, tto);
if (n > n0)
saw_larger = true;
}
EXPECT_TRUE(saw_larger);
(void)k1;
}
// ---------------------------------------------------------------------------
// rotation_iterable: indexing past the wrap (the deferred-view bug class)
// ---------------------------------------------------------------------------
TEST(RotationIterable, OperatorBracketMatchesIteratorOrder)
{
std::vector<int> values{10, 20, 30, 40, 50};
dpf::rotation_iterable rot(values.begin(), values.end(), 3);
std::vector<int> by_it;
for (auto it = rot.begin(); it != rot.end(); ++it)
by_it.push_back(*it);
EXPECT_EQ(by_it, (std::vector<int>{40, 50, 10, 20, 30}));
std::vector<int> by_idx;
for (std::ptrdiff_t i = 0; i < static_cast<std::ptrdiff_t>(values.size()); ++i)
by_idx.push_back(rot[i]);
EXPECT_EQ(by_idx, by_it);
}
TEST(RotationIterable, NegativeAndFullCycleDistanceNormalize)
{
std::vector<int> values{1, 2, 3, 4};
dpf::rotation_iterable neg(values.begin(), values.end(), -1);
EXPECT_EQ(neg.distance(), 3);
EXPECT_EQ(neg[0], 4);
EXPECT_EQ(neg[1], 1);
dpf::rotation_iterable full(values.begin(), values.end(), 8);
EXPECT_EQ(full.distance(), 0);
EXPECT_EQ(full[0], 1);
EXPECT_EQ(full[3], 4);
}
TEST(RotationIterable, WrappingSubrangeViaIndexDoesNotWalkPastEnd)
{
// Contiguous std::next from rot.begin() past the physical end is wrong
// for a wrapping logical slice; operator[] with modulo is the safe path.
std::vector<int> values{0, 1, 2, 3, 4, 5, 6, 7};
dpf::rotation_iterable rot(values.begin(), values.end(), 5);
// Logical slice starting at index 6 of the unrotated domain, length 4:
// rotated indices (6+5)%8 ... => values 3,4,5,6 in rot order from start 6.
const std::size_t start = 6;
const std::size_t count = 4;
const std::size_t n = values.size();
std::vector<int> got;
for (std::size_t i = 0; i < count; ++i)
got.push_back(rot[static_cast<std::ptrdiff_t>((start + i) % n)]);
EXPECT_EQ(got, (std::vector<int>{3, 4, 5, 6}));
}
TEST(RotationIterable, BidirectionalRoundTrip)
{
std::vector<int> values{1, 2, 3, 4, 5};
dpf::rotation_iterable rot(values.begin(), values.end(), 2);
auto it = rot.begin();
++it;
++it;
EXPECT_EQ(*it, 5);
--it;
EXPECT_EQ(*it, 4);
--it;
EXPECT_EQ(it, rot.begin());
EXPECT_EQ(*it, 3);
}
// ---------------------------------------------------------------------------
// Eager eval_interval wrap vs pointwise
// ---------------------------------------------------------------------------
TEST(EagerWrap, Uint8MatchesPointwise)
{
using In = std::uint8_t;
using Out = std::uint32_t;
constexpr In from{200}, to{10}, alpha{250};
constexpr Out beta{0xABCDEF01u};
auto [k0, k1] = dpf::make_dpf(alpha, beta);
auto [buf0, it0] = dpf::eval_interval(k0, from, to);
auto [buf1, it1] = dpf::eval_interval(k1, from, to);
auto a = std::begin(it0);
auto b = std::begin(it1);
std::size_t n = 0;
for_inclusive_wrap(from, to, [&](In x)
{
ASSERT_NE(a, std::end(it0));
ASSERT_NE(b, std::end(it1));
EXPECT_EQ(recon(*a, *b),
recon(*dpf::eval_point(k0, x), *dpf::eval_point(k1, x)))
<< "x=" << +x;
++a;
++b;
++n;
});
EXPECT_EQ(a, std::end(it0));
EXPECT_EQ(b, std::end(it1));
EXPECT_EQ(n, inclusive_span(from, to));
(void)buf0;
(void)buf1;
}
TEST(EagerWrap, SameLeafWrapMatchesPointwise)
{
using In = std::uint8_t;
using Out = std::uint32_t;
// from=10, to=9 wraps almost the full domain; opl may share a leaf.
auto [k0, k1] = dpf::make_dpf(In{40}, Out{0x11111111u});
auto [buf0, it0] = dpf::eval_interval(k0, In{10}, In{9});
auto [buf1, it1] = dpf::eval_interval(k1, In{10}, In{9});
auto a = std::begin(it0);
auto b = std::begin(it1);
for_inclusive_wrap(In{10}, In{9}, [&](In x)
{
ASSERT_NE(a, std::end(it0));
ASSERT_NE(b, std::end(it1));
EXPECT_EQ(recon(*a, *b),
recon(*dpf::eval_point(k0, x), *dpf::eval_point(k1, x)));
++a;
++b;
});
EXPECT_EQ(a, std::end(it0));
EXPECT_EQ(b, std::end(it1));
(void)buf0;
(void)buf1;
}
// ---------------------------------------------------------------------------
// Wildcard offset: memoizer / buffer must size on tree coordinates
// ---------------------------------------------------------------------------
TEST(OffsetSizing, LogicalMemoizerCanUndersizeAfterAssign)
{
using In = std::int8_t;
using Out = std::uint32_t;
constexpr In from{-40}, to{10};
using key_t = std::decay_t<decltype(
std::get<0>(dpf::make_dpf(dpf::wildcard_value<In>{}, Out{7})))>;
ASSERT_GT(key_t::outputs_per_leaf, 1u);
const auto n_logical =
dpf::utils::get_leafnodes_in_output_interval<key_t>(from, to);
bool found = false;
for (int trial = 0; trial < 256; ++trial)
{
auto [k0, k1] = dpf::make_dpf(dpf::wildcard_value<In>{}, Out{7});
const In alpha = static_cast<In>(trial - 128);
assign_input_local(k0, k1, alpha);
const auto tfrom = k0.offset_x(from);
const auto tto = k0.offset_x(to);
const auto n_tree =
dpf::utils::get_leafnodes_in_output_interval<key_t>(tfrom, tto);
In ff = tfrom, ft = tto;
dpf::utils::flip_msb_if_signed_integral(ff);
dpf::utils::flip_msb_if_signed_integral(ft);
constexpr auto to_int = dpf::utils::to_integral_type<In>{};
using integral = typename key_t::integral_type;
const bool wraps = dpf::utils::interval_wraps(
static_cast<integral>(to_int(ff)),
static_cast<integral>(to_int(ft)), 8);
// Single-segment undersize: each wrap half may still fit in n_logical.
if (wraps || n_tree <= n_logical)
continue;
found = true;
auto small = dpf::make_basic_interval_memoizer<key_t>(from, to);
auto buf = dpf::make_output_buffer_for_interval(k0, from, to);
EXPECT_THROW(
(void)dpf::eval_interval(k0, from, to, buf, small),
std::exception);
auto memo = dpf::make_basic_interval_memoizer(k0, from, to);
auto buf2 = dpf::make_output_buffer_for_interval(k0, from, to);
auto it = dpf::eval_interval(k0, from, to, buf2, memo);
auto it1_buf = dpf::make_output_buffer_for_interval(k1, from, to);
auto memo1 = dpf::make_basic_interval_memoizer(k1, from, to);
auto it1 = dpf::eval_interval(k1, from, to, it1_buf, memo1);
auto a = std::begin(it);
auto b = std::begin(it1);
for_inclusive_wrap(from, to, [&](In x)
{
ASSERT_NE(a, std::end(it));
ASSERT_NE(b, std::end(it1));
EXPECT_EQ(recon(*a, *b),
recon(*dpf::eval_point(k0, x), *dpf::eval_point(k1, x)));
++a;
++b;
});
break;
}
ASSERT_TRUE(found) << "no non-wrapping misaligned offset found";
}
TEST(OffsetSizing, ConvenienceEvalIntervalSurvivesSignedWildcardSweep)
{
using In = std::int8_t;
using Out = std::uint32_t;
constexpr In from{-40}, to{10};
constexpr Out beta{9};
int ok = 0;
for (int trial = 0; trial < 64; ++trial)
{
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<In>{}, beta);
assign_input_local(d0, d1, static_cast<In>(trial * 3 - 96));
auto buf0 = dpf::make_output_buffer_for_interval(d0, from, to);
auto buf1 = dpf::make_output_buffer_for_interval(d1, from, to);
// Default memoizer path must not throw after the sizing fix.
auto it0 = dpf::eval_interval(d0, from, to, buf0);
auto it1 = dpf::eval_interval(d1, from, to, buf1);
auto a = std::begin(it0);
auto b = std::begin(it1);
for_inclusive_wrap(from, to, [&](In x)
{
ASSERT_NE(a, std::end(it0));
ASSERT_NE(b, std::end(it1));
EXPECT_EQ(recon(*a, *b),
recon(*dpf::eval_point(d0, x), *dpf::eval_point(d1, x)))
<< "x=" << +x << " trial=" << trial;
++a;
++b;
});
++ok;
}
EXPECT_EQ(ok, 64);
}
// ---------------------------------------------------------------------------
// Deferred wrap: index-based view vs eager (regression for contiguous-next bug)
// ---------------------------------------------------------------------------
TEST(DeferWrap, WrappingUint8MatchesEagerAndPointwise)
{
using In = std::uint8_t;
using Out = std::uint32_t;
constexpr In from{200}, to{10};
constexpr Out beta{42};
for (unsigned alpha_i = 0; alpha_i < 256; alpha_i += 37)
{
const In alpha = static_cast<In>(alpha_i);
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<In>{}, beta);
auto buf0 = dpf::make_output_buffer_for_full(d0);
auto buf1 = dpf::make_output_buffer_for_full(d1);
auto def0 = dpf::defer_eval_interval(d0, from, to, buf0);
auto def1 = dpf::defer_eval_interval(d1, from, to, buf1);
assign_input_local(d0, d1, alpha);
auto eager_buf0 = dpf::make_output_buffer_for_interval(d0, from, to);
auto eager_buf1 = dpf::make_output_buffer_for_interval(d1, from, to);
auto eager0 = dpf::eval_interval(d0, from, to, eager_buf0);
auto eager1 = dpf::eval_interval(d1, from, to, eager_buf1);
auto v0 = def0.get();
auto v1 = def1.get();
auto it_a = std::begin(v0);
auto it_b = std::begin(v1);
auto it_c = std::begin(eager0);
auto it_d = std::begin(eager1);
for_inclusive_wrap(from, to, [&](In x)
{
ASSERT_NE(it_a, std::end(v0));
ASSERT_NE(it_b, std::end(v1));
ASSERT_NE(it_c, std::end(eager0));
ASSERT_NE(it_d, std::end(eager1));
const auto y_def = recon(*it_a, *it_b);
const auto y_eag = recon(*it_c, *it_d);
const auto y_pt = recon(*dpf::eval_point(d0, x),
*dpf::eval_point(d1, x));
EXPECT_EQ(y_def, y_eag) << "x=" << +x << " alpha=" << +alpha;
EXPECT_EQ(y_def, y_pt) << "x=" << +x << " alpha=" << +alpha;
++it_a;
++it_b;
++it_c;
++it_d;
});
EXPECT_EQ(it_a, std::end(v0));
EXPECT_EQ(it_b, std::end(v1));
}
}
TEST(DeferWrap, SignedSpanAcrossZeroMatchesEager)
{
using In = std::int8_t;
using Out = std::uint32_t;
constexpr In from{-5}, to{5};
constexpr Out beta{3};
for (int trial = 0; trial < 32; ++trial)
{
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<In>{}, beta);
auto buf0 = dpf::make_output_buffer_for_full(d0);
auto buf1 = dpf::make_output_buffer_for_full(d1);
auto def0 = dpf::defer_eval_interval(d0, from, to, buf0);
auto def1 = dpf::defer_eval_interval(d1, from, to, buf1);
assign_input_local(d0, d1, static_cast<In>(trial * 7 - 112));
auto eager_buf0 = dpf::make_output_buffer_for_interval(d0, from, to);
auto eager_buf1 = dpf::make_output_buffer_for_interval(d1, from, to);
auto eager0 = dpf::eval_interval(d0, from, to, eager_buf0);
auto eager1 = dpf::eval_interval(d1, from, to, eager_buf1);
auto v0 = def0.get();
auto v1 = def1.get();
auto a = std::begin(v0);
auto b = std::begin(v1);
auto c = std::begin(eager0);
auto d = std::begin(eager1);
while (a != std::end(v0))
{
ASSERT_NE(b, std::end(v1));
ASSERT_NE(c, std::end(eager0));
ASSERT_NE(d, std::end(eager1));
EXPECT_EQ(recon(*a, *b), recon(*c, *d));
++a;
++b;
++c;
++d;
}
EXPECT_EQ(b, std::end(v1));
EXPECT_EQ(c, std::end(eager0));
EXPECT_EQ(d, std::end(eager1));
}
}
TEST(DeferWrap, MultiOplUnalignedWrappingMatchesEager)
{
using In = std::uint8_t;
using Out = std::uint64_t;
constexpr In from{0xF1}, to{0x0E};
constexpr Out beta{0x55};
auto [d0, d1] = dpf::make_dpf(dpf::wildcard_value<In>{}, beta);
ASSERT_GT(decltype(d0)::outputs_per_leaf, std::size_t{1});
auto buf0 = dpf::make_output_buffer_for_full(d0);
auto buf1 = dpf::make_output_buffer_for_full(d1);
auto def0 = dpf::defer_eval_interval(d0, from, to, buf0);
auto def1 = dpf::defer_eval_interval(d1, from, to, buf1);
assign_input_local(d0, d1, In{0xA3});
auto eager_buf0 = dpf::make_output_buffer_for_interval(d0, from, to);
auto eager_buf1 = dpf::make_output_buffer_for_interval(d1, from, to);
auto eager0 = dpf::eval_interval(d0, from, to, eager_buf0);
auto eager1 = dpf::eval_interval(d1, from, to, eager_buf1);
auto v0 = def0.get();
auto v1 = def1.get();
auto a = std::begin(v0);
auto b = std::begin(v1);
auto c = std::begin(eager0);
auto d = std::begin(eager1);
for_inclusive_wrap(from, to, [&](In x)
{
ASSERT_NE(a, std::end(v0));
ASSERT_NE(b, std::end(v1));
ASSERT_NE(c, std::end(eager0));
ASSERT_NE(d, std::end(eager1));
EXPECT_EQ(recon(*a, *b), recon(*c, *d)) << "x=" << +x;
EXPECT_EQ(recon(*a, *b),
recon(*dpf::eval_point(d0, x), *dpf::eval_point(d1, x)));
++a;
++b;
++c;
++d;
});
}
// ---------------------------------------------------------------------------
// Inner-product wrap already covered; keep a wildcard-offset variant
// ---------------------------------------------------------------------------
TEST(InnerProductWrap, WildcardOffsetWrappingMatchesPoints)
{
using In = std::uint8_t;
constexpr In from{250}, to{4};
auto [k0, k1] = dpf::make_dpf(dpf::wildcard_value<In>{}, std::uint32_t{9});
assign_input_local(k0, k1, In{1});
std::vector<std::uint32_t> w;
std::uint64_t expect = 0;
for_inclusive_wrap(from, to, [&](In x)
{
w.push_back(static_cast<std::uint32_t>(w.size() + 1));
expect += static_cast<std::uint64_t>(
recon(*dpf::eval_point(k0, x), *dpf::eval_point(k1, x)))
* w.back();
});
EXPECT_EQ(recon(
dpf::eval_inner_product(dpf::paired, k0, from, to, w),
dpf::eval_inner_product(dpf::paired, k1, from, to, w)),
expect);
}

View file

@ -0,0 +1,350 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <limits>
#include <stdexcept>
#include <tuple>
#include <utility>
#include <vector>
#include "dpf.hpp"
#include "dpf/yao.hpp"
#include "dpf/yao_share.hpp"
namespace
{
template <typename Ring>
Ring xor_bits(const std::vector<std::uint8_t> & a, const std::vector<std::uint8_t> & b)
{
Ring v{};
for (std::size_t i = 0; i < a.size() && i < 8u * sizeof(Ring); ++i)
v = static_cast<Ring>(v | (static_cast<Ring>(a[i] ^ b[i]) << i));
return v;
}
template <typename Ring>
Ring low_mask(unsigned width)
{
if (width == 0 || width >= 8u * sizeof(Ring))
return static_cast<Ring>(~Ring{0});
return static_cast<Ring>((Ring{1} << width) - Ring{1});
}
template <typename Ring>
void expect_additive(Ring secret, unsigned width)
{
const unsigned bits = width == 0 ? static_cast<unsigned>(8u * sizeof(Ring)) : width;
const Ring r = dpf::uniform_sample<Ring>();
const auto a0 = dpf::additive_share<Ring, 0>::from_raw(r);
const auto a1 = dpf::additive_share<Ring, 1>::from_raw(static_cast<Ring>(secret - r));
const auto [y0, y1] = dpf::yao::a2y(a0, a1, width);
ASSERT_EQ(y0.size(), bits);
ASSERT_EQ(y1.size(), bits);
EXPECT_EQ(xor_bits<Ring>(y0, y1), static_cast<Ring>(secret & low_mask<Ring>(bits)));
const auto [b0, b1] = dpf::yao::y2a<Ring>(y0, y1, bits);
EXPECT_EQ(dpf::reconstruct(b0, b1), static_cast<Ring>(secret & low_mask<Ring>(bits)));
}
template <typename Ring>
void expect_subtractive(Ring s0, Ring s1, unsigned width)
{
const unsigned bits = width == 0 ? static_cast<unsigned>(8u * sizeof(Ring)) : width;
const Ring opened = static_cast<Ring>(s0 - s1);
const auto b0 = dpf::subtractive_share<Ring, 0>::from_raw(s0);
const auto b1 = dpf::subtractive_share<Ring, 1>::from_raw(s1);
const auto [y0, y1] = dpf::yao::b2y(b0, b1, width);
EXPECT_EQ(xor_bits<Ring>(y0, y1), static_cast<Ring>(opened & low_mask<Ring>(bits)));
const auto [z0, z1] = dpf::yao::y2b<Ring>(y0, y1, bits);
EXPECT_EQ(dpf::reconstruct(z0, z1), static_cast<Ring>(opened & low_mask<Ring>(bits)));
const auto f0 = b0.as_fss();
const auto f1 = b1.as_fss();
const auto [g0, g1] = dpf::yao::fss2y(f0, f1, width);
EXPECT_EQ(xor_bits<Ring>(g0, g1), static_cast<Ring>(opened & low_mask<Ring>(bits)));
const auto [h0, h1] = dpf::yao::y2fss<Ring>(g0, g1, bits);
EXPECT_EQ(dpf::reconstruct(h0, h1), static_cast<Ring>(opened & low_mask<Ring>(bits)));
}
} // namespace
TEST(YaoShare, WidthSweep)
{
const unsigned widths64[] = {1, 2, 7, 8, 9, 16, 31, 32, 63, 64, 0};
for (unsigned w : widths64)
expect_additive<std::uint64_t>(0x1234abcd5ull, w);
const unsigned widths32[] = {1, 8, 31, 32, 0};
for (unsigned w : widths32)
expect_additive<std::uint32_t>(0x89abcdefu, w);
expect_additive<std::uint16_t>(0xBEEFu, 0);
expect_additive<std::uint16_t>(0xBEEFu, 9);
expect_additive<std::uint8_t>(0xA5u, 0);
expect_additive<std::uint8_t>(0xA5u, 3);
}
TEST(YaoShare, CornerSecrets)
{
expect_additive<std::uint64_t>(0, 64);
expect_additive<std::uint64_t>(1, 64);
expect_additive<std::uint64_t>(~std::uint64_t{0}, 64);
expect_additive<std::uint64_t>(std::uint64_t{1} << 63, 64);
expect_additive<std::uint64_t>(0x105u, 8);
expect_subtractive<std::uint64_t>(0, 0, 64);
expect_subtractive<std::uint64_t>(1, 3, 64);
expect_subtractive<std::uint64_t>(3, 1, 16);
expect_subtractive<std::uint32_t>(0, 1, 32);
expect_subtractive<std::uint8_t>(0x10, 0x10, 8);
}
TEST(YaoShare, RandomRoundTrips)
{
for (int i = 0; i < 24; ++i)
{
const auto secret = dpf::uniform_sample<std::uint64_t>();
expect_additive<std::uint64_t>(secret, 64);
const auto s0 = dpf::uniform_sample<std::uint32_t>();
const auto s1 = dpf::uniform_sample<std::uint32_t>();
expect_subtractive<std::uint32_t>(s0, s1, 32);
}
}
TEST(YaoShare, BitFlipChangesThatPlace)
{
const auto a0 = dpf::additive_share<std::uint64_t, 0>::from_raw(0x111u);
const auto a1 = dpf::additive_share<std::uint64_t, 1>::from_raw(0x222u);
auto [y0, y1] = dpf::yao::a2y(a0, a1, 12);
const auto opened = xor_bits<std::uint64_t>(y0, y1);
for (unsigned i = 0; i < 12; ++i)
{
auto flipped = y0;
flipped[i] = static_cast<std::uint8_t>(flipped[i] ^ 1u);
EXPECT_EQ(xor_bits<std::uint64_t>(flipped, y1), opened ^ (std::uint64_t{1} << i));
const auto [b0, b1] = dpf::yao::y2a<std::uint64_t>(flipped, y1, 12);
EXPECT_EQ(dpf::reconstruct(b0, b1), opened ^ (std::uint64_t{1} << i));
}
}
TEST(YaoShare, DefaultWidthUsesTheVector)
{
const auto a0 = dpf::additive_share<std::uint16_t, 0>::from_raw(0x00FFu);
const auto a1 = dpf::additive_share<std::uint16_t, 1>::from_raw(0);
const auto [y0, y1] = dpf::yao::a2y(a0, a1, 8);
const auto [b0, b1] = dpf::yao::y2a<std::uint16_t>(y0, y1);
EXPECT_EQ(dpf::reconstruct(b0, b1), 0x00FFu);
}
TEST(YaoShare, RejectsBadInputs)
{
const auto a0 = dpf::additive_share<std::uint64_t, 0>::from_raw(1);
const auto a1 = dpf::additive_share<std::uint64_t, 1>::from_raw(0);
EXPECT_THROW(dpf::yao::a2y(a0, a1, 65), std::invalid_argument);
EXPECT_THROW(dpf::yao::a2y(
dpf::additive_share<std::uint8_t, 0>::from_raw(1),
dpf::additive_share<std::uint8_t, 1>::from_raw(0), 9),
std::invalid_argument);
std::vector<std::uint8_t> bits(8, 0);
std::vector<std::uint8_t> shortv(3, 0);
EXPECT_THROW(dpf::yao::y2a<std::uint64_t>(shortv, bits, 8), std::invalid_argument);
bits[4] = 2;
EXPECT_THROW(dpf::yao::y2a<std::uint64_t>(bits, std::vector<std::uint8_t>(8, 0), 8),
std::invalid_argument);
const auto rss = dpf::make_replicated_shares<std::uint32_t>(1, 2, 3);
auto r0 = std::get<0>(rss);
r0.next = 99;
EXPECT_THROW(dpf::yao::rss2y(r0, std::get<1>(rss)), std::invalid_argument);
}
TEST(YaoShare, ReplicatedManyAndFresh)
{
for (int i = 0; i < 12; ++i)
{
const auto x0 = dpf::uniform_sample<std::uint32_t>();
const auto x1 = dpf::uniform_sample<std::uint32_t>();
const auto x2 = dpf::uniform_sample<std::uint32_t>();
const auto secret = static_cast<std::uint32_t>(x0 + x1 + x2);
const auto shares = dpf::make_replicated_shares(x0, x1, x2);
const auto [y0, y1] = dpf::yao::rss2y(std::get<0>(shares), std::get<1>(shares));
EXPECT_EQ(xor_bits<std::uint32_t>(y0, y1), secret);
const auto back = dpf::yao::y2rss<std::uint32_t>(y0, y1);
EXPECT_EQ(dpf::reconstruct(std::get<0>(back), std::get<1>(back)), secret);
EXPECT_EQ(dpf::reconstruct(std::get<1>(back), std::get<2>(back)), secret);
EXPECT_EQ(dpf::reconstruct(std::get<0>(back), std::get<2>(back)), secret);
const auto again = dpf::yao::y2rss<std::uint32_t>(y0, y1);
EXPECT_NE(std::get<0>(back).own, std::get<0>(again).own);
}
}
TEST(YaoShare, ConcreteAdditiveSum)
{
const auto a0 = dpf::additive_share<std::uint16_t, 0>::from_raw(0x1234);
const auto a1 = dpf::additive_share<std::uint16_t, 1>::from_raw(0x0100);
const auto [y0, y1] = dpf::yao::a2y(a0, a1, 16);
EXPECT_EQ(xor_bits<std::uint16_t>(y0, y1), static_cast<std::uint16_t>(0x1334));
}
TEST(YaoShare, PointLeafOnAndOff)
{
for (int n = 0; n < 16; ++n)
{
const auto alpha = dpf::uniform_sample<std::uint8_t>();
const auto beta = dpf::uniform_sample<std::uint32_t>();
auto [k0, k1] = dpf::make_dpf(alpha, beta);
auto s0 = *dpf::eval_point(k0, alpha);
auto s1 = *dpf::eval_point(k1, alpha);
const auto [y0, y1] = dpf::yao::b2y(s0, s1, 32);
EXPECT_EQ(xor_bits<std::uint32_t>(y0, y1), beta);
const auto [z0, z1] = dpf::yao::y2b<std::uint32_t>(y0, y1, 32);
EXPECT_EQ(dpf::reconstruct(z0, z1), beta);
const auto other = static_cast<std::uint8_t>(alpha + 1u);
auto t0 = *dpf::eval_point(k0, other);
auto t1 = *dpf::eval_point(k1, other);
const auto [u0, u1] = dpf::yao::b2y(t0, t1, 32);
EXPECT_EQ(xor_bits<std::uint32_t>(u0, u1), 0u);
}
}
TEST(YaoShare, PointLeafPlusPublicAndSecondLeaf)
{
const std::uint8_t alpha = 9;
const std::uint32_t beta = 40;
auto [k0, k1] = dpf::make_dpf(alpha, beta);
auto s0 = *dpf::eval_point(k0, alpha);
auto s1 = *dpf::eval_point(k1, alpha);
s0 += std::uint32_t{5};
const auto [y0, y1] = dpf::yao::b2y(s0, s1, 32);
EXPECT_EQ(xor_bits<std::uint32_t>(y0, y1), 45u);
auto [j0, j1] = dpf::make_dpf(alpha, std::uint32_t{7});
auto u0 = *dpf::eval_point(k0, alpha) + *dpf::eval_point(j0, alpha);
auto u1 = *dpf::eval_point(k1, alpha) + *dpf::eval_point(j1, alpha);
const auto [v0, v1] = dpf::yao::b2y(u0, u1, 32);
EXPECT_EQ(xor_bits<std::uint32_t>(v0, v1), 47u);
}
TEST(YaoShare, ComparisonLeaf)
{
const std::uint32_t alpha = 100u;
const std::uint64_t yt = 5u;
const std::uint64_t yf = 9u;
auto [k0, k1] = dpf::make_dpf(alpha, dpf::lt(yt, yf));
const std::uint64_t mask = k0.cmp().mask;
auto c0 = dpf::eval_point(dpf::cmp, k0, 50u);
auto c1 = dpf::eval_point(dpf::cmp, k1, 50u);
const auto [y0, y1] = dpf::yao::a2y(c0, c1, 64);
const auto bits = xor_bits<std::uint64_t>(y0, y1);
EXPECT_EQ(bits, dpf::reconstruct(c0, c1));
EXPECT_EQ(bits & mask, yt);
auto d0 = dpf::eval_point(dpf::cmp, k0, alpha);
auto d1 = dpf::eval_point(dpf::cmp, k1, alpha);
const auto [z0, z1] = dpf::yao::a2y(d0, d1, 64);
EXPECT_EQ(xor_bits<std::uint64_t>(z0, z1) & mask, yf);
}
TEST(YaoShare, LeafBitsThroughANetlist)
{
const std::uint8_t alpha = 42;
const std::uint32_t beta = 0x6bu;
auto [k0, k1] = dpf::make_dpf(alpha, beta);
auto s0 = *dpf::eval_point(k0, alpha);
auto s1 = *dpf::eval_point(k1, alpha);
const auto [p0, p1] = dpf::yao::b2y(s0, s1, 8);
dpf::yao::netlist n;
dpf::yao::bit in[8];
for (int i = 0; i < 8; ++i)
in[i] = n.shared_in();
n.out(n.and_(in[0], in[1]));
for (int i = 0; i < 8; ++i)
n.out(in[i]);
auto [o0, o1] = dpf::yao::eval_pair(n, p0.data(), p1.data());
ASSERT_EQ(o0.size(), 9u);
EXPECT_EQ(o0[0] ^ o1[0], static_cast<std::uint8_t>((beta & 1u) & ((beta >> 1) & 1u)));
for (int i = 0; i < 8; ++i)
EXPECT_EQ(o0[static_cast<std::size_t>(i + 1)] ^ o1[static_cast<std::size_t>(i + 1)],
static_cast<std::uint8_t>((beta >> i) & 1u));
std::vector<std::uint8_t> and0{o0[0]};
std::vector<std::uint8_t> and1{o1[0]};
const auto [z0, z1] = dpf::yao::y2b<std::uint32_t>(and0, and1, 1);
EXPECT_EQ(dpf::reconstruct(z0, z1), 1u);
}
TEST(YaoShare, RandomNetlistMatchesPlain)
{
std::uint32_t rng = 0xC0FFEEu;
auto nxt = [&] {
rng = rng * 1664525u + 1013904223u;
return rng;
};
for (int trial = 0; trial < 20; ++trial)
{
dpf::yao::netlist n;
dpf::yao::bit w[24];
std::uint8_t sem[4];
std::uint8_t p0[4];
std::uint8_t p1[4];
int nwire = 4;
for (int i = 0; i < 4; ++i)
{
w[i] = n.shared_in();
sem[i] = static_cast<std::uint8_t>(nxt() & 1u);
p0[i] = static_cast<std::uint8_t>(nxt() & 1u);
p1[i] = static_cast<std::uint8_t>(sem[i] ^ p0[i]);
}
for (int g = 0; g < 32; ++g)
{
const int ia = static_cast<int>(nxt() % static_cast<std::uint32_t>(nwire));
const int ib = static_cast<int>(nxt() % static_cast<std::uint32_t>(nwire));
dpf::yao::bit d;
switch (nxt() % 3u)
{
case 0: d = n.xor_(w[ia], w[ib]); break;
case 1: d = n.and_(w[ia], w[ib]); break;
default: d = n.not_(w[ia]); break;
}
if (nwire < 24)
w[nwire++] = d;
}
for (int i = 0; i < 4; ++i)
n.out(w[nwire - 1 - i]);
const auto plain = dpf::yao::eval_plain(n, sem);
EXPECT_EQ(dpf::yao::eval_local(n, sem), plain) << trial;
auto [a, b] = dpf::yao::eval_pair(n, p0, p1);
for (std::size_t i = 0; i < plain.size(); ++i)
EXPECT_EQ(static_cast<std::uint8_t>(a[i] ^ b[i]), plain[i]) << trial;
}
}
TEST(YaoShare, MixedPrivateInputs)
{
dpf::yao::netlist n;
const auto a = n.priv_in(0);
const auto b = n.priv_in(1);
const auto c = n.shared_in();
n.out(n.xor_(n.and_(a, b), c));
n.out(n.xor_public(c, 0));
n.out(n.not_(n.not_(a)));
const std::uint8_t p0[3] = {1, 0, 1};
const std::uint8_t p1[3] = {9, 1, 0};
auto [o0, o1] = dpf::yao::eval_pair(n, p0, p1);
EXPECT_EQ(static_cast<std::uint8_t>(o0[0] ^ o1[0]), 0u);
EXPECT_EQ(static_cast<std::uint8_t>(o0[1] ^ o1[1]), 1u);
EXPECT_EQ(static_cast<std::uint8_t>(o0[2] ^ o1[2]), 1u);
}
TEST(YaoShare, NetlistRejects)
{
dpf::yao::netlist n;
EXPECT_THROW(n.priv_in(2), std::invalid_argument);
EXPECT_THROW(n.xor_(dpf::yao::bit{3}, dpf::yao::bit{0}), std::invalid_argument);
const auto a = n.shared_in();
n.out(a);
EXPECT_THROW(n.shared_in(), std::logic_error);
std::uint8_t bad = 2;
EXPECT_THROW(dpf::yao::eval_local(n, &bad), std::invalid_argument);
}

View file

@ -0,0 +1,314 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <vector>
#include "dpf/yao.hpp"
#include "dpf/yao_stack.hpp"
namespace
{
using dpf::yao::bit;
using dpf::yao::netlist;
netlist and_n(unsigned n)
{
netlist nl;
std::vector<bit> in;
in.reserve(n);
for (unsigned i = 0; i < n; ++i)
in.push_back(nl.shared_in());
bit acc = in[0];
for (unsigned i = 1; i < n; ++i)
acc = nl.and_(acc, in[i]);
nl.out(acc);
return nl;
}
netlist xor2()
{
netlist nl;
auto a = nl.shared_in();
auto b = nl.shared_in();
nl.out(nl.xor_(a, b));
return nl;
}
std::uint8_t plain_one(const netlist & nl, const std::vector<std::uint8_t> & in)
{
return dpf::yao::eval_plain(nl, in.data())[0];
}
} // namespace
TEST(YaoStack, IfCostsTheHeavierBranch)
{
auto heavy = and_n(3);
auto light = and_n(2);
EXPECT_EQ(heavy.n_and(), 2u);
EXPECT_EQ(light.n_and(), 1u);
std::vector<std::uint8_t> h0{1, 1, 1};
std::vector<std::uint8_t> h1{0, 0, 0};
std::vector<std::uint8_t> l0{1, 1};
std::vector<std::uint8_t> l1{0, 1};
for (std::uint8_t c0 = 0; c0 < 2; ++c0)
{
for (std::uint8_t c1 = 0; c1 < 2; ++c1)
{
auto got = dpf::yao::eval_if(heavy, light, c0, c1,
h0.data(), h1.data(), l0.data(), l1.data());
EXPECT_EQ(got.stack_blocks, 4u);
EXPECT_EQ(got.naive_blocks, 6u);
EXPECT_LT(got.stack_blocks, got.naive_blocks);
const std::uint8_t sem = static_cast<std::uint8_t>(c0 ^ c1);
const std::uint8_t want = (sem == 0) ? plain_one(heavy, {1, 1, 1})
: plain_one(light, {1, 0});
ASSERT_EQ(got.share0.size(), 1u);
EXPECT_EQ(static_cast<std::uint8_t>(got.share0[0] ^ got.share1[0]), want);
}
}
}
TEST(YaoStack, OneHotCostsTheHeaviestBranch)
{
auto a = and_n(2);
auto b = xor2();
auto c = and_n(4);
EXPECT_EQ(a.n_and(), 1u);
EXPECT_EQ(b.n_and(), 0u);
EXPECT_EQ(c.n_and(), 3u);
std::vector<netlist> branches;
branches.push_back(a);
branches.push_back(b);
branches.push_back(c);
std::vector<std::vector<std::uint8_t>> p0{
{1, 1},
{1, 0},
{1, 1, 1, 0},
};
std::vector<std::vector<std::uint8_t>> p1{
{0, 0},
{0, 1},
{0, 0, 0, 0},
};
for (std::uint16_t idx = 0; idx < 3; ++idx)
{
auto got = dpf::yao::eval_one_hot(branches, idx, 0, p0, p1);
EXPECT_EQ(got.stack_blocks, 6u);
EXPECT_EQ(got.naive_blocks, 8u);
std::vector<std::uint8_t> sem(p0[idx].size());
for (std::size_t i = 0; i < sem.size(); ++i)
sem[i] = static_cast<std::uint8_t>(p0[idx][i] ^ p1[idx][i]);
const std::uint8_t want = plain_one(branches[idx], sem);
EXPECT_EQ(static_cast<std::uint8_t>(got.share0[0] ^ got.share1[0]), want);
}
}
namespace
{
std::uint8_t xor_share(const std::vector<std::uint8_t> & a,
const std::vector<std::uint8_t> & b, std::size_t i)
{
return static_cast<std::uint8_t>(a[i] ^ b[i]);
}
netlist not1()
{
netlist nl;
nl.out(nl.not_(nl.shared_in()));
return nl;
}
netlist two_outs()
{
netlist nl;
auto a = nl.shared_in();
auto b = nl.shared_in();
nl.out(nl.and_(a, b));
nl.out(nl.xor_(a, b));
return nl;
}
netlist not_and_id()
{
netlist nl;
auto a = nl.shared_in();
nl.shared_in();
nl.out(nl.not_(a));
nl.out(a);
return nl;
}
netlist priv_and()
{
netlist nl;
auto p = nl.priv_in(0);
auto s = nl.shared_in();
nl.out(nl.and_(p, s));
return nl;
}
} // namespace
TEST(YaoStack, EveryShareSplitOfBothBranches)
{
auto then_nl = and_n(2);
auto else_nl = not1();
for (std::uint8_t c0 = 0; c0 < 2; ++c0)
{
for (std::uint8_t c1 = 0; c1 < 2; ++c1)
{
for (std::uint8_t a0 = 0; a0 < 2; ++a0)
{
for (std::uint8_t a1 = 0; a1 < 2; ++a1)
{
for (std::uint8_t b0 = 0; b0 < 2; ++b0)
{
for (std::uint8_t b1 = 0; b1 < 2; ++b1)
{
for (std::uint8_t e0 = 0; e0 < 2; ++e0)
{
for (std::uint8_t e1 = 0; e1 < 2; ++e1)
{
const std::uint8_t tp0[2] = {a0, b0};
const std::uint8_t tp1[2] = {a1, b1};
const std::uint8_t ep0[1] = {e0};
const std::uint8_t ep1[1] = {e1};
auto got = dpf::yao::eval_if(then_nl, else_nl, c0, c1,
tp0, tp1, ep0, ep1);
const std::uint8_t sem =
static_cast<std::uint8_t>(c0 ^ c1);
std::uint8_t want = 0;
if (sem == 0)
{
const std::uint8_t in[2] = {
xor_share({a0}, {a1}, 0),
static_cast<std::uint8_t>(b0 ^ b1),
};
want = plain_one(then_nl, {in[0], in[1]});
}
else
{
want = plain_one(else_nl,
{static_cast<std::uint8_t>(e0 ^ e1)});
}
EXPECT_EQ(static_cast<std::uint8_t>(
got.share0[0] ^ got.share1[0]),
want);
EXPECT_EQ(got.stack_blocks, 2u);
EXPECT_EQ(got.naive_blocks, 2u);
}
}
}
}
}
}
}
}
}
TEST(YaoStack, TwoOutputsAndAPrivateInput)
{
auto then_nl = two_outs();
auto else_nl = not_and_id();
const std::uint8_t tp0[2] = {1, 0};
const std::uint8_t tp1[2] = {1, 1};
const std::uint8_t ep0[2] = {0, 1};
const std::uint8_t ep1[2] = {1, 0};
auto got = dpf::yao::eval_if(then_nl, else_nl, 1, 0, tp0, tp1, ep0, ep1);
ASSERT_EQ(got.share0.size(), 2u);
const std::uint8_t sem_in[2] = {static_cast<std::uint8_t>(1 ^ 1),
static_cast<std::uint8_t>(0 ^ 1)};
auto want = dpf::yao::eval_plain(then_nl, sem_in);
for (std::size_t i = 0; i < 2; ++i)
EXPECT_EQ(static_cast<std::uint8_t>(got.share0[i] ^ got.share1[i]), want[i]);
auto priv = priv_and();
auto neg = not1();
const std::uint8_t pp0[2] = {1, 1};
const std::uint8_t pp1[2] = {0, 1};
const std::uint8_t np0[1] = {0};
const std::uint8_t np1[1] = {0};
auto branched = dpf::yao::eval_if(priv, neg, 0, 1, pp0, pp1, np0, np1);
EXPECT_EQ(static_cast<std::uint8_t>(branched.share0[0] ^ branched.share1[0]),
plain_one(neg, {0}));
}
TEST(YaoStack, EmptyBranchesCostNothing)
{
netlist left;
netlist right;
auto a = left.shared_in();
left.out(left.xor_(a, left.shared_in()));
auto b = right.shared_in();
right.out(right.not_(right.xor_(b, right.shared_in())));
const std::uint8_t z[2] = {1, 1};
auto got = dpf::yao::eval_if(left, right, 0, 0, z, z, z, z);
EXPECT_EQ(got.stack_blocks, 0u);
EXPECT_EQ(got.naive_blocks, 0u);
EXPECT_EQ(static_cast<std::uint8_t>(got.share0[0] ^ got.share1[0]), 0);
}
TEST(YaoStack, OneHotEveryIndexAndShare)
{
std::vector<netlist> branches;
for (unsigned i = 0; i < 8; ++i)
branches.push_back(i % 2 == 0 ? and_n(2) : xor2());
const std::uint8_t patterns[2][2] = {{0, 0}, {1, 1}};
for (std::uint16_t index = 0; index < 8; ++index)
{
for (std::uint16_t mask = 0; mask < 2; ++mask)
{
const std::uint16_t p0 = static_cast<std::uint16_t>(index ^ mask);
const std::uint16_t p1 = mask;
std::vector<std::vector<std::uint8_t>> in0(8), in1(8);
for (unsigned b = 0; b < 8; ++b)
{
in0[b] = {patterns[b & 1][0], static_cast<std::uint8_t>(b & 1u)};
in1[b] = {patterns[b & 1][1], 1};
}
auto got = dpf::yao::eval_one_hot(branches, p0, p1, in0, in1);
EXPECT_EQ(got.stack_blocks, 2u);
EXPECT_EQ(got.naive_blocks, 8u);
EXPECT_LT(got.stack_blocks, got.naive_blocks);
std::vector<std::uint8_t> sem{
static_cast<std::uint8_t>(in0[index][0] ^ in1[index][0]),
static_cast<std::uint8_t>(in0[index][1] ^ in1[index][1]),
};
EXPECT_EQ(static_cast<std::uint8_t>(got.share0[0] ^ got.share1[0]),
plain_one(branches[index], sem));
}
}
}
TEST(YaoStack, RejectsBadShapes)
{
auto a = and_n(2);
auto b = not1();
const std::uint8_t bit = 1;
EXPECT_THROW(dpf::yao::eval_if(a, b, 2, 0, &bit, &bit, &bit, &bit),
std::invalid_argument);
netlist one;
one.out(one.shared_in());
netlist two;
auto t0 = two.shared_in();
auto t1 = two.shared_in();
two.out(t0);
two.out(t1);
EXPECT_THROW(dpf::yao::eval_if(one, two, 0, 0, &bit, &bit, &bit, &bit),
std::invalid_argument);
std::vector<netlist> only{a};
EXPECT_THROW(dpf::yao::eval_one_hot(only, 0, 0, {{}}, {{}}), std::invalid_argument);
std::vector<netlist> too_many(9, a);
EXPECT_THROW(dpf::yao::eval_one_hot(too_many, 0, 0,
std::vector<std::vector<std::uint8_t>>(9),
std::vector<std::vector<std::uint8_t>>(9)),
std::invalid_argument);
std::vector<netlist> pair{a, xor2()};
EXPECT_THROW(dpf::yao::eval_one_hot(pair, 2, 0, {{1, 1}, {1, 1}}, {{0, 0}, {0, 0}}),
std::invalid_argument);
EXPECT_THROW(dpf::yao::eval_one_hot(pair, 0, 0, {{1}, {1, 1}}, {{0, 0}, {0, 0}}),
std::invalid_argument);
}

421
test/tests/yao_test.cpp Normal file
View file

@ -0,0 +1,421 @@
#include <gtest/gtest.h>
#include <array>
#include <cstdint>
#include <cstring>
#include <exception>
#include <filesystem>
#include <string>
#include <thread>
#include <unistd.h>
#include <utility>
#include <vector>
#include "aes_mmo_ref.hpp"
#include "dpf/net/trio.hpp"
#include "dpf/prg_aes.hpp"
#include "dpf/random.hpp"
#include "dpf/yao.hpp"
#include "dpf/yao_aes.hpp"
#include "dpf/verifiable.hpp"
#include "dpf/yao_share.hpp"
namespace
{
using dpf::yao::bit;
using dpf::yao::bits8;
using dpf::yao::netlist;
void bytes_to_bits(const std::uint8_t * bytes, int nbytes, std::uint8_t * bits)
{
for (int i = 0; i < nbytes; ++i)
for (int k = 0; k < 8; ++k)
bits[i * 8 + k] = static_cast<std::uint8_t>((bytes[i] >> (7 - k)) & 1u);
}
void bits_to_bytes(const std::uint8_t * bits, int nbytes, std::uint8_t * bytes)
{
for (int i = 0; i < nbytes; ++i)
{
bytes[i] = 0;
for (int k = 0; k < 8; ++k)
bytes[i] = static_cast<std::uint8_t>(bytes[i] | (bits[i * 8 + k] << (7 - k)));
}
}
std::uint8_t pack_byte(const std::uint8_t * bits)
{
std::uint8_t b = 0;
bits_to_bytes(bits, 1, &b);
return b;
}
netlist sbox_netlist()
{
netlist n;
const bits8 in = dpf::yao::shared_byte(n);
dpf::yao::out_byte(n, dpf::yao::sbox(n, in));
return n;
}
void expand_key(const std::uint8_t key[16], std::uint8_t rk[11][16])
{
std::uint8_t w[176];
std::memcpy(w, key, 16);
int n = 16;
std::uint8_t rcon = 1;
while (n < 176)
{
std::uint8_t t[4];
std::memcpy(t, w + n - 4, 4);
if (n % 16 == 0)
{
const std::uint8_t tmp = t[0];
t[0] = t[1];
t[1] = t[2];
t[2] = t[3];
t[3] = tmp;
for (int i = 0; i < 4; ++i)
t[i] = dpf::party::aes_ref::sbox_at(t[i]);
t[0] = static_cast<std::uint8_t>(t[0] ^ rcon);
rcon = dpf::party::aes_ref::xtime(rcon);
}
for (int i = 0; i < 4; ++i)
{
w[n] = static_cast<std::uint8_t>(w[n - 16] ^ t[i]);
++n;
}
}
std::memcpy(rk, w, 176);
}
void encrypt_aes128(std::uint8_t s[16], const std::uint8_t rk[11][16])
{
using namespace dpf::party::aes_ref;
add_round_key(s, rk[0]);
for (int r = 1; r < 10; ++r)
{
sub_bytes(s);
shift_rows(s);
mix_columns(s);
add_round_key(s, rk[r]);
}
sub_bytes(s);
shift_rows(s);
add_round_key(s, rk[10]);
}
std::vector<std::uint8_t> reconstruct(std::vector<std::uint8_t> a,
const std::vector<std::uint8_t> & b)
{
EXPECT_EQ(a.size(), b.size());
for (std::size_t i = 0; i < a.size(); ++i)
a[i] = static_cast<std::uint8_t>(a[i] ^ b[i]);
return a;
}
template <typename Fn0, typename Fn1>
void run_pair(Fn0 && fn0, Fn1 && fn1)
{
const auto dir = std::filesystem::temp_directory_path()
/ ("libdpf_yao_" + std::to_string(::getpid()));
std::filesystem::create_directories(dir);
std::exception_ptr ep0;
std::exception_ptr ep1;
std::thread t0([&] {
try
{
auto net = dpf::net::trio::connect_pair(dpf::net::role::p0, dir.string());
fn0(net);
}
catch (...)
{
ep0 = std::current_exception();
}
});
std::thread t1([&] {
try
{
auto net = dpf::net::trio::connect_pair(dpf::net::role::p1, dir.string());
fn1(net);
}
catch (...)
{
ep1 = std::current_exception();
}
});
t0.join();
t1.join();
std::filesystem::remove_all(dir);
if (ep0)
std::rethrow_exception(ep0);
if (ep1)
std::rethrow_exception(ep1);
}
} // namespace
TEST(Yao, AndXorNot)
{
netlist n;
const bit a = n.shared_in();
const bit b = n.shared_in();
const bit x = n.xor_(a, b);
const bit y = n.and_(a, b);
const bit z = n.not_(a);
const bit w = n.xnor_(a, b);
n.out(x);
n.out(y);
n.out(z);
n.out(w);
n.out(n.xor_public(y, 1));
EXPECT_EQ(n.n_and(), 1u);
for (int av = 0; av < 2; ++av)
{
for (int bv = 0; bv < 2; ++bv)
{
const std::uint8_t in[2] = {
static_cast<std::uint8_t>(av), static_cast<std::uint8_t>(bv)};
const auto plain = dpf::yao::eval_plain(n, in);
const auto got = dpf::yao::eval_local(n, in);
const std::uint8_t expect[5] = {
static_cast<std::uint8_t>(av ^ bv),
static_cast<std::uint8_t>(av & bv),
static_cast<std::uint8_t>(av ^ 1),
static_cast<std::uint8_t>(1 ^ av ^ bv),
static_cast<std::uint8_t>((av & bv) ^ 1)};
EXPECT_EQ(plain, (std::vector<std::uint8_t>(expect, expect + 5)));
EXPECT_EQ(got, plain);
}
}
}
TEST(Yao, SboxPlainAndGarbled)
{
const netlist n = sbox_netlist();
EXPECT_EQ(n.n_and(), static_cast<std::uint32_t>(aes_bp::and_count));
for (int x = 0; x < 256; ++x)
{
const auto byte = static_cast<std::uint8_t>(x);
std::uint8_t bits[8];
bytes_to_bits(&byte, 1, bits);
const auto plain = dpf::yao::eval_plain(n, bits);
const auto garbled = dpf::yao::eval_local(n, bits);
EXPECT_EQ(pack_byte(plain.data()), dpf::party::aes_ref::sbox_at(byte)) << x;
EXPECT_EQ(garbled, plain) << x;
}
}
TEST(Yao, SboxShares)
{
const netlist n = sbox_netlist();
const std::uint8_t byte = 0x53;
std::uint8_t semantic[8];
bytes_to_bits(&byte, 1, semantic);
std::uint8_t p0[8], p1[8];
for (int i = 0; i < 8; ++i)
{
p0[i] = static_cast<std::uint8_t>(dpf::uniform_sample<unsigned char>() & 1u);
p1[i] = static_cast<std::uint8_t>(semantic[i] ^ p0[i]);
}
auto [a, b] = dpf::yao::eval_pair(n, p0, p1);
EXPECT_EQ(pack_byte(reconstruct(a, b).data()), dpf::party::aes_ref::sbox_at(byte));
}
TEST(Yao, Aes128Nist)
{
const std::uint8_t key[16] = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f};
const std::uint8_t pt[16] = {
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff};
const std::uint8_t ct[16] = {
0x69, 0xc4, 0xe0, 0xd8, 0x6a, 0x7b, 0x04, 0x30,
0xd8, 0xcd, 0xb7, 0x80, 0x70, 0xb4, 0xc5, 0x5a};
std::uint8_t rk[11][16];
expand_key(key, rk);
std::uint8_t state[16];
std::memcpy(state, pt, 16);
encrypt_aes128(state, rk);
EXPECT_EQ(std::vector<std::uint8_t>(state, state + 16),
std::vector<std::uint8_t>(ct, ct + 16));
const netlist n = dpf::yao::aes128_netlist();
EXPECT_EQ(n.n_and(), 6400u);
std::uint8_t bits[256];
bytes_to_bits(pt, 16, bits);
bytes_to_bits(key, 16, bits + 128);
const auto plain = dpf::yao::eval_plain(n, bits);
std::uint8_t got[16];
bits_to_bytes(plain.data(), 16, got);
EXPECT_EQ(std::vector<std::uint8_t>(got, got + 16),
std::vector<std::uint8_t>(ct, ct + 16));
const auto garbled = dpf::yao::eval_local(n, bits);
bits_to_bytes(garbled.data(), 16, got);
EXPECT_EQ(std::vector<std::uint8_t>(got, got + 16),
std::vector<std::uint8_t>(ct, ct + 16));
}
TEST(Yao, AesMmoMatchesPrg)
{
const std::uint32_t positions[] = {0u, 1u, 0x01020304u};
const std::uint8_t msg[16] = {
0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c};
simde__m128i block{};
std::memcpy(&block, msg, 16);
std::uint8_t bits[128];
dpf::yao::block_to_bits(bits, block);
for (std::uint32_t pos : positions)
{
const auto expect = dpf::prg::aes128::eval(block, pos);
const netlist n = dpf::yao::aes_mmo_netlist(pos);
EXPECT_EQ(n.n_and(), 5120u);
const auto got_bits = dpf::yao::eval_local(n, bits);
const auto got = dpf::yao::bits_to_block(got_bits.data());
std::uint8_t a[16], b[16];
std::memcpy(a, &expect, 16);
std::memcpy(b, &got, 16);
EXPECT_EQ(std::vector<std::uint8_t>(a, a + 16),
std::vector<std::uint8_t>(b, b + 16)) << pos;
}
}
TEST(Yao, SessionSboxAndReuse)
{
const netlist gates = [] {
netlist n;
const bit a = n.priv_in(0);
const bit b = n.priv_in(1);
const bit c = n.shared_in();
n.out(n.xor_(n.and_(a, b), c));
n.out(n.not_(c));
return n;
}();
const netlist priv_xor = [] {
netlist n;
const bit a = n.priv_in(0);
const bit b = n.priv_in(0);
n.out(n.xor_(a, b));
return n;
}();
const netlist box = sbox_netlist();
const std::uint8_t byte = 0xa7;
std::uint8_t semantic[8];
bytes_to_bits(&byte, 1, semantic);
std::uint8_t sp0[8], sp1[8];
for (int i = 0; i < 8; ++i)
{
sp0[i] = static_cast<std::uint8_t>(i & 1u);
sp1[i] = static_cast<std::uint8_t>(semantic[i] ^ sp0[i]);
}
std::vector<std::uint8_t> g0, g1, s0, s1, x0, x1;
run_pair(
[&](dpf::net::trio & net) {
dpf::yao::session s;
const std::uint8_t xin[2] = {1, 1};
x0 = s.eval(0, priv_xor, xin, net.to(dpf::net::role::p1));
const std::uint8_t in0[3] = {1, 0, 1};
g0 = s.eval(0, gates, in0, net.to(dpf::net::role::p1));
const std::uint8_t in1[3] = {1, 0, 0};
auto second = s.eval(0, gates, in1, net.to(dpf::net::role::p1));
g0.insert(g0.end(), second.begin(), second.end());
s0 = s.eval(0, box, sp0, net.to(dpf::net::role::p1));
},
[&](dpf::net::trio & net) {
dpf::yao::session s;
const std::uint8_t xin[2] = {0, 0};
x1 = s.eval(1, priv_xor, xin, net.to(dpf::net::role::p0));
const std::uint8_t in0[3] = {0, 1, 0};
g1 = s.eval(1, gates, in0, net.to(dpf::net::role::p0));
const std::uint8_t in1[3] = {0, 0, 1};
auto second = s.eval(1, gates, in1, net.to(dpf::net::role::p0));
g1.insert(g1.end(), second.begin(), second.end());
s1 = s.eval(1, box, sp1, net.to(dpf::net::role::p0));
});
EXPECT_EQ(reconstruct(x0, x1), std::vector<std::uint8_t>{0});
const auto opened = reconstruct(g0, g1);
const std::uint8_t expect_gates[4] = {0, 0, 1, 0};
EXPECT_EQ(std::vector<std::uint8_t>(opened.begin(), opened.begin() + 4),
std::vector<std::uint8_t>(expect_gates, expect_gates + 4));
EXPECT_EQ(pack_byte(reconstruct(s0, s1).data()),
dpf::party::aes_ref::sbox_at(byte));
}
TEST(Yao, LeafBitsOnSession)
{
const std::uint32_t beta = 0x6bu;
const auto s0 = dpf::subtractive_share<std::uint32_t, 0>::from_raw(beta);
const auto s1 = dpf::subtractive_share<std::uint32_t, 1>::from_raw(0);
const auto [p0, p1] = dpf::yao::b2y(s0, s1, 8);
const dpf::yao::netlist n = [] {
dpf::yao::netlist c;
dpf::yao::bit in[8];
for (int i = 0; i < 8; ++i)
in[i] = c.shared_in();
c.out(c.and_(in[0], in[1]));
return c;
}();
std::vector<std::uint8_t> o0, o1;
bool locked = false;
run_pair(
[&](dpf::net::trio & net) {
dpf::yao::session s;
auto & link = net.to(dpf::net::role::p1);
o0 = s.eval(0, n, p0.data(), link);
try
{
s.eval(1, n, p0.data(), link);
}
catch (const std::logic_error &)
{
locked = true;
}
},
[&](dpf::net::trio & net) {
dpf::yao::session s;
o1 = s.eval(1, n, p1.data(), net.to(dpf::net::role::p0));
});
EXPECT_TRUE(locked);
ASSERT_EQ(o0.size(), 1u);
EXPECT_EQ(static_cast<std::uint8_t>(o0[0] ^ o1[0]), 1u);
const auto [z0, z1] = dpf::yao::y2b<std::uint32_t>(o0, o1, 1);
EXPECT_EQ(dpf::reconstruct(z0, z1), 1u);
}
TEST(Yao, CorrectionLevelMatchesMakeCs)
{
const simde__m128i s0 = dpf::uniform_sample<simde__m128i>();
const simde__m128i s1 = dpf::uniform_sample<simde__m128i>();
const std::uint64_t prefix = 0x0123456789abcdefull;
const std::uint64_t share0 = 0x1111222233334444ull;
const std::uint64_t share1 = prefix ^ share0;
const std::size_t level = 0x10ffu;
std::uint8_t in0[dpf::yao::correction_level_in_bits];
std::uint8_t in1[dpf::yao::correction_level_in_bits];
dpf::yao::pack_correction_level(0, s0, share0, level, in0);
dpf::yao::pack_correction_level(1, s1, share1, level, in1);
const auto nl = dpf::yao::correction_level_netlist();
EXPECT_EQ(nl.n_and(), dpf::yao::correction_level_ands);
EXPECT_EQ(nl.n_in(), static_cast<std::uint32_t>(dpf::yao::correction_level_in_bits));
auto [a, b] = dpf::yao::eval_pair(nl, in0, in1);
ASSERT_EQ(a.size(), dpf::yao::correction_level_out_bits);
const auto expect = dpf::detail::vdpf::make_cs(level, prefix, s0, s1);
for (int lane = 0; lane < 4; ++lane)
{
std::uint8_t bits[128];
for (int i = 0; i < 128; ++i)
bits[i] = static_cast<std::uint8_t>(
a[static_cast<std::size_t>(lane * 128 + i)]
^ b[static_cast<std::size_t>(lane * 128 + i)]);
const auto got = dpf::yao::bits_to_block(bits);
EXPECT_EQ(std::memcmp(&got, &expect[static_cast<std::size_t>(lane)], 16), 0)
<< lane;
}
}