Skip to main content

openvm_stark_backend/verifier/
stacked_reduction.rs

1use 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/// `has_preprocessed` must be per present trace in sorted AIR order.
28#[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    /*
45     * SETUP
46     *
47     * We start by setting up for the rounds below. Most importantly, we need to ensure that the
48     * order we process column_openings is the same as the stacked reduction prover. The prover
49     * orders the claims per commit -> per column (as in layouts), but column_openings is per AIR
50     * -> per part (common main, preprocessed, then cached) -> per column. Note that the verifier
51     * needs to compute and pass in has_preprocessed, which is expected to be sorted in the same
52     * way column_openings is (i.e. sorted by trace height).
53     */
54
55    // omega_order = 2^l_skip
56    let omega_order = omega_shift_pows.len();
57    let omega_order_f = SC::F::from_usize(omega_order);
58
59    // layouts and need_rot_per_commit both have length equal to the number of commitments
60    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            // This is true by construction of need_rot_for_commit:
68            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    // t_claims_len = w_{\Scr T, stack} from the paper
80    let t_claims_len = lambda_idx;
81    let mut t_claims = Vec::with_capacity(t_claims_len);
82
83    // Proof shape asserts that column_openings.len() == num_traces and each parts.len() ==
84    // vk.num_parts() common main columns (commit 0)
85    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    // preprocessed and cached columns (commits 1..)
91    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    /*
107     * INITIAL UNIVARIATE ROUND
108     *
109     * In this round we compute s_0 = sum_i (t_i * lambda^i) from the column opening claims t_i
110     * and compare it to the s_1 polynomial in proof. If the polynomial was correctly computed,
111     * then we should have s_0 == sum_{z in D} s_1(z).
112     *
113     * Note that we abuse the properties of multiplicative subgroup D to speed up the computation
114     * of sum_{z in D} s_1(z). Suppose s_1(x) = a_0 + a_1 * x + ... a_k * x^k. Because we have
115     * omega^{|D|} == 1, sum_{z in D} s_1(z) = |D| * (a_0 + a_{|D|} + ...).
116     */
117    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    // Proof shape asserts that univariate_round_coeffs.len() == 2 * (2^l_skip - 1) + 1
121    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    /*
145     * SUMCHECK ROUNDS 1 TO N
146     *
147     * We sample size n_stack vector u using the transcript, and run the verifier sumcheck for
148     * rounds 1 to n_stack. We start by evaluating the univariate round polynomial at u_0, which
149     * we store as s_0(u_0). We then evaluate s_j(0) = s_{j - 1}(u_{j - 1}) - s_j(1) for each j,
150     * which we then use with s_j(1) and s_j(2) to interpolate s_j(u_j).
151     */
152
153    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    /*
165     * FINAL VERIFICATION
166     *
167     * Finally, to verify that the claims about t_i(r) were properly reduced we assert that the
168     * final s_{n_stack}(u_{n_stack}) == sum_j (lambda^j * q_{j'}(u) * h(u, r, b_j)), where each
169     * j maps to some (non-unique) j' and h(u, r, b_j) is either (a) eq(u_{n_j}, r_{n_j}) *
170     * eq(u_{> n_j}, b_j) or (b) rot(u_{n_j}, r_{n_j}) * eq(u_{> n_j}, b_j).
171     *
172     * It is up to the verifier to compute each h(u, r, b_j). Let q_coeffs[j'] be the sum of all
173     * lambda^j * h(u, r, b_j) such that j maps to j' - given claims q_{j'}(u), we thus want to
174     * assert s_{n_stack}(u_{n_stack}) == sum_{j'} q_{j'}(u) * q_coeffs[j'].
175     */
176
177    // proof shape asserts that stacking_openings.len() == layouts.len() = number of commitments
178    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}