Skip to main content

kimchi/circuits/polynomials/range_check/
circuitgates.rs

1use alloc::{vec, vec::Vec};
2//~ The multi range check gadget is comprised of three circuit gates (`RangeCheck0`,
3//~ `RangeCheck1` and `Zero`) and can perform range checks on three values ($v_0,
4//~ v_1$ and $v_2$) of up to 88 bits each.
5//~
6//~ Values can be copied as inputs to the multi range check gadget in two ways:
7//~
8//~ * (Standard mode) With 3 copies, by copying $v_0, v_1$ and $v_2$ to the first
9//~     cells of the first 3 rows of the gadget.  In this mode the first gate
10//~     coefficient is set to `0`.
11//~ * (Compact mode) With 2 copies, by copying $v_2$ to the first cell of the first
12//~     row and copying $v_{10} = v_0 + 2^{\ell} \cdot v_1$ to the 2nd cell of row 2.
13//~     In this mode the first gate coefficient is set to `1`.
14//~
15//~ The `RangeCheck0` gate can also be used on its own to perform 64-bit range checks by
16//~ constraining witness cells 1-2 to zero.
17//~
18//~ **Byte-order:**
19//~
20//~ * Each cell value is in little-endian byte order
21//~ * Limbs are mapped to columns in big-endian order (i.e. the lowest columns
22//~   contain the highest bits)
23//~ * We also have the highest bits covered by copy constraints and plookups, so that
24//~   we can copy the highest two constraints to zero and get a 64-bit lookup, which
25//~   are envisioned to be a common case
26//~
27//~ The values are decomposed into limbs as follows:
28//~
29//~ * `L` is a 12-bit lookup (or copy) limb,
30//~ * `C` is a 2-bit "crumb" limb (we call half a nybble a crumb).
31//~
32//~ ```text
33//~         <----6----> <------8------>
34//~    v0 = L L L L L L C C C C C C C C
35//~    v1 = L L L L L L C C C C C C C C
36//~         <2> <--4--> <---------------18---------------->
37//~    v2 = C C L L L L C C C C C C C C C C C C C C C C C C
38//~ ```
39//~
40//~ **Witness structure:**
41//~
42//~ | Row | Contents        |
43//~ | --- | --------------- |
44//~ |  0  | $v_0$           |
45//~ |  1  | $v_1$           |
46//~ |  2  | $v_2$           |
47//~ |  3  | $v_0, v_1, v_2$ |
48//~
49//~ * The first 2 rows contain $v_0$ and $v_1$ and their respective decompositions
50//~   into 12-bit and 2-bit limbs
51//~ * The 3rd row contains $v_2$ and part of its decomposition: four 12-bit limbs and
52//~   the 1st 10 crumbs
53//~ * The final row contains $v_0$'s and $v_1$'s 5th and 6th 12-bit limbs as well as the
54//~   remaining 10 crumbs of $v_2$
55//~
56//~ ```admonish
57//~ Because we are constrained to 4 lookups per row, we are forced to postpone
58//~ some lookups of v0 and v1 to the final row.
59//~ ```
60//~
61//~ **Constraints:**
62//~
63//~ For efficiency, the limbs are constrained differently according to their type:
64//~
65//~ * 12-bit limbs are constrained with plookups
66//~ * 2-bit crumbs are constrained with degree-4 constraints $x(x-1)(x-2)(x-3)$
67//~
68//~ **Layout:**
69//~
70//~ This is how the three 88-bit inputs $v_0, v_1$ and $v_2$ are laid out and constrained.
71//~
72//~ * `vipj` is the jth 12-bit limb of value $v_i$
73//~ * `vicj` is the jth 2-bit crumb limb of value $v_i$
74//~
75//~ | Gates | `RangeCheck0`  | `RangeCheck0`  | `RangeCheck1`   | `Zero`          |
76//~ | ----- | -------------- | -------------- | --------------- | --------------- |
77//~ | Rows  |          0     |          1     |          2      |          3      |
78//~ | Cols  |                |                |                 |                 |
79//~ |     0 |         `v0`   |         `v1`   |          `v2`   | crumb   `v2c9`  |
80//~ |  MS:1 | copy    `v0p0` | copy    `v1p0` | optional `v12`  | crumb   `v2c10` |
81//~ |     2 | copy    `v0p1` | copy    `v1p1` | crumb    `v2c0` | crumb   `v2c11` |
82//~ |     3 | plookup `v0p2` | plookup `v1p2` | plookup  `v2p0` | plookup `v0p0`  |
83//~ |     4 | plookup `v0p3` | plookup `v1p3` | plookup  `v2p1` | plookup `v0p1`  |
84//~ |     5 | plookup `v0p4` | plookup `v1p4` | plookup  `v2p2` | plookup `v1p0`  |
85//~ |     6 | plookup `v0p5` | plookup `v1p5` | plookup  `v2p3` | plookup `v1p1`  |
86//~ |     7 | crumb   `v0c0` | crumb   `v1c0` | crumb    `v2c1` | crumb   `v2c12` |
87//~ |     8 | crumb   `v0c1` | crumb   `v1c1` | crumb    `v2c2` | crumb   `v2c13` |
88//~ |     9 | crumb   `v0c2` | crumb   `v1c2` | crumb    `v2c3` | crumb   `v2c14` |
89//~ |    10 | crumb   `v0c3` | crumb   `v1c3` | crumb    `v2c4` | crumb   `v2c15` |
90//~ |    11 | crumb   `v0c4` | crumb   `v1c4` | crumb    `v2c5` | crumb   `v2c16` |
91//~ |    12 | crumb   `v0c5` | crumb   `v1c5` | crumb    `v2c6` | crumb   `v2c17` |
92//~ |    13 | crumb   `v0c6` | crumb   `v1c6` | crumb    `v2c7` | crumb   `v2c18` |
93//~ | LS:14 | crumb   `v0c7` | crumb   `v1c7` | crumb    `v2c8` | crumb   `v2c19` |
94//~
95//~ The 12-bit chunks are constrained with plookups and the 2-bit crumbs are
96//~ constrained with degree-4 constraints of the form $x (x - 1) (x - 2) (x - 3)$.
97//~
98//~ Note that copy denotes a plookup that is deferred to the 4th gate (i.e. `Zero`).
99//~ This is because of the limitation that we have at most 4 lookups per row.
100//~ The copies are constrained using the permutation argument.
101//~
102//~ **Gate types:**
103//~
104//~ Different rows are constrained using different `CircuitGate` types
105//~
106//~ | Row | `CircuitGate` | Purpose                                                            |
107//~ | --- | ------------- | ------------------------------------------------------------------ |
108//~ |   0 | `RangeCheck0` | Partially constrain $v_0$                                          |
109//~ |   1 | `RangeCheck0` | Partially constrain $v_1$                                          |
110//~ |   2 | `RangeCheck1` | Fully constrain $v_2$ (and trigger plookups constraints on row 3)  |
111//~ |   3 | `Zero`        | Complete the constraining of $v_0$ and $v_1$ using lookups         |
112//~
113//~ ```admonish
114//~  Each CircuitGate type corresponds to a unique polynomial and thus is assigned
115//~  its own unique powers of alpha
116//~ ```
117
118use core::marker::PhantomData;
119
120use crate::circuits::{
121    argument::{Argument, ArgumentEnv, ArgumentType},
122    berkeley_columns::BerkeleyChallengeTerm,
123    expr::{
124        constraints::{crumb, ExprOps},
125        Cache,
126    },
127    gate::GateType,
128    polynomial::COLUMNS,
129};
130use ark_ff::PrimeField;
131
132//~
133//~ **`RangeCheck0` - Range check constraints**
134//~
135//~ * This circuit gate is used to partially constrain values $v_0$ and $v_1$
136//~ * Optionally, it can be used on its own as a single 64-bit range check by
137//~   constraining columns 1 and 2 to zero
138//~ * The rest of $v_0$ and $v_1$ are constrained by the lookups in the `Zero` gate row
139//~ * This gate operates on the `Curr` row
140//~
141//~ It uses three different types of constraints:
142//~
143//~ * copy    - copy to another cell (12-bits)
144//~ * plookup - plookup (12-bits)
145//~ * crumb   - degree-4 constraint (2-bits)
146//~
147//~ Given value `v` the layout looks like this
148//~
149//~ | Column | `Curr`        |
150//~ | ------ | ------------- |
151//~ |      0 |         `v`   |
152//~ |      1 | copy    `vp0` |
153//~ |      2 | copy    `vp1` |
154//~ |      3 | plookup `vp2` |
155//~ |      4 | plookup `vp3` |
156//~ |      5 | plookup `vp4` |
157//~ |      6 | plookup `vp5` |
158//~ |      7 | crumb   `vc0` |
159//~ |      8 | crumb   `vc1` |
160//~ |      9 | crumb   `vc2` |
161//~ |     10 | crumb   `vc3` |
162//~ |     11 | crumb   `vc4` |
163//~ |     12 | crumb   `vc5` |
164//~ |     13 | crumb   `vc6` |
165//~ |     14 | crumb   `vc7` |
166//~
167//~ where the notation `vpi` and `vci` defined in the "Layout" section above.
168
169#[derive(Default)]
170pub struct RangeCheck0<F>(PhantomData<F>);
171
172impl<F> Argument<F> for RangeCheck0<F>
173where
174    F: PrimeField,
175{
176    const ARGUMENT_TYPE: ArgumentType = ArgumentType::Gate(GateType::RangeCheck0);
177    const CONSTRAINTS: u32 = 10;
178
179    // Constraints for RangeCheck0
180    //   * Operates on Curr row
181    //   * Range constrain all limbs except vp0 and vp1 (barring plookup constraints, which are done elsewhere)
182    //   * Constrain that combining all limbs equals the limb stored in column 0
183    fn constraint_checks<T: ExprOps<F, BerkeleyChallengeTerm>>(
184        env: &ArgumentEnv<F, T>,
185        _cache: &mut Cache,
186    ) -> Vec<T> {
187        // 1) Apply range constraints on the limbs
188        //    * Columns 1-2 are 12-bit copy constraints
189        //        * They are copied 3 rows ahead (to the final row) and are constrained by lookups
190        //          triggered by RangeCheck1 on the Next row
191        //        * Optionally, they can be constrained to zero to convert the RangeCheck0 gate into
192        //          a single 64-bit range check
193        //    * Columns 3-6 are 12-bit plookup range constraints (these are specified in the lookup gate)
194        //    * Columns 7-14 are 2-bit crumb range constraints
195        let mut constraints = (7..COLUMNS)
196            .map(|i| crumb(&env.witness_curr(i)))
197            .collect::<Vec<T>>();
198
199        // 2) Constrain that the combined limbs equals the value v stored in w(0):
200        //
201        //        w(0) = v = vp0 vp1 vp2 vp3 vp4 vp5 vc0 vc1 vc2 vc3 vc4 vc5 vc6 vc7
202        //
203        //    where the value and limbs are stored in little-endian byte order, but mapped
204        //    to cells in big-endian order.
205        //
206        //    Cols: 0  1   2   3   4   5   6   7   8   9   10  11  12  13  14
207        //    Curr: v  vp0 vp1 vp2 vp3 vp4 vp5 vc0 vc1 vc2 vc3 vc4 vc5 vc6 vc7  <- LSB
208
209        let mut power_of_2 = T::one();
210        let mut sum_of_limbs = T::zero();
211
212        // Sum 2-bit limbs
213        for i in (7..COLUMNS).rev() {
214            sum_of_limbs += power_of_2.clone() * env.witness_curr(i);
215            power_of_2 *= T::from(4u64); // 2 bits
216        }
217
218        // Sum 12-bit limbs
219        for i in (1..=6).rev() {
220            sum_of_limbs += power_of_2.clone() * env.witness_curr(i);
221            power_of_2 *= 4096u64.into(); // 12 bits
222        }
223
224        // Check value v against the sum of limbs
225        constraints.push(sum_of_limbs - env.witness_curr(0));
226
227        // Optional compact limbs format (enabled when coeff[0] == 1, disabled when coeff[1] = 0)
228        //   Constrain decomposition of compact limb next(1)
229        //   next(1) = curr(0) + 2^L * next(0)
230        constraints.push(
231            env.coeff(0)
232                * (env.witness_next(1)
233                    - (env.witness_curr(0) + T::two_to_limb() * env.witness_next(0))),
234        );
235
236        constraints
237    }
238}
239
240//~
241//~ **`RangeCheck1` - Range check constraints**
242//~
243//~ * This circuit gate is used to fully constrain $v_2$
244//~ * It operates on the `Curr` and `Next` rows
245//~
246//~ It uses two different types of constraints:
247//~
248//~ * plookup - plookup (12-bits)
249//~ * crumb   - degree-4 constraint (2-bits)
250//~
251//~ Given value `v2` the layout looks like this
252//~
253//~ | Column | `Curr`          | `Next`        |
254//~ | ------ | --------------- | ------------- |
255//~ |      0 |          `v2`   | crumb `v2c9`  |
256//~ |      1 | optional `v12`  | crumb `v2c10` |
257//~ |      2 | crumb    `v2c0` | crumb `v2c11` |
258//~ |      3 | plookup  `v2p0` | (ignored)     |
259//~ |      4 | plookup  `v2p1` | (ignored)     |
260//~ |      5 | plookup  `v2p2` | (ignored)     |
261//~ |      6 | plookup  `v2p3` | (ignored)     |
262//~ |      7 | crumb    `v2c1` | crumb `v2c12` |
263//~ |      8 | crumb    `v2c2` | crumb `v2c13` |
264//~ |      9 | crumb    `v2c3` | crumb `v2c14` |
265//~ |     10 | crumb    `v2c4` | crumb `v2c15` |
266//~ |     11 | crumb    `v2c5` | crumb `v2c16` |
267//~ |     12 | crumb    `v2c6` | crumb `v2c17` |
268//~ |     13 | crumb    `v2c7` | crumb `v2c18` |
269//~ |     14 | crumb    `v2c8` | crumb `v2c19` |
270//~
271//~ where the notation `v2ci` and `v2pi` defined in the "Layout" section above.
272
273#[derive(Default)]
274pub struct RangeCheck1<F>(PhantomData<F>);
275
276impl<F> Argument<F> for RangeCheck1<F>
277where
278    F: PrimeField,
279{
280    const ARGUMENT_TYPE: ArgumentType = ArgumentType::Gate(GateType::RangeCheck1);
281    const CONSTRAINTS: u32 = 21;
282
283    // Constraints for RangeCheck1
284    //   * Operates on Curr and Next row
285    //   * Range constrain all limbs (barring plookup constraints, which are done elsewhere)
286    //   * Constrain that combining all limbs equals the value v2 stored in row Curr, column 0
287    fn constraint_checks<T: ExprOps<F, BerkeleyChallengeTerm>>(
288        env: &ArgumentEnv<F, T>,
289        _cache: &mut Cache,
290    ) -> Vec<T> {
291        // 1) Apply range constraints on limbs for Curr row
292        //    * Column 2 is a 2-bit crumb
293        let mut constraints = vec![crumb(&env.witness_curr(2))];
294
295        //    * Columns 3-6 are 12-bit plookup range constraints (these are specified
296        //      in the lookup gate)
297        //    * Columns 7-14 are 2-bit crumb range constraints
298        constraints.append(
299            &mut (7..COLUMNS)
300                .map(|i| crumb(&env.witness_curr(i)))
301                .collect::<Vec<T>>(),
302        );
303
304        // 2) Apply range constraints on limbs for Next row
305        //    * Columns 0-2 are 2-bit crumbs
306        constraints.append(
307            &mut (0..=2)
308                .map(|i| crumb(&env.witness_next(i)))
309                .collect::<Vec<T>>(),
310        );
311        //    * Columns 3-6 are 12-bit plookup range constraints for v0 and v1 (these
312        //      are specified in the lookup gate)
313        //    * Columns 7-14 are more 2-bit crumbs
314        constraints.append(
315            &mut (7..COLUMNS)
316                .map(|i| crumb(&env.witness_next(i)))
317                .collect::<Vec<T>>(),
318        );
319
320        // 2) Constrain that the combined limbs equals the value v2 stored in w(0) where
321        //
322        //    w(0) = v2 = vc0 vc1 vp0 vp1 vp2 vp3 vc2 vc3 vc4 vc5 vc6 vc7 vc8 vc9 vc10 vc11 vc12
323        //                vc13 vc14 vc15 vc16 vc17 vc18 vc19
324        //
325        //    where the value and limbs are stored in little-endian byte order, but mapped
326        //    to cells in big-endian order.
327        //
328        //          0  1   2   3   4   5    6    7    8    9    10   11   12   13   14
329        //    Curr  v2 vc0 vc1 vp0 vp1 vp2  vp3  vc2  vc3  vc4  vc5  vc6  vc7  vc8  vc9
330        //    Next                     vc10 vc11 vc12 vc13 vc14 vc15 vc16 vc17 vc18 vc19 <- LSB
331
332        let mut power_of_2 = T::one();
333        let mut sum_of_limbs = T::zero();
334
335        // Next row: Sum 2-bit limbs
336        for i in (7..COLUMNS).rev() {
337            sum_of_limbs += power_of_2.clone() * env.witness_next(i);
338            power_of_2 *= 4u64.into(); // 2 bits
339        }
340
341        // Next row:  Sum remaining 2-bit limbs v2c9, v2c10, and v2c11 (reverse order)
342        for i in (0..=2).rev() {
343            sum_of_limbs += power_of_2.clone() * env.witness_next(i);
344            power_of_2 *= 4u64.into(); // 2 bits
345        }
346
347        // Curr row:  Sum 2-bit limbs
348        for i in (7..COLUMNS).rev() {
349            sum_of_limbs += power_of_2.clone() * env.witness_curr(i);
350            power_of_2 *= 4u64.into(); // 2 bits
351        }
352
353        // Curr row: Sum 12-bit limbs
354        for i in (3..=6).rev() {
355            sum_of_limbs += power_of_2.clone() * env.witness_curr(i);
356            power_of_2 *= 4096u64.into(); // 12 bits
357        }
358
359        // Curr row:  Add remaining 2-bit limb v2c0 to sum
360        sum_of_limbs += power_of_2.clone() * env.witness_curr(2);
361
362        // Check value v2 against the sum of limbs
363        constraints.push(sum_of_limbs - env.witness_curr(0));
364
365        constraints
366    }
367}