From cf8054a0b3b319b36c84fd526fb471f96b1b38dc Mon Sep 17 00:00:00 2001 From: Ryan Henry Date: Sat, 26 Sep 2026 18:34:05 -0600 Subject: [PATCH] Add point-programmable vector commitments. A commitment can be published before its hidden coordinate is chosen. Opening one side of each aligned 1-bit DPF pair sets that coordinate or the vector sum, and a shift moves it onto a public index. Co-authored-by: Cursor --- doc/Doxyfile | 1 + doc/examples.dox | 11 +- doc/pages/introduction.md | 1 + doc/pages/ppvc.md | 85 +++++ examples/mwe/ppvc.cpp | 26 ++ include/dpf.hpp | 2 + include/dpf/ppvc.hpp | 740 ++++++++++++++++++++++++++++++++++++++ test/CMakeLists.txt | 2 + test/tests/ppvc_test.cpp | 441 +++++++++++++++++++++++ 9 files changed, 1308 insertions(+), 1 deletion(-) create mode 100644 doc/pages/ppvc.md create mode 100644 examples/mwe/ppvc.cpp create mode 100644 include/dpf/ppvc.hpp create mode 100644 test/tests/ppvc_test.cpp diff --git a/doc/Doxyfile b/doc/Doxyfile index b86ce67..483ef1b 100644 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -1011,6 +1011,7 @@ INPUT = include/dpf.hpp \ include/grotto.hpp \ include/grotto/ \ doc/libdpf_full.md \ + doc/pages/ppvc.md \ doc/examples.dox \ doc/namespaces.dox \ doc/directories.dox \ diff --git a/doc/examples.dox b/doc/examples.dox index fa59ad8..1d5f8f9 100644 --- a/doc/examples.dox +++ b/doc/examples.dox @@ -102,4 +102,13 @@ /// @example iterables/zip_iterable.cpp zip_iterable.cpp /// @brief an example of `dpf::zip_iterable` in use -/// @} \ No newline at end of file +/// @} + + +/// @{ + +/// @example mwe/ppvc.cpp ppvc.cpp +/// @brief a point-programmable vector commitment + +/// @} + diff --git a/doc/pages/introduction.md b/doc/pages/introduction.md index faf680d..56606d7 100644 --- a/doc/pages/introduction.md +++ b/doc/pages/introduction.md @@ -21,6 +21,7 @@ zero-knowledge arguments, anonymous messaging, and more. - memoizers - json serialization - asynchronous I/O + - [point-programmable vector commitments](@ref ppvc_manual) ## Credits {#credits} diff --git a/doc/pages/ppvc.md b/doc/pages/ppvc.md new file mode 100644 index 0000000..a8ab139 --- /dev/null +++ b/doc/pages/ppvc.md @@ -0,0 +1,85 @@ +# Point-programmable vector commitments {#ppvc_manual} + +A point-programmable vector commitment binds a vector +`x` in `(Z/2^s Z)^n` and still lets one hidden coordinate be chosen +after the commitment is published. + +`n` is a power of two, the bit length of the input type, and at most +2^16. `s` is the `Width` parameter, from 1 to 64. +The manual construction is `dpf::ppvc`. `dpf::k_ppvc` is `K` +independent copies of that object. + +The committer samples an index `i` and builds `s` aligned 1-bit DPF +pairs there, the same point key as [DPF basics](@ref basics_body). +Both roots of every pair are bound with a Naor commitment under a +public matrix `A`. Opening releases one key from each pair, together +with a shift `delta = xi - i`. + +Off `i`, the two keys of a pair evaluate to the same bit. +At `i`, they evaluate to opposite bits. +Choosing the side therefore writes an arbitrary value into that one +coordinate and leaves every other coordinate fixed. +The shift moves the written coordinate from `i` onto the public target +`xi`. The opening carries `delta`, not `i` and not `xi`. + +`open(st, mu, tau, xi)` has two modes. + +- `mu = 0` programs the coordinate. After rotation, entry `xi` equals `tau`. +- `mu = 1` programs the sum of every coordinate. That sum equals `tau`. + +Those two maps are bijections on `Z/2^s Z`. Programming one of them +programs the other. + +```cpp +using scheme = dpf::ppvc; +const auto pp = scheme::setup(); +const auto [com, st] = scheme::commit(pp); + +const std::uint8_t xi = 40; +const auto op = scheme::open(st, 0, 0x5a, xi); +const auto x = scheme::eval(op); // hidden indexing +const auto rotated = scheme::eval_rotated(op); // value 0x5a sits at xi +const bool ok = scheme::accept(pp, com, op, x, xi); +``` + +`setup` samples `A`. `setup_from_seed` expands one 128-bit seed into the +same matrix, which is the common random string when many sessions share +it. `commit` samples `i`. `commit_at` uses an index the caller already +chose. The shift hides `i` when that index was sampled independently of +`xi`. `commit_from_seed` and `commit_at_from_seed` rerun key generation +from a replica seed. Seed expansion keeps its counter in thread-local +storage, so two expansions on one thread must not overlap. + +## What an opening proves {#ppvc_verify} + +`verify` checks each opened root against its Naor string. +`accept` also checks the programmed statement: the rotated coordinate +when `mu` is 0, the column sum when `mu` is 1. + +Correction words travel with the opened key. They are not inside the +commitment. `verify` sees one side of each pair. +`check_well_formed` is the check on a replica the committer still holds: +shared correction words, party bits 0 and 1, both Naor openings, and +exactly one place where the two keys disagree, at the recorded index, +with payload 1. +`audit` expands a seed and accepts when the published commitment matches +that expansion and the replica is well formed. + +`k_ppvc` asks for the same checks on every copy, and for distinct hidden +indices. `combine_rotated` adds the rotated vectors in `Z/2^s Z`. +Reprogramming copy `r` changes coordinate `xi[r]` of that sum. + +The commitment is `2 * s * (3 * 128 + Sigma)` bits. +`Sigma` defaults to 128 and must be a multiple of 8. +The generator is `dpf::prg::aes128` unless another 128-bit PRG is named. + +**Defined in**\n +@ref dpf/ppvc.hpp + +**Try**\n +@ref mwe/ppvc.cpp + +Naor's string commitment is Moni Naor, "Bit Commitment Using +Pseudorandomness," Journal of Cryptology 4(2), 1991, pp. 151–158. +The point keys are the Boyle–Gilboa–Ishai construction named in +[DPF basics](@ref point_functions). diff --git a/examples/mwe/ppvc.cpp b/examples/mwe/ppvc.cpp new file mode 100644 index 0000000..67074b7 --- /dev/null +++ b/examples/mwe/ppvc.cpp @@ -0,0 +1,26 @@ +#include +#include + +#include "dpf.hpp" + +// Complete program. A point-programmable vector commitment publishes the +// commitment before the hidden coordinate is chosen. +// +// c++ -std=c++17 -march=native -I include -I thirdparty examples/mwe/ppvc.cpp +int main() +{ + using scheme = dpf::ppvc; + + const auto pp = scheme::setup(); + const auto [com, st] = scheme::commit(pp); + + const std::uint8_t xi = 40; + const std::uint64_t tau = 0x5a; + const auto op = scheme::open(st, 0, tau, xi); + const auto x = scheme::eval(op); + const auto rotated = scheme::eval_rotated(op); + + const bool ok = scheme::accept(pp, com, op, x, xi) && rotated[xi] == tau; + std::cout << rotated[xi] << "\n"; + return ok ? 0 : 1; +} diff --git a/include/dpf.hpp b/include/dpf.hpp index afd0d10..8d77538 100644 --- a/include/dpf.hpp +++ b/include/dpf.hpp @@ -122,6 +122,8 @@ #include "dpf/multipoint.hpp" +#include "dpf/ppvc.hpp" + #include "dpf/vec.hpp" #include "dpf/interval.hpp" diff --git a/include/dpf/ppvc.hpp b/include/dpf/ppvc.hpp new file mode 100644 index 0000000..015a015 --- /dev/null +++ b/include/dpf/ppvc.hpp @@ -0,0 +1,740 @@ +/// @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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +struct ppvc +{ + static_assert(std::is_unsigned_v, "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; + + 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; + 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, + "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 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 columns{}; + }; + + /// @brief Published commitment. Slot `[j][β]` binds the root of layer `j`, side `β`. + struct commitment + { + std::array, 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, 2>, Width> keys{}; + std::array, 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, Width> keys{}; + std::array 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 commit(const public_params & pp) + { + return commit_at(pp, dpf::uniform_sample()); + } + + /// @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 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 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 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 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(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 eval(const opening & op) + { + std::vector 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 eval_rotated(const opening & op) + { + const auto x = eval(op); + const std::size_t delta = index_of(op.delta); + std::vector 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 & 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 & 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 & 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(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::integral_type; + return dpf::utils::make_from_integral_value{}( + static_cast(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(lanes), block); + return point(static_cast(lanes[0])); + } + + /// @brief Which side was opened in each layer. Bit `j` is the disclosure bit. + static std::array disclosure(const opening & op) + { + std::array 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(dpf::utils::to_integral_type{}(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(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(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(raw), block); + const std::size_t take = std::min(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(*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(dpf::make_dpfargs(i, dpf::bit::one), &rng::next) + : dpf::make_dpf(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(); + st.coins[j][1] = seeded + ? rng::next() + : dpf::uniform_sample(); + } + 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 +struct k_ppvc +{ + static_assert(K >= 1, "k-ppvc needs at least one point"); + + using one = ppvc; + 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 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 copies{}; + }; + + struct opening + { + std::array copies{}; + }; + + /// @brief Sample `K` distinct indices and commit one replica at each. + static std::pair commit(const public_params & pp) + { + commitment com; + state st; + std::array used{}; + for (std::size_t r = 0; r < K; ++r) + { + InputT index{}; + for (;;) + { + index = dpf::uniform_sample(); + 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 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 indices{}; + std::array 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 & tau, const std::array & 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 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 combine_rotated(const opening & op) + { + std::vector 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 & 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__ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 83b0869..5fb608c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -137,4 +137,6 @@ add_executable(ic_test tests/ic_test.cpp) gtest_discover_tests(ic_test) add_executable(wide_payload_test tests/wide_payload_test.cpp) gtest_discover_tests(wide_payload_test) +add_executable(ppvc_test tests/ppvc_test.cpp) +gtest_discover_tests(ppvc_test) gtest_discover_tests(corner_gaps_test) diff --git a/test/tests/ppvc_test.cpp b/test/tests/ppvc_test.cpp new file mode 100644 index 0000000..f54ad42 --- /dev/null +++ b/test/tests/ppvc_test.cpp @@ -0,0 +1,441 @@ +#include + +#include "dpf.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using block = dpf::prg::aes128::block_type; + +block seed_block(std::uint64_t lo, std::uint64_t hi) +{ + alignas(16) std::uint64_t lanes[2] = {lo, hi}; + return simde_mm_load_si128(reinterpret_cast(lanes)); +} + +bool same_root(const block & a, const block & b) +{ + alignas(16) unsigned char aa[16], bb[16]; + simde_mm_store_si128(reinterpret_cast(aa), a); + simde_mm_store_si128(reinterpret_cast(bb), b); + return std::memcmp(aa, bb, 16) == 0; +} + +template +void expect_point_programming(const typename Scheme::public_params & pp, + const typename Scheme::commitment & com, const typename Scheme::state & st, + typename Scheme::input_type xi) +{ + ASSERT_TRUE(Scheme::check_well_formed(pp, com, st)); + const auto baseline = Scheme::eval(Scheme::open(st, 0, 0, xi)); + std::set seen; + for (std::uint64_t tau = 0; tau <= Scheme::value_mask(); ++tau) + { + const auto op = Scheme::open(st, 0, tau, xi); + const auto x = Scheme::eval(op); + const auto rotated = Scheme::eval_rotated(op); + + EXPECT_TRUE(Scheme::accept(pp, com, op, x, xi)); + EXPECT_EQ(op.delta, Scheme::sub(xi, st.i)); + EXPECT_EQ(x[Scheme::index_of(st.i)], tau); + EXPECT_EQ(rotated[Scheme::index_of(xi)], tau); + EXPECT_EQ(Scheme::column_sum(x), Scheme::column_sum(rotated)); + for (std::size_t y = 0; y < Scheme::domain_size; ++y) + { + if (y == Scheme::index_of(st.i)) + continue; + EXPECT_EQ(x[y], baseline[y]); + } + + std::uint64_t pack = 0; + const auto bits = Scheme::disclosure(op); + for (std::size_t j = 0; j < Scheme::width; ++j) + if (bits[j]) + pack |= std::uint64_t{1} << j; + EXPECT_TRUE(seen.insert(pack).second); + + for (std::size_t j = 0; j < Scheme::width; ++j) + { + const bool side0 = same_root(op.keys[j]->root(), st.keys[j][0]->root()); + const bool side1 = same_root(op.keys[j]->root(), st.keys[j][1]->root()); + EXPECT_NE(side0, side1); + EXPECT_TRUE(same_root(op.coins[j], side1 ? st.coins[j][1] : st.coins[j][0])); + } + } + EXPECT_EQ(seen.size(), Scheme::value_mask() + 1); + + const auto point_open = Scheme::open(st, 0, 1, xi); + const auto point_vector = Scheme::eval(point_open); + const auto sum = Scheme::column_sum(point_vector); + const auto sum_open = Scheme::open(st, 1, sum, xi); + EXPECT_EQ(point_vector, Scheme::eval(sum_open)); + EXPECT_TRUE(Scheme::accept(pp, com, sum_open, point_vector, xi)); +} + +template +void expect_sum_programming(const typename Scheme::public_params & pp, + const typename Scheme::commitment & com, const typename Scheme::state & st, + typename Scheme::input_type xi) +{ + const auto baseline = Scheme::eval(Scheme::open(st, 0, 0, xi)); + const auto hidden = Scheme::index_of(st.i); + std::uint64_t previous = 0; + bool have_previous = false; + for (std::uint64_t tau = 0; tau <= Scheme::value_mask(); ++tau) + { + const auto op = Scheme::open(st, 1, tau, xi); + const auto x = Scheme::eval(op); + EXPECT_TRUE(Scheme::accept(pp, com, op, x, xi)); + EXPECT_EQ(Scheme::column_sum(x), tau); + EXPECT_EQ(Scheme::column_sum(Scheme::eval_rotated(op)), tau); + for (std::size_t y = 0; y < Scheme::domain_size; ++y) + { + if (y == hidden) + continue; + EXPECT_EQ(x[y], baseline[y]); + } + if (have_previous) + { + EXPECT_EQ((x[hidden] - previous) & Scheme::value_mask(), + (tau - (tau - 1)) & Scheme::value_mask()); + } + previous = x[hidden]; + have_previous = true; + } +} + +} // namespace + +TEST(Ppvc, ProgramsEveryCoordinate) +{ + using narrow = dpf::ppvc; + using wide = dpf::ppvc; + const auto pp1 = narrow::setup_from_seed(seed_block(7, 9)); + const auto made1 = narrow::commit_from_seed(pp1, seed_block(11, 13)); + expect_point_programming(pp1, made1.first, made1.second, narrow::point(40)); + + const auto pp4 = wide::setup_from_seed(seed_block(7, 9)); + const auto made4 = wide::commit_from_seed(pp4, seed_block(11, 13)); + expect_point_programming(pp4, made4.first, made4.second, wide::point(40)); +} + +TEST(Ppvc, ProgramsEverySum) +{ + using narrow = dpf::ppvc; + using wide = dpf::ppvc; + const auto pp1 = narrow::setup_from_seed(seed_block(7, 9)); + const auto made1 = narrow::commit_from_seed(pp1, seed_block(11, 13)); + expect_sum_programming(pp1, made1.first, made1.second, narrow::point(40)); + + const auto pp4 = wide::setup_from_seed(seed_block(7, 9)); + const auto made4 = wide::commit_from_seed(pp4, seed_block(11, 13)); + expect_sum_programming(pp4, made4.first, made4.second, wide::point(40)); +} + +TEST(Ppvc, FreshCommitOpens) +{ + using scheme = dpf::ppvc; + const auto pp = scheme::setup(); + const auto [com, st] = scheme::commit(pp); + const auto xi = scheme::point(200); + const auto op = scheme::open(st, 0, 0x5a, xi); + const auto x = scheme::eval(op); + EXPECT_TRUE(scheme::accept(pp, com, op, x, xi)); + EXPECT_EQ(x[scheme::index_of(st.i)], 0x5au); + EXPECT_EQ(scheme::eval_rotated(op)[200], 0x5au); +} + +TEST(Ppvc, PrescribedIndexAndWrappingShift) +{ + using scheme = dpf::ppvc; + const auto pp = scheme::setup_from_seed(seed_block(8, 8)); + for (std::size_t y : {std::size_t{0}, std::size_t{1}, std::size_t{255}}) + EXPECT_EQ(scheme::index_of(scheme::point(y)), y); + + const auto hidden = scheme::point(255); + const auto [com, st] = scheme::commit_at(pp, hidden); + EXPECT_EQ(st.i, hidden); + ASSERT_TRUE(scheme::check_well_formed(pp, com, st)); + + const auto wrapped = scheme::open(st, 0, 7, scheme::point(0)); + EXPECT_EQ(scheme::index_of(wrapped.delta), 1u); + EXPECT_EQ(scheme::eval(wrapped)[255], 7u); + EXPECT_EQ(scheme::eval_rotated(wrapped)[0], 7u); + EXPECT_TRUE(scheme::accept(pp, com, wrapped, scheme::eval(wrapped), scheme::point(0))); + + const auto unshifted = scheme::open(st, 0, 7, hidden); + EXPECT_EQ(scheme::index_of(unshifted.delta), 0u); + EXPECT_EQ(scheme::eval_rotated(unshifted)[255], 7u); +} + +TEST(Ppvc, SeedExpansionIsDeterministic) +{ + using scheme = dpf::ppvc; + const auto seed = seed_block(3, 4); + const auto pp = scheme::setup_from_seed(seed); + EXPECT_EQ(pp.columns, scheme::setup_from_seed(seed).columns); + EXPECT_NE(pp.columns, scheme::setup_from_seed(seed_block(3, 5)).columns); + + const auto replica = seed_block(5, 6); + const auto first = scheme::commit_from_seed(pp, replica); + const auto second = scheme::commit_from_seed(pp, replica); + EXPECT_EQ(first.first, second.first); + EXPECT_EQ(first.second.i, second.second.i); + EXPECT_TRUE(same_root(first.second.keys[0][0]->root(), second.second.keys[0][0]->root())); + EXPECT_EQ(0, std::memcmp(first.second.keys[0][0]->correction_words().data(), + second.second.keys[0][0]->correction_words().data(), + sizeof(first.second.keys[0][0]->correction_words()))); + EXPECT_TRUE(scheme::audit(pp, first.first, replica)); + EXPECT_FALSE(scheme::audit(pp, first.first, seed_block(5, 7))); + + const auto fixed = scheme::point(3); + const auto at = scheme::commit_at_from_seed(pp, replica, fixed); + const auto at_again = scheme::commit_at_from_seed(pp, replica, fixed); + EXPECT_EQ(at.first, at_again.first); + EXPECT_EQ(at.second.i, fixed); + EXPECT_TRUE(scheme::check_well_formed(pp, at.first, at.second)); +} + +TEST(Ppvc, RejectsATamperedOpening) +{ + using scheme = dpf::ppvc; + const auto pp = scheme::setup_from_seed(seed_block(3, 4)); + auto [com, st] = scheme::commit_from_seed(pp, seed_block(5, 6)); + const auto xi = scheme::point(9); + auto op = scheme::open(st, 0, 3, xi); + ASSERT_TRUE(scheme::verify(pp, com, op)); + + alignas(16) std::uint8_t raw[16]; + simde_mm_store_si128(reinterpret_cast(raw), op.coins[0]); + raw[0] = static_cast(raw[0] ^ 0x1u); + op.coins[0] = simde_mm_load_si128(reinterpret_cast(raw)); + EXPECT_FALSE(scheme::verify(pp, com, op)); + + op = scheme::open(st, 0, 3, xi); + const unsigned side = scheme::disclosure(op)[0] ? 1u : 0u; + op.coins[0] = st.coins[0][side ^ 1u]; + EXPECT_FALSE(scheme::verify(pp, com, op)); + + op = scheme::open(st, 0, 3, xi); + com.slots[0][side].bytes[0] = + static_cast(com.slots[0][side].bytes[0] ^ 0x1u); + EXPECT_FALSE(scheme::verify(pp, com, op)); + + op.keys[0].reset(); + EXPECT_FALSE(scheme::verify(pp, com, op)); + EXPECT_THROW(scheme::eval(op), std::invalid_argument); + EXPECT_THROW(scheme::disclosure(op), std::invalid_argument); + + op = scheme::open(st, 0, 3, xi); + const auto x = scheme::eval(op); + EXPECT_FALSE(scheme::check_statement(op, std::vector(3), xi)); + auto wrong = x; + std::fill(wrong.begin(), wrong.end(), scheme::value_type{0}); + EXPECT_FALSE(scheme::check_statement(op, wrong, xi)); + op.mu = 3; + EXPECT_FALSE(scheme::check_statement(op, x, xi)); +} + +TEST(Ppvc, RejectsAMalformedReplica) +{ + using scheme = dpf::ppvc; + const auto pp = scheme::setup_from_seed(seed_block(21, 22)); + auto [com, st] = scheme::commit_from_seed(pp, seed_block(23, 24)); + ASSERT_TRUE(scheme::check_well_formed(pp, com, st)); + EXPECT_EQ(0, std::memcmp(st.keys[0][0]->correction_words().data(), + st.keys[0][1]->correction_words().data(), + sizeof(st.keys[0][0]->correction_words()))); + EXPECT_EQ(st.keys[0][0]->correction_advice(), st.keys[0][1]->correction_advice()); + + const auto moved = scheme::point(scheme::index_of(st.i) + 1); + auto fresh = dpf::make_dpf(moved, dpf::bit::one); + st.keys[0][0] = fresh.first.key(); + st.keys[0][1] = fresh.second.key(); + st.coins[0][0] = dpf::uniform_sample(); + st.coins[0][1] = dpf::uniform_sample(); + com.slots[0][0] = scheme::commit_root(pp, st.keys[0][0]->root(), st.coins[0][0]); + com.slots[0][1] = scheme::commit_root(pp, st.keys[0][1]->root(), st.coins[0][1]); + EXPECT_FALSE(scheme::check_well_formed(pp, com, st)); + + auto [com2, st2] = scheme::commit_from_seed(pp, seed_block(23, 24)); + auto replacement = dpf::make_dpf(st2.i, dpf::bit::one); + st2.keys[1][0] = replacement.first.key(); + st2.coins[1][0] = dpf::uniform_sample(); + com2.slots[1][0] = scheme::commit_root(pp, st2.keys[1][0]->root(), st2.coins[1][0]); + EXPECT_FALSE(scheme::check_well_formed(pp, com2, st2)); + + EXPECT_FALSE(scheme::check_well_formed(pp, scheme::commitment{}, scheme::state{})); +} + +TEST(Ppvc, RejectsAnOutOfRangeProgram) +{ + using scheme = dpf::ppvc; + const auto pp = scheme::setup_from_seed(seed_block(1, 1)); + const auto [com, st] = scheme::commit_from_seed(pp, seed_block(2, 2)); + (void)com; + EXPECT_THROW(scheme::open(st, 2, 0, scheme::point(0)), std::invalid_argument); + EXPECT_THROW(scheme::open(st, 0, 16, scheme::point(0)), std::invalid_argument); + EXPECT_THROW(scheme::open(st, 1, scheme::value_mask() + 1, scheme::point(0)), + std::invalid_argument); +} + +TEST(Ppvc, DeeperDomain) +{ + using scheme = dpf::ppvc; + EXPECT_GT(scheme::domain_bits, 8u); + const auto pp = scheme::setup_from_seed(seed_block(40, 41)); + const auto hidden = scheme::point(1000); + const auto [com, st] = scheme::commit_at_from_seed(pp, seed_block(42, 43), hidden); + ASSERT_TRUE(scheme::check_well_formed(pp, com, st)); + const auto xi = scheme::point(7); + const auto op = scheme::open(st, 0, 3, xi); + const auto x = scheme::eval(op); + EXPECT_TRUE(scheme::accept(pp, com, op, x, xi)); + EXPECT_EQ(scheme::eval_rotated(op)[7], 3u); + + const auto summed = scheme::open(st, 1, 1, xi); + EXPECT_EQ(scheme::column_sum(scheme::eval(summed)), 1u); + EXPECT_TRUE(scheme::audit(pp, scheme::commit_from_seed(pp, seed_block(42, 43)).first, + seed_block(42, 43))); +} + +TEST(Ppvc, FullWidthValue) +{ + using scheme = dpf::ppvc; + EXPECT_EQ(scheme::value_mask(), ~std::uint64_t{0}); + const auto pp = scheme::setup_from_seed(seed_block(50, 51)); + const auto [com, st] = scheme::commit_from_seed(pp, seed_block(52, 53)); + const auto xi = scheme::point(12); + for (std::uint64_t tau : {std::uint64_t{0}, std::uint64_t{1}, + std::uint64_t{1} << 63, ~std::uint64_t{0}}) + { + const auto point = scheme::open(st, 0, tau, xi); + const auto x = scheme::eval(point); + EXPECT_TRUE(scheme::accept(pp, com, point, x, xi)); + EXPECT_EQ(x[scheme::index_of(st.i)], tau); + + const auto summed = scheme::open(st, 1, tau, xi); + EXPECT_EQ(scheme::column_sum(scheme::eval(summed)), tau); + } +} + +TEST(Ppvc, DefaultSigmaUsesWholeBlocks) +{ + using scheme = dpf::ppvc; + EXPECT_EQ(scheme::sigma, 128u); + EXPECT_EQ(scheme::m_bits % 128, 0u); + EXPECT_EQ(scheme::commitment_bits, 2u * scheme::width * scheme::m_bits); + const auto pp = scheme::setup_from_seed(seed_block(60, 61)); + const auto [com, st] = scheme::commit_from_seed(pp, seed_block(62, 63)); + const auto xi = scheme::point(4); + const auto op = scheme::open(st, 0, 2, xi); + EXPECT_TRUE(scheme::accept(pp, com, op, scheme::eval(op), xi)); + EXPECT_TRUE(scheme::audit(pp, com, seed_block(62, 63))); +} + +TEST(KPpvc, OneCopyMatchesTheSingleScheme) +{ + using scheme = dpf::k_ppvc<1, std::uint8_t, 4, 8>; + const auto pp = scheme::one::setup_from_seed(seed_block(70, 71)); + const auto [com, st] = scheme::commit_from_seed(pp, seed_block(72, 73)); + ASSERT_TRUE(scheme::check_well_formed(pp, com, st)); + const std::array xi{scheme::one::point(15)}; + const std::array tau{6}; + const auto op = scheme::open(st, 0, tau, xi); + EXPECT_TRUE(scheme::verify(pp, com, op)); + EXPECT_EQ(scheme::combine_rotated(op), scheme::one::eval_rotated(op.copies[0])); +} + +TEST(KPpvc, ReprogramsOneCoordinateOfTheSum) +{ + using scheme = dpf::k_ppvc<2, std::uint8_t, 3, 8>; + const auto pp = scheme::one::setup_from_seed(seed_block(30, 31)); + const auto seed = seed_block(32, 33); + const auto [com, st] = scheme::commit_from_seed(pp, seed); + const auto again = scheme::commit_from_seed(pp, seed); + EXPECT_EQ(com, again.first); + EXPECT_EQ(st.copies[0].i, again.second.copies[0].i); + EXPECT_EQ(st.copies[1].i, again.second.copies[1].i); + EXPECT_NE(st.copies[0].i, st.copies[1].i); + ASSERT_TRUE(scheme::check_well_formed(pp, com, st)); + ASSERT_TRUE(scheme::audit(pp, com, seed)); + EXPECT_FALSE(scheme::audit(pp, com, seed_block(32, 34))); + + const std::array xi{scheme::one::point(4), scheme::one::point(90)}; + const std::array tau{1, 5}; + const auto op = scheme::open(st, 0, tau, xi); + ASSERT_TRUE(scheme::verify(pp, com, op)); + const auto sum = scheme::combine_rotated(op); + + std::array tau2{3, 5}; + const auto sum2 = scheme::combine_rotated(scheme::open(st, 0, tau2, xi)); + for (std::size_t y = 0; y < scheme::one::domain_size; ++y) + { + if (y == scheme::one::index_of(xi[0])) + EXPECT_NE(sum2[y], sum[y]); + else + EXPECT_EQ(sum2[y], sum[y]); + } + EXPECT_EQ((sum2[scheme::one::index_of(xi[0])] - sum[scheme::one::index_of(xi[0])]) + & scheme::one::value_mask(), + (tau2[0] - tau[0]) & scheme::one::value_mask()); + + std::array tau3{1, 2}; + const auto sum3 = scheme::combine_rotated(scheme::open(st, 0, tau3, xi)); + for (std::size_t y = 0; y < scheme::one::domain_size; ++y) + { + if (y == scheme::one::index_of(xi[1])) + EXPECT_NE(sum3[y], sum[y]); + else + EXPECT_EQ(sum3[y], sum[y]); + } +} + +TEST(KPpvc, SumModeAndFreshCommit) +{ + using scheme = dpf::k_ppvc<2, std::uint8_t, 3, 8>; + const auto pp = scheme::one::setup(); + const auto [com, st] = scheme::commit(pp); + EXPECT_NE(st.copies[0].i, st.copies[1].i); + ASSERT_TRUE(scheme::check_well_formed(pp, com, st)); + + const std::array xi{scheme::one::point(1), scheme::one::point(2)}; + const std::array tau{2, 4}; + const auto op = scheme::open(st, 1, tau, xi); + EXPECT_TRUE(scheme::verify(pp, com, op)); + for (std::size_t r = 0; r < 2; ++r) + EXPECT_EQ(scheme::one::column_sum(scheme::one::eval(op.copies[r])), tau[r]); + + auto bad = op; + bad.copies[1].coins[0] = seed_block(1, 2); + EXPECT_FALSE(scheme::verify(pp, com, bad)); + EXPECT_THROW(scheme::open(st, 3, tau, xi), std::invalid_argument); +} + +TEST(KPpvc, RejectsASharedHiddenIndex) +{ + using scheme = dpf::k_ppvc<2, std::uint8_t, 2, 8>; + const auto pp = scheme::one::setup_from_seed(seed_block(80, 81)); + const auto index = scheme::one::point(7); + auto [c0, s0] = scheme::one::commit_at(pp, index); + auto [c1, s1] = scheme::one::commit_at(pp, index); + ASSERT_TRUE(scheme::one::check_well_formed(pp, c0, s0)); + ASSERT_TRUE(scheme::one::check_well_formed(pp, c1, s1)); + + scheme::commitment com; + com.copies[0] = std::move(c0); + com.copies[1] = std::move(c1); + scheme::state st; + st.copies[0] = std::move(s0); + st.copies[1] = std::move(s1); + EXPECT_EQ(st.copies[0].i, st.copies[1].i); + EXPECT_FALSE(scheme::check_well_formed(pp, com, st)); +}