Skip to content
98 changes: 79 additions & 19 deletions crypto/stark/src/constraints/zerofier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,10 @@ use crate::domain::Domain;
/// the constraint must skip. This returns its roots `rᵢ` so callers can
/// evaluate the product `∏(x - rᵢ)` directly at the points they need.
pub fn end_exemptions_roots<F: IsField>(
meta: &ConstraintMeta,
end_exemptions: usize,
trace_primitive_root: &FieldElement<F>,
trace_length: usize,
) -> Vec<FieldElement<F>> {
let end_exemptions = meta.end_exemptions;
if end_exemptions == 0 {
return Vec::new();
}
Expand All @@ -49,7 +48,7 @@ pub fn end_exemptions_lde_evaluations<F: IsFFTField>(
domain: &Domain<F>,
) -> Vec<FieldElement<F>> {
let roots = end_exemptions_roots(
meta,
meta.end_exemptions,
&domain.trace_primitive_root,
domain.trace_roots_of_unity.len(),
);
Expand Down Expand Up @@ -115,10 +114,16 @@ pub fn zerofier_evaluations_on_extended_domain<F: IsFFTField>(
.collect()
}

/// Evaluation of the constraint's zerofier at some point `z`, which may be in
/// a field extension.
pub fn evaluate_zerofier<F, E>(
meta: &ConstraintMeta,
/// The end-exemptions correction `∏(z − rᵢ)` at `z`, where `rᵢ` are the roots for a
/// constraint skipping its last `end_exemptions` rows (`1` when there are none).
///
/// This is the only per-constraint-varying factor of the transition zerofier at
/// `z`: the full inverse zerofier is `1/(zᴺ − 1)` × this. Exposed separately so a
/// caller evaluating many constraints at the same `z` computes `1/(zᴺ − 1)` once
/// and this once per distinct `end_exemptions`, rather than a fresh `zᴺ` power and
/// extension inversion per constraint (see the verifier's OOD zerofier sum).
pub fn end_exemptions_correction<F, E>(
end_exemptions: usize,
z: &FieldElement<E>,
trace_primitive_root: &FieldElement<F>,
trace_length: usize,
Expand All @@ -127,16 +132,71 @@ where
F: IsSubFieldOf<E>,
E: IsField,
{
let roots = end_exemptions_roots(meta, trace_primitive_root, trace_length);
// Factor `z - rᵢ` written as `-(rᵢ - z)`: the field ops only go
// subfield − superfield, and `rᵢ ∈ F`, `z ∈ E`.
let end_exemptions_eval = roots.iter().fold(FieldElement::<E>::one(), |acc, root| {
acc * -(root.clone() - z.clone())
});

// 1/(z^N − 1), times the end-exemptions correction.
(-FieldElement::<F>::one() + z.pow(trace_length))
.inv()
.unwrap()
* &end_exemptions_eval
// Written `-(rᵢ - z)` so the field ops only go subfield − superfield
// (`rᵢ ∈ F`, `z ∈ E`). Empty roots fold to `1`, the no-exemptions case.
end_exemptions_roots(end_exemptions, trace_primitive_root, trace_length)
.iter()
.fold(FieldElement::<E>::one(), |acc, root| {
acc * -(root.clone() - z.clone())
})
}

#[cfg(test)]
mod tests {
use super::*;
use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField;
use math::field::goldilocks::GoldilocksField;

type F = GoldilocksField;
type E = Degree3GoldilocksExtensionField;

/// The exempt-row product `∏(z − gⁱ)` over the last `end_exemptions` rows
/// (indices `N−e .. N−1`), derived directly from `gⁱ` — the value
/// [`end_exemptions_correction`] must equal. Independent of its backward
/// `g⁻¹` walk, so a mismatch catches an error in that root derivation.
fn exempt_product(
end_exemptions: usize,
z: &FieldElement<E>,
g: &FieldElement<F>,
n: usize,
) -> FieldElement<E> {
let mut acc = FieldElement::<E>::one();
for i in (n - end_exemptions)..n {
// `-(gⁱ − z) = z − gⁱ`, keeping the ops subfield − superfield to
// match the production body.
acc *= -(g.pow(i) - *z);
}
acc
}

#[test]
fn correction_matches_direct_exempt_product_over_cubic_extension() {
let n = 16usize;
let g = F::get_primitive_root_of_unity(n.trailing_zeros() as u64).unwrap();
// A point off the trace domain, genuinely inside the cubic extension.
let z = FieldElement::<E>::new([
FieldElement::<F>::from(7u64),
FieldElement::<F>::from(3u64),
FieldElement::<F>::from(1u64),
]);

// 0 → one() (no exemptions); 1..=3 exercise the multi-group fold the
// verifier drives, one distinct `end_exemptions` per group.
for end_exemptions in 0..=3usize {
let got = end_exemptions_correction::<F, E>(end_exemptions, &z, &g, n);
let want = exempt_product(end_exemptions, &z, &g, n);
assert_eq!(got, want, "mismatch for end_exemptions = {end_exemptions}");
}
}

#[test]
fn correction_with_no_exemptions_is_one() {
let n = 8usize;
let g = F::get_primitive_root_of_unity(n.trailing_zeros() as u64).unwrap();
let z = FieldElement::<E>::from(5u64);
assert_eq!(
end_exemptions_correction::<F, E>(0, &z, &g, n),
FieldElement::<E>::one()
);
}
}
18 changes: 12 additions & 6 deletions crypto/stark/src/prover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4184,15 +4184,21 @@ pub trait IsStarkProver<
);
let transition_evals = air.compute_transition(&ctx);

// `1/(zᴺ − 1)` is shared by every transition constraint; only the
// end-exemptions correction varies per constraint.
let inv_zerofier_denominator = (-FieldElement::<Field>::one() + z.pow(trace_length))
.inv()
.unwrap();
let mut denominators =
vec![FieldElement::<FieldExtension>::zero(); air.num_transition_constraints()];
air.constraints_meta().iter().for_each(|m| {
denominators[m.constraint_idx] = crate::constraints::zerofier::evaluate_zerofier(
m,
z,
&domain.trace_primitive_root,
trace_length,
);
denominators[m.constraint_idx] =
crate::constraints::zerofier::end_exemptions_correction(
m.end_exemptions,
z,
&domain.trace_primitive_root,
trace_length,
) * &inv_zerofier_denominator;
});
let transition_sum = transition_evals
.into_iter()
Expand Down
Loading
Loading