openvm_stark_backend/verifier/
stacked_reduction.rs1use std::iter::zip;
2
3use itertools::Itertools;
4use p3_field::{PrimeCharacteristicRing, TwoAdicField};
5use thiserror::Error;
6use tracing::{debug, instrument};
7
8use crate::{
9 poly_common::{
10 eval_eq_mle, eval_eq_prism, eval_in_uni, eval_rot_kernel_prism, horner_eval,
11 interpolate_quadratic_at_012,
12 },
13 proof::{column_openings_by_rot, StackingProof},
14 prover::stacked_pcs::StackedLayout,
15 FiatShamirTranscript, StarkProtocolConfig,
16};
17
18#[derive(Error, Debug, PartialEq, Eq)]
19pub enum StackedReductionError<EF: core::fmt::Debug + core::fmt::Display + PartialEq + Eq> {
20 #[error("s_0 does not match s_0 polynomial evaluation sum: {s_0} != {s_0_sum_eval}")]
21 S0Mismatch { s_0: EF, s_0_sum_eval: EF },
22
23 #[error("s_n(u_n) does not match claimed q(u) sum: {claim} != {final_sum}")]
24 FinalSumMismatch { claim: EF, final_sum: EF },
25}
26
27#[allow(clippy::too_many_arguments)]
29#[instrument(level = "debug", skip_all)]
30pub fn verify_stacked_reduction<SC: StarkProtocolConfig, TS: FiatShamirTranscript<SC>>(
31 transcript: &mut TS,
32 proof: &StackingProof<SC>,
33 layouts: &[StackedLayout],
34 need_rot_per_commit: &[Vec<bool>],
35 l_skip: usize,
36 n_stack: usize,
37 column_openings: &Vec<Vec<Vec<SC::EF>>>,
38 r: &[SC::EF],
39 omega_shift_pows: &[SC::F],
40) -> Result<Vec<SC::EF>, StackedReductionError<SC::EF>>
41where
42 SC::EF: TwoAdicField,
43{
44 let omega_order = omega_shift_pows.len();
57 let omega_order_f = SC::F::from_usize(omega_order);
58
59 debug_assert_eq!(layouts.len(), need_rot_per_commit.len());
61 let mut lambda_idx = 0usize;
62 let lambda_indices_per_layout: Vec<Vec<(usize, bool)>> = layouts
63 .iter()
64 .enumerate()
65 .map(|(commit_idx, layout)| {
66 let need_rot_for_commit = &need_rot_per_commit[commit_idx];
67 debug_assert_eq!(need_rot_for_commit.len(), layout.mat_starts.len());
69 layout
70 .sorted_cols
71 .iter()
72 .map(|&(mat_idx, _col_idx, _slice)| {
73 lambda_idx += 1;
74 (lambda_idx - 1, need_rot_for_commit[mat_idx])
75 })
76 .collect_vec()
77 })
78 .collect_vec();
79 let t_claims_len = lambda_idx;
81 let mut t_claims = Vec::with_capacity(t_claims_len);
82
83 for (trace_idx, parts) in column_openings.iter().enumerate() {
86 let need_rot = need_rot_per_commit[0][trace_idx];
87 t_claims.extend(column_openings_by_rot(&parts[0], need_rot));
88 }
89
90 let mut commit_idx = 1usize;
92 for parts in column_openings {
93 for cols in parts.iter().skip(1) {
94 let need_rot = need_rot_per_commit[commit_idx][0];
95 t_claims.extend(column_openings_by_rot(cols, need_rot));
96 commit_idx += 1;
97 }
98 }
99
100 assert_eq!(t_claims.len(), t_claims_len);
101 debug!(?t_claims);
102
103 let lambda = transcript.sample_ext();
104 let lambda_sqr_powers = (lambda * lambda).powers().take(t_claims_len).collect_vec();
105
106 let s_0 = zip(&t_claims, &lambda_sqr_powers)
118 .map(|(&t_i, &lambda_i)| (t_i.0 + t_i.1 * lambda) * lambda_i)
119 .sum::<SC::EF>();
120 let s_0_sum_eval = proof
122 .univariate_round_coeffs
123 .iter()
124 .step_by(omega_order)
125 .copied()
126 .sum::<SC::EF>()
127 * omega_order_f;
128
129 if s_0 != s_0_sum_eval {
130 return Err(StackedReductionError::S0Mismatch { s_0, s_0_sum_eval });
131 }
132
133 for coeffs in &proof.univariate_round_coeffs {
134 transcript.observe_ext(*coeffs);
135 }
136
137 let mut u = vec![SC::EF::ZERO; n_stack + 1];
138 u[0] = transcript.sample_ext();
139 debug!(round = 0, u_round = %u[0]);
140
141 let mut s_j_0 = s_0;
142 let mut claim = horner_eval(&proof.univariate_round_coeffs, u[0]);
143
144 u.iter_mut().enumerate().skip(1).for_each(|(j, u_j)| {
154 let s_j_1 = proof.sumcheck_round_polys[j - 1][0];
155 let s_j_2 = proof.sumcheck_round_polys[j - 1][1];
156 transcript.observe_ext(s_j_1);
157 transcript.observe_ext(s_j_2);
158 *u_j = transcript.sample_ext();
159 s_j_0 = claim - s_j_1;
160 claim = interpolate_quadratic_at_012(&[s_j_0, s_j_1, s_j_2], *u_j);
161 debug!(round = %j, sum_claim = %claim);
162 });
163
164 let mut q_coeffs = proof
179 .stacking_openings
180 .iter()
181 .map(|vec| vec![SC::EF::ZERO; vec.len()])
182 .collect_vec();
183
184 layouts
185 .iter()
186 .enumerate()
187 .zip(q_coeffs.iter_mut())
188 .for_each(|((commit_idx, layout), coeffs)| {
189 let lambda_indices = &lambda_indices_per_layout[commit_idx];
190 layout
191 .sorted_cols
192 .iter()
193 .enumerate()
194 .for_each(|(col_idx, &(_, _, s))| {
195 let (lambda_idx, need_rot) = lambda_indices[col_idx];
196 let n = s.log_height() as isize - l_skip as isize;
197 let n_lift = n.max(0) as usize;
198 let b = (l_skip + n_lift..l_skip + n_stack)
199 .map(|j| SC::F::from_bool((s.row_idx >> j) & 1 == 1))
200 .collect_vec();
201 let eq_mle = eval_eq_mle(&u[n_lift + 1..], &b);
202 let ind = eval_in_uni(l_skip, n, u[0]);
203 let (l, rs_n) = if n.is_negative() {
204 (
205 l_skip.wrapping_add_signed(n),
206 &[r[0].exp_power_of_2(-n as usize)] as &[_],
207 )
208 } else {
209 (l_skip, &r[..=n_lift])
210 };
211 let eq_prism = eval_eq_prism(l, &u[..=n_lift], rs_n);
212 let mut batched = lambda_sqr_powers[lambda_idx] * eq_prism;
213 if need_rot {
214 let rot_kernel_prism = eval_rot_kernel_prism(l, &u[..=n_lift], rs_n);
215 batched += lambda_sqr_powers[lambda_idx] * lambda * rot_kernel_prism;
216 }
217 coeffs[s.col_idx] += eq_mle * batched * ind;
218 });
219 });
220
221 let final_sum = q_coeffs.iter().zip(proof.stacking_openings.iter()).fold(
222 SC::EF::ZERO,
223 |acc, (q_coeff_vec, q_j_vec)| {
224 acc + q_coeff_vec.iter().zip(q_j_vec.iter()).fold(
225 SC::EF::ZERO,
226 |acc, (&q_coeff, &q_j)| {
227 transcript.observe_ext(q_j);
228 acc + (q_coeff * q_j)
229 },
230 )
231 },
232 );
233
234 if claim != final_sum {
235 return Err(StackedReductionError::FinalSumMismatch { claim, final_sum });
236 }
237
238 Ok(u)
239}