Skip to main content

lib_q_zkp/
circuit.rs

1//! Circuit builder for arithmetic constraints
2//!
3//! This module provides a circuit abstraction for building arithmetic constraints
4//! that can be compiled into AIR (Algebraic Intermediate Representation) for STARK proofs.
5
6extern crate alloc;
7use alloc::vec;
8use alloc::vec::Vec;
9
10use lib_q_stark_air::{
11    Air,
12    AirBuilder,
13    BaseAir,
14    WindowAccess,
15};
16use lib_q_stark_field::Field;
17
18/// A wire in the circuit, representing a field element
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub struct Wire {
21    /// The index of the wire in the witness vector
22    pub index: usize,
23}
24
25impl Wire {
26    /// Create a new wire with the given index
27    pub fn new(index: usize) -> Self {
28        Self { index }
29    }
30}
31
32/// A constraint in the circuit
33#[derive(Debug, Clone)]
34pub enum Constraint<F: Field> {
35    /// Assert that a wire equals zero: `wire == 0`
36    AssertZero(Wire),
37    /// Assert that two wires are equal: `left == right`
38    AssertEqual(Wire, Wire),
39    /// Assert that a wire equals a constant: `wire == constant`
40    AssertConstant(Wire, F),
41    /// Assert that a wire equals the sum of two wires: `wire == left + right`
42    AssertAdd(Wire, Wire, Wire),
43    /// Assert that a wire equals the product of two wires: `wire == left * right`
44    AssertMul(Wire, Wire, Wire),
45}
46
47/// An arithmetic circuit containing constraints and metadata
48#[derive(Debug, Clone)]
49pub struct ArithmeticCircuit<F: Field> {
50    /// The constraints in the circuit
51    pub constraints: Vec<Constraint<F>>,
52    /// The number of witness wires (excluding public inputs)
53    pub witness_size: usize,
54    /// The number of public input wires
55    pub public_input_size: usize,
56}
57
58impl<F: Field> ArithmeticCircuit<F> {
59    /// Create a new empty circuit
60    pub fn new(witness_size: usize, public_input_size: usize) -> Self {
61        Self {
62            constraints: Vec::new(),
63            witness_size,
64            public_input_size,
65        }
66    }
67
68    /// Get the total number of wires (witness + public inputs)
69    pub fn total_wires(&self) -> usize {
70        self.witness_size + self.public_input_size
71    }
72
73    /// Add a constraint to the circuit
74    pub fn add_constraint(&mut self, constraint: Constraint<F>) {
75        self.constraints.push(constraint);
76    }
77
78    /// Largest wire index referenced by any constraint, or `None` if there are no constraints.
79    fn max_referenced_wire(&self) -> Option<usize> {
80        self.constraints
81            .iter()
82            .flat_map(|c| match c {
83                Constraint::AssertZero(w) => alloc::vec![w.index],
84                Constraint::AssertEqual(l, r) => alloc::vec![l.index, r.index],
85                Constraint::AssertConstant(w, _) => alloc::vec![w.index],
86                Constraint::AssertAdd(o, l, r) => alloc::vec![o.index, l.index, r.index],
87                Constraint::AssertMul(o, l, r) => alloc::vec![o.index, l.index, r.index],
88            })
89            .max()
90    }
91
92    /// Validate the circuit against a concrete trace `width`.
93    ///
94    /// Rejects circuits whose constraints reference a wire index `>= width`. Such indices were
95    /// previously silently skipped during trace generation and constraint evaluation, which
96    /// dropped the constraint entirely and broke soundness (a prover could omit any constraint
97    /// by giving its output wire an out-of-range index). Returning an error makes the failure
98    /// explicit instead.
99    pub fn validate(&self, width: usize) -> Result<(), lib_q_core::Error> {
100        if let Some(max_wire) = self.max_referenced_wire() {
101            if max_wire >= width {
102                return Err(lib_q_core::Error::InvalidState {
103                    operation: "ArithmeticCircuit::validate".into(),
104                    reason: alloc::format!(
105                        "Constraint references wire index {} but trace width is {}; \
106                         out-of-range wire indices are rejected",
107                        max_wire,
108                        width
109                    ),
110                });
111            }
112        }
113        Ok(())
114    }
115}
116
117/// Builder for constructing arithmetic circuits
118pub struct CircuitBuilder<F: Field> {
119    circuit: ArithmeticCircuit<F>,
120    next_wire: usize,
121}
122
123impl<F: Field> CircuitBuilder<F> {
124    /// Create a new circuit builder
125    ///
126    /// # Arguments
127    ///
128    /// * `witness_size` - Number of witness wires (private inputs)
129    /// * `public_input_size` - Number of public input wires
130    ///
131    /// # Example
132    ///
133    /// ```rust,ignore
134    /// use lib_q_zkp::circuit::CircuitBuilder;
135    /// use lib_q_stark_field::extension::Complex;
136    /// use lib_q_stark_mersenne31::Mersenne31;
137    ///
138    /// type Val = Complex<Mersenne31>;
139    ///
140    /// let mut builder = CircuitBuilder::<Val>::new(2, 1);
141    /// let a = builder.wire(0);  // witness wire 0
142    /// let b = builder.wire(1);  // witness wire 1
143    /// let sum = builder.add(a, b);
144    /// builder.assert_zero(sum);
145    /// let circuit = builder.build();
146    /// ```
147    pub fn new(witness_size: usize, public_input_size: usize) -> Self {
148        Self {
149            circuit: ArithmeticCircuit::new(witness_size, public_input_size),
150            next_wire: witness_size + public_input_size,
151        }
152    }
153
154    /// Allocate a new intermediate wire
155    pub fn alloc_wire(&mut self) -> Wire {
156        let wire = Wire::new(self.next_wire);
157        self.next_wire += 1;
158        wire
159    }
160
161    /// Get a wire by index (for public inputs and witness)
162    pub fn wire(&self, index: usize) -> Wire {
163        Wire::new(index)
164    }
165
166    /// Assert that a wire equals zero
167    pub fn assert_zero(&mut self, wire: Wire) {
168        self.circuit.add_constraint(Constraint::AssertZero(wire));
169    }
170
171    /// Assert that two wires are equal
172    pub fn assert_eq(&mut self, left: Wire, right: Wire) {
173        self.circuit
174            .add_constraint(Constraint::AssertEqual(left, right));
175    }
176
177    /// Assert that a wire equals a constant
178    pub fn assert_constant(&mut self, wire: Wire, constant: F) {
179        self.circuit
180            .add_constraint(Constraint::AssertConstant(wire, constant));
181    }
182
183    /// Add two wires and return the result wire
184    pub fn add(&mut self, left: Wire, right: Wire) -> Wire {
185        let result = self.alloc_wire();
186        self.circuit
187            .add_constraint(Constraint::AssertAdd(result, left, right));
188        result
189    }
190
191    /// Multiply two wires and return the result wire
192    pub fn mul(&mut self, left: Wire, right: Wire) -> Wire {
193        let result = self.alloc_wire();
194        self.circuit
195            .add_constraint(Constraint::AssertMul(result, left, right));
196        result
197    }
198
199    /// Build the circuit
200    pub fn build(self) -> ArithmeticCircuit<F> {
201        self.circuit
202    }
203}
204
205/// AIR implementation for an arithmetic circuit
206///
207/// This converts a circuit into an AIR that can be used with STARK proving.
208/// The trace represents all wire values, with one row containing all wire values.
209pub struct CircuitAir<F: Field> {
210    circuit: ArithmeticCircuit<F>,
211}
212
213impl<F: Field> CircuitAir<F> {
214    /// Create a new CircuitAir from an ArithmeticCircuit
215    pub fn new(circuit: ArithmeticCircuit<F>) -> Self {
216        Self { circuit }
217    }
218
219    /// Get a reference to the underlying circuit
220    pub fn circuit(&self) -> &ArithmeticCircuit<F> {
221        &self.circuit
222    }
223
224    /// Generate an execution trace from witness values
225    ///
226    /// The witness values should include all wire values in the circuit.
227    /// Wire indices 0..witness_size are witness wires,
228    /// indices witness_size..witness_size+public_input_size are public inputs,
229    /// and remaining indices are intermediate wires.
230    ///
231    /// # Arguments
232    ///
233    /// * `witness` - Private witness values (witness wires)
234    /// * `public` - Public input values
235    ///
236    /// # Returns
237    ///
238    /// A RowMajorMatrix containing the trace, or an error if validation fails
239    pub fn generate_trace(
240        &self,
241        witness: &[F],
242        public: &[F],
243    ) -> Result<lib_q_stark_matrix::dense::RowMajorMatrix<F>, lib_q_core::Error> {
244        use lib_q_stark_matrix::dense::RowMajorMatrix;
245
246        // Validate input sizes
247        if witness.len() != self.circuit.witness_size {
248            return Err(lib_q_core::Error::InvalidState {
249                operation: "CircuitAir::generate_trace".into(),
250                reason: alloc::format!(
251                    "Witness size mismatch: expected {}, got {}",
252                    self.circuit.witness_size,
253                    witness.len()
254                ),
255            });
256        }
257
258        if public.len() != self.circuit.public_input_size {
259            return Err(lib_q_core::Error::InvalidState {
260                operation: "CircuitAir::generate_trace".into(),
261                reason: alloc::format!(
262                    "Public input size mismatch: expected {}, got {}",
263                    self.circuit.public_input_size,
264                    public.len()
265                ),
266            });
267        }
268
269        let width = self.width();
270
271        // Reject circuits with out-of-range wire indices instead of silently dropping the
272        // affected constraints (which would be unsound).
273        self.circuit.validate(width)?;
274
275        // Witness/public inputs must fit within the trace width.
276        if self.circuit.witness_size + self.circuit.public_input_size > width {
277            return Err(lib_q_core::Error::InvalidState {
278                operation: "CircuitAir::generate_trace".into(),
279                reason: alloc::format!(
280                    "Witness ({}) + public inputs ({}) exceed trace width {}",
281                    self.circuit.witness_size,
282                    self.circuit.public_input_size,
283                    width
284                ),
285            });
286        }
287
288        // Allocate trace for a single row (power of 2)
289        let mut trace_values = F::zero_vec(width);
290
291        // Fill witness wires (bounds guaranteed by the checks above).
292        for (i, val) in witness.iter().enumerate() {
293            trace_values[i] = *val;
294        }
295
296        // Fill public input wires
297        for (i, val) in public.iter().enumerate() {
298            let idx = self.circuit.witness_size + i;
299            trace_values[idx] = *val;
300        }
301
302        // Compute intermediate wire values by evaluating constraints. Indices are guaranteed
303        // in-range by `validate`, so no defensive skipping is needed.
304        for constraint in &self.circuit.constraints {
305            match constraint {
306                Constraint::AssertAdd(out, l, r) => {
307                    trace_values[out.index] = trace_values[l.index] + trace_values[r.index];
308                }
309                Constraint::AssertMul(out, l, r) => {
310                    trace_values[out.index] = trace_values[l.index] * trace_values[r.index];
311                }
312                // Other constraints don't compute new values
313                _ => {}
314            }
315        }
316
317        // Pad to at least MIN_TRACE_ROWS so FRI has sufficient two-adic height (degree >= 1)
318        const MIN_TRACE_ROWS: usize = 64;
319        if MIN_TRACE_ROWS > 1 {
320            let mut padded = trace_values.clone();
321            for _ in 1..MIN_TRACE_ROWS {
322                padded.extend_from_slice(&trace_values);
323            }
324            Ok(RowMajorMatrix::new(padded, width))
325        } else {
326            Ok(RowMajorMatrix::new(trace_values, width))
327        }
328    }
329}
330
331impl<F: Field> BaseAir<F> for CircuitAir<F> {
332    fn width(&self) -> usize {
333        // The width is the total number of wires (witness + public inputs + intermediate wires)
334        // We need to compute this from the constraints
335        let max_wire = self
336            .circuit
337            .constraints
338            .iter()
339            .flat_map(|c| match c {
340                Constraint::AssertZero(w) => vec![w.index],
341                Constraint::AssertEqual(l, r) => vec![l.index, r.index],
342                Constraint::AssertConstant(w, _) => vec![w.index],
343                Constraint::AssertAdd(r, l, r2) => vec![r.index, l.index, r2.index],
344                Constraint::AssertMul(r, l, r2) => vec![r.index, l.index, r2.index],
345            })
346            .max()
347            .unwrap_or(0);
348        (max_wire + 1).max(self.circuit.total_wires())
349    }
350}
351
352impl<F: Field, AB: AirBuilder<F = F>> Air<AB> for CircuitAir<F> {
353    fn eval(&self, builder: &mut AB) {
354        // SOUNDNESS: bind the public-input wires to the declared public values. Without this
355        // the public inputs live only in the trace and are never tied to the verifier-supplied
356        // public values, so a prover could prove the statement for *any* public inputs.
357        // Public input wires occupy columns [witness_size, witness_size + public_input_size).
358        let pubs = builder.public_values().to_vec();
359
360        let main = builder.main();
361        let row = main.current_slice();
362
363        // Bind public inputs to public values.
364        let base = self.circuit.witness_size;
365        for (i, pv) in pubs.iter().enumerate() {
366            let col = base + i;
367            if col < row.len() {
368                builder.assert_eq(row[col], *pv);
369            }
370        }
371
372        // Evaluate each constraint in the circuit
373        for constraint in &self.circuit.constraints {
374            match constraint {
375                Constraint::AssertZero(w) => {
376                    // Constraint: wire[w.index] == 0
377                    if w.index < row.len() {
378                        builder.assert_zero(row[w.index]);
379                    }
380                }
381                Constraint::AssertEqual(l, r) => {
382                    // Constraint: wire[l.index] == wire[r.index]
383                    if l.index < row.len() && r.index < row.len() {
384                        builder.assert_eq(row[l.index], row[r.index]);
385                    }
386                }
387                Constraint::AssertConstant(w, c) => {
388                    // Constraint: wire[w.index] == constant
389                    if w.index < row.len() {
390                        builder.assert_eq(row[w.index], *c);
391                    }
392                }
393                Constraint::AssertAdd(out, l, r) => {
394                    // Constraint: wire[out.index] == wire[l.index] + wire[r.index]
395                    if out.index < row.len() && l.index < row.len() && r.index < row.len() {
396                        let sum = row[l.index] + row[r.index];
397                        builder.assert_eq(row[out.index], sum);
398                    }
399                }
400                Constraint::AssertMul(out, l, r) => {
401                    // Constraint: wire[out.index] == wire[l.index] * wire[r.index]
402                    if out.index < row.len() && l.index < row.len() && r.index < row.len() {
403                        let product = row[l.index] * row[r.index];
404                        builder.assert_eq(row[out.index], product);
405                    }
406                }
407            }
408        }
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use lib_q_stark_air::BaseAir;
415    use lib_q_stark_field::PrimeCharacteristicRing;
416    use lib_q_stark_field::extension::Complex;
417    use lib_q_stark_mersenne31::Mersenne31;
418
419    use super::*;
420
421    type TestField = Complex<Mersenne31>;
422
423    #[test]
424    fn test_circuit_builder_new() {
425        let builder = CircuitBuilder::<TestField>::new(5, 2);
426        let circuit = builder.build();
427        assert_eq!(circuit.witness_size, 5);
428        assert_eq!(circuit.public_input_size, 2);
429        assert_eq!(circuit.total_wires(), 7);
430    }
431
432    #[test]
433    fn test_circuit_builder_alloc_wire() {
434        let mut builder = CircuitBuilder::<TestField>::new(3, 2);
435        let wire1 = builder.alloc_wire();
436        let wire2 = builder.alloc_wire();
437        assert_eq!(wire1.index, 5); // 3 witness + 2 public = 5
438        assert_eq!(wire2.index, 6);
439    }
440
441    #[test]
442    fn test_circuit_builder_constraints() {
443        let mut builder = CircuitBuilder::<TestField>::new(2, 1);
444        let w0 = builder.wire(0);
445        let w1 = builder.wire(1);
446        let w2 = builder.wire(2);
447
448        builder.assert_zero(w0);
449        builder.assert_eq(w1, w2);
450        builder.assert_constant(w0, <TestField as PrimeCharacteristicRing>::ONE);
451
452        let circuit = builder.build();
453        assert_eq!(circuit.constraints.len(), 3);
454    }
455
456    #[test]
457    fn test_circuit_builder_add_mul() {
458        let mut builder = CircuitBuilder::<TestField>::new(2, 1);
459        let a = builder.wire(0);
460        let b = builder.wire(1);
461        let sum = builder.add(a, b);
462        let product = builder.mul(a, b);
463
464        assert!(sum.index >= 3);
465        assert!(product.index >= 3);
466        assert!(product.index > sum.index);
467
468        let circuit = builder.build();
469        assert_eq!(circuit.constraints.len(), 2);
470    }
471
472    #[test]
473    fn test_arithmetic_circuit() {
474        let mut circuit = ArithmeticCircuit::<TestField>::new(3, 2);
475        circuit.add_constraint(Constraint::AssertZero(Wire::new(0)));
476        circuit.add_constraint(Constraint::AssertEqual(Wire::new(1), Wire::new(2)));
477
478        assert_eq!(circuit.constraints.len(), 2);
479        assert_eq!(circuit.total_wires(), 5);
480    }
481
482    #[test]
483    fn test_circuit_air_width() {
484        let mut circuit = ArithmeticCircuit::<TestField>::new(2, 1);
485        circuit.add_constraint(Constraint::AssertZero(Wire::new(0)));
486        circuit.add_constraint(Constraint::AssertEqual(Wire::new(1), Wire::new(2)));
487
488        let air = CircuitAir::new(circuit);
489        assert!(BaseAir::<TestField>::width(&air) >= 3); // At least total_wires
490    }
491}