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#[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#[derive(Clone, Serialize, Deserialize, Debug, Default)]
39pub struct LookupSelectors<T> {
40 pub xor: Option<T>,
42 pub lookup: Option<T>,
44 pub range_check: Option<T>,
46 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 #[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 #[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 #[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 #[serde_as(as = "LookupSelectorsSerdeAs<F>")]
177 pub lookup_selectors: LookupSelectors<E<F, D<F>>>,
178
179 #[serde_as(as = "Option<o1_utils::serialization::SerdeAs>")]
182 pub runtime_selector: Option<E<F, D<F>>>,
183
184 pub runtime_tables: Option<Vec<RuntimeTableSpec>>,
186
187 pub runtime_table_offset: Option<usize>,
189
190 #[serde(bound = "LookupConfiguration<F>: Serialize + DeserializeOwned")]
192 pub configuration: LookupConfiguration<F>,
193}
194
195impl<F: PrimeField> LookupConstraintSystem<F> {
196 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 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 let max_num_entries = d1_size - zk_rows - 1;
219
220 let (lookup_selectors, gate_lookup_tables) =
223 lookup_info.selector_polynomials_and_tables(domain, gates);
224
225 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 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 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 let (runtime_table_offset, runtime_selector) =
262 if let Some(runtime_tables) = &runtime_tables {
263 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 let mut runtime_table_offset = 0;
272 for table in &lookup_tables {
273 runtime_table_offset += table.len();
274 }
275
276 let mut runtime_len = 0;
278 for t in runtime_tables {
279 runtime_len += t.len();
280 }
281
282 let runtime_selector = {
284 let mut evals = Vec::with_capacity(d1_size);
285
286 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 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 for runtime_table in runtime_tables {
307 let (id, first_column) =
308 (runtime_table.id, runtime_table.first_column.clone());
309
310 if id == 0 {
313 has_table_id_0 = true;
314 }
315
316 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 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 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 let table_id: F = i32_to_field(table.id);
405 table_ids.extend(repeat_n(table_id, table_len));
406
407 for (i, col) in table.data.iter().enumerate() {
409 if col.len() != table_len {
411 return Err(LookupError::InconsistentTableLength);
412 }
413 lookup_table[i].extend(col);
414 }
415
416 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 has_table_id_0 && !has_table_id_0_with_zero_entry {
425 return Err(LookupError::TableIDZeroMustHaveZeroEntry);
426 }
427
428 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 lookup_table
447 .iter_mut()
448 .for_each(|col| col.extend(repeat_n(F::zero(), max_num_entries - col.len())));
449
450 table_ids.extend(repeat_n(F::zero(), max_num_entries - table_ids.len()));
452
453 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 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 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}