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}