Skip to main content

midnight_circuits/instructions/
canonicity.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//! Canonicity instructions interface.
15//!
16//! It provides functions for checking if assigned bits, representing a value
17//! `k`, are canonical with respect to some field order or a given bound `n`,
18//! i.e. iff `|bits| <= n::NUM_BITS` and `k`, interpreted in little-endian,
19//! is strictly lower than `n`.
20//!
21//! The implementors of this trait need to implement [FieldInstructions]
22//! where the notion of `canonical` makes sense.
23
24use ff::PrimeField;
25use midnight_proofs::{circuit::Layouter, plonk::Error};
26use num_bigint::BigUint;
27
28use crate::{
29    instructions::{AssignmentInstructions, FieldInstructions},
30    types::{AssignedBit, InnerConstants, Instantiable},
31};
32
33/// The set of circuit instructions for canonicity assertions.
34pub trait CanonicityInstructions<F, Assigned>:
35    FieldInstructions<F, Assigned> + AssignmentInstructions<F, AssignedBit<F>>
36where
37    F: PrimeField,
38    Assigned::Element: PrimeField,
39    Assigned: Instantiable<F> + InnerConstants + Clone,
40{
41    /// Returns `true` iff the given sequence of bits is canonical in the
42    /// underlying field `Assigned::Element`. Namely, iff
43    /// `|bits| <= Assigned::Element::NUM_BITS` and the integer represented by
44    /// the given sequence of assigned bits, interpreted in little-endian,
45    /// is strictly lower than the order of `Assigned::Element`.
46    /// ```
47    /// # midnight_circuits::run_test_native_gadget!(chip, layouter, {
48    /// let x: Vec<AssignedBit<F>> = chip.assign_many(
49    ///     &mut layouter,
50    ///     &[
51    ///         Value::known(true),
52    ///         Value::known(false),
53    ///         Value::known(false),
54    ///         Value::known(true),
55    ///         Value::known(false),
56    ///     ],
57    /// )?;
58    ///
59    /// let check: AssignedBit<F> = chip.is_canonical(&mut layouter, &x)?;
60    /// // This is not sufficient to check that the value is canonical,
61    /// // we need to check that the output is true.
62    /// chip.assert_equal_to_fixed(&mut layouter, &check, true)?;
63    /// # });
64    /// ```
65    fn is_canonical(
66        &self,
67        layouter: &mut impl Layouter<F>,
68        bits: &[AssignedBit<F>],
69    ) -> Result<AssignedBit<F>, Error> {
70        let order = self.order();
71        if bits.len() > order.bits() as usize {
72            self.assign_fixed(layouter, false)
73        } else {
74            self.le_bits_lower_than(layouter, bits, order)
75        }
76    }
77
78    /// Returns `true` iff the integer represented by the given sequence of
79    /// assigned bits, interpreted in little-endian, is strictly lower than the
80    /// given bound.
81    /// ```
82    /// # use num_bigint::BigUint;
83    /// # midnight_circuits::run_test_native_gadget!(chip, layouter, {
84    /// let x: Vec<AssignedBit<F>> = chip.assign_many(
85    ///     &mut layouter,
86    ///     &[
87    ///         Value::known(true),
88    ///         Value::known(false),
89    ///         Value::known(false),
90    ///         Value::known(true),
91    ///         Value::known(true),
92    ///     ],
93    /// )?;
94    ///
95    /// // assert the value is less than 32
96    /// let check1: AssignedBit<F> = chip.le_bits_lower_than(&mut layouter, &x, BigUint::from(32u8))?;
97    /// chip.assert_equal_to_fixed(&mut layouter, &check1, true)?;
98    ///
99    /// // we can also compare the number with non-powers of two
100    /// let check2: AssignedBit<F> = chip.le_bits_lower_than(&mut layouter, &x, BigUint::from(17u8))?;
101    /// chip.assert_equal_to_fixed(&mut layouter, &check2, false)?;
102    /// # });
103    /// ```
104    fn le_bits_lower_than(
105        &self,
106        layouter: &mut impl Layouter<F>,
107        bits: &[AssignedBit<F>],
108        bound: BigUint,
109    ) -> Result<AssignedBit<F>, Error>;
110
111    /// Returns `true` iff the integer represented by the given sequence of
112    /// assigned bits, interpreted in little-endian, is greater than or equal
113    /// to the given bound.
114    fn le_bits_geq_than(
115        &self,
116        layouter: &mut impl Layouter<F>,
117        bits: &[AssignedBit<F>],
118        bound: BigUint,
119    ) -> Result<AssignedBit<F>, Error>;
120}
121
122#[cfg(test)]
123pub(crate) mod tests {
124    use std::marker::PhantomData;
125
126    use ff::FromUniformBytes;
127    use midnight_proofs::{
128        circuit::{Layouter, SimpleFloorPlanner},
129        dev::MockProver,
130        plonk::{Circuit, ConstraintSystem},
131    };
132    use num_traits::{One, Zero};
133    use rand::{RngCore, SeedableRng};
134    use rand_chacha::ChaCha8Rng;
135
136    use super::*;
137    use crate::{
138        instructions::{AssertionInstructions, AssignmentInstructions},
139        types::InnerValue,
140        utils::{
141            circuit_modeling::circuit_to_json,
142            util::{modulus, FromScratch},
143        },
144    };
145
146    #[derive(Clone, Debug)]
147    enum Operation {
148        Canonical,
149        Lower,
150        Geq,
151    }
152
153    #[derive(Clone, Debug)]
154    struct TestCircuit<F, Assigned, CanonicityChip>
155    where
156        Assigned: InnerValue,
157    {
158        bits: Vec<bool>,
159        bound: BigUint,
160        expected: bool,
161        operation: Operation,
162        _marker: PhantomData<(F, Assigned, CanonicityChip)>,
163    }
164
165    impl<F, Assigned, CanonicityChip> Circuit<F> for TestCircuit<F, Assigned, CanonicityChip>
166    where
167        F: PrimeField,
168        Assigned::Element: PrimeField,
169        Assigned: Instantiable<F> + InnerConstants + Clone,
170        CanonicityChip: CanonicityInstructions<F, Assigned>
171            + AssertionInstructions<F, Assigned>
172            + AssertionInstructions<F, AssignedBit<F>>
173            + AssignmentInstructions<F, Assigned>
174            + FromScratch<F>,
175    {
176        type Config = <CanonicityChip as FromScratch<F>>::Config;
177        type FloorPlanner = SimpleFloorPlanner;
178        type Params = ();
179
180        fn without_witnesses(&self) -> Self {
181            unreachable!()
182        }
183
184        fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
185            let committed_instance_column = meta.instance_column();
186            let instance_column = meta.instance_column();
187            CanonicityChip::configure_from_scratch(
188                meta,
189                &[committed_instance_column, instance_column],
190            )
191        }
192
193        fn synthesize(
194            &self,
195            config: Self::Config,
196            mut layouter: impl Layouter<F>,
197        ) -> Result<(), Error> {
198            let chip = CanonicityChip::new_from_scratch(&config);
199
200            let bits = self
201                .bits
202                .iter()
203                .map(|b| chip.assign_fixed(&mut layouter, *b))
204                .collect::<Result<Vec<_>, Error>>()?;
205            let bound = self.bound.clone();
206
207            let res = match self.operation {
208                Operation::Canonical => chip.is_canonical(&mut layouter, &bits),
209                Operation::Lower => chip.le_bits_lower_than(&mut layouter, &bits, bound),
210                Operation::Geq => chip.le_bits_geq_than(&mut layouter, &bits, bound),
211            }?;
212
213            chip.assert_equal_to_fixed(&mut layouter, &res, self.expected)?;
214
215            chip.load_from_scratch(&mut layouter)
216        }
217    }
218
219    #[allow(clippy::too_many_arguments)]
220    fn run<F, Assigned, CanonicityChip>(
221        bits: &[u8],
222        bound: Option<&BigUint>,
223        expected: bool,
224        operation: Operation,
225        must_pass: bool,
226        cost_model: bool,
227        chip_name: &str,
228        op_name: &str,
229    ) where
230        F: PrimeField + FromUniformBytes<64> + Ord,
231        Assigned::Element: PrimeField,
232        Assigned: Instantiable<F> + InnerConstants + Clone,
233        CanonicityChip: CanonicityInstructions<F, Assigned>
234            + AssertionInstructions<F, Assigned>
235            + AssertionInstructions<F, AssignedBit<F>>
236            + AssignmentInstructions<F, Assigned>
237            + FromScratch<F>,
238    {
239        let circuit = TestCircuit::<F, Assigned, CanonicityChip> {
240            bits: bits.iter().map(|b| *b != 0).collect::<Vec<_>>(),
241            bound: bound.unwrap_or(&BigUint::default()).clone(),
242            expected,
243            operation,
244            _marker: PhantomData,
245        };
246        let log2_nb_rows = 10;
247        let public_inputs = vec![vec![], vec![]];
248        match MockProver::run(log2_nb_rows, &circuit, public_inputs) {
249            Ok(prover) => match prover.verify() {
250                Ok(()) => assert!(must_pass),
251                Err(e) => assert!(!must_pass, "Failed verifier with error {e:?}"),
252            },
253            Err(e) => assert!(!must_pass, "Failed prover with error {e:?}"),
254        }
255
256        if cost_model {
257            circuit_to_json(chip_name, op_name, circuit);
258        }
259    }
260
261    /// The output type is u8 instead of bool because, for readability, we
262    /// express the test vectors with integers `0` and `1` instead of
263    /// `false` and `true` (respectively).
264    fn decompose_biguint(n: &BigUint) -> Vec<u8> {
265        (0..(n.bits() as usize)).map(|i| if n.bit(i as u64) { 1 } else { 0 }).collect()
266    }
267
268    pub fn test_canonical<F, Assigned, CanonicityChip>(name: &str)
269    where
270        F: PrimeField + FromUniformBytes<64> + Ord,
271        Assigned::Element: PrimeField,
272        Assigned: Instantiable<F> + InnerConstants + Clone,
273        CanonicityChip: CanonicityInstructions<F, Assigned>
274            + AssertionInstructions<F, Assigned>
275            + AssertionInstructions<F, AssignedBit<F>>
276            + AssignmentInstructions<F, Assigned>
277            + FromScratch<F>,
278    {
279        let m = modulus::<Assigned::Element>();
280        let mut cost_model = true;
281        [
282            (vec![0], true),
283            (vec![1], true),
284            (vec![1, 0, 1], true),
285            (decompose_biguint(&m), false),
286            (decompose_biguint(&(m - BigUint::one())), true),
287            (vec![0; Assigned::Element::NUM_BITS as usize], true),
288            (vec![1; Assigned::Element::NUM_BITS as usize], false),
289            (vec![0; 1 + Assigned::Element::NUM_BITS as usize], false),
290        ]
291        .iter()
292        .for_each(|(bits, expected)| {
293            run::<F, Assigned, CanonicityChip>(
294                bits,
295                None,
296                *expected,
297                Operation::Canonical,
298                true,
299                cost_model,
300                name,
301                "canonical",
302            );
303            cost_model = false;
304            run::<F, Assigned, CanonicityChip>(
305                bits,
306                None,
307                !expected,
308                Operation::Canonical,
309                false,
310                false,
311                "",
312                "",
313            );
314        });
315    }
316
317    pub fn test_le_bits_lower_and_geq<F, Assigned, CanonChip>(name: &str)
318    where
319        F: PrimeField + FromUniformBytes<64> + Ord,
320        Assigned::Element: PrimeField,
321        Assigned: Instantiable<F> + InnerConstants + Clone,
322        CanonChip: CanonicityInstructions<F, Assigned>
323            + AssertionInstructions<F, Assigned>
324            + AssertionInstructions<F, AssignedBit<F>>
325            + AssignmentInstructions<F, Assigned>
326            + FromScratch<F>,
327    {
328        let mut rng = ChaCha8Rng::seed_from_u64(0xc0ffee);
329        let r: BigUint = rng.next_u64().into();
330        let m = modulus::<Assigned::Element>();
331        let mut cost_model = true;
332        [
333            (decompose_biguint(&r), r.clone() - BigUint::one(), true),
334            (decompose_biguint(&r), r.clone(), true),
335            (decompose_biguint(&r), r + BigUint::one(), false),
336            (decompose_biguint(&m), m.clone(), true),
337            (decompose_biguint(&m), m + BigUint::one(), false),
338            (vec![0], BigUint::zero(), true),
339            (vec![1], BigUint::zero(), true),
340            (vec![1, 0, 1], BigUint::from(5u64), true),
341            (vec![1, 0, 1], BigUint::from(6u64), false),
342            (vec![1, 1, 1, 0, 0, 0], BigUint::from(7u64), true),
343            (vec![1, 1, 1, 0, 0, 0], BigUint::from(8u64), false),
344        ]
345        .iter()
346        .for_each(|(bits, bound, geq)| {
347            run::<_, _, CanonChip>(
348                bits,
349                Some(bound),
350                !geq,
351                Operation::Lower,
352                true,
353                cost_model,
354                name,
355                "lt",
356            );
357            run::<_, _, CanonChip>(
358                bits,
359                Some(bound),
360                *geq,
361                Operation::Geq,
362                true,
363                cost_model,
364                name,
365                "geq",
366            );
367            cost_model = false;
368            run::<_, _, CanonChip>(
369                bits,
370                Some(bound),
371                *geq,
372                Operation::Lower,
373                false,
374                false,
375                "",
376                "",
377            );
378            run::<_, _, CanonChip>(
379                bits,
380                Some(bound),
381                !geq,
382                Operation::Geq,
383                false,
384                false,
385                "",
386                "",
387            );
388        });
389    }
390}