741 lines
26 KiB
C++
741 lines
26 KiB
C++
|
|
/// @file dpf/ppvc.hpp
|
||
|
|
/// @brief Point-programmable vector commitments.
|
||
|
|
/// @details `dpf::ppvc` commits to a vector in `(Z/2^s Z)^n` on a
|
||
|
|
/// power-of-two domain. The committer samples a hidden index, publishes a
|
||
|
|
/// Naor commitment to both roots of `s` aligned 1-bit DPF pairs, and later
|
||
|
|
/// opens one side of each pair. The two keys agree off that index and
|
||
|
|
/// disagree on it, so the choice of side writes the hidden coordinate and
|
||
|
|
/// leaves the rest of the vector fixed. A shift `delta = xi - i` moves
|
||
|
|
/// that coordinate onto a public target. `dpf::k_ppvc` is `k` independent
|
||
|
|
/// copies.
|
||
|
|
///
|
||
|
|
/// `verify` checks the opened Naor roots. Correction words travel with the
|
||
|
|
/// opened key. `check_well_formed` checks both keys of a replica the
|
||
|
|
/// committer still holds, and `audit` reruns generation from a seed.
|
||
|
|
/// Evaluation walks the domain, so the input bitlength is at most 16.
|
||
|
|
/// The manual is [Point-programmable vector commitments](@ref ppvc_manual).
|
||
|
|
/// @copyright Copyright (c) 2019-2026 Ryan Henry and [others](@ref authors)
|
||
|
|
/// @license Released under a GNU General Public v2.0 (GPLv2) license;
|
||
|
|
/// see [LICENSE.md](@ref license) for details.
|
||
|
|
|
||
|
|
#ifndef LIBDPF_INCLUDE_DPF_PPVC_HPP__
|
||
|
|
#define LIBDPF_INCLUDE_DPF_PPVC_HPP__
|
||
|
|
|
||
|
|
#include "hedley/hedley.h"
|
||
|
|
|
||
|
|
#include <algorithm>
|
||
|
|
#include <array>
|
||
|
|
#include <cstddef>
|
||
|
|
#include <cstdint>
|
||
|
|
#include <cstring>
|
||
|
|
#include <optional>
|
||
|
|
#include <stdexcept>
|
||
|
|
#include <type_traits>
|
||
|
|
#include <utility>
|
||
|
|
#include <vector>
|
||
|
|
|
||
|
|
#include "simde/simde/x86/avx2.h"
|
||
|
|
|
||
|
|
#include "dpf/bit.hpp"
|
||
|
|
#include "dpf/dpf_key.hpp"
|
||
|
|
#include "dpf/eval_point.hpp"
|
||
|
|
#include "dpf/prg.hpp"
|
||
|
|
#include "dpf/random.hpp"
|
||
|
|
#include "dpf/twiddle.hpp"
|
||
|
|
|
||
|
|
namespace dpf
|
||
|
|
{
|
||
|
|
|
||
|
|
/// @brief Point-programmable vector commitment over a power-of-two domain.
|
||
|
|
/// @tparam InputT unsigned domain type. The domain size is `2` to the bit length of `InputT`.
|
||
|
|
/// @tparam Width value bit width `s`, from 1 to 64. Coordinates live in `Z/2^s Z`.
|
||
|
|
/// @tparam Sigma Naor statistical parameter. The string length is `m = 3 * 128 + Sigma` bits.
|
||
|
|
/// @tparam PRG generator used for the DPF tree and for Naor's `G`. Defaults to `dpf::prg::aes128`.
|
||
|
|
template <typename InputT,
|
||
|
|
std::size_t Width,
|
||
|
|
std::size_t Sigma = 128,
|
||
|
|
typename PRG = dpf::prg::aes128>
|
||
|
|
struct ppvc
|
||
|
|
{
|
||
|
|
static_assert(std::is_unsigned_v<InputT>, "ppvc domain must be an unsigned integer");
|
||
|
|
static_assert(Width >= 1 && Width <= 64, "ppvc width must be in 1..64");
|
||
|
|
static_assert(Sigma % 8 == 0, "ppvc sigma must be a multiple of 8");
|
||
|
|
|
||
|
|
using input_type = InputT;
|
||
|
|
using value_type = std::uint64_t;
|
||
|
|
using block_type = typename PRG::block_type;
|
||
|
|
using bare_key = dpf::utils::dpf_type_t<PRG, PRG, InputT, dpf::bit>;
|
||
|
|
|
||
|
|
static constexpr std::size_t width = Width;
|
||
|
|
static constexpr std::size_t sigma = Sigma;
|
||
|
|
static constexpr std::size_t kappa = 128;
|
||
|
|
static constexpr std::size_t m_bits = 3 * kappa + Sigma;
|
||
|
|
static constexpr std::size_t nbytes = m_bits / 8;
|
||
|
|
static constexpr std::size_t domain_bits = dpf::utils::bitlength_of_v<InputT>;
|
||
|
|
static constexpr std::size_t domain_size = std::size_t{1} << domain_bits;
|
||
|
|
static constexpr std::size_t commitment_bits = 2 * Width * m_bits;
|
||
|
|
|
||
|
|
static_assert(sizeof(block_type) == 16, "ppvc PRG block must be 128 bits");
|
||
|
|
static_assert(m_bits % 8 == 0, "ppvc Naor string must be a whole number of bytes");
|
||
|
|
static_assert(domain_bits >= dpf::lg_outputs_per_leaf_v<dpf::bit, block_type>,
|
||
|
|
"ppvc domain must cover one packed leaf");
|
||
|
|
static_assert(domain_bits <= 16, "ppvc evaluation materializes the domain");
|
||
|
|
|
||
|
|
/// @brief `m`-bit string, the codomain of Naor's `G`.
|
||
|
|
struct naor_string
|
||
|
|
{
|
||
|
|
std::array<std::uint8_t, nbytes> bytes{};
|
||
|
|
|
||
|
|
friend bool operator==(const naor_string & a, const naor_string & b) noexcept
|
||
|
|
{
|
||
|
|
return a.bytes == b.bytes;
|
||
|
|
}
|
||
|
|
friend bool operator!=(const naor_string & a, const naor_string & b) noexcept
|
||
|
|
{
|
||
|
|
return !(a == b);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/// @brief Public matrix `A`, `m` rows by 128 columns, stored by column.
|
||
|
|
struct public_params
|
||
|
|
{
|
||
|
|
std::array<naor_string, kappa> columns{};
|
||
|
|
};
|
||
|
|
|
||
|
|
/// @brief Published commitment. Slot `[j][β]` binds the root of layer `j`, side `β`.
|
||
|
|
struct commitment
|
||
|
|
{
|
||
|
|
std::array<std::array<naor_string, 2>, Width> slots{};
|
||
|
|
|
||
|
|
friend bool operator==(const commitment & a, const commitment & b) noexcept
|
||
|
|
{
|
||
|
|
return a.slots == b.slots;
|
||
|
|
}
|
||
|
|
friend bool operator!=(const commitment & a, const commitment & b) noexcept
|
||
|
|
{
|
||
|
|
return !(a == b);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/// @brief Committer state. Both keys of every pair, their Naor coins, and `i`.
|
||
|
|
struct state
|
||
|
|
{
|
||
|
|
InputT i{};
|
||
|
|
std::array<std::array<std::optional<bare_key>, 2>, Width> keys{};
|
||
|
|
std::array<std::array<block_type, 2>, Width> coins{};
|
||
|
|
};
|
||
|
|
|
||
|
|
/// @brief One-sided opening. One key and one Naor coin per layer, plus `delta`.
|
||
|
|
struct opening
|
||
|
|
{
|
||
|
|
int mu = 0;
|
||
|
|
value_type tau = 0;
|
||
|
|
InputT delta{};
|
||
|
|
std::array<std::optional<bare_key>, Width> keys{};
|
||
|
|
std::array<block_type, Width> coins{};
|
||
|
|
};
|
||
|
|
|
||
|
|
/// @brief All-ones mask for a `Width`-bit value. `2^64 - 1` when `Width` is 64.
|
||
|
|
static constexpr value_type value_mask() noexcept
|
||
|
|
{
|
||
|
|
if constexpr (Width == 64)
|
||
|
|
return ~value_type{0};
|
||
|
|
else
|
||
|
|
return (value_type{1} << Width) - 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Sample a fresh public matrix.
|
||
|
|
static public_params setup()
|
||
|
|
{
|
||
|
|
public_params pp;
|
||
|
|
for (auto & column : pp.columns)
|
||
|
|
dpf::uniform_fill(column.bytes);
|
||
|
|
return pp;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Expand one 128-bit seed into the public matrix.
|
||
|
|
static public_params setup_from_seed(block_type seed)
|
||
|
|
{
|
||
|
|
public_params pp;
|
||
|
|
std::uint32_t counter = 0;
|
||
|
|
for (auto & column : pp.columns)
|
||
|
|
column = stretch_counter(seed, counter);
|
||
|
|
return pp;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Naor commitment `G(r) XOR A*rho`.
|
||
|
|
static naor_string commit_root(const public_params & pp, block_type rho, block_type r)
|
||
|
|
{
|
||
|
|
return xor_strings(stretch(r), matrix_vector(pp, rho));
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Commit at a freshly sampled index.
|
||
|
|
static std::pair<commitment, state> commit(const public_params & pp)
|
||
|
|
{
|
||
|
|
return commit_at(pp, dpf::uniform_sample<InputT>());
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Commit at a prescribed index.
|
||
|
|
/// @details The shift hides `i` when `i` is sampled independently of the
|
||
|
|
/// later target. `commit` does that sampling.
|
||
|
|
static std::pair<commitment, state> commit_at(const public_params & pp, InputT i)
|
||
|
|
{
|
||
|
|
state st = make_state(i, false);
|
||
|
|
return {bind(pp, st), std::move(st)};
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Commit from a replica seed. The seed determines `i` and every key.
|
||
|
|
/// @details Seed expansion uses a thread-local counter. Two expansions
|
||
|
|
/// must not run at the same time on one thread.
|
||
|
|
static std::pair<commitment, state> commit_from_seed(const public_params & pp, block_type seed)
|
||
|
|
{
|
||
|
|
using rng = seed_rng;
|
||
|
|
rng::seed = seed;
|
||
|
|
rng::counter = 0;
|
||
|
|
InputT i = index_from_block(rng::next());
|
||
|
|
state st = make_state(i, true);
|
||
|
|
return {bind(pp, st), std::move(st)};
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Same expansion as `commit_from_seed`, with `i` supplied by the caller.
|
||
|
|
/// @details The seed is spent on roots and Naor coins. A `k`-PPVC uses this
|
||
|
|
/// so it can reject colliding indices and try another seed.
|
||
|
|
static std::pair<commitment, state> commit_at_from_seed(const public_params & pp,
|
||
|
|
block_type seed, InputT i)
|
||
|
|
{
|
||
|
|
using rng = seed_rng;
|
||
|
|
rng::seed = seed;
|
||
|
|
rng::counter = 0;
|
||
|
|
state st = make_state(i, true);
|
||
|
|
return {bind(pp, st), std::move(st)};
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Program `tau` and shift the hidden coordinate onto `xi`.
|
||
|
|
/// `mu = 0` programs the coordinate. `mu = 1` programs the sum of coordinates.
|
||
|
|
static opening open(const state & st, int mu, value_type tau, InputT xi)
|
||
|
|
{
|
||
|
|
if (mu != 0 && mu != 1)
|
||
|
|
throw std::invalid_argument("ppvc: mu must be 0 or 1");
|
||
|
|
if (tau > value_mask())
|
||
|
|
throw std::invalid_argument("ppvc: tau does not fit in the value width");
|
||
|
|
|
||
|
|
const std::size_t hidden = index_of(st.i);
|
||
|
|
std::array<bool, Width> u{};
|
||
|
|
for (std::size_t j = 0; j < Width; ++j)
|
||
|
|
u[j] = bit_at(key_of(st, j, 0), hidden);
|
||
|
|
|
||
|
|
value_type target = tau;
|
||
|
|
if (mu == 1)
|
||
|
|
{
|
||
|
|
value_type off_sum = 0;
|
||
|
|
for (std::size_t y = 0; y < domain_size; ++y)
|
||
|
|
{
|
||
|
|
if (y == hidden)
|
||
|
|
continue;
|
||
|
|
off_sum = (off_sum + column_at(st, 0, y)) & value_mask();
|
||
|
|
}
|
||
|
|
target = (tau - off_sum) & value_mask();
|
||
|
|
}
|
||
|
|
|
||
|
|
opening op;
|
||
|
|
op.mu = mu;
|
||
|
|
op.tau = tau;
|
||
|
|
op.delta = sub(xi, st.i);
|
||
|
|
for (std::size_t j = 0; j < Width; ++j)
|
||
|
|
{
|
||
|
|
const bool want = ((target >> j) & 1u) != 0;
|
||
|
|
const unsigned side = (u[j] != want) ? 1u : 0u;
|
||
|
|
op.keys[j] = st.keys[j][side];
|
||
|
|
op.coins[j] = st.coins[j][side];
|
||
|
|
}
|
||
|
|
return op;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Accept the opening when every opened root matches its Naor string.
|
||
|
|
static bool verify(const public_params & pp, const commitment & com, const opening & op)
|
||
|
|
{
|
||
|
|
for (std::size_t j = 0; j < Width; ++j)
|
||
|
|
{
|
||
|
|
if (!op.keys[j])
|
||
|
|
return false;
|
||
|
|
const block_type rho = op.keys[j]->root();
|
||
|
|
const unsigned beta = static_cast<unsigned>(dpf::get_lo_bit(rho));
|
||
|
|
if (beta > 1)
|
||
|
|
return false;
|
||
|
|
if (commit_root(pp, rho, op.coins[j]) != com.slots[j][beta])
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief One-sided vector in the hidden indexing, one entry per domain point.
|
||
|
|
static std::vector<value_type> eval(const opening & op)
|
||
|
|
{
|
||
|
|
std::vector<value_type> x(domain_size);
|
||
|
|
for (std::size_t y = 0; y < domain_size; ++y)
|
||
|
|
{
|
||
|
|
value_type column = 0;
|
||
|
|
for (std::size_t j = 0; j < Width; ++j)
|
||
|
|
{
|
||
|
|
if (!op.keys[j])
|
||
|
|
throw std::invalid_argument("ppvc: opening is missing a key");
|
||
|
|
if (bit_at(*op.keys[j], y))
|
||
|
|
column |= value_type{1} << j;
|
||
|
|
}
|
||
|
|
x[y] = column;
|
||
|
|
}
|
||
|
|
return x;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Rotated vector. Entry `y` is the unrotated entry at `y - delta`.
|
||
|
|
static std::vector<value_type> eval_rotated(const opening & op)
|
||
|
|
{
|
||
|
|
const auto x = eval(op);
|
||
|
|
const std::size_t delta = index_of(op.delta);
|
||
|
|
std::vector<value_type> rotated(domain_size);
|
||
|
|
for (std::size_t y = 0; y < domain_size; ++y)
|
||
|
|
rotated[y] = x[(y - delta) & (domain_size - 1)];
|
||
|
|
return rotated;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Sum of coordinates, reduced in `Z/2^Width Z`.
|
||
|
|
static value_type column_sum(const std::vector<value_type> & x)
|
||
|
|
{
|
||
|
|
value_type sum = 0;
|
||
|
|
for (value_type column : x)
|
||
|
|
sum = (sum + column) & value_mask();
|
||
|
|
return sum;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Check the programmed statement against the unrotated vector.
|
||
|
|
/// @details For `mu = 0`, the entry at `xi - delta` equals `tau`.
|
||
|
|
/// For `mu = 1`, the sum of coordinates equals `tau`.
|
||
|
|
static bool check_statement(const opening & op, const std::vector<value_type> & x_circ, InputT xi)
|
||
|
|
{
|
||
|
|
if (x_circ.size() != domain_size)
|
||
|
|
return false;
|
||
|
|
if (op.mu == 0)
|
||
|
|
return x_circ[index_of(sub(xi, op.delta))] == op.tau;
|
||
|
|
if (op.mu == 1)
|
||
|
|
return column_sum(x_circ) == op.tau;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief `verify` and `check_statement`.
|
||
|
|
static bool accept(const public_params & pp, const commitment & com,
|
||
|
|
const opening & op, const std::vector<value_type> & x_circ, InputT xi)
|
||
|
|
{
|
||
|
|
return verify(pp, com, op) && check_statement(op, x_circ, xi);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Both sides are DPF keys for payload 1 at `state.i`, and both Naor slots open.
|
||
|
|
static bool check_well_formed(const public_params & pp, const commitment & com, const state & st)
|
||
|
|
{
|
||
|
|
const std::size_t hidden = index_of(st.i);
|
||
|
|
for (std::size_t j = 0; j < Width; ++j)
|
||
|
|
{
|
||
|
|
if (!st.keys[j][0] || !st.keys[j][1])
|
||
|
|
return false;
|
||
|
|
const bare_key & left = *st.keys[j][0];
|
||
|
|
const bare_key & right = *st.keys[j][1];
|
||
|
|
if (dpf::get_lo_bit(left.root()) != 0 || dpf::get_lo_bit(right.root()) != 1)
|
||
|
|
return false;
|
||
|
|
if (commit_root(pp, left.root(), st.coins[j][0]) != com.slots[j][0])
|
||
|
|
return false;
|
||
|
|
if (commit_root(pp, right.root(), st.coins[j][1]) != com.slots[j][1])
|
||
|
|
return false;
|
||
|
|
if (!shared_corrections(left, right))
|
||
|
|
return false;
|
||
|
|
|
||
|
|
int spikes = 0;
|
||
|
|
std::size_t where = 0;
|
||
|
|
for (std::size_t y = 0; y < domain_size; ++y)
|
||
|
|
{
|
||
|
|
if (bit_at(left, y) != bit_at(right, y))
|
||
|
|
{
|
||
|
|
++spikes;
|
||
|
|
where = y;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (spikes != 1 || where != hidden)
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Re-expand `seed` and accept when it reproduces `com` and a well-formed replica.
|
||
|
|
static bool audit(const public_params & pp, const commitment & com, block_type seed)
|
||
|
|
{
|
||
|
|
auto [expanded, st] = commit_from_seed(pp, seed);
|
||
|
|
return expanded == com && check_well_formed(pp, com, st);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Domain subtraction modulo `domain_size`.
|
||
|
|
static InputT sub(InputT a, InputT b)
|
||
|
|
{
|
||
|
|
return point((index_of(a) - index_of(b)) & (domain_size - 1));
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Integer representative of a domain point, in `0 .. domain_size-1`.
|
||
|
|
static std::size_t index_of(InputT x)
|
||
|
|
{
|
||
|
|
return static_cast<std::size_t>(as_u64(x) & (domain_size - 1));
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Domain point whose integer representative is `index` modulo `domain_size`.
|
||
|
|
static InputT point(std::size_t index)
|
||
|
|
{
|
||
|
|
using integral = typename dpf::utils::to_integral_type<InputT>::integral_type;
|
||
|
|
return dpf::utils::make_from_integral_value<InputT>{}(
|
||
|
|
static_cast<integral>(index & (domain_size - 1)));
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Low domain bits of a PRG block, used as a hidden index.
|
||
|
|
static InputT index_from_block(block_type block)
|
||
|
|
{
|
||
|
|
alignas(16) std::uint64_t lanes[2];
|
||
|
|
simde_mm_store_si128(reinterpret_cast<simde__m128i *>(lanes), block);
|
||
|
|
return point(static_cast<std::size_t>(lanes[0]));
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Which side was opened in each layer. Bit `j` is the disclosure bit.
|
||
|
|
static std::array<bool, Width> disclosure(const opening & op)
|
||
|
|
{
|
||
|
|
std::array<bool, Width> bits{};
|
||
|
|
for (std::size_t j = 0; j < Width; ++j)
|
||
|
|
{
|
||
|
|
if (!op.keys[j])
|
||
|
|
throw std::invalid_argument("ppvc: opening is missing a key");
|
||
|
|
bits[j] = dpf::get_lo_bit(op.keys[j]->root()) != 0;
|
||
|
|
}
|
||
|
|
return bits;
|
||
|
|
}
|
||
|
|
|
||
|
|
private:
|
||
|
|
/// @brief Counter-mode draw for one replica seed. Not reentrant.
|
||
|
|
struct seed_rng
|
||
|
|
{
|
||
|
|
static inline thread_local block_type seed{};
|
||
|
|
static inline thread_local std::uint32_t counter{0};
|
||
|
|
|
||
|
|
HEDLEY_WARN_UNUSED_RESULT
|
||
|
|
static block_type next()
|
||
|
|
{
|
||
|
|
return PRG::eval(seed, counter++);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
HEDLEY_WARN_UNUSED_RESULT
|
||
|
|
static std::uint64_t as_u64(InputT x)
|
||
|
|
{
|
||
|
|
return static_cast<std::uint64_t>(dpf::utils::to_integral_type<InputT>{}(x));
|
||
|
|
}
|
||
|
|
|
||
|
|
HEDLEY_WARN_UNUSED_RESULT
|
||
|
|
static bool block_bit(block_type block, std::size_t index)
|
||
|
|
{
|
||
|
|
alignas(16) std::uint64_t lanes[2];
|
||
|
|
simde_mm_store_si128(reinterpret_cast<simde__m128i *>(lanes), block);
|
||
|
|
return ((lanes[index / 64] >> (index % 64)) & 1u) != 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
HEDLEY_WARN_UNUSED_RESULT
|
||
|
|
static naor_string xor_strings(naor_string lhs, const naor_string & rhs)
|
||
|
|
{
|
||
|
|
for (std::size_t i = 0; i < nbytes; ++i)
|
||
|
|
lhs.bytes[i] = static_cast<std::uint8_t>(lhs.bytes[i] ^ rhs.bytes[i]);
|
||
|
|
return lhs;
|
||
|
|
}
|
||
|
|
|
||
|
|
HEDLEY_WARN_UNUSED_RESULT
|
||
|
|
static naor_string stretch(block_type seed)
|
||
|
|
{
|
||
|
|
std::uint32_t counter = 0;
|
||
|
|
return stretch_counter(seed, counter);
|
||
|
|
}
|
||
|
|
|
||
|
|
HEDLEY_WARN_UNUSED_RESULT
|
||
|
|
static naor_string stretch_counter(block_type seed, std::uint32_t & counter)
|
||
|
|
{
|
||
|
|
naor_string out;
|
||
|
|
std::size_t filled = 0;
|
||
|
|
while (filled < nbytes)
|
||
|
|
{
|
||
|
|
const block_type block = PRG::eval(seed, counter++);
|
||
|
|
alignas(16) std::uint8_t raw[16];
|
||
|
|
simde_mm_store_si128(reinterpret_cast<simde__m128i *>(raw), block);
|
||
|
|
const std::size_t take = std::min<std::size_t>(16, nbytes - filled);
|
||
|
|
std::memcpy(out.bytes.data() + filled, raw, take);
|
||
|
|
filled += take;
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
HEDLEY_WARN_UNUSED_RESULT
|
||
|
|
static naor_string matrix_vector(const public_params & pp, block_type rho)
|
||
|
|
{
|
||
|
|
naor_string acc;
|
||
|
|
for (std::size_t bit = 0; bit < kappa; ++bit)
|
||
|
|
{
|
||
|
|
if (block_bit(rho, bit))
|
||
|
|
acc = xor_strings(acc, pp.columns[bit]);
|
||
|
|
}
|
||
|
|
return acc;
|
||
|
|
}
|
||
|
|
|
||
|
|
HEDLEY_WARN_UNUSED_RESULT
|
||
|
|
static const bare_key & key_of(const state & st, std::size_t layer, unsigned side)
|
||
|
|
{
|
||
|
|
if (!st.keys[layer][side])
|
||
|
|
throw std::invalid_argument("ppvc: commit state is missing a key");
|
||
|
|
return *st.keys[layer][side];
|
||
|
|
}
|
||
|
|
|
||
|
|
HEDLEY_WARN_UNUSED_RESULT
|
||
|
|
static bool bit_at(const bare_key & key, std::size_t index)
|
||
|
|
{
|
||
|
|
return static_cast<bool>(*dpf::eval_point(key, point(index)));
|
||
|
|
}
|
||
|
|
|
||
|
|
HEDLEY_WARN_UNUSED_RESULT
|
||
|
|
static value_type column_at(const state & st, unsigned side, std::size_t index)
|
||
|
|
{
|
||
|
|
value_type column = 0;
|
||
|
|
for (std::size_t j = 0; j < Width; ++j)
|
||
|
|
{
|
||
|
|
if (bit_at(key_of(st, j, side), index))
|
||
|
|
column |= value_type{1} << j;
|
||
|
|
}
|
||
|
|
return column;
|
||
|
|
}
|
||
|
|
|
||
|
|
HEDLEY_WARN_UNUSED_RESULT
|
||
|
|
static bool shared_corrections(const bare_key & left, const bare_key & right)
|
||
|
|
{
|
||
|
|
const auto & words_l = left.correction_words();
|
||
|
|
const auto & words_r = right.correction_words();
|
||
|
|
if (std::memcmp(words_l.data(), words_r.data(), sizeof(words_l)) != 0)
|
||
|
|
return false;
|
||
|
|
return left.correction_advice() == right.correction_advice();
|
||
|
|
}
|
||
|
|
|
||
|
|
HEDLEY_WARN_UNUSED_RESULT
|
||
|
|
static commitment bind(const public_params & pp, const state & st)
|
||
|
|
{
|
||
|
|
commitment com;
|
||
|
|
for (std::size_t j = 0; j < Width; ++j)
|
||
|
|
{
|
||
|
|
for (unsigned beta = 0; beta < 2; ++beta)
|
||
|
|
{
|
||
|
|
const bare_key & key = key_of(st, j, beta);
|
||
|
|
com.slots[j][beta] = commit_root(pp, key.root(), st.coins[j][beta]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return com;
|
||
|
|
}
|
||
|
|
|
||
|
|
HEDLEY_WARN_UNUSED_RESULT
|
||
|
|
static state make_state(InputT i, bool seeded)
|
||
|
|
{
|
||
|
|
using rng = seed_rng;
|
||
|
|
state st;
|
||
|
|
st.i = i;
|
||
|
|
for (std::size_t j = 0; j < Width; ++j)
|
||
|
|
{
|
||
|
|
auto made = seeded
|
||
|
|
? dpf::make_dpf<PRG, PRG>(dpf::make_dpfargs(i, dpf::bit::one), &rng::next)
|
||
|
|
: dpf::make_dpf<PRG, PRG>(dpf::make_dpfargs(i, dpf::bit::one));
|
||
|
|
st.keys[j][0] = made.first.key();
|
||
|
|
st.keys[j][1] = made.second.key();
|
||
|
|
st.coins[j][0] = seeded
|
||
|
|
? rng::next()
|
||
|
|
: dpf::uniform_sample<block_type>();
|
||
|
|
st.coins[j][1] = seeded
|
||
|
|
? rng::next()
|
||
|
|
: dpf::uniform_sample<block_type>();
|
||
|
|
}
|
||
|
|
return st;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/// @brief `k` independent point-programmable commitments.
|
||
|
|
/// @details Each copy has its own hidden index. The sum of the rotated
|
||
|
|
/// openings is one vector. Reprogramming copy `r` changes coordinate `xi[r]`
|
||
|
|
/// and leaves the other coordinates fixed.
|
||
|
|
/// @tparam K number of programmable coordinates. At most the domain size.
|
||
|
|
template <std::size_t K,
|
||
|
|
typename InputT,
|
||
|
|
std::size_t Width,
|
||
|
|
std::size_t Sigma = 128,
|
||
|
|
typename PRG = dpf::prg::aes128>
|
||
|
|
struct k_ppvc
|
||
|
|
{
|
||
|
|
static_assert(K >= 1, "k-ppvc needs at least one point");
|
||
|
|
|
||
|
|
using one = ppvc<InputT, Width, Sigma, PRG>;
|
||
|
|
using public_params = typename one::public_params;
|
||
|
|
using value_type = typename one::value_type;
|
||
|
|
using input_type = InputT;
|
||
|
|
using block_type = typename one::block_type;
|
||
|
|
|
||
|
|
static constexpr std::size_t points = K;
|
||
|
|
static constexpr std::size_t width = Width;
|
||
|
|
|
||
|
|
static_assert(K <= one::domain_size, "k-ppvc asks for more distinct points than the domain has");
|
||
|
|
|
||
|
|
struct commitment
|
||
|
|
{
|
||
|
|
std::array<typename one::commitment, K> copies{};
|
||
|
|
|
||
|
|
friend bool operator==(const commitment & a, const commitment & b) noexcept
|
||
|
|
{
|
||
|
|
return a.copies == b.copies;
|
||
|
|
}
|
||
|
|
friend bool operator!=(const commitment & a, const commitment & b) noexcept
|
||
|
|
{
|
||
|
|
return !(a == b);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
struct state
|
||
|
|
{
|
||
|
|
std::array<typename one::state, K> copies{};
|
||
|
|
};
|
||
|
|
|
||
|
|
struct opening
|
||
|
|
{
|
||
|
|
std::array<typename one::opening, K> copies{};
|
||
|
|
};
|
||
|
|
|
||
|
|
/// @brief Sample `K` distinct indices and commit one replica at each.
|
||
|
|
static std::pair<commitment, state> commit(const public_params & pp)
|
||
|
|
{
|
||
|
|
commitment com;
|
||
|
|
state st;
|
||
|
|
std::array<InputT, K> used{};
|
||
|
|
for (std::size_t r = 0; r < K; ++r)
|
||
|
|
{
|
||
|
|
InputT index{};
|
||
|
|
for (;;)
|
||
|
|
{
|
||
|
|
index = dpf::uniform_sample<InputT>();
|
||
|
|
bool clash = false;
|
||
|
|
for (std::size_t p = 0; p < r; ++p)
|
||
|
|
clash = clash || used[p] == index;
|
||
|
|
if (!clash)
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
used[r] = index;
|
||
|
|
auto [slot, replica] = one::commit_at(pp, index);
|
||
|
|
com.copies[r] = std::move(slot);
|
||
|
|
st.copies[r] = std::move(replica);
|
||
|
|
}
|
||
|
|
return {std::move(com), std::move(st)};
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Expand one seed into `k` replicas with distinct hidden indices.
|
||
|
|
static std::pair<commitment, state> commit_from_seed(const public_params & pp, block_type master)
|
||
|
|
{
|
||
|
|
block_type material = master;
|
||
|
|
for (int attempt = 0; attempt < 64; ++attempt)
|
||
|
|
{
|
||
|
|
if (attempt > 0)
|
||
|
|
material = PRG::eval(material, 0x00ffffffu);
|
||
|
|
std::uint32_t counter = 0;
|
||
|
|
std::array<InputT, K> indices{};
|
||
|
|
std::array<block_type, K> subseeds{};
|
||
|
|
for (std::size_t r = 0; r < K; ++r)
|
||
|
|
{
|
||
|
|
indices[r] = one::index_from_block(PRG::eval(material, counter++));
|
||
|
|
subseeds[r] = PRG::eval(material, counter++);
|
||
|
|
}
|
||
|
|
if (!distinct(indices))
|
||
|
|
continue;
|
||
|
|
|
||
|
|
commitment com;
|
||
|
|
state st;
|
||
|
|
for (std::size_t r = 0; r < K; ++r)
|
||
|
|
{
|
||
|
|
auto [slot, replica] = one::commit_at_from_seed(pp, subseeds[r], indices[r]);
|
||
|
|
com.copies[r] = std::move(slot);
|
||
|
|
st.copies[r] = std::move(replica);
|
||
|
|
}
|
||
|
|
return {std::move(com), std::move(st)};
|
||
|
|
}
|
||
|
|
throw std::runtime_error("k-ppvc: seed did not yield distinct points");
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Open every replica. `mu` is `0` to program each coordinate, `1` to program each sum.
|
||
|
|
static opening open(const state & st, int mu,
|
||
|
|
const std::array<value_type, K> & tau, const std::array<InputT, K> & xi)
|
||
|
|
{
|
||
|
|
opening op;
|
||
|
|
for (std::size_t r = 0; r < K; ++r)
|
||
|
|
op.copies[r] = one::open(st.copies[r], mu, tau[r], xi[r]);
|
||
|
|
return op;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Accept when every replica's opened Naor roots match.
|
||
|
|
static bool verify(const public_params & pp, const commitment & com, const opening & op)
|
||
|
|
{
|
||
|
|
for (std::size_t r = 0; r < K; ++r)
|
||
|
|
{
|
||
|
|
if (!one::verify(pp, com.copies[r], op.copies[r]))
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Every replica is well formed, and the hidden indices are distinct.
|
||
|
|
static bool check_well_formed(const public_params & pp, const commitment & com, const state & st)
|
||
|
|
{
|
||
|
|
std::array<InputT, K> indices{};
|
||
|
|
for (std::size_t r = 0; r < K; ++r)
|
||
|
|
{
|
||
|
|
if (!one::check_well_formed(pp, com.copies[r], st.copies[r]))
|
||
|
|
return false;
|
||
|
|
indices[r] = st.copies[r].i;
|
||
|
|
}
|
||
|
|
return distinct(indices);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Re-expand `seed` and accept when it reproduces `com` and a well-formed object.
|
||
|
|
static bool audit(const public_params & pp, const commitment & com, block_type seed)
|
||
|
|
{
|
||
|
|
auto [expanded, st] = commit_from_seed(pp, seed);
|
||
|
|
return expanded == com && check_well_formed(pp, com, st);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// @brief Sum of the `k` rotated vectors, reduced in `Z/2^Width Z`.
|
||
|
|
static std::vector<value_type> combine_rotated(const opening & op)
|
||
|
|
{
|
||
|
|
std::vector<value_type> sum(one::domain_size, 0);
|
||
|
|
for (std::size_t r = 0; r < K; ++r)
|
||
|
|
{
|
||
|
|
const auto rotated = one::eval_rotated(op.copies[r]);
|
||
|
|
for (std::size_t y = 0; y < sum.size(); ++y)
|
||
|
|
sum[y] = (sum[y] + rotated[y]) & one::value_mask();
|
||
|
|
}
|
||
|
|
return sum;
|
||
|
|
}
|
||
|
|
|
||
|
|
private:
|
||
|
|
HEDLEY_WARN_UNUSED_RESULT
|
||
|
|
static bool distinct(const std::array<InputT, K> & indices)
|
||
|
|
{
|
||
|
|
for (std::size_t r = 0; r < K; ++r)
|
||
|
|
{
|
||
|
|
for (std::size_t p = 0; p < r; ++p)
|
||
|
|
{
|
||
|
|
if (indices[p] == indices[r])
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
} // namespace dpf
|
||
|
|
|
||
|
|
#endif // LIBDPF_INCLUDE_DPF_PPVC_HPP__
|