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;
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(std::iter::repeat_n(F::one(), runtime_table_offset));
289 evals.extend(std::iter::repeat_n(F::zero(), runtime_len));
290 evals.extend(std::iter::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(std::iter::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(std::iter::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.iter_mut().for_each(|col| {
447 col.extend(std::iter::repeat_n(F::zero(), max_num_entries - col.len()))
448 });
449
450 table_ids.extend(std::iter::repeat_n(
452 F::zero(),
453 max_num_entries - table_ids.len(),
454 ));
455
456 let mut lookup_table_polys: Vec<DP<F>> = vec![];
458 let mut lookup_table8: Vec<E<F, D<F>>> = vec![];
459 for col in lookup_table {
460 let poly = E::<F, D<F>>::from_vec_and_domain(col, domain.d1).interpolate();
461 let eval = poly.evaluate_over_domain_by_ref(domain.d8);
462 lookup_table_polys.push(poly);
463 lookup_table8.push(eval);
464 }
465
466 let (table_ids, table_ids8) = if non_zero_table_id {
469 let table_ids: DP<F> =
470 E::<F, D<F>>::from_vec_and_domain(table_ids, domain.d1).interpolate();
471 let table_ids8: E<F, D<F>> = table_ids.evaluate_over_domain_by_ref(domain.d8);
472 (Some(table_ids), Some(table_ids8))
473 } else {
474 (None, None)
475 };
476
477 let runtime_tables =
479 runtime_tables.map(|rt| rt.into_iter().map(Into::into).collect());
480
481 let configuration = LookupConfiguration::new(lookup_info);
482
483 Ok(Some(Self {
484 lookup_selectors,
485 lookup_table8,
486 lookup_table: lookup_table_polys,
487 table_ids,
488 table_ids8,
489 runtime_selector,
490 runtime_tables,
491 runtime_table_offset,
492 configuration,
493 }))
494 }
495 }
496 }
497}
498
499#[cfg(test)]
500mod tests {
501
502 use super::{LookupError, LookupTable, RuntimeTableCfg};
503 use crate::{
504 circuits::{
505 constraints::ConstraintSystem, gate::CircuitGate, lookup::tables::xor,
506 polynomials::range_check,
507 },
508 error::SetupError,
509 };
510 use mina_curves::pasta::Fp;
511
512 #[test]
513 fn test_colliding_table_ids() {
514 let (_, gates) = CircuitGate::<Fp>::create_multi_range_check(0);
515 let collision_id: i32 = 5;
516
517 let cs = ConstraintSystem::<Fp>::create(gates.clone())
518 .lookup(vec![range_check::gadget::lookup_table()])
519 .build();
520
521 assert!(
522 matches!(
523 cs,
524 Err(SetupError::LookupCreation(
525 LookupError::LookupTableIdCollision { .. }
526 ))
527 ),
528 "LookupConstraintSystem::create(...) must fail due to range table passed twice"
529 );
530
531 let cs = ConstraintSystem::<Fp>::create(gates.clone())
532 .lookup(vec![xor::xor_table()])
533 .build();
534
535 assert!(
536 cs.is_ok(),
537 "LookupConstraintSystem::create(...) must succeed, no duplicates exist"
538 );
539
540 let cs = ConstraintSystem::<Fp>::create(gates.clone())
541 .lookup(vec![
542 LookupTable {
543 id: collision_id,
544 data: vec![vec![From::from(0); 16]],
545 },
546 LookupTable {
547 id: collision_id,
548 data: vec![vec![From::from(1); 16]],
549 },
550 ])
551 .build();
552
553 assert!(
554 matches!(
555 cs,
556 Err(SetupError::LookupCreation(
557 LookupError::LookupTableIdCollision { .. }
558 ))
559 ),
560 "LookupConstraintSystem::create(...) must fail, collision in fixed ids"
561 );
562
563 let cs = ConstraintSystem::<Fp>::create(gates.clone())
564 .runtime(Some(vec![
565 RuntimeTableCfg {
566 id: collision_id,
567 first_column: vec![From::from(0); 16],
568 },
569 RuntimeTableCfg {
570 id: collision_id,
571 first_column: vec![From::from(1); 16],
572 },
573 ]))
574 .build();
575
576 assert!(
577 matches!(
578 cs,
579 Err(SetupError::LookupCreation(
580 LookupError::LookupTableIdCollision { .. }
581 ))
582 ),
583 "LookupConstraintSystem::create(...) must fail, collision in runtime ids"
584 );
585
586 let cs = ConstraintSystem::<Fp>::create(gates.clone())
587 .lookup(vec![LookupTable {
588 id: collision_id,
589 data: vec![vec![From::from(0); 16]],
590 }])
591 .runtime(Some(vec![RuntimeTableCfg {
592 id: collision_id,
593 first_column: vec![From::from(1); 16],
594 }]))
595 .build();
596
597 assert!(
598 cs.is_ok(),
599 "LookupConstraintSystem::create(...) must not fail when there is a collision between runtime and lookup ids"
600 );
601 }
602}