Skip to main content

sonobe_primitives/circuits/
mod.rs

1//! This module defines circuits and helpers used by Sonobe.
2
3use ark_ff::{Field, PrimeField};
4use ark_r1cs_std::{GR1CSVar, alloc::AllocVar, eq::EqGadget, fields::fp::FpVar};
5use ark_relations::gr1cs::{
6    ConstraintSynthesizer, ConstraintSystem, ConstraintSystemRef, SynthesisError, SynthesisMode,
7};
8use ark_std::{
9    fmt::Debug,
10    ops::{Deref, Index, IndexMut},
11};
12
13use crate::transcripts::{Absorbable, AbsorbableVar};
14
15pub mod utils;
16
17/// [`FCircuit`] defines the trait of step circuits being proven by IVC schemes.
18///
19/// In IVC, a step circuit is repeatedly invoked to update some state persisted
20/// throughout the execution.
21/// For flexibility, we further allow each step to take some external inputs
22/// and produce some external outputs that are not part of the state, which may
23/// or may not be constrained inside the step circuit.
24///
25/// Such a design has several advantages:
26/// 1. It allows the implementation to keep the state minimal, only including
27///    the parts that need to be preserved and constrained across steps, while
28///    step-specific inputs that might be large are not part of the state.
29///
30///    For example, in a Merkle tree update circuit, the state may only contain
31///    the root of the tree, while the leaf value and authentication path which
32///    are large can be provided as external inputs at each step.
33///
34/// 2. The caller of the step circuit can peek into the circuit execution at
35///    each step via the external outputs by having the circuit return
36///    `var.value()` for desired variables.
37///
38///    For example, in a Merkle tree update circuit, the circuit can return the
39///    intermediate hashes computed at each step as external outputs, allowing
40///    the caller to test if the hash computation is correct.
41///
42/// 3. The implementation can mix out-of-circuit and in-circuit logic in this
43///    structure, where the out-of-circuit logic may consume external inputs and
44///    produce external outputs for the next step.
45///    This is why the implementation may choose to constrain or not constrain
46///    the external inputs/outputs inside the step circuit.
47///    Such a mixed design can be helpful if the out-of-circuit logic and the
48///    in-circuit logic are highly interdependent.
49///
50///    For example, in a Merkle tree update circuit, one may write both the
51///    Merkle proof generation (out-of-circuit) and verification (in-circuit)
52///    logic in a single [`FCircuit::generate_step_constraints`].
53///    In this case, the external inputs contain the leaf value to be added, as
54///    well as all the existing tree nodes.
55///    The latter will be used by the out-of-circuit logic to compute the path,
56///    but will not be constrained inside the circuit.
57///    The external outputs contain the new tree nodes after the update, which
58///    will be used as the inputs to the next step.
59///
60/// To summarize, the step circuit takes as input the current state and some
61/// external inputs, and returns the next state and some external outputs.
62pub trait FCircuit {
63    /// [`FCircuit::Field`] is the field over which the circuit is defined.
64    type Field: PrimeField;
65    /// [`FCircuit::State`] is the type of the state.
66    ///
67    /// It is usually an array of field elements, but we make our design quite
68    /// flexible so that the implementation is free to choose any structure for
69    /// it.
70    type State: Clone + PartialEq + Absorbable;
71    /// [`FCircuit::StateVar`] is the in-circuit variable type for the state.
72    ///
73    /// If the implementation chooses custom structures for the state, it should
74    /// implement the required traits for the corresponding variable type.
75    type StateVar: GR1CSVar<Self::Field, Value = Self::State>
76        + AllocVar<Self::State, Self::Field>
77        + AbsorbableVar<Self::Field>
78        + EqGadget<Self::Field>;
79    /// [`FCircuit::ExternalInputs`] is the type of external inputs provided to
80    /// each step of the circuit.
81    type ExternalInputs;
82    /// [`FCircuit::ExternalOutputs`] is the type of external outputs produced
83    /// by each step of the circuit.
84    type ExternalOutputs;
85
86    /// [`FCircuit::same_state_shape`] returns whether two states `a` and `b`
87    /// have the same shape/structure.
88    ///
89    /// This allows the verifier to check whether the prover's claimed states
90    /// have the desired shape.
91    ///
92    /// The implementation should perform the checks carefully to ensure that
93    /// all fields/members of the provided states are consistent. Specifically,
94    /// if all fields/members of [`FCircuit::State`] have fixed size, then the
95    /// shape consistency is trivially `true`. However, if [`FCircuit::State`]
96    /// contains variable-length fields/members (e.g., `Vec<F>`, `Vec<Vec<F>>`),
97    /// then the implementation must examine all of such fields/members and
98    /// return `false` when any of them are differently sized in `a` and `b`.
99    fn same_state_shape(a: &Self::State, b: &Self::State) -> bool;
100
101    /// [`FCircuit::dummy_state`] returns a dummy state for the circuit.
102    ///
103    /// The dummy state should have the same shape as states in real IVC
104    /// executions.
105    fn dummy_state(&self) -> Self::State;
106
107    /// [`FCircuit::dummy_external_inputs`] returns dummy external inputs for
108    /// the circuit.
109    fn dummy_external_inputs(&self) -> Self::ExternalInputs;
110
111    /// [`FCircuit::generate_step_constraints`] generates the constraints for
112    /// the `i`-th step of invocation of the step circuit with the current state
113    /// `state` and external inputs `external_inputs`, producing the next state
114    /// and external outputs.
115    ///
116    /// ### Tips
117    ///
118    /// - Since this method uses `self`, the implementation can store some fixed
119    ///   info that is shared across all steps inside `self`.
120    /// - Variables in the implementation should be allocated as witnesses (not
121    ///   public inputs) in the implementation.
122    /// - If needed, the constraint system `cs` can be accessed via `i.cs()` or
123    ///   `state.cs()` using arkworks' [`GR1CSVar::cs`] method.
124    fn generate_step_constraints(
125        &self,
126        i: FpVar<Self::Field>,
127        state: Self::StateVar,
128        external_inputs: Self::ExternalInputs,
129    ) -> Result<(Self::StateVar, Self::ExternalOutputs), SynthesisError>;
130}
131
132/// [`Assignments`] represents a full assignment vector `z = (u, x, w)` for a
133/// constraint system.
134#[derive(Clone, Debug, PartialEq)]
135pub struct Assignments<F, V> {
136    /// [`Assignments::constant`] is the "constant" part (leading scalar) of the
137    /// assignment, which is usually 1 but might be relaxed in some cases.
138    pub constant: F,
139    /// [`Assignments::public`] contains the public inputs.
140    pub public: V,
141    /// [`Assignments::private`] contains the witnesses.
142    pub private: V,
143}
144
145/// [`AssignmentsOwned`] is a convenience alias for owned assignment vectors.
146pub type AssignmentsOwned<F> = Assignments<F, Vec<F>>;
147
148impl<F, V> From<(F, V, V)> for Assignments<F, V> {
149    fn from((u, x, w): (F, V, V)) -> Self {
150        Self {
151            constant: u,
152            public: x,
153            private: w,
154        }
155    }
156}
157
158impl<F, V: AsRef<[F]>> Index<usize> for Assignments<F, V> {
159    type Output = F;
160
161    fn index(&self, index: usize) -> &Self::Output {
162        let public = self.public.as_ref();
163        let private = self.private.as_ref();
164        if index == 0 {
165            &self.constant
166        } else if index <= public.len() {
167            &public[index - 1]
168        } else {
169            &private[index - 1 - public.len()]
170        }
171    }
172}
173
174impl<F, V: AsRef<[F]> + AsMut<[F]>> IndexMut<usize> for Assignments<F, V> {
175    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
176        let public = self.public.as_mut();
177        let private = self.private.as_mut();
178        if index == 0 {
179            &mut self.constant
180        } else if index <= public.len() {
181            &mut public[index - 1]
182        } else {
183            &mut private[index - 1 - public.len()]
184        }
185    }
186}
187
188/// [`ConstraintSystemExt`] wraps a `ConstraintSystemRef` with compile-time
189/// flags that control whether constraint matrices (`ARITH_ENABLED`) and / or
190/// assignment vectors (`ASSIGNMENTS_ENABLED`) are collected during synthesis.
191pub struct ConstraintSystemExt<F: Field, const ARITH_ENABLED: bool, const ASSIGNMENTS_ENABLED: bool>
192{
193    cs: ConstraintSystemRef<F>,
194}
195
196impl<F: Field, const ARITH_ENABLED: bool, const ASSIGNMENTS_ENABLED: bool> Deref
197    for ConstraintSystemExt<F, ARITH_ENABLED, ASSIGNMENTS_ENABLED>
198{
199    type Target = ConstraintSystemRef<F>;
200
201    fn deref(&self) -> &Self::Target {
202        &self.cs
203    }
204}
205
206impl<F: Field, const ARITH_ENABLED: bool, const ASSIGNMENTS_ENABLED: bool>
207    ConstraintSystemExt<F, ARITH_ENABLED, ASSIGNMENTS_ENABLED>
208{
209    /// [`ConstraintSystemExt::new`] creates a new constraint system wrapper
210    /// with the specified flags.
211    pub fn new() -> Self {
212        let cs = ConstraintSystem::<F>::new_ref();
213        let mode = if ASSIGNMENTS_ENABLED {
214            SynthesisMode::Prove {
215                construct_matrices: ARITH_ENABLED,
216                generate_lc_assignments: ARITH_ENABLED,
217            }
218        } else {
219            SynthesisMode::Setup
220        };
221        cs.set_mode(mode);
222        Self { cs }
223    }
224
225    /// [`ConstraintSystemExt::execute_synthesizer`] executes a circuit inside
226    /// the constraint system, where the circuit should implement the
227    /// [`ConstraintSynthesizer`] trait.
228    pub fn execute_synthesizer(
229        &self,
230        circuit: impl ConstraintSynthesizer<F>,
231    ) -> Result<(), SynthesisError> {
232        self.execute_fn(|cs| circuit.generate_constraints(cs))
233    }
234
235    /// [`ConstraintSystemExt::execute_fn`] executes a circuit inside the
236    /// constraint system, where the circuit should be defined as a closure that
237    /// takes as input a `ConstraintSystemRef` and returns a result of type `R`.
238    /// The return value of the closure will be returned by this method.
239    pub fn execute_fn<R>(
240        &self,
241        circuit: impl FnOnce(ConstraintSystemRef<F>) -> Result<R, SynthesisError>,
242    ) -> Result<R, SynthesisError> {
243        let result = circuit(self.cs.clone())?;
244        if ARITH_ENABLED {
245            self.cs.finalize();
246        }
247        Ok(result)
248    }
249}
250
251impl<F: Field, const ARITH_ENABLED: bool, const ASSIGNMENTS_ENABLED: bool> Default
252    for ConstraintSystemExt<F, ARITH_ENABLED, ASSIGNMENTS_ENABLED>
253{
254    fn default() -> Self {
255        Self::new()
256    }
257}
258
259/// [`ArithExtractor`] collects only the constraint matrices (no assignments)
260/// from a synthesized circuit.
261pub type ArithExtractor<F> = ConstraintSystemExt<F, true, false>;
262/// [`AssignmentsExtractor`] collects only the assignments (no constraint
263/// matrices) from a synthesized circuit.
264pub type AssignmentsExtractor<F> = ConstraintSystemExt<F, false, true>;
265
266impl<F: Field> ArithExtractor<F> {
267    /// [`ArithExtractor::arith`] extracts the constraint matrices from the
268    /// circuit and returns them as an arithmetization / constraint system
269    /// structure of type `A`.
270    pub fn arith<A: From<ConstraintSystem<F>>>(self) -> Result<A, SynthesisError> {
271        Ok(self.cs.into_inner().unwrap().into())
272    }
273}
274
275impl<F: Field> AssignmentsExtractor<F> {
276    /// [`AssignmentsExtractor::assignments`] extracts the assignments from the
277    /// circuit and returns them as `Assignments`.
278    pub fn assignments(self) -> Result<Assignments<F, Vec<F>>, SynthesisError> {
279        let witness = self.cs.witness_assignment()?.to_vec();
280        // skip the first element which is '1'
281        let instance = self.cs.instance_assignment()?[1..].to_vec();
282
283        Ok((F::one(), instance, witness).into())
284    }
285}
286
287/// [`WitnessToPublic`] defines a helper trait for marking witness variables as
288/// public inputs in the constraint system.
289pub trait WitnessToPublic {
290    /// [`WitnessToPublic::mark_as_public`] marks a witness variable as public.
291    fn mark_as_public(&self) -> Result<(), SynthesisError>;
292}
293
294impl<T: WitnessToPublic> WitnessToPublic for [T] {
295    fn mark_as_public(&self) -> Result<(), SynthesisError> {
296        self.iter().try_for_each(|x| x.mark_as_public())
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use ark_bn254::Fr;
303    use ark_ff::UniformRand;
304    use ark_relations::gr1cs::ConstraintSynthesizer;
305    use ark_std::{error::Error, rand::thread_rng};
306    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
307    use wasm_bindgen_test::wasm_bindgen_test as test;
308
309    use super::{
310        utils::{CircuitForTest, constraints_for_test, satisfying_assignments_for_test},
311        *,
312    };
313    use crate::arithmetizations::r1cs::R1CS;
314
315    #[test]
316    fn test_satisfiability() -> Result<(), Box<dyn Error>> {
317        let mut rng = thread_rng();
318        let circuit = CircuitForTest::<Fr> {
319            x: Fr::rand(&mut rng),
320        };
321        let cs = ConstraintSystem::new_ref();
322        circuit.generate_constraints(cs.clone())?;
323        assert!(cs.is_satisfied()?);
324
325        Ok(())
326    }
327
328    #[test]
329    fn test_constraint_extraction() -> Result<(), Box<dyn Error>> {
330        let mut rng = thread_rng();
331        let circuit = CircuitForTest::<Fr> {
332            x: Fr::rand(&mut rng),
333        };
334        let cs = ArithExtractor::new();
335        cs.execute_synthesizer(circuit)?;
336        assert_eq!(cs.arith::<R1CS<_>>()?, constraints_for_test());
337        Ok(())
338    }
339
340    #[test]
341    fn test_witness_extraction() -> Result<(), Box<dyn Error>> {
342        let mut rng = thread_rng();
343        let x = Fr::rand(&mut rng);
344        let circuit = CircuitForTest::<Fr> { x };
345
346        let cs = AssignmentsExtractor::new();
347        cs.execute_synthesizer(circuit)?;
348        assert_eq!(cs.assignments()?, satisfying_assignments_for_test(x));
349        Ok(())
350    }
351}