Skip to main content

midnight_circuits/instructions/
control_flow.rs

1// This file is part of MIDNIGHT-ZK.
2// Copyright (C) 2025 Midnight Foundation
3// SPDX-License-Identifier: Apache-2.0
4// Licensed under the Apache License, Version 2.0 (the "License");
5// You may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7// http://www.apache.org/licenses/LICENSE-2.0
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14//! Control flow instructions interface.
15//!
16//! It provides functions for conditionally selecting and asserting equality a
17//! pair of `Assigned` elements.
18//!
19//! The trait is parametrized by `Assigned` type.
20
21use ff::PrimeField;
22use midnight_proofs::{circuit::Layouter, plonk::Error};
23
24use super::AssertionInstructions;
25use crate::types::{AssignedBit, InnerValue};
26
27/// The set of circuit instructions for control flow operations.
28pub trait ControlFlowInstructions<F: PrimeField, Assigned>:
29    AssertionInstructions<F, Assigned>
30where
31    Assigned: InnerValue,
32{
33    /// Returns `x` if `cond = true` and `y` otherwise.
34    /// ```
35    /// # midnight_circuits::run_test_native_gadget!(chip, layouter, {
36    /// let x: AssignedNative<F> = chip.assign(&mut layouter, Value::known(F::ZERO))?;
37    /// let y: AssignedNative<F> = chip.assign(&mut layouter, Value::known(F::ONE))?;
38    /// let cond: AssignedBit<F> = chip.assign(&mut layouter, Value::known(true))?;
39    ///
40    /// let choice = chip.select(&mut layouter, &cond, &x, &y)?;
41    /// chip.assert_equal(&mut layouter, &choice, &x)?;
42    /// # });
43    /// ```
44    fn select(
45        &self,
46        layouter: &mut impl Layouter<F>,
47        cond: &AssignedBit<F>,
48        x: &Assigned,
49        y: &Assigned,
50    ) -> Result<Assigned, Error>;
51
52    /// Equality assertion only if `cond` is set to `1`.
53    fn cond_assert_equal(
54        &self,
55        layouter: &mut impl Layouter<F>,
56        cond: &AssignedBit<F>,
57        x: &Assigned,
58        y: &Assigned,
59    ) -> Result<(), Error> {
60        let x = self.select(layouter, cond, x, y)?;
61        self.assert_equal(layouter, &x, y)
62    }
63
64    /// Swaps two elements `x` and `y` only if `cond` is set to `1`.
65    fn cond_swap(
66        &self,
67        layouter: &mut impl Layouter<F>,
68        cond: &AssignedBit<F>,
69        x: &Assigned,
70        y: &Assigned,
71    ) -> Result<(Assigned, Assigned), Error> {
72        let new_x = self.select(layouter, cond, y, x)?;
73        let new_y = self.select(layouter, cond, x, y)?;
74
75        Ok((new_x, new_y))
76    }
77}
78
79#[cfg(test)]
80pub(crate) mod tests {
81    use std::marker::PhantomData;
82
83    use ff::FromUniformBytes;
84    use midnight_proofs::{
85        circuit::{Layouter, SimpleFloorPlanner, Value},
86        dev::MockProver,
87        plonk::{Circuit, Column, ConstraintSystem, Fixed},
88    };
89    use rand::SeedableRng;
90    use rand_chacha::ChaCha8Rng;
91
92    use super::*;
93    use crate::{
94        instructions::{AssertionInstructions, AssignmentInstructions},
95        testing_utils::{FromScratch, Sampleable},
96        types::InnerValue,
97        utils::circuit_modeling::circuit_to_json,
98    };
99
100    #[derive(Clone, Debug)]
101    enum Operation {
102        Select,
103        CondAssertEqual,
104        CondSwap,
105    }
106
107    #[derive(Clone, Debug)]
108    struct TestCircuit<F, Assigned, ControlFlowChip>
109    where
110        Assigned: InnerValue,
111    {
112        x: Assigned::Element,
113        y: Assigned::Element,
114        cond: bool,
115        expected: Assigned::Element,
116        expected_extra: Option<Assigned::Element>,
117        operation: Operation,
118        _marker: PhantomData<(F, Assigned, ControlFlowChip)>,
119    }
120
121    impl<F, Assigned, ControlFlowChip> Circuit<F> for TestCircuit<F, Assigned, ControlFlowChip>
122    where
123        F: PrimeField,
124        Assigned: InnerValue,
125        Assigned::Element: Default,
126        ControlFlowChip: ControlFlowInstructions<F, Assigned>
127            + AssignmentInstructions<F, Assigned>
128            + AssertionInstructions<F, Assigned>
129            + FromScratch<F>,
130    {
131        type Config = (<ControlFlowChip as FromScratch<F>>::Config, Column<Fixed>);
132        type FloorPlanner = SimpleFloorPlanner;
133        type Params = ();
134
135        fn without_witnesses(&self) -> Self {
136            unreachable!()
137        }
138
139        fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
140            let committed_instance_column = meta.instance_column();
141            let instance_column = meta.instance_column();
142            let fixed_column = meta.fixed_column();
143            meta.enable_equality(fixed_column);
144            (
145                ControlFlowChip::configure_from_scratch(
146                    meta,
147                    &[committed_instance_column, instance_column],
148                ),
149                fixed_column,
150            )
151        }
152
153        fn synthesize(
154            &self,
155            (config, fixed_column): Self::Config,
156            mut layouter: impl Layouter<F>,
157        ) -> Result<(), Error> {
158            let chip = ControlFlowChip::new_from_scratch(&config);
159
160            let x = chip.assign_fixed(&mut layouter, self.x.clone())?;
161            let y = chip.assign_fixed(&mut layouter, self.y.clone())?;
162            let cond_value = layouter.assign_region(
163                || "Assign fixed",
164                |mut region| {
165                    region.assign_fixed(
166                        || "cond value",
167                        fixed_column,
168                        0,
169                        || Value::known(if self.cond { F::ONE } else { F::ZERO }),
170                    )
171                },
172            )?;
173
174            let cond = AssignedBit(cond_value);
175
176            match self.operation {
177                Operation::Select => {
178                    let res = chip.select(&mut layouter, &cond, &x, &y)?;
179                    chip.assert_equal_to_fixed(&mut layouter, &res, self.expected.clone())
180                }
181                Operation::CondAssertEqual => chip.cond_assert_equal(&mut layouter, &cond, &x, &y),
182                Operation::CondSwap => {
183                    let (fst, snd) = chip.cond_swap(&mut layouter, &cond, &x, &y)?;
184                    chip.assert_equal_to_fixed(&mut layouter, &fst, self.expected.clone())?;
185                    chip.assert_equal_to_fixed(
186                        &mut layouter,
187                        &snd,
188                        self.expected_extra.clone().unwrap(),
189                    )
190                }
191            }?;
192
193            chip.load_from_scratch(&mut layouter)
194        }
195    }
196
197    #[allow(clippy::too_many_arguments)]
198    fn run<F, Assigned, ControlFlowChip>(
199        x: &Assigned::Element,
200        y: &Assigned::Element,
201        cond: bool,
202        expected: &Assigned::Element,
203        expected_extra: Option<&Assigned::Element>,
204        operation: Operation,
205        must_pass: bool,
206        cost_model: bool,
207        chip_name: &str,
208        op_name: &str,
209    ) where
210        F: PrimeField + FromUniformBytes<64> + Ord,
211        Assigned: InnerValue,
212        Assigned::Element: Default,
213        ControlFlowChip: ControlFlowInstructions<F, Assigned>
214            + AssignmentInstructions<F, Assigned>
215            + AssertionInstructions<F, Assigned>
216            + FromScratch<F>,
217    {
218        let circuit = TestCircuit::<F, Assigned, ControlFlowChip> {
219            x: x.clone(),
220            y: y.clone(),
221            cond,
222            expected: expected.clone(),
223            expected_extra: expected_extra.cloned(),
224            operation,
225            _marker: PhantomData,
226        };
227
228        let log2_nb_rows = 10;
229        let public_inputs = vec![vec![], vec![]];
230        match MockProver::run(log2_nb_rows, &circuit, public_inputs) {
231            Ok(prover) => match prover.verify() {
232                Ok(()) => assert!(must_pass),
233                Err(e) => assert!(!must_pass, "Failed verifier with error {e:?}"),
234            },
235            Err(e) => assert!(!must_pass, "Failed prover with error {e:?}"),
236        }
237
238        if cost_model {
239            circuit_to_json(chip_name, op_name, circuit);
240        }
241    }
242
243    pub fn test_select<F, Assigned, ControlFlowChip>(name: &str)
244    where
245        F: PrimeField + FromUniformBytes<64> + Ord,
246        Assigned: InnerValue + Sampleable,
247        Assigned::Element: Default,
248        ControlFlowChip: ControlFlowInstructions<F, Assigned>
249            + AssignmentInstructions<F, Assigned>
250            + AssertionInstructions<F, Assigned>
251            + FromScratch<F>,
252    {
253        let mut rng = ChaCha8Rng::seed_from_u64(0xc0ffee);
254        let x = Assigned::sample_inner(&mut rng);
255        let y = Assigned::sample_inner(&mut rng);
256
257        let mut cost_model = true;
258        let mut test = |cond: bool, output: &Assigned::Element, must_pass: bool| {
259            run::<F, Assigned, ControlFlowChip>(
260                &x,
261                &y,
262                cond,
263                output,
264                None,
265                Operation::Select,
266                must_pass,
267                cost_model,
268                name,
269                "select",
270            );
271            cost_model = false;
272        };
273
274        test(true, &x, true);
275        test(false, &y, true);
276        test(true, &y, false);
277        test(false, &x, false);
278    }
279
280    pub fn test_cond_assert_equal<F, Assigned, ControlFlowChip>(name: &str)
281    where
282        F: PrimeField + FromUniformBytes<64> + Ord,
283        Assigned: InnerValue + Sampleable,
284        Assigned::Element: Default,
285        ControlFlowChip: ControlFlowInstructions<F, Assigned>
286            + AssignmentInstructions<F, Assigned>
287            + AssertionInstructions<F, Assigned>
288            + FromScratch<F>,
289    {
290        let mut rng = ChaCha8Rng::seed_from_u64(0xc0ffee);
291        let x = Assigned::sample_inner(&mut rng);
292        let y = Assigned::sample_inner(&mut rng);
293
294        let mut cost_model = true;
295        let mut test =
296            |inputs: (&Assigned::Element, &Assigned::Element), cond: bool, must_pass: bool| {
297                run::<F, Assigned, ControlFlowChip>(
298                    inputs.0,
299                    inputs.1,
300                    cond,
301                    &Assigned::Element::default(),
302                    None,
303                    Operation::CondAssertEqual,
304                    must_pass,
305                    cost_model,
306                    name,
307                    "cond_assert_equal",
308                );
309                cost_model = false;
310            };
311
312        test((&x, &x), true, true);
313        test((&x, &y), true, false);
314        test((&x, &x), false, true);
315        test((&x, &y), false, true);
316    }
317
318    pub fn test_cond_swap<F, Assigned, ControlFlowChip>(name: &str)
319    where
320        F: PrimeField + FromUniformBytes<64> + Ord,
321        Assigned: InnerValue + Sampleable,
322        Assigned::Element: Default,
323        ControlFlowChip: ControlFlowInstructions<F, Assigned>
324            + AssignmentInstructions<F, Assigned>
325            + AssertionInstructions<F, Assigned>
326            + FromScratch<F>,
327    {
328        let mut rng = ChaCha8Rng::seed_from_u64(0xc0ffee);
329        let x = Assigned::sample_inner(&mut rng);
330        let y = Assigned::sample_inner(&mut rng);
331
332        let mut cost_model = true;
333        let mut test =
334            |cond: bool, outputs: (&Assigned::Element, &Assigned::Element), must_pass: bool| {
335                run::<F, Assigned, ControlFlowChip>(
336                    &x,
337                    &y,
338                    cond,
339                    outputs.0,
340                    Some(outputs.1),
341                    Operation::CondSwap,
342                    must_pass,
343                    cost_model,
344                    name,
345                    "cond_swap",
346                );
347                cost_model = false;
348            };
349
350        test(true, (&y, &x), true);
351        test(false, (&x, &y), true);
352        test(true, (&x, &y), false);
353        test(false, (&y, &x), false);
354
355        test(true, (&x, &x), false);
356        test(true, (&y, &y), false);
357    }
358}