kimchi/circuits/polynomials/foreign_field_add/
circuitgates.rs

1//! Foreign field addition gate.
2
3use crate::circuits::{
4    argument::{Argument, ArgumentEnv, ArgumentType},
5    berkeley_columns::BerkeleyChallengeTerm,
6    expr::{
7        constraints::{compact_limb, ExprOps},
8        Cache,
9    },
10    gate::GateType,
11    polynomials::foreign_field_common::LIMB_COUNT,
12};
13use ark_ff::PrimeField;
14use core::{array, marker::PhantomData};
15
16//~ These circuit gates are used to constrain that
17//~
18//~ ```text
19//~ left_input +/- right_input = field_overflow * foreign_modulus + result
20//~```
21//~
22//~ ##### Documentation
23//~
24//~  For more details please see the [Foreign Field Addition](../kimchi/foreign_field_add.md) chapter.
25//~
26//~ ##### Mapping
27//~
28//~  To make things clearer, the following mapping between the variable names
29//~  used in the code and those of the RFC document can be helpful.
30//~
31//~ ```text
32//~     left_input_lo -> a0  right_input_lo -> b0  result_lo -> r0  bound_lo -> u0
33//~     left_input_mi -> a1  right_input_mi -> b1  result_mi -> r1  bound_mi -> u1
34//~     left_input_hi -> a2  right_input_hi -> b2  result_hi -> r2  bound_hi -> u2
35//~
36//~     field_overflow  -> q
37//~     sign            -> s
38//~     carry_lo        -> c0
39//~     carry_mi        -> c1
40//~     bound_carry_lo  -> k0
41//~     bound_carry_mi  -> k1
42//~```
43//~
44//~ Note: Our limbs are 88-bit long. We denote with:
45//~
46//~ * `lo` the least significant limb (in little-endian, this is from 0 to 87)
47//~ * `mi` the middle limb            (in little-endian, this is from 88 to 175)
48//~ * `hi` the most significant limb  (in little-endian, this is from 176 to 263)
49//~
50//~ Let `left_input_lo`, `left_input_mi`, `left_input_hi` be 88-bit limbs of the left element
51//~
52//~ Let `right_input_lo`, `right_input_mi`, `right_input_hi` be 88-bit limbs of the right element
53//~
54//~ Let `foreign_modulus_lo`, `foreign_modulus_mi`, `foreign_modulus_hi` be 88-bit limbs of the foreign modulus
55//~
56//~ Then the limbs of the result are
57//~
58//~ * `result_lo = left_input_lo +/- right_input_lo - field_overflow * foreign_modulus_lo - 2^{88} * result_carry_lo`
59//~ * `result_mi = left_input_mi +/- right_input_mi - field_overflow * foreign_modulus_mi - 2^{88} * result_carry_mi + result_carry_lo`
60//~ * `result_hi = left_input_hi +/- right_input_hi - field_overflow * foreign_modulus_hi + result_carry_mi`
61//~
62//~ `field_overflow` $=0$ or $1$ or $-1$ handles overflows in the field
63//~
64//~ `result_carry_i` $= -1, 0, 1$ are auxiliary variables that handle carries between limbs
65//~
66//~ Apart from the range checks of the chained inputs, we need to do an additional range check for a final bound
67//~ to make sure that the result is less than the modulus, by adding `2^{3*88} - foreign_modulus` to it.
68//~ (This can be computed easily from the limbs of the modulus)
69//~ Note that `2^{264}` as limbs represents: (0, 0, 0, 1) then:
70//~
71//~ The upper-bound check can be calculated as:
72//~
73//~ * `bound_lo = result_lo - foreign_modulus_lo - bound_carry_lo * 2^{88}`
74//~ * `bound_mi = result_mi - foreign_modulus_mi - bound_carry_mi * 2^{88} + bound_carry_lo`
75//~ * `bound_hi = result_hi - foreign_modulus_hi + 2^{88} + bound_carry_mi`
76//~
77//~ Which is equivalent to another foreign field addition with right input 2^{264}, q = 1 and s = 1
78//~
79//~ * `bound_lo = result_lo + s *      0 - q * foreign_modulus_lo - bound_carry_lo * 2^{88}`
80//~ * `bound_mi = result_mi + s *      0 - q * foreign_modulus_mi - bound_carry_mi * 2^{88} + bound_carry_lo`
81//~ * `bound_hi = result_hi + s * 2^{88} - q * foreign_modulus_hi                           + bound_carry_mi`
82//~
83//~ `bound_carry_i` $= 0$ or $1$ or $-1$ are auxiliary variables that handle carries between limbs
84//~
85//~ The range check of `bound` can be skipped until the end of the operations
86//~ and `result` is an intermediate value that is unused elsewhere (since the final `result`
87//~ must have had the right amount of moduli subtracted along the way, meaning a multiple of the modulus).
88//~ In other words, intermediate results could potentially give a valid witness that satisfies the constraints
89//~ but where the result is larger than the modulus (yet smaller than 2^{264}). The reason that we have a
90//~ final bound check is to make sure that the final result (`min_result`) is indeed the minimum one
91//~ (meaning less than the modulus).
92//~
93//~ A more optimized version of these constraints is able to reduce by 2 the number of constraints and
94//~ by 1 the number of witness cells needed. The idea is to condense the low and middle limbs in one longer
95//~ limb of 176 bits (which fits inside our native field) and getting rid of the low carry flag.
96//~ With this idea in mind, the sole carry flag we need is the one located between the middle and the high limbs.
97//~
98//~ ##### Layout
99//~
100//~ The sign of the operation (whether it is an addition or a subtraction) is stored in the fourth coefficient as
101//~ a value +1 (for addition) or -1 (for subtraction). The first 3 coefficients are the 3 limbs of the foreign modulus.
102//~ One could lay this out as a double-width gate for chained foreign additions and a final row, e.g.:
103//~
104//~ | col | `ForeignFieldAdd`        | chain `ForeignFieldAdd` | final `ForeignFieldAdd` | final `Zero`      |
105//~ | --- | ------------------------ | ----------------------- | ----------------------- | ----------------- |
106//~ |   0 | `left_input_lo`  (copy)  | `result_lo` (copy)      | `min_result_lo` (copy)  | `bound_lo` (copy) |
107//~ |   1 | `left_input_mi`  (copy)  | `result_mi` (copy)      | `min_result_mi` (copy)  | `bound_mi` (copy) |
108//~ |   2 | `left_input_hi`  (copy)  | `result_hi` (copy)      | `min_result_hi` (copy)  | `bound_hi` (copy) |
109//~ |   3 | `right_input_lo` (copy)  |                         |  0              (check) |                   |
110//~ |   4 | `right_input_mi` (copy)  |                         |  0              (check) |                   |
111//~ |   5 | `right_input_hi` (copy)  |                         |  2^88           (check) |                   |
112//~ |   6 | `field_overflow` (copy?) |                         |  1              (check) |                   |
113//~ |   7 | `carry`                  |                         | `bound_carry`           |                   |
114//~ |   8 |                          |                         |                         |                   |
115//~ |   9 |                          |                         |                         |                   |
116//~ |  10 |                          |                         |                         |                   |
117//~ |  11 |                          |                         |                         |                   |
118//~ |  12 |                          |                         |                         |                   |
119//~ |  13 |                          |                         |                         |                   |
120//~ |  14 |                          |                         |                         |                   |
121//~
122//~ We reuse the foreign field addition gate for the final bound check since this is an addition with a
123//~ specific parameter structure. Checking that the correct right input, overflow, and overflow are used shall
124//~ be done by copy constraining these values with a public input value. One could have a specific gate
125//~ for just this check requiring less constrains, but the cost of adding one more selector gate outweights
126//~ the savings of one row and a few constraints of difference.
127//~
128//~ ##### Integration
129//~
130//~ * Copy final overflow bit from public input containing value 1
131//~ * Range check the final bound
132//~
133
134/// Implementation of the foreign field addition gate
135/// - Operates on Curr and Next rows.
136#[derive(Default)]
137pub struct ForeignFieldAdd<F>(PhantomData<F>);
138
139impl<F> Argument<F> for ForeignFieldAdd<F>
140where
141    F: PrimeField,
142{
143    const ARGUMENT_TYPE: ArgumentType = ArgumentType::Gate(GateType::ForeignFieldAdd);
144    const CONSTRAINTS: u32 = 4;
145
146    fn constraint_checks<T: ExprOps<F, BerkeleyChallengeTerm>>(
147        env: &ArgumentEnv<F, T>,
148        _cache: &mut Cache,
149    ) -> Vec<T> {
150        let foreign_modulus: [T; LIMB_COUNT] = array::from_fn(|i| env.coeff(i));
151
152        // stored as coefficient for better correspondance with the relation being proved
153        // this reduces the number of copy constraints needed to check the operation
154        // it also allows the final bound check to copy the overflow bit to be 1
155        // because otherwise it did not fit in the first 7 columns of the row
156        let sign = env.coeff(3);
157
158        let left_input_lo = env.witness_curr(0);
159        let left_input_mi = env.witness_curr(1);
160        let left_input_hi = env.witness_curr(2);
161
162        let right_input_lo = env.witness_curr(3);
163        let right_input_mi = env.witness_curr(4);
164        let right_input_hi = env.witness_curr(5);
165
166        // sign in <7 to be able to check against public input of opcodes
167        let field_overflow = env.witness_curr(6);
168
169        // Result carry bits for limb overflows / underflows.
170        let carry = env.witness_curr(7);
171
172        let result_lo = env.witness_next(0);
173        let result_mi = env.witness_next(1);
174        let result_hi = env.witness_next(2);
175
176        // Sign flag is 1 or -1
177        // NOTE: we used to check this because sign was in the witness,
178        // but now it is publicly checkable as part of the relation itself
179
180        // Field overflow flag is 0 or s
181        let mut checks = vec![field_overflow.clone() * (field_overflow.clone() - sign.clone())];
182
183        // Constraints to check the carry flag is -1, 0, or 1.
184        checks.push(is_carry(&carry));
185
186        // Auxiliary inline function to obtain the constraints of a foreign field addition result
187
188        // a_bot = a_0 + a_1 * 2^88
189        // b_bot = b_0 + b_1 * 2^88
190        // f_bot = f_0 + f_1 * 2^88
191        // r_bot = a_bot + s * b_bot - q * f_bot - c * 2^176
192        let result_bot = compact_limb(&left_input_lo, &left_input_mi)
193            + sign.clone() * compact_limb(&right_input_lo, &right_input_mi)
194            - field_overflow.clone() * compact_limb(&foreign_modulus[0], &foreign_modulus[1])
195            - carry.clone() * T::two_to_2limb();
196        // r_top = a_2 + s * b_2 - q * f_2 + c
197        let result_top = left_input_hi + sign * right_input_hi
198            - field_overflow * foreign_modulus[2].clone()
199            + carry;
200        // Result values match
201        // r_bot = r_0 + r_1 * 2^88
202        // r_top = r_2
203        checks.push(result_bot - compact_limb(&result_lo, &result_mi));
204        checks.push(result_top - result_hi);
205
206        checks
207    }
208}
209
210// Auxiliary function to obtain the constraints to check a carry flag
211fn is_carry<F: PrimeField, T: ExprOps<F, BerkeleyChallengeTerm>>(flag: &T) -> T {
212    // Carry bits are -1, 0, or 1.
213    flag.clone() * (flag.clone() - T::one()) * (flag.clone() + T::one())
214}