libdpf/include/dpf/blocked_dcf.hpp
Ryan Henry 0d22946a0e Checkpoint the party/runtime stack before share-program and malicious-mode work.
Ship the TLS mesh, composer, Beaver/Yao/leaf MPC, prep/online paths, apps, and docs so the tree is pushable before elevating share_expr, security_mode, and prep resume.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-28 05:59:19 -06:00

610 lines
21 KiB
C++

/// @file dpf/blocked_dcf.hpp
/// @brief Blocked-checkpoint comparison: one ring word per block of levels.
/// @details The seed spine stays dense. Ring words are published only at a
/// public checkpoint schedule. Point eval expands parked siblings
/// up to the next checkpoint; a full-domain memoizer already holds
/// those nodes. `q` tail bits, when the comparison sets the key
/// depth, are a residual table on the node at height `h`.
/// @note Boyle, Chandran, Gilboa, Gupta, Ishai, Kumar, and Rathee (EUROCRYPT 2021, ePrint 2020/1392) publish a value-correction word on every level. This implementation is ahead of that DCF on payload size: one ring word every B levels, about B times fewer value words, with the same one-seed-word spine. Point evaluation expands the siblings between checkpoints.
/// @copyright Copyright (c) 2019-2026 Ryan Henry and [others](@ref authors)
/// @license Released under a GNU General Public v2.0 (GPLv2) license.
#ifndef LIBDPF_INCLUDE_DPF_BLOCKED_DCF_HPP__
#define LIBDPF_INCLUDE_DPF_BLOCKED_DCF_HPP__
#include <array>
#include <cstddef>
#include <cstdint>
#include <type_traits>
#include <utility>
#include <vector>
#include "hedley/hedley.h"
#include "dpf/dcf.hpp"
#include "dpf/path_memoizer.hpp"
#include "dpf/tree_traits.hpp"
#include "dpf/twiddle.hpp"
#include "dpf/utils.hpp"
#include "dpf/verifiable.hpp"
namespace dpf
{
namespace detail
{
namespace blocked
{
/// @brief High bit set on `fold_node` level so blocked proofs diverge from native.
/// @details Must match the level passed to `make_cs` at blocked keygen. Parked
/// sibling folds reuse this tag with the sibling's tree depth so they
/// share `correction_seeds[depth-1]`.
inline constexpr std::size_t fold_spine_tag = std::size_t{1} << 15;
template <typename NodeT>
HEDLEY_ALWAYS_INLINE
void fold_spine_node(proof_token & pi, std::size_t level,
psnip_uint64_t x_bits, NodeT seed, const cs_block & cs) noexcept
{
detail::vdpf::fold_node(pi, fold_spine_tag | level, x_bits, seed, cs);
}
template <std::size_t H, std::size_t B>
struct schedule
{
static constexpr std::size_t count =
(B == 0 || H == 0) ? 0 : (H + B - 1) / B;
static constexpr auto depths = [] {
std::array<std::size_t, count == 0 ? 1 : count> cs{};
if constexpr (count == 0)
return cs;
const std::size_t base = H / count;
const std::size_t extra = H % count;
std::size_t acc = 0;
for (std::size_t i = 0; i < count; ++i)
{
acc += base + (i < extra ? 1 : 0);
cs[i] = acc;
}
return cs;
}();
HEDLEY_CONST
HEDLEY_NO_THROW
static constexpr bool contains(std::size_t depth) noexcept
{
for (std::size_t i = 0; i < count; ++i)
{
if (depths[i] == depth)
return true;
}
return false;
}
HEDLEY_CONST
HEDLEY_NO_THROW
static constexpr std::size_t index(std::size_t depth) noexcept
{
for (std::size_t i = 0; i < count; ++i)
{
if (depths[i] == depth)
return i;
}
return static_cast<std::size_t>(-1);
}
};
template <typename PRG, typename Node>
HEDLEY_NO_THROW
HEDLEY_ALWAYS_INLINE
uint64_t rho_of(const Node & node, uint64_t mask) noexcept
{
auto kids = dpf::tree_traits<PRG>::expand_value(node);
return dcf_impl::convert_node(kids[0], mask);
}
HEDLEY_CONST
HEDLEY_NO_THROW
HEDLEY_ALWAYS_INLINE
constexpr int control_sign(uint8_t t0, uint8_t t1) noexcept
{
return static_cast<int>(t0) - static_cast<int>(t1);
}
HEDLEY_CONST
HEDLEY_NO_THROW
HEDLEY_ALWAYS_INLINE
constexpr uint64_t mul_sgn(int sgn, uint64_t v, uint64_t mask) noexcept
{
if (sgn > 0)
return v & mask;
if (sgn < 0)
return dcf_impl::neg_m(v, mask);
return 0;
}
/// @brief Group element `sgn` (`+1`, `-1`, or `0`) used as an `assign_cmp` coefficient.
/// @param sgn the `sgn`
/// @param mask the bit mask
/// @return Group element `sgn` (`+1`, `-1`, or `0`) used as an `assign_cmp` coefficient
HEDLEY_CONST
HEDLEY_NO_THROW
HEDLEY_ALWAYS_INLINE
constexpr uint64_t sgn_coeff(int sgn, uint64_t mask) noexcept
{
if (sgn > 0)
return 1ULL & mask;
if (sgn < 0)
return dcf_impl::neg_m(1ULL, mask);
return 0;
}
template <typename PRG, typename Node>
HEDLEY_NO_THROW
uint64_t checkpoint_word(const Node & n0, const Node & n1, uint64_t beta,
uint64_t mask) noexcept
{
const int sgn = control_sign(
static_cast<uint8_t>(dpf::get_lo_bit(n0)),
static_cast<uint8_t>(dpf::get_lo_bit(n1)));
const uint64_t r0 = rho_of<PRG>(n0, mask);
const uint64_t r1 = rho_of<PRG>(n1, mask);
const uint64_t inner =
(beta + dcf_impl::neg_m(r0, mask) + r1) & mask;
return mul_sgn(sgn, inner, mask);
}
template <typename Node>
HEDLEY_PURE
HEDLEY_NO_THROW
uint64_t checkpoint_coeff(const Node & n0, const Node & n1,
uint64_t mask) noexcept
{
return sgn_coeff(control_sign(
static_cast<uint8_t>(dpf::get_lo_bit(n0)),
static_cast<uint8_t>(dpf::get_lo_bit(n1))), mask);
}
template <typename PRG, typename Node>
HEDLEY_NON_NULL(4)
HEDLEY_NO_THROW
void suffix_masks(const Node & seed, std::size_t q, uint64_t mask,
uint64_t * out) noexcept
{
Node cur[4]{};
Node nxt[8]{};
cur[0] = seed;
std::size_t n = 1;
for (std::size_t lvl = 0; lvl < q; ++lvl)
{
std::size_t m = 0;
for (std::size_t i = 0; i < n; ++i)
{
auto kids = dpf::tree_traits<PRG>::expand_value(cur[i]);
nxt[m++] = kids[0];
nxt[m++] = kids[1];
}
for (std::size_t i = 0; i < m; ++i)
cur[i] = nxt[i];
n = m;
}
for (std::size_t i = 0; i < n; ++i)
out[i] = dcf_impl::convert_node(cur[i], mask);
}
template <typename PRG, typename Node>
HEDLEY_NON_NULL(8)
HEDLEY_NO_THROW
void tail_words(const Node & n0, const Node & n1, uint64_t beta, uint64_t mask,
bool include_eq, uint64_t suffix, std::size_t q, uint64_t * words,
uint64_t * coeffs) noexcept
{
uint64_t u0[4]{};
uint64_t u1[4]{};
suffix_masks<PRG>(n0, q, mask, u0);
suffix_masks<PRG>(n1, q, mask, u1);
const int sgn = control_sign(
static_cast<uint8_t>(dpf::get_lo_bit(n0)),
static_cast<uint8_t>(dpf::get_lo_bit(n1)));
const uint64_t coeff = sgn_coeff(sgn, mask);
const std::size_t n = std::size_t{1} << q;
for (std::size_t z = 0; z < n; ++z)
{
const bool pred = include_eq
? (z <= suffix)
: (z < suffix);
const uint64_t inner =
((pred ? beta : 0ULL) + dcf_impl::neg_m(u0[z], mask) + u1[z]) & mask;
words[z] = mul_sgn(sgn, inner, mask);
if (coeffs != nullptr)
coeffs[z] = pred ? coeff : 0ULL;
}
}
template <typename KeyT>
HEDLEY_NO_THROW
uint64_t add_membership(uint64_t acc, const typename KeyT::interior_node & node,
uint64_t word, uint64_t mask, int party) noexcept
{
using prg = typename KeyT::interior_prg;
const uint8_t t = static_cast<uint8_t>(dpf::get_lo_bit(node));
const uint64_t y =
(rho_of<prg>(node, mask) + (t ? word : 0ULL)) & mask;
return (acc + (party ? dcf_impl::neg_m(y, mask) : y)) & mask;
}
template <typename KeyT>
uint64_t add_frontier(uint64_t acc, const typename KeyT::interior_node & seed,
std::size_t from_depth, std::size_t to_depth, const KeyT & dpf, uint64_t word,
uint64_t mask, int party)
{
using node = typename KeyT::interior_node;
HEDLEY_PRAGMA(GCC diagnostic push)
HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes")
std::vector<node> cur;
std::vector<node> nxt;
HEDLEY_PRAGMA(GCC diagnostic pop)
cur.push_back(seed);
for (std::size_t lvl = from_depth; lvl < to_depth; ++lvl)
{
nxt.clear();
nxt.reserve(cur.size() * 2);
const node cw0 = dpf.correction_word(lvl, false);
const node cw1 = dpf.correction_word(lvl, true);
const bool is_last = KeyT::tree::is_last_level(lvl, KeyT::depth);
for (const node & fs : cur)
{
auto kids = KeyT::traverse_interior01(fs, cw0, cw1, is_last);
nxt.push_back(kids[0]);
nxt.push_back(kids[1]);
}
cur.swap(nxt);
}
for (const node & fs : cur)
acc = add_membership<KeyT>(acc, fs, word, mask, party);
return acc;
}
template <typename KeyT, typename InputT>
HEDLEY_CONST
HEDLEY_NO_THROW
HEDLEY_ALWAYS_INLINE
constexpr uint64_t query_suffix(InputT tx, std::size_t nbits, std::size_t q) noexcept
{
if (q == 0)
return 0;
uint64_t z = 0;
auto bit_mask = KeyT::msb_mask >> (nbits - q);
for (std::size_t j = 0; j < q; ++j, bit_mask >>= 1)
z = (z << 1) | static_cast<uint64_t>(!!(bit_mask & tx));
return z;
}
template <typename KeyT>
HEDLEY_NO_THROW
uint64_t finish_share(const KeyT & dpf, uint64_t suffix, uint64_t acc,
const typename KeyT::interior_node & at_h, int party) noexcept
{
using namespace dcf_impl;
using prg = typename KeyT::interior_prg;
const auto & ch = dpf.cmp();
const uint64_t mask = ch.mask;
constexpr std::size_t q = KeyT::cmp_q;
constexpr std::size_t h = KeyT::cmp_h;
if constexpr (q == 0)
{
if (ch.include_eq)
{
constexpr auto wi = schedule<h, KeyT::cmp_block>::index(h);
acc = add_membership<KeyT>(acc, at_h, dpf.value_cw(wi), mask, party);
}
(void)suffix;
}
else
{
const uint64_t z = suffix;
uint64_t u[4]{};
suffix_masks<prg>(at_h, q, mask, u);
const uint8_t t = static_cast<uint8_t>(dpf::get_lo_bit(at_h));
const uint64_t y = (u[z] + (t ? dpf.tail_cw(z) : 0ULL)) & mask;
acc = (acc + (party ? neg_m(y, mask) : y)) & mask;
}
if (ch.eval_as_ge)
acc = neg_m(acc, mask);
const uint64_t add = [&]() -> uint64_t {
if constexpr (is_party_key_v<KeyT>)
return dpf.cmp_addend().raw();
else
return dpf.cmp_addend();
}();
return (acc + add) & mask;
}
template <typename KeyT, typename InputT, typename PathMemoizer>
uint64_t eval_share(const KeyT & dpf, InputT tx, PathMemoizer & path,
proof_token * pi = nullptr)
{
using node = typename KeyT::interior_node;
const auto & ch = dpf.cmp();
const uint64_t mask = ch.mask;
const uint64_t add = [&]() -> uint64_t {
if constexpr (is_party_key_v<KeyT>)
return dpf.cmp_addend().raw();
else
return dpf.cmp_addend();
}();
if (ch.trivial == cmp_trivial::always_true
|| ch.trivial == cmp_trivial::always_false)
return add & mask;
constexpr std::size_t h = KeyT::cmp_h;
using sched = schedule<h, KeyT::cmp_block>;
const std::size_t nbits = static_cast<std::size_t>(ch.nbits);
const int party = dpf::get_lo_bit(dpf.root()) ? 1 : 0;
// Expand the seed spine without native path folds; blocked proofs use
// domain-separated tags on newly filled levels only (path-memo safe).
const auto resume = dpf::detail::path_resume_for_level(path, dpf, tx, h);
dpf::detail::ensure_level(dpf, tx, path, h);
if constexpr (KeyT::is_verifiable)
{
if (pi != nullptr && resume <= h)
{
constexpr auto input_bits =
utils::bitlength_of_v<typename KeyT::input_type>;
for (std::size_t level = resume; level <= h; ++level)
{
const auto x_bits = static_cast<psnip_uint64_t>(
utils::to_integral_type<typename KeyT::input_type>{}(tx)
>> (input_bits - level));
fold_spine_node(*pi, level - 1, x_bits, path[level],
dpf.correction_seeds()[level - 1]);
}
}
}
struct parked
{
node seed;
std::size_t depth;
psnip_uint64_t prefix_bits;
};
parked pend[128];
std::size_t npend = 0;
uint64_t acc = 0;
auto bit_mask = KeyT::msb_mask;
// Value accumulation always walks from the root. Proof folds for parked
// siblings must not repeat depths already authenticated on a warm path:
// re-folding XORs the same contribution away (path-memo cancel bug).
for (std::size_t level = 0; level < h; ++level, bit_mask >>= 1)
{
const bool xi = !!(bit_mask & tx);
const node & parent = path[level];
const node right = KeyT::traverse_interior(parent,
dpf.correction_word(level, true), true,
KeyT::tree::is_last_level(level, KeyT::depth));
if (!xi)
{
pend[npend].seed = right;
pend[npend].depth = level + 1;
// Sibling is the right child of `parent`: path bits with low bit 1.
const auto path_bits = static_cast<psnip_uint64_t>(
utils::to_integral_type<typename KeyT::input_type>{}(tx)
>> (utils::bitlength_of_v<typename KeyT::input_type>
- (level + 1)));
pend[npend].prefix_bits = (path_bits & ~static_cast<psnip_uint64_t>(1))
| static_cast<psnip_uint64_t>(1);
++npend;
}
const std::size_t c = level + 1;
if (sched::contains(c))
{
const uint64_t word = dpf.value_cw(sched::index(c));
for (std::size_t p = 0; p < npend; ++p)
{
if constexpr (KeyT::is_verifiable)
{
if (pi != nullptr && pend[p].depth >= resume)
{
// Tree depth (not checkpoint index): must match CS level.
fold_spine_node(*pi, pend[p].depth - 1, pend[p].prefix_bits,
pend[p].seed, dpf.correction_seeds()[pend[p].depth - 1]);
}
}
acc = add_frontier<KeyT>(acc, pend[p].seed, pend[p].depth, c,
dpf, word, mask, party);
}
npend = 0;
}
}
const uint64_t suffix = query_suffix<KeyT>(tx, nbits, KeyT::cmp_q);
return finish_share(dpf, suffix, acc, path[h], party);
}
template <typename KeyT, typename Integral, typename Memo>
HEDLEY_PURE
HEDLEY_NO_THROW
bool memo_has(const Memo & memo, Integral prefix, std::size_t depth,
Integral from_lane, Integral to_excl) noexcept
{
const auto shift = KeyT::cmp_depth - depth;
const auto from_p = from_lane >> shift;
if (prefix < from_p)
return false;
const auto idx = static_cast<std::size_t>(prefix - from_p);
const auto count = memo.get_nodes_at_level(depth, from_lane, to_excl);
return idx < count;
}
template <typename KeyT, typename Integral, typename Memo>
const typename KeyT::interior_node & memo_node(const Memo & memo, Integral prefix,
std::size_t depth, Integral from_lane)
{
const auto shift = KeyT::cmp_depth - depth;
const auto from_p = from_lane >> shift;
const auto idx = static_cast<std::size_t>(prefix - from_p);
return memo[depth][idx];
}
/// @brief Evaluate one lane from an interval memo; optionally fold a VDPF proof.
/// @details When `pi == nullptr` or the key is not verifiable, matches the
/// historical body (no folds). When set, folds path / covered
/// checkpoint / frontier seeds with `fold_spine_node`, skipping depths
/// already covered by `path` (same resume rule as `eval_share`).
/// Interval BFS prove must keep passing a null `pi` here.
template <typename KeyT, typename Integral, typename Memo,
typename PathMemoizer = ::dpf::basic_path_memoizer<KeyT>>
uint64_t eval_share_memo(const KeyT & dpf, Integral lane,
Integral from_lane, Integral to_excl, const Memo & memo,
proof_token * pi = nullptr, PathMemoizer * path = nullptr)
{
using node = typename KeyT::interior_node;
const auto & ch = dpf.cmp();
const uint64_t mask = ch.mask;
const uint64_t add = [&]() -> uint64_t {
if constexpr (is_party_key_v<KeyT>)
return dpf.cmp_addend().raw();
else
return dpf.cmp_addend();
}();
if (ch.trivial == cmp_trivial::always_true
|| ch.trivial == cmp_trivial::always_false)
return add & mask;
constexpr std::size_t h = KeyT::cmp_h;
using sched = schedule<h, KeyT::cmp_block>;
const int party = dpf::get_lo_bit(dpf.root()) ? 1 : 0;
const std::size_t nbits = static_cast<std::size_t>(ch.nbits);
std::size_t resume = 0;
if constexpr (KeyT::is_verifiable)
{
if (pi != nullptr && path != nullptr)
{
const auto tx = static_cast<typename KeyT::input_type>(lane);
resume = dpf::detail::path_resume_for_level(*path, dpf, tx, h);
dpf::detail::path_note_filled_to(*path, h);
}
}
if constexpr (KeyT::is_verifiable)
{
if (pi != nullptr && resume <= h)
{
Integral pfx = 0;
for (std::size_t depth = 0; depth <= h; ++depth)
{
if (depth >= resume && depth >= 1)
{
fold_spine_node(*pi, depth - 1,
static_cast<psnip_uint64_t>(pfx),
memo_node<KeyT>(memo, pfx, depth, from_lane),
dpf.correction_seeds()[depth - 1]);
}
if (depth < h)
{
const bool xi =
((lane >> (nbits - 1 - depth)) & Integral{1}) != 0;
pfx = static_cast<Integral>(
(pfx << 1) | (xi ? Integral{1} : Integral{0}));
}
}
}
}
struct parked
{
node seed;
Integral prefix;
std::size_t depth;
};
parked pend[128];
std::size_t npend = 0;
uint64_t acc = 0;
Integral path_pref = 0;
for (std::size_t level = 0; level < h; ++level)
{
const bool xi = ((lane >> (nbits - 1 - level)) & Integral{1}) != 0;
const node & parent = memo_node<KeyT>(memo, path_pref, level, from_lane);
const Integral sib = static_cast<Integral>((path_pref << 1) | Integral{1});
if (!xi)
{
const node right = KeyT::traverse_interior(parent,
dpf.correction_word(level, true), true,
KeyT::tree::is_last_level(level, KeyT::depth));
pend[npend].seed = right;
pend[npend].prefix = sib;
pend[npend].depth = level + 1;
++npend;
}
path_pref = static_cast<Integral>(
(path_pref << 1) | (xi ? Integral{1} : Integral{0}));
const std::size_t c = level + 1;
if (!sched::contains(c))
continue;
const uint64_t word = dpf.value_cw(sched::index(c));
for (std::size_t p = 0; p < npend; ++p)
{
const std::size_t extra = c - pend[p].depth;
const Integral leftmost =
static_cast<Integral>(pend[p].prefix << extra);
const Integral rightmost = static_cast<Integral>(
leftmost + static_cast<Integral>((Integral{1} << extra) - 1));
const bool covered =
memo_has<KeyT>(memo, leftmost, c, from_lane, to_excl)
&& memo_has<KeyT>(memo, rightmost, c, from_lane, to_excl);
if (covered && extra < 16)
{
const Integral nleaf = static_cast<Integral>(Integral{1} << extra);
for (Integral k = 0; k < nleaf; ++k)
{
const auto pref = static_cast<Integral>(leftmost + k);
if constexpr (KeyT::is_verifiable)
{
if (pi != nullptr && c >= resume && c >= 1)
{
fold_spine_node(*pi, c - 1,
static_cast<psnip_uint64_t>(pref),
memo_node<KeyT>(memo, pref, c, from_lane),
dpf.correction_seeds()[c - 1]);
}
}
acc = add_membership<KeyT>(acc,
memo_node<KeyT>(memo, pref, c, from_lane), word, mask,
party);
}
}
else
{
if constexpr (KeyT::is_verifiable)
{
if (pi != nullptr && pend[p].depth >= resume
&& pend[p].depth >= 1)
{
fold_spine_node(*pi, pend[p].depth - 1,
static_cast<psnip_uint64_t>(pend[p].prefix),
pend[p].seed,
dpf.correction_seeds()[pend[p].depth - 1]);
}
}
acc = add_frontier<KeyT>(acc, pend[p].seed, pend[p].depth, c,
dpf, word, mask, party);
}
}
npend = 0;
}
const node & at_h = memo_node<KeyT>(memo, path_pref, h, from_lane);
const uint64_t suffix = static_cast<uint64_t>(lane)
& (KeyT::cmp_q == 0 ? 0ULL : ((1ULL << KeyT::cmp_q) - 1ULL));
return finish_share(dpf, suffix, acc, at_h, party);
}
} // namespace blocked
} // namespace detail
} // namespace dpf
#endif // LIBDPF_INCLUDE_DPF_BLOCKED_DCF_HPP__