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}