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));
+}