Skip to main content

sigma_proofs/
schnorr_protocol.rs

1//! Implementation of the generic Schnorr Sigma Protocol over a [`group::Group`].
2//!
3//! This module defines the [`SchnorrProof`] structure, which implements
4//! a Sigma protocol proving different types of discrete logarithm relations (eg. Schnorr, Pedersen's commitments)
5//! through a group morphism abstraction (see [Maurer09](https://crypto-test.ethz.ch/publications/files/Maurer09.pdf)).
6
7use crate::errors::{Error, Result};
8use crate::linear_relation::CanonicalLinearRelation;
9use crate::traits::{ScalarRng, SigmaProtocol, SigmaProtocolSimulator, Transcript};
10use crate::{LinearRelation, MultiScalarMul, Nizk};
11use alloc::vec::Vec;
12
13use group::prime::PrimeGroup;
14use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize};
15
16fn protocol_identifier_for_group<G>() -> [u8; 64] {
17    let _ = core::marker::PhantomData::<G>;
18
19    #[cfg(feature = "p256")]
20    if core::any::type_name::<G>() == core::any::type_name::<p256::ProjectivePoint>() {
21        return pad_identifier(b"sigma-proofs_Shake128_P256");
22    }
23
24    #[cfg(feature = "bls12_381")]
25    if core::any::type_name::<G>() == core::any::type_name::<bls12_381::G1Projective>() {
26        return pad_identifier(b"sigma-proofs_Shake128_BLS12381");
27    }
28
29    pad_identifier(b"ietf sigma proof linear relation")
30}
31
32fn pad_identifier(identifier: &[u8]) -> [u8; 64] {
33    assert!(
34        identifier.len() <= 64,
35        "identifier must fit within 64 bytes"
36    );
37
38    let mut padded = [0u8; 64];
39    padded[..identifier.len()].copy_from_slice(identifier);
40    padded
41}
42
43impl<G> SigmaProtocol for CanonicalLinearRelation<G>
44where
45    G: PrimeGroup + Encoding<[u8]> + NargSerialize + NargDeserialize + MultiScalarMul,
46    G::Scalar: Encoding<[u8]> + NargSerialize + NargDeserialize + Decoding<[u8]>,
47{
48    type Commitment = G;
49    type ProverState = (Vec<G::Scalar>, Vec<G::Scalar>);
50    type Response = G::Scalar;
51    type Witness = Vec<G::Scalar>;
52    type Challenge = G::Scalar;
53
54    /// Prover's first message: generates a commitment using random nonces.
55    ///
56    /// # Parameters
57    /// - `witness`: A vector of scalars that satisfy the linear map relation.
58    /// - `rng`: A cryptographically secure random number generator.
59    ///
60    /// # Returns
61    /// - A tuple containing:
62    ///     - The commitment (a vector of group elements).
63    ///     - The prover state (random nonces and witness) used to compute the response.
64    ///
65    /// # Errors
66    ///
67    /// -[`Error::InvalidInstanceWitnessPair`] if the witness vector length is less than the number of scalar variables.
68    /// If the witness vector is larger, extra variables are ignored.
69    fn prover_commit(
70        &self,
71        witness: &Self::Witness,
72        rng: &mut impl ScalarRng,
73    ) -> Result<(Vec<Self::Commitment>, Self::ProverState)> {
74        if witness.len() < self.num_scalars {
75            return Err(Error::InvalidInstanceWitnessPair);
76        }
77
78        let nonces = rng.random_scalars_vec::<G>(self.num_scalars);
79        let commitment = self.evaluate(&nonces);
80        let prover_state = (nonces.to_vec(), witness.to_vec());
81        Ok((commitment, prover_state))
82    }
83
84    /// Computes the prover's response (second message) using the challenge.
85    ///
86    /// # Parameters
87    /// - `state`: The prover state returned by `prover_commit`, typically containing randomness and witness components.
88    /// - `challenge`: The verifier's challenge scalar.
89    ///
90    /// # Returns
91    /// - A vector of scalars forming the prover's response.
92    ///
93    /// # Errors
94    /// - Returns [`Error::InvalidInstanceWitnessPair`] if the prover state vectors have incorrect lengths.
95    fn prover_response(
96        &self,
97        prover_state: Self::ProverState,
98        challenge: &Self::Challenge,
99    ) -> Result<Vec<Self::Response>> {
100        let (nonces, witness) = prover_state;
101
102        let responses = nonces
103            .into_iter()
104            .zip(witness)
105            .map(|(r, w)| r + w * challenge)
106            .collect();
107        Ok(responses)
108    }
109    /// Verifies the correctness of the proof.
110    ///
111    /// # Parameters
112    /// - `commitment`: The prover's commitment vector (group elements).
113    /// - `challenge`: The challenge scalar.
114    /// - `response`: The prover's response vector.
115    ///
116    /// # Returns
117    /// - `Ok(())` if the proof is valid.
118    /// - `Err(Error::VerificationFailure)` if the proof is invalid.
119    /// - `Err(Error::InvalidInstanceWitnessPair)` if the lengths of commitment or response do not match the expected counts.
120    ///
121    /// # Errors
122    /// -[`Error::VerificationFailure`] if the computed relation
123    /// does not hold for the provided challenge and response, indicating proof invalidity.
124    /// -[`Error::InvalidInstanceWitnessPair`] if the commitment or response length is incorrect.
125    fn verifier(
126        &self,
127        commitment: &[Self::Commitment],
128        challenge: &Self::Challenge,
129        response: &[Self::Response],
130    ) -> Result<()> {
131        if commitment.len() != self.image.len() || response.len() != self.num_scalars {
132            return Err(Error::InvalidInstanceWitnessPair);
133        }
134
135        let lhs = self.evaluate(response);
136        let mut rhs = Vec::new();
137        for (img, g) in self.image_elements().zip(commitment) {
138            rhs.push(img * challenge + g);
139        }
140        if lhs == rhs {
141            Ok(())
142        } else {
143            Err(Error::VerificationFailure)
144        }
145    }
146    fn commitment_len(&self) -> usize {
147        self.image.len()
148    }
149
150    fn response_len(&self) -> usize {
151        self.num_scalars
152    }
153
154    fn instance_label(&self) -> impl AsRef<[u8]> {
155        self.label()
156    }
157
158    fn protocol_identifier(&self) -> [u8; 64] {
159        protocol_identifier_for_group::<G>()
160    }
161}
162
163impl<G> CanonicalLinearRelation<G>
164where
165    G: PrimeGroup + Encoding<[u8]> + NargSerialize + NargDeserialize + MultiScalarMul,
166    G::Scalar: Encoding<[u8]> + NargSerialize + NargDeserialize + Decoding<[u8]>,
167{
168    /// Convert this LinearRelation into a non-interactive zero-knowledge protocol
169    /// using the ShakeCodec and a specified context/domain separator.
170    ///
171    /// # Parameters
172    /// - `context`: Domain separator bytes for the Fiat-Shamir transform
173    ///
174    /// # Returns
175    /// A `Nizk` instance ready for proving and verification
176    ///
177    /// # Example
178    /// ```
179    /// # #[cfg(feature = "curve25519-dalek")] {
180    /// # use sigma_proofs::{LinearRelation, Nizk};
181    /// # use curve25519_dalek::RistrettoPoint as G;
182    /// # use curve25519_dalek::scalar::Scalar;
183    /// # use rand::rngs::OsRng;
184    /// # use group::Group;
185    ///
186    /// let mut relation = LinearRelation::<G>::new();
187    /// let x_var = relation.allocate_scalar();
188    /// let g_var = relation.allocate_element();
189    /// let p_var = relation.allocate_eq(x_var * g_var);
190    ///
191    /// relation.set_element(g_var, G::generator());
192    /// let x = Scalar::random(&mut OsRng);
193    /// relation.compute_image(&[x]).unwrap();
194    ///
195    /// // Convert to NIZK with custom context
196    /// let nizk = relation.into_nizk(b"my-protocol-v1").unwrap();
197    /// let proof = nizk.prove_batchable(&vec![x], &mut OsRng).unwrap();
198    /// assert!(nizk.verify_batchable(&proof).is_ok());
199    /// # }
200    /// ```
201    pub fn into_nizk(self, session_identifier: &[u8]) -> Result<Nizk<CanonicalLinearRelation<G>>> {
202        Ok(Nizk::new(session_identifier, self))
203    }
204}
205
206impl<G> LinearRelation<G>
207where
208    G: PrimeGroup + Encoding<[u8]> + NargSerialize + NargDeserialize + MultiScalarMul,
209    G::Scalar: Encoding<[u8]> + NargSerialize + NargDeserialize + Decoding<[u8]>,
210{
211    /// Convert this LinearRelation into a non-interactive zero-knowledge protocol
212    /// using the Fiat-Shamir transform.
213    ///
214    /// This is a convenience method that combines `.canonical()` and `.into_nizk()`.
215    ///
216    /// # Parameters
217    /// - `session_identifier`: Domain separator bytes for the Fiat-Shamir transform
218    ///
219    /// # Returns
220    /// A `Nizk` instance ready for proving and verification
221    ///
222    /// # Example
223    /// ```
224    /// # #[cfg(feature = "curve25519-dalek")] {
225    /// # use sigma_proofs::{LinearRelation, Nizk};
226    /// # use curve25519_dalek::RistrettoPoint as G;
227    /// # use curve25519_dalek::scalar::Scalar;
228    /// # use rand::rngs::OsRng;
229    /// # use group::Group;
230    ///
231    /// let mut relation = LinearRelation::<G>::new();
232    /// let x_var = relation.allocate_scalar();
233    /// let g_var = relation.allocate_element();
234    /// let p_var = relation.allocate_eq(x_var * g_var);
235    ///
236    /// relation.set_element(g_var, G::generator());
237    /// let x = Scalar::random(&mut OsRng);
238    /// relation.compute_image(&[x]).unwrap();
239    ///
240    /// // Convert to NIZK directly
241    /// let nizk = relation.into_nizk(b"my-protocol-v1").unwrap();
242    /// let proof = nizk.prove_batchable(&vec![x], &mut OsRng).unwrap();
243    /// assert!(nizk.verify_batchable(&proof).is_ok());
244    /// # }
245    /// ```
246    pub fn into_nizk(
247        self,
248        session_identifier: &[u8],
249    ) -> crate::errors::Result<crate::Nizk<CanonicalLinearRelation<G>>>
250    where
251        G: PrimeGroup + Encoding<[u8]> + NargSerialize + NargDeserialize,
252        G::Scalar: Encoding<[u8]> + NargSerialize + NargDeserialize + Decoding<[u8]>,
253    {
254        self.canonical()
255            .map_err(|_| crate::errors::Error::InvalidInstanceWitnessPair)?
256            .into_nizk(session_identifier)
257    }
258}
259impl<G> SigmaProtocolSimulator for CanonicalLinearRelation<G>
260where
261    G: PrimeGroup + Encoding<[u8]> + NargSerialize + NargDeserialize + MultiScalarMul,
262    G::Scalar: Encoding<[u8]> + NargSerialize + NargDeserialize + Decoding<[u8]>,
263{
264    /// Simulates a valid transcript for a given challenge without a witness.
265    ///
266    /// # Parameters
267    /// - `challenge`: A scalar value representing the challenge.
268    /// - `rng`: A cryptographically secure RNG.
269    ///
270    /// # Returns
271    /// - A commitment and response forming a valid proof for the given challenge.
272    fn simulate_response(&self, rng: &mut impl ScalarRng) -> Vec<Self::Response> {
273        rng.random_scalars_vec::<G>(self.num_scalars)
274    }
275
276    /// Simulates a full proof transcript using a randomly generated challenge.
277    ///
278    /// # Parameters
279    /// - `rng`: A cryptographically secure RNG.
280    ///
281    /// # Returns
282    /// - A tuple `(commitment, challenge, response)` forming a valid proof.
283    fn simulate_transcript(&self, rng: &mut impl ScalarRng) -> Result<Transcript<Self>> {
284        let [challenge] = rng.random_scalars::<G, _>();
285        let response = self.simulate_response(rng);
286        let commitment = self.simulate_commitment(&challenge, &response)?;
287        Ok((commitment, challenge, response))
288    }
289
290    /// Recomputes the commitment from the challenge and response (used in compact proofs).
291    ///
292    /// # Parameters
293    /// - `challenge`: The challenge scalar issued by the verifier or derived via Fiat–Shamir.
294    /// - `response`: The prover's response vector.
295    ///
296    /// # Returns
297    /// - A vector of group elements representing the simulated commitment (one per linear constraint).
298    ///
299    /// # Errors
300    /// - [`Error::InvalidInstanceWitnessPair`] if the response length does not match the expected number of scalars.
301    fn simulate_commitment(
302        &self,
303        challenge: &Self::Challenge,
304        response: &[Self::Response],
305    ) -> Result<Vec<Self::Commitment>> {
306        if response.len() != self.num_scalars {
307            return Err(Error::InvalidInstanceWitnessPair);
308        }
309
310        let response_image = self.evaluate(response);
311        let commitment = response_image
312            .iter()
313            .zip(self.image_elements())
314            .map(|(res, img)| *res - img * challenge)
315            .collect::<Vec<_>>();
316        Ok(commitment)
317    }
318}