sigma_rs/fiat_shamir.rs
1//! Fiat-Shamir transformation for [`SigmaProtocol`]s.
2//!
3//! This module defines [`NISigmaProtocol`], a generic non-interactive Sigma protocol wrapper,
4//! based on applying the Fiat-Shamir heuristic using a codec.
5//!
6//! It transforms an interactive [`SigmaProtocol`] into a non-interactive one,
7//! by deriving challenges deterministically from previous protocol messages
8//! via a cryptographic sponge function (Codec).
9//!
10//! # Usage
11//! This struct is generic over:
12//! - `P`: the underlying Sigma protocol ([`SigmaProtocol`] trait).
13//! - `C`: the codec ([`Codec`] trait).
14
15use crate::errors::Error;
16use crate::traits::SigmaProtocol;
17use crate::{codec::Codec, traits::SigmaProtocolSimulator};
18
19use rand::{CryptoRng, RngCore};
20
21type Transcript<P> = (
22 <P as SigmaProtocol>::Commitment,
23 <P as SigmaProtocol>::Challenge,
24 <P as SigmaProtocol>::Response,
25);
26
27/// A Fiat-Shamir transformation of a [`SigmaProtocol`] into a non-interactive proof.
28///
29/// [`NISigmaProtocol`] wraps an interactive Sigma protocol `P`
30/// and a hash-based codec `C`, to produce non-interactive proofs.
31///
32/// It manages the domain separation, codec reset,
33/// proof generation, and proof verification.
34///
35/// # Type Parameters
36/// - `P`: the Sigma protocol implementation.
37/// - `C`: the codec used for Fiat-Shamir.
38#[derive(Debug)]
39pub struct NISigmaProtocol<P, C>
40where
41 P: SigmaProtocol,
42 P::Challenge: PartialEq,
43 C: Codec<Challenge = P::Challenge>,
44{
45 /// Current codec state.
46 pub hash_state: C,
47 /// Underlying interactive proof.
48 pub interactive_proof: P,
49}
50
51impl<P, C> NISigmaProtocol<P, C>
52where
53 P: SigmaProtocol,
54 P::Challenge: PartialEq,
55 C: Codec<Challenge = P::Challenge> + Clone,
56{
57 /// Constructs a new [`NISigmaProtocol`] instance.
58 ///
59 /// # Parameters
60 /// - `iv`: Domain separation tag for the hash function (e.g., protocol name or context).
61 /// - `instance`: An instance of the interactive Sigma protocol.
62 ///
63 /// # Returns
64 /// A new [`NISigmaProtocol`] that can generate and verify non-interactive proofs.
65 pub fn new(session_identifier: &[u8], interactive_proof: P) -> Self {
66 let hash_state = C::new(
67 interactive_proof.protocol_identifier().as_ref(),
68 session_identifier,
69 interactive_proof.instance_label().as_ref(),
70 );
71 Self {
72 hash_state,
73 interactive_proof,
74 }
75 }
76
77 pub fn from_iv(iv: [u8; 32], interactive_proof: P) -> Self {
78 let hash_state = C::from_iv(iv);
79 Self {
80 hash_state,
81 interactive_proof,
82 }
83 }
84
85 /// Generates a non-interactive proof for a witness.
86 ///
87 /// Executes the interactive protocol steps (commit, derive challenge via hash, respond),
88 /// and checks the result locally for consistency.
89 ///
90 /// # Parameters
91 /// - `witness`: The secret witness for the Sigma protocol.
92 /// - `rng`: A cryptographically secure random number generator.
93 ///
94 /// # Returns
95 /// A [`Result`] containing a `Transcript<P>` on success. The `Transcript` includes:
96 /// - `P::Commitment`: The prover's commitment(s).
97 /// - `P::Challenge`: The challenge derived via Fiat-Shamir.
98 /// - `P::Response`: The prover's response.
99 ///
100 /// # Panics
101 /// Panics if local verification fails.
102 fn prove(
103 &self,
104 witness: &P::Witness,
105 rng: &mut (impl RngCore + CryptoRng),
106 ) -> Result<Transcript<P>, Error> {
107 let mut hash_state = self.hash_state.clone();
108
109 let (commitment, prover_state) = self.interactive_proof.prover_commit(witness, rng)?;
110 // Fiat Shamir challenge
111 let serialized_commitment = self.interactive_proof.serialize_commitment(&commitment);
112 hash_state.prover_message(&serialized_commitment);
113 let challenge = hash_state.verifier_challenge();
114 // Prover's response
115 let response = self
116 .interactive_proof
117 .prover_response(prover_state, &challenge)?;
118
119 // Local verification of the proof
120 debug_assert!(self
121 .interactive_proof
122 .verifier(&commitment, &challenge, &response)
123 .is_ok());
124 Ok((commitment, challenge, response))
125 }
126
127 /// Verifies a non-interactive proof using the Fiat-Shamir transformation.
128 ///
129 /// # Parameters
130 /// - `commitment`: The commitment(s) sent by the prover.
131 /// - `challenge`: The challenge allegedly derived via Fiat-Shamir.
132 /// - `response`: The prover's response to the challenge.
133 ///
134 /// # Returns
135 /// - `Ok(())` if the proof is valid.
136 /// - `Err(Error::VerificationFailure)` if the challenge is invalid or the response fails to verify.
137 ///
138 /// # Errors
139 /// - Returns [`Error::VerificationFailure`] if:
140 /// - The challenge doesn't match the recomputed one from the commitment.
141 /// - The response fails verification under the Sigma protocol.
142 fn verify(
143 &self,
144 commitment: &P::Commitment,
145 challenge: &P::Challenge,
146 response: &P::Response,
147 ) -> Result<(), Error> {
148 let mut hash_state = self.hash_state.clone();
149
150 // Recompute the challenge
151 let serialized_commitment = self.interactive_proof.serialize_commitment(commitment);
152 hash_state.prover_message(&serialized_commitment);
153 let expected_challenge = hash_state.verifier_challenge();
154 // Verification of the proof
155 match *challenge == expected_challenge {
156 true => self
157 .interactive_proof
158 .verifier(commitment, challenge, response),
159 false => Err(Error::VerificationFailure),
160 }
161 }
162 /// Generates a batchable, serialized non-interactive proof.
163 ///
164 /// # Parameters
165 /// - `witness`: The secret witness.
166 /// - `rng`: A cryptographically secure random number generator.
167 ///
168 /// # Returns
169 /// A serialized proof suitable for batch verification.
170 ///
171 /// # Panics
172 /// Panics if serialization fails (should not happen under correct implementation).
173 pub fn prove_batchable(
174 &self,
175 witness: &P::Witness,
176 rng: &mut (impl RngCore + CryptoRng),
177 ) -> Result<Vec<u8>, Error> {
178 let (commitment, _challenge, response) = self.prove(witness, rng)?;
179 let mut bytes = Vec::new();
180 bytes.extend_from_slice(&self.interactive_proof.serialize_commitment(&commitment));
181 bytes.extend_from_slice(&self.interactive_proof.serialize_response(&response));
182 Ok(bytes)
183 }
184
185 /// Verifies a batchable non-interactive proof.
186 ///
187 /// # Parameters
188 /// - `proof`: A serialized batchable proof.
189 ///
190 /// # Returns
191 /// - `Ok(())` if the proof is valid.
192 /// - `Err(Error)` if deserialization or verification fails.
193 ///
194 /// # Errors
195 /// - Returns [`Error::VerificationFailure`] if:
196 /// - The challenge doesn't match the recomputed one from the commitment.
197 /// - The response fails verification under the Sigma protocol.
198 pub fn verify_batchable(&self, proof: &[u8]) -> Result<(), Error> {
199 let commitment = self.interactive_proof.deserialize_commitment(proof)?;
200 let commitment_size = self
201 .interactive_proof
202 .serialize_commitment(&commitment)
203 .len();
204 let response = self
205 .interactive_proof
206 .deserialize_response(&proof[commitment_size..])?;
207
208 let mut hash_state = self.hash_state.clone();
209
210 // Recompute the challenge
211 let serialized_commitment = self.interactive_proof.serialize_commitment(&commitment);
212 hash_state.prover_message(&serialized_commitment);
213 let challenge = hash_state.verifier_challenge();
214 // Verification of the proof
215 self.interactive_proof
216 .verifier(&commitment, &challenge, &response)
217 }
218}
219
220impl<P, C> NISigmaProtocol<P, C>
221where
222 P: SigmaProtocol + SigmaProtocolSimulator,
223 P::Challenge: PartialEq,
224 C: Codec<Challenge = P::Challenge> + Clone,
225{
226 /// Generates a compact serialized proof.
227 ///
228 /// Uses a more space-efficient representation compared to batchable proofs.
229 ///
230 /// # Parameters
231 /// - `witness`: The secret witness.
232 /// - `rng`: A cryptographically secure random number generator.
233 ///
234 /// # Returns
235 /// A compact, serialized proof.
236 ///
237 /// # Panics
238 /// Panics if serialization fails.
239 pub fn prove_compact(
240 &self,
241 witness: &P::Witness,
242 rng: &mut (impl RngCore + CryptoRng),
243 ) -> Result<Vec<u8>, Error> {
244 let (_commitment, challenge, response) = self.prove(witness, rng)?;
245 let mut bytes = Vec::new();
246 bytes.extend_from_slice(&self.interactive_proof.serialize_challenge(&challenge));
247 bytes.extend_from_slice(&self.interactive_proof.serialize_response(&response));
248 Ok(bytes)
249 }
250
251 /// Verifies a compact proof.
252 ///
253 /// Recomputes the commitment from the challenge and response, then verifies it.
254 ///
255 /// # Parameters
256 /// - `proof`: A compact serialized proof.
257 ///
258 /// # Returns
259 /// - `Ok(())` if the proof is valid.
260 /// - `Err(Error)` if deserialization or verification fails.
261 ///
262 /// # Errors
263 /// - Returns [`Error::VerificationFailure`] if:
264 /// - Deserialization fails.
265 /// - The recomputed commitment or response is invalid under the Sigma protocol.
266 pub fn verify_compact(&self, proof: &[u8]) -> Result<(), Error> {
267 // Deserialize challenge and response from compact proof
268 let challenge = self.interactive_proof.deserialize_challenge(proof)?;
269 let challenge_size = self.interactive_proof.serialize_challenge(&challenge).len();
270 let response = self
271 .interactive_proof
272 .deserialize_response(&proof[challenge_size..])?;
273
274 // Compute the commitments
275 let commitment = self
276 .interactive_proof
277 .simulate_commitment(&challenge, &response)?;
278 // Verify the proof
279 self.verify(&commitment, &challenge, &response)
280 }
281}