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}