diff --git a/crypto/stark/src/domain.rs b/crypto/stark/src/domain.rs index 912f9608b..4ee720aae 100644 --- a/crypto/stark/src/domain.rs +++ b/crypto/stark/src/domain.rs @@ -6,6 +6,44 @@ use math::{ }, }; +/// Precomputed constants for barycentric interpolation on the trace-size coset. +/// +/// Derived from a [`Domain`]: the N evaluation points at stride `blowup_factor` +/// within the LDE coset, plus the field scalars that appear in every barycentric +/// evaluation. Computed once in round 3 and shared across composition-poly and +/// trace OOD evaluations. +pub struct DomainConstants { + /// The N trace-size coset points: `lde_coset[i * blowup_factor]` for `i in 0..N`. + pub points: Vec>, + /// `coset_offset ^ N`. + pub offset_pow_n: FieldElement, + /// `1 / N` in the base field. + pub size_inv: FieldElement, + /// `(coset_offset ^ N) ^ -1`. + pub offset_pow_n_inv: FieldElement, +} + +impl DomainConstants { + pub fn from_domain(domain: &Domain) -> Self { + let n = domain.interpolation_domain_size; + let bf = domain.blowup_factor; + let points = (0..n) + .map(|i| domain.lde_roots_of_unity_coset[i * bf].clone()) + .collect(); + let offset_pow_n = domain.coset_offset.pow(n); + let size_inv = FieldElement::::from(n as u64) + .inv() + .expect("domain size is non-zero; field characteristic must not divide n"); + let offset_pow_n_inv = offset_pow_n.inv().expect("coset_offset_pow_n is non-zero"); + Self { + points, + offset_pow_n, + size_inv, + offset_pow_n_inv, + } + } +} + use super::traits::AIR; /// Full domain with pre-computed roots of unity. Used by the prover which needs diff --git a/crypto/stark/src/prover.rs b/crypto/stark/src/prover.rs index 380fdcb11..a5386017a 100644 --- a/crypto/stark/src/prover.rs +++ b/crypto/stark/src/prover.rs @@ -33,7 +33,7 @@ use crate::trace::LDETraceTable; use super::config::{BatchedMerkleTree, BatchedMerkleTreeBackend, Commitment}; use super::constraints::evaluator::ConstraintEvaluator; -use super::domain::Domain; +use super::domain::{Domain, DomainConstants}; use super::fri::fri_decommit::FriDecommitment; use super::grinding; use super::lookup::BusPublicInputs; @@ -989,25 +989,12 @@ pub trait IsStarkProver< let domain_size = domain.interpolation_domain_size; let blowup_factor = domain.blowup_factor; - // === Composition poly parts: barycentric evaluation at z^num_parts === - // Extract trace-size coset points from the LDE coset (stride = blowup_factor) - // Keep coset points in base field — mixed F×E arithmetic is cheaper than E×E. - let coset_points: Vec> = (0..domain_size) - .map(|i| domain.lde_roots_of_unity_coset[i * blowup_factor].clone()) - .collect(); - // Keep coset_offset_pow_n and g_n_inv in base field F — the barycentric - // functions use F×E→E mixed arithmetic, avoiding field conversions. - let coset_offset_pow_n: FieldElement = domain.coset_offset.pow(domain_size); - let domain_size_inv: FieldElement = FieldElement::::from(domain_size as u64) - .inv() - .expect("domain_size is a power of two, hence non-zero in the field"); - let g_n_inv: FieldElement = coset_offset_pow_n - .inv() - .expect("coset_offset_pow_n is non-zero"); + // === Shared domain constants for barycentric evaluation === + let dc = DomainConstants::from_domain(domain); - // Precompute inv_denoms for z^num_parts (shared across all composition poly parts) + // === Composition poly parts: barycentric evaluation at z^num_parts === let comp_z_pow_n = z_power.pow(domain_size); - let comp_inv_denoms = math::polynomial::barycentric_inv_denoms(&z_power, &coset_points); + let comp_inv_denoms = math::polynomial::barycentric_inv_denoms(&z_power, &dc.points); let composition_poly_parts_ood_evaluation: Vec<_> = round_2_result .lde_composition_poly_evaluations @@ -1019,10 +1006,10 @@ pub trait IsStarkProver< .collect(); math::polynomial::interpolate_coset_eval_ext_with_g_n_inv( &comp_z_pow_n, - &coset_offset_pow_n, - &domain_size_inv, - &g_n_inv, - &coset_points, + &dc.offset_pow_n, + &dc.size_inv, + &dc.offset_pow_n_inv, + &dc.points, &evals, &comp_inv_denoms, ) @@ -1030,20 +1017,13 @@ pub trait IsStarkProver< .collect(); // === Trace polynomials: barycentric evaluation via LDE === - // Uses get_trace_evaluations_from_lde which performs barycentric interpolation - // on the LDE trace data, avoiding the need for coefficient-form trace_polys. - // Reuses coset_points, coset_offset_pow_n, domain_size_inv, g_n_inv already - // computed above for composition poly evaluation — avoids redundant work. let trace_ood_evaluations = crate::trace::get_trace_evaluations_from_lde( &round_1_result.lde_trace, domain, z, &air.context().transition_offsets, air.step_size(), - &coset_points, - &coset_offset_pow_n, - &domain_size_inv, - &g_n_inv, + &dc, ); Round3 { diff --git a/crypto/stark/src/tests/prover_tests.rs b/crypto/stark/src/tests/prover_tests.rs index c4d0a22fd..b1e403b36 100644 --- a/crypto/stark/src/tests/prover_tests.rs +++ b/crypto/stark/src/tests/prover_tests.rs @@ -1,7 +1,7 @@ use crypto::fiat_shamir::default_transcript::DefaultTranscript; use crate::{ - domain::Domain, + domain::{Domain, DomainConstants}, examples::{ quadratic_air::QuadraticAIR, simple_fibonacci::{self, FibonacciAIR, FibonacciPublicInputs}, @@ -169,28 +169,11 @@ fn barycentric_trace_eval_matches_horner_trace_eval() { step_size, ); - // Precompute shared barycentric scalars - let n = domain.interpolation_domain_size; - let bf = domain.blowup_factor; - let coset_points: Vec = (0..n) - .map(|i| domain.lde_roots_of_unity_coset[i * bf]) - .collect(); - let coset_offset_pow_n: Felt = domain.coset_offset.pow(n); - let n_inv: Felt = Felt::from(n as u64).inv().expect("n is a power of two"); - let g_n_inv: Felt = coset_offset_pow_n.inv().expect("non-zero"); + let dc = DomainConstants::from_domain(&domain); // Barycentric evaluation (new path) - let result = get_trace_evaluations_from_lde( - &lde_trace, - &domain, - &z, - &frame_offsets, - step_size, - &coset_points, - &coset_offset_pow_n, - &n_inv, - &g_n_inv, - ); + let result = + get_trace_evaluations_from_lde(&lde_trace, &domain, &z, &frame_offsets, step_size, &dc); assert_eq!(result.width, expected.width); assert_eq!(result.height, expected.height); diff --git a/crypto/stark/src/trace.rs b/crypto/stark/src/trace.rs index b826877be..ef6ee7833 100644 --- a/crypto/stark/src/trace.rs +++ b/crypto/stark/src/trace.rs @@ -1,4 +1,4 @@ -use crate::domain::Domain; +use crate::domain::{Domain, DomainConstants}; use crate::table::Table; use itertools::Itertools; use math::fft::errors::FFTError; @@ -362,20 +362,16 @@ where /// Taking every blowup_factor-th point gives N evaluations on the trace-size coset /// {g * w_trace^i}, which is sufficient to interpolate a degree < N polynomial. /// -/// Accepts precomputed `coset_points`, `coset_offset_pow_n`, `n_inv`, and `g_n_inv` -/// to avoid redundant computation when these are already available from the caller -/// (e.g., round_3 in prover.rs computes identical values for composition poly eval). -#[allow(clippy::too_many_arguments)] +/// Accepts a [`DomainConstants`] to avoid redundant computation when the caller +/// has already derived these values (e.g., round_3 shares them with composition +/// poly evaluation). pub fn get_trace_evaluations_from_lde( lde_trace: &LDETraceTable, domain: &Domain, z: &FieldElement, frame_offsets: &[usize], step_size: usize, - coset_points: &[FieldElement], - coset_offset_pow_n: &FieldElement, - n_inv: &FieldElement, - g_n_inv: &FieldElement, + dc: &DomainConstants, ) -> Table where F: IsSubFieldOf + IsFFTField, @@ -387,21 +383,18 @@ where let num_aux_cols = lde_trace.num_aux_cols(); let table_width = num_main_cols + num_aux_cols; - // Caller-supplied barycentric scalars must match the domain they describe. - // A mismatch would silently produce wrong evaluations (if too short) or panic - // with an opaque index-out-of-bounds in the LDE stride access below. debug_assert_eq!( - coset_points.len(), + dc.points.len(), n, - "coset_points length must equal domain.interpolation_domain_size" + "DomainConstants.points length must equal domain.interpolation_domain_size" ); // Build evaluation points: for each frame offset and step within, z * w_trace^exponent let evaluation_points = compute_frame_evaluation_points(z, frame_offsets, &domain.trace_primitive_root, step_size); - // Precompute n_inv * g_n_inv once — shared across all eval points and columns. - let n_inv_g_n_inv: FieldElement = n_inv * g_n_inv; + // Precompute size_inv * offset_pow_n_inv once — shared across all eval points and columns. + let n_inv_g_n_inv: FieldElement = &dc.size_inv * &dc.offset_pow_n_inv; let mut table_data = Vec::with_capacity(evaluation_points.len() * table_width); @@ -409,16 +402,17 @@ where // z_pow_n for this evaluation point let z_pow_n = eval_point.pow(n); - // vanishing_factor = (z^N - g^N) * n_inv * g_n_inv — shared across all columns - let vanishing = z_pow_n.sub_subfield(coset_offset_pow_n); + // vanishing_factor = (z^N - offset^N) * size_inv * offset_pow_n_inv + let vanishing = z_pow_n.sub_subfield(&dc.offset_pow_n); let vanishing_factor = &n_inv_g_n_inv * &vanishing; // Precompute inv_denoms = 1/(eval_point - coset_point_i) — shared across all columns - let inv_denoms = barycentric_inv_denoms(eval_point, coset_points); + let inv_denoms = barycentric_inv_denoms(eval_point, &dc.points); - // Precompute col_scale[i] = coset_point[i] * inv_denom[i] — shared across ALL columns. + // Precompute col_scale[i] = point[i] * inv_denom[i] — shared across ALL columns. // This eliminates N redundant F×E multiplies per column. - let col_scale: Vec> = coset_points + let col_scale: Vec> = dc + .points .iter() .zip(inv_denoms.iter()) .map(|(point, inv_d)| point * inv_d)