/// @file dpf/eval_until.hpp /// @brief Prefix-resuming evaluation for multilevel / incremental DPF keys. /// @details Google's incremental-DPF `EvaluateUntil(level, prefixes, ctx)` /// (Poplar / private heavy hitters, ePrint 2021/017) walks only the /// live prefixes. `eval_prefixes` always restarts at the root and /// materializes all `2^N` nodes; a path memoizer resumes one path. /// `idpf_eval_ctx` keeps the interior node under each live prefix. /// `eval_until(ctx, level, prefixes)` returns the output shares at /// that hierarchy level for those prefixes only, then updates the /// context. Empty `prefixes` on a fresh context (level 0) leaves the /// root seed in place. Each later prefix must extend a prefix from /// the previous call. /// @see dpf/eval_walk.hpp (`eval_prefixes`), dpf/idpf_agg.hpp /// @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_EVAL_UNTIL_HPP__ #define LIBDPF_INCLUDE_DPF_EVAL_UNTIL_HPP__ #include #include #include #include #include #include #include "hedley/hedley.h" #include "dpf/dpf_key.hpp" #include "dpf/eval_common.hpp" #include "dpf/incremental.hpp" #include "dpf/placement.hpp" #include "dpf/secret_share.hpp" #include "dpf/utils.hpp" namespace dpf { /// @brief Live-prefix evaluation context for a multilevel / idpf key. /// @details Hierarchy level 0 holds only the root seed. After /// `eval_until(..., L, prefixes)`, `level()` is `L` and /// `node_count()` equals `prefixes.size()`. /// \complexity O(|prefixes|) stored nodes (not O(2^level)). template class idpf_eval_ctx { public: using key_type = unwrap_party_key_t; using input_type = typename key_type::input_type; using node_type = typename key_type::interior_node; explicit idpf_eval_ctx(const KeyT & key) : key_{&key}, level_{0}, prefixes_{}, nodes_{} { nodes_.push_back(static_cast(key).root()); // Compact parent of every length-1 prefix is the empty prefix `0`. prefixes_.push_back(input_type{0}); } HEDLEY_ALWAYS_INLINE HEDLEY_NO_THROW const KeyT & key() const noexcept { return *key_; } /// @brief Last hierarchy level that `eval_until` wrote (0 = root only). HEDLEY_ALWAYS_INLINE HEDLEY_NO_THROW std::size_t level() const noexcept { return level_; } /// @brief Number of saved interior nodes (one per live prefix). HEDLEY_ALWAYS_INLINE HEDLEY_NO_THROW std::size_t node_count() const noexcept { return nodes_.size(); } HEDLEY_ALWAYS_INLINE HEDLEY_NO_THROW const std::vector & prefixes() const noexcept { return prefixes_; } /// @brief Drop every saved prefix except `prefix` (must be live). void retain(input_type prefix) { for (std::size_t i = 0; i < prefixes_.size(); ++i) { if (prefixes_[i] == prefix) { prefixes_ = {prefix}; nodes_ = {nodes_[i]}; return; } } throw std::invalid_argument( "idpf_eval_ctx::retain: prefix is not live in this context"); } private: template friend auto eval_until(idpf_eval_ctx & ctx, std::size_t level, PrefRange && prefixes); const KeyT * key_; std::size_t level_; std::vector prefixes_; std::vector nodes_; }; namespace detail { namespace eval_until_detail { template HEDLEY_NO_THROW constexpr std::size_t slot_for_prefix(std::size_t prefix_len) noexcept { for (std::size_t i = 0; i < KeyT::num_outputs; ++i) { if (KeyT::meta[i].prefix == prefix_len) return i; } return static_cast(-1); } template HEDLEY_NO_THROW constexpr std::size_t tree_level_for_prefix(std::size_t prefix_len) noexcept { const auto i = slot_for_prefix(prefix_len); return (i == static_cast(-1)) ? static_cast(-1) : KeyT::meta[i].tree_level; } /// @brief Walk `from_tree_level` โ†’ `to_tree_level` along the high bits of `x`. /// \complexity O(to โˆ’ from) interior traversals. template HEDLEY_ALWAYS_INLINE Node walk_interior(const KeyT & key, Node node, typename KeyT::input_type x, std::size_t from_tree_level, std::size_t to_tree_level) { using key_type = KeyT; if (to_tree_level <= from_tree_level) return node; auto level_index = from_tree_level + 1; auto mask = key.msb_mask >> (level_index - 1); for (; level_index <= to_tree_level; ++level_index, mask >>= 1) { const bool bit = !!(mask & x); auto cw = key.correction_word(level_index - 1, bit); const bool is_last = key_type::tree::is_last_level(level_index - 1, key.depth); node = key_type::traverse_interior(node, cw, bit, is_last); } return node; } template HEDLEY_ALWAYS_INLINE auto exterior_at(const KeyT & key, const Node & node, InputT domain_x) { using key_type = unwrap_party_key_t; using output_type = typename key_type::template concrete_output_type; constexpr auto N = key_type::meta[I].prefix; // `traverse_exterior` lives on the underlying key; party_key inherits it. auto leaf = key.template traverse_exterior(node); auto lane_x = detail::incr::lane_input(domain_x, N, key_type::input_bits); detail::incr::absorb_public_addend_lane( static_cast(key), leaf, lane_x); // Pass the party-tagged key type so the share carries the party id. return *make_eval_dpf_output(leaf, lane_x); } template HEDLEY_ALWAYS_INLINE bool exterior_dispatch(std::size_t slot, const KeyT & key, const Node & node, InputT domain_x, Out & out, std::index_sequence) { return ((Is == slot ? (out = exterior_at(key, node, domain_x), true) : false) || ...); } template HEDLEY_ALWAYS_INLINE auto share_type_tag() { using key_type = unwrap_party_key_t; using output_type = typename key_type::template concrete_output_type<0>; return eval_leaf_result_t{}; } } // namespace eval_until_detail } // namespace detail /// @brief Evaluate output shares at hierarchy `level` for `prefixes` only. /// @details Each prefix is a compact integer in `[0, 2^level)`. The first call /// may pass an empty range at level 0 to keep the root seed. Later /// calls require `level > ctx.level()` and every prefix to extend one /// saved parent. Returns one share per prefix, in the same order. /// Opened values match `eval_point(out, key, domain)` for the /// same prefix length (see `eval_until_test`). /// \complexity O(|prefixes| ยท (level โˆ’ ctx.level())) interior traversals. /// Saved nodes stay O(|prefixes|), not O(2^level). /// @see ePrint 2021/017 (Poplar EvaluateUntil), ePrint 2024/1190 (I-DPF agg) template HEDLEY_WARN_UNUSED_RESULT auto eval_until(idpf_eval_ctx & ctx, std::size_t level, PrefRange && prefixes) { using key_type = typename idpf_eval_ctx::key_type; using input_type = typename idpf_eval_ctx::input_type; using node_type = typename idpf_eval_ctx::node_type; using share_type = decltype(detail::eval_until_detail::share_type_tag()); static_assert(is_multilevel_key_v, "eval_until requires a multilevel / incremental DPF key"); const KeyT & key_ref = ctx.key(); const key_type & key = static_cast(key_ref); std::vector pref_list; for (auto && p : prefixes) pref_list.push_back(static_cast(p)); if (pref_list.empty()) { if (ctx.level_ != 0 || level != 0) { throw std::invalid_argument( "eval_until: empty prefixes only valid on a fresh context at level 0"); } return std::vector{}; } if (level == 0) { throw std::invalid_argument( "eval_until: level 0 has only the root; pass a positive hierarchy level"); } if (level <= ctx.level_) { throw std::invalid_argument( "eval_until: level must strictly advance past the context level"); } if (level > key_type::input_bits) { throw std::invalid_argument( "eval_until: level exceeds the input bit length"); } const auto slot = detail::eval_until_detail::slot_for_prefix(level); if (slot == static_cast(-1)) { throw std::invalid_argument( "eval_until: key has no output planted at that prefix length"); } const auto to_tree = detail::eval_until_detail::tree_level_for_prefix(level); const std::size_t from_tree = (ctx.level_ == 0) ? 0 : detail::eval_until_detail::tree_level_for_prefix(ctx.level_); constexpr auto bits = key_type::input_bits; const auto parent_shift = level - ctx.level_; std::vector new_nodes; std::vector shares; new_nodes.reserve(pref_list.size()); shares.reserve(pref_list.size()); for (auto p : pref_list) { if (level < bits && (static_cast(p) >> level) != 0) { throw std::invalid_argument( "eval_until: prefix does not fit in the requested bit length"); } const input_type parent = static_cast(p >> parent_shift); std::size_t parent_idx = static_cast(-1); for (std::size_t i = 0; i < ctx.prefixes_.size(); ++i) { if (ctx.prefixes_[i] == parent) { parent_idx = i; break; } } if (parent_idx == static_cast(-1)) { throw std::invalid_argument( "eval_until: prefix does not extend a live parent"); } auto domain_x = static_cast( static_cast(p) << (bits - level)); domain_x = key.offset_x(domain_x); utils::flip_msb_if_signed_integral(domain_x); node_type node = detail::eval_until_detail::walk_interior(key, ctx.nodes_[parent_idx], domain_x, from_tree, to_tree); share_type share{}; const bool ok = detail::eval_until_detail::exterior_dispatch(slot, key_ref, node, domain_x, share, std::make_index_sequence{}); if (!ok) { throw std::logic_error("eval_until: exterior dispatch missed slot"); } new_nodes.push_back(node); shares.push_back(share); } ctx.level_ = level; ctx.prefixes_ = std::move(pref_list); ctx.nodes_ = std::move(new_nodes); return shares; } } // namespace dpf #endif // LIBDPF_INCLUDE_DPF_EVAL_UNTIL_HPP__