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}