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}