Skip to main content

kimchi/circuits/polynomials/foreign_field_add/
circuitgates.rs

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