#include #include #include "dpf.hpp" #include "grotto/offset_horner.hpp" #include "grotto/prefix_parity.hpp" #include #include #include #include #include #include #include #include #include namespace { using grotto::offset_horner_group_add; using grotto::offset_horner_group_sub; template uint64_t lift(T v) { if constexpr (std::is_signed_v) return static_cast(static_cast(v)); else return static_cast(v); } template int circular_piece(T point, const std::vector & knots) { if (knots.size() <= 1) return 0; for (std::size_t i = 0; i + 1 < knots.size(); ++i) { if (point >= knots[i] && point < knots[i + 1]) return static_cast(i); } return static_cast(knots.size() - 1); } template uint64_t power_sum(const std::array & a, uint64_t point) { uint64_t acc = 0; uint64_t p = 1; for (std::size_t m = 0; m <= Degree; ++m) { acc += a[m] * p; p *= point; } return acc; } template std::array binomial_shift( const std::array & a, uint64_t center) { static constexpr uint64_t binom[4][4] = { {1, 0, 0, 0}, {1, 1, 0, 0}, {1, 2, 1, 0}, {1, 3, 3, 1}, }; std::array c{}; for (std::size_t m = 0; m <= Degree; ++m) { for (std::size_t k = 0; k <= m; ++k) { uint64_t cmk = 1; for (std::size_t t = 0; t < m - k; ++t) cmk *= center; c[k] += a[m] * binom[m][k] * cmk; } } return c; } template const std::array & coeff_of_wrapped( T center, T eta, const std::vector & knots, const std::vector> & coeff) { struct row { T knot; std::size_t id; }; std::vector rows(knots.size()); for (std::size_t i = 0; i < knots.size(); ++i) rows[i] = row{offset_horner_group_sub(knots[i], eta), i}; std::sort(rows.begin(), rows.end(), [](const row & a, const row & b) { return a.knot < b.knot; }); std::vector shifted(rows.size()); for (std::size_t i = 0; i < rows.size(); ++i) shifted[i] = rows[i].knot; const int hot = circular_piece(center, shifted); return coeff[rows[static_cast(hot)].id]; } template uint64_t gold(T center, T eta, const std::vector & knots, const std::vector> & coeff) { const T wrapped = offset_horner_group_add(center, eta); const auto & a = coeff[static_cast(circular_piece(wrapped, knots))]; return power_sum(a, lift(wrapped)); } template uint64_t party_eval(const grotto::offset_horner_keys & mat, const std::vector & knots, const std::vector> & coeff, T eta) { return grotto::offset_horner_eval(mat, knots, coeff, eta); } template uint64_t open_eval(const grotto::offset_horner_keys & mat, const std::vector & knots, const std::vector> & coeff, T eta) { return party_eval<0, Degree>(mat, knots, coeff, eta) + party_eval<1, Degree>(mat, knots, coeff, eta); } template std::array open_coeffs( const grotto::offset_horner_keys & mat, const std::vector & knots, const std::vector> & coeff, T eta) { auto a = grotto::offset_horner_coefficient_share<0, Degree>(mat, knots, coeff, eta); auto b = grotto::offset_horner_coefficient_share<1, Degree>(mat, knots, coeff, eta); for (std::size_t k = 0; k <= Degree; ++k) a[k] += b[k]; return a; } inline std::vector> pad3( std::initializer_list> rows) { std::vector> out; out.reserve(rows.size()); for (const auto & row : rows) { std::array a{}; std::size_t k = 0; for (uint64_t v : row) { if (k >= 4) break; a[k++] = v; } out.push_back(a); } return out; } template std::vector> take_degree( const std::vector> & rows) { std::vector> out(rows.size()); for (std::size_t i = 0; i < rows.size(); ++i) for (std::size_t k = 0; k <= Degree; ++k) out[i][k] = rows[i][k]; return out; } } // namespace TEST(OffsetHorner, BinomialAgreesWithPowerSum) { const std::array a{5, 0, 1, 2}; const auto c = binomial_shift<3>(a, 3); EXPECT_EQ(c[0], 68u); EXPECT_EQ(c[1], 60u); EXPECT_EQ(c[2], 19u); EXPECT_EQ(c[3], 2u); uint64_t y = 0; uint64_t p = 1; for (uint64_t ck : c) { y += ck * p; p *= 4; } EXPECT_EQ(y, 740u); EXPECT_EQ(power_sum<3>(a, 7), 740u); } TEST(OffsetHorner, HandCubicAtCenterPlusEta) { constexpr std::size_t D = 3; const std::vector knots{0}; const auto coeff = take_degree(pad3({{5, 0, 1, 2}})); const uint8_t center = 3; const uint8_t eta = 4; auto mat = grotto::make_offset_horner_keys(center); EXPECT_EQ(open_eval(mat, knots, coeff, eta), 740u); EXPECT_EQ(grotto::offset_horner_clear(center, knots, coeff, eta), 740u); const auto got = open_coeffs(mat, knots, coeff, eta); uint64_t summed = 0; for (uint64_t term : got) summed += term; EXPECT_EQ(summed, 740u); const auto q = grotto::offset_horner_clear_coefficients(center, knots, coeff, eta); EXPECT_EQ(q, binomial_shift(coeff[0], lift(eta))); // Each party evaluates from its own shares and the public eta. const uint64_t p0 = party_eval<0, D>(mat, knots, coeff, eta); const uint64_t p1 = party_eval<1, D>(mat, knots, coeff, eta); EXPECT_EQ(p0 + p1, 740u); EXPECT_NE(p0, 740u); } TEST(OffsetHorner, MultiPieceSelectsWrappedInput) { constexpr std::size_t D = 3; const std::vector knots{0, 10, 50}; const auto coeff = take_degree(pad3({ {1, 0, 0, 0}, {0, 2, 0, 0}, {7, 0, 0, 1}, })); const uint8_t center = 12; const uint8_t eta = 3; auto mat = grotto::make_offset_horner_keys(center); const uint64_t want = gold(center, eta, knots, coeff); EXPECT_EQ(want, 30u); EXPECT_EQ(open_eval(mat, knots, coeff, eta), want); } TEST(OffsetHorner, CarrySplitEvaluatesTheWrappedRepresentative) { constexpr std::size_t D = 1; const std::vector knots{0, 30, 80}; const auto coeff = take_degree(pad3({ {0, 1, 0, 0}, {0, 2, 0, 0}, {9, 0, 0, 0}, })); const uint8_t center = 144; const uint8_t eta = 156; const uint8_t wrapped = offset_horner_group_add(center, eta); EXPECT_EQ(wrapped, 44); EXPECT_EQ(lift(center) + lift(eta), 300u); auto mat = grotto::make_offset_horner_keys(center); const uint64_t got = open_eval(mat, knots, coeff, eta); EXPECT_EQ(got, gold(center, eta, knots, coeff)); EXPECT_EQ(got, 88u); EXPECT_NE(got, 600u); const std::vector sknots{-128, 0}; const auto scoeff = take_degree(pad3({ {0, 3, 0, 0}, {5, 0, 0, 0}, })); const int8_t sc = 100; const int8_t se = 100; const int8_t sw = offset_horner_group_add(sc, se); EXPECT_EQ(sw, int8_t{-56}); auto smat = grotto::make_offset_horner_keys(sc); EXPECT_EQ(open_eval(smat, sknots, scoeff, se), gold(sc, se, sknots, scoeff)); EXPECT_EQ(open_eval(smat, sknots, scoeff, se), power_sum(scoeff[0], lift(sw))); } TEST(OffsetHorner, XPlusRWiring) { constexpr std::size_t D = 2; const std::vector knots{0, 40, 100}; const auto coeff = take_degree(pad3({ {3, 1, 0, 0}, {0, 0, 1, 0}, {4, 0, 0, 0}, })); const uint8_t x = 20; const uint8_t r = 6; const auto q = grotto::offset_horner_at_x_plus_r(x, r); EXPECT_EQ(q.eta, offset_horner_group_sub(x, r)); EXPECT_EQ(q.center, offset_horner_group_add(r, r)); auto mat = grotto::make_offset_horner_keys(q.center); EXPECT_EQ(open_eval(mat, knots, coeff, q.eta), gold(q.center, q.eta, knots, coeff)); } TEST(OffsetHorner, DegreesZeroOneAndTwo) { const std::vector knots{0, 20, 40}; const auto full = pad3({ {4, 0, 0, 0}, {1, 3, 0, 0}, {2, 0, 5, 0}, }); const uint8_t center = 25; const uint8_t eta = 7; { constexpr std::size_t D = 0; const auto coeff = take_degree(full); auto mat = grotto::make_offset_horner_keys(center); EXPECT_EQ(open_eval(mat, knots, coeff, eta), gold(center, eta, knots, coeff)); } { constexpr std::size_t D = 1; const auto coeff = take_degree(full); auto mat = grotto::make_offset_horner_keys(center); EXPECT_EQ(open_eval(mat, knots, coeff, eta), gold(center, eta, knots, coeff)); } { constexpr std::size_t D = 2; const auto coeff = take_degree(full); auto mat = grotto::make_offset_horner_keys(center); EXPECT_EQ(open_eval(mat, knots, coeff, eta), gold(center, eta, knots, coeff)); const auto parts = open_coeffs(mat, knots, coeff, eta); uint64_t summed = 0; for (uint64_t term : parts) summed += term; EXPECT_EQ(summed, open_eval(mat, knots, coeff, eta)); } } TEST(OffsetHorner, WholeDomainAndWrapPiece) { constexpr std::size_t D = 3; const auto only = take_degree(pad3({{8, 1, 0, 1}})); auto one = grotto::make_offset_horner_keys(uint8_t{200}); EXPECT_EQ(open_eval(one, std::vector{0}, only, uint8_t{9}), gold(uint8_t{200}, uint8_t{9}, std::vector{0}, only)); const std::vector knots{10, 20}; const auto coeff = take_degree(pad3({ {1, 0, 0, 0}, {6, 2, 0, 0}, })); for (uint8_t center : {uint8_t{0}, uint8_t{5}, uint8_t{10}, uint8_t{19}, uint8_t{20}, uint8_t{255}}) { auto mat = grotto::make_offset_horner_keys(center); for (uint8_t eta : {uint8_t{0}, uint8_t{1}, uint8_t{15}, uint8_t{200}}) EXPECT_EQ(open_eval(mat, knots, coeff, eta), gold(center, eta, knots, coeff)) << "center=" << int(center) << " eta=" << int(eta); } } TEST(OffsetHorner, CenterOrEtaZero) { constexpr std::size_t D = 3; const std::vector knots{0, 8, 16, 64, 200}; const auto coeff = take_degree(pad3({ {1, 2, 3, 4}, {5, 0, 1, 0}, {0, 0, 0, 1}, {9, 9, 0, 0}, {2, 0, 0, 0}, })); auto at0 = grotto::make_offset_horner_keys(uint8_t{0}); EXPECT_EQ(open_eval(at0, knots, coeff, uint8_t{3}), gold(uint8_t{0}, uint8_t{3}, knots, coeff)); auto at = grotto::make_offset_horner_keys(uint8_t{70}); EXPECT_EQ(open_eval(at, knots, coeff, uint8_t{0}), gold(uint8_t{70}, uint8_t{0}, knots, coeff)); } TEST(OffsetHorner, NegativeCoefficientsAndSignedDomain) { constexpr std::size_t D = 3; const std::vector knots{-128, -40, -1, 0, 20, 100}; const auto coeff = take_degree(pad3({ {uint64_t(-3), 4, 0, 1}, {0, uint64_t(-1), 2, 0}, {8, 0, 0, 0}, {1, 1, 1, 1}, {uint64_t(-5), uint64_t(-5), 0, 0}, {2, 0, uint64_t(-1), 0}, })); const int8_t center = -2; const int8_t eta = -3; auto mat = grotto::make_offset_horner_keys(center); EXPECT_EQ(open_eval(mat, knots, coeff, eta), gold(center, eta, knots, coeff)); const auto parts = open_coeffs(mat, knots, coeff, eta); uint64_t summed = 0; for (uint64_t term : parts) summed += term; EXPECT_EQ(summed, open_eval(mat, knots, coeff, eta)); auto at_max = grotto::make_offset_horner_keys(int8_t{127}); EXPECT_EQ(open_eval(at_max, knots, coeff, int8_t{-4}), gold(int8_t{127}, int8_t{-4}, knots, coeff)); auto at_min = grotto::make_offset_horner_keys(std::numeric_limits::min()); EXPECT_EQ(open_eval(at_min, knots, coeff, int8_t{1}), gold(std::numeric_limits::min(), int8_t{1}, knots, coeff)); } TEST(OffsetHorner, ShiftedKnotsThatAreNotSortedStillSelect) { constexpr std::size_t D = 2; const std::vector knots{0, 10, 20}; const auto coeff = take_degree(pad3({ {1, 0, 0, 0}, {0, 3, 0, 0}, {0, 0, 2, 0}, })); // eta = 5 rotates 0,10,20 to 251,5,15. The walk sees them sorted. const uint8_t eta = 5; for (uint8_t center : {uint8_t{3}, uint8_t{6}, uint8_t{16}, uint8_t{252}}) { auto mat = grotto::make_offset_horner_keys(center); EXPECT_EQ(open_eval(mat, knots, coeff, eta), gold(center, eta, knots, coeff)) << int(center); const uint8_t wrapped = offset_horner_group_add(center, eta); const auto & selected = coeff_of_wrapped(center, eta, knots, coeff); EXPECT_EQ(selected, coeff[static_cast(circular_piece(wrapped, knots))]); } } TEST(OffsetHorner, RingOverflowDiffersFromWideInteger) { constexpr std::size_t D = 3; const std::vector knots{0}; const std::array a{0, 0, 0, 1}; const auto coeff = std::vector>{a}; const uint32_t center = 3u << 20; const uint32_t eta = 1u << 20; using u128 = unsigned __int128; const u128 wide = u128(center) + eta; const u128 wide_p = wide * wide * wide; auto mat = grotto::make_offset_horner_keys(center); const uint64_t got = open_eval(mat, knots, coeff, eta); EXPECT_EQ(got, gold(center, eta, knots, coeff)); EXPECT_EQ(got, static_cast(wide_p)); EXPECT_NE(wide_p, u128(got)); } TEST(OffsetHorner, WrapSharesHideThePayload) { constexpr std::size_t D = 3; const uint8_t center = 9; auto mat = grotto::make_offset_horner_keys(center); uint64_t pow = 1; for (std::size_t m = 0; m <= D; ++m) { EXPECT_EQ(mat.wrap_share[m][0] + mat.wrap_share[m][1], pow); if (pow != 0) EXPECT_NE(mat.wrap_share[m][0], pow); pow *= lift(center); } } TEST(OffsetHorner, PowersShareOneSeedSpine) { constexpr std::size_t D = 3; const uint16_t center = 1000; auto mat = grotto::make_offset_horner_keys(center); 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); const std::size_t depth = std::remove_reference_t::depth; EXPECT_GT(depth, 0u); EXPECT_EQ(k0.value_cw(0), k1.value_cw(0)); } TEST(OffsetHorner, DegreeZeroMatchesSignRespectingDot) { constexpr std::size_t D = 0; const std::vector knots{0, 15, 80, 200}; const auto coeff = take_degree(pad3({ {4, 0, 0, 0}, {11, 0, 0, 0}, {uint64_t(-2), 0, 0, 0}, {9, 0, 0, 0}, })); const uint8_t center = 90; const uint8_t eta = 30; auto mat = grotto::make_offset_horner_keys(center); struct row { uint8_t knot; uint64_t a; }; std::vector rows; for (std::size_t i = 0; i < knots.size(); ++i) rows.push_back(row{offset_horner_group_sub(knots[i], eta), coeff[i][0]}); std::sort(rows.begin(), rows.end(), [](const row & a, const row & b) { return a.knot < b.knot; }); std::array shifted{}; for (std::size_t i = 0; i < rows.size(); ++i) shifted[i] = rows[i].knot; auto unit = dpf::make_dpf(center, dpf::gt(uint64_t{1})); const auto s0 = grotto::signed_segment_parities(std::get<0>(unit), shifted); const auto s1 = grotto::signed_segment_parities(std::get<1>(unit), shifted); uint64_t dot = 0; for (std::size_t i = 0; i < rows.size(); ++i) dot += (s0[i] + s1[i]) * rows[i].a; EXPECT_EQ(open_eval(mat, knots, coeff, eta), dot); EXPECT_EQ(dot, gold(center, eta, knots, coeff)); } TEST(OffsetHorner, AdviceBitSignIsNotACoefficientShare) { const std::vector knots{0, 10, 40, 90, 140, 200}; std::array ends{}; for (std::size_t i = 0; i < knots.size(); ++i) ends[i] = knots[i]; const std::array constants{3, 5, 7, 11, 13, 17}; bool saw_negative = false; bool saw_positive = false; for (int alpha = 0; alpha < 256; ++alpha) { const auto a = static_cast(alpha); auto [k0, k1] = dpf::make_dpf(a, dpf::bit::one); const auto s0 = grotto::segment_parities(k0, ends); const auto s1 = grotto::segment_parities(k1, ends); int sign = 0; int64_t acc = 0; for (std::size_t i = 0; i < ends.size(); ++i) { const int bit = int(s0[i]) - int(s1[i]); sign += bit; acc += bit * static_cast(constants[i]); } ASSERT_EQ(sign * sign, 1) << alpha; const int64_t corrected = sign * acc; const int hot = circular_piece(a, knots); ASSERT_EQ(corrected, static_cast(constants[static_cast(hot)])) << alpha; if (sign < 0) { saw_negative = true; EXPECT_EQ(acc, -corrected); } else { saw_positive = true; } } EXPECT_TRUE(saw_negative); EXPECT_TRUE(saw_positive); // The sign-respecting unit payload does not flip. const uint8_t probe = 40; auto cmp = dpf::make_dpf(probe, dpf::gt(uint64_t{1})); const auto p0 = grotto::signed_segment_parities(std::get<0>(cmp), ends); const auto p1 = grotto::signed_segment_parities(std::get<1>(cmp), ends); uint64_t opened = 0; for (std::size_t i = 0; i < ends.size(); ++i) opened += (p0[i] + p1[i]) * constants[i]; EXPECT_EQ(opened, constants[static_cast(circular_piece(probe, knots))]); } TEST(OffsetHorner, SignRespectingOneHotIsNotTheShiftedCubic) { constexpr std::size_t D = 2; const std::vector knots{0, 50, 150}; const auto coeff = take_degree(pad3({ {1, 0, 0, 0}, {0, 4, 1, 0}, {8, 0, 0, 0}, })); const uint8_t center = 10; const uint8_t eta = 60; auto mat = grotto::make_offset_horner_keys(center); const uint64_t ours = open_eval(mat, knots, coeff, eta); auto unit = dpf::make_dpf(center, dpf::gt(uint64_t{1})); std::array ends{0, 50, 150}; const auto s0 = grotto::signed_segment_parities(std::get<0>(unit), ends); const auto s1 = grotto::signed_segment_parities(std::get<1>(unit), ends); uint64_t const_term = 0; for (std::size_t i = 0; i < ends.size(); ++i) const_term += (s0[i] + s1[i]) * coeff[i][0]; // Unit payload, unshifted knots: the piece of `center`, and only its constant. EXPECT_EQ(const_term, 1u); EXPECT_NE(ours, const_term); EXPECT_EQ(ours, gold(center, eta, knots, coeff)); } TEST(OffsetHorner, PrefixIntoMatchesFixedArray) { const uint8_t alpha = 40; auto [k0, k1] = dpf::make_dpf(alpha, dpf::gt(uint64_t{7})); const std::array ends{0, 1, 10, 40, 200}; const auto fixed = grotto::signed_prefix_parities(k0, ends); uint64_t into[5] = {}; grotto::signed_prefix_parities_into(k0, ends.data(), ends.size(), into); for (std::size_t i = 0; i < ends.size(); ++i) EXPECT_EQ(into[i], fixed[i]); const uint64_t mask = k0.cmp().mask; for (std::size_t i = 0; i < ends.size(); ++i) { const auto one = std::array{ends[i]}; const auto alone0 = grotto::signed_prefix_parities(k0, one); const auto alone1 = grotto::signed_prefix_parities(k1, one); EXPECT_EQ((alone0[0] + alone1[0]) & mask, ends[i] > alpha ? 7u : 0u); } } TEST(OffsetHorner, RejectsBadKnots) { constexpr std::size_t D = 1; const auto coeff = take_degree(pad3({{1, 1, 0, 0}, {2, 0, 0, 0}})); auto mat = grotto::make_offset_horner_keys(uint8_t{1}); EXPECT_THROW(open_eval(mat, std::vector{}, coeff, uint8_t{0}), std::invalid_argument); EXPECT_THROW(open_eval(mat, std::vector{1, 1}, coeff, uint8_t{0}), std::invalid_argument); EXPECT_THROW(open_eval(mat, std::vector{3, 2}, coeff, uint8_t{0}), std::invalid_argument); EXPECT_THROW(open_eval(mat, std::vector{0}, coeff, uint8_t{0}), std::invalid_argument); } TEST(OffsetHorner, ManyPiecesAndRandomUint16) { constexpr std::size_t D = 3; std::mt19937 rng(0x0ff5e7u); std::vector knots; for (uint16_t k = 0; knots.size() < 20; k = static_cast(k + 1000)) knots.push_back(k); std::vector> coeff(knots.size()); for (auto & row : coeff) for (uint64_t & a : row) a = rng(); std::uniform_int_distribution dist(0, 65535); for (int trial = 0; trial < 30; ++trial) { const auto center = static_cast(dist(rng)); const auto eta = static_cast(dist(rng)); auto mat = grotto::make_offset_horner_keys(center); EXPECT_EQ(open_eval(mat, knots, coeff, eta), gold(center, eta, knots, coeff)) << trial; const auto parts = open_coeffs(mat, knots, coeff, eta); uint64_t summed = 0; for (uint64_t term : parts) summed += term; EXPECT_EQ(summed, gold(center, eta, knots, coeff)) << trial; } } TEST(OffsetHorner, ExhaustiveUint8AgreesWithGoldAndCountsWraps) { constexpr std::size_t D = 2; const std::vector knots{0, 30, 80, 140, 200}; const auto coeff = take_degree(pad3({ {1, 2, 0, 0}, {0, 0, 1, 0}, {4, 1, 0, 0}, {9, 0, 2, 0}, {3, 5, 0, 0}, })); int point_mismatch = 0; int value_mismatch = 0; int piece_mismatch = 0; for (int c = 0; c < 256; ++c) { const auto center = static_cast(c); auto mat = grotto::make_offset_horner_keys(center); for (int e = 0; e < 256; ++e) { const auto eta = static_cast(e); const uint64_t got = open_eval(mat, knots, coeff, eta); const uint64_t want = gold(center, eta, knots, coeff); const uint64_t cleared = grotto::offset_horner_clear(center, knots, coeff, eta); if (got != want || cleared != want) { ADD_FAILURE() << "center=" << c << " eta=" << e << " got=" << got << " clear=" << cleared << " want=" << want; return; } const uint8_t wrapped = offset_horner_group_add(center, eta); const uint64_t unreduced = lift(center) + lift(eta); if (unreduced != lift(wrapped)) ++point_mismatch; const auto & selected = coeff_of_wrapped(center, eta, knots, coeff); const auto & wrapped_row = coeff[static_cast(circular_piece(wrapped, knots))]; if (selected != wrapped_row) ++piece_mismatch; if (power_sum(selected, unreduced) != power_sum(selected, lift(wrapped))) ++value_mismatch; } } EXPECT_EQ(point_mismatch, 32640); EXPECT_EQ(piece_mismatch, 0); EXPECT_GT(value_mismatch, 0); EXPECT_LT(value_mismatch, 65536); } TEST(OffsetHorner, ExhaustiveInt8AgreesWithGoldAndCountsOverflows) { constexpr std::size_t D = 3; const std::vector knots{-128, -50, -3, 0, 40, 90}; const auto coeff = take_degree(pad3({ {1, 0, 0, 1}, {0, uint64_t(-2), 0, 0}, {5, 1, 1, 0}, {0, 0, 3, 0}, {2, 0, 0, 0}, {uint64_t(-4), 1, 0, 1}, })); int point_mismatch = 0; int piece_mismatch = 0; for (int c = -128; c <= 127; ++c) { const auto center = static_cast(c); auto mat = grotto::make_offset_horner_keys(center); for (int e = -128; e <= 127; ++e) { const auto eta = static_cast(e); const uint64_t got = open_eval(mat, knots, coeff, eta); const uint64_t want = gold(center, eta, knots, coeff); const uint64_t cleared = grotto::offset_horner_clear(center, knots, coeff, eta); if (got != want || cleared != want) { ADD_FAILURE() << "center=" << c << " eta=" << e << " got=" << got << " clear=" << cleared << " want=" << want; return; } const int8_t wrapped = offset_horner_group_add(center, eta); const uint64_t unreduced = lift(center) + lift(eta); if (unreduced != lift(wrapped)) ++point_mismatch; const auto & selected = coeff_of_wrapped(center, eta, knots, coeff); const auto & wrapped_row = coeff[static_cast(circular_piece(wrapped, knots))]; if (selected != wrapped_row) ++piece_mismatch; } } EXPECT_EQ(point_mismatch, 16384); EXPECT_EQ(piece_mismatch, 0); } TEST(OffsetHorner, GenevalXorSharesMatchTheDealerPoint) { constexpr std::size_t D = 3; const std::vector knots{0, 10, 50}; const auto coeff = take_degree(pad3({ {1, 0, 0, 0}, {0, 2, 0, 0}, {7, 0, 0, 1}, })); const uint8_t center = 12; const uint8_t share = 0x3c; const uint8_t other = static_cast(center ^ share); const uint8_t eta = 3; EXPECT_EQ(grotto::geneval_offset_horner_center(share, other), center); EXPECT_EQ(grotto::geneval_offset_horner_center(center, uint8_t{0}), center); const auto got = grotto::geneval_offset_horner(share, other, eta, knots, coeff); EXPECT_EQ(got.eta, eta); EXPECT_EQ(got.value0 + got.value1, gold(center, eta, knots, coeff)); EXPECT_EQ(got.value0 + got.value1, 30u); } TEST(OffsetHorner, GenevalFromAdditiveSharesOfXAndR) { constexpr std::size_t D = 2; const std::vector knots{0, 30, 80}; const auto coeff = take_degree(pad3({ {0, 1, 0, 0}, {0, 2, 0, 0}, {9, 0, 0, 0}, })); const uint8_t x = 100; const uint8_t r = 200; const uint8_t x0 = 7; const uint8_t r0 = 11; const uint8_t x1 = offset_horner_group_sub(x, x0); const uint8_t r1 = offset_horner_group_sub(r, r0); const auto got = grotto::geneval_offset_horner(x0, x1, r0, r1, knots, coeff); 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.value0 + got.value1, gold(center, eta, knots, coeff)); EXPECT_EQ(got.value0 + got.value1, 88u); const uint8_t wrapped = offset_horner_group_add(center, eta); EXPECT_EQ(wrapped, 44); EXPECT_EQ(got.value0 + got.value1, power_sum(coeff[static_cast(circular_piece(wrapped, knots))], lift(wrapped))); } TEST(OffsetHorner, GenevalSignedSharesUseGenevalConvention) { constexpr std::size_t D = 3; const std::vector knots{-128, -40, 0, 20, 100}; const auto coeff = take_degree(pad3({ {uint64_t(-3), 4, 0, 1}, {0, uint64_t(-1), 2, 0}, {1, 1, 1, 1}, {uint64_t(-5), uint64_t(-5), 0, 0}, {2, 0, uint64_t(-1), 0}, })); const int8_t center = -20; const int8_t share = 3; const int8_t other = static_cast(center ^ share); const int8_t eta = -3; EXPECT_EQ(grotto::geneval_offset_horner_center(share, other), center); const auto got = grotto::geneval_offset_horner(share, other, eta, knots, coeff); EXPECT_EQ(got.value0 + got.value1, gold(center, eta, knots, coeff)); const int8_t x = 40; const int8_t r = -15; const int8_t x0 = -100; const int8_t r0 = 50; const auto from_mask = grotto::geneval_offset_horner( x0, offset_horner_group_sub(x, x0), r0, offset_horner_group_sub(r, r0), 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.eta, expect_eta); EXPECT_EQ(from_mask.value0 + from_mask.value1, gold(expect_center, expect_eta, knots, coeff)); } TEST(OffsetHorner, HornerOfOpenedCoefficientsMatchesValue) { constexpr std::size_t D = 3; std::mt19937 rng(1); const std::vector knots{0, 25, 100, 180}; std::vector> coeff(knots.size()); for (auto & row : coeff) for (uint64_t & a : row) a = rng(); const uint8_t center = 77; const uint8_t eta = 19; auto mat = grotto::make_offset_horner_keys(center); const auto c = open_coeffs(mat, knots, coeff, eta); uint64_t y = 0; for (uint64_t ck : c) y += ck; EXPECT_EQ(y, open_eval(mat, knots, coeff, eta)); EXPECT_EQ(y, gold(center, eta, knots, coeff)); } TEST(OffsetHorner, OpenedSharesAreNotHornerInputs) { constexpr std::size_t D = 2; const std::vector knots{0, 50, 150}; const auto coeff = take_degree(pad3({ {1, 0, 0, 0}, {0, 4, 1, 0}, {8, 0, 0, 0}, })); const uint8_t center = 10; const uint8_t eta = 60; auto mat = grotto::make_offset_horner_keys(center); const auto c = open_coeffs(mat, knots, coeff, eta); uint64_t sum = 0; for (uint64_t ck : c) sum += ck; const uint64_t value = gold(center, eta, knots, coeff); EXPECT_EQ(sum, value); EXPECT_EQ(value, 5180u); uint64_t horner = c[D]; const uint64_t limb = lift(center); for (std::size_t k = D; k-- > 0; ) horner = horner * limb + c[k]; EXPECT_NE(horner, value); } template void expect_wrapped(const grotto::offset_horner_keys & mat, const std::vector & knots, const std::vector> & coeff, T center, T eta, const char * where) { const uint64_t want = gold(center, eta, knots, coeff); const uint64_t got = open_eval(mat, knots, coeff, eta); const uint64_t cleared = grotto::offset_horner_clear(center, knots, coeff, eta); const auto q = grotto::offset_horner_clear_coefficients(center, knots, coeff, eta); uint64_t horner = q[Degree]; const uint64_t limb = lift(center); for (std::size_t k = Degree; k-- > 0; ) horner = horner * limb + q[k]; EXPECT_EQ(got, want) << where; EXPECT_EQ(cleared, want) << where; EXPECT_EQ(horner, want) << where; if (got != want || cleared != want || horner != want) return; } template void expect_geneval(T center, T eta, const std::vector & knots, const std::vector> & coeff, const char * where) { const T share = static_cast(0x3c); const T other = static_cast(center ^ share); const auto g = grotto::geneval_offset_horner(share, other, eta, knots, coeff); const uint64_t want = gold(center, eta, knots, coeff); EXPECT_EQ(g.value0 + g.value1, want) << where; uint64_t summed = 0; for (std::size_t k = 0; k <= Degree; ++k) summed += g.coeff0[k] + g.coeff1[k]; EXPECT_EQ(summed, want) << where; } TEST(OffsetHorner, KnotsThatOmitZeroStillSplitTheCarry) { constexpr std::size_t D = 3; const std::vector knots{40, 90, 150, 220}; const auto coeff = take_degree(pad3({ {1, 0, 0, 1}, {0, uint64_t(-3), 1, 0}, {4, 2, 0, uint64_t(-1)}, {9, 0, 2, 1}, })); for (int c = 0; c < 256; ++c) { const auto center = static_cast(c); auto mat = grotto::make_offset_horner_keys(center); for (int e = 0; e < 256; e += 1) { const auto eta = static_cast(e); expect_wrapped(mat, knots, coeff, center, eta, "omit-zero"); if (HasFailure()) { ADD_FAILURE() << "center=" << c << " eta=" << e; return; } } } } TEST(OffsetHorner, SignedKnotsThatOmitTheMinimum) { constexpr std::size_t D = 3; const std::vector knots{-40, 10, 70}; const auto coeff = take_degree(pad3({ {uint64_t(-2), 1, 0, 1}, {3, 0, uint64_t(-1), 0}, {0, 4, 2, uint64_t(-3)}, })); for (int c = -128; c <= 127; ++c) { const auto center = static_cast(c); auto mat = grotto::make_offset_horner_keys(center); for (int e = -128; e <= 127; ++e) { const auto eta = static_cast(e); expect_wrapped(mat, knots, coeff, center, eta, "omit-min"); if (HasFailure()) { ADD_FAILURE() << "center=" << c << " eta=" << e; return; } } } } TEST(OffsetHorner, CarryThresholdLandsOnEveryKnotAndOnTheDomainEnds) { constexpr std::size_t D = 2; const std::vector knots{1, 16, 64, 128, 200, 255}; const auto coeff = take_degree(pad3({ {1, 1, 0, 0}, {2, 0, 1, 0}, {3, uint64_t(-1), 0, 0}, {4, 2, 2, 0}, {5, 0, 0, 0}, {6, 3, 1, 0}, })); for (uint8_t knot : knots) { if (knot == 0) continue; const uint8_t eta = static_cast(256u - knot); for (int c = 0; c < 256; ++c) { const auto center = static_cast(c); auto mat = grotto::make_offset_horner_keys(center); expect_wrapped(mat, knots, coeff, center, eta, "threshold-on-knot"); if (HasFailure()) { ADD_FAILURE() << "knot=" << int(knot) << " center=" << c; return; } } } for (uint8_t eta : {uint8_t{0}, uint8_t{1}, uint8_t{255}}) { for (uint8_t center : {uint8_t{0}, uint8_t{1}, uint8_t{254}, uint8_t{255}}) { auto mat = grotto::make_offset_horner_keys(center); expect_wrapped(mat, knots, coeff, center, eta, "domain-end"); if (HasFailure()) return; } } } TEST(OffsetHorner, DegreeZeroIsThePieceConstantOnBothSidesOfTheCarry) { constexpr std::size_t D = 0; const std::vector knots{10, 80, 200}; const auto coeff = take_degree(pad3({ {4, 0, 0, 0}, {11, 0, 0, 0}, {uint64_t(-2), 0, 0, 0}, })); for (int c = 0; c < 256; c += 3) { const auto center = static_cast(c); auto mat = grotto::make_offset_horner_keys(center); for (int e = 0; e < 256; e += 5) { const auto eta = static_cast(e); const uint64_t want = gold(center, eta, knots, coeff); EXPECT_EQ(open_eval(mat, knots, coeff, eta), want); const auto wrapped = offset_horner_group_add(center, eta); const auto piece = static_cast(circular_piece(wrapped, knots)); EXPECT_EQ(want, coeff[piece][0]); if (HasFailure()) return; } } } TEST(OffsetHorner, CubicAcrossUnsignedAndSignedCarryHasANegativeKappa) { constexpr std::size_t D = 3; const std::vector uknots{1, 70}; const auto ucoeff = take_degree(pad3({ {1, 0, 0, 1}, {2, 3, uint64_t(-1), 1}, })); const uint8_t uc = 200; const uint8_t ue = 100; const uint8_t uw = offset_horner_group_add(uc, ue); EXPECT_EQ(uw, 44); EXPECT_NE(lift(uc) + lift(ue), lift(uw)); auto umat = grotto::make_offset_horner_keys(uc); expect_wrapped(umat, uknots, ucoeff, uc, ue, "cubic-unsigned"); EXPECT_NE(open_eval(umat, uknots, ucoeff, ue), power_sum(ucoeff[static_cast(circular_piece(uw, uknots))], lift(uc) + lift(ue))); const std::vector sknots{-20, 30}; const auto scoeff = take_degree(pad3({ {0, 0, 0, 1}, {7, 1, 0, 0}, })); const int8_t sc = -100; const int8_t se = -80; const int8_t sw = offset_horner_group_add(sc, se); EXPECT_LT(int(sc) + int(se), -128); auto smat = grotto::make_offset_horner_keys(sc); expect_wrapped(smat, sknots, scoeff, sc, se, "cubic-signed-low"); EXPECT_EQ(open_eval(smat, sknots, scoeff, se), power_sum(scoeff[static_cast(circular_piece(sw, sknots))], lift(sw))); const int8_t hc = 90; const int8_t he = 80; auto hmat = grotto::make_offset_horner_keys(hc); expect_wrapped(hmat, sknots, scoeff, hc, he, "cubic-signed-high"); const int8_t hw = offset_horner_group_add(hc, he); EXPECT_GT(int(hc) + int(he), 127); EXPECT_EQ(open_eval(hmat, sknots, scoeff, he), power_sum(scoeff[static_cast(circular_piece(hw, sknots))], lift(hw))); } TEST(OffsetHorner, ZeroPolynomialAndProperShares) { constexpr std::size_t D = 3; const std::vector knots{5, 40, 90}; const auto coeff = take_degree(pad3({ {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, })); auto mat = grotto::make_offset_horner_keys(uint8_t{200}); EXPECT_EQ(open_eval(mat, knots, coeff, uint8_t{200}), 0u); EXPECT_EQ(grotto::offset_horner_clear(uint8_t{200}, knots, coeff, uint8_t{200}), 0u); const auto live = take_degree(pad3({ {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 8, 7, 6}, })); const uint64_t p0 = party_eval<0, D>(mat, knots, live, uint8_t{180}); const uint64_t p1 = party_eval<1, D>(mat, knots, live, uint8_t{180}); const uint64_t want = gold(uint8_t{200}, uint8_t{180}, knots, live); EXPECT_EQ(p0 + p1, want); EXPECT_NE(p0, want); EXPECT_NE(p1, want); } TEST(OffsetHorner, XPlusRMatchesTheWrappedSumOnAStride) { constexpr std::size_t D = 3; const std::vector knots{7, 60, 130, 210}; const auto coeff = take_degree(pad3({ {1, 1, 0, 1}, {0, uint64_t(-4), 2, 0}, {3, 0, 0, uint64_t(-1)}, {8, 2, 1, 0}, })); for (int rv = 0; rv < 256; rv += 5) { const auto r = static_cast(rv); const auto center = offset_horner_group_add(r, r); auto mat = grotto::make_offset_horner_keys(center); for (int xv = 0; xv < 256; xv += 5) { const auto x = static_cast(xv); const auto eta = offset_horner_group_sub(x, r); const uint64_t got = open_eval(mat, knots, coeff, eta); const auto sum = offset_horner_group_add(x, r); EXPECT_EQ(offset_horner_group_add(center, eta), sum); EXPECT_EQ(got, gold(center, eta, knots, coeff)); EXPECT_EQ(got, power_sum( coeff[static_cast(circular_piece(sum, knots))], lift(sum))); if (HasFailure()) { ADD_FAILURE() << "x=" << xv << " r=" << rv; return; } } } } TEST(OffsetHorner, GenevalAgreesWithDealerAcrossCarryAndEdges) { constexpr std::size_t D = 3; const std::vector knots{25, 80, 140, 200}; const auto coeff = take_degree(pad3({ {1, 0, 2, 1}, {uint64_t(-5), 3, 0, 1}, {4, 0, uint64_t(-2), 0}, {0, 1, 1, uint64_t(-1)}, })); auto check = [&](uint8_t center, uint8_t eta) { auto mat = grotto::make_offset_horner_keys(center); expect_wrapped(mat, knots, coeff, center, eta, "dealer"); expect_geneval(center, eta, knots, coeff, "geneval"); }; for (int c = 0; c < 256; c += 8) { for (int e = 0; e < 256; e += 8) { check(static_cast(c), static_cast(e)); if (HasFailure()) { ADD_FAILURE() << "center=" << c << " eta=" << e; return; } } } for (uint8_t end : {uint8_t{0}, uint8_t{1}, uint8_t{127}, uint8_t{128}, uint8_t{254}, uint8_t{255}}) { check(end, uint8_t{1}); check(end, uint8_t{255}); check(uint8_t{200}, end); check(uint8_t{3}, end); if (HasFailure()) return; } const std::vector sknots{-100, -5, 20, 90}; const auto scoeff = take_degree(pad3({ {1, 0, 0, 1}, {0, uint64_t(-1), 2, 0}, {4, 3, 0, uint64_t(-2)}, {9, 0, 1, 1}, })); for (int c = -128; c <= 127; c += 9) { for (int e = -128; e <= 127; e += 9) { const auto center = static_cast(c); const auto eta = static_cast(e); auto mat = grotto::make_offset_horner_keys(center); expect_wrapped(mat, sknots, scoeff, center, eta, "signed-dealer"); expect_geneval(center, eta, sknots, scoeff, "signed-geneval"); if (HasFailure()) { ADD_FAILURE() << "center=" << c << " eta=" << e; return; } } } } TEST(OffsetHorner, WiderRandomDomainsMatchWrappedGold) { constexpr std::size_t D = 3; std::mt19937 rng(0x0c0ffe); std::uniform_int_distribution u16(0, 65535); std::vector uknots{0, 1000, 8000, 20000, 40000, 60000}; std::vector> ucoeff(uknots.size()); for (auto & row : ucoeff) for (uint64_t & a : row) a = rng(); for (int trial = 0; trial < 40; ++trial) { const auto center = static_cast(u16(rng)); const auto eta = static_cast(u16(rng)); auto mat = grotto::make_offset_horner_keys(center); expect_wrapped(mat, uknots, ucoeff, center, eta, "u16"); expect_geneval(center, eta, uknots, ucoeff, "u16-geneval"); if (HasFailure()) return; } std::uniform_int_distribution s16(-32768, 32767); std::vector sknots{-32768, -20000, -100, 0, 5000, 30000}; std::vector> scoeff(sknots.size()); for (auto & row : scoeff) for (uint64_t & a : row) a = rng(); for (int trial = 0; trial < 40; ++trial) { const auto center = static_cast(s16(rng)); const auto eta = static_cast(s16(rng)); auto mat = grotto::make_offset_horner_keys(center); expect_wrapped(mat, sknots, scoeff, center, eta, "i16"); expect_geneval(center, eta, sknots, scoeff, "i16-geneval"); if (HasFailure()) return; } } template int64_t math_of(T value) { if constexpr (std::is_signed_v) return static_cast(value); else return static_cast(static_cast>(value)); } template bool fits_in_domain(int64_t value) { return value >= math_of(std::numeric_limits::min()) && value <= math_of(std::numeric_limits::max()); } template bool addition_leaves_domain(T center, T eta) { constexpr unsigned bits = dpf::utils::bitlength_of_v; if constexpr (bits > 62) return false; else { const int64_t sum = math_of(center) + math_of(eta); const int64_t mod = int64_t{1} << bits; if constexpr (std::is_signed_v) return sum >= (mod >> 1) || sum < -(mod >> 1); else return sum >= mod; } } template void exercise_big_domain() { constexpr std::size_t D = 3; constexpr unsigned bits = dpf::utils::bitlength_of_v; using lim = std::numeric_limits; const T minv = lim::min(); const T maxv = lim::max(); std::vector knots; if constexpr (std::is_signed_v) { knots.push_back(static_cast(minv / 2)); knots.push_back(T{-2}); knots.push_back(T{-1}); knots.push_back(T{1}); knots.push_back(T{2}); knots.push_back(static_cast(maxv / 2)); } else { using u = std::make_unsigned_t; knots.push_back(T{1}); knots.push_back(T{2}); knots.push_back(static_cast(u{1} << (bits / 2))); if (bits > 1 && bits <= 63) knots.push_back(static_cast(u{1} << (bits - 1))); knots.push_back(static_cast(maxv - 2)); knots.push_back(static_cast(maxv - 1)); } std::sort(knots.begin(), knots.end()); knots.erase(std::unique(knots.begin(), knots.end()), knots.end()); ASSERT_GE(knots.size(), 4u); ASSERT_NE(knots.front(), minv); std::vector> coeff(knots.size()); for (std::size_t i = 0; i < knots.size(); ++i) { coeff[i] = { static_cast(i + 1), static_cast(-static_cast(i) - 3), static_cast(i * 5 + 1), uint64_t{1} << (8 + (i % 4)), }; } std::vector points; auto add_point = [&](T value) { points.push_back(value); }; add_point(minv); add_point(static_cast(minv + T{1})); if constexpr (std::is_signed_v) { add_point(T{-1}); add_point(T{0}); add_point(T{1}); } else { add_point(T{0}); } add_point(static_cast(maxv - T{1})); add_point(maxv); for (T knot : knots) { add_point(knot); if (knot != minv) add_point(static_cast(knot - T{1})); if (knot != maxv) add_point(static_cast(knot + T{1})); } std::mt19937 rng(0xB16Du ^ bits ^ (std::is_signed_v ? 0x51u : 0u)); std::uniform_int_distribution dist( 0, std::numeric_limits>::max()); for (int n = 0; n < 24; ++n) add_point(static_cast(dist(rng))); std::vector etas = points; if (bits <= 62) { const int64_t mod = int64_t{1} << bits; const int64_t half = mod >> 1; for (T knot : knots) { const int64_t k = math_of(knot); if constexpr (std::is_signed_v) { if (fits_in_domain(half - k)) etas.push_back(static_cast(half - k)); if (fits_in_domain(-half - k)) etas.push_back(static_cast(-half - k)); } else if (k != 0 && fits_in_domain(mod - k)) { etas.push_back(static_cast(mod - k)); } } } std::sort(etas.begin(), etas.end()); etas.erase(std::unique(etas.begin(), etas.end()), etas.end()); std::sort(points.begin(), points.end()); points.erase(std::unique(points.begin(), points.end()), points.end()); int wraps = 0; for (T center : points) { auto mat = grotto::make_offset_horner_keys(center); for (T eta : etas) { if (addition_leaves_domain(center, eta)) ++wraps; expect_wrapped(mat, knots, coeff, center, eta, "big-dealer"); expect_geneval(center, eta, knots, coeff, "big-geneval"); if (::testing::Test::HasFailure()) { if constexpr (std::is_signed_v) ADD_FAILURE() << "signed " << bits << " center=" << static_cast(center) << " eta=" << static_cast(eta); else ADD_FAILURE() << "unsigned " << bits << " center=" << static_cast(center) << " eta=" << static_cast(eta); return; } } } if (bits <= 62) EXPECT_GT(wraps, 0) << (std::is_signed_v ? "signed " : "unsigned ") << bits; const std::vector> constants(knots.size(), {uint64_t{42}}); const T const_center = points.back(); const T const_eta = etas.front(); auto const_keys = grotto::make_offset_horner_keys(const_center); EXPECT_EQ(open_eval<0>(const_keys, knots, constants, const_eta), gold<0>(const_center, const_eta, knots, constants)); for (int n = 0; n < 8; ++n) { const T x = static_cast(dist(rng)); const T r = static_cast(dist(rng)); const T x0 = static_cast(dist(rng)); const T r0 = static_cast(dist(rng)); const T x1 = offset_horner_group_sub(x, x0); const T r1 = offset_horner_group_sub(r, r0); const auto got = grotto::geneval_offset_horner(x0, x1, r0, r1, knots, coeff); const T sum = offset_horner_group_add(x, 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(expect_center, got.eta), sum); EXPECT_EQ(got.value0 + got.value1, gold(expect_center, got.eta, knots, coeff)); if (::testing::Test::HasFailure()) return; } } TEST(OffsetHorner, BiggerDomainsExerciseCarrySplitAndGeneval) { exercise_big_domain(); if (HasFailure()) return; exercise_big_domain(); if (HasFailure()) return; exercise_big_domain(); if (HasFailure()) return; exercise_big_domain(); if (HasFailure()) return; exercise_big_domain(); if (HasFailure()) return; exercise_big_domain(); }