/// @file grotto/prefix_parity.hpp /// @author Ryan Henry /// @brief Prefix-parity and signed-prefix shares from a comparison key. /// @note Following Storrier, Vadapalli, Lyons, and Henry, ePrint 2023/108: prefix parity along one point or comparison key, the step they use to replace a DCF per spline piece. /// @copyright Copyright (c) 2019-2023 Ryan Henry and others /// @license Released under a GNU General Public v2.0 (GPLv2) license; /// see [LICENSE.md](@ref license) for details. #ifndef LIBDPF_INCLUDE_GROTTO_PREFIX_PARITY_HPP__ #define LIBDPF_INCLUDE_GROTTO_PREFIX_PARITY_HPP__ #include "hedley/hedley.h" #include "dpf/dpf_key.hpp" #include "dpf/bit.hpp" #include "dpf/bitstring.hpp" #include "dpf/twiddle.hpp" #include "dpf/leaf_node.hpp" #include "dpf/dcf.hpp" #include "dpf/blocked_dcf.hpp" #include "grotto/offset_iterable.hpp" #include "dpf/path_memoizer.hpp" #include "dpf/utils.hpp" #include "dpf/verifiable.hpp" #include #include namespace grotto { /// @brief Parity of the advice bits in `node` strictly below the bit offset of `x`. /// @tparam NodeT exterior-node type /// @tparam InputT input type used to place `x` inside the node /// @param node advice-bit node /// @param x query, used only for its offset within the node /// @return the XOR-parity of those bits /// @see grotto::prefix_parities /// \complexity XORs the limbs of `node` below that offset, then one `parity64`. `Θ(limbs in the node)`, extra space `Θ(1)`. template HEDLEY_NO_THROW auto parity_of_substring_prefix(const NodeT & node, InputT x) noexcept { static constexpr auto bits_per_limb = dpf::utils::bitlength_of_v; std::size_t off = dpf::offset_within_block(x); auto div = std::lldiv(off, bits_per_limb); auto parity = node[div.quot] & ((1ul << div.rem) - 1ul); for (std::size_t i = 0; i < static_cast(div.quot); ++i) parity ^= node[i]; return psnip_builtin_parity64(parity); } /// \complexity One path walk per endpoint. `depth` is `bitlength` of the key's input type. /// `for_each_offset` visits the endpoints in rotated order and `path_resume_for_level` restarts at the first level that differs, so a shared prefix is not walked twice. /// With the early-stop flag (the default), a zero suffix does not descend below the leaf alignment. /// Worst-case time is `Θ(E · depth)` interior steps for `E` endpoints. The memoizer stores one node per level, `Θ(depth)` extra space, plus the `E`-slot result. /// \rounds None. The key and the endpoints are arguments. /// \communication None. /// @see grotto::signed_prefix_parities /// @see grotto::offset_horner_eval template , bool> = false> static auto prefix_parities(const DpfKey & dpf, const std::array & endpoints, dpf::proof_token * pi = nullptr) { static constexpr std::size_t num_parts = NumParts; static constexpr std::size_t depth = DpfKey::depth; using input_type = typename DpfKey::input_type; static constexpr std::size_t input_bits = dpf::utils::bitlength_of_v; using exterior_node = typename DpfKey::exterior_node; if (pi != nullptr) { if constexpr (!DpfKey::is_verifiable) throw std::invalid_argument( "prefix_parities(..., prove): key must carry dpf::verifiable"); dpf::detail::vdpf::init_proof(*pi, dpf); } exterior_node leaf; auto path = make_basic_path_memoizer(dpf); std::array direction = { 0 }; // always "traverse left" to get to the root std::array parity = { 0 }; std::array prefix_parities; // A prefix that ends on a node boundary (trailing zeros past the leaf // width) is the parity accumulated at that node. Stopping any earlier // would cut through a leaf, so those endpoints still open the exterior // node. The path memoizer's high-water mark keeps a later endpoint from // resuming past nodes this walk never filled. std::size_t new_first = for_each_offset(std::begin(endpoints), std::end(endpoints), dpf.offset_x(0), [&](std::size_t which_part, input_type current_endpoint) { std::size_t to_level = depth; if constexpr (use_early_terminate_optimization) { // Stop where the remaining suffix is all zeros, but never // above a partial leaf. `parity_of_substring_prefix` indexes // bits inside the exterior node, which is wider than // `lg_outputs_per_leaf` when several outputs share that node. HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") constexpr std::size_t leaf_bit_lg = dpf::lg_outputs_per_leaf_v; HEDLEY_PRAGMA(GCC diagnostic pop) constexpr std::size_t align = std::max( DpfKey::lg_outputs_per_leaf, leaf_bit_lg); const std::size_t tz = dpf::utils::countr_zero{}(current_endpoint); if (tz >= input_bits) to_level = 0; else if (tz > align) to_level = input_bits - tz; } // assign_x sets path[0] = dpf.root on the first point and returns // the first level that differs from the previous point. direction[0] // and parity[0] stay 0, which is the empty-prefix parity. const std::size_t next_level = dpf::detail::path_resume_for_level( path, dpf, current_endpoint, to_level); std::size_t level_index = next_level - 1; if (level_index < to_level) { DPF_UNROLL_LOOP for (auto mask = dpf.msb_mask >> level_index; level_index < to_level; ++level_index, mask>>=1) { bool bit = !!(mask & current_endpoint); direction[level_index+1] = bit; path[level_index+1] = DpfKey::traverse_interior(path[level_index], dpf.correction_word(level_index, direction[level_index+1]), direction[level_index+1], DpfKey::tree::is_last_level(level_index, DpfKey::depth)); parity[level_index+1] = parity[level_index] ^ ((direction[level_index] ^ direction[level_index+1]) & dpf::get_lo_bit(path[level_index])); if constexpr (DpfKey::is_verifiable) { if (pi != nullptr) { const auto x_bits = static_cast( dpf::utils::to_integral_type{}(current_endpoint) >> (input_bits - (level_index + 1))); dpf::detail::vdpf::fold_node(*pi, level_index, x_bits, path[level_index + 1], dpf.correction_seeds()[level_index]); } } } } dpf::detail::path_note_filled_to(path, to_level); if (HEDLEY_UNLIKELY(to_level != depth)) { // Same quantity as a full walk that then steps only left into // an empty leaf substring: parity[L] ^ (direction[L] & t[L]). prefix_parities[which_part] = parity[to_level] ^ (direction[to_level] & dpf::get_lo_bit(path[to_level])); } else { if (next_level <= depth) { leaf = dpf.template traverse_exterior<0>(path[depth]); } prefix_parities[which_part] = parity[depth] ^ ((direction[depth] & dpf::get_lo_bit(path[depth])) ^ parity_of_substring_prefix(leaf, current_endpoint)); } }); if (pi != nullptr) dpf::detail::vdpf::fold_output_binding(*pi, dpf); return std::make_tuple(prefix_parities, new_first); } /// @brief Prefix parities with a VDPF proof fold on every traversed node. /// \complexity One path walk per endpoint. `depth` is `bitlength` of the key's input type. /// `for_each_offset` visits the endpoints in rotated order and `path_resume_for_level` restarts at the first level that differs, so a shared prefix is not walked twice. /// With the early-stop flag (the default), a zero suffix does not descend below the leaf alignment. /// Worst-case time is `Θ(E · depth)` interior steps for `E` endpoints. The memoizer stores one node per level, `Θ(depth)` extra space, plus the `E`-slot result. /// \rounds None. The key and the endpoints are arguments. /// \communication None. /// @see grotto::signed_prefix_parities /// @see grotto::offset_horner_eval template , bool> = false> static auto prefix_parities(const DpfKey & dpf, const std::array & endpoints, dpf::prove_ref pr) { return prefix_parities(dpf, endpoints, &pr.token); } template static auto all_segment_parities_from_prefix_parities(const DpfKey & dpf, const std::array & prefix_parities, std::size_t new_first) { static constexpr std::size_t num_parts = NumParts; std::array segment_parities; // currently setup so that for endpoints = {A, B, C}, segment_parities[0] = [A, B) while segment_parities[2] = [C, A) // to change to having the wrapping segment first, xor with previous prefix_parity and not subsequent one // also need to change final xor to simply using new_first // segment_parities[0] = prefix_parities[0] ^ prefix_parities[num_parts-1]; for (std::size_t i = 0; i < num_parts; ++i) { segment_parities[i] = prefix_parities[i] ^ prefix_parities[(i+1)%num_parts]; } segment_parities[(new_first+num_parts-1)%num_parts] ^= dpf::get_lo_bit(dpf.root()); return segment_parities; } template static auto specific_segment_parities_from_prefix_parities(const DpfKey & dpf, const std::array & segment_indices, const std::array & prefix_parities, std::size_t new_first) { assert(std::is_sorted(std::begin(segment_indices), std::end(segment_indices))); static constexpr std::size_t num_segments = NumSegments; static constexpr std::size_t num_parts = NumParts; std::array segment_parities; for (std::size_t i = 0; i < num_segments; ++i) { segment_parities[i] = prefix_parities[segment_indices[i]] ^ prefix_parities[segment_indices[(i+1)%num_segments]]; } auto begin = std::begin(segment_indices), end = std::end(segment_indices); segment_parities[(std::distance(begin, std::lower_bound(begin, end, new_first))+num_segments-1)%num_segments] ^= dpf::get_lo_bit(dpf.root()); return segment_parities; } /// \complexity One `prefix_parities` walk, then `Θ(E)` XORs to turn prefixes into segments. Same path cost as `prefix_parities`. /// @see grotto::prefix_parities /// @see grotto::signed_segment_parities template , bool> = false> static auto segment_parities(const DpfKey & dpf, const std::array & endpoints) { if constexpr (NumParts == 0) { (void)dpf; return std::array{}; } else if constexpr (NumParts == 1) { (void)endpoints; // One segment wraps the whole domain. Its parity share is the root // control bit (the general n-way formula reduces to the same value). return std::array{ static_cast(dpf::get_lo_bit(dpf.root()))}; } else { auto [prefix_parities, new_first] = grotto::prefix_parities(dpf, endpoints); return all_segment_parities_from_prefix_parities(dpf, prefix_parities, new_first); } } namespace detail { template struct key_has_cmp : std::false_type {}; template struct key_has_cmp().has_cmp())>> : std::true_type {}; template HEDLEY_NO_THROW uint64_t cmp_addend_raw(const KeyT & key) noexcept { if constexpr (dpf::is_party_key_v) return key.cmp_addend().raw(); else return key.cmp_addend(); } } // namespace detail /// @brief Additive prefix indicators from the key's DCF value sums. /// /// Same multi-knot walk as `prefix_parities` (shared path, MSB-first), but each /// level contributes the Boyle/Guo value correction instead of an advice-bit /// XOR. Party 0 + party 1 equals `eval` of the comparison at that endpoint, in /// the comparison group: for `dpf::gt(1)` that is `1` when `α < endpoint` and /// `0` otherwise. A published per-level sign is not used; that bit is the keep /// child's control and would leak the path (elementary2 /// `notes/signed-prefix-parity.md` §5). /// /// Endpoints are visited in the order given. The running sum is reused from the /// longest common prefix. The zero-suffix early stop of the XOR walk is not /// applied: later levels still add a Convert word. /// @tparam InputT input domain type /// @tparam DpfKey DPF key type /// @tparam NumParts num parts /// @tparam input_type input type /// @param dpf the DPF key /// @param endpoints sorted public endpoints /// @param pi proof token folded along the walk, or null /// @return Additive prefix indicators from the key's DCF value sums /// @throws std::invalid_argument if `key has no comparison channel` /// \complexity One path walk per endpoint. `depth` is `bitlength` of the key's input type. /// `for_each_offset` visits the endpoints in rotated order and `path_resume_for_level` restarts at the first level that differs, so a shared prefix is not walked twice. /// With the early-stop flag (the default), a zero suffix does not descend below the leaf alignment. /// Worst-case time is `Θ(E · depth)` interior steps for `E` endpoints. The memoizer stores one node per level, `Θ(depth)` extra space, plus the `E`-slot result. /// \rounds None. The key and the endpoints are arguments. /// \communication None. /// @see grotto::signed_prefix_parities /// @see grotto::offset_horner_eval template , bool> = false> static auto signed_prefix_parities(const DpfKey & dpf, const std::array & endpoints, dpf::proof_token * pi = nullptr) { if constexpr (!detail::key_has_cmp::value) { (void)dpf; (void)endpoints; (void)pi; throw std::invalid_argument("signed_prefix_parities: key has no comparison channel"); } else if (!dpf.has_cmp()) { throw std::invalid_argument("signed_prefix_parities: key has no comparison channel"); } else if (!dpf.cmp_assigned()) { throw std::invalid_argument( "signed_prefix_parities: wildcard comparison payload not assigned"); } else { if (pi != nullptr) { if constexpr (!DpfKey::is_verifiable) throw std::invalid_argument( "signed_prefix_parities(..., prove): key must carry dpf::verifiable"); dpf::detail::vdpf::init_proof(*pi, dpf); } using namespace dpf::detail::dcf_impl; using key_type = dpf::unwrap_party_key_t; constexpr std::size_t depth = key_type::depth; const auto & ch = dpf.cmp(); const std::size_t nbits = static_cast(ch.nbits); if constexpr (key_type::cmp_block > 0) { if (key_type::cmp_h > depth) throw std::invalid_argument("signed_prefix_parities: comparison is deeper than the key"); } else if (nbits > depth) throw std::invalid_argument("signed_prefix_parities: comparison is deeper than the key"); const uint64_t mask = ch.mask; const int party = dpf::get_lo_bit(dpf.root()) ? 1 : 0; const uint64_t addend = detail::cmp_addend_raw(dpf); auto path = dpf::make_basic_path_memoizer(dpf); // `sum_at[i]` is the path sum after the first `i` levels, before `cw_last`. std::array sum_at{}; std::array prefixes{}; for (std::size_t which = 0; which < NumParts; ++which) { auto tx = dpf.offset_x(endpoints[which]); dpf::utils::flip_msb_if_signed_integral(tx); if (ch.trivial == dpf::cmp_trivial::always_true || ch.trivial == dpf::cmp_trivial::always_false) { prefixes[which] = addend & mask; continue; } if constexpr (key_type::cmp_block > 0) { prefixes[which] = dpf::detail::blocked::eval_share(dpf, tx, path, pi); continue; } const std::size_t resume = dpf::detail::path_resume_for_level( path, dpf, tx, nbits); for (std::size_t level = resume, shift = level == 0 ? 0 : level - 1; level <= nbits; ++level, ++shift) { const auto bit_mask = key_type::msb_mask >> shift; const bool bit = !!(bit_mask & tx); path[level] = DpfKey::traverse_interior(path[level - 1], dpf.correction_word(level - 1, bit), bit, DpfKey::tree::is_last_level(level - 1, DpfKey::depth)); if constexpr (DpfKey::is_verifiable) { if (pi != nullptr) { const auto x_bits = static_cast( dpf::utils::to_integral_type{}(tx) >> (dpf::utils::bitlength_of_v - level)); dpf::detail::vdpf::fold_node(*pi, level - 1, x_bits, path[level], dpf.correction_seeds()[level - 1]); } } } dpf::detail::path_note_filled_to(path, nbits); const std::size_t start = resume > nbits ? nbits : resume - 1; uint64_t V = sum_at[start]; if (resume <= nbits) { auto bit_mask = key_type::msb_mask >> start; for (std::size_t i = start; i < nbits; ++i, bit_mask >>= 1) { const bool xi = !!(bit_mask & tx); const auto & parent = path[i]; const uint8_t t = static_cast(dpf::get_lo_bit(parent)); auto kids = key_type::interior_prg::eval01(dpf::unset_lo_2bits(parent)); const uint64_t v = convert_node(kids[xi ? 1 : 0], mask); const uint64_t contrib = (v + (t ? dpf.value_cw(i) : 0ULL)) & mask; V = (V + (party ? neg_m(contrib, mask) : contrib)) & mask; sum_at[i + 1] = V; } } const auto & leaf = path[nbits]; const uint8_t t = static_cast(dpf::get_lo_bit(leaf)); const uint64_t c = convert_node(leaf, mask); const uint64_t contrib = (c + (t ? dpf.cw_last() : 0ULL)) & mask; V = (V + (party ? neg_m(contrib, mask) : contrib)) & mask; if (ch.eval_as_ge) V = neg_m(V, mask); prefixes[which] = (V + addend) & mask; } if (pi != nullptr) dpf::detail::vdpf::fold_output_binding(*pi, dpf); return prefixes; } } /// \complexity One path walk per endpoint. `depth` is `bitlength` of the key's input type. /// `for_each_offset` visits the endpoints in rotated order and `path_resume_for_level` restarts at the first level that differs, so a shared prefix is not walked twice. /// With the early-stop flag (the default), a zero suffix does not descend below the leaf alignment. /// Worst-case time is `Θ(E · depth)` interior steps for `E` endpoints. The memoizer stores one node per level, `Θ(depth)` extra space, plus the `E`-slot result. /// \rounds None. The key and the endpoints are arguments. /// \communication None. /// @see grotto::signed_prefix_parities /// @see grotto::offset_horner_eval template , bool> = false> static auto signed_prefix_parities(const DpfKey & dpf, const std::array & endpoints, dpf::prove_ref pr) { return signed_prefix_parities(dpf, endpoints, &pr.token); } /// @brief Runtime-length form of `signed_prefix_parities`. `out[i]` receives the same /// share a one-element call would return for `endpoints[i]`. /// @tparam InputT input domain type /// @tparam DpfKey DPF key type /// @param dpf the DPF key /// @param endpoints sorted public endpoints /// @param n element count /// @param out the output buffer /// @param pi proof token folded along the walk, or null /// @throws std::invalid_argument if `key has no comparison channel` /// \complexity One path walk per endpoint. `depth` is `bitlength` of the key's input type. /// `for_each_offset` visits the endpoints in rotated order and `path_resume_for_level` restarts at the first level that differs, so a shared prefix is not walked twice. /// With the early-stop flag (the default), a zero suffix does not descend below the leaf alignment. /// Worst-case time is `Θ(E · depth)` interior steps for `E` endpoints. The memoizer stores one node per level, `Θ(depth)` extra space, plus the `E`-slot result. /// \rounds None. The key and the endpoints are arguments. /// \communication None. /// @see grotto::signed_prefix_parities /// @see grotto::offset_horner_eval template static void signed_prefix_parities_into(const DpfKey & dpf, const InputT * endpoints, std::size_t n, uint64_t * out, dpf::proof_token * pi = nullptr) { if constexpr (!detail::key_has_cmp::value) { (void)dpf; (void)endpoints; (void)n; (void)out; (void)pi; throw std::invalid_argument("signed_prefix_parities: key has no comparison channel"); } else if (!dpf.has_cmp()) { throw std::invalid_argument("signed_prefix_parities: key has no comparison channel"); } else if (!dpf.cmp_assigned()) { throw std::invalid_argument( "signed_prefix_parities: wildcard comparison payload not assigned"); } else if (n != 0 && (endpoints == nullptr || out == nullptr)) { throw std::invalid_argument("signed_prefix_parities: null endpoint buffer"); } else { if (pi != nullptr) { if constexpr (!DpfKey::is_verifiable) throw std::invalid_argument( "signed_prefix_parities(..., prove): key must carry dpf::verifiable"); dpf::detail::vdpf::init_proof(*pi, dpf); } using namespace dpf::detail::dcf_impl; using key_type = dpf::unwrap_party_key_t; constexpr std::size_t depth = key_type::depth; const auto & ch = dpf.cmp(); const std::size_t nbits = static_cast(ch.nbits); if constexpr (key_type::cmp_block > 0) { if (key_type::cmp_h > depth) throw std::invalid_argument("signed_prefix_parities: comparison is deeper than the key"); } else if (nbits > depth) throw std::invalid_argument("signed_prefix_parities: comparison is deeper than the key"); const uint64_t mask = ch.mask; const int party = dpf::get_lo_bit(dpf.root()) ? 1 : 0; const uint64_t addend = detail::cmp_addend_raw(dpf); auto path = dpf::make_basic_path_memoizer(dpf); std::array sum_at{}; for (std::size_t which = 0; which < n; ++which) { auto tx = dpf.offset_x(endpoints[which]); dpf::utils::flip_msb_if_signed_integral(tx); if (ch.trivial == dpf::cmp_trivial::always_true || ch.trivial == dpf::cmp_trivial::always_false) { out[which] = addend & mask; continue; } if constexpr (key_type::cmp_block > 0) { out[which] = dpf::detail::blocked::eval_share(dpf, tx, path, pi); continue; } const std::size_t resume = dpf::detail::path_resume_for_level( path, dpf, tx, nbits); for (std::size_t level = resume, shift = level == 0 ? 0 : level - 1; level <= nbits; ++level, ++shift) { const auto bit_mask = key_type::msb_mask >> shift; const bool bit = !!(bit_mask & tx); path[level] = DpfKey::traverse_interior(path[level - 1], dpf.correction_word(level - 1, bit), bit, DpfKey::tree::is_last_level(level - 1, DpfKey::depth)); if constexpr (DpfKey::is_verifiable) { if (pi != nullptr) { const auto x_bits = static_cast( dpf::utils::to_integral_type{}(tx) >> (dpf::utils::bitlength_of_v - level)); dpf::detail::vdpf::fold_node(*pi, level - 1, x_bits, path[level], dpf.correction_seeds()[level - 1]); } } } dpf::detail::path_note_filled_to(path, nbits); const std::size_t start = resume > nbits ? nbits : resume - 1; uint64_t V = sum_at[start]; if (resume <= nbits) { auto bit_mask = key_type::msb_mask >> start; for (std::size_t i = start; i < nbits; ++i, bit_mask >>= 1) { const bool xi = !!(bit_mask & tx); const auto & parent = path[i]; const uint8_t t = static_cast(dpf::get_lo_bit(parent)); auto kids = key_type::interior_prg::eval01(dpf::unset_lo_2bits(parent)); const uint64_t v = convert_node(kids[xi ? 1 : 0], mask); const uint64_t contrib = (v + (t ? dpf.value_cw(i) : 0ULL)) & mask; V = (V + (party ? neg_m(contrib, mask) : contrib)) & mask; sum_at[i + 1] = V; } } const auto & leaf = path[nbits]; const uint8_t t = static_cast(dpf::get_lo_bit(leaf)); const uint64_t c = convert_node(leaf, mask); const uint64_t contrib = (c + (t ? dpf.cw_last() : 0ULL)) & mask; V = (V + (party ? neg_m(contrib, mask) : contrib)) & mask; if (ch.eval_as_ge) V = neg_m(V, mask); out[which] = (V + addend) & mask; } if (pi != nullptr) dpf::detail::vdpf::fold_output_binding(*pi, dpf); } } /// @brief One-hot segment shares for a unit `gt` comparison (`if_true = 1`, `if_false = 0`). /// /// `endpoints` is sorted ascending. Piece `i < n-1` is `[endpoints[i], endpoints[i+1])` /// and the last piece wraps. Party 0 + party 1 is `1` on the piece that contains /// the key's target and `0` on the others, so a public dot product with LUT /// constants is an additive share of the function value (not of its negation). /// @tparam InputT input domain type /// @tparam DpfKey DPF key type /// @tparam NumParts num parts /// @tparam input_type input type /// @param dpf the DPF key /// @param endpoints sorted public endpoints /// @return One-hot segment shares for a unit `gt` comparison (`if_true = 1`, `if_false = 0`) /// @throws std::invalid_argument if `key has no comparison channel` /// \complexity One `signed_prefix_parities` over the `NumParts` endpoints, then `Θ(NumParts)` subtractions. Same path cost. /// @note Requires a `gt` comparison. The hot piece reconstructs to 1, the others to 0. /// @see grotto::signed_prefix_parities template , bool> = false> static auto signed_segment_parities(const DpfKey & dpf, const std::array & endpoints) { using namespace dpf::detail::dcf_impl; if constexpr (!detail::key_has_cmp::value) { (void)dpf; (void)endpoints; throw std::invalid_argument("signed_segment_parities: key has no comparison channel"); } else if (!dpf.has_cmp()) { throw std::invalid_argument("signed_segment_parities: key has no comparison channel"); } else { const auto & ch = dpf.cmp(); if (ch.kind != dpf::cmp_kind::gt) throw std::invalid_argument( "signed_segment_parities: comparison must be gt"); const uint64_t mask = ch.mask; // Public +1 sits with party 0 (root control bit clear), matching a dealer // split of the constant 1 with a zero blind on party 1. const uint64_t one = dpf::get_lo_bit(dpf.root()) ? 0ULL : (1ULL & mask); if constexpr (NumParts == 0) { (void)dpf; (void)endpoints; return std::array{}; } else if constexpr (NumParts == 1) { (void)endpoints; return std::array{one}; } else { const auto prefix = signed_prefix_parities(dpf, endpoints); std::array segments{}; for (std::size_t i = 0; i < NumParts; ++i) { const uint64_t nxt = prefix[(i + 1) % NumParts]; segments[i] = (nxt + neg_m(prefix[i], mask)) & mask; } // `segments[n-1]` is `p[0] - p[n-1]`. The wrap piece is that difference // plus the public 1, which makes the pieces sum to 1. segments[NumParts - 1] = (segments[NumParts - 1] + one) & mask; return segments; } } } } // namespace grotto #endif // LIBDPF_INCLUDE_GROTTO_PREFIX_PARITY_HPP__