2026-09-24 14:08:32 -06:00
/// @file grotto/prefix_parity.hpp
/// @author Ryan Henry <ryan.henry@ucalgary.ca>
2026-09-24 23:18:10 -06:00
/// @brief Prefix-parity and signed-prefix shares from a comparison key.
2026-09-28 05:59:19 -06:00
/// @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.
2026-09-24 14:08:32 -06:00
/// @copyright Copyright (c) 2019-2023 Ryan Henry and others
/// @license Released under a GNU General Public v2.0 (GPLv2) license;
2026-09-24 23:27:47 -06:00
/// see [LICENSE.md](@ref license) for details.
2026-09-24 14:08:32 -06:00
# 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"
2026-09-24 20:44:07 -06:00
# include "dpf/blocked_dcf.hpp"
2026-09-24 14:08:32 -06:00
# include "grotto/offset_iterable.hpp"
# include "dpf/path_memoizer.hpp"
# include "dpf/utils.hpp"
2026-09-28 05:59:19 -06:00
# include "dpf/verifiable.hpp"
2026-09-24 14:08:32 -06:00
# include <stdexcept>
# include <type_traits>
namespace grotto
{
2026-09-28 05:59:19 -06:00
/// @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)`.
2026-09-24 14:08:32 -06:00
template < typename NodeT ,
typename InputT >
2026-09-24 20:44:07 -06:00
HEDLEY_NO_THROW
2026-09-24 14:08:32 -06:00
auto parity_of_substring_prefix ( const NodeT & node , InputT x ) noexcept
{
static constexpr auto bits_per_limb = dpf : : utils : : bitlength_of_v < decltype ( node [ 0 ] ) > ;
std : : size_t off = dpf : : offset_within_block < dpf : : bit , NodeT > ( x ) ;
auto div = std : : lldiv ( off , bits_per_limb ) ;
auto parity = node [ div . quot ] & ( ( 1ul < < div . rem ) - 1ul ) ;
2026-09-28 05:59:19 -06:00
for ( std : : size_t i = 0 ; i < static_cast < std : : size_t > ( div . quot ) ; + + i ) parity ^ = node [ i ] ;
2026-09-24 14:08:32 -06:00
return psnip_builtin_parity64 ( parity ) ;
}
2026-09-28 05:59:19 -06:00
/// \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
2026-09-24 14:08:32 -06:00
template < bool use_early_terminate_optimization = true ,
typename InputT ,
typename DpfKey ,
std : : size_t NumParts ,
std : : enable_if_t < std : : is_same_v < InputT , typename DpfKey : : input_type > , bool > = false >
2026-09-28 05:59:19 -06:00
static auto prefix_parities ( const DpfKey & dpf , const std : : array < InputT , NumParts > & endpoints ,
dpf : : proof_token * pi = nullptr )
2026-09-24 14:08:32 -06:00
{
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 < input_type > ;
using exterior_node = typename DpfKey : : exterior_node ;
2026-09-28 05:59:19 -06:00
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 ) ;
}
2026-09-24 14:08:32 -06:00
exterior_node leaf ;
auto path = make_basic_path_memoizer ( dpf ) ;
std : : array < uint_fast8_t , depth + 1 > direction = { 0 } ; // always "traverse left" to get to the root
std : : array < uint_fast8_t , depth + 1 > parity = { 0 } ;
std : : array < bool , num_parts > 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.
2026-09-24 23:18:10 -06:00
HEDLEY_PRAGMA ( GCC diagnostic push )
HEDLEY_PRAGMA ( GCC diagnostic ignored " -Wignored-attributes " )
2026-09-24 14:08:32 -06:00
constexpr std : : size_t leaf_bit_lg
= dpf : : lg_outputs_per_leaf_v < dpf : : bit , exterior_node > ;
2026-09-24 23:18:10 -06:00
HEDLEY_PRAGMA ( GCC diagnostic pop )
2026-09-24 14:08:32 -06:00
constexpr std : : size_t align = std : : max (
DpfKey : : lg_outputs_per_leaf , leaf_bit_lg ) ;
const std : : size_t tz = dpf : : utils : : countr_zero < input_type > { } ( 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 ;
2026-09-24 23:18:10 -06:00
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 ) ) ;
2026-09-24 14:08:32 -06:00
parity [ level_index + 1 ] = parity [ level_index ] ^ ( ( direction [ level_index ] ^ direction [ level_index + 1 ] ) & dpf : : get_lo_bit ( path [ level_index ] ) ) ;
2026-09-28 05:59:19 -06:00
if constexpr ( DpfKey : : is_verifiable )
{
if ( pi ! = nullptr )
{
const auto x_bits = static_cast < psnip_uint64_t > (
dpf : : utils : : to_integral_type < input_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 ] ) ;
}
}
2026-09-24 14:08:32 -06:00
}
}
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 ]
2026-09-28 05:59:19 -06:00
^ ( ( direction [ depth ] & dpf : : get_lo_bit ( path [ depth ] ) )
2026-09-24 14:08:32 -06:00
^ parity_of_substring_prefix ( leaf , current_endpoint ) ) ;
}
} ) ;
2026-09-28 05:59:19 -06:00
if ( pi ! = nullptr )
dpf : : detail : : vdpf : : fold_output_binding ( * pi , dpf ) ;
2026-09-24 14:08:32 -06:00
return std : : make_tuple ( prefix_parities , new_first ) ;
}
2026-09-28 05:59:19 -06:00
/// @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 use_early_terminate_optimization = true ,
typename InputT ,
typename DpfKey ,
std : : size_t NumParts ,
std : : enable_if_t < std : : is_same_v < InputT , typename DpfKey : : input_type > , bool > = false >
static auto prefix_parities ( const DpfKey & dpf , const std : : array < InputT , NumParts > & endpoints ,
dpf : : prove_ref pr )
{
return prefix_parities < use_early_terminate_optimization > ( dpf , endpoints , & pr . token ) ;
}
2026-09-24 14:08:32 -06:00
template < typename DpfKey ,
std : : size_t NumParts >
static auto all_segment_parities_from_prefix_parities ( const DpfKey & dpf ,
const std : : array < bool , NumParts > & prefix_parities , std : : size_t new_first )
{
static constexpr std : : size_t num_parts = NumParts ;
std : : array < bool , num_parts > 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 < typename DpfKey ,
std : : size_t NumSegments ,
std : : size_t NumParts >
static auto specific_segment_parities_from_prefix_parities ( const DpfKey & dpf ,
const std : : array < std : : size_t , NumSegments > & segment_indices ,
const std : : array < bool , NumParts > & 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 < bool , num_segments > 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 ;
}
2026-09-28 05:59:19 -06:00
/// \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
2026-09-24 14:08:32 -06:00
template < bool use_early_terminate_optimization = true ,
typename InputT ,
typename DpfKey ,
std : : size_t NumParts ,
std : : enable_if_t < std : : is_same_v < InputT , typename DpfKey : : input_type > , bool > = false >
static auto segment_parities ( const DpfKey & dpf , const std : : array < InputT , NumParts > & endpoints )
{
if constexpr ( NumParts = = 0 )
{
( void ) dpf ;
return std : : array < bool , 0 > { } ;
}
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 < bool , 1 > {
static_cast < bool > ( dpf : : get_lo_bit ( dpf . root ( ) ) ) } ;
}
else
{
auto [ prefix_parities , new_first ] = grotto : : prefix_parities < use_early_terminate_optimization > ( dpf , endpoints ) ;
return all_segment_parities_from_prefix_parities ( dpf , prefix_parities , new_first ) ;
}
}
namespace detail
{
template < typename T , typename = void >
struct key_has_cmp : std : : false_type { } ;
template < typename T >
struct key_has_cmp < T , std : : void_t < decltype ( std : : declval < const T & > ( ) . has_cmp ( ) ) > >
: std : : true_type { } ;
template < typename KeyT >
2026-09-24 20:44:07 -06:00
HEDLEY_NO_THROW
2026-09-24 14:08:32 -06:00
uint64_t cmp_addend_raw ( const KeyT & key ) noexcept
{
if constexpr ( dpf : : is_party_key_v < KeyT > )
return key . cmp_addend ( ) . raw ( ) ;
else
return key . cmp_addend ( ) ;
}
} // namespace detail
2026-09-24 23:18:10 -06:00
/// @brief Additive prefix indicators from the key's DCF value sums.
2026-09-24 14:08:32 -06:00
///
/// 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.
2026-09-24 23:18:10 -06:00
/// @tparam InputT input domain type
/// @tparam DpfKey DPF key type
/// @tparam NumParts num parts
/// @tparam input_type input type
/// @param dpf the DPF key
2026-09-28 05:59:19 -06:00
/// @param endpoints sorted public endpoints
/// @param pi proof token folded along the walk, or null
2026-09-24 23:18:10 -06:00
/// @return Additive prefix indicators from the key's DCF value sums
/// @throws std::invalid_argument if `key has no comparison channel`
2026-09-28 05:59:19 -06:00
/// \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
2026-09-24 14:08:32 -06:00
template < typename InputT ,
typename DpfKey ,
std : : size_t NumParts ,
std : : enable_if_t < std : : is_same_v < InputT , typename DpfKey : : input_type > , bool > = false >
static auto signed_prefix_parities ( const DpfKey & dpf ,
2026-09-28 05:59:19 -06:00
const std : : array < InputT , NumParts > & endpoints ,
dpf : : proof_token * pi = nullptr )
2026-09-24 14:08:32 -06:00
{
if constexpr ( ! detail : : key_has_cmp < DpfKey > : : value )
{
( void ) dpf ;
( void ) endpoints ;
2026-09-28 05:59:19 -06:00
( void ) pi ;
2026-09-24 14:08:32 -06:00
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
{
2026-09-28 05:59:19 -06:00
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 ) ;
}
2026-09-24 14:08:32 -06:00
using namespace dpf : : detail : : dcf_impl ;
using key_type = dpf : : unwrap_party_key_t < DpfKey > ;
constexpr std : : size_t depth = key_type : : depth ;
const auto & ch = dpf . cmp ( ) ;
const std : : size_t nbits = static_cast < std : : size_t > ( ch . nbits ) ;
2026-09-24 20:44:07 -06:00
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 )
2026-09-24 14:08:32 -06:00
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 < uint64_t , depth + 1 > sum_at { } ;
std : : array < uint64_t , NumParts > 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 ;
}
2026-09-24 20:44:07 -06:00
if constexpr ( key_type : : cmp_block > 0 )
{
2026-09-28 05:59:19 -06:00
prefixes [ which ] = dpf : : detail : : blocked : : eval_share ( dpf , tx , path , pi ) ;
2026-09-24 20:44:07 -06:00
continue ;
}
2026-09-24 14:08:32 -06:00
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 ] ,
2026-09-24 23:18:10 -06:00
dpf . correction_word ( level - 1 , bit ) , bit ,
DpfKey : : tree : : is_last_level ( level - 1 , DpfKey : : depth ) ) ;
2026-09-28 05:59:19 -06:00
if constexpr ( DpfKey : : is_verifiable )
{
if ( pi ! = nullptr )
{
const auto x_bits = static_cast < psnip_uint64_t > (
dpf : : utils : : to_integral_type < InputT > { } ( tx )
> > ( dpf : : utils : : bitlength_of_v < InputT > - level ) ) ;
dpf : : detail : : vdpf : : fold_node ( * pi , level - 1 , x_bits ,
path [ level ] , dpf . correction_seeds ( ) [ level - 1 ] ) ;
}
}
2026-09-24 14:08:32 -06:00
}
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 < uint8_t > ( 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 < uint8_t > ( 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 ;
}
2026-09-28 05:59:19 -06:00
if ( pi ! = nullptr )
dpf : : detail : : vdpf : : fold_output_binding ( * pi , dpf ) ;
2026-09-24 14:08:32 -06:00
return prefixes ;
}
}
2026-09-28 05:59:19 -06:00
/// \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 < typename InputT ,
typename DpfKey ,
std : : size_t NumParts ,
std : : enable_if_t < std : : is_same_v < InputT , typename DpfKey : : input_type > , bool > = false >
static auto signed_prefix_parities ( const DpfKey & dpf ,
const std : : array < InputT , NumParts > & endpoints , dpf : : prove_ref pr )
{
return signed_prefix_parities ( dpf , endpoints , & pr . token ) ;
}
2026-09-24 14:08:32 -06:00
2026-09-24 23:18:10 -06:00
/// @brief Runtime-length form of `signed_prefix_parities`. `out[i]` receives the same
2026-09-24 14:08:32 -06:00
/// share a one-element call would return for `endpoints[i]`.
2026-09-24 23:18:10 -06:00
/// @tparam InputT input domain type
/// @tparam DpfKey DPF key type
/// @param dpf the DPF key
2026-09-28 05:59:19 -06:00
/// @param endpoints sorted public endpoints
/// @param n element count
2026-09-24 23:18:10 -06:00
/// @param out the output buffer
2026-09-28 05:59:19 -06:00
/// @param pi proof token folded along the walk, or null
2026-09-24 23:18:10 -06:00
/// @throws std::invalid_argument if `key has no comparison channel`
2026-09-28 05:59:19 -06:00
/// \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
2026-09-24 14:08:32 -06:00
template < typename InputT ,
typename DpfKey >
static void signed_prefix_parities_into ( const DpfKey & dpf ,
2026-09-28 05:59:19 -06:00
const InputT * endpoints , std : : size_t n , uint64_t * out ,
dpf : : proof_token * pi = nullptr )
2026-09-24 14:08:32 -06:00
{
if constexpr ( ! detail : : key_has_cmp < DpfKey > : : value )
{
( void ) dpf ;
( void ) endpoints ;
( void ) n ;
( void ) out ;
2026-09-28 05:59:19 -06:00
( void ) pi ;
2026-09-24 14:08:32 -06:00
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
{
2026-09-28 05:59:19 -06:00
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 ) ;
}
2026-09-24 14:08:32 -06:00
using namespace dpf : : detail : : dcf_impl ;
using key_type = dpf : : unwrap_party_key_t < DpfKey > ;
constexpr std : : size_t depth = key_type : : depth ;
const auto & ch = dpf . cmp ( ) ;
const std : : size_t nbits = static_cast < std : : size_t > ( ch . nbits ) ;
2026-09-24 20:44:07 -06:00
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 )
2026-09-24 14:08:32 -06:00
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 < uint64_t , depth + 1 > 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 ;
}
2026-09-24 20:44:07 -06:00
if constexpr ( key_type : : cmp_block > 0 )
{
2026-09-28 05:59:19 -06:00
out [ which ] = dpf : : detail : : blocked : : eval_share ( dpf , tx , path , pi ) ;
2026-09-24 20:44:07 -06:00
continue ;
}
2026-09-24 14:08:32 -06:00
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 ] ,
2026-09-24 23:18:10 -06:00
dpf . correction_word ( level - 1 , bit ) , bit ,
DpfKey : : tree : : is_last_level ( level - 1 , DpfKey : : depth ) ) ;
2026-09-28 05:59:19 -06:00
if constexpr ( DpfKey : : is_verifiable )
{
if ( pi ! = nullptr )
{
const auto x_bits = static_cast < psnip_uint64_t > (
dpf : : utils : : to_integral_type < InputT > { } ( tx )
> > ( dpf : : utils : : bitlength_of_v < InputT > - level ) ) ;
dpf : : detail : : vdpf : : fold_node ( * pi , level - 1 , x_bits ,
path [ level ] , dpf . correction_seeds ( ) [ level - 1 ] ) ;
}
}
2026-09-24 14:08:32 -06:00
}
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 < uint8_t > ( 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 < uint8_t > ( 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 ;
}
2026-09-28 05:59:19 -06:00
if ( pi ! = nullptr )
dpf : : detail : : vdpf : : fold_output_binding ( * pi , dpf ) ;
2026-09-24 14:08:32 -06:00
}
}
2026-09-24 23:18:10 -06:00
/// @brief One-hot segment shares for a unit `gt` comparison (`if_true = 1`, `if_false = 0`).
2026-09-24 14:08:32 -06:00
///
/// `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).
2026-09-24 23:18:10 -06:00
/// @tparam InputT input domain type
/// @tparam DpfKey DPF key type
/// @tparam NumParts num parts
/// @tparam input_type input type
/// @param dpf the DPF key
2026-09-28 05:59:19 -06:00
/// @param endpoints sorted public endpoints
2026-09-24 23:18:10 -06:00
/// @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`
2026-09-28 05:59:19 -06:00
/// \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
2026-09-24 14:08:32 -06:00
template < typename InputT ,
typename DpfKey ,
std : : size_t NumParts ,
std : : enable_if_t < std : : is_same_v < InputT , typename DpfKey : : input_type > , bool > = false >
static auto signed_segment_parities ( const DpfKey & dpf ,
const std : : array < InputT , NumParts > & endpoints )
{
using namespace dpf : : detail : : dcf_impl ;
if constexpr ( ! detail : : key_has_cmp < DpfKey > : : 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 < uint64_t , 0 > { } ;
}
else if constexpr ( NumParts = = 1 )
{
( void ) endpoints ;
return std : : array < uint64_t , 1 > { one } ;
}
else
{
const auto prefix = signed_prefix_parities ( dpf , endpoints ) ;
std : : array < uint64_t , NumParts > 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__