commonware_cryptography/zk/circuit.rs
1//! Utilities for creating arithmetic circuits.
2//!
3//! A [`Circuit`] holds the additions and multiplications making up a
4//! computation over a ring `F`, along with assertions that two values in
5//! the computation are equal. The inputs to the computation are constants,
6//! or witnesses, whose values are chosen by the prover. Proof systems
7//! consume circuits to prove that the assertions hold, without revealing
8//! the witnesses.
9//!
10//! Circuits are built by writing plain Rust over [`Var`], which implements
11//! the algebra traits from [`commonware_math`]. This allows the same code to
12//! be generic over `F` and `Var<F>`. The building code runs in one of two
13//! modes:
14//!
15//! - [`build`] records only the circuit itself (verifier mode),
16//! - [`build_with_values`] also computes every value in the computation as
17//! the circuit is constructed (prover mode).
18//!
19//! Because both modes run the same code, the prover and the verifier
20//! construct the same circuit.
21//!
22//! # Example
23//!
24//! ```
25//! use commonware_cryptography::zk::circuit::{build_with_values, Var};
26//! use commonware_math::test::F;
27//!
28//! // Constrain a witness `x` to satisfy `x^3 + x + 5 = 35`.
29//! let (valued, _) = build_with_values(|ctx| {
30//! let x = Var::witness(ctx, |_| F::from(3u64));
31//! let out = x.clone() * &x * &x + &x + &Var::constant(ctx, F::from(5u64));
32//! out.assert_eq(&Var::constant(ctx, F::from(35u64)));
33//! Vec::new()
34//! });
35//! assert!(valued.is_satisfied());
36//! ```
37//!
38//! # Caveats
39//!
40//! ## Witness Closures
41//!
42//! The `init` closure passed to [`Var::witness`] must not use the
43//! [`Context`], for example by creating new vars: the build deadlocks,
44//! hanging without an error. Compute the witness value using only the
45//! [`Values`] view the closure receives.
46//!
47//! ## Inversion
48//!
49//! [`Field::inv`] requires that inverting zero produce zero. Circuit-backed
50//! vars deviate: inverting zero adds an unsatisfiable constraint instead,
51//! with no error when building. Generic code relying on `inv(0) = 0` will
52//! produce circuits that can never be satisfied.
53
54use commonware_math::algebra::{Additive, Field, Multiplicative, Object, Ring};
55use commonware_utils::sync::Mutex;
56use std::{
57 fmt,
58 marker::PhantomData,
59 ops::{
60 Add, AddAssign, BitAnd, BitOr, Div, DivAssign, Index, Mul, MulAssign, Neg, Not, Sub,
61 SubAssign,
62 },
63};
64
65/// Identifies a value in a [`Circuit`]: a constant, a witness, or the
66/// output of an operation.
67///
68/// Witnesses are numbered in allocation order, letting callers name
69/// specific witnesses after building, for example to choose which values a
70/// proof system should commit to.
71#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
72pub enum CircuitIdx {
73 Constant(u32),
74 Witness(u32),
75 Node(u32),
76}
77
78/// An addition or multiplication of two earlier values.
79pub(crate) enum CircuitNode {
80 Add(CircuitIdx, CircuitIdx),
81 Mul(CircuitIdx, CircuitIdx),
82}
83
84/// An arithmetic circuit over `F`.
85///
86/// Create one with [`build`] or [`build_with_values`]. On its own, a
87/// circuit only describes constraints; proving and verifying that they hold
88/// is the job of a proof system consuming it.
89//
90// Exposing the structure of the circuit directly (here and in CircuitNode
91// and ValuedCircuit) is not ideal: proof systems would be better served by
92// an abstraction over it. We should wait until we have a few different
93// backends before designing one, so that we don't freeze an abstraction
94// that won't work for all of our use cases. In the meantime, exposing the
95// structure at the crate pub level is not harmful.
96pub struct Circuit<F> {
97 pub(crate) witnesses: u32,
98 pub(crate) constants: Vec<F>,
99 pub(crate) nodes: Vec<CircuitNode>,
100 pub(crate) assertions: Vec<(CircuitIdx, CircuitIdx)>,
101}
102
103impl<F> Default for Circuit<F> {
104 fn default() -> Self {
105 Self {
106 witnesses: 0,
107 constants: Vec::new(),
108 nodes: Vec::new(),
109 assertions: Vec::new(),
110 }
111 }
112}
113
114impl<F> Circuit<F> {
115 const fn next_witness(&mut self) -> CircuitIdx {
116 let next = CircuitIdx::Witness(self.witnesses);
117 self.witnesses += 1;
118 next
119 }
120
121 fn next_constant(&mut self, x: F) -> CircuitIdx {
122 let next = CircuitIdx::Constant(self.constants.len() as u32);
123 self.constants.push(x);
124 next
125 }
126
127 fn next_node(&mut self, n: CircuitNode) -> CircuitIdx {
128 let next = CircuitIdx::Node(self.nodes.len() as u32);
129 self.nodes.push(n);
130 next
131 }
132}
133
134/// A circuit together with concrete values for its whole computation.
135///
136/// Produced by [`build_with_values`]. Use [`Self::is_satisfied`] to check
137/// whether the values satisfy the circuit's assertions.
138pub struct ValuedCircuit<F> {
139 pub(crate) circuit: Circuit<F>,
140 pub(crate) witnesses: Vec<F>,
141 pub(crate) nodes: Vec<F>,
142}
143
144#[doc(hidden)]
145impl<F> Index<CircuitIdx> for ValuedCircuit<F> {
146 type Output = F;
147
148 fn index(&self, index: CircuitIdx) -> &Self::Output {
149 match index {
150 CircuitIdx::Constant(i) => &self.circuit.constants[i as usize],
151 CircuitIdx::Witness(i) => &self.witnesses[i as usize],
152 CircuitIdx::Node(i) => &self.nodes[i as usize],
153 }
154 }
155}
156
157impl<F: PartialEq> ValuedCircuit<F> {
158 /// Checks whether the values assigned to this circuit satisfy its assertions.
159 #[must_use]
160 pub fn is_satisfied(&self) -> bool {
161 self.circuit
162 .assertions
163 .iter()
164 .all(|&(a, b)| self[a] == self[b])
165 }
166}
167
168struct ValuesBuilder<F> {
169 witnesses: Vec<F>,
170 nodes: Vec<F>,
171}
172
173/// A view of the values assigned so far during prover-mode construction.
174///
175/// A view is passed to witness `init` closures, which read the values of
176/// earlier vars with [`Var::value`]. This is how a prover supplies values
177/// that are cheaper to verify than to compute with circuit operations, such
178/// as inverses: compute the value natively, then constrain it with
179/// assertions.
180///
181/// Closures receiving a view must not call back into the [`Context`], for
182/// example by creating new vars: doing so deadlocks.
183///
184/// # Example
185///
186/// ```
187/// use commonware_cryptography::zk::circuit::{build_with_values, Var};
188/// use commonware_math::{
189/// algebra::{Field, Ring},
190/// test::F,
191/// };
192///
193/// let (valued, _) = build_with_values(|ctx| {
194/// let x = Var::witness(ctx, |_| F::from(3u64));
195/// // The prover computes the inverse natively...
196/// let inv = Var::witness(ctx, {
197/// let x = x.clone();
198/// move |v| x.value(v).inv()
199/// });
200/// // ...and the circuit checks it with a single multiplication.
201/// (x * &inv).assert_eq(&Var::one());
202/// Vec::new()
203/// });
204/// assert!(valued.is_satisfied());
205/// ```
206pub struct Values<'a, F> {
207 constants: &'a [F],
208 witnesses: &'a [F],
209 nodes: &'a [F],
210}
211
212// Manual `Copy`/`Clone` so they hold for any `F`: the derived versions would
213// add a spurious `F: Copy`/`F: Clone` bound, but `Values` only holds slices.
214impl<F> Clone for Values<'_, F> {
215 fn clone(&self) -> Self {
216 *self
217 }
218}
219impl<F> Copy for Values<'_, F> {}
220
221#[doc(hidden)]
222impl<'a, F> Index<CircuitIdx> for Values<'a, F> {
223 type Output = F;
224
225 fn index(&self, index: CircuitIdx) -> &Self::Output {
226 match index {
227 CircuitIdx::Witness(id) => &self.witnesses[id as usize],
228 CircuitIdx::Constant(id) => &self.constants[id as usize],
229 CircuitIdx::Node(id) => &self.nodes[id as usize],
230 }
231 }
232}
233
234struct ContextInner<F> {
235 values: Option<Mutex<ValuesBuilder<F>>>,
236 circuit: Mutex<Circuit<F>>,
237}
238
239/// A handle for recording operations into a circuit being built.
240///
241/// A context is passed to the closure given to [`build`] or
242/// [`build_with_values`], and is captured by the [`Var`]s created from it.
243/// Contexts are `Copy`, so they can be passed around freely; vars from two
244/// different builds cannot be mixed.
245pub struct Context<'ctx, F> {
246 inner: &'ctx ContextInner<F>,
247 /// Make this struct invariant in 'ctx, so two Contexts from different
248 /// `build` calls have incompatible types.
249 _brand: PhantomData<fn(&'ctx ()) -> &'ctx ()>,
250}
251
252impl<F> Clone for Context<'_, F> {
253 fn clone(&self) -> Self {
254 *self
255 }
256}
257impl<F> Copy for Context<'_, F> {}
258
259impl<'ctx, F> Context<'ctx, F> {
260 fn allocate_constant(self, combine: impl Fn(&[F]) -> F) -> CircuitIdx {
261 let mut circuit = self.inner.circuit.lock();
262 let combined = combine(&circuit.constants);
263 circuit.next_constant(combined)
264 }
265
266 fn allocate(
267 self,
268 init: impl for<'a> FnOnce(Values<'a, F>) -> Option<F>,
269 reserve: impl FnOnce(&mut Circuit<F>) -> CircuitIdx,
270 ) -> CircuitIdx {
271 // Both locks are held while `init` runs, so an `init` closure that
272 // calls back into the Context deadlocks. This is why Values forbids
273 // doing so.
274 let mut circuit = self.inner.circuit.lock();
275 if let Some(values) = &self.inner.values {
276 let mut values = values.lock();
277 let value = init(Values {
278 constants: &circuit.constants,
279 witnesses: &values.witnesses,
280 nodes: &values.nodes,
281 });
282 let idx = reserve(&mut circuit);
283 match idx {
284 CircuitIdx::Witness(_) => {
285 values
286 .witnesses
287 .push(value.expect("witness allocations populate prover assignments"));
288 }
289 CircuitIdx::Node(_) => {
290 values
291 .nodes
292 .push(value.expect("node allocations populate prover assignments"));
293 }
294 CircuitIdx::Constant(_) => {
295 assert!(
296 value.is_none(),
297 "constants do not populate prover assignments"
298 );
299 }
300 }
301 return idx;
302 }
303
304 reserve(&mut circuit)
305 }
306
307 /// Push a node into the circuit. In prover mode, `init` runs with read
308 /// access to the current circuit values so it can compute the node's
309 /// value, which is appended in lockstep with the node.
310 fn node(self, n: CircuitNode, init: impl for<'a> FnOnce(Values<'a, F>) -> F) -> CircuitIdx {
311 self.allocate(|values| Some(init(values)), |circuit| circuit.next_node(n))
312 }
313
314 fn assert_eq(self, a: CircuitIdx, b: CircuitIdx) {
315 self.inner.circuit.lock().assertions.push((a, b));
316 }
317
318 /// Allocate a fresh witness slot. In prover mode, `init` runs with read
319 /// access to the current circuit values to compute the witness value.
320 fn witness(self, init: impl for<'a> FnOnce(Values<'a, F>) -> F) -> CircuitIdx {
321 self.allocate(|values| Some(init(values)), Circuit::next_witness)
322 }
323}
324
325impl<'ctx, F> Context<'ctx, F> {
326 fn constant(self, x: F) -> CircuitIdx {
327 self.allocate(|_| None, |circuit| circuit.next_constant(x))
328 }
329}
330
331#[derive(Clone)]
332enum VarInner<'ctx, F> {
333 Native(F),
334 Circuit {
335 ctx: Context<'ctx, F>,
336 idx: CircuitIdx,
337 },
338}
339
340/// A value in a circuit being built.
341///
342/// Vars are created with [`Self::witness`] and [`Self::constant`], combined
343/// with the usual arithmetic operators, and constrained with
344/// [`Self::assert_eq`]. Vars implement the algebra traits from
345/// [`commonware_math`], so code written against [`Ring`] or [`Field`] runs
346/// unchanged over circuit values.
347///
348/// Values produced by [`Additive::zero`] and [`Ring::one`] are "native":
349/// they live outside the circuit until combined with a circuit value. This
350/// is visible in two places: equality compares what vars refer to, not what
351/// they evaluate to (a native var is never equal to a circuit-backed var,
352/// even when their values agree), and [`Self::assert_eq`] panics on two
353/// unequal native vars. Generic code that branches on equality may
354/// therefore behave differently over vars than over plain values.
355#[derive(Clone)]
356pub struct Var<'ctx, F> {
357 inner: VarInner<'ctx, F>,
358}
359
360impl<F: fmt::Debug> fmt::Debug for Var<'_, F> {
361 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362 match &self.inner {
363 VarInner::Native(value) => f.debug_tuple("Native").field(value).finish(),
364 VarInner::Circuit { ctx, idx } => {
365 let ctx_ptr = ctx.inner as *const ContextInner<F>;
366 f.debug_struct("Circuit")
367 .field("ctx", &ctx_ptr)
368 .field("idx", idx)
369 .finish()
370 }
371 }
372 }
373}
374
375impl<F: PartialEq> PartialEq for Var<'_, F> {
376 fn eq(&self, other: &Self) -> bool {
377 match (&self.inner, &other.inner) {
378 (VarInner::Native(a), VarInner::Native(b)) => a == b,
379 (VarInner::Circuit { idx: a_idx, .. }, VarInner::Circuit { idx: b_idx, .. }) => {
380 a_idx == b_idx
381 }
382 _ => false,
383 }
384 }
385}
386
387impl<F: Eq> Eq for Var<'_, F> {}
388
389impl<'ctx, F> Var<'ctx, F> {
390 /// Allocate a fresh witness.
391 ///
392 /// In prover mode, `init` receives the values assigned so far, and must
393 /// return the value of this witness. In verifier mode, `init` does not
394 /// run.
395 ///
396 /// `init` must not use the [`Context`]: doing so deadlocks. See
397 /// [`Values`].
398 pub fn witness(ctx: Context<'ctx, F>, init: impl for<'a> FnOnce(Values<'a, F>) -> F) -> Self {
399 Self {
400 inner: VarInner::Circuit {
401 ctx,
402 idx: ctx.witness(init),
403 },
404 }
405 }
406
407 /// Create a "native" var holding a value outside any circuit.
408 ///
409 /// Like the vars produced by [`Additive::zero`] and [`Ring::one`], a
410 /// native var lives outside the circuit until it is combined with a
411 /// circuit-backed var, at which point it is folded in as a constant. This
412 /// is convenient for fixed constants (such as curve parameters) that are
413 /// the same in every circuit and so do not need a [`Context`] to create.
414 pub const fn native(value: F) -> Self {
415 Self {
416 inner: VarInner::Native(value),
417 }
418 }
419
420 /// Create a var with a fixed, public value.
421 pub fn constant(ctx: Context<'ctx, F>, value: F) -> Self {
422 Self {
423 inner: VarInner::Circuit {
424 ctx,
425 idx: ctx.constant(value),
426 },
427 }
428 }
429
430 /// Assert that this var equals `other`.
431 ///
432 /// The constraint must hold for the circuit to be satisfied.
433 ///
434 /// # Panics
435 ///
436 /// Panics if both vars are native and their values differ, since there
437 /// is no circuit to record the failure in.
438 pub fn assert_eq(&self, other: &Self)
439 where
440 F: Clone + PartialEq,
441 {
442 match (&self.inner, &other.inner) {
443 (VarInner::Native(a), VarInner::Native(b)) => {
444 assert!(a == b, "asserted equality between distinct native vars");
445 }
446 (VarInner::Native(a), VarInner::Circuit { ctx, idx })
447 | (VarInner::Circuit { ctx, idx }, VarInner::Native(a)) => {
448 ctx.assert_eq(Self::constant(*ctx, a.clone()).circuit_idx(), *idx);
449 }
450 (VarInner::Circuit { ctx, idx: a }, VarInner::Circuit { idx: b, .. }) => {
451 ctx.assert_eq(*a, *b);
452 }
453 }
454 }
455
456 fn circuit_idx(&self) -> CircuitIdx {
457 match self.inner {
458 VarInner::Circuit { idx, .. } => idx,
459 VarInner::Native(_) => panic!("expected circuit-backed var"),
460 }
461 }
462}
463
464impl<'ctx, F: Clone> Var<'ctx, F> {
465 /// The value of this var, under a prover-mode assignment.
466 pub fn value(&self, values: Values<'_, F>) -> F {
467 match &self.inner {
468 VarInner::Native(value) => value.clone(),
469 VarInner::Circuit { idx, .. } => values[*idx].clone(),
470 }
471 }
472
473 /// Combine `self` and `other` with a commutative binary operation.
474 ///
475 /// `combine` is the value-level operation used both for the all-native case
476 /// and for prover-mode node evaluation. `node` is the circuit node
477 /// constructor (for example `CircuitNode::Add` or `CircuitNode::Mul`).
478 fn merge(
479 self,
480 other: &Self,
481 combine: impl Fn(&F, &F) -> F,
482 node: fn(CircuitIdx, CircuitIdx) -> CircuitNode,
483 ) -> Self {
484 let (ctx, a_idx, b_idx) = match (self.inner, &other.inner) {
485 (VarInner::Native(a), VarInner::Native(b)) => {
486 return Self {
487 inner: VarInner::Native(combine(&a, b)),
488 }
489 }
490 (VarInner::Native(ref a), &VarInner::Circuit { ctx, idx: b_idx })
491 | (VarInner::Circuit { ctx, idx: b_idx }, &VarInner::Native(ref a)) => {
492 (ctx, Self::constant(ctx, a.clone()).circuit_idx(), b_idx)
493 }
494 (VarInner::Circuit { ctx, idx: a }, &VarInner::Circuit { idx: b, .. }) => (ctx, a, b),
495 };
496 if let (CircuitIdx::Constant(a_idx), CircuitIdx::Constant(b_idx)) = (a_idx, b_idx) {
497 return Self {
498 inner: VarInner::Circuit {
499 ctx,
500 idx: ctx.allocate_constant(|constants| {
501 combine(&constants[a_idx as usize], &constants[b_idx as usize])
502 }),
503 },
504 };
505 }
506 let new_idx = ctx.node(node(a_idx, b_idx), move |v| combine(&v[a_idx], &v[b_idx]));
507 Self {
508 inner: VarInner::Circuit { ctx, idx: new_idx },
509 }
510 }
511}
512
513impl<'ctx, F: Object> Object for Var<'ctx, F> {}
514
515impl<'ctx, F: Additive> Add<&Self> for Var<'ctx, F> {
516 type Output = Self;
517 fn add(self, rhs: &Self) -> Self {
518 self.merge(rhs, |a, b| a.clone() + b, CircuitNode::Add)
519 }
520}
521
522impl<'ctx, F: Additive> AddAssign<&Self> for Var<'ctx, F> {
523 fn add_assign(&mut self, rhs: &Self) {
524 *self = self.clone() + rhs;
525 }
526}
527
528impl<'ctx, F: Additive + Ring> Neg for Var<'ctx, F> {
529 type Output = Self;
530 fn neg(self) -> Self {
531 match self.inner {
532 VarInner::Native(a) => Self {
533 inner: VarInner::Native(-a),
534 },
535 VarInner::Circuit {
536 ctx,
537 idx: CircuitIdx::Constant(idx),
538 } => Self {
539 inner: VarInner::Circuit {
540 ctx,
541 idx: ctx.allocate_constant(|constants| -constants[idx as usize].clone()),
542 },
543 },
544 VarInner::Circuit { ctx, idx } => {
545 let minus_one = Var::constant(ctx, -F::one()).circuit_idx();
546 let new_idx = ctx.node(CircuitNode::Mul(minus_one, idx), move |v| -v[idx].clone());
547 Self {
548 inner: VarInner::Circuit { ctx, idx: new_idx },
549 }
550 }
551 }
552 }
553}
554
555impl<'ctx, F: Additive + Ring> Sub<&Self> for Var<'ctx, F> {
556 type Output = Self;
557 fn sub(self, rhs: &Self) -> Self {
558 self + &(-rhs.clone())
559 }
560}
561
562impl<'ctx, F: Additive + Ring> SubAssign<&Self> for Var<'ctx, F> {
563 fn sub_assign(&mut self, rhs: &Self) {
564 *self = self.clone() - rhs;
565 }
566}
567
568impl<'ctx, F: Multiplicative> Mul<&Self> for Var<'ctx, F> {
569 type Output = Self;
570 fn mul(self, rhs: &Self) -> Self {
571 self.merge(rhs, |a, b| a.clone() * b, CircuitNode::Mul)
572 }
573}
574
575impl<'ctx, F: Multiplicative> MulAssign<&Self> for Var<'ctx, F> {
576 fn mul_assign(&mut self, rhs: &Self) {
577 *self = self.clone() * rhs;
578 }
579}
580
581/// Division by `rhs`, computed as a single multiplication constraint.
582///
583/// Rather than inverting `rhs` and multiplying (which costs two
584/// multiplications), the prover supplies the quotient `q = self / rhs` as a
585/// witness and the circuit constrains `q * rhs == self`.
586///
587/// # Caveats
588///
589/// Like [`Field::inv`] on a circuit-backed var, this deviates from the
590/// `inv(0) = 0` field contract: dividing by a circuit-backed `rhs` of zero
591/// adds an unsatisfiable constraint when `self != 0`. Worse, `0 / 0`
592/// constrains `q * 0 == 0`, which holds for *any* `q`, leaving the quotient
593/// unconstrained. Only use `/` where `rhs` is known to be nonzero.
594impl<'ctx, F: Field> Div<&Self> for Var<'ctx, F> {
595 type Output = Self;
596
597 fn div(self, rhs: &Self) -> Self {
598 let &ctx = match (&self.inner, &rhs.inner) {
599 (VarInner::Native(a), VarInner::Native(b)) => {
600 return Self {
601 inner: VarInner::Native(a.clone() * &b.inv()),
602 }
603 }
604 (VarInner::Circuit { ctx, .. }, _) | (_, VarInner::Circuit { ctx, .. }) => ctx,
605 };
606 let q = { Self::witness(ctx, |v| self.value(v) * &rhs.value(v).inv()) };
607 (q.clone() * rhs).assert_eq(&self);
608 q
609 }
610}
611
612impl<'ctx, F: Field> DivAssign<&Self> for Var<'ctx, F> {
613 fn div_assign(&mut self, rhs: &Self) {
614 *self = self.clone() / rhs;
615 }
616}
617
618impl<'ctx, F: Additive + Ring> Additive for Var<'ctx, F> {
619 fn zero() -> Self {
620 Self {
621 inner: VarInner::Native(F::zero()),
622 }
623 }
624}
625
626impl<'ctx, F: Multiplicative> Multiplicative for Var<'ctx, F> {}
627
628impl<'ctx, F: Ring> Ring for Var<'ctx, F> {
629 fn one() -> Self {
630 Self {
631 inner: VarInner::Native(F::one()),
632 }
633 }
634}
635
636/// Unlike the [`Field::inv`] contract, inverting a circuit-backed zero does
637/// not produce zero: it adds an unsatisfiable constraint to the circuit.
638impl<'ctx, F: Field> Field for Var<'ctx, F> {
639 fn inv(&self) -> Self {
640 match &self.inner {
641 VarInner::Native(c) => Self {
642 inner: VarInner::Native(c.inv()),
643 },
644 &VarInner::Circuit { ctx, .. } => {
645 // Prover supplies the inverse via the oracle; verifier just
646 // allocates the slot.
647 let inv = Self::witness(ctx, |v| self.value(v).inv());
648 (inv.clone() * self).assert_eq(&Self::one());
649 inv
650 }
651 }
652 }
653}
654
655/// A circuit value constrained to be `0` or `1`.
656///
657/// A `BoolVar` wraps a [`Var`] together with a guarantee that it holds a
658/// boolean: every constructor either produces a value that is boolean by
659/// construction, or adds the constraint `b * (1 - b) == 0` enforcing it.
660/// Holding that guarantee in the type lets later operations skip
661/// re-checking: [`Self::select`] and the boolean combinators below are sound
662/// precisely because their inputs are already known to be boolean.
663///
664/// The motivating use is scalar multiplication in a circuit, where a scalar
665/// is decomposed into bits (each a `BoolVar`) and a point is accumulated by
666/// conditionally adding with [`Self::select`].
667///
668/// # Native Vars
669///
670/// Like [`Var`], a `BoolVar` built from a native value (see
671/// [`Self::constant`]) lives outside the circuit until combined with a
672/// circuit-backed value. [`Self::assert`] on a native, non-boolean var
673/// therefore panics rather than recording an unsatisfiable constraint, in
674/// keeping with the native-var semantics described on [`Var`].
675///
676/// # Example
677///
678/// ```
679/// use commonware_cryptography::zk::circuit::{build_with_values, BoolVar, Var};
680/// use commonware_math::test::F;
681///
682/// // Use a bit to choose between two values, then check the choice.
683/// let (valued, _) = build_with_values(|ctx| {
684/// let bit = BoolVar::witness(ctx, |_| true);
685/// let a = Var::constant(ctx, F::from(7u64));
686/// let b = Var::constant(ctx, F::from(9u64));
687/// bit.select(&a, &b).assert_eq(&Var::constant(ctx, F::from(7u64)));
688/// Vec::new()
689/// });
690/// assert!(valued.is_satisfied());
691/// ```
692#[derive(Clone)]
693pub struct BoolVar<'ctx, F> {
694 var: Var<'ctx, F>,
695}
696
697impl<F: fmt::Debug> fmt::Debug for BoolVar<'_, F> {
698 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
699 f.debug_tuple("BoolVar").field(&self.var).finish()
700 }
701}
702
703impl<'ctx, F> BoolVar<'ctx, F> {
704 /// The underlying [`Var`], whose value is `0` or `1`.
705 pub const fn var(&self) -> &Var<'ctx, F> {
706 &self.var
707 }
708
709 /// Consume this `BoolVar`, returning the underlying [`Var`].
710 #[allow(clippy::missing_const_for_fn)]
711 pub fn into_var(self) -> Var<'ctx, F> {
712 self.var
713 }
714}
715
716impl<'ctx, F: Ring> BoolVar<'ctx, F> {
717 /// Create a boolean constant with a fixed, public value.
718 ///
719 /// Like [`Var::native`], the value lives outside the circuit until it is
720 /// combined with a circuit-backed var. No constraint is added: the value
721 /// is boolean by construction.
722 pub fn constant(value: bool) -> Self {
723 Self {
724 var: Var::native(if value { F::one() } else { F::zero() }),
725 }
726 }
727
728 /// Assert that this var equals `other`.
729 pub fn assert_eq(&self, other: &Self) {
730 self.var.assert_eq(other.var());
731 }
732}
733
734impl<'ctx, F: Ring + PartialEq> BoolVar<'ctx, F> {
735 /// Allocate a fresh boolean witness, constrained to be `0` or `1`.
736 ///
737 /// In prover mode, `init` receives the values assigned so far and must
738 /// return the bit's value. In verifier mode, `init` does not run. The
739 /// constraint `b * (1 - b) == 0` is added in both modes.
740 ///
741 /// `init` must not use the [`Context`]: doing so deadlocks. See
742 /// [`Values`].
743 pub fn witness(
744 ctx: Context<'ctx, F>,
745 init: impl for<'a> FnOnce(Values<'a, F>) -> bool,
746 ) -> Self {
747 let var = Var::witness(ctx, |v| if init(v) { F::one() } else { F::zero() });
748 Self::enforce(&var);
749 Self { var }
750 }
751
752 /// Constrain an arbitrary var to be boolean and wrap it.
753 ///
754 /// Adds the constraint `var * (1 - var) == 0`, which holds exactly when
755 /// `var` is `0` or `1`.
756 ///
757 /// # Panics
758 ///
759 /// Panics if `var` is a native var whose value is not `0` or `1`, since
760 /// there is no circuit to record the failed constraint in. See
761 /// [`Var::assert_eq`].
762 pub fn assert(var: Var<'ctx, F>) -> Self {
763 Self::enforce(&var);
764 Self { var }
765 }
766
767 /// Add the booleanity constraint `var * var == var` (equivalently
768 /// `var * (1 - var) == 0`).
769 fn enforce(var: &Var<'ctx, F>) {
770 (var.clone() * var).assert_eq(var);
771 }
772}
773
774impl<'ctx, F: Ring> BoolVar<'ctx, F> {
775 /// Select between two vars based on this bit.
776 ///
777 /// Returns `on_true` when the bit is `1` and `on_false` when it is `0`,
778 /// computed as `on_false + b * (on_true - on_false)` with a single
779 /// multiplication.
780 pub fn select(&self, on_true: &Var<'ctx, F>, on_false: &Var<'ctx, F>) -> Var<'ctx, F> {
781 on_false.clone() + &(self.var.clone() * &(on_true.clone() - on_false))
782 }
783}
784
785impl<'ctx, F: Ring> Not for BoolVar<'ctx, F> {
786 type Output = Self;
787
788 fn not(self) -> Self::Output {
789 Self {
790 var: Var::one() - &self.var,
791 }
792 }
793}
794
795impl<'ctx, F: Ring> BitAnd for BoolVar<'ctx, F> {
796 type Output = Self;
797
798 // Over 0/1, boolean `and` IS multiplication, so `self.var * &rhs.var` is
799 // correct. But clippy's `suspicious_arithmetic_impl` lint assumes any
800 // operator impl that uses a *different* operator internally (here, `*` inside
801 // `BitAnd`) is a copy-paste typo. That heuristic is wrong: it has no notion
802 // of the algebra, so it flags correct code and forces this `#[allow]`. The
803 // lint gives confident, authoritative advice that is simply false, which is
804 // why its designer now resides in the Eighth Circle of Hell,
805 // among the fraudulent counselors (Inferno XXVI) who misused their cleverness
806 // to advise others into error, each concealed within a tongue of flame.
807 #[allow(clippy::suspicious_arithmetic_impl)]
808 fn bitand(self, rhs: Self) -> Self::Output {
809 Self {
810 var: self.var * &rhs.var,
811 }
812 }
813}
814
815impl<'ctx, F: Ring> BitOr for BoolVar<'ctx, F> {
816 type Output = Self;
817
818 // Boolean `or` over 0/1 is `a + b - a * b`, which is correct. Same lint, same
819 // false alarm over the `+`/`-`/`*`, same `#[allow]`. See `bitand` above for
820 // why the lint's designer is doing time in the Eighth Circle of Hell.
821 #[allow(clippy::suspicious_arithmetic_impl)]
822 fn bitor(self, rhs: Self) -> Self::Output {
823 Self {
824 var: self.var.clone() + &rhs.var - &(self.var * &rhs.var),
825 }
826 }
827}
828
829/// A tool for selecting among multiple items.
830///
831/// This is a generalization of using a [`BoolVar`] to select among two items,
832/// letting you use `k` bits to select among `2^k` items.
833pub struct Selector<'ctx, F> {
834 monomials: Vec<Var<'ctx, F>>,
835}
836
837impl<'ctx, F: Ring> Selector<'ctx, F> {
838 /// Create a new selector, using a given number of in-circuit bits.
839 ///
840 /// This selector will then be able to select among exactly `2^k` items,
841 /// where `k` is the number of bits passed in here.
842 ///
843 /// It is more efficient to create one selector and reuse it.
844 ///
845 /// The selection is made in ascending order, i.e. 0..00, 0..01, 0..10, ...
846 /// In other words, if you pass in, e.g. 010 to this function, you then
847 /// will always get the 3rd (index 2) of 8 items.
848 pub fn new(bits: &[BoolVar<'ctx, F>]) -> Self {
849 let mut monomials = vec![Var::one(); 1usize << bits.len()];
850 for mask in 1..monomials.len() {
851 let bit = mask.trailing_zeros() as usize;
852 let prev = mask ^ (1usize << bit);
853 monomials[mask] = if prev == 0 {
854 bits[bit].var().clone()
855 } else {
856 monomials[prev].clone() * bits[bit].var()
857 };
858 }
859 Self { monomials }
860 }
861
862 /// Select a constant value among `2^k` possibilities.
863 pub fn select_constant(&self, constants: &[F]) -> Var<'ctx, F> {
864 assert_eq!(
865 self.monomials.len(),
866 constants.len(),
867 "constants len must match selectors len"
868 );
869
870 let values = {
871 let mut values = constants.to_vec();
872 let len = values.len().trailing_zeros() as usize;
873 for bit in 0..len {
874 for mask in 0..values.len() {
875 if mask & (1usize << bit) != 0 {
876 let prev = values[mask ^ (1usize << bit)].clone();
877 values[mask] -= &prev;
878 }
879 }
880 }
881 values
882 };
883
884 values
885 .into_iter()
886 .zip(&self.monomials)
887 .map(|(v_i, m_i)| Var::native(v_i) * m_i)
888 .reduce(|acc, x| acc + &x)
889 .expect("values is non empty")
890 }
891}
892
893/// Build a circuit without computing an assignment (verifier mode).
894///
895/// Witness `init` closures do not run in this mode.
896///
897/// The closure returns the vars whose circuit indices the caller wants back
898/// (e.g. the committed outputs of a circuit); the returned indices are in the
899/// same order. Return an empty vec to ignore this.
900///
901/// # Panics
902///
903/// Panics if any returned var is native (not backed by the circuit).
904pub fn build<F: Ring + PartialEq>(
905 f: impl for<'ctx> FnOnce(Context<'ctx, F>) -> Vec<Var<'ctx, F>>,
906) -> (Circuit<F>, Vec<CircuitIdx>) {
907 let inner = ContextInner {
908 values: None,
909 circuit: Mutex::new(Circuit::default()),
910 };
911 let indices = f(Context {
912 inner: &inner,
913 _brand: PhantomData,
914 })
915 .iter()
916 .map(Var::circuit_idx)
917 .collect();
918 (inner.circuit.into_inner(), indices)
919}
920
921/// Build a circuit while simultaneously computing the assignment (prover mode).
922///
923/// Each witness's value comes from the `init` closure passed to
924/// [`Var::witness`].
925///
926/// The closure returns the vars whose circuit indices the caller wants back
927/// (e.g. the committed outputs of a circuit); the returned indices are in the
928/// same order. Return an empty vec to ignore this.
929///
930/// # Panics
931///
932/// Panics if any returned var is native (not backed by the circuit).
933pub fn build_with_values<F: Ring + PartialEq>(
934 f: impl for<'ctx> FnOnce(Context<'ctx, F>) -> Vec<Var<'ctx, F>>,
935) -> (ValuedCircuit<F>, Vec<CircuitIdx>) {
936 let inner = ContextInner {
937 values: Some(Mutex::new(ValuesBuilder {
938 witnesses: Vec::new(),
939 nodes: Vec::new(),
940 })),
941 circuit: Mutex::new(Circuit::default()),
942 };
943 let indices = f(Context {
944 inner: &inner,
945 _brand: PhantomData,
946 })
947 .iter()
948 .map(Var::circuit_idx)
949 .collect();
950 let circuit = inner.circuit.into_inner();
951 let values = inner.values.unwrap().into_inner();
952 (
953 ValuedCircuit {
954 circuit,
955 witnesses: values.witnesses,
956 nodes: values.nodes,
957 },
958 indices,
959 )
960}
961
962/// Fuzzing utilities, comparing circuit satisfaction against native
963/// evaluation of the same operations.
964#[commonware_macros::stability(ALPHA)]
965#[cfg(any(test, feature = "fuzz"))]
966pub mod fuzz {
967 use super::*;
968 use arbitrary::{Arbitrary, Unstructured};
969 use commonware_math::test::F;
970
971 #[derive(Debug)]
972 enum Op {
973 Witness(F),
974 Constant(F),
975 Zero,
976 One,
977 Add(usize, usize),
978 Sub(usize, usize),
979 Mul(usize, usize),
980 Neg(usize),
981 Inv(usize),
982 AssertEq(usize, usize),
983 }
984
985 /// A random sequence of circuit operations, together with whether the
986 /// circuit they build should be satisfied.
987 ///
988 /// Values are drawn from a small range so that assertions have a decent
989 /// chance of holding, exercising both outcomes.
990 #[derive(Debug)]
991 pub struct Plan {
992 ops: Vec<Op>,
993 satisfied: bool,
994 }
995
996 impl Arbitrary<'_> for Plan {
997 fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result<Self> {
998 let mut ops = Vec::new();
999 let mut values: Vec<F> = Vec::new();
1000 let mut is_native: Vec<bool> = Vec::new();
1001 let mut satisfied = true;
1002 for _ in 0..u.int_in_range(1..=32)? {
1003 let kind = if values.is_empty() {
1004 u.int_in_range(0..=3)?
1005 } else {
1006 u.int_in_range(0..=9)?
1007 };
1008 if kind <= 3 {
1009 let v = F::from(u.int_in_range::<u8>(0..=4)?);
1010 let (op, value, native) = match kind {
1011 0 => (Op::Witness(v), v, false),
1012 1 => (Op::Constant(v), v, false),
1013 2 => (Op::Zero, F::zero(), true),
1014 _ => (Op::One, F::one(), true),
1015 };
1016 ops.push(op);
1017 values.push(value);
1018 is_native.push(native);
1019 continue;
1020 }
1021 let a = u.int_in_range(0..=values.len() - 1)?;
1022 let b = u.int_in_range(0..=values.len() - 1)?;
1023 let merged = is_native[a] && is_native[b];
1024 let (op, value, native) = match kind {
1025 4 => (Op::Add(a, b), values[a] + &values[b], merged),
1026 5 => (Op::Sub(a, b), values[a] - &values[b], merged),
1027 6 => (Op::Mul(a, b), values[a] * &values[b], merged),
1028 7 => (Op::Neg(a), -values[a], is_native[a]),
1029 8 => {
1030 // Inverting a circuit-backed zero constrains z * 0 = 1,
1031 // which is unsatisfiable. Native vars add no constraint.
1032 if !is_native[a] && values[a] == F::zero() {
1033 satisfied = false;
1034 }
1035 (Op::Inv(a), values[a].inv(), is_native[a])
1036 }
1037 _ => {
1038 // Asserting equality between two unequal native vars is
1039 // a panic by design, so the plan must avoid it.
1040 if merged && values[a] != values[b] {
1041 continue;
1042 }
1043 ops.push(Op::AssertEq(a, b));
1044 satisfied = satisfied && values[a] == values[b];
1045 continue;
1046 }
1047 };
1048 ops.push(op);
1049 values.push(value);
1050 is_native.push(native);
1051 }
1052 Ok(Self { ops, satisfied })
1053 }
1054 }
1055
1056 impl Plan {
1057 /// Check that satisfaction matches the natively computed expectation.
1058 pub fn run(self, _u: &mut Unstructured<'_>) -> arbitrary::Result<()> {
1059 assert_eq!(
1060 self.build().is_satisfied(),
1061 self.satisfied(),
1062 "plan: {self:?}"
1063 );
1064 Ok(())
1065 }
1066
1067 /// Whether the circuit built by [`Self::build`] should be satisfied.
1068 pub const fn satisfied(&self) -> bool {
1069 self.satisfied
1070 }
1071
1072 /// Build the circuit, along with its prover assignment.
1073 pub fn build(&self) -> ValuedCircuit<F> {
1074 build_with_values(|ctx| {
1075 let mut vars: Vec<Var<'_, F>> = Vec::new();
1076 for op in &self.ops {
1077 let var = match *op {
1078 Op::Witness(v) => Var::witness(ctx, move |_| v),
1079 Op::Constant(v) => Var::constant(ctx, v),
1080 Op::Zero => Var::zero(),
1081 Op::One => Var::one(),
1082 Op::Add(a, b) => vars[a].clone() + &vars[b],
1083 Op::Sub(a, b) => vars[a].clone() - &vars[b],
1084 Op::Mul(a, b) => vars[a].clone() * &vars[b],
1085 Op::Neg(a) => -vars[a].clone(),
1086 Op::Inv(a) => vars[a].inv(),
1087 Op::AssertEq(a, b) => {
1088 vars[a].assert_eq(&vars[b]);
1089 continue;
1090 }
1091 };
1092 vars.push(var);
1093 }
1094 Vec::new()
1095 })
1096 .0
1097 }
1098 }
1099}
1100
1101#[cfg(test)]
1102mod tests {
1103 use super::*;
1104 use commonware_invariants::minifuzz;
1105 use commonware_math::test::F;
1106
1107 #[test]
1108 fn test_is_satisfied_matches_native_evaluation_minifuzz() {
1109 minifuzz::test(|u| u.arbitrary::<fuzz::Plan>()?.run(u));
1110 }
1111
1112 #[test]
1113 fn test_is_satisfied_cubic() {
1114 // Constrain a witness `x` to satisfy `x^3 + x + 5 = 35`.
1115 let cubic = |x_value: u64| {
1116 build_with_values(move |ctx| {
1117 let x = Var::witness(ctx, move |_| F::from(x_value));
1118 let out = x.clone() * &x * &x + &x + &Var::constant(ctx, F::from(5u64));
1119 out.assert_eq(&Var::constant(ctx, F::from(35u64)));
1120 Vec::new()
1121 })
1122 .0
1123 };
1124 assert!(cubic(3).is_satisfied());
1125 assert!(!cubic(4).is_satisfied());
1126 }
1127
1128 #[test]
1129 fn test_bool_witness_enforces_booleanity() {
1130 // A boolean witness from a `bool` is always satisfiable.
1131 for b in [false, true] {
1132 let (valued, _) = build_with_values(move |ctx| {
1133 BoolVar::<F>::witness(ctx, move |_| b);
1134 Vec::new()
1135 });
1136 assert!(valued.is_satisfied());
1137 }
1138
1139 // A var that is not 0 or 1 fails the booleanity constraint.
1140 let (bad, _) = build_with_values(|ctx| {
1141 let two = Var::witness(ctx, |_| F::from(2u64));
1142 BoolVar::assert(two);
1143 Vec::new()
1144 });
1145 assert!(!bad.is_satisfied());
1146
1147 // 0 and 1 pass `from_var`.
1148 for v in [0u64, 1u64] {
1149 let (valued, _) = build_with_values(move |ctx| {
1150 let x = Var::witness(ctx, move |_| F::from(v));
1151 BoolVar::assert(x);
1152 Vec::new()
1153 });
1154 assert!(valued.is_satisfied());
1155 }
1156 }
1157
1158 #[test]
1159 fn test_bool_select() {
1160 // `select` returns `on_true` when the bit is set, else `on_false`.
1161 for b in [false, true] {
1162 let (valued, _) = build_with_values(move |ctx| {
1163 let bit = BoolVar::witness(ctx, move |_| b);
1164 let on_true = Var::witness(ctx, |_| F::from(7u64));
1165 let on_false = Var::witness(ctx, |_| F::from(9u64));
1166 let selected = bit.select(&on_true, &on_false);
1167 let expected = if b { F::from(7u64) } else { F::from(9u64) };
1168 selected.assert_eq(&Var::constant(ctx, expected));
1169 Vec::new()
1170 });
1171 assert!(valued.is_satisfied());
1172 }
1173 }
1174
1175 #[test]
1176 fn test_bool_combinators_truth_tables() {
1177 for a in [false, true] {
1178 for b in [false, true] {
1179 let (valued, _) = build_with_values::<F>(move |ctx| {
1180 let a_var = BoolVar::witness(ctx, move |_| a);
1181 let b_var = BoolVar::witness(ctx, move |_| b);
1182 (!a_var.clone()).assert_eq(&BoolVar::constant(!a));
1183 (a_var.clone() & b_var.clone()).assert_eq(&BoolVar::constant(a & b));
1184 (a_var | b_var).assert_eq(&BoolVar::constant(a | b));
1185 Vec::new()
1186 });
1187 assert!(valued.is_satisfied());
1188 }
1189 }
1190 }
1191
1192 #[test]
1193 fn test_bool_constant() {
1194 let (valued, _) = build_with_values(|ctx| {
1195 // A native boolean constant folds in correctly when combined.
1196 let t = BoolVar::<F>::constant(true);
1197 let f = BoolVar::<F>::constant(false);
1198 let x = Var::witness(ctx, |_| F::from(5u64));
1199 t.select(&x, &Var::zero())
1200 .assert_eq(&Var::constant(ctx, F::from(5u64)));
1201 f.select(&x, &Var::zero()).assert_eq(&Var::zero());
1202 Vec::new()
1203 });
1204 assert!(valued.is_satisfied());
1205 }
1206}