Skip to main content

dory_pcs/
evaluation_proof.rs

1//! Evaluation proof generation and verification using Eval-VMV-RE protocol
2//!
3//! Implements the full proof generation and verification by:
4//! 1. Computing VMV message (C, D2, E1)
5//! 2. Running max(nu, sigma) rounds of inner product protocol (reduce and fold)
6//! 3. Producing final scalar product message
7//!
8//! ## Matrix Layout
9//!
10//! Supports flexible matrix layouts with constraint nu ≤ sigma:
11//! - **Square matrices** (nu = sigma): Traditional layout, e.g., 16×16 for nu=4, sigma=4
12//! - **Non-square matrices** (nu < sigma): Wider layouts, e.g., 8×16 for nu=3, sigma=4
13//!
14//! The protocol automatically pads shorter dimensions and uses max(nu, sigma) rounds
15//! in the reduce-and-fold phase.
16//!
17//! ## Homomorphic Properties
18//!
19//! The evaluation proof protocol preserves the homomorphic properties of Dory commitments.
20//! This enables proving evaluations of linear combinations:
21//!
22//! ```text
23//! Com(r₁·P₁ + r₂·P₂) = r₁·Com(P₁) + r₂·Com(P₂)
24//! ```
25//!
26//! See `examples/homomorphic.rs` for a complete demonstration.
27
28use crate::error::DoryError;
29use crate::messages::VMVMessage;
30use crate::mode::Mode;
31use crate::primitives::arithmetic::{DoryRoutines, Field, Group, PairingCurve};
32use crate::primitives::poly::MultilinearLagrange;
33use crate::primitives::transcript::Transcript;
34use crate::proof::DoryProof;
35use crate::reduce_and_fold::{DoryProverState, DoryVerifierState};
36use crate::setup::{ProverSetup, VerifierSetup};
37
38/// Create evaluation proof for a polynomial at a point
39///
40/// Implements Eval-VMV-RE protocol from Dory Section 5.
41/// The protocol proves that polynomial(point) = evaluation via the VMV relation:
42/// evaluation = L^T × M × R
43///
44/// # Algorithm
45/// 1. Compute or use provided row commitments (Tier 1 commitment)
46/// 2. Split evaluation point into left and right vectors
47/// 3. Compute v_vec (column evaluations)
48/// 4. Create VMV message (C, D2, E1)
49/// 5. Initialize prover state for inner product / reduce-and-fold protocol
50/// 6. Run max(nu, sigma) rounds of reduce-and-fold (with automatic padding for non-square):
51///    - First reduce: compute message and apply beta challenge (reduce)
52///    - Second reduce: compute message and apply alpha challenge (fold)
53/// 7. Compute final scalar product message
54///
55/// # Parameters
56/// - `polynomial`: Polynomial to prove evaluation for
57/// - `point`: Evaluation point (length nu + sigma)
58/// - `row_commitments`: Optional precomputed row commitments from polynomial.commit()
59/// - `commit_blind`: GT-level blinding scalar from `commit()`. Ignored when
60///   `row_commitments` is `None` (the blind is computed internally in that case).
61/// - `nu`: Log₂ of number of rows (constraint: nu ≤ sigma)
62/// - `sigma`: Log₂ of number of columns
63/// - `setup`: Prover setup
64/// - `transcript`: Fiat-Shamir transcript for challenge generation
65///
66/// # Returns
67/// Complete Dory proof containing VMV message, reduce messages, and final message
68///
69/// # Errors
70/// Returns error if dimensions are invalid (nu > sigma) or protocol fails
71///
72/// # Matrix Layout
73/// Supports both square (nu = sigma) and non-square (nu < sigma) matrices.
74/// For non-square matrices, vectors are automatically padded to length 2^sigma.
75#[allow(clippy::type_complexity)]
76#[allow(clippy::too_many_arguments)]
77#[tracing::instrument(skip_all, name = "create_evaluation_proof")]
78pub fn create_evaluation_proof<F, E, M1, M2, T, P, Mo>(
79    polynomial: &P,
80    point: &[F],
81    row_commitments: Option<Vec<E::G1>>,
82    commit_blind: F,
83    nu: usize,
84    sigma: usize,
85    setup: &ProverSetup<E>,
86    transcript: &mut T,
87) -> Result<(DoryProof<E::G1, E::G2, E::GT>, Option<F>), DoryError>
88where
89    F: Field,
90    E: PairingCurve,
91    E::G1: Group<Scalar = F>,
92    E::G2: Group<Scalar = F>,
93    E::GT: Group<Scalar = F>,
94    M1: DoryRoutines<E::G1>,
95    M2: DoryRoutines<E::G2>,
96    T: Transcript<Curve = E>,
97    P: MultilinearLagrange<F>,
98    Mo: Mode,
99{
100    if point.len() != nu + sigma {
101        return Err(DoryError::InvalidPointDimension {
102            expected: nu + sigma,
103            actual: point.len(),
104        });
105    }
106
107    // Validate matrix dimensions: nu must be ≤ sigma (rows ≤ columns)
108    if nu > sigma {
109        return Err(DoryError::InvalidSize {
110            expected: sigma,
111            actual: nu,
112        });
113    }
114
115    let (row_commitments, commit_blind) = match row_commitments {
116        Some(rc) => (rc, commit_blind),
117        None => {
118            let (_, rc, blind) = polynomial.commit::<E, Mo, M1>(nu, sigma, setup)?;
119            (rc, blind)
120        }
121    };
122
123    let (left_vec, right_vec) = polynomial.compute_evaluation_vectors(point, nu, sigma);
124    let v_vec = polynomial.vector_matrix_product(&left_vec, nu, sigma);
125
126    let mut padded_row_commitments = row_commitments.clone();
127    if nu < sigma {
128        padded_row_commitments.resize(1 << sigma, E::G1::identity());
129    }
130
131    // Sample VMV blinds (zero in Transparent, random in ZK)
132    let (r_c, r_d2, r_e1, r_e2): (F, F, F, F) =
133        (Mo::sample(), Mo::sample(), Mo::sample(), Mo::sample());
134
135    let g2_fin = &setup.g2_vec[0];
136
137    // C = e(⟨row_commitments, v_vec⟩, Γ2,fin) + r_c·HT
138    let t_vec_v = M1::msm(&padded_row_commitments, &v_vec);
139    let c = Mo::mask(E::pair(&t_vec_v, g2_fin), &setup.ht, &r_c);
140
141    // D₂ = e(⟨Γ₁[sigma], v_vec⟩, Γ2,fin) + r_d2·HT
142    let d2 = Mo::mask(
143        E::pair(&M1::msm(&setup.g1_vec[..1 << sigma], &v_vec), g2_fin),
144        &setup.ht,
145        &r_d2,
146    );
147
148    // E₁ = ⟨row_commitments, left_vec⟩ + r_e1·H₁
149    let e1 = Mo::mask(M1::msm(&row_commitments, &left_vec), &setup.h1, &r_e1);
150
151    let vmv_message = VMVMessage { c, d2, e1 };
152
153    transcript.append_serde(b"vmv_c", &vmv_message.c);
154    transcript.append_serde(b"vmv_d2", &vmv_message.d2);
155    transcript.append_serde(b"vmv_e1", &vmv_message.e1);
156
157    #[cfg(feature = "zk")]
158    let (zk_e2, zk_y_com, zk_sigma1, zk_sigma2, zk_r_y) = if Mo::BLINDING {
159        use crate::reduce_and_fold::{generate_sigma1_proof, generate_sigma2_proof};
160        let y = polynomial.evaluate(point);
161        let r_y: F = Mo::sample();
162        let e2 = Mo::mask(g2_fin.scale(&y), &setup.h2, &r_e2);
163        let y_com = setup.g1_vec[0].scale(&y) + setup.h1.scale(&r_y);
164        transcript.append_serde(b"vmv_e2", &e2);
165        transcript.append_serde(b"vmv_y_com", &y_com);
166        let s1 = generate_sigma1_proof::<E, T>(&y, &r_e2, &r_y, setup, transcript);
167        let s2 = generate_sigma2_proof::<E, T>(&r_e1, &-r_d2, setup, transcript);
168        (Some(e2), Some(y_com), Some(s1), Some(s2), Some(r_y))
169    } else {
170        (None, None, None, None, None)
171    };
172
173    // v₂ = v_vec · Γ₂,fin (each scalar scales g_fin)
174    let v2 = M2::fixed_base_vector_scalar_mul(g2_fin, &v_vec);
175
176    let mut padded_right_vec = right_vec.clone();
177    let mut padded_left_vec = left_vec.clone();
178    if nu < sigma {
179        padded_right_vec.resize(1 << sigma, F::zero());
180        padded_left_vec.resize(1 << sigma, F::zero());
181    }
182
183    let mut prover_state: DoryProverState<'_, E, Mo> = DoryProverState::new(
184        padded_row_commitments, // v1 = T_vec_prime (row commitments, padded)
185        v2,                     // v2 = v_vec · g_fin
186        Some(v_vec),            // v2_scalars for first-round MSM+pair optimization
187        padded_right_vec,       // s1 = right_vec (padded)
188        padded_left_vec,        // s2 = left_vec (padded)
189        setup,
190    );
191    prover_state.set_initial_blinds(commit_blind, r_c, r_d2, r_e1, r_e2);
192
193    let num_rounds = nu.max(sigma);
194    let mut first_messages = Vec::with_capacity(num_rounds);
195    let mut second_messages = Vec::with_capacity(num_rounds);
196
197    for _round in 0..num_rounds {
198        let first_msg = prover_state.compute_first_message::<M1, M2>();
199
200        transcript.append_serde(b"d1_left", &first_msg.d1_left);
201        transcript.append_serde(b"d1_right", &first_msg.d1_right);
202        transcript.append_serde(b"d2_left", &first_msg.d2_left);
203        transcript.append_serde(b"d2_right", &first_msg.d2_right);
204        transcript.append_serde(b"e1_beta", &first_msg.e1_beta);
205        transcript.append_serde(b"e2_beta", &first_msg.e2_beta);
206
207        let beta = transcript.challenge_scalar(b"beta");
208        prover_state.apply_first_challenge::<M1, M2>(&beta);
209        first_messages.push(first_msg);
210
211        let second_msg = prover_state.compute_second_message::<M1, M2>();
212
213        transcript.append_serde(b"c_plus", &second_msg.c_plus);
214        transcript.append_serde(b"c_minus", &second_msg.c_minus);
215        transcript.append_serde(b"e1_plus", &second_msg.e1_plus);
216        transcript.append_serde(b"e1_minus", &second_msg.e1_minus);
217        transcript.append_serde(b"e2_plus", &second_msg.e2_plus);
218        transcript.append_serde(b"e2_minus", &second_msg.e2_minus);
219
220        let alpha = transcript.challenge_scalar(b"alpha");
221        prover_state.apply_second_challenge::<M1, M2>(&alpha);
222        second_messages.push(second_msg);
223    }
224
225    let gamma = transcript.challenge_scalar(b"gamma");
226
227    #[cfg(feature = "zk")]
228    let scalar_product_proof = if Mo::BLINDING {
229        Some(prover_state.scalar_product_proof(transcript))
230    } else {
231        None
232    };
233
234    let final_message = prover_state.compute_final_message::<M1, M2>(&gamma);
235
236    transcript.append_serde(b"final_e1", &final_message.e1);
237    transcript.append_serde(b"final_e2", &final_message.e2);
238    let _d = transcript.challenge_scalar(b"d");
239
240    let proof = DoryProof {
241        vmv_message,
242        first_messages,
243        second_messages,
244        final_message,
245        nu,
246        sigma,
247        #[cfg(feature = "zk")]
248        e2: zk_e2,
249        #[cfg(feature = "zk")]
250        y_com: zk_y_com,
251        #[cfg(feature = "zk")]
252        sigma1_proof: zk_sigma1,
253        #[cfg(feature = "zk")]
254        sigma2_proof: zk_sigma2,
255        #[cfg(feature = "zk")]
256        scalar_product_proof,
257    };
258    #[cfg(feature = "zk")]
259    return Ok((proof, zk_r_y));
260    #[cfg(not(feature = "zk"))]
261    Ok((proof, None))
262}
263
264/// Verify an evaluation proof
265///
266/// Verifies that a committed polynomial evaluates to the claimed value at the given point.
267/// Works with both square and non-square matrix layouts (nu ≤ sigma).
268///
269/// # Algorithm
270/// 1. Extract VMV message from proof
271/// 2. Compute e2 = Γ2,fin * evaluation (or use proof.e2 in ZK mode)
272/// 3. Initialize verifier state with commitment and VMV message
273/// 4. Run max(nu, sigma) rounds of reduce-and-fold verification (with automatic padding)
274/// 5. Derive gamma and d challenges
275/// 6. Verify final scalar product message
276///
277/// # Parameters
278/// - `commitment`: Polynomial commitment (in GT) - can be a homomorphically combined commitment
279/// - `evaluation`: Claimed evaluation result
280/// - `point`: Evaluation point (length must equal proof.nu + proof.sigma)
281/// - `proof`: Evaluation proof to verify (contains nu and sigma dimensions)
282/// - `setup`: Verifier setup
283/// - `transcript`: Fiat-Shamir transcript for challenge generation
284///
285/// # Returns
286/// `Ok(())` if proof is valid, `Err(DoryError)` otherwise
287///
288/// # Homomorphic Verification
289/// This function can verify proofs for homomorphically combined polynomials.
290/// The commitment parameter should be the combined commitment, and the evaluation
291/// should be the evaluation of the combined polynomial.
292///
293/// # Errors
294/// Returns `DoryError::InvalidProof` if verification fails, or other variants
295/// if the input parameters are incorrect (e.g., point dimension mismatch).
296#[tracing::instrument(skip_all, name = "verify_evaluation_proof")]
297pub fn verify_evaluation_proof<F, E, M1, M2, T>(
298    commitment: E::GT,
299    evaluation: F,
300    point: &[F],
301    proof: &DoryProof<E::G1, E::G2, E::GT>,
302    setup: VerifierSetup<E>,
303    transcript: &mut T,
304) -> Result<(), DoryError>
305where
306    F: Field,
307    E: PairingCurve,
308    E::G1: Group<Scalar = F>,
309    E::G2: Group<Scalar = F>,
310    E::GT: Group<Scalar = F>,
311    M1: DoryRoutines<E::G1>,
312    M2: DoryRoutines<E::G2>,
313    T: Transcript<Curve = E>,
314{
315    let nu = proof.nu;
316    let sigma = proof.sigma;
317
318    if point.len() != nu + sigma {
319        return Err(DoryError::InvalidPointDimension {
320            expected: nu + sigma,
321            actual: point.len(),
322        });
323    }
324
325    let vmv_message = &proof.vmv_message;
326    transcript.append_serde(b"vmv_c", &vmv_message.c);
327    transcript.append_serde(b"vmv_d2", &vmv_message.d2);
328    transcript.append_serde(b"vmv_e1", &vmv_message.e1);
329
330    #[cfg(feature = "zk")]
331    let (e2, is_zk) = match (&proof.e2, &proof.y_com) {
332        (Some(pe2), Some(yc)) => {
333            use crate::reduce_and_fold::{verify_sigma1_proof, verify_sigma2_proof};
334            transcript.append_serde(b"vmv_e2", pe2);
335            transcript.append_serde(b"vmv_y_com", yc);
336            match (&proof.sigma1_proof, &proof.sigma2_proof) {
337                (Some(s1), Some(s2)) => {
338                    verify_sigma1_proof::<E, T>(pe2, yc, s1, &setup, transcript)?;
339                    verify_sigma2_proof::<E, T>(
340                        &vmv_message.e1,
341                        &vmv_message.d2,
342                        s2,
343                        &setup,
344                        transcript,
345                    )?;
346                }
347                _ => return Err(DoryError::InvalidProof),
348            }
349            (*pe2, true)
350        }
351        (None, None) => (setup.g2_0.scale(&evaluation), false),
352        _ => return Err(DoryError::InvalidProof),
353    };
354    #[cfg(not(feature = "zk"))]
355    let (e2, _is_zk) = (setup.g2_0.scale(&evaluation), false);
356
357    // Folded-scalar accumulation with per-round coordinates.
358    // num_rounds = sigma (we fold column dimensions).
359    let num_rounds = sigma;
360
361    // Bounds check: reject proofs with mismatched message counts or that exceed setup capacity.
362    let max_rounds = setup.max_log_n / 2;
363    if num_rounds > max_rounds
364        || proof.first_messages.len() != num_rounds
365        || proof.second_messages.len() != num_rounds
366    {
367        return Err(DoryError::InvalidProof);
368    }
369
370    // s1 (right/prover): the σ column coordinates in natural order (LSB→MSB).
371    // No padding here: the verifier folds across the σ column dimensions.
372    // With MSB-first folding, these coordinates are only consumed after the first σ−ν rounds,
373    // which correspond to the padded MSB dimensions on the left tensor, matching the prover.
374    let s1_coords: Vec<F> = point[..sigma].to_vec();
375    // s2 (left/prover): the ν row coordinates in natural order, followed by zeros for the extra
376    // MSB dimensions. Conceptually this is s ⊗ [1,0]^(σ−ν): under MSB-first folds, the first
377    // σ−ν rounds multiply s2 by α⁻¹ while contributing no right halves (since those entries are 0).
378    let mut s2_coords: Vec<F> = vec![F::zero(); sigma];
379    s2_coords[..nu].copy_from_slice(&point[sigma..sigma + nu]);
380
381    let mut verifier_state = DoryVerifierState::new(
382        vmv_message.c,  // c from VMV message
383        commitment,     // d1 = commitment
384        vmv_message.d2, // d2 from VMV message
385        vmv_message.e1, // e1 from VMV message
386        e2,             // e2 computed from evaluation
387        s1_coords,      // s1: columns c0..c_{σ−1} (LSB→MSB), no padding; folded across σ dims
388        s2_coords,      // s2: rows r0..r_{ν−1} then zeros in MSB dims (emulates s ⊗ [1,0]^(σ−ν))
389        num_rounds,
390        setup.clone(),
391    );
392
393    for round in 0..num_rounds {
394        let first_msg = &proof.first_messages[round];
395        let second_msg = &proof.second_messages[round];
396
397        transcript.append_serde(b"d1_left", &first_msg.d1_left);
398        transcript.append_serde(b"d1_right", &first_msg.d1_right);
399        transcript.append_serde(b"d2_left", &first_msg.d2_left);
400        transcript.append_serde(b"d2_right", &first_msg.d2_right);
401        transcript.append_serde(b"e1_beta", &first_msg.e1_beta);
402        transcript.append_serde(b"e2_beta", &first_msg.e2_beta);
403        let beta = transcript.challenge_scalar(b"beta");
404
405        transcript.append_serde(b"c_plus", &second_msg.c_plus);
406        transcript.append_serde(b"c_minus", &second_msg.c_minus);
407        transcript.append_serde(b"e1_plus", &second_msg.e1_plus);
408        transcript.append_serde(b"e1_minus", &second_msg.e1_minus);
409        transcript.append_serde(b"e2_plus", &second_msg.e2_plus);
410        transcript.append_serde(b"e2_minus", &second_msg.e2_minus);
411        let alpha = transcript.challenge_scalar(b"alpha");
412
413        verifier_state.process_round(first_msg, second_msg, &alpha, &beta)?;
414    }
415
416    let gamma = transcript.challenge_scalar(b"gamma");
417
418    // In ZK mode: absorb scalar product proof into transcript before deriving d.
419    #[cfg(feature = "zk")]
420    let zk_data = if is_zk {
421        if let Some(ref sp) = proof.scalar_product_proof {
422            for (l, v) in [
423                (b"sigma_p1" as &[u8], &sp.p1),
424                (b"sigma_p2", &sp.p2),
425                (b"sigma_q", &sp.q),
426                (b"sigma_r", &sp.r),
427            ] {
428                transcript.append_serde(l, v);
429            }
430            let c = transcript.challenge_scalar(b"sigma_c");
431            Some((sp, c))
432        } else {
433            return Err(DoryError::InvalidProof);
434        }
435    } else {
436        None
437    };
438
439    // Shared: absorb final message and derive d.
440    transcript.append_serde(b"final_e1", &proof.final_message.e1);
441    transcript.append_serde(b"final_e2", &proof.final_message.e2);
442    let d = transcript.challenge_scalar(b"d");
443
444    #[cfg(feature = "zk")]
445    let zk = zk_data.as_ref().map(|(sp, c)| (*sp, c));
446    #[cfg(not(feature = "zk"))]
447    let zk: Option<(&crate::messages::ScalarProductProof<_, _, _, _>, _)> = None;
448
449    verifier_state.verify_final(&proof.final_message, &gamma, &d, zk)
450}