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 fn constant(self, x: F) -> CircuitIdx {
325 self.allocate(|_| None, |circuit| circuit.next_constant(x))
326 }
327}
328
329#[derive(Clone)]
330enum VarInner<'ctx, F> {
331 Native(F),
332 Circuit {
333 ctx: Context<'ctx, F>,
334 idx: CircuitIdx,
335 },
336}
337
338/// A value in a circuit being built.
339///
340/// Vars are created with [`Self::witness`] and [`Self::constant`], combined
341/// with the usual arithmetic operators, and constrained with
342/// [`Self::assert_eq`]. Vars implement the algebra traits from
343/// [`commonware_math`], so code written against [`Ring`] or [`Field`] runs
344/// unchanged over circuit values.
345///
346/// Values produced by [`Additive::zero`] and [`Ring::one`] are "native":
347/// they live outside the circuit until combined with a circuit value. This
348/// is visible in two places: equality compares what vars refer to, not what
349/// they evaluate to (a native var is never equal to a circuit-backed var,
350/// even when their values agree), and [`Self::assert_eq`] panics on two
351/// unequal native vars. Generic code that branches on equality may
352/// therefore behave differently over vars than over plain values.
353#[derive(Clone)]
354pub struct Var<'ctx, F> {
355 inner: VarInner<'ctx, F>,
356}
357
358impl<F: fmt::Debug> fmt::Debug for Var<'_, F> {
359 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360 match &self.inner {
361 VarInner::Native(value) => f.debug_tuple("Native").field(value).finish(),
362 VarInner::Circuit { ctx, idx } => {
363 let ctx_ptr = ctx.inner as *const ContextInner<F>;
364 f.debug_struct("Circuit")
365 .field("ctx", &ctx_ptr)
366 .field("idx", idx)
367 .finish()
368 }
369 }
370 }
371}
372
373impl<F: PartialEq> PartialEq for Var<'_, F> {
374 fn eq(&self, other: &Self) -> bool {
375 match (&self.inner, &other.inner) {
376 (VarInner::Native(a), VarInner::Native(b)) => a == b,
377 (VarInner::Circuit { idx: a_idx, .. }, VarInner::Circuit { idx: b_idx, .. }) => {
378 a_idx == b_idx
379 }
380 _ => false,
381 }
382 }
383}
384
385impl<F: Eq> Eq for Var<'_, F> {}
386
387impl<'ctx, F> Var<'ctx, F> {
388 /// Allocate a fresh witness.
389 ///
390 /// In prover mode, `init` receives the values assigned so far, and must
391 /// return the value of this witness. In verifier mode, `init` does not
392 /// run.
393 ///
394 /// `init` must not use the [`Context`]: doing so deadlocks. See
395 /// [`Values`].
396 pub fn witness(ctx: Context<'ctx, F>, init: impl for<'a> FnOnce(Values<'a, F>) -> F) -> Self {
397 Self {
398 inner: VarInner::Circuit {
399 ctx,
400 idx: ctx.witness(init),
401 },
402 }
403 }
404
405 /// Create a "native" var holding a value outside any circuit.
406 ///
407 /// Like the vars produced by [`Additive::zero`] and [`Ring::one`], a
408 /// native var lives outside the circuit until it is combined with a
409 /// circuit-backed var, at which point it is folded in as a constant. This
410 /// is convenient for fixed constants (such as curve parameters) that are
411 /// the same in every circuit and so do not need a [`Context`] to create.
412 pub const fn native(value: F) -> Self {
413 Self {
414 inner: VarInner::Native(value),
415 }
416 }
417
418 /// Create a var with a fixed, public value.
419 pub fn constant(ctx: Context<'ctx, F>, value: F) -> Self {
420 Self {
421 inner: VarInner::Circuit {
422 ctx,
423 idx: ctx.constant(value),
424 },
425 }
426 }
427
428 /// Assert that this var equals `other`.
429 ///
430 /// The constraint must hold for the circuit to be satisfied.
431 ///
432 /// # Panics
433 ///
434 /// Panics if both vars are native and their values differ, since there
435 /// is no circuit to record the failure in.
436 pub fn assert_eq(&self, other: &Self)
437 where
438 F: Clone + PartialEq,
439 {
440 match (&self.inner, &other.inner) {
441 (VarInner::Native(a), VarInner::Native(b)) => {
442 assert!(a == b, "asserted equality between distinct native vars");
443 }
444 (VarInner::Native(a), VarInner::Circuit { ctx, idx })
445 | (VarInner::Circuit { ctx, idx }, VarInner::Native(a)) => {
446 ctx.assert_eq(Self::constant(*ctx, a.clone()).circuit_idx(), *idx);
447 }
448 (VarInner::Circuit { ctx, idx: a }, VarInner::Circuit { idx: b, .. }) => {
449 ctx.assert_eq(*a, *b);
450 }
451 }
452 }
453
454 fn circuit_idx(&self) -> CircuitIdx {
455 match self.inner {
456 VarInner::Circuit { idx, .. } => idx,
457 VarInner::Native(_) => panic!("expected circuit-backed var"),
458 }
459 }
460}
461
462impl<'ctx, F: Clone> Var<'ctx, F> {
463 /// The value of this var, under a prover-mode assignment.
464 pub fn value(&self, values: Values<'_, F>) -> F {
465 match &self.inner {
466 VarInner::Native(value) => value.clone(),
467 VarInner::Circuit { idx, .. } => values[*idx].clone(),
468 }
469 }
470
471 /// Combine `self` and `other` with a commutative binary operation.
472 ///
473 /// `combine` is the value-level operation used both for the all-native case
474 /// and for prover-mode node evaluation. `node` is the circuit node
475 /// constructor (for example `CircuitNode::Add` or `CircuitNode::Mul`).
476 fn merge(
477 self,
478 other: &Self,
479 combine: impl Fn(&F, &F) -> F,
480 node: fn(CircuitIdx, CircuitIdx) -> CircuitNode,
481 ) -> Self {
482 let (ctx, a_idx, b_idx) = match (self.inner, &other.inner) {
483 (VarInner::Native(a), VarInner::Native(b)) => {
484 return Self {
485 inner: VarInner::Native(combine(&a, b)),
486 };
487 }
488 (VarInner::Native(ref a), &VarInner::Circuit { ctx, idx: b_idx })
489 | (VarInner::Circuit { ctx, idx: b_idx }, &VarInner::Native(ref a)) => {
490 (ctx, Self::constant(ctx, a.clone()).circuit_idx(), b_idx)
491 }
492 (VarInner::Circuit { ctx, idx: a }, &VarInner::Circuit { idx: b, .. }) => (ctx, a, b),
493 };
494 if let (CircuitIdx::Constant(a_idx), CircuitIdx::Constant(b_idx)) = (a_idx, b_idx) {
495 return Self {
496 inner: VarInner::Circuit {
497 ctx,
498 idx: ctx.allocate_constant(|constants| {
499 combine(&constants[a_idx as usize], &constants[b_idx as usize])
500 }),
501 },
502 };
503 }
504 let new_idx = ctx.node(node(a_idx, b_idx), move |v| combine(&v[a_idx], &v[b_idx]));
505 Self {
506 inner: VarInner::Circuit { ctx, idx: new_idx },
507 }
508 }
509}
510
511impl<'ctx, F: Object> Object for Var<'ctx, F> {}
512
513impl<'ctx, F: Additive> Add<&Self> for Var<'ctx, F> {
514 type Output = Self;
515 fn add(self, rhs: &Self) -> Self {
516 self.merge(rhs, |a, b| a.clone() + b, CircuitNode::Add)
517 }
518}
519
520impl<'ctx, F: Additive> AddAssign<&Self> for Var<'ctx, F> {
521 fn add_assign(&mut self, rhs: &Self) {
522 *self = self.clone() + rhs;
523 }
524}
525
526impl<'ctx, F: Ring> Neg for Var<'ctx, F> {
527 type Output = Self;
528 fn neg(self) -> Self {
529 match self.inner {
530 VarInner::Native(a) => Self {
531 inner: VarInner::Native(-a),
532 },
533 VarInner::Circuit {
534 ctx,
535 idx: CircuitIdx::Constant(idx),
536 } => Self {
537 inner: VarInner::Circuit {
538 ctx,
539 idx: ctx.allocate_constant(|constants| -constants[idx as usize].clone()),
540 },
541 },
542 VarInner::Circuit { ctx, idx } => {
543 let minus_one = Var::constant(ctx, -F::one()).circuit_idx();
544 let new_idx = ctx.node(CircuitNode::Mul(minus_one, idx), move |v| -v[idx].clone());
545 Self {
546 inner: VarInner::Circuit { ctx, idx: new_idx },
547 }
548 }
549 }
550 }
551}
552
553impl<'ctx, F: Ring> Sub<&Self> for Var<'ctx, F> {
554 type Output = Self;
555 fn sub(self, rhs: &Self) -> Self {
556 self + &(-rhs.clone())
557 }
558}
559
560impl<'ctx, F: Ring> SubAssign<&Self> for Var<'ctx, F> {
561 fn sub_assign(&mut self, rhs: &Self) {
562 *self = self.clone() - rhs;
563 }
564}
565
566impl<'ctx, F: Multiplicative> Mul<&Self> for Var<'ctx, F> {
567 type Output = Self;
568 fn mul(self, rhs: &Self) -> Self {
569 self.merge(rhs, |a, b| a.clone() * b, CircuitNode::Mul)
570 }
571}
572
573impl<'ctx, F: Multiplicative> MulAssign<&Self> for Var<'ctx, F> {
574 fn mul_assign(&mut self, rhs: &Self) {
575 *self = self.clone() * rhs;
576 }
577}
578
579/// Division by `rhs`, computed as a single multiplication constraint.
580///
581/// Rather than inverting `rhs` and multiplying (which costs two
582/// multiplications), the prover supplies the quotient `q = self / rhs` as a
583/// witness and the circuit constrains `q * rhs == self`.
584///
585/// # Caveats
586///
587/// Like [`Field::inv`] on a circuit-backed var, this deviates from the
588/// `inv(0) = 0` field contract: dividing by a circuit-backed `rhs` of zero
589/// adds an unsatisfiable constraint when `self != 0`. Worse, `0 / 0`
590/// constrains `q * 0 == 0`, which holds for *any* `q`, leaving the quotient
591/// unconstrained. Only use `/` where `rhs` is known to be nonzero.
592impl<'ctx, F: Field> Div<&Self> for Var<'ctx, F> {
593 type Output = Self;
594
595 fn div(self, rhs: &Self) -> Self {
596 let &ctx = match (&self.inner, &rhs.inner) {
597 (VarInner::Native(a), VarInner::Native(b)) => {
598 return Self {
599 inner: VarInner::Native(a.clone() * &b.inv()),
600 };
601 }
602 (VarInner::Circuit { ctx, .. }, _) | (_, VarInner::Circuit { ctx, .. }) => ctx,
603 };
604 let q = { Self::witness(ctx, |v| self.value(v) * &rhs.value(v).inv()) };
605 (q.clone() * rhs).assert_eq(&self);
606 q
607 }
608}
609
610impl<'ctx, F: Field> DivAssign<&Self> for Var<'ctx, F> {
611 fn div_assign(&mut self, rhs: &Self) {
612 *self = self.clone() / rhs;
613 }
614}
615
616impl<'ctx, F: Ring> Additive for Var<'ctx, F> {
617 fn zero() -> Self {
618 Self {
619 inner: VarInner::Native(F::zero()),
620 }
621 }
622}
623
624impl<'ctx, F: Multiplicative> Multiplicative for Var<'ctx, F> {}
625
626impl<'ctx, F: Ring> Ring for Var<'ctx, F> {
627 fn one() -> Self {
628 Self {
629 inner: VarInner::Native(F::one()),
630 }
631 }
632}
633
634/// Unlike the [`Field::inv`] contract, inverting a circuit-backed zero does
635/// not produce zero: it adds an unsatisfiable constraint to the circuit.
636impl<'ctx, F: Field> Field for Var<'ctx, F> {
637 fn inv(&self) -> Self {
638 match &self.inner {
639 VarInner::Native(c) => Self {
640 inner: VarInner::Native(c.inv()),
641 },
642 &VarInner::Circuit { ctx, .. } => {
643 // Prover supplies the inverse via the oracle; verifier just
644 // allocates the slot.
645 let inv = Self::witness(ctx, |v| self.value(v).inv());
646 (inv.clone() * self).assert_eq(&Self::one());
647 inv
648 }
649 }
650 }
651}
652
653/// A circuit value constrained to be `0` or `1`.
654///
655/// A `BoolVar` wraps a [`Var`] together with a guarantee that it holds a
656/// boolean: every constructor either produces a value that is boolean by
657/// construction, or adds the constraint `b * (1 - b) == 0` enforcing it.
658/// Holding that guarantee in the type lets later operations skip
659/// re-checking: [`Self::select`] and the boolean combinators below are sound
660/// precisely because their inputs are already known to be boolean.
661///
662/// The motivating use is scalar multiplication in a circuit, where a scalar
663/// is decomposed into bits (each a `BoolVar`) and a point is accumulated by
664/// conditionally adding with [`Self::select`].
665///
666/// # Native Vars
667///
668/// Like [`Var`], a `BoolVar` built from a native value (see
669/// [`Self::constant`]) lives outside the circuit until combined with a
670/// circuit-backed value. [`Self::assert`] on a native, non-boolean var
671/// therefore panics rather than recording an unsatisfiable constraint, in
672/// keeping with the native-var semantics described on [`Var`].
673///
674/// # Example
675///
676/// ```
677/// use commonware_cryptography::zk::circuit::{build_with_values, BoolVar, Var};
678/// use commonware_math::test::F;
679///
680/// // Use a bit to choose between two values, then check the choice.
681/// let (valued, _) = build_with_values(|ctx| {
682/// let bit = BoolVar::witness(ctx, |_| true);
683/// let a = Var::constant(ctx, F::from(7u64));
684/// let b = Var::constant(ctx, F::from(9u64));
685/// bit.select(&a, &b).assert_eq(&Var::constant(ctx, F::from(7u64)));
686/// Vec::new()
687/// });
688/// assert!(valued.is_satisfied());
689/// ```
690#[derive(Clone)]
691pub struct BoolVar<'ctx, F> {
692 var: Var<'ctx, F>,
693}
694
695impl<F: fmt::Debug> fmt::Debug for BoolVar<'_, F> {
696 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
697 f.debug_tuple("BoolVar").field(&self.var).finish()
698 }
699}
700
701impl<'ctx, F> BoolVar<'ctx, F> {
702 /// The underlying [`Var`], whose value is `0` or `1`.
703 pub const fn var(&self) -> &Var<'ctx, F> {
704 &self.var
705 }
706
707 /// Consume this `BoolVar`, returning the underlying [`Var`].
708 pub fn into_var(self) -> Var<'ctx, F> {
709 self.var
710 }
711}
712
713impl<'ctx, F: Ring> BoolVar<'ctx, F> {
714 /// Create a boolean constant with a fixed, public value.
715 ///
716 /// Like [`Var::native`], the value lives outside the circuit until it is
717 /// combined with a circuit-backed var. No constraint is added: the value
718 /// is boolean by construction.
719 pub fn constant(value: bool) -> Self {
720 Self {
721 var: Var::native(if value { F::one() } else { F::zero() }),
722 }
723 }
724
725 /// Assert that this var equals `other`.
726 pub fn assert_eq(&self, other: &Self) {
727 self.var.assert_eq(other.var());
728 }
729
730 /// Select between two vars based on this bit.
731 ///
732 /// Returns `on_true` when the bit is `1` and `on_false` when it is `0`,
733 /// computed as `on_false + b * (on_true - on_false)` with a single
734 /// multiplication.
735 pub fn select(&self, on_true: &Var<'ctx, F>, on_false: &Var<'ctx, F>) -> Var<'ctx, F> {
736 on_false.clone() + &(self.var.clone() * &(on_true.clone() - on_false))
737 }
738
739 /// Allocate a fresh boolean witness, constrained to be `0` or `1`.
740 ///
741 /// In prover mode, `init` receives the values assigned so far and must
742 /// return the bit's value. In verifier mode, `init` does not run. The
743 /// constraint `b * (1 - b) == 0` is added in both modes.
744 ///
745 /// `init` must not use the [`Context`]: doing so deadlocks. See
746 /// [`Values`].
747 pub fn witness(
748 ctx: Context<'ctx, F>,
749 init: impl for<'a> FnOnce(Values<'a, F>) -> bool,
750 ) -> Self {
751 let var = Var::witness(ctx, |v| if init(v) { F::one() } else { F::zero() });
752 Self::enforce(&var);
753 Self { var }
754 }
755
756 /// Constrain an arbitrary var to be boolean and wrap it.
757 ///
758 /// Adds the constraint `var * (1 - var) == 0`, which holds exactly when
759 /// `var` is `0` or `1`.
760 ///
761 /// # Panics
762 ///
763 /// Panics if `var` is a native var whose value is not `0` or `1`, since
764 /// there is no circuit to record the failed constraint in. See
765 /// [`Var::assert_eq`].
766 pub fn assert(var: Var<'ctx, F>) -> Self {
767 Self::enforce(&var);
768 Self { var }
769 }
770
771 /// Add the booleanity constraint `var * var == var` (equivalently
772 /// `var * (1 - var) == 0`).
773 fn enforce(var: &Var<'ctx, F>) {
774 (var.clone() * var).assert_eq(var);
775 }
776}
777
778impl<'ctx, F: Ring> Not for BoolVar<'ctx, F> {
779 type Output = Self;
780
781 fn not(self) -> Self::Output {
782 Self {
783 var: Var::one() - &self.var,
784 }
785 }
786}
787
788impl<'ctx, F: Ring> BitAnd for BoolVar<'ctx, F> {
789 type Output = Self;
790
791 // Over 0/1, boolean `and` IS multiplication, so `self.var * &rhs.var` is
792 // correct. But clippy's `suspicious_arithmetic_impl` lint assumes any
793 // operator impl that uses a *different* operator internally (here, `*` inside
794 // `BitAnd`) is a copy-paste typo. That heuristic is wrong: it has no notion
795 // of the algebra, so it flags correct code and forces this `#[allow]`. The
796 // lint gives confident, authoritative advice that is simply false, which is
797 // why its designer now resides in the Eighth Circle of Hell,
798 // among the fraudulent counselors (Inferno XXVI) who misused their cleverness
799 // to advise others into error, each concealed within a tongue of flame.
800 #[allow(clippy::suspicious_arithmetic_impl)]
801 fn bitand(self, rhs: Self) -> Self::Output {
802 Self {
803 var: self.var * &rhs.var,
804 }
805 }
806}
807
808impl<'ctx, F: Ring> BitOr for BoolVar<'ctx, F> {
809 type Output = Self;
810
811 // Boolean `or` over 0/1 is `a + b - a * b`, which is correct.
812 fn bitor(self, rhs: Self) -> Self::Output {
813 Self {
814 var: self.var.clone() + &rhs.var - &(self.var * &rhs.var),
815 }
816 }
817}
818
819/// A tool for selecting among multiple items.
820///
821/// This is a generalization of using a [`BoolVar`] to select among two items,
822/// letting you use `k` bits to select among `2^k` items.
823pub struct Selector<'ctx, F> {
824 monomials: Vec<Var<'ctx, F>>,
825}
826
827impl<'ctx, F: Ring> Selector<'ctx, F> {
828 /// Create a new selector, using a given number of in-circuit bits.
829 ///
830 /// This selector will then be able to select among exactly `2^k` items,
831 /// where `k` is the number of bits passed in here.
832 ///
833 /// It is more efficient to create one selector and reuse it.
834 ///
835 /// The selection is made in ascending order, i.e. 0..00, 0..01, 0..10, ...
836 /// In other words, if you pass in, e.g. 010 to this function, you then
837 /// will always get the 3rd (index 2) of 8 items.
838 pub fn new(bits: &[BoolVar<'ctx, F>]) -> Self {
839 let mut monomials = vec![Var::one(); 1usize << bits.len()];
840 for mask in 1..monomials.len() {
841 let bit = mask.trailing_zeros() as usize;
842 let prev = mask ^ (1usize << bit);
843 monomials[mask] = if prev == 0 {
844 bits[bit].var().clone()
845 } else {
846 monomials[prev].clone() * bits[bit].var()
847 };
848 }
849 Self { monomials }
850 }
851
852 /// Select a constant value among `2^k` possibilities.
853 pub fn select_constant(&self, constants: &[F]) -> Var<'ctx, F> {
854 assert_eq!(
855 self.monomials.len(),
856 constants.len(),
857 "constants len must match selectors len"
858 );
859
860 let values = {
861 let mut values = constants.to_vec();
862 let len = values.len().trailing_zeros() as usize;
863 for bit in 0..len {
864 for mask in 0..values.len() {
865 if mask & (1usize << bit) != 0 {
866 let prev = values[mask ^ (1usize << bit)].clone();
867 values[mask] -= &prev;
868 }
869 }
870 }
871 values
872 };
873
874 values
875 .into_iter()
876 .zip(&self.monomials)
877 .map(|(v_i, m_i)| Var::native(v_i) * m_i)
878 .reduce(|acc, x| acc + &x)
879 .expect("values is non empty")
880 }
881}
882
883/// Build a circuit without computing an assignment (verifier mode).
884///
885/// Witness `init` closures do not run in this mode.
886///
887/// The closure returns the vars whose circuit indices the caller wants back
888/// (e.g. the committed outputs of a circuit); the returned indices are in the
889/// same order. Return an empty vec to ignore this.
890///
891/// # Panics
892///
893/// Panics if any returned var is native (not backed by the circuit).
894pub fn build<F: Ring>(
895 f: impl for<'ctx> FnOnce(Context<'ctx, F>) -> Vec<Var<'ctx, F>>,
896) -> (Circuit<F>, Vec<CircuitIdx>) {
897 let inner = ContextInner {
898 values: None,
899 circuit: Mutex::new(Circuit::default()),
900 };
901 let indices = f(Context {
902 inner: &inner,
903 _brand: PhantomData,
904 })
905 .iter()
906 .map(Var::circuit_idx)
907 .collect();
908 (inner.circuit.into_inner(), indices)
909}
910
911/// Build a circuit while simultaneously computing the assignment (prover mode).
912///
913/// Each witness's value comes from the `init` closure passed to
914/// [`Var::witness`].
915///
916/// The closure returns the vars whose circuit indices the caller wants back
917/// (e.g. the committed outputs of a circuit); the returned indices are in the
918/// same order. Return an empty vec to ignore this.
919///
920/// # Panics
921///
922/// Panics if any returned var is native (not backed by the circuit).
923pub fn build_with_values<F: Ring>(
924 f: impl for<'ctx> FnOnce(Context<'ctx, F>) -> Vec<Var<'ctx, F>>,
925) -> (ValuedCircuit<F>, Vec<CircuitIdx>) {
926 let inner = ContextInner {
927 values: Some(Mutex::new(ValuesBuilder {
928 witnesses: Vec::new(),
929 nodes: Vec::new(),
930 })),
931 circuit: Mutex::new(Circuit::default()),
932 };
933 let indices = f(Context {
934 inner: &inner,
935 _brand: PhantomData,
936 })
937 .iter()
938 .map(Var::circuit_idx)
939 .collect();
940 let circuit = inner.circuit.into_inner();
941 let values = inner.values.unwrap().into_inner();
942 (
943 ValuedCircuit {
944 circuit,
945 witnesses: values.witnesses,
946 nodes: values.nodes,
947 },
948 indices,
949 )
950}
951
952/// Fuzzing utilities, comparing circuit satisfaction against native
953/// evaluation of the same operations.
954#[commonware_macros::stability(ALPHA)]
955#[cfg(any(test, feature = "fuzz"))]
956pub mod fuzz {
957 use super::*;
958 use arbitrary::{Arbitrary, Unstructured};
959 use commonware_math::test::F;
960
961 #[derive(Debug)]
962 enum Op {
963 Witness(F),
964 Constant(F),
965 Zero,
966 One,
967 Add(usize, usize),
968 Sub(usize, usize),
969 Mul(usize, usize),
970 Neg(usize),
971 Inv(usize),
972 AssertEq(usize, usize),
973 }
974
975 /// A random sequence of circuit operations, together with whether the
976 /// circuit they build should be satisfied.
977 ///
978 /// Values are drawn from a small range so that assertions have a decent
979 /// chance of holding, exercising both outcomes.
980 #[derive(Debug)]
981 pub struct Plan {
982 ops: Vec<Op>,
983 satisfied: bool,
984 }
985
986 impl Arbitrary<'_> for Plan {
987 fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result<Self> {
988 let mut ops = Vec::new();
989 let mut values: Vec<F> = Vec::new();
990 let mut is_native: Vec<bool> = Vec::new();
991 let mut satisfied = true;
992 for _ in 0..u.int_in_range(1..=32)? {
993 let kind = if values.is_empty() {
994 u.int_in_range(0..=3)?
995 } else {
996 u.int_in_range(0..=9)?
997 };
998 if kind <= 3 {
999 let v = F::from(u.int_in_range::<u8>(0..=4)?);
1000 let (op, value, native) = match kind {
1001 0 => (Op::Witness(v), v, false),
1002 1 => (Op::Constant(v), v, false),
1003 2 => (Op::Zero, F::zero(), true),
1004 _ => (Op::One, F::one(), true),
1005 };
1006 ops.push(op);
1007 values.push(value);
1008 is_native.push(native);
1009 continue;
1010 }
1011 let a = u.int_in_range(0..=values.len() - 1)?;
1012 let b = u.int_in_range(0..=values.len() - 1)?;
1013 let merged = is_native[a] && is_native[b];
1014 let (op, value, native) = match kind {
1015 4 => (Op::Add(a, b), values[a] + &values[b], merged),
1016 5 => (Op::Sub(a, b), values[a] - &values[b], merged),
1017 6 => (Op::Mul(a, b), values[a] * &values[b], merged),
1018 7 => (Op::Neg(a), -values[a], is_native[a]),
1019 8 => {
1020 // Inverting a circuit-backed zero constrains z * 0 = 1,
1021 // which is unsatisfiable. Native vars add no constraint.
1022 if !is_native[a] && values[a] == F::zero() {
1023 satisfied = false;
1024 }
1025 (Op::Inv(a), values[a].inv(), is_native[a])
1026 }
1027 _ => {
1028 // Asserting equality between two unequal native vars is
1029 // a panic by design, so the plan must avoid it.
1030 if merged && values[a] != values[b] {
1031 continue;
1032 }
1033 ops.push(Op::AssertEq(a, b));
1034 satisfied = satisfied && values[a] == values[b];
1035 continue;
1036 }
1037 };
1038 ops.push(op);
1039 values.push(value);
1040 is_native.push(native);
1041 }
1042 Ok(Self { ops, satisfied })
1043 }
1044 }
1045
1046 impl Plan {
1047 /// Check that satisfaction matches the natively computed expectation.
1048 pub fn run(self, _u: &mut Unstructured<'_>) -> arbitrary::Result<()> {
1049 assert_eq!(
1050 self.build().is_satisfied(),
1051 self.satisfied(),
1052 "plan: {self:?}"
1053 );
1054 Ok(())
1055 }
1056
1057 /// Whether the circuit built by [`Self::build`] should be satisfied.
1058 pub const fn satisfied(&self) -> bool {
1059 self.satisfied
1060 }
1061
1062 /// Build the circuit, along with its prover assignment.
1063 pub fn build(&self) -> ValuedCircuit<F> {
1064 build_with_values(|ctx| {
1065 let mut vars: Vec<Var<'_, F>> = Vec::new();
1066 for op in &self.ops {
1067 let var = match *op {
1068 Op::Witness(v) => Var::witness(ctx, move |_| v),
1069 Op::Constant(v) => Var::constant(ctx, v),
1070 Op::Zero => Var::zero(),
1071 Op::One => Var::one(),
1072 Op::Add(a, b) => vars[a].clone() + &vars[b],
1073 Op::Sub(a, b) => vars[a].clone() - &vars[b],
1074 Op::Mul(a, b) => vars[a].clone() * &vars[b],
1075 Op::Neg(a) => -vars[a].clone(),
1076 Op::Inv(a) => vars[a].inv(),
1077 Op::AssertEq(a, b) => {
1078 vars[a].assert_eq(&vars[b]);
1079 continue;
1080 }
1081 };
1082 vars.push(var);
1083 }
1084 Vec::new()
1085 })
1086 .0
1087 }
1088 }
1089}
1090
1091#[cfg(test)]
1092mod tests {
1093 use super::*;
1094 use commonware_invariants::minifuzz;
1095 use commonware_math::test::F;
1096
1097 #[test]
1098 fn test_is_satisfied_matches_native_evaluation_minifuzz() {
1099 minifuzz::test(|u| u.arbitrary::<fuzz::Plan>()?.run(u));
1100 }
1101
1102 #[test]
1103 fn test_is_satisfied_cubic() {
1104 // Constrain a witness `x` to satisfy `x^3 + x + 5 = 35`.
1105 let cubic = |x_value: u64| {
1106 build_with_values(move |ctx| {
1107 let x = Var::witness(ctx, move |_| F::from(x_value));
1108 let out = x.clone() * &x * &x + &x + &Var::constant(ctx, F::from(5u64));
1109 out.assert_eq(&Var::constant(ctx, F::from(35u64)));
1110 Vec::new()
1111 })
1112 .0
1113 };
1114 assert!(cubic(3).is_satisfied());
1115 assert!(!cubic(4).is_satisfied());
1116 }
1117
1118 #[test]
1119 fn test_bool_witness_enforces_booleanity() {
1120 // A boolean witness from a `bool` is always satisfiable.
1121 for b in [false, true] {
1122 let (valued, _) = build_with_values(move |ctx| {
1123 BoolVar::<F>::witness(ctx, move |_| b);
1124 Vec::new()
1125 });
1126 assert!(valued.is_satisfied());
1127 }
1128
1129 // A var that is not 0 or 1 fails the booleanity constraint.
1130 let (bad, _) = build_with_values(|ctx| {
1131 let two = Var::witness(ctx, |_| F::from(2u64));
1132 BoolVar::assert(two);
1133 Vec::new()
1134 });
1135 assert!(!bad.is_satisfied());
1136
1137 // 0 and 1 pass `from_var`.
1138 for v in [0u64, 1u64] {
1139 let (valued, _) = build_with_values(move |ctx| {
1140 let x = Var::witness(ctx, move |_| F::from(v));
1141 BoolVar::assert(x);
1142 Vec::new()
1143 });
1144 assert!(valued.is_satisfied());
1145 }
1146 }
1147
1148 #[test]
1149 fn test_bool_select() {
1150 // `select` returns `on_true` when the bit is set, else `on_false`.
1151 for b in [false, true] {
1152 let (valued, _) = build_with_values(move |ctx| {
1153 let bit = BoolVar::witness(ctx, move |_| b);
1154 let on_true = Var::witness(ctx, |_| F::from(7u64));
1155 let on_false = Var::witness(ctx, |_| F::from(9u64));
1156 let selected = bit.select(&on_true, &on_false);
1157 let expected = if b { F::from(7u64) } else { F::from(9u64) };
1158 selected.assert_eq(&Var::constant(ctx, expected));
1159 Vec::new()
1160 });
1161 assert!(valued.is_satisfied());
1162 }
1163 }
1164
1165 #[test]
1166 fn test_bool_combinators_truth_tables() {
1167 for a in [false, true] {
1168 for b in [false, true] {
1169 let (valued, _) = build_with_values::<F>(move |ctx| {
1170 let a_var = BoolVar::witness(ctx, move |_| a);
1171 let b_var = BoolVar::witness(ctx, move |_| b);
1172 (!a_var.clone()).assert_eq(&BoolVar::constant(!a));
1173 (a_var.clone() & b_var.clone()).assert_eq(&BoolVar::constant(a & b));
1174 (a_var | b_var).assert_eq(&BoolVar::constant(a | b));
1175 Vec::new()
1176 });
1177 assert!(valued.is_satisfied());
1178 }
1179 }
1180 }
1181
1182 #[test]
1183 fn test_bool_constant() {
1184 let (valued, _) = build_with_values(|ctx| {
1185 // A native boolean constant folds in correctly when combined.
1186 let t = BoolVar::<F>::constant(true);
1187 let f = BoolVar::<F>::constant(false);
1188 let x = Var::witness(ctx, |_| F::from(5u64));
1189 t.select(&x, &Var::zero())
1190 .assert_eq(&Var::constant(ctx, F::from(5u64)));
1191 f.select(&x, &Var::zero()).assert_eq(&Var::zero());
1192 Vec::new()
1193 });
1194 assert!(valued.is_satisfied());
1195 }
1196}