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}