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}