kimchi/circuits/lookup/
index.rs

1use super::runtime_tables::{RuntimeTableCfg, RuntimeTableSpec};
2use crate::circuits::{
3    domains::EvaluationDomains,
4    gate::CircuitGate,
5    lookup::{
6        constraints::LookupConfiguration,
7        lookups::{LookupInfo, LookupPattern},
8        tables::LookupTable,
9    },
10};
11use ark_ff::{FftField, PrimeField};
12use ark_poly::{
13    univariate::DensePolynomial as DP, EvaluationDomain, Evaluations as E,
14    Radix2EvaluationDomain as D,
15};
16use o1_utils::{field_helpers::i32_to_field, repeat_n};
17use serde::{de::DeserializeOwned, Deserialize, Serialize};
18use serde_with::serde_as;
19use thiserror::Error;
20
21/// Represents an error found when computing the lookup constraint system
22#[derive(Debug, Error, Clone, Serialize, Deserialize)]
23pub enum LookupError {
24    #[error("One of the lookup tables has columns of different lengths")]
25    InconsistentTableLength,
26    #[error("The combined lookup table is larger than allowed by the domain size. Observed: {length}, expected: {maximum_allowed}")]
27    LookupTableTooLong {
28        length: usize,
29        maximum_allowed: usize,
30    },
31    #[error("The table with id 0 must have an entry of all zeros")]
32    TableIDZeroMustHaveZeroEntry,
33    #[error("Cannot create a combined table since ids for sub-tables are colliding. The collision type is: {collision_type}")]
34    LookupTableIdCollision { collision_type: String },
35}
36
37/// Lookup selectors
38#[derive(Clone, Serialize, Deserialize, Debug, Default)]
39pub struct LookupSelectors<T> {
40    /// XOR pattern lookup selector
41    pub xor: Option<T>,
42    /// Lookup pattern lookup selector
43    pub lookup: Option<T>,
44    /// Range check pattern lookup selector
45    pub range_check: Option<T>,
46    /// Foreign field multiplication pattern lookup selector
47    pub ffmul: Option<T>,
48}
49
50#[serde_as]
51#[derive(Clone, Serialize, Deserialize, Debug, Default)]
52struct LookupSelectorsSerdeAs<F: FftField> {
53    #[serde_as(as = "Option<o1_utils::serialization::SerdeAs>")]
54    pub xor: Option<E<F, D<F>>>,
55    #[serde_as(as = "Option<o1_utils::serialization::SerdeAs>")]
56    pub lookup: Option<E<F, D<F>>>,
57    #[serde_as(as = "Option<o1_utils::serialization::SerdeAs>")]
58    pub range_check: Option<E<F, D<F>>>,
59    #[serde_as(as = "Option<o1_utils::serialization::SerdeAs>")]
60    pub ffmul: Option<E<F, D<F>>>,
61}
62
63impl<F: FftField> serde_with::SerializeAs<LookupSelectors<E<F, D<F>>>>
64    for LookupSelectorsSerdeAs<F>
65{
66    fn serialize_as<S>(val: &LookupSelectors<E<F, D<F>>>, serializer: S) -> Result<S::Ok, S::Error>
67    where
68        S: serde::Serializer,
69    {
70        let repr = LookupSelectorsSerdeAs {
71            xor: val.xor.clone(),
72            lookup: val.lookup.clone(),
73            range_check: val.range_check.clone(),
74            ffmul: val.ffmul.clone(),
75        };
76        repr.serialize(serializer)
77    }
78}
79
80impl<'de, F: FftField> serde_with::DeserializeAs<'de, LookupSelectors<E<F, D<F>>>>
81    for LookupSelectorsSerdeAs<F>
82{
83    fn deserialize_as<Dz>(deserializer: Dz) -> Result<LookupSelectors<E<F, D<F>>>, Dz::Error>
84    where
85        Dz: serde::Deserializer<'de>,
86    {
87        let LookupSelectorsSerdeAs {
88            xor,
89            lookup,
90            range_check,
91            ffmul,
92        } = LookupSelectorsSerdeAs::deserialize(deserializer)?;
93        Ok(LookupSelectors {
94            xor,
95            lookup,
96            range_check,
97            ffmul,
98        })
99    }
100}
101
102impl<T> core::ops::Index<LookupPattern> for LookupSelectors<T> {
103    type Output = Option<T>;
104
105    fn index(&self, index: LookupPattern) -> &Self::Output {
106        match index {
107            LookupPattern::Xor => &self.xor,
108            LookupPattern::Lookup => &self.lookup,
109            LookupPattern::RangeCheck => &self.range_check,
110            LookupPattern::ForeignFieldMul => &self.ffmul,
111        }
112    }
113}
114
115impl<T> core::ops::IndexMut<LookupPattern> for LookupSelectors<T> {
116    fn index_mut(&mut self, index: LookupPattern) -> &mut Self::Output {
117        match index {
118            LookupPattern::Xor => &mut self.xor,
119            LookupPattern::Lookup => &mut self.lookup,
120            LookupPattern::RangeCheck => &mut self.range_check,
121            LookupPattern::ForeignFieldMul => &mut self.ffmul,
122        }
123    }
124}
125
126impl<T> LookupSelectors<T> {
127    pub fn map<U, F: Fn(T) -> U>(self, f: F) -> LookupSelectors<U> {
128        let LookupSelectors {
129            xor,
130            lookup,
131            range_check,
132            ffmul,
133        } = self;
134        // This closure isn't really redundant -- it shields the parameter from a copy -- but
135        // clippy isn't smart enough to figure that out..
136        #[allow(clippy::redundant_closure)]
137        let f = |x| f(x);
138        LookupSelectors {
139            xor: xor.map(f),
140            lookup: lookup.map(f),
141            range_check: range_check.map(f),
142            ffmul: ffmul.map(f),
143        }
144    }
145
146    pub fn as_ref(&self) -> LookupSelectors<&T> {
147        LookupSelectors {
148            xor: self.xor.as_ref(),
149            lookup: self.lookup.as_ref(),
150            range_check: self.range_check.as_ref(),
151            ffmul: self.ffmul.as_ref(),
152        }
153    }
154}
155
156#[serde_as]
157#[derive(Clone, Serialize, Deserialize, Debug)]
158pub struct LookupConstraintSystem<F: FftField> {
159    /// Lookup tables
160    #[serde_as(as = "Vec<o1_utils::serialization::SerdeAs>")]
161    pub lookup_table: Vec<DP<F>>,
162    #[serde_as(as = "Vec<o1_utils::serialization::SerdeAs>")]
163    pub lookup_table8: Vec<E<F, D<F>>>,
164
165    /// Table IDs for the lookup values.
166    /// This may be `None` if all lookups originate from table 0.
167    #[serde_as(as = "Option<o1_utils::serialization::SerdeAs>")]
168    pub table_ids: Option<DP<F>>,
169    #[serde_as(as = "Option<o1_utils::serialization::SerdeAs>")]
170    pub table_ids8: Option<E<F, D<F>>>,
171
172    /// Lookup selectors:
173    /// For each kind of lookup-pattern, we have a selector that's
174    /// 1 at the rows where that pattern should be enforced, and 0 at
175    /// all other rows.
176    #[serde_as(as = "LookupSelectorsSerdeAs<F>")]
177    pub lookup_selectors: LookupSelectors<E<F, D<F>>>,
178
179    /// An optional runtime table selector. It is 0 everywhere,
180    /// except at the rows where the runtime tables apply.
181    #[serde_as(as = "Option<o1_utils::serialization::SerdeAs>")]
182    pub runtime_selector: Option<E<F, D<F>>>,
183
184    /// Optional runtime tables, listed as tuples `(length, id)`.
185    pub runtime_tables: Option<Vec<RuntimeTableSpec>>,
186
187    /// The offset of the runtime table within the concatenated table
188    pub runtime_table_offset: Option<usize>,
189
190    /// Configuration for the lookup constraint.
191    #[serde(bound = "LookupConfiguration<F>: Serialize + DeserializeOwned")]
192    pub configuration: LookupConfiguration<F>,
193}
194
195impl<F: PrimeField> LookupConstraintSystem<F> {
196    /// Create the `LookupConstraintSystem`.
197    ///
198    /// # Errors
199    ///
200    /// Will give error if inputs validation do not match.
201    pub fn create(
202        gates: &[CircuitGate<F>],
203        fixed_lookup_tables: Vec<LookupTable<F>>,
204        runtime_tables: Option<Vec<RuntimeTableCfg<F>>>,
205        domain: &EvaluationDomains<F>,
206        zk_rows: usize,
207    ) -> Result<Option<Self>, LookupError> {
208        //~ 1. If no lookup is used in the circuit, do not create a lookup index
209        match LookupInfo::create_from_gates(gates, runtime_tables.is_some()) {
210            None => Ok(None),
211            Some(lookup_info) => {
212                let d1_size = domain.d1.size();
213
214                // The maximum number of entries that can be provided across all tables.
215                // Since we do not assert the lookup constraint on the final `zk_rows` rows, and
216                // because the row before is used to assert that the lookup argument's final
217                // product is 1, we cannot use those rows to store any values.
218                let max_num_entries = d1_size - zk_rows - 1;
219
220                //~ 2. Get the lookup selectors and lookup tables that are specified implicitly
221                // by the lookup gates.
222                let (lookup_selectors, gate_lookup_tables) =
223                    lookup_info.selector_polynomials_and_tables(domain, gates);
224
225                // Checks whether an iterator contains any duplicates, and if yes, raises
226                // a corresponding LookupTableIdCollision error.
227                fn check_id_duplicates<'a, I: Iterator<Item = &'a i32>>(
228                    iter: I,
229                    msg: &str,
230                ) -> Result<(), LookupError> {
231                    use itertools::Itertools;
232                    match iter.duplicates().collect::<Vec<_>>() {
233                        dups if !dups.is_empty() => Err(LookupError::LookupTableIdCollision {
234                            collision_type: format!("{}: {:?}", msg, dups).to_string(),
235                        }),
236                        _ => Ok(()),
237                    }
238                }
239
240                // If there is a gate using a lookup table, this table must not be added
241                // explicitly to the constraint system.
242                let fixed_gate_joint_ids: Vec<i32> = fixed_lookup_tables
243                    .iter()
244                    .map(|lt| lt.id)
245                    .chain(gate_lookup_tables.iter().map(|lt| lt.id))
246                    .collect();
247                check_id_duplicates(
248                    fixed_gate_joint_ids.iter(),
249                    "duplicates between fixed given and fixed from-gate tables",
250                )?;
251
252                //~ 3. Concatenate explicit runtime lookup tables with the ones (implicitly) used by gates.
253                let mut lookup_tables: Vec<_> = fixed_lookup_tables
254                    .into_iter()
255                    .chain(gate_lookup_tables)
256                    .collect();
257
258                let mut has_table_id_0 = false;
259
260                // if we are using runtime tables
261                let (runtime_table_offset, runtime_selector) =
262                    if let Some(runtime_tables) = &runtime_tables {
263                        // Check duplicates in runtime table ids
264                        let runtime_tables_ids: Vec<i32> =
265                            runtime_tables.iter().map(|rt| rt.id).collect();
266                        check_id_duplicates(runtime_tables_ids.iter(), "runtime table duplicates")?;
267                        // Runtime table IDs /may/ collide with lookup
268                        // table IDs, so we intentionally do not perform another potential check.
269
270                        // save the offset of the end of the table
271                        let mut runtime_table_offset = 0;
272                        for table in &lookup_tables {
273                            runtime_table_offset += table.len();
274                        }
275
276                        // compute the length of the runtime table
277                        let mut runtime_len = 0;
278                        for t in runtime_tables {
279                            runtime_len += t.len();
280                        }
281
282                        // compute the runtime selector
283                        let runtime_selector = {
284                            let mut evals = Vec::with_capacity(d1_size);
285
286                            // it's 1 everywhere, except at the entries where
287                            // the runtime table applies
288                            evals.extend(repeat_n(F::one(), runtime_table_offset));
289                            evals.extend(repeat_n(F::zero(), runtime_len));
290                            evals.extend(repeat_n(
291                                F::one(),
292                                d1_size - runtime_table_offset - runtime_len,
293                            ));
294
295                            // although the last zk_rows are fine
296                            for e in evals.iter_mut().rev().take(zk_rows) {
297                                *e = F::zero();
298                            }
299
300                            E::<F, D<F>>::from_vec_and_domain(evals, domain.d1)
301                                .interpolate()
302                                .evaluate_over_domain(domain.d8)
303                        };
304
305                        // create fixed tables for indexing the runtime tables
306                        for runtime_table in runtime_tables {
307                            let (id, first_column) =
308                                (runtime_table.id, runtime_table.first_column.clone());
309
310                            // record if table ID 0 is used in one of the runtime tables
311                            // note: the check later will still force you to have a fixed table with ID 0
312                            if id == 0 {
313                                has_table_id_0 = true;
314                            }
315
316                            // important: we still need a placeholder column to make sure that
317                            // if all other tables have a single column
318                            // we don't use the second table as table ID column.
319                            let placeholders = vec![F::zero(); first_column.len()];
320                            let data = vec![first_column, placeholders];
321                            let table = LookupTable { id, data };
322                            lookup_tables.push(table);
323                        }
324
325                        (Some(runtime_table_offset), Some(runtime_selector))
326                    } else {
327                        (None, None)
328                    };
329
330                //~ 4. Get the highest number of columns `max_table_width`
331                //~    that a lookup table can have.
332                let max_table_width = lookup_tables
333                    .iter()
334                    .map(|table| table.width())
335                    .max()
336                    .unwrap_or(0);
337
338                let max_table_width = core::cmp::max(
339                    max_table_width,
340                    lookup_info.max_joint_size.try_into().unwrap(),
341                );
342
343                //~ 5. Create the concatenated table of all the fixed lookup tables.
344                //~    It will be of height the size of the domain,
345                //~    and of width the maximum width of any of the lookup tables.
346                //~    In addition, create an additional column to store all the tables' table IDs.
347                //~
348                //~    For example, if you have a table with ID 0
349                //~
350                //~    |       |       |       |
351                //~    | :---: | :---: | :---: |
352                //~    |   1   |   2   |   3   |
353                //~    |   5   |   6   |   7   |
354                //~    |   0   |   0   |   0   |
355                //~
356                //~    and another table with ID 1
357                //~
358                //~    |       |       |
359                //~    | :---: | :---: |
360                //~    |   8   |   9   |
361                //~
362                //~    the concatenated table in a domain of size 5 looks like this:
363                //~
364                //~    |       |       |       |
365                //~    | :---: | :---: | :---: |
366                //~    |   1   |   2   |   3   |
367                //~    |   5   |   6   |   7   |
368                //~    |   0   |   0   |   0   |
369                //~    |   8   |   9   |   0   |
370                //~    |   0   |   0   |   0   |
371                //~
372                //~    with the table id vector:
373                //~
374                //~    | table id |
375                //~    | :------: |
376                //~    |    0     |
377                //~    |    0     |
378                //~    |    0     |
379                //~    |    1     |
380                //~    |    0     |
381                //~
382                //~    To do this, for each table:
383                //~
384                let mut lookup_table = vec![Vec::with_capacity(d1_size); max_table_width];
385                let mut table_ids: Vec<F> = Vec::with_capacity(d1_size);
386
387                let mut non_zero_table_id = false;
388                let mut has_table_id_0_with_zero_entry = false;
389
390                for table in &lookup_tables {
391                    let table_len = table.len();
392
393                    if table.id == 0 {
394                        has_table_id_0 = true;
395                        if table.has_zero_entry() {
396                            has_table_id_0_with_zero_entry = true;
397                        }
398                    } else {
399                        non_zero_table_id = true;
400                    }
401
402                    //~~ * Update the corresponding entries in a table id vector (of size the domain as well)
403                    //~    with the table ID of the table.
404                    let table_id: F = i32_to_field(table.id);
405                    table_ids.extend(repeat_n(table_id, table_len));
406
407                    //~~ * Copy the entries from the table to new rows in the corresponding columns of the concatenated table.
408                    for (i, col) in table.data.iter().enumerate() {
409                        // See GH issue: https://github.com/MinaProtocol/mina/issues/14097
410                        if col.len() != table_len {
411                            return Err(LookupError::InconsistentTableLength);
412                        }
413                        lookup_table[i].extend(col);
414                    }
415
416                    //~~ * Fill in any unused columns with 0 (to match the dummy value)
417                    for lookup_table in lookup_table.iter_mut().skip(table.width()) {
418                        lookup_table.extend(repeat_n(F::zero(), table_len));
419                    }
420                }
421
422                // If a table has ID 0, then it must have a zero entry.
423                // This is for the dummy lookups to work.
424                if has_table_id_0 && !has_table_id_0_with_zero_entry {
425                    return Err(LookupError::TableIDZeroMustHaveZeroEntry);
426                }
427
428                // Note: we use `>=` here to leave space for the dummy value.
429                if lookup_table[0].len() >= max_num_entries {
430                    return Err(LookupError::LookupTableTooLong {
431                        length: lookup_table[0].len(),
432                        maximum_allowed: max_num_entries - 1,
433                    });
434                }
435
436                //~ 6. Pad the end of the concatened table with the dummy value.
437                //     By padding with 0, we constraint the table with ID 0 to
438                //     have a zero entry.
439                //     This is for the rows which do not have a lookup selector,
440                //     see ../../../../book/src/kimchi/lookup.md.
441                //     The zero entry row is contained in the built-in XOR table.
442                //     An error is raised when creating the CS if a user-defined
443                //     table is defined with ID 0 without a row contain zeroes.
444                //     If no such table is used, we artificially add a dummy
445                //     table with ID 0 and a row containing only zeroes.
446                lookup_table
447                    .iter_mut()
448                    .for_each(|col| col.extend(repeat_n(F::zero(), max_num_entries - col.len())));
449
450                //~ 7. Pad the end of the table id vector with 0s.
451                table_ids.extend(repeat_n(F::zero(), max_num_entries - table_ids.len()));
452
453                //~ 8. pre-compute polynomial and evaluation form for the look up tables
454                let mut lookup_table_polys: Vec<DP<F>> = vec![];
455                let mut lookup_table8: Vec<E<F, D<F>>> = vec![];
456                for col in lookup_table {
457                    let poly = E::<F, D<F>>::from_vec_and_domain(col, domain.d1).interpolate();
458                    let eval = poly.evaluate_over_domain_by_ref(domain.d8);
459                    lookup_table_polys.push(poly);
460                    lookup_table8.push(eval);
461                }
462
463                //~ 9. pre-compute polynomial and evaluation form for the table IDs,
464                //~    only if a table with an ID different from zero was used.
465                let (table_ids, table_ids8) = if non_zero_table_id {
466                    let table_ids: DP<F> =
467                        E::<F, D<F>>::from_vec_and_domain(table_ids, domain.d1).interpolate();
468                    let table_ids8: E<F, D<F>> = table_ids.evaluate_over_domain_by_ref(domain.d8);
469                    (Some(table_ids), Some(table_ids8))
470                } else {
471                    (None, None)
472                };
473
474                // store only the length of custom runtime tables in the index
475                let runtime_tables =
476                    runtime_tables.map(|rt| rt.into_iter().map(Into::into).collect());
477
478                let configuration = LookupConfiguration::new(lookup_info);
479
480                Ok(Some(Self {
481                    lookup_selectors,
482                    lookup_table8,
483                    lookup_table: lookup_table_polys,
484                    table_ids,
485                    table_ids8,
486                    runtime_selector,
487                    runtime_tables,
488                    runtime_table_offset,
489                    configuration,
490                }))
491            }
492        }
493    }
494}
495
496#[cfg(test)]
497mod tests {
498
499    use super::{LookupError, LookupTable, RuntimeTableCfg};
500    use crate::{
501        circuits::{
502            constraints::ConstraintSystem, gate::CircuitGate, lookup::tables::xor,
503            polynomials::range_check,
504        },
505        error::SetupError,
506    };
507    use mina_curves::pasta::Fp;
508
509    #[test]
510    fn test_colliding_table_ids() {
511        let (_, gates) = CircuitGate::<Fp>::create_multi_range_check(0);
512        let collision_id: i32 = 5;
513
514        let cs = ConstraintSystem::<Fp>::create(gates.clone())
515            .lookup(vec![range_check::gadget::lookup_table()])
516            .build();
517
518        assert!(
519            matches!(
520                cs,
521                Err(SetupError::LookupCreation(
522                    LookupError::LookupTableIdCollision { .. }
523                ))
524            ),
525            "LookupConstraintSystem::create(...) must fail due to range table passed twice"
526        );
527
528        let cs = ConstraintSystem::<Fp>::create(gates.clone())
529            .lookup(vec![xor::xor_table()])
530            .build();
531
532        assert!(
533            cs.is_ok(),
534            "LookupConstraintSystem::create(...) must succeed, no duplicates exist"
535        );
536
537        let cs = ConstraintSystem::<Fp>::create(gates.clone())
538            .lookup(vec![
539                LookupTable {
540                    id: collision_id,
541                    data: vec![vec![From::from(0); 16]],
542                },
543                LookupTable {
544                    id: collision_id,
545                    data: vec![vec![From::from(1); 16]],
546                },
547            ])
548            .build();
549
550        assert!(
551            matches!(
552                cs,
553                Err(SetupError::LookupCreation(
554                    LookupError::LookupTableIdCollision { .. }
555                ))
556            ),
557            "LookupConstraintSystem::create(...) must fail, collision in fixed ids"
558        );
559
560        let cs = ConstraintSystem::<Fp>::create(gates.clone())
561            .runtime(Some(vec![
562                RuntimeTableCfg {
563                    id: collision_id,
564                    first_column: vec![From::from(0); 16],
565                },
566                RuntimeTableCfg {
567                    id: collision_id,
568                    first_column: vec![From::from(1); 16],
569                },
570            ]))
571            .build();
572
573        assert!(
574            matches!(
575                cs,
576                Err(SetupError::LookupCreation(
577                    LookupError::LookupTableIdCollision { .. }
578                ))
579            ),
580            "LookupConstraintSystem::create(...) must fail, collision in runtime ids"
581        );
582
583        let cs = ConstraintSystem::<Fp>::create(gates.clone())
584            .lookup(vec![LookupTable {
585                id: collision_id,
586                data: vec![vec![From::from(0); 16]],
587            }])
588            .runtime(Some(vec![RuntimeTableCfg {
589                id: collision_id,
590                first_column: vec![From::from(1); 16],
591            }]))
592            .build();
593
594        assert!(
595            cs.is_ok(),
596            "LookupConstraintSystem::create(...) must not fail when there is a collision between runtime and lookup ids"
597        );
598    }
599}