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;
12use itertools::Itertools;
13
14use group::prime::PrimeGroup;
15use spongefish::{Decoding, Encoding, NargDeserialize, NargSerialize};
16
17fn protocol_identifier_for_group<G>() -> [u8; 64] {
18    let _ = core::marker::PhantomData::<G>;
19
20    #[cfg(feature = "p256")]
21    if core::any::type_name::<G>() == core::any::type_name::<p256::ProjectivePoint>() {
22        return pad_identifier(b"sigma-proofs_Shake128_P256");
23    }
24
25    #[cfg(feature = "bls12_381")]
26    if core::any::type_name::<G>() == core::any::type_name::<bls12_381::G1Projective>() {
27        return pad_identifier(b"sigma-proofs_Shake128_BLS12381");
28    }
29
30    pad_identifier(b"ietf sigma proof linear relation")
31}
32
33fn pad_identifier(identifier: &[u8]) -> [u8; 64] {
34    assert!(
35        identifier.len() <= 64,
36        "identifier must fit within 64 bytes"
37    );
38
39    let mut padded = [0u8; 64];
40    padded[..identifier.len()].copy_from_slice(identifier);
41    padded
42}
43
44impl<G> SigmaProtocol for CanonicalLinearRelation<G>
45where
46    G: PrimeGroup + Encoding<[u8]> + NargSerialize + NargDeserialize + MultiScalarMul,
47    G::Scalar: Encoding<[u8]> + NargSerialize + NargDeserialize + Decoding<[u8]>,
48{
49    type Commitment = G;
50    type ProverState = (Vec<G::Scalar>, Vec<G::Scalar>);
51    type Response = G::Scalar;
52    type Witness = Vec<G::Scalar>;
53    type Challenge = G::Scalar;
54
55    /// Prover's first message: generates a commitment using random nonces.
56    ///
57    /// # Parameters
58    /// - `witness`: A vector of scalars that satisfy the linear map relation.
59    /// - `rng`: A cryptographically secure random number generator.
60    ///
61    /// # Returns
62    /// - A tuple containing:
63    ///     - The commitment (a vector of group elements).
64    ///     - The prover state (random nonces and witness) used to compute the response.
65    ///
66    /// # Errors
67    ///
68    /// -[`Error::InvalidInstanceWitnessPair`] if the witness vector length is not equal to the number of scalar variables.
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        if witness.len() != self.num_scalars || nonces.len() != self.num_scalars {
102            return Err(Error::InvalidInstanceWitnessPair);
103        }
104
105        let responses = nonces
106            .into_iter()
107            .zip_eq(witness)
108            .map(|(r, w)| r + w * challenge)
109            .collect();
110        Ok(responses)
111    }
112    /// Verifies the correctness of the proof.
113    ///
114    /// # Parameters
115    /// - `commitment`: The prover's commitment vector (group elements).
116    /// - `challenge`: The challenge scalar.
117    /// - `response`: The prover's response vector.
118    ///
119    /// # Returns
120    /// - `Ok(())` if the proof is valid.
121    /// - `Err(Error::VerificationFailure)` if the proof is invalid.
122    /// - `Err(Error::InvalidInstanceWitnessPair)` if the lengths of commitment or response do not match the expected counts.
123    ///
124    /// # Errors
125    /// -[`Error::VerificationFailure`] if the computed relation
126    /// does not hold for the provided challenge and response, indicating proof invalidity.
127    /// -[`Error::InvalidInstanceWitnessPair`] if the commitment or response length is incorrect.
128    fn verifier(
129        &self,
130        commitment: &[Self::Commitment],
131        challenge: &Self::Challenge,
132        response: &[Self::Response],
133    ) -> Result<()> {
134        if commitment.len() != self.image.len() || response.len() != self.num_scalars {
135            return Err(Error::InvalidInstanceWitnessPair);
136        }
137
138        let lhs = self.evaluate(response);
139        let mut rhs = Vec::new();
140        for (img, g) in self.image_elements().zip_eq(commitment) {
141            rhs.push(img * challenge + g);
142        }
143        if lhs == rhs {
144            Ok(())
145        } else {
146            Err(Error::VerificationFailure)
147        }
148    }
149    fn commitment_len(&self) -> usize {
150        self.image.len()
151    }
152
153    fn response_len(&self) -> usize {
154        self.num_scalars
155    }
156
157    fn instance_label(&self) -> impl AsRef<[u8]> {
158        self.label()
159    }
160
161    fn protocol_identifier(&self) -> [u8; 64] {
162        protocol_identifier_for_group::<G>()
163    }
164}
165
166impl<G> CanonicalLinearRelation<G>
167where
168    G: PrimeGroup + Encoding<[u8]> + NargSerialize + NargDeserialize + MultiScalarMul,
169    G::Scalar: Encoding<[u8]> + NargSerialize + NargDeserialize + Decoding<[u8]>,
170{
171    /// Convert this LinearRelation into a non-interactive zero-knowledge protocol
172    /// using the ShakeCodec and a specified context/domain separator.
173    ///
174    /// # Parameters
175    /// - `context`: Domain separator bytes for the Fiat-Shamir transform
176    ///
177    /// # Returns
178    /// A `Nizk` instance ready for proving and verification
179    ///
180    /// # Example
181    /// ```
182    /// # #[cfg(feature = "curve25519-dalek")] {
183    /// # use sigma_proofs::{LinearRelation, Nizk};
184    /// # use curve25519_dalek::RistrettoPoint as G;
185    /// # use curve25519_dalek::scalar::Scalar;
186    /// # use rand::rngs::OsRng;
187    /// # use group::Group;
188    ///
189    /// let mut relation = LinearRelation::<G>::new();
190    /// let x_var = relation.allocate_scalar();
191    /// let g_var = relation.allocate_element();
192    /// let p_var = relation.allocate_eq(x_var * g_var);
193    ///
194    /// relation.set_element(g_var, G::generator());
195    /// let x = Scalar::random(&mut OsRng);
196    /// relation.compute_image(&[x]).unwrap();
197    ///
198    /// // Convert to NIZK with custom context
199    /// let nizk = relation.into_nizk(b"my-protocol-v1").unwrap();
200    /// let proof = nizk.prove_batchable(&vec![x], &mut OsRng).unwrap();
201    /// assert!(nizk.verify_batchable(&proof).is_ok());
202    /// # }
203    /// ```
204    pub fn into_nizk(self, session_identifier: &[u8]) -> Result<Nizk<CanonicalLinearRelation<G>>> {
205        Ok(Nizk::new(session_identifier, self))
206    }
207}
208
209impl<G> LinearRelation<G>
210where
211    G: PrimeGroup + Encoding<[u8]> + NargSerialize + NargDeserialize + MultiScalarMul,
212    G::Scalar: Encoding<[u8]> + NargSerialize + NargDeserialize + Decoding<[u8]>,
213{
214    /// Convert this LinearRelation into a non-interactive zero-knowledge protocol
215    /// using the Fiat-Shamir transform.
216    ///
217    /// This is a convenience method that combines `.canonical()` and `.into_nizk()`.
218    ///
219    /// # Parameters
220    /// - `session_identifier`: Domain separator bytes for the Fiat-Shamir transform
221    ///
222    /// # Returns
223    /// A `Nizk` instance ready for proving and verification
224    ///
225    /// # Example
226    /// ```
227    /// # #[cfg(feature = "curve25519-dalek")] {
228    /// # use sigma_proofs::{LinearRelation, Nizk};
229    /// # use curve25519_dalek::RistrettoPoint as G;
230    /// # use curve25519_dalek::scalar::Scalar;
231    /// # use rand::rngs::OsRng;
232    /// # use group::Group;
233    ///
234    /// let mut relation = LinearRelation::<G>::new();
235    /// let x_var = relation.allocate_scalar();
236    /// let g_var = relation.allocate_element();
237    /// let p_var = relation.allocate_eq(x_var * g_var);
238    ///
239    /// relation.set_element(g_var, G::generator());
240    /// let x = Scalar::random(&mut OsRng);
241    /// relation.compute_image(&[x]).unwrap();
242    ///
243    /// // Convert to NIZK directly
244    /// let nizk = relation.into_nizk(b"my-protocol-v1").unwrap();
245    /// let proof = nizk.prove_batchable(&vec![x], &mut OsRng).unwrap();
246    /// assert!(nizk.verify_batchable(&proof).is_ok());
247    /// # }
248    /// ```
249    pub fn into_nizk(
250        self,
251        session_identifier: &[u8],
252    ) -> crate::errors::Result<crate::Nizk<CanonicalLinearRelation<G>>>
253    where
254        G: PrimeGroup + Encoding<[u8]> + NargSerialize + NargDeserialize,
255        G::Scalar: Encoding<[u8]> + NargSerialize + NargDeserialize + Decoding<[u8]>,
256    {
257        self.canonical()
258            .map_err(|_| crate::errors::Error::InvalidInstanceWitnessPair)?
259            .into_nizk(session_identifier)
260    }
261}
262impl<G> SigmaProtocolSimulator for CanonicalLinearRelation<G>
263where
264    G: PrimeGroup + Encoding<[u8]> + NargSerialize + NargDeserialize + MultiScalarMul,
265    G::Scalar: Encoding<[u8]> + NargSerialize + NargDeserialize + Decoding<[u8]>,
266{
267    /// Simulates a valid transcript for a given challenge without a witness.
268    ///
269    /// # Parameters
270    /// - `challenge`: A scalar value representing the challenge.
271    /// - `rng`: A cryptographically secure RNG.
272    ///
273    /// # Returns
274    /// - A commitment and response forming a valid proof for the given challenge.
275    fn simulate_response(&self, rng: &mut impl ScalarRng) -> Vec<Self::Response> {
276        rng.random_scalars_vec::<G>(self.num_scalars)
277    }
278
279    /// Simulates a full proof transcript using a randomly generated challenge.
280    ///
281    /// # Parameters
282    /// - `rng`: A cryptographically secure RNG.
283    ///
284    /// # Returns
285    /// - A tuple `(commitment, challenge, response)` forming a valid proof.
286    fn simulate_transcript(&self, rng: &mut impl ScalarRng) -> Result<Transcript<Self>> {
287        let [challenge] = rng.random_scalars::<G, _>();
288        let response = self.simulate_response(rng);
289        let commitment = self.simulate_commitment(&challenge, &response)?;
290        Ok((commitment, challenge, response))
291    }
292
293    /// Recomputes the commitment from the challenge and response (used in compact proofs).
294    ///
295    /// # Parameters
296    /// - `challenge`: The challenge scalar issued by the verifier or derived via Fiat–Shamir.
297    /// - `response`: The prover's response vector.
298    ///
299    /// # Returns
300    /// - A vector of group elements representing the simulated commitment (one per linear constraint).
301    ///
302    /// # Errors
303    /// - [`Error::InvalidInstanceWitnessPair`] if the response length does not match the expected number of scalars.
304    fn simulate_commitment(
305        &self,
306        challenge: &Self::Challenge,
307        response: &[Self::Response],
308    ) -> Result<Vec<Self::Commitment>> {
309        if response.len() != self.num_scalars {
310            return Err(Error::InvalidInstanceWitnessPair);
311        }
312
313        // Evaluate the constraint linear combinations using the response scalars.
314        // NOTE: This does not use CanonicalLinearRelation::evaluate because we also want to
315        // include the multiplication of the response image by the challenge in the same MSM.
316        let commitment = itertools::zip_eq(&self.linear_combinations, self.image_elements())
317            .map(|(constraint, img)| {
318                let scalars = constraint
319                    .iter()
320                    .map(|(scalar_var, _)| response[scalar_var.index()])
321                    .chain(core::iter::once(-*challenge))
322                    .collect::<Vec<_>>();
323                let bases = constraint
324                    .iter()
325                    .map(|(_, group_var)| self.group_elements.get(*group_var).unwrap())
326                    .chain(core::iter::once(img))
327                    .collect::<Vec<_>>();
328                MultiScalarMul::msm(&scalars, &bases)
329            })
330            .collect();
331
332        Ok(commitment)
333    }
334}