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