Skip to main content

sigma_proof_compiler/
compiler.rs

1use crate::{
2    absorb::{SymInstance, SymWitness},
3    equations::{SymPoint, SymScalar},
4    errors::{SigmaProofError, SigmaProofResult},
5    transcript::ProofTranscript,
6};
7use curve25519_dalek::{constants::RISTRETTO_BASEPOINT_POINT, RistrettoPoint, Scalar};
8
9/// Escape a variable name for LaTeX and wrap in texttt
10fn latex_var(name: &str) -> String {
11    let escaped = name.replace('_', "\\_");
12    format!("\\texttt{{{}}}", escaped)
13}
14
15/// Convert a SymPoint expression to LaTeX notation with context
16fn sympoint_to_latex_with_context(
17    point: &SymPoint,
18    var_names: &[&str],
19    is_instance: bool,
20) -> String {
21    match point {
22        SymPoint::Const(p) => {
23            if *p == RISTRETTO_BASEPOINT_POINT {
24                // The base point G is always G unless we're in a specific context
25                "G".to_string()
26            } else {
27                "P".to_string() // Some other point (could be a public key or other point)
28            }
29        }
30        SymPoint::Var(Some(_)) => "P".to_string(), // Variable point
31        SymPoint::Var(None) => "?".to_string(),    // Uninstantiated variable point
32        SymPoint::Add(p1, p2) => {
33            format!(
34                "({} + {})",
35                sympoint_to_latex_with_context(p1, var_names, is_instance),
36                sympoint_to_latex_with_context(p2, var_names, is_instance)
37            )
38        }
39        SymPoint::Sub(p1, p2) => {
40            format!(
41                "({} - {})",
42                sympoint_to_latex_with_context(p1, var_names, is_instance),
43                sympoint_to_latex_with_context(p2, var_names, is_instance)
44            )
45        }
46        SymPoint::Neg(p) => {
47            format!(
48                "(-{})",
49                sympoint_to_latex_with_context(p, var_names, is_instance)
50            )
51        }
52        SymPoint::Scale(s, p) => {
53            // Check if p is one of our dummy instance points
54            let point_str = match p.as_ref() {
55                SymPoint::Const(pt) if *pt == Scalar::from(2u64) * RISTRETTO_BASEPOINT_POINT => {
56                    latex_var("pubkey")
57                }
58                SymPoint::Const(pt) if *pt == Scalar::from(3u64) * RISTRETTO_BASEPOINT_POINT => {
59                    latex_var("commitment")
60                }
61                SymPoint::Const(pt) if *pt == Scalar::from(4u64) * RISTRETTO_BASEPOINT_POINT => {
62                    latex_var("handle")
63                }
64                _ => sympoint_to_latex_with_context(p, var_names, is_instance),
65            };
66            format!("{} \\cdot {}", symscalar_to_latex(s, var_names), point_str)
67        }
68    }
69}
70
71/// Convert a SymPoint expression to LaTeX notation (wrapper for backwards compatibility)
72fn sympoint_to_latex(point: &SymPoint, var_names: &[&str]) -> String {
73    sympoint_to_latex_with_context(point, var_names, false)
74}
75
76/// Convert a SymScalar expression to LaTeX notation
77fn symscalar_to_latex(scalar: &SymScalar, var_names: &[&str]) -> String {
78    match scalar {
79        SymScalar::Const(s) => {
80            // Try to match against common small values
81            if *s == Scalar::from(1u64) {
82                "1".to_string()
83            } else if *s == Scalar::from(2u64) {
84                "2".to_string()
85            } else if *s == Scalar::from(3u64) {
86                "3".to_string()
87            } else if *s == Scalar::from(4u64) {
88                "4".to_string()
89            } else if *s == Scalar::from(5u64) {
90                "5".to_string()
91            } else {
92                "c".to_string() // Some constant
93            }
94        }
95        SymScalar::Var(Some(s)) => {
96            // Try to match against dummy values 1, 2, 3, etc.
97            if *s == Scalar::from(1u64) && !var_names.is_empty() {
98                latex_var(var_names[0])
99            } else if *s == Scalar::from(2u64) && var_names.len() > 1 {
100                latex_var(var_names[1])
101            } else if *s == Scalar::from(3u64) && var_names.len() > 2 {
102                latex_var(var_names[2])
103            } else if *s == Scalar::from(4u64) && var_names.len() > 3 {
104                latex_var(var_names[3])
105            } else if *s == Scalar::from(5u64) && var_names.len() > 4 {
106                latex_var(var_names[4])
107            } else {
108                "v".to_string() // Some variable
109            }
110        }
111        SymScalar::Var(None) => "?".to_string(), // Uninstantiated
112        SymScalar::Add(s1, s2) => {
113            format!(
114                "({} + {})",
115                symscalar_to_latex(s1, var_names),
116                symscalar_to_latex(s2, var_names)
117            )
118        }
119        SymScalar::Sub(s1, s2) => {
120            format!(
121                "({} - {})",
122                symscalar_to_latex(s1, var_names),
123                symscalar_to_latex(s2, var_names)
124            )
125        }
126        SymScalar::Neg(s) => {
127            format!("(-{})", symscalar_to_latex(s, var_names))
128        }
129        SymScalar::Mul(s1, s2) => {
130            format!(
131                "({} \\cdot {})",
132                symscalar_to_latex(s1, var_names),
133                symscalar_to_latex(s2, var_names)
134            )
135        }
136    }
137}
138
139pub trait SigmaProof {
140    const LABEL: &'static [u8];
141
142    type WITNESS: SymWitness;
143    type INSTANCE: SymInstance;
144
145    fn f(instance: &Self::INSTANCE) -> Vec<SymPoint>;
146
147    fn psi(witness: &Self::WITNESS, instance: &Self::INSTANCE) -> Vec<SymPoint>;
148
149    fn prove(witness: &Self::WITNESS, instance: &Self::INSTANCE) -> SigmaProofResult<Vec<u8>> {
150        // init transcript
151        let mut transcript = ProofTranscript::new_prover(Self::LABEL);
152
153        // absorb instance, not f(instance)
154        for point in instance.points() {
155            transcript.common_absorb_point(b"", &point.evaluate()?);
156        }
157        for scalar in instance.scalars() {
158            transcript.common_absorb_scalar(b"", &scalar.evaluate()?);
159        }
160
161        // round 1
162        let rng = &mut rand::rngs::OsRng;
163        let alphas = Self::WITNESS::rand(rng);
164        let commited_alphas = Self::psi(&alphas, instance);
165        for point in &commited_alphas {
166            transcript.prover_absorb_point(b"r", &point.evaluate()?);
167        }
168
169        // round 2
170        let e = transcript.challenge(b"e");
171
172        // round 3
173        for z_i in witness
174            .values()?
175            .into_iter()
176            .zip(alphas.values()?)
177            .map(|(s, a)| s * e + a)
178        {
179            transcript.prover_absorb_scalar(b"z", &z_i);
180        }
181
182        Ok(transcript.finalize())
183    }
184
185    fn verify(instance: &Self::INSTANCE, proof: &[u8]) -> Result<(), SigmaProofError> {
186        // sanity check
187        if proof.len() % 32 != 0 {
188            return Err(SigmaProofError::TranscriptFinalizationFailed);
189        }
190
191        // init transcript
192        let mut transcript = ProofTranscript::new_verifier(Self::LABEL, proof);
193
194        // evaluate f(instance)
195        let big_x_points: Vec<_> = Self::f(instance)
196            .into_iter()
197            .map(|p| p.evaluate())
198            .collect::<Result<Vec<_>, _>>()?;
199
200        // absorb instance, not f(instance)
201        for point in instance.points() {
202            transcript.common_absorb_point(b"", &point.evaluate()?);
203        }
204        for scalar in instance.scalars() {
205            transcript.common_absorb_scalar(b"", &scalar.evaluate()?);
206        }
207
208        // -> A
209        let big_a = transcript
210            .verifier_receive_points(b"r", big_x_points.len())
211            .ok_or(SigmaProofError::TranscriptError)?;
212
213        // <- challenge
214        let e = transcript.challenge(b"e");
215
216        // -> sigma
217        let sigmas = transcript
218            .verifier_receives_all_scalars(b"z")
219            .ok_or(SigmaProofError::TranscriptError)?;
220        println!("sigmas received: {}", sigmas.len());
221        let sigmas_as_input = Self::WITNESS::from_values(&sigmas)?;
222
223        let psi_output = Self::psi(&sigmas_as_input, instance);
224
225        // checks
226        if big_x_points.len() != psi_output.len() {
227            return Err(SigmaProofError::PsiOutputLengthMismatch);
228        }
229
230        for ((big_x_i, big_a_i), psi_i) in big_x_points.iter().zip(&big_a).zip(&psi_output) {
231            let rhs = big_a_i + e * big_x_i;
232            if psi_i.evaluate()? != rhs {
233                return Err(SigmaProofError::EquationCheckFailed);
234            }
235        }
236
237        Ok(())
238    }
239
240    /// Generate a specification document in Markdown+LaTeX format
241    fn spec() -> String {
242        let psi_in_len = Self::WITNESS::num_scalars();
243        let f_scalars_in = Self::INSTANCE::num_scalars();
244        let f_points_in = Self::INSTANCE::num_points();
245
246        let protocol_name = String::from_utf8_lossy(Self::LABEL);
247
248        // Generate dummy witness with sequential scalars 1, 2, 3, etc.
249        let dummy_scalars: Vec<Scalar> = (1..=psi_in_len).map(|i| Scalar::from(i as u64)).collect();
250        let dummy_witness = match Self::WITNESS::from_values(&dummy_scalars) {
251            Ok(w) => w,
252            Err(_) => {
253                // Fallback if we can't create dummy witness
254                return format!(
255                    r#"#### {}
256Error: Could not generate symbolic analysis for this protocol."#,
257                    protocol_name
258                );
259            }
260        };
261
262        // Generate dummy instance with sequential scalars and distinct points
263        let dummy_f_scalars_in: Vec<Scalar> =
264            (1..=f_scalars_in).map(|i| Scalar::from(i as u64)).collect();
265        // Use different multiples of G for different instance points to distinguish them
266        let dummy_instance_points: Vec<RistrettoPoint> = (0..f_points_in)
267            .map(|i| Scalar::from((i + 2) as u64) * RISTRETTO_BASEPOINT_POINT)
268            .collect();
269        let dummy_instance =
270            match Self::INSTANCE::from_values(&dummy_f_scalars_in, &dummy_instance_points) {
271                Ok(i) => i,
272                Err(_) => {
273                    // Fallback if we can't create dummy instance
274                    return format!(
275                        r#"#### {}
276Error: Could not generate symbolic analysis for this protocol."#,
277                        protocol_name
278                    );
279                }
280            };
281
282        // Get variable names for the witness
283        let var_names: Vec<&str> = (0..psi_in_len)
284            .map(|i| Self::WITNESS::get_var_name(i))
285            .collect();
286
287        // Get instance field names for better output
288        let instance_field_names = Self::INSTANCE::get_field_names();
289
290        // Symbolically evaluate f function (instance function)
291        let f_result = Self::f(&dummy_instance);
292
293        // Convert f result to LaTeX with field name tracking
294        let f_equations: Vec<String> = f_result
295            .iter()
296            .map(|point| {
297                // For each output, try to match it to an instance field
298                match point {
299                    SymPoint::Const(p) if *p == Scalar::from(2u64) * RISTRETTO_BASEPOINT_POINT => {
300                        // First instance point field
301                        if instance_field_names.len() > f_scalars_in {
302                            latex_var(&instance_field_names[f_scalars_in])
303                        } else {
304                            "P_1".to_string()
305                        }
306                    }
307                    SymPoint::Const(p) if *p == Scalar::from(3u64) * RISTRETTO_BASEPOINT_POINT => {
308                        // Second instance point field
309                        if instance_field_names.len() > f_scalars_in + 1 {
310                            latex_var(&instance_field_names[f_scalars_in + 1])
311                        } else {
312                            "P_2".to_string()
313                        }
314                    }
315                    SymPoint::Const(p) if *p == Scalar::from(4u64) * RISTRETTO_BASEPOINT_POINT => {
316                        // Third instance point field
317                        if instance_field_names.len() > f_scalars_in + 2 {
318                            latex_var(&instance_field_names[f_scalars_in + 2])
319                        } else {
320                            "P_3".to_string()
321                        }
322                    }
323                    _ => sympoint_to_latex_with_context(point, &var_names, true),
324                }
325            })
326            .collect();
327
328        // Symbolically evaluate psi function
329        let psi_result = Self::psi(&dummy_witness, &dummy_instance);
330
331        // Convert psi result to LaTeX
332        let psi_equations: Vec<String> = psi_result
333            .iter()
334            .map(|point| sympoint_to_latex(point, &var_names))
335            .collect();
336
337        let checks = psi_equations
338            .iter()
339            .zip(f_equations.iter())
340            .map(|(psi, f)| format!("* ${} = {}$", psi, f))
341            .collect::<Vec<_>>()
342            .join("\n");
343
344        format!(
345            r#"The Sigma protocol is labeled as `{protocol_name}`.
346
347The **witness** is defined as $\mathbf \omega = \{{ {witness_field_names} \}}$.
348
349The **instance** is defined as $\mathbf X = \{{ {instance_field_names} \}}$.
350
351The sigma protocol allows us to prove knowledge of $\mathbf \omega$ such that  $\psi(\mathbf \omega) = f(\mathbf X)$.
352
353The homomorphism $\psi$ is defined as:
354
355$$
356\begin{{aligned}}
357\psi : \mathbb{{F}}^{{{psi_in_len}}} &\to \mathbb{{G}}^{{{psi_out_len}}} \\
358\mathbf \omega &\mapsto ({psi_latex})
359\end{{aligned}}
360$$
361
362The transformation $f$ is defined as:
363
364$$
365\begin{{aligned}}
366f : \mathbb{{F}}^{{{f_scalars_in}}} \times \mathbb{{G}}^{{{f_points_in}}} &\to \mathbb{{G}}^{{{psi_out_len}}} \\
367\mathbf X &\mapsto ({f_latex})
368\end{{aligned}}
369$$
370
371In other words, the following is being proven:
372
373{checks}
374"#,
375            psi_out_len = f_result.len(),
376            psi_latex = psi_equations.join(", "),
377            f_latex = f_equations.join(", "),
378            witness_field_names = var_names
379                .iter()
380                .map(|name| latex_var(name))
381                .collect::<Vec<_>>()
382                .join(", "),
383            instance_field_names = Self::INSTANCE::get_field_names()
384                .iter()
385                .map(|name| latex_var(name))
386                .collect::<Vec<_>>()
387                .join(", "),
388        )
389    }
390}