labrador/
prover.rs

1//! Labrador Prover
2//!
3//! This module implements the **prover** side of the protocol sketched in
4//! Fig. 2 of the paper (page 17). All algebra is carried out over the ring
5//! \(R_q = \mathbb{Z}_q[x]/(x^d+1)\), where `q` is 32-bit modulo and `d` is polynomial degree.  
6//!
7//! **Important — security parameters**:  The protocol is parameterised by
8//! *rank* `n`, *multiplicity* `r`, *operator norm* `β`, decomposition depths `t_1,t_2`, and
9//! security parameter `λ` = [`env_params::SECURITY_PARAMETER`].
10
11use crate::commitments::{
12    ajtai_commitment, common_instances::AjtaiInstances, outer_commitments,
13    outer_commitments::DecompositionParameters, CommitError,
14};
15use crate::core::{
16    aggregate::FunctionsAggregation, aggregate::ZeroConstantFunctionsAggregation,
17    garbage_polynomials, inner_product, jl::Projection,
18};
19use crate::relation::{
20    env_params::{self, EnvironmentParameters},
21    statement::Statement,
22    witness::Witness,
23};
24use crate::ring::{rq_matrix::RqMatrix, rq_vector::RqVector, zq::Zq};
25use crate::transcript::{LabradorTranscript, Sponge};
26use thiserror::Error;
27
28#[derive(Debug, Error)]
29pub enum ProverError {
30    /// Indicates that the L2 norm (squared) of the witness exceeded the allowed threshold.
31    #[error("invalid witness size: norm_squared {norm_squared}, allowed {allowed}")]
32    WitnessL2NormViolated { norm_squared: Zq, allowed: Zq },
33    #[error("Invalid Projection of index {index}. Expected {expected}, got {computed}")]
34    ProjectionError {
35        index: usize,
36        expected: Zq,
37        computed: Zq,
38    },
39    #[error("commitment failure")]
40    CommitError(#[from] ajtai_commitment::CommitError),
41    #[error("decomposition failure")]
42    DecompositionError(#[from] outer_commitments::DecompositionError),
43}
44
45/// Implements the steps executed by the prover \(\mathcal{P}\) in the paper.
46pub struct LabradorProver<'a> {
47    /// System‑wide environment parameters like rank and multiplicity.
48    params: &'a EnvironmentParameters,
49    /// Common reference string (CRS) holding the commitment matrices
50    /// \(\mathbf{A},\mathbf{B},\mathbf{C},\mathbf{D}\).
51    crs: &'a AjtaiInstances,
52    /// Secret witness vectors \(\{\mathbf{s}_1,\dots,\mathbf{s}_r\}\).
53    witness: &'a Witness,
54    /// Public relation statement.
55    st: &'a Statement,
56    /* --- aggregation instances ---------------------------------------------------------- */
57    constant_aggregator: ZeroConstantFunctionsAggregation<'a>,
58    funcs_aggregator: FunctionsAggregation<'a>,
59}
60
61impl<'a> LabradorProver<'a> {
62    /// Constructs a new prover instance.
63    pub fn new(
64        params: &'a EnvironmentParameters,
65        crs: &'a AjtaiInstances,
66        witness: &'a Witness,
67        st: &'a Statement,
68    ) -> Self {
69        Self {
70            params,
71            crs,
72            witness,
73            st,
74            constant_aggregator: ZeroConstantFunctionsAggregation::new(params),
75            funcs_aggregator: FunctionsAggregation::new(params),
76        }
77    }
78
79    // -----------------------------------------------------------------------------
80    // Step 1.a — Ajtai commitments t_i
81    // -----------------------------------------------------------------------------
82    /// Computes the **Ajtai commitments**
83    /// \[\mathbf{t}_i = \mathbf{A}\,\mathbf{s}_i\\].
84    ///
85    /// # Errors
86    /// Returns [`CommitError`] if the commitment scheme fails.
87    fn compute_vector_ti(&self) -> Result<RqMatrix, CommitError> {
88        let commitments = self
89            .witness
90            .s
91            .iter()
92            .cloned()
93            .map(|s_i| self.crs.commitment_scheme_a.commit(&s_i))
94            .collect::<Result<Vec<_>, CommitError>>()?;
95
96        Ok(RqMatrix::new(commitments, false))
97    }
98
99    // -----------------------------------------------------------------------------
100    // Step 1.b — First outer commitment u_1
101    // -----------------------------------------------------------------------------
102
103    /// Computes the *first outer commitment* \(\mathbf{u}_1\).
104    ///
105    /// Internally this function
106    /// 1. calls [`compute_vector_ti`] to obtain all \(\mathbf{t}_i\),
107    /// 2. evaluates the garbage polynomials \(\mathbf{g}_{ij}=\langle\mathbf{s}_i,\mathbf{s}_j\rangle\),
108    /// 4. finally aggregates and commit using [`compute_u1`].
109    ///
110    /// The resulting value is
111    /// \[
112    ///     \mathbf{u}_1
113    ///      = \sum_{i=1}^r \sum_{k=0}^{t_1-1} \mathbf{B}_{ik}\,\mathbf{t}_i^{(k)}
114    ///      + \sum_{i\le j} \sum_{k=0}^{t_2-1}
115    ///        \mathbf{C}_{ijk}\,\mathbf{g}_{ij}^{(k)}.
116    /// \]
117    ///
118    /// The commitment is recorded inside the transcript so the verifier can later
119    /// retrieve it.
120    fn compute_u1<S: Sponge>(
121        &mut self,
122        transcript: &mut LabradorTranscript<S>,
123    ) -> Result<(RqMatrix, RqMatrix), ProverError> {
124        let t_i = self.compute_vector_ti()?;
125        let garbage_polynomial_g = garbage_polynomials::compute_g(&self.witness.s);
126        let commitment_u1 = outer_commitments::compute_u1(
127            self.crs,
128            &t_i,
129            DecompositionParameters::new(self.params.b, self.params.t_1)?,
130            &garbage_polynomial_g,
131            DecompositionParameters::new(self.params.b, self.params.t_2)?,
132        );
133        transcript.set_u1(commitment_u1);
134        Ok((t_i, garbage_polynomial_g))
135    }
136
137    // -----------------------------------------------------------------------------
138    // Step 2 — JL projections
139    // -----------------------------------------------------------------------------
140
141    /// Computes the Johnson–Lindenstrauss projections `p`.
142    ///
143    /// \[p_j = \sum_{i=1}^r \langle {\pi}_i^{(j)},\mathbf{s}_i\rangle\]
144    ///
145    /// |p| =  `2 * SECURITY_PARAMETER`
146    ///
147    /// p is recorded inside the transcript so the verifier can later
148    /// retrieve it. Projection instance is returned for use in next steps of the prover.
149    fn compute_p<S: Sponge>(&self, transcript: &mut LabradorTranscript<S>) -> Projection {
150        let projections = transcript.generate_projections(
151            env_params::SECURITY_PARAMETER,
152            self.params.rank,
153            self.params.multiplicity,
154        );
155        let vector_p = projections.compute_batch_projection(&self.witness.s);
156        transcript.set_vector_p(vector_p);
157        projections
158    }
159
160    // -----------------------------------------------------------------------------
161    // Step 3 — Aggregation of zero‑constant functions
162    // -----------------------------------------------------------------------------
163
164    /// Computes the aggregated triples \((a''^{(k)},\varphi''_i,b''^{(k)})\).
165    ///
166    /// We sample vectors *ψ* and *ω* from the sponge and call the helper struct
167    /// [`ZeroConstantFunctionsAggregation`] for the computation.
168    fn compute_b_double_prime<S: Sponge>(
169        &mut self,
170        transcript: &mut LabradorTranscript<S>,
171        projections: &Projection,
172    ) {
173        // --- sample randomisers ------------------------------------------------------
174        let vector_psi =
175            transcript.generate_vector_psi(self.params.const_agg_length, self.params.constraint_l);
176        let vector_omega = transcript
177            .generate_vector_omega(self.params.const_agg_length, env_params::SECURITY_PARAMETER);
178        // --- perform aggregations -------------------------------------------
179        self.constant_aggregator
180            .calculate_agg_a_double_prime(&vector_psi, &self.st.a_ct);
181        self.constant_aggregator.calculate_agg_phi_double_prime(
182            &self.st.phi_ct,
183            &projections.get_conjugated_projection_matrices(),
184            &vector_psi,
185            &vector_omega,
186        );
187        let b_ct_aggr = self
188            .constant_aggregator
189            .calculate_agg_b_double_prime(&self.witness.s);
190        transcript.set_vector_b_ct_aggr(b_ct_aggr);
191    }
192
193    // -----------------------------------------------------------------------------
194    // Step 4 — Second outer commitment u_2
195    // -----------------------------------------------------------------------------
196
197    /// Computes the *second outer commitment* \(\mathbf{u}_2\).
198    ///
199    /// First we create an *aggregated* vector of functions (via
200    /// [`FunctionsAggregation`]). We then evaluate the garbage polynomials
201    /// \(h_{ij} = \langle\tilde{\varphi}_i,\tilde{\mathbf{s}}_j\rangle
202    ///                    + \langle\varphi_i^{\prime(j)},\tilde{\mathbf{s}}_i\rangle)
203    /// and finally commit using the CRS matrices \(\mathbf{D}_{ijk}\):
204    ///
205    /// \[
206    ///     \mathbf{u}_2 = \sum_{i\le j}\sum_{k=0}^{t_1-1}
207    ///                     \mathbf{D}_{ijk}\,h_{ij}^{(k)}.
208    /// \]
209    /// * Note: As dividing by two is not supported in our scheme (Zq is not necessarily a filed), h_ij in the implementation does not have 1/2 factor in the paper. In subsequent verifications, this is considered.
210    fn compute_u2<S: Sponge>(
211        &mut self,
212        transcript: &mut LabradorTranscript<S>,
213    ) -> Result<RqMatrix, ProverError> {
214        // --- sample randomisers ------------------------------------------------------
215        let alpha_vector = transcript.generate_rq_vector(self.params.constraint_k);
216        let beta_vector = transcript.generate_rq_vector(self.params.const_agg_length);
217        // --- perform aggregations -------------------------------------------
218        self.funcs_aggregator.calculate_aggr_phi(
219            &self.st.phi_constraint,
220            self.constant_aggregator.get_phi_double_prime(),
221            &alpha_vector,
222            &beta_vector,
223        );
224
225        let garbage_polynomial_h =
226            garbage_polynomials::compute_h(&self.witness.s, self.funcs_aggregator.get_appr_phi());
227        let commitment_u2 = outer_commitments::compute_u2(
228            self.crs,
229            &garbage_polynomial_h,
230            DecompositionParameters::new(self.params.b, self.params.t_1)?,
231        );
232        transcript.set_u2(commitment_u2);
233        Ok(garbage_polynomial_h)
234    }
235
236    // -----------------------------------------------------------------------------
237    // Step 5 — Linear combination z
238    // -----------------------------------------------------------------------------
239
240    /// Computes the random linear combination
241    /// \[\mathbf{z}=\sum_{i=1}^r c_i\,\mathbf{s}_i\] with coefficients
242    /// `c_i ← C` (uniformly random challenges).
243    fn compute_z<S: Sponge>(&mut self, transcript: &mut LabradorTranscript<S>) -> RqVector {
244        let challenges =
245            transcript.generate_challenges(env_params::OPERATOR_NORM, self.params.multiplicity);
246        let z = inner_product::compute_linear_combination(&self.witness.s, challenges.elements());
247        z
248    }
249
250    /// Executes **all** prover steps (Fig. 2, page 17).
251    ///
252    /// On success the returned [`LabradorTranscript`] contains every message that
253    /// needs to be sent to the verifier.
254    pub fn prove<S: Sponge>(&mut self) -> Result<LabradorTranscript<S>, ProverError> {
255        // Initialise transcript and sponge -----------------------------------------------
256        let mut transcript = LabradorTranscript::new(S::default());
257
258        // --- Step 1: Outer Commitment u1 ------------------------------------------------
259        let (t_i, garbage_polynomial_g) = self.compute_u1(&mut transcript)?;
260
261        // --- Step 2: JL Projections -----------------------------------------------------
262        let projections = self.compute_p(&mut transcript);
263
264        // --- Step 3: Aggregations +  ----------------------------------------------------
265        self.compute_b_double_prime(&mut transcript, &projections);
266
267        // --- Step 4: Outer Commitment u2 ------------------------------------------------
268        let garbage_polynomial_h = self.compute_u2(&mut transcript)?;
269
270        // --- Step 5: Amortization -------------------------------------------------------
271        let z = self.compute_z(&mut transcript);
272
273        // --- Finalise Transcript: Add t_i, g_ij, h_ij to transcript----------------------
274        transcript.set_recursive_part(z, t_i, garbage_polynomial_g, garbage_polynomial_h);
275
276        Ok(transcript)
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use crate::transcript::sponges::shake::ShakeSponge;
284
285    #[test]
286    fn test_prove() {
287        // set up example environment parameters, use default set for testing.
288        let ep_1 = EnvironmentParameters::default();
289        // generate a random witness based on environment parameters above
290        let witness_1 = Witness::new(ep_1.rank, ep_1.multiplicity, ep_1.beta_sq);
291        // generate public statement based on witness_1
292        let st: Statement = Statement::new(&witness_1, &ep_1);
293        // generate the common reference string matrices A, B, C, D
294        let crs: AjtaiInstances = AjtaiInstances::new(&ep_1);
295
296        // create a new prover
297        let mut prover = LabradorProver::new(&ep_1, &crs, &witness_1, &st);
298        let _: LabradorTranscript<ShakeSponge> = prover.prove().unwrap();
299    }
300}