libdpf/include/grotto/offset_repr.hpp

425 lines
16 KiB
C++
Raw Permalink Normal View History

/// @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 <array>
#include <cstddef>
#include <cstdint>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <vector>
namespace grotto
{
inline constexpr std::size_t offset_repr_max_dim = 8;
template <typename InputT>
struct offset_repr_keys
{
static_assert(std::is_integral_v<InputT>, "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<uint64_t, dim>`.
offset_horner_detail::lane_keys<InputT, false> keys{};
offset_horner_detail::lane_keys<InputT, true> keys_v{};
std::vector<std::array<uint64_t, 2>> 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<std::vector<uint64_t>> & 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<std::vector<uint64_t>> identity(std::size_t dim)
{
std::vector<std::vector<uint64_t>> I(dim, std::vector<uint64_t>(dim, 0));
for (std::size_t i = 0; i < dim; ++i)
I[i][i] = 1;
return I;
}
inline std::vector<std::vector<uint64_t>> matmul(
const std::vector<std::vector<uint64_t>> & A,
const std::vector<std::vector<uint64_t>> & B)
{
const std::size_t n = A.size();
std::vector<std::vector<uint64_t>> C(n, std::vector<uint64_t>(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<uint64_t> matvec(
const std::vector<std::vector<uint64_t>> & M,
const std::vector<uint64_t> & v)
{
const std::size_t n = M.size();
std::vector<uint64_t> 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<std::vector<uint64_t>> inverse(
std::vector<std::vector<uint64_t>> 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<std::vector<uint64_t>> offset_repr_matrix_pow(
std::vector<std::vector<uint64_t>> 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<std::uint64_t>(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<std::vector<uint64_t>> 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<uint64_t> 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<std::int64_t>(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<std::vector<uint64_t>> 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<<i` after `2^k` steps.
// Depends only on `poly`, so build once.
static const auto jump = [] {
std::array<std::array<std::uint32_t, 32>, 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 <typename InputT>
offset_repr_keys<InputT> make_offset_repr_keys(
InputT center, const std::vector<uint64_t> & state)
{
using namespace offset_horner_detail;
offset_repr_detail::check_dim(state.size());
offset_repr_keys<InputT> 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<uint64_t>();
mat.wrap_share.push_back({blind, state[i] - blind});
}
mat.keys = offset_horner_detail::make_lane_keys<InputT, false>(
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 <typename InputT>
offset_repr_keys<InputT> make_offset_repr_keys(
InputT center, const std::vector<uint64_t> & state, dpf::verifiable)
{
using namespace offset_horner_detail;
offset_repr_detail::check_dim(state.size());
offset_repr_keys<InputT> 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<uint64_t>();
mat.wrap_share.push_back({blind, state[i] - blind});
}
mat.keys_v = offset_horner_detail::make_lane_keys<InputT, true>(
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::size_t Party, typename InputT>
std::vector<std::vector<uint64_t>> offset_repr_state_shares(
const offset_repr_keys<InputT> & mat,
const std::vector<InputT> & 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<std::vector<uint64_t>> dummy(knots.size(),
std::vector<uint64_t>(mat.dim, 0));
const auto pieces = prepare(knots, dummy, eta);
std::vector<InputT> 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<Party>(mat.keys_v, shifted, mat.wrap_share, pi)
: lane_segments<Party>(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::size_t Party, typename InputT>
std::vector<uint64_t> offset_repr_eval(
const offset_repr_keys<InputT> & mat,
const std::vector<std::vector<uint64_t>> & M,
const std::vector<InputT> & knots,
InputT eta,
dpf::proof_token * tokens = nullptr)
{
offset_repr_detail::check_square(M, mat.dim);
using namespace offset_poly_detail;
std::vector<std::vector<uint64_t>> dummy(knots.size(),
std::vector<uint64_t>(mat.dim, 0));
const auto pieces = prepare(knots, dummy, eta);
const auto states = offset_repr_state_shares<Party>(mat, knots, eta, tokens);
std::vector<uint64_t> 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 <typename InputT>
std::vector<uint64_t> offset_repr_clear(
InputT center,
const std::vector<uint64_t> & state,
const std::vector<std::vector<uint64_t>> & M,
const std::vector<InputT> & 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<std::vector<uint64_t>> dummy(knots.size(),
std::vector<uint64_t>(state.size(), 0));
const auto pieces = prepare(knots, dummy, eta);
std::vector<InputT> 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__