libdpf/include/grotto/offset_repr.hpp
Ryan Henry 0d22946a0e 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>
2026-09-28 05:59:19 -06:00

424 lines
16 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// @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__