8484
8585pub type DeepPolynomialEvaluations < F > = ( Vec < FieldElement < F > > , Vec < FieldElement < F > > ) ;
8686
87+ /// Deep-composition sums that are identical across all FRI queries of a
88+ /// single proof (see `compute_query_invariant_deep_terms`).
89+ pub struct QueryInvariantDeepTerms < FieldExtension >
90+ where
91+ FieldExtension : Send + Sync + IsField ,
92+ {
93+ /// `ood_row_sum[row] = sum_col trace_term_coeffs[col][row] * ood(row, col)`.
94+ ood_row_sum : Vec < FieldElement < FieldExtension > > ,
95+ /// Derived from `proof.composition_poly_parts_ood_evaluation().len()`.
96+ number_of_parts : usize ,
97+ /// `challenges.z.pow(number_of_parts)`.
98+ z_pow : FieldElement < FieldExtension > ,
99+ /// `sum_j composition_poly_parts_ood_evaluation[j] * challenges.gammas[j]`.
100+ h_sum_zpow : FieldElement < FieldExtension > ,
101+ }
102+
87103// The verifier reads proofs in place from their rkyv archive; archived field
88104// elements are viewed as native ones, which is only valid on little-endian.
89105#[ cfg( not( target_endian = "little" ) ) ]
@@ -649,6 +665,60 @@ pub trait IsStarkVerifier<
649665 openings_ok & terminal_ok
650666 }
651667
668+ /// Sums that depend only on `challenges` and proof-level OOD/gamma data —
669+ /// identical for every FRI query — computed once instead of once per
670+ /// query.
671+ fn compute_query_invariant_deep_terms (
672+ challenges : & Challenges < FieldExtension > ,
673+ proof : StarkProofView < ' _ , Field , FieldExtension , PI > ,
674+ ) -> Option < QueryInvariantDeepTerms < FieldExtension > > {
675+ let trace_ood_evaluations = proof. trace_ood_evaluations ( ) ;
676+ let ood_evaluations_table_height = trace_ood_evaluations. height ( ) ;
677+ let ood_evaluations_table_width = trace_ood_evaluations. width ( ) ;
678+ let ood_data = trace_ood_evaluations. row_major_data ( ) ;
679+ let trace_term_coeffs = & challenges. trace_term_coeffs ;
680+
681+ if trace_term_coeffs. is_empty ( )
682+ || trace_term_coeffs. len ( ) * trace_term_coeffs[ 0 ] . len ( )
683+ != ood_evaluations_table_height * ood_evaluations_table_width
684+ {
685+ return None ;
686+ }
687+
688+ let mut ood_row_sum = Vec :: with_capacity ( ood_evaluations_table_height) ;
689+ for row_idx in 0 ..ood_evaluations_table_height {
690+ let ood_row = & ood_data[ row_idx * ood_evaluations_table_width
691+ ..( row_idx + 1 ) * ood_evaluations_table_width] ;
692+ let mut sum = FieldElement :: < FieldExtension > :: zero ( ) ;
693+ for col_idx in 0 ..ood_evaluations_table_width {
694+ sum += & trace_term_coeffs[ col_idx] [ row_idx] * & ood_row[ col_idx] ;
695+ }
696+ ood_row_sum. push ( sum) ;
697+ }
698+
699+ let composition_parts_ood = proof. composition_poly_parts_ood_evaluation ( ) ;
700+ let number_of_parts = composition_parts_ood. len ( ) ;
701+ let z_pow = challenges. z . pow ( number_of_parts) ;
702+
703+ // A malformed proof/challenge set can advertise more composition
704+ // parts than sampled gammas; reject rather than silently truncate
705+ // the sum below.
706+ if challenges. gammas . len ( ) < number_of_parts {
707+ return None ;
708+ }
709+ let mut h_sum_zpow = FieldElement :: < FieldExtension > :: zero ( ) ;
710+ for ( h_i_zpower, gamma) in composition_parts_ood. iter ( ) . zip ( challenges. gammas . iter ( ) ) {
711+ h_sum_zpow += h_i_zpower * gamma;
712+ }
713+
714+ Some ( QueryInvariantDeepTerms {
715+ ood_row_sum,
716+ number_of_parts,
717+ z_pow,
718+ h_sum_zpow,
719+ } )
720+ }
721+
652722 fn reconstruct_deep_composition_poly_evaluations_for_all_queries (
653723 challenges : & Challenges < FieldExtension > ,
654724 domain : & VerifierDomain < Field > ,
@@ -676,6 +746,8 @@ pub trait IsStarkVerifier<
676746 let primitive_root = & Field :: get_primitive_root_of_unity ( domain. root_order as u64 )
677747 . expect ( "verifier domain root_order is a valid power of two" ) ;
678748
749+ let query_invariant_terms = Self :: compute_query_invariant_deep_terms ( challenges, proof) ?;
750+
679751 for ( i, iota) in challenges. iotas . iter ( ) . enumerate ( ) {
680752 let opening = proof. deep_poly_opening ( i) ;
681753
@@ -694,19 +766,6 @@ pub trait IsStarkVerifier<
694766 . map ( |a| a. evaluations ( ) )
695767 . unwrap_or ( & [ ] ) ;
696768
697- let evaluation_point = Self :: query_challenge_to_evaluation_point ( * iota, false , domain) ;
698- deep_poly_evaluations. push ( Self :: reconstruct_deep_composition_poly_evaluation (
699- proof,
700- & evaluation_point,
701- primitive_root,
702- challenges,
703- lde_precomputed,
704- lde_main,
705- lde_aux,
706- opening. composition_poly ( ) . evaluations ( ) ,
707- ) ?) ;
708-
709- // Mirror for the symmetric query point.
710769 let lde_precomputed_sym: & [ FieldElement < Field > ] = opening
711770 . precomputed_trace_polys ( )
712771 . map ( |p| p. evaluations_sym ( ) )
@@ -718,43 +777,62 @@ pub trait IsStarkVerifier<
718777 . map ( |a| a. evaluations_sym ( ) )
719778 . unwrap_or ( & [ ] ) ;
720779
721- let evaluation_point = Self :: query_challenge_to_evaluation_point ( * iota, true , domain) ;
722- deep_poly_evaluations_sym. push ( Self :: reconstruct_deep_composition_poly_evaluation (
723- proof,
724- & evaluation_point,
725- primitive_root,
726- challenges,
727- lde_precomputed_sym,
728- lde_main_sym,
729- lde_aux_sym,
730- opening. composition_poly ( ) . evaluations_sym ( ) ,
731- ) ?) ;
780+ let evaluation_point = Self :: query_challenge_to_evaluation_point ( * iota, false , domain) ;
781+ let evaluation_point_sym =
782+ Self :: query_challenge_to_evaluation_point ( * iota, true , domain) ;
783+ let ( evaluation, evaluation_sym) =
784+ Self :: reconstruct_deep_composition_poly_evaluation_pair (
785+ proof,
786+ & evaluation_point,
787+ & evaluation_point_sym,
788+ primitive_root,
789+ challenges,
790+ & query_invariant_terms,
791+ lde_precomputed,
792+ lde_main,
793+ lde_aux,
794+ opening. composition_poly ( ) . evaluations ( ) ,
795+ lde_precomputed_sym,
796+ lde_main_sym,
797+ lde_aux_sym,
798+ opening. composition_poly ( ) . evaluations_sym ( ) ,
799+ ) ?;
800+ deep_poly_evaluations. push ( evaluation) ;
801+ deep_poly_evaluations_sym. push ( evaluation_sym) ;
732802 }
733803 Some ( ( deep_poly_evaluations, deep_poly_evaluations_sym) )
734804 }
735805
806+ /// Reconstructs the deep composition polynomial evaluation at a query's
807+ /// point and its symmetric counterpart together. Rewriting the per-element
808+ /// trace term `coeff*(base-ood)*denom` as `denom*(coeff*base - coeff*ood)`
809+ /// isolates `coeff*ood` (identical for both points, hoisted into
810+ /// `query_invariant_terms`) from `coeff*base` (per-point), so both points
811+ /// share the OOD walk and a single batch-inverse for their denominators.
736812 #[ allow( clippy:: too_many_arguments) ]
737- fn reconstruct_deep_composition_poly_evaluation < ' b > (
813+ fn reconstruct_deep_composition_poly_evaluation_pair < ' b > (
738814 proof : StarkProofView < ' _ , Field , FieldExtension , PI > ,
739815 evaluation_point : & FieldElement < Field > ,
816+ evaluation_point_sym : & FieldElement < Field > ,
740817 primitive_root : & FieldElement < Field > ,
741818 challenges : & Challenges < FieldExtension > ,
819+ query_invariant_terms : & QueryInvariantDeepTerms < FieldExtension > ,
742820 lde_trace_precomputed_evaluations : & ' b [ FieldElement < Field > ] ,
743821 lde_trace_main_evaluations : & ' b [ FieldElement < Field > ] ,
744822 lde_trace_aux_evaluations : & [ FieldElement < FieldExtension > ] ,
745823 lde_composition_poly_parts_evaluation : & [ FieldElement < FieldExtension > ] ,
746- ) -> Option < FieldElement < FieldExtension > > {
747- let trace_ood_evaluations = proof . trace_ood_evaluations ( ) ;
748- let ood_evaluations_table_height = trace_ood_evaluations . height ( ) ;
749- let ood_evaluations_table_width = trace_ood_evaluations . width ( ) ;
750- // Hot loop below: resolve the OOD data to one flat slice once instead
751- // of re-deriving a row slice per element.
752- let ood_data = trace_ood_evaluations. row_major_data ( ) ;
824+ lde_trace_precomputed_evaluations_sym : & ' b [ FieldElement < Field > ] ,
825+ lde_trace_main_evaluations_sym : & ' b [ FieldElement < Field > ] ,
826+ lde_trace_aux_evaluations_sym : & [ FieldElement < FieldExtension > ] ,
827+ lde_composition_poly_parts_evaluation_sym : & [ FieldElement < FieldExtension > ] ,
828+ ) -> Option < ( FieldElement < FieldExtension > , FieldElement < FieldExtension > ) > {
829+ let ood_evaluations_table_height = query_invariant_terms . ood_row_sum . len ( ) ;
830+ let ood_evaluations_table_width = proof . trace_ood_evaluations ( ) . width ( ) ;
753831 let trace_term_coeffs = & challenges. trace_term_coeffs ;
754832
755833 // Base columns are supplied as two slices (precomputed ‖ main) that the
756- // prover concatenated in this order; `num_base` is their combined width
757- // and `base_at` indexes into them as if concatenated, without allocating.
834+ // prover concatenated in this order; `num_base`/`base_at` index into
835+ // them as if concatenated, without allocating.
758836 let num_precomputed = lde_trace_precomputed_evaluations. len ( ) ;
759837 let num_base = num_precomputed + lde_trace_main_evaluations. len ( ) ;
760838 let base_at = move |col : usize | -> & ' b FieldElement < Field > {
@@ -764,68 +842,96 @@ pub trait IsStarkVerifier<
764842 & lde_trace_main_evaluations[ col - num_precomputed]
765843 }
766844 } ;
845+ let num_precomputed_sym = lde_trace_precomputed_evaluations_sym. len ( ) ;
846+ let num_base_sym = num_precomputed_sym + lde_trace_main_evaluations_sym. len ( ) ;
847+ let base_at_sym = move |col : usize | -> & ' b FieldElement < Field > {
848+ if col < num_precomputed_sym {
849+ & lde_trace_precomputed_evaluations_sym[ col]
850+ } else {
851+ & lde_trace_main_evaluations_sym[ col - num_precomputed_sym]
852+ }
853+ } ;
767854
768- // Runtime guard : a malformed proof may supply opening evaluations whose
769- // column count does not match the OOD table width, or whose composition
770- // poly parts count does not match the proof's `composition_poly_parts_ood_evaluation`.
771- // Without these checks the indexing below would panic in release builds.
772- if num_base + lde_trace_aux_evaluations . len ( ) != ood_evaluations_table_width {
855+ // Runtime guards : a malformed proof may supply opening evaluations
856+ // whose column count does not match the OOD table width, or whose
857+ // regular/symmetric base-column split disagree. Without these checks
858+ // the indexing below would panic in release builds.
859+ if num_base != num_base_sym {
773860 return None ;
774861 }
775- if trace_term_coeffs. is_empty ( )
776- || trace_term_coeffs. len ( ) * trace_term_coeffs[ 0 ] . len ( )
777- != ood_evaluations_table_height * ood_evaluations_table_width
862+ if num_base + lde_trace_aux_evaluations. len ( ) != ood_evaluations_table_width
863+ || num_base + lde_trace_aux_evaluations_sym. len ( ) != ood_evaluations_table_width
778864 {
779865 return None ;
780866 }
781867
782- let mut denoms_trace = Vec :: with_capacity ( ood_evaluations_table_height) ;
868+ // Build both denominator sets (regular, then symmetric) and invert
869+ // them together in a single batch.
870+ let mut denoms = Vec :: with_capacity ( 2 * ood_evaluations_table_height) ;
871+ let mut current_z = challenges. z . clone ( ) ;
872+ for _ in 0 ..ood_evaluations_table_height {
873+ denoms. push ( evaluation_point - & current_z) ;
874+ current_z = primitive_root * & current_z;
875+ }
783876 let mut current_z = challenges. z . clone ( ) ;
784877 for _ in 0 ..ood_evaluations_table_height {
785- denoms_trace . push ( evaluation_point - & current_z) ;
878+ denoms . push ( evaluation_point_sym - & current_z) ;
786879 current_z = primitive_root * & current_z;
787880 }
788881 // A malformed proof can land an OOD evaluation point on the LDE coset, reject.
789- FieldElement :: inplace_batch_inverse ( & mut denoms_trace) . ok ( ) ?;
790-
791- let trace_term = ( 0 ..ood_evaluations_table_width)
792- . zip ( & challenges. trace_term_coeffs )
793- . fold ( FieldElement :: zero ( ) , |trace_terms, ( col_idx, coeff_row) | {
794- let trace_i = ( 0 ..ood_evaluations_table_height) . zip ( coeff_row) . fold (
795- FieldElement :: zero ( ) ,
796- |trace_t, ( row_idx, coeff) | {
797- let ood_val = & ood_data[ row_idx * ood_evaluations_table_width + col_idx] ;
798- // Stay in base when we can: F: IsSubFieldOf<E> gives F - E -> E.
799- let diff: FieldElement < FieldExtension > = if col_idx < num_base {
800- base_at ( col_idx) - ood_val
801- } else {
802- & lde_trace_aux_evaluations[ col_idx - num_base] - ood_val
803- } ;
804- let poly_evaluation = diff * & denoms_trace[ row_idx] ;
805- trace_t + & poly_evaluation * coeff
806- } ,
807- ) ;
808- trace_terms + trace_i
809- } ) ;
882+ FieldElement :: inplace_batch_inverse ( & mut denoms) . ok ( ) ?;
883+ let ( denoms_trace, denoms_trace_sym) = denoms. split_at ( ood_evaluations_table_height) ;
884+
885+ let mut trace_term = FieldElement :: < FieldExtension > :: zero ( ) ;
886+ let mut trace_term_sym = FieldElement :: < FieldExtension > :: zero ( ) ;
887+ for row_idx in 0 ..ood_evaluations_table_height {
888+ let ood_row_sum = & query_invariant_terms. ood_row_sum [ row_idx] ;
889+ let mut base_row_sum = FieldElement :: < FieldExtension > :: zero ( ) ;
890+ let mut base_row_sum_sym = FieldElement :: < FieldExtension > :: zero ( ) ;
891+ for ( col_idx, coeff_col) in trace_term_coeffs. iter ( ) . enumerate ( ) {
892+ let coeff = & coeff_col[ row_idx] ;
893+ if col_idx < num_base {
894+ // F: IsSubFieldOf<E> gives the cheap asymmetric F * E -> E product.
895+ base_row_sum += base_at ( col_idx) * coeff;
896+ base_row_sum_sym += base_at_sym ( col_idx) * coeff;
897+ } else {
898+ let aux_idx = col_idx - num_base;
899+ base_row_sum += coeff * & lde_trace_aux_evaluations[ aux_idx] ;
900+ base_row_sum_sym += coeff * & lde_trace_aux_evaluations_sym[ aux_idx] ;
901+ }
902+ }
903+ trace_term += & denoms_trace[ row_idx] * & ( & base_row_sum - ood_row_sum) ;
904+ trace_term_sym += & denoms_trace_sym[ row_idx] * & ( & base_row_sum_sym - ood_row_sum) ;
905+ }
810906
811- let composition_parts_ood = proof. composition_poly_parts_ood_evaluation ( ) ;
812- let number_of_parts = lde_composition_poly_parts_evaluation. len ( ) ;
813- let z_pow = & challenges. z . pow ( number_of_parts) ;
814-
815- // A malformed proof can make evaluation_point == z^N, reject.
816- let denom_composition = ( evaluation_point - z_pow) . inv ( ) . ok ( ) ?;
817- let mut h_terms = FieldElement :: zero ( ) ;
818- for ( j, h_i_upsilon) in lde_composition_poly_parts_evaluation. iter ( ) . enumerate ( ) {
819- // Bounds-check via `.get(j)?`: a malformed opening may have more
820- // parts than the proof header advertises.
821- let h_i_zpower = composition_parts_ood. get ( j) ?;
822- let gamma = challenges. gammas . get ( j) ?;
823- let h_i_term = ( h_i_upsilon - h_i_zpower) * gamma;
824- h_terms += h_i_term;
907+ let number_of_parts = query_invariant_terms. number_of_parts ;
908+ // Also rejects a per-query opening length that disagrees with the
909+ // proof-level `number_of_parts`, not just a regular/symmetric mismatch.
910+ if lde_composition_poly_parts_evaluation. len ( ) != number_of_parts
911+ || lde_composition_poly_parts_evaluation_sym. len ( ) != number_of_parts
912+ {
913+ return None ;
914+ }
915+ let z_pow = & query_invariant_terms. z_pow ;
916+
917+ // A malformed proof can make evaluation_point == z_pow, reject.
918+ let mut denom_composition_pair = [ evaluation_point - z_pow, evaluation_point_sym - z_pow] ;
919+ FieldElement :: inplace_batch_inverse ( & mut denom_composition_pair) . ok ( ) ?;
920+ let [ denom_composition, denom_composition_sym] = denom_composition_pair;
921+
922+ let mut h_sum = FieldElement :: < FieldExtension > :: zero ( ) ;
923+ let mut h_sum_sym = FieldElement :: < FieldExtension > :: zero ( ) ;
924+ for j in 0 ..number_of_parts {
925+ let h_i_upsilon = & lde_composition_poly_parts_evaluation[ j] ;
926+ let h_i_upsilon_sym = & lde_composition_poly_parts_evaluation_sym[ j] ;
927+ let gamma = & challenges. gammas [ j] ;
928+ h_sum += h_i_upsilon * gamma;
929+ h_sum_sym += h_i_upsilon_sym * gamma;
825930 }
826- h_terms *= denom_composition;
931+ let h_terms = ( & h_sum - & query_invariant_terms. h_sum_zpow ) * denom_composition;
932+ let h_terms_sym = ( & h_sum_sym - & query_invariant_terms. h_sum_zpow ) * denom_composition_sym;
827933
828- Some ( trace_term + h_terms)
934+ Some ( ( trace_term + h_terms, trace_term_sym + h_terms_sym ) )
829935 }
830936
831937 /// Verifies one or more STARK proofs with their corresponding AIRs.
0 commit comments