1use serde::{Deserialize, Serialize};
23
24use crate::error::{ProofError, Result};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32pub struct Variable {
33 pub index: usize,
35 pub value: Option<u64>,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct LinearCombination {
47 pub terms: Vec<(u64, usize)>,
48}
49
50impl LinearCombination {
51 #[must_use]
53 pub fn zero() -> Self {
54 Self { terms: Vec::new() }
55 }
56
57 #[must_use]
59 pub fn from_variable(var: Variable) -> Self {
60 Self {
61 terms: vec![(1, var.index)],
62 }
63 }
64
65 #[must_use]
67 pub fn constant(value: u64) -> Self {
68 Self {
69 terms: vec![(value, usize::MAX)],
70 }
71 }
72
73 pub fn add_term(&mut self, coeff: u64, var_index: usize) {
75 self.terms.push((coeff, var_index));
76 }
77
78 pub fn evaluate(&self, vars: &[u64]) -> Result<u64> {
81 let mut sum: u64 = 0;
82 for &(coeff, idx) in &self.terms {
83 let val = if idx == usize::MAX {
84 1u64
85 } else if idx < vars.len() {
86 vars[idx]
87 } else {
88 return Err(ProofError::ConstraintError(format!(
89 "linear combination references unallocated variable index {idx}; allocated variables: {}",
90 vars.len()
91 )));
92 };
93 let term = coeff.checked_mul(val).ok_or_else(|| {
94 ProofError::ConstraintError(format!(
95 "linear combination term overflows u64: coefficient {coeff} multiplied by value {val}"
96 ))
97 })?;
98 sum = sum.checked_add(term).ok_or_else(|| {
99 ProofError::ConstraintError(format!(
100 "linear combination sum overflows u64 when adding term {term}"
101 ))
102 })?;
103 }
104 Ok(sum)
105 }
106
107 fn references_only_allocated_variables(&self, variable_count: usize) -> bool {
108 self.terms
109 .iter()
110 .all(|&(_, idx)| idx == usize::MAX || idx < variable_count)
111 }
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120pub struct Constraint {
121 pub a_terms: LinearCombination,
122 pub b_terms: LinearCombination,
123 pub c_terms: LinearCombination,
124}
125
126#[derive(Debug, Clone)]
132pub struct ConstraintSystem {
133 pub variables: Vec<Variable>,
135 pub constraints: Vec<Constraint>,
137 pub num_public_inputs: usize,
139 pub public_input_indices: Vec<usize>,
141}
142
143impl ConstraintSystem {
144 #[must_use]
146 pub fn new() -> Self {
147 Self {
148 variables: Vec::new(),
149 constraints: Vec::new(),
150 num_public_inputs: 0,
151 public_input_indices: Vec::new(),
152 }
153 }
154
155 #[must_use]
157 pub fn num_variables(&self) -> usize {
158 self.variables.len()
159 }
160
161 #[must_use]
163 pub fn num_constraints(&self) -> usize {
164 self.constraints.len()
165 }
166
167 pub fn is_satisfied(&self) -> bool {
169 let mut vals = Vec::with_capacity(self.variables.len());
170 for variable in &self.variables {
171 let Some(value) = variable.value else {
172 return false;
173 };
174 vals.push(value);
175 }
176
177 for c in &self.constraints {
178 if !c.a_terms.references_only_allocated_variables(vals.len())
179 || !c.b_terms.references_only_allocated_variables(vals.len())
180 || !c.c_terms.references_only_allocated_variables(vals.len())
181 {
182 return false;
183 }
184 let (Ok(a), Ok(b), Ok(c_val)) = (
185 c.a_terms.evaluate(&vals),
186 c.b_terms.evaluate(&vals),
187 c.c_terms.evaluate(&vals),
188 ) else {
189 return false;
190 };
191 let Some(product) = a.checked_mul(b) else {
192 return false;
193 };
194 if product != c_val {
195 return false;
196 }
197 }
198 true
199 }
200}
201
202impl Default for ConstraintSystem {
203 fn default() -> Self {
204 Self::new()
205 }
206}
207
208pub fn allocate(cs: &mut ConstraintSystem, value: Option<u64>) -> Variable {
210 let index = cs.variables.len();
211 let var = Variable { index, value };
212 cs.variables.push(var);
213 var
214}
215
216pub fn allocate_public(cs: &mut ConstraintSystem, value: Option<u64>) -> Variable {
218 let var = allocate(cs, value);
219 cs.public_input_indices.push(var.index);
220 cs.num_public_inputs += 1;
221 var
222}
223
224pub fn enforce(
226 cs: &mut ConstraintSystem,
227 a: &LinearCombination,
228 b: &LinearCombination,
229 c: &LinearCombination,
230) {
231 cs.constraints.push(Constraint {
232 a_terms: a.clone(),
233 b_terms: b.clone(),
234 c_terms: c.clone(),
235 });
236}
237
238pub trait Circuit {
244 fn synthesize(&self, cs: &mut ConstraintSystem) -> Result<()>;
246}
247
248#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[derive(Debug)]
258 struct MultiplyCircuit {
259 x: Option<u64>,
260 y: Option<u64>,
261 z: Option<u64>,
262 }
263
264 impl Circuit for MultiplyCircuit {
265 fn synthesize(&self, cs: &mut ConstraintSystem) -> Result<()> {
266 let x = allocate_public(cs, self.x);
267 let y = allocate(cs, self.y);
268 let z = allocate_public(cs, self.z);
269
270 let a = LinearCombination::from_variable(x);
272 let b = LinearCombination::from_variable(y);
273 let c = LinearCombination::from_variable(z);
274 enforce(cs, &a, &b, &c);
275
276 Ok(())
277 }
278 }
279
280 #[derive(Debug)]
282 struct AddCircuit {
283 a: Option<u64>,
284 b: Option<u64>,
285 c: Option<u64>,
286 }
287
288 impl Circuit for AddCircuit {
289 fn synthesize(&self, cs: &mut ConstraintSystem) -> Result<()> {
290 let a_var = allocate_public(cs, self.a);
291 let b_var = allocate(cs, self.b);
292 let c_var = allocate_public(cs, self.c);
293
294 let mut a_lc = LinearCombination::zero();
296 a_lc.add_term(1, a_var.index);
297 a_lc.add_term(1, b_var.index);
298 let b_lc = LinearCombination::constant(1);
299 let c_lc = LinearCombination::from_variable(c_var);
300 enforce(cs, &a_lc, &b_lc, &c_lc);
301
302 Ok(())
303 }
304 }
305
306 #[test]
307 fn empty_constraint_system() {
308 let cs = ConstraintSystem::new();
309 assert_eq!(cs.num_variables(), 0);
310 assert_eq!(cs.num_constraints(), 0);
311 assert!(cs.is_satisfied());
312 }
313
314 #[test]
315 fn default_constraint_system() {
316 let cs = ConstraintSystem::default();
317 assert!(cs.is_satisfied());
318 }
319
320 #[test]
321 fn allocate_variable() {
322 let mut cs = ConstraintSystem::new();
323 let v = allocate(&mut cs, Some(42));
324 assert_eq!(v.index, 0);
325 assert_eq!(v.value, Some(42));
326 assert_eq!(cs.num_variables(), 1);
327 }
328
329 #[test]
330 fn allocate_public_input() {
331 let mut cs = ConstraintSystem::new();
332 let v = allocate_public(&mut cs, Some(10));
333 assert_eq!(v.index, 0);
334 assert_eq!(cs.num_public_inputs, 1);
335 }
336
337 #[test]
338 fn multiply_circuit_satisfied() {
339 let circuit = MultiplyCircuit {
340 x: Some(3),
341 y: Some(4),
342 z: Some(12),
343 };
344 let mut cs = ConstraintSystem::new();
345 circuit.synthesize(&mut cs).unwrap();
346 assert_eq!(cs.num_variables(), 3);
347 assert_eq!(cs.num_constraints(), 1);
348 assert_eq!(cs.num_public_inputs, 2);
349 assert!(cs.is_satisfied());
350 }
351
352 #[test]
353 fn multiply_circuit_not_satisfied() {
354 let circuit = MultiplyCircuit {
355 x: Some(3),
356 y: Some(4),
357 z: Some(13), };
359 let mut cs = ConstraintSystem::new();
360 circuit.synthesize(&mut cs).unwrap();
361 assert!(!cs.is_satisfied());
362 }
363
364 #[test]
365 fn add_circuit_satisfied() {
366 let circuit = AddCircuit {
367 a: Some(5),
368 b: Some(7),
369 c: Some(12),
370 };
371 let mut cs = ConstraintSystem::new();
372 circuit.synthesize(&mut cs).unwrap();
373 assert!(cs.is_satisfied());
374 }
375
376 #[test]
377 fn add_circuit_not_satisfied() {
378 let circuit = AddCircuit {
379 a: Some(5),
380 b: Some(7),
381 c: Some(11), };
383 let mut cs = ConstraintSystem::new();
384 circuit.synthesize(&mut cs).unwrap();
385 assert!(!cs.is_satisfied());
386 }
387
388 #[test]
389 fn linear_combination_evaluate() {
390 let mut lc = LinearCombination::zero();
391 lc.add_term(2, 0); lc.add_term(3, 1); lc.add_term(5, usize::MAX); let vars = vec![10, 20]; assert_eq!(lc.evaluate(&vars).unwrap(), 2 * 10 + 3 * 20 + 5);
397 }
398
399 #[test]
400 fn linear_combination_zero() {
401 let lc = LinearCombination::zero();
402 assert_eq!(lc.evaluate(&[1, 2, 3]).unwrap(), 0);
403 }
404
405 #[test]
406 fn linear_combination_constant() {
407 let lc = LinearCombination::constant(42);
408 assert_eq!(lc.evaluate(&[]).unwrap(), 42);
409 }
410
411 #[test]
412 fn linear_combination_from_variable() {
413 let v = Variable {
414 index: 2,
415 value: Some(7),
416 };
417 let lc = LinearCombination::from_variable(v);
418 assert_eq!(lc.evaluate(&[0, 0, 7]).unwrap(), 7);
419 }
420
421 #[test]
422 fn enforce_constraint() {
423 let mut cs = ConstraintSystem::new();
424 let x = allocate(&mut cs, Some(5));
425 let y = allocate(&mut cs, Some(5));
426 let z = allocate(&mut cs, Some(25));
427
428 let a = LinearCombination::from_variable(x);
429 let b = LinearCombination::from_variable(y);
430 let c = LinearCombination::from_variable(z);
431 enforce(&mut cs, &a, &b, &c);
432
433 assert!(cs.is_satisfied());
434 }
435
436 #[test]
437 fn multiple_constraints() {
438 let mut cs = ConstraintSystem::new();
439 let x = allocate(&mut cs, Some(2));
440 let y = allocate(&mut cs, Some(3));
441 let z = allocate(&mut cs, Some(6)); let w = allocate(&mut cs, Some(36)); enforce(
446 &mut cs,
447 &LinearCombination::from_variable(x),
448 &LinearCombination::from_variable(y),
449 &LinearCombination::from_variable(z),
450 );
451
452 enforce(
454 &mut cs,
455 &LinearCombination::from_variable(z),
456 &LinearCombination::from_variable(z),
457 &LinearCombination::from_variable(w),
458 );
459
460 assert_eq!(cs.num_constraints(), 2);
461 assert!(cs.is_satisfied());
462 }
463
464 #[test]
465 fn constraint_system_with_missing_witness_is_not_satisfied() {
466 let mut cs = ConstraintSystem::new();
467 let x = allocate(&mut cs, None);
468
469 enforce(
470 &mut cs,
471 &LinearCombination::from_variable(x),
472 &LinearCombination::constant(1),
473 &LinearCombination::constant(0),
474 );
475
476 assert!(
477 !cs.is_satisfied(),
478 "missing witness variables must not be substituted with zero"
479 );
480 }
481
482 #[test]
483 fn constraint_system_with_unallocated_variable_reference_is_not_satisfied() {
484 let mut cs = ConstraintSystem::new();
485 let x = allocate(&mut cs, Some(0));
486 let mut unallocated = LinearCombination::zero();
487 unallocated.add_term(1, x.index + 1);
488
489 enforce(
490 &mut cs,
491 &unallocated,
492 &LinearCombination::constant(1),
493 &LinearCombination::constant(0),
494 );
495
496 assert!(
497 !cs.is_satisfied(),
498 "constraints must not satisfy by treating unallocated variables as zero"
499 );
500 }
501
502 #[test]
503 fn constraint_system_with_overflowing_linear_combination_is_not_satisfied() {
504 let mut cs = ConstraintSystem::new();
505 let x = allocate(&mut cs, Some(u64::MAX));
506 let y = allocate(&mut cs, Some(1));
507
508 let mut overflowing_sum = LinearCombination::zero();
509 overflowing_sum.add_term(1, x.index);
510 overflowing_sum.add_term(1, y.index);
511
512 enforce(
513 &mut cs,
514 &overflowing_sum,
515 &LinearCombination::constant(1),
516 &LinearCombination::constant(0),
517 );
518
519 assert!(
520 !cs.is_satisfied(),
521 "linear combination overflow must fail closed instead of wrapping to zero"
522 );
523 }
524
525 #[test]
526 fn constraint_system_with_overflowing_constraint_product_is_not_satisfied() {
527 let mut cs = ConstraintSystem::new();
528 let x = allocate(&mut cs, Some(u64::MAX));
529 let y = allocate(&mut cs, Some(2));
530 let z = allocate(&mut cs, Some(u64::MAX - 1));
531
532 enforce(
533 &mut cs,
534 &LinearCombination::from_variable(x),
535 &LinearCombination::from_variable(y),
536 &LinearCombination::from_variable(z),
537 );
538
539 assert!(
540 !cs.is_satisfied(),
541 "constraint multiplication overflow must fail closed instead of wrapping"
542 );
543 }
544
545 #[test]
546 fn variable_without_value() {
547 let mut cs = ConstraintSystem::new();
548 let _v = allocate(&mut cs, None);
549 assert_eq!(cs.variables[0].value, None);
550 }
551
552 #[test]
553 fn constraint_clone_eq() {
554 let c = Constraint {
555 a_terms: LinearCombination::constant(1),
556 b_terms: LinearCombination::constant(2),
557 c_terms: LinearCombination::constant(2),
558 };
559 let c2 = c.clone();
560 assert_eq!(c, c2);
561 }
562}