/// @file grotto/offset_repr.hpp /// @brief Representation shift: advance a linear-recurrence state by a public offset. /// @details Offset Horner is the unipotent (Pascal) case of a shift-invariant /// module. Here the dealer keys one incremental comparison whose /// payload is the state vector /// \f$S_c\in(\mathbb{Z}/2^{64})^d\f$ at the hidden center. The seed /// spine is stored once. After `eta` /// opens, each refined piece has a public carry `kappa`, and the /// parties apply the public matrix power /// \f$S_{c+\kappa}=M^{\kappa}S_c.\f$ /// Negative `kappa` uses \f$M^{-1}\f$ (the determinant must be a unit). /// The wrap branch is multiplication by the public constant /// \f$M^{\mp 2^n}\f$; when \f$M^{2^n}=I\f$ it is the identity. /// /// Fibonacci is the companion matrix of \f$T^2-T-1\f$, via the Lucas /// addition formula \f$F_{c+\kappa}=F_{\kappa}F_{c+1}+F_{\kappa-1}F_c\f$. /// A \f$1\times 1\f$ matrix \f$[\lambda]\f$ is a geometric jump /// \f$\lambda^{c+\kappa}=\lambda^{\kappa}\lambda^{c}\f$. CRC / LFSR /// resume over \f$\mathrm{GF}(2)\f$ is the same checkpoint with XOR /// shares; the cleartext jump helper documents that twin. /// @copyright Copyright (c) 2019-2026 Ryan Henry and [others](@ref authors) /// @license Released under a GNU General Public v2.0 (GPLv2) license. #ifndef LIBDPF_INCLUDE_GROTTO_OFFSET_REPR_HPP__ #define LIBDPF_INCLUDE_GROTTO_OFFSET_REPR_HPP__ #include "grotto/offset_poly.hpp" #include #include #include #include #include #include #include namespace grotto { inline constexpr std::size_t offset_repr_max_dim = 8; template struct offset_repr_keys { static_assert(std::is_integral_v, "offset repr domain must be an integer group"); using input_type = InputT; std::size_t dim = 0; bool verifiable = false; /// @brief One `idcf(gt)` of `vec`. offset_horner_detail::lane_keys keys{}; offset_horner_detail::lane_keys keys_v{}; std::vector> wrap_share; }; namespace offset_repr_detail { inline void check_dim(std::size_t dim) { if (dim == 0 || dim > offset_repr_max_dim) throw std::invalid_argument("offset repr: dimension must be in 1..8"); } inline void check_square(const std::vector> & M, std::size_t dim) { if (M.size() != dim) throw std::invalid_argument("offset repr: matrix row count differs from dim"); for (const auto & row : M) { if (row.size() != dim) throw std::invalid_argument("offset repr: matrix is not square"); } } inline std::vector> identity(std::size_t dim) { std::vector> I(dim, std::vector(dim, 0)); for (std::size_t i = 0; i < dim; ++i) I[i][i] = 1; return I; } inline std::vector> matmul( const std::vector> & A, const std::vector> & B) { const std::size_t n = A.size(); std::vector> C(n, std::vector(n, 0)); for (std::size_t i = 0; i < n; ++i) for (std::size_t k = 0; k < n; ++k) for (std::size_t j = 0; j < n; ++j) C[i][j] += A[i][k] * B[k][j]; return C; } inline std::vector matvec( const std::vector> & M, const std::vector & v) { const std::size_t n = M.size(); std::vector out(n, 0); for (std::size_t i = 0; i < n; ++i) for (std::size_t j = 0; j < n; ++j) out[i] += M[i][j] * v[j]; return out; } inline uint64_t inv_u64(uint64_t a) { if ((a & 1u) == 0) throw std::invalid_argument("offset repr: matrix determinant is not a unit"); uint64_t x = a; x *= 2 - a * x; x *= 2 - a * x; x *= 2 - a * x; x *= 2 - a * x; x *= 2 - a * x; return x; } /// @brief Matrix inverse over \f$\mathbb{Z}/2^{64}\f$ by Gauss–Jordan. inline std::vector> inverse( std::vector> A) { const std::size_t n = A.size(); auto I = identity(n); for (std::size_t k = 0; k < n; ++k) { std::size_t piv = k; while (piv < n && (A[piv][k] & 1u) == 0) ++piv; if (piv == n) throw std::invalid_argument("offset repr: matrix is not invertible over Z/2^64"); std::swap(A[piv], A[k]); std::swap(I[piv], I[k]); const uint64_t inv = inv_u64(A[k][k]); for (std::size_t j = 0; j < n; ++j) { A[k][j] *= inv; I[k][j] *= inv; } for (std::size_t i = 0; i < n; ++i) { if (i == k) continue; const uint64_t f = A[i][k]; for (std::size_t j = 0; j < n; ++j) { A[i][j] -= f * A[k][j]; I[i][j] -= f * I[k][j]; } } } return I; } } // namespace offset_repr_detail /// @brief \f$M^{e}\f$ for a signed exponent, over \f$\mathbb{Z}/2^{64}\f$. /// \complexity If `exp < 0`, one Gauss–Jordan inverse (`inverse`, three nested loops over `n = M.size()`). /// Then one squaring per bit of `|exp|`: the `while (e != 0)` loop, each step two `matmul`s (`Θ(n³)` each). `exp` is an `int64_t`, so at most 63 squarings. /// Extra space `Θ(n²)` for `acc` and `base`. This function does not apply `offset_repr_max_dim`; callers that go through `check_dim` use `n ≤ 8`. /// @see grotto::offset_repr_eval inline std::vector> offset_repr_matrix_pow( std::vector> M, std::int64_t exp) { const std::size_t n = M.size(); offset_repr_detail::check_square(M, n); if (exp < 0) { M = offset_repr_detail::inverse(std::move(M)); exp = -exp; } auto acc = offset_repr_detail::identity(n); auto base = std::move(M); auto e = static_cast(exp); while (e != 0) { if (e & 1u) acc = offset_repr_detail::matmul(acc, base); base = offset_repr_detail::matmul(base, base); e >>= 1; } return acc; } /// @brief Companion matrix of Fibonacci: \f$S_n=(F_{n+1},F_n)\f$, \f$S_{n+1}=MS_n\f$. /// \complexity Returns a 2×2 constant. `Θ(1)`. /// @see grotto::offset_repr_fibonacci_state inline std::vector> offset_repr_fibonacci_matrix() { return {{1, 1}, {1, 0}}; } /// @brief Fibonacci state at index `n`: \f$(F_{n+1}, F_n)\f$ in \f$\mathbb{Z}/2^{64}\f$. /// \complexity One `offset_repr_matrix_pow` of the 2×2 companion at exponent `n`, then a matrix-vector product. `Θ(log n)` 2×2 multiplies. /// @see grotto::offset_repr_matrix_pow inline std::vector offset_repr_fibonacci_state(std::uint64_t n) { if (n == 0) return {1, 0}; // F_1=1, F_0=0 auto M = offset_repr_matrix_pow(offset_repr_fibonacci_matrix(), static_cast(n)); // S_0 = (1, 0); S_n = M^n S_0. return offset_repr_detail::matvec(M, {1, 0}); } /// @brief \f$1\times 1\f$ geometric matrix \f$[\lambda]\f$. /// \complexity Returns a 1×1 matrix. `Θ(1)`. /// @see grotto::offset_repr_matrix_pow inline std::vector> offset_repr_geometric_matrix(uint64_t lambda) { return {{lambda}}; } /// @brief Cleartext CRC-32 jump by `steps` bits (ISO / Ethernet polynomial). /// @details Public twin of a GF(2) representation shift: one register word, /// multiply by \f$x^{\mathrm{steps}}\f$ in \f$\mathrm{GF}(2)[x]/(p)\f$. /// Keyed CRC wants XOR shares of the register; this helper is the /// public \f$A^{\kappa}\f$ factor those shares would be multiplied by. /// \complexity Builds a `64 × 32` jump table once (function-local static), then /// applies one table lookup per set bit of `steps` (at most 64). /// Extra space is that `64 × 32` table of `uint32_t`. Cleartext; no keys. /// @see grotto::offset_repr_eval /// @note This is the public twin. It does not take XOR shares. inline std::uint32_t offset_repr_crc32_jump(std::uint32_t state, std::uint64_t steps) { constexpr std::uint32_t poly = 0xEDB88320u; // jump[k][i] = image of the singleton bit `1<, 64> table{}; auto step1 = [](std::uint32_t s) { return (s >> 1) ^ (poly & (0u - (s & 1u))); }; for (unsigned i = 0; i < 32; ++i) table[0][i] = step1(std::uint32_t{1u} << i); for (unsigned k = 1; k < 64; ++k) for (unsigned i = 0; i < 32; ++i) { std::uint32_t s = table[k - 1][i]; std::uint32_t out = 0; for (unsigned b = 0; b < 32; ++b) if ((s >> b) & 1u) out ^= table[k - 1][b]; table[k][i] = out; } return table; }(); for (unsigned k = 0; k < 64; ++k) { if (((steps >> k) & 1u) == 0) continue; std::uint32_t nxt = 0; for (unsigned b = 0; b < 32; ++b) if ((state >> b) & 1u) nxt ^= jump[k][b]; state = nxt; } return state; } /// \complexity One `dpf::make_dpf` of `idcf(gt)` on a `dim`-lane vector. `check_dim` requires the length in `1 .. offset_repr_max_dim` (8). /// The seed spine is one key. Value words grow with `dim`. /// \rounds No party interaction. /// \communication None inside this function. /// \preprocessing One comparison key and `dim` additive splits of the state lanes. /// @see grotto::offset_horner_eval /// @see [Representation shift](@ref offset_repr) template offset_repr_keys make_offset_repr_keys( InputT center, const std::vector & state) { using namespace offset_horner_detail; offset_repr_detail::check_dim(state.size()); offset_repr_keys mat; mat.dim = state.size(); mat.verifiable = false; mat.wrap_share.reserve(mat.dim); for (std::size_t i = 0; i < mat.dim; ++i) { const uint64_t blind = dpf::uniform_sample(); mat.wrap_share.push_back({blind, state[i] - blind}); } mat.keys = offset_horner_detail::make_lane_keys( center, state.data(), state.size()); return mat; } /// \complexity One `dpf::make_dpf` of `idcf(gt)` on a `dim`-lane vector. `check_dim` requires the length in `1 .. offset_repr_max_dim` (8). /// The seed spine is one key. Value words grow with `dim`. /// \rounds No party interaction. /// \communication None inside this function. /// \preprocessing One comparison key and `dim` additive splits of the state lanes. /// @see grotto::offset_horner_eval /// @see [Representation shift](@ref offset_repr) template offset_repr_keys make_offset_repr_keys( InputT center, const std::vector & state, dpf::verifiable) { using namespace offset_horner_detail; offset_repr_detail::check_dim(state.size()); offset_repr_keys mat; mat.dim = state.size(); mat.verifiable = true; mat.wrap_share.reserve(mat.dim); for (std::size_t i = 0; i < mat.dim; ++i) { const uint64_t blind = dpf::uniform_sample(); mat.wrap_share.push_back({blind, state[i] - blind}); } mat.keys_v = offset_horner_detail::make_lane_keys( center, state.data(), state.size()); return mat; } /// @brief Shares of \f$S_c\f$ on every refined piece (pre-advance), for one party. /// \complexity One segment walk of the `dim`-lane payload (`dim ≤ 8`) over the refined pieces. Extra space `Θ(P · dim)`. /// \rounds None. /// \communication None. /// \preprocessing None created here. /// @see grotto::offset_repr_eval template std::vector> offset_repr_state_shares( const offset_repr_keys & mat, const std::vector & knots, InputT eta, dpf::proof_token * tokens = nullptr) { static_assert(Party < 2, "offset repr party is 0 or 1"); using namespace offset_horner_detail; using namespace offset_poly_detail; const std::size_t lanes = mat.verifiable ? mat.keys_v.index() : mat.keys.index(); if (lanes != mat.dim) throw std::invalid_argument("offset repr: comparison payload width differs from the state"); std::vector> dummy(knots.size(), std::vector(mat.dim, 0)); const auto pieces = prepare(knots, dummy, eta); std::vector shifted; for (const auto & piece : pieces) shifted.push_back(piece.knot); dpf::proof_token * pi = (mat.verifiable && tokens != nullptr) ? &tokens[0] : nullptr; auto out = mat.verifiable ? lane_segments(mat.keys_v, shifted, mat.wrap_share, pi) : lane_segments(mat.keys, shifted, mat.wrap_share, nullptr); if (mat.verifiable) replicate_proof(tokens, mat.dim); return out; } /// @brief One party's share of \f$M^{\kappa}S_c\f$ at the wrapped point. /// \complexity One segment walk of the `dim`-lane payload (`dim ≤ 8`), then `offset_repr_matrix_pow(M, kappa)` and a matrix-vector product on every refined piece. /// `offset_repr_matrix_pow` squares a `dim × dim` matrix once per bit of `|kappa|` (`matmul` is three nested `dim` loops). /// Refined pieces: the knot vector, plus the cuts `prepare` / `prepare_pieces` inserts (domain minimum, and the carry threshold when the input width is at most 62 bits). Call that count `P`. /// Time is one walk plus `Θ(P · bitlength(kappa) · dim³)` field operations. Extra space `Θ(P · dim)` for the state table and `Θ(dim²)` for the matrix powers. /// \rounds None. `eta` is an argument. /// \communication None. /// \preprocessing None created here. Uses the keys from `make_offset_repr_keys`. /// @note A negative `kappa` inverts `M`. `inverse` rejects an even determinant (not a unit in `Z/2^64`). /// @see grotto::offset_horner_eval /// @see [Representation shift](@ref offset_repr) template std::vector offset_repr_eval( const offset_repr_keys & mat, const std::vector> & M, const std::vector & knots, InputT eta, dpf::proof_token * tokens = nullptr) { offset_repr_detail::check_square(M, mat.dim); using namespace offset_poly_detail; std::vector> dummy(knots.size(), std::vector(mat.dim, 0)); const auto pieces = prepare(knots, dummy, eta); const auto states = offset_repr_state_shares(mat, knots, eta, tokens); std::vector out(mat.dim, 0); for (std::size_t p = 0; p < pieces.size(); ++p) { const auto Mk = offset_repr_matrix_pow(M, pieces[p].kappa); const auto advanced = offset_repr_detail::matvec(Mk, states[p]); for (std::size_t i = 0; i < mat.dim; ++i) out[i] += advanced[i]; } return out; } /// @brief Cleartext \f$M^{\kappa}S_c\f$ on the hot piece. /// \complexity Same matrix powers as `offset_repr_eval`, but on the single hot piece and with the clear state instead of shares. `Θ(bitlength(kappa) · dim³)`. /// @see grotto::offset_repr_eval template std::vector offset_repr_clear( InputT center, const std::vector & state, const std::vector> & M, const std::vector & knots, InputT eta) { offset_repr_detail::check_dim(state.size()); offset_repr_detail::check_square(M, state.size()); using namespace offset_poly_detail; std::vector> dummy(knots.size(), std::vector(state.size(), 0)); const auto pieces = prepare(knots, dummy, eta); std::vector cuts; for (const auto & piece : pieces) cuts.push_back(piece.knot); std::size_t hot = pieces.size() - 1; for (std::size_t i = 0; i + 1 < cuts.size(); ++i) { if (center >= cuts[i] && center < cuts[i + 1]) { hot = i; break; } } const auto Mk = offset_repr_matrix_pow(M, pieces[hot].kappa); return offset_repr_detail::matvec(Mk, state); } } // namespace grotto #endif // LIBDPF_INCLUDE_GROTTO_OFFSET_REPR_HPP__