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