1use std::iter;
6
7use binius_compute::Allocator;
8use binius_field::{BinaryField, Divisible, PackedField};
9use binius_ip::{
10 fracaddcheck::FracAddEvalClaim,
11 logup_star::{
12 LogupOutput, LogupTableOutput, LogupTransparentOutput, LogupTransparentTableOutput,
13 },
14};
15use binius_math::{FieldBuffer, FieldSlice, FieldVec, univariate::evaluate_univariate};
16use binius_utils::{checked_arithmetics::log2_ceil_usize, rayon::prelude::*};
17use itertools::izip;
18
19use super::{
20 pushforward::{PushforwardOutput, TableWitness, prove_pushforward},
21 witness,
22};
23use crate::{
24 channel::IPProverChannel,
25 fracaddcheck::{self, FracAddCircuit, fraction::Fraction, unpad_leaf_claim},
26};
27
28#[derive(Debug, Clone, Copy)]
30pub struct Looker<'a, F> {
31 pub index: &'a [usize],
33 pub eval_point: &'a [F],
35 pub eval_claim: F,
37}
38
39pub struct TableLookup<'a, P: PackedField> {
41 pub table: FieldSlice<'a, P>,
43 pub lookers: Vec<Looker<'a, P::Scalar>>,
46}
47
48pub fn prove<'a, A, F, P>(
94 alloc: &A,
95 gamma: F,
96 tables: impl IntoIterator<Item = TableLookup<'a, P>>,
97 channel: &mut impl IPProverChannel<F>,
98) -> LogupOutput<F>
99where
100 A: Allocator,
101 F: BinaryField<Underlier: Divisible<u64>>,
102 P: PackedField<Scalar = F> + 'a,
103{
104 let tables = tables.into_iter().collect::<Vec<_>>();
105
106 let (numerators, pushforwards) = witness::combined_lookers::<A, F, P>(alloc, gamma, &tables);
112
113 let pushforward_slices = pushforwards
116 .iter()
117 .map(FieldBuffer::as_view)
118 .collect::<Vec<_>>();
119 prove_reduction(alloc, gamma, &tables, numerators, &pushforward_slices, channel)
120}
121
122pub fn prove_transparent<'a, A, F, P>(
137 alloc: &A,
138 gamma: F,
139 tables: impl IntoIterator<Item = TableLookup<'a, P>>,
140 channel: &mut impl IPProverChannel<F>,
141) -> LogupTransparentOutput<F>
142where
143 A: Allocator,
144 F: BinaryField<Underlier: Divisible<u64>>,
145 P: PackedField<Scalar = F> + 'a,
146{
147 let tables = tables.into_iter().collect::<Vec<_>>();
148 let (numerators, pushforwards) = witness::combined_lookers::<A, F, P>(alloc, gamma, &tables);
149 let pushforward_slices = pushforwards
150 .iter()
151 .map(FieldBuffer::as_view)
152 .collect::<Vec<_>>();
153 prove_reduction_transparent(alloc, gamma, &tables, numerators, &pushforward_slices, channel)
154}
155
156#[tracing::instrument(
180 skip_all,
181 level = "debug",
182 name = "logup* reduction",
183 fields(n_tables = tables.len())
184)]
185pub fn prove_reduction<A, F, P>(
186 alloc: &A,
187 gamma: F,
188 tables: &[TableLookup<'_, P>],
189 numerators: Vec<Vec<FieldVec<P, A>>>,
190 pushforwards: &[FieldSlice<'_, P>],
191 channel: &mut impl IPProverChannel<F>,
192) -> LogupOutput<F>
193where
194 A: Allocator,
195 F: BinaryField<Underlier: Divisible<u64>>,
196 P: PackedField<Scalar = F>,
197{
198 let LogupTransparentOutput {
199 index_eval_point,
200 tables: open_tables,
201 } = prove_reduction_transparent(alloc, gamma, tables, numerators, pushforwards, channel);
202
203 let table_witnesses =
206 izip!(tables, pushforwards, &open_tables).map(|(table, &pushforward, open)| TableWitness {
207 table: table.table,
208 pushforward,
209 eval_claim: open.product_claim,
210 pushforward_eval_claim: open.pushforward_eval_claim,
211 pushforward_eval_point: &open.pushforward_eval_point,
212 });
213 let PushforwardOutput {
214 table_eval_point,
215 table_eval_claims,
216 pushforward_eval_claims,
217 } = prove_pushforward(alloc, table_witnesses, channel);
218
219 LogupOutput {
220 table_eval_point,
221 index_eval_point,
222 tables: izip!(table_eval_claims, pushforward_eval_claims, open_tables)
223 .map(|(eval_claim, pushforward_claim, open)| LogupTableOutput {
224 eval_claim,
225 pushforward_claim,
226 index_eval_claims: open.index_eval_claims,
227 })
228 .collect(),
229 }
230}
231
232#[tracing::instrument(
246 skip_all,
247 level = "debug",
248 name = "logup* fracadd reduction",
249 fields(n_tables = tables.len())
250)]
251pub fn prove_reduction_transparent<A, F, P>(
252 alloc: &A,
253 gamma: F,
254 tables: &[TableLookup<'_, P>],
255 numerators: Vec<Vec<FieldVec<P, A>>>,
256 pushforwards: &[FieldSlice<'_, P>],
257 channel: &mut impl IPProverChannel<F>,
258) -> LogupTransparentOutput<F>
259where
260 A: Allocator,
261 F: BinaryField<Underlier: Divisible<u64>>,
262 P: PackedField<Scalar = F>,
263{
264 assert!(!tables.is_empty(), "at least one table is required");
265 assert!(
267 tables.iter().all(|table| table.table.log_len() > 0),
268 "every table must have at least one variable"
269 );
270 assert!(
271 tables.iter().all(|table| !table.lookers.is_empty()),
272 "every table must have at least one looker"
273 );
274
275 let n_lookers = tables
276 .iter()
277 .map(|table| table.lookers.len())
278 .sum::<usize>();
279 let k = log2_ceil_usize(n_lookers + tables.len());
281
282 let cs = channel.sample_many(tables.len());
286
287 let circuits_guard = tracing::debug_span!("Build fracadd circuits").entered();
293 let flat_lookers = tables
296 .iter()
297 .zip(&cs)
298 .flat_map(|(table, &c)| table.lookers.iter().map(move |looker| (looker, c)))
299 .collect::<Vec<_>>();
300 let flat_numerators = numerators.into_iter().flatten().collect::<Vec<_>>();
301 let (mut provers, mut roots): (Vec<_>, Vec<_>) = (flat_lookers.as_slice(), flat_numerators)
302 .into_par_iter()
303 .map(|(&(looker, c), numerator)| {
304 let den = witness::looker_denominator::<A, F, P>(alloc, c, looker.index);
305 let (prover, root) = FracAddCircuit::build(
307 looker.eval_point.len(),
308 alloc,
309 Fraction::new(numerator, den),
310 );
311 (prover, root.as_ref().map(|buffer| buffer.get(0)))
312 })
313 .unzip();
314 for ((&c, table), pushforward) in iter::zip(iter::zip(&cs, tables), pushforwards) {
317 let table = table.table;
318 let table_den = witness::table_denominator::<A, F, P>(alloc, c, table.log_len());
319 let (table_prover, table_root) = FracAddCircuit::build(
322 table.log_len(),
323 alloc,
324 Fraction::new(FieldBuffer::from_view_in(alloc, pushforward.as_view()), table_den),
325 );
326 provers.push(table_prover);
327 roots.push(table_root.as_ref().map(|buffer| buffer.get(0)));
328 }
329
330 let (top_prover, top_root) = FracAddCircuit::build(k, alloc, {
338 let (mut root_nums, mut root_dens): (Vec<_>, Vec<_>) =
339 roots.iter().map(|&root| (root.num, root.den)).unzip();
340 root_nums.resize(1 << k, Fraction::ZERO.num);
342 root_dens.resize(1 << k, Fraction::ZERO.den);
343 Fraction::new(
344 FieldBuffer::<P, _>::from_values_in(alloc, &root_nums),
345 FieldBuffer::<P, _>::from_values_in(alloc, &root_dens),
346 )
347 });
348 let root_den = top_root.den.get(0);
349 drop(circuits_guard);
350
351 assert_eq!(
358 top_root.num.get(0),
359 F::ZERO,
360 "the lookup identities must hold: each table's looker fractions must sum to its own"
361 );
362 channel.send_one(root_den);
363
364 let gkr_guard = tracing::debug_span!("Combined GKR").entered();
369 let top_claim = top_prover.prove(
370 FracAddEvalClaim {
371 num_eval: F::ZERO,
372 den_eval: root_den,
373 point: Vec::new(),
374 },
375 channel,
376 );
377 let selector_point = top_claim.point;
378 let fracaddcheck::BatchProveOutput {
379 eval_point,
380 fractions,
381 } = fracaddcheck::batch_prove_unequal_depths(provers, roots, selector_point, channel);
382 drop(gkr_guard);
383
384 let node_point = &eval_point[k..];
387 let n_layers = node_point.len();
388 let (looker_fractions, table_fractions) = fractions.split_at(n_lookers);
389
390 let mut remaining_fractions = looker_fractions;
394 let index_eval_claims = iter::zip(tables, &cs)
395 .map(|(table, &c)| {
396 let (table_fractions, rest) = remaining_fractions.split_at(table.lookers.len());
397 remaining_fractions = rest;
398 iter::zip(table_fractions, &table.lookers)
399 .map(|(&fraction, looker)| {
400 let leaf =
401 unpad_leaf_claim(fraction, node_point, n_layers - looker.eval_point.len());
402 c - leaf.den_eval
403 })
404 .collect::<Vec<_>>()
405 })
406 .collect::<Vec<_>>();
407 let max_n = tables
411 .iter()
412 .flat_map(|table| &table.lookers)
413 .map(|looker| looker.eval_point.len())
414 .max()
415 .expect("every table has at least one looker");
416 let index_eval_point = node_point[n_layers - max_n..].to_vec();
417
418 let table_leaves = iter::zip(table_fractions, tables)
421 .map(|(&fraction, table)| {
422 unpad_leaf_claim(fraction, node_point, n_layers - table.table.log_len())
423 })
424 .collect::<Vec<_>>();
425
426 let pushforward_evals = table_leaves
430 .iter()
431 .map(|leaf| leaf.num_eval)
432 .collect::<Vec<_>>();
433 for claims in &index_eval_claims {
434 channel.send_many(claims);
435 }
436 channel.send_many(&pushforward_evals);
437
438 LogupTransparentOutput {
441 index_eval_point,
442 tables: izip!(tables, table_leaves, index_eval_claims)
443 .map(|(table, leaf, index_eval_claims)| {
444 let claims = table
447 .lookers
448 .iter()
449 .map(|looker| looker.eval_claim)
450 .collect::<Vec<_>>();
451 LogupTransparentTableOutput {
452 pushforward_eval_point: leaf.point,
453 pushforward_eval_claim: leaf.num_eval,
454 product_claim: evaluate_univariate(&claims, &gamma),
455 index_eval_claims,
456 }
457 })
458 .collect(),
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use binius_compute::GlobalAllocator;
465 use binius_field::{
466 BinaryField1b, ExtensionField, Field,
467 arch::{OptimalB128, OptimalPackedB128},
468 util::powers,
469 };
470 use binius_ip::{channel::IPVerifierChannel, logup_star};
471 use binius_math::{
472 FieldBuffer,
473 inner_product::inner_product_buffers,
474 multilinear::{eq::eq_ind_partial_eval_scalars, evaluate::evaluate},
475 test_utils::{random_field_buffer, random_scalars},
476 };
477 use binius_transcript::{ProverTranscript, fiat_shamir::HasherChallenger};
478 use rand::prelude::*;
479
480 use super::*;
481
482 type F = OptimalB128;
483 type P = OptimalPackedB128;
484 type StdChallenger = HasherChallenger<sha2::Sha256>;
485
486 fn iota(j: usize, m: usize) -> F {
490 (0..m)
491 .filter(|t| (j >> t) & 1 == 1)
492 .map(<F as ExtensionField<BinaryField1b>>::basis)
493 .fold(F::ZERO, |acc, b| acc + b)
494 }
495
496 struct TestLooker {
498 index: Vec<usize>,
499 eval_point: Vec<F>,
500 eq_r: Vec<F>,
501 eval_claim: F,
502 }
503
504 struct TestTable {
506 values: FieldBuffer<P>,
507 lookers: Vec<TestLooker>,
508 }
509
510 fn random_looker(rng: &mut StdRng, n: usize, table_values: &FieldBuffer<P>) -> TestLooker {
512 let m = table_values.log_len();
513 let index = (0..(1usize << n))
514 .map(|_| rng.random_range(0..(1usize << m)))
515 .collect::<Vec<_>>();
516 let eval_point = random_scalars::<F>(&mut *rng, n);
517
518 let eq_r = eq_ind_partial_eval_scalars(&eval_point);
520 let eval_claim = index
521 .iter()
522 .zip(&eq_r)
523 .map(|(&j, &eq)| eq * table_values.get(j))
524 .fold(F::ZERO, |acc, t| acc + t);
525
526 TestLooker {
527 index,
528 eval_point,
529 eq_r,
530 eval_claim,
531 }
532 }
533
534 fn random_instance(spec: &[(usize, Vec<usize>)], seed: u64) -> Vec<TestTable> {
537 let mut rng = StdRng::seed_from_u64(seed);
538 spec.iter()
539 .map(|(m, looker_n_vars)| {
540 let values = random_field_buffer::<P>(&mut rng, *m);
541 let lookers = looker_n_vars
542 .iter()
543 .map(|&n| random_looker(&mut rng, n, &values))
544 .collect::<Vec<_>>();
545 TestTable { values, lookers }
546 })
547 .collect()
548 }
549
550 fn prover_tables(tables: &[TestTable]) -> Vec<TableLookup<'_, P>> {
552 tables
553 .iter()
554 .map(|table| TableLookup {
555 table: table.values.as_view(),
556 lookers: table
557 .lookers
558 .iter()
559 .map(|looker| Looker {
560 index: &looker.index,
561 eval_point: &looker.eval_point,
562 eval_claim: looker.eval_claim,
563 })
564 .collect(),
565 })
566 .collect()
567 }
568
569 fn verifier_tables(tables: &[TestTable]) -> Vec<logup_star::TableLookup<'_, F>> {
571 tables
572 .iter()
573 .map(|table| logup_star::TableLookup {
574 n_vars: table.values.log_len(),
575 lookers: table
576 .lookers
577 .iter()
578 .map(|looker| logup_star::LookerClaim {
579 eval_point: &looker.eval_point,
580 eval_claim: looker.eval_claim,
581 })
582 .collect(),
583 })
584 .collect()
585 }
586
587 fn honest_pushforward(table: &TestTable, gamma: F) -> FieldBuffer<P> {
590 let mut pushforward = vec![F::ZERO; table.values.len()];
591 for (looker, power) in iter::zip(&table.lookers, powers(gamma)) {
592 for (&j, &eq) in iter::zip(&looker.index, &looker.eq_r) {
593 pushforward[j] += power * eq;
594 }
595 }
596 FieldBuffer::from_values(&pushforward)
597 }
598
599 fn check_index_claims(
604 index_point: &[F],
605 claims_by_table: &[Vec<F>],
606 tables: &[TestTable],
607 shape: &str,
608 ) {
609 for (table_index, (table, claims)) in iter::zip(tables, claims_by_table).enumerate() {
610 let m = table.values.log_len();
611 assert_eq!(claims.len(), table.lookers.len(), "claim count ({shape})");
612 for (looker, claim) in iter::zip(&table.lookers, claims) {
613 let embedded = looker.index.iter().map(|&j| iota(j, m)).collect::<Vec<_>>();
614 let embedded = FieldBuffer::<P>::from_values(&embedded);
615 let own_point = &index_point[index_point.len() - looker.eval_point.len()..];
616 assert_eq!(
617 *claim,
618 evaluate(&embedded, own_point),
619 "index claim wrong for table {table_index}, n={} ({shape})",
620 looker.eval_point.len()
621 );
622 }
623 }
624 }
625
626 fn check_round_trip(spec: &[(usize, Vec<usize>)], seed: u64) {
631 let alloc = GlobalAllocator;
632 let tables = random_instance(spec, seed);
633 let shape = format!("{spec:?}");
634
635 let mut prover_transcript = ProverTranscript::new(StdChallenger::default());
638 let gamma = IPProverChannel::<F>::sample(&mut prover_transcript);
639 let prover_out = prove::<GlobalAllocator, F, P>(
640 &alloc,
641 gamma,
642 prover_tables(&tables),
643 &mut prover_transcript,
644 );
645
646 let mut verifier_transcript = prover_transcript.into_verifier();
647 let verifier_gamma = IPVerifierChannel::<F>::sample(&mut verifier_transcript);
648 assert_eq!(verifier_gamma, gamma, "both sides must draw the same challenge ({shape})");
649 let verifier_out = logup_star::verify_reduction::<F, _>(
650 &verifier_gamma,
651 verifier_tables(&tables),
652 &mut verifier_transcript,
653 )
654 .expect("verification succeeds");
655
656 assert_eq!(prover_out, verifier_out, "outputs disagree ({shape})");
658
659 let table_point = &prover_out.table_eval_point;
662 for (table_index, table) in tables.iter().enumerate() {
663 let own_point = &table_point[..table.values.log_len()];
664
665 assert_eq!(
666 prover_out.tables[table_index].eval_claim,
667 evaluate(&table.values, own_point),
668 "table claim wrong for table {table_index} ({shape})"
669 );
670 assert_eq!(
671 prover_out.tables[table_index].pushforward_claim,
672 evaluate(&honest_pushforward(table, gamma), own_point),
673 "pushforward claim wrong for table {table_index} ({shape})"
674 );
675 }
676
677 let claims_by_table = prover_out
678 .tables
679 .iter()
680 .map(|table| table.index_eval_claims.clone())
681 .collect::<Vec<_>>();
682 check_index_claims(&prover_out.index_eval_point, &claims_by_table, &tables, &shape);
683 }
684
685 fn check_transparent_round_trip(spec: &[(usize, Vec<usize>)], seed: u64) {
687 let alloc = GlobalAllocator;
688 let tables = random_instance(spec, seed);
689 let shape = format!("{spec:?}");
690
691 let mut prover_transcript = ProverTranscript::new(StdChallenger::default());
692 let gamma = IPProverChannel::<F>::sample(&mut prover_transcript);
693 let prover_out = prove_transparent::<GlobalAllocator, F, P>(
694 &alloc,
695 gamma,
696 prover_tables(&tables),
697 &mut prover_transcript,
698 );
699
700 let mut verifier_transcript = prover_transcript.into_verifier();
701 let verifier_gamma = IPVerifierChannel::<F>::sample(&mut verifier_transcript);
702 let verifier_out = logup_star::verify_reduction_transparent::<F, _>(
703 &verifier_gamma,
704 verifier_tables(&tables),
705 &mut verifier_transcript,
706 )
707 .expect("verification succeeds");
708 assert_eq!(prover_out, verifier_out, "outputs disagree ({shape})");
709
710 for (table_index, (table, out)) in iter::zip(&tables, &prover_out.tables).enumerate() {
713 let pushforward = honest_pushforward(table, gamma);
714 assert_eq!(
715 out.pushforward_eval_claim,
716 evaluate(&pushforward, &out.pushforward_eval_point),
717 "leaf claim wrong for table {table_index} ({shape})"
718 );
719 assert_eq!(
720 out.product_claim,
721 inner_product_buffers(&table.values, &pushforward),
722 "product claim wrong for table {table_index} ({shape})"
723 );
724 }
725
726 let claims_by_table = prover_out
727 .tables
728 .iter()
729 .map(|table| table.index_eval_claims.clone())
730 .collect::<Vec<_>>();
731 check_index_claims(&prover_out.index_eval_point, &claims_by_table, &tables, &shape);
732 }
733
734 #[test]
735 fn test_prove_verify_round_trip() {
736 for (n, m) in [(6, 2), (5, 3), (4, 4), (3, 5), (7, 1)] {
738 check_round_trip(&[(m, vec![n])], 0);
739 }
740 }
741
742 #[test]
743 fn test_prove_verify_single_table_variable() {
744 check_round_trip(&[(1, vec![4])], 1);
746 }
747
748 #[test]
749 fn test_prove_verify_single_looker_row() {
750 check_round_trip(&[(3, vec![0])], 2);
752 }
753
754 #[test]
755 fn test_multi_looker_round_trip() {
756 check_round_trip(&[(3, vec![5, 5, 5])], 11);
758 }
759
760 #[test]
761 fn test_lookers_of_unequal_length() {
762 for (looker_n_vars, m) in [
766 (vec![5usize, 2, 4], 3usize),
767 (vec![2, 6], 2),
768 (vec![1, 1, 5], 4),
769 (vec![3, 3], 5),
770 (vec![0, 4], 3),
771 ] {
772 check_round_trip(&[(m, looker_n_vars)], 17);
773 }
774 }
775
776 #[test]
777 fn test_multi_table_round_trip() {
778 for spec in [
782 vec![(3usize, vec![5usize, 3usize]), (2, vec![2, 6])],
784 vec![(4, vec![1]), (2, vec![3]), (5, vec![2])],
786 vec![(2, vec![4, 4, 2]), (3, vec![5])],
788 vec![(3, vec![4]), (3, vec![4]), (3, vec![4])],
790 vec![(1, vec![0]), (4, vec![3])],
792 ] {
793 check_round_trip(&spec, 23);
794 }
795 }
796
797 #[test]
798 fn test_tables_are_independent() {
799 check_round_trip(&[(3, vec![4, 2]), (3, vec![5])], 29);
802 }
803
804 #[test]
805 fn test_prove_verify_transparent_round_trip() {
806 for (n, m) in [(6, 2), (4, 4), (3, 5), (7, 1)] {
809 check_transparent_round_trip(&[(m, vec![n])], 0);
810 }
811 }
812
813 #[test]
814 fn test_transparent_multi_table_round_trip() {
815 for spec in [
818 vec![(3usize, vec![5usize, 3usize]), (2, vec![2, 6])],
819 vec![(4, vec![1]), (2, vec![3]), (5, vec![2])],
820 vec![(1, vec![0]), (4, vec![3])],
821 ] {
822 check_transparent_round_trip(&spec, 23);
823 }
824 }
825
826 #[test]
827 fn test_transparent_reduction_leaves_the_product_claim_unchecked() {
828 let alloc = GlobalAllocator;
831 let tables = random_instance(&[(3, vec![5])], 3);
832 let table = &tables[0];
833 let looker = &table.lookers[0];
834 let wrong_claim = looker.eval_claim + F::ONE;
835
836 let mut prover_transcript = ProverTranscript::new(StdChallenger::default());
837 let gamma = IPProverChannel::<F>::sample(&mut prover_transcript);
838 prove_transparent::<GlobalAllocator, F, P>(
839 &alloc,
840 gamma,
841 [TableLookup {
842 table: table.values.as_view(),
843 lookers: vec![Looker {
844 index: &looker.index,
845 eval_point: &looker.eval_point,
846 eval_claim: wrong_claim,
847 }],
848 }],
849 &mut prover_transcript,
850 );
851
852 let mut verifier_transcript = prover_transcript.into_verifier();
853 let gamma = IPVerifierChannel::<F>::sample(&mut verifier_transcript);
854 let out = logup_star::verify_reduction_transparent::<F, _>(
855 &gamma,
856 [logup_star::TableLookup {
857 n_vars: 3,
858 lookers: vec![logup_star::LookerClaim {
859 eval_point: &looker.eval_point,
860 eval_claim: wrong_claim,
861 }],
862 }],
863 &mut verifier_transcript,
864 )
865 .expect("the reduction itself still verifies");
866
867 assert_eq!(out.tables[0].product_claim, wrong_claim);
870 assert_ne!(
871 out.tables[0].product_claim,
872 inner_product_buffers(&table.values, &honest_pushforward(table, gamma))
873 );
874 }
875
876 #[test]
877 fn test_verifier_rejects_wrong_eval_claim() {
878 let alloc = GlobalAllocator;
879 let tables = random_instance(&[(3, vec![5])], 3);
880 let table = &tables[0];
881 let looker = &table.lookers[0];
882
883 let wrong_claim = looker.eval_claim + F::ONE;
885 let mut prover_transcript = ProverTranscript::new(StdChallenger::default());
886 let gamma = IPProverChannel::<F>::sample(&mut prover_transcript);
887 prove::<GlobalAllocator, F, P>(
888 &alloc,
889 gamma,
890 [TableLookup {
891 table: table.values.as_view(),
892 lookers: vec![Looker {
893 index: &looker.index,
894 eval_point: &looker.eval_point,
895 eval_claim: wrong_claim,
896 }],
897 }],
898 &mut prover_transcript,
899 );
900
901 let mut verifier_transcript = prover_transcript.into_verifier();
903 let gamma = IPVerifierChannel::<F>::sample(&mut verifier_transcript);
904 let result = logup_star::verify_reduction::<F, _>(
905 &gamma,
906 [logup_star::TableLookup {
907 n_vars: 3,
908 lookers: vec![logup_star::LookerClaim {
909 eval_point: &looker.eval_point,
910 eval_claim: wrong_claim,
911 }],
912 }],
913 &mut verifier_transcript,
914 );
915 assert!(result.is_err(), "verifier must reject a wrong eval claim");
916 }
917
918 #[test]
919 fn test_verifier_rejects_lookup_against_the_wrong_table() {
920 let alloc = GlobalAllocator;
923 let tables = random_instance(&[(3, vec![4]), (3, vec![4])], 31);
924 let looker = &tables[0].lookers[0];
925
926 let mut prover_transcript = ProverTranscript::new(StdChallenger::default());
927 let gamma = IPProverChannel::<F>::sample(&mut prover_transcript);
928 prove::<GlobalAllocator, F, P>(
930 &alloc,
931 gamma,
932 [
933 TableLookup {
934 table: tables[0].values.as_view(),
935 lookers: vec![Looker {
936 index: &tables[0].lookers[0].index,
937 eval_point: &tables[0].lookers[0].eval_point,
938 eval_claim: tables[0].lookers[0].eval_claim,
939 }],
940 },
941 TableLookup {
942 table: tables[1].values.as_view(),
943 lookers: vec![Looker {
944 index: &looker.index,
945 eval_point: &looker.eval_point,
946 eval_claim: looker.eval_claim,
947 }],
948 },
949 ],
950 &mut prover_transcript,
951 );
952
953 let mut verifier_transcript = prover_transcript.into_verifier();
954 let gamma = IPVerifierChannel::<F>::sample(&mut verifier_transcript);
955 let result = logup_star::verify_reduction::<F, _>(
956 &gamma,
957 [
958 logup_star::TableLookup {
959 n_vars: 3,
960 lookers: vec![logup_star::LookerClaim {
961 eval_point: &tables[0].lookers[0].eval_point,
962 eval_claim: tables[0].lookers[0].eval_claim,
963 }],
964 },
965 logup_star::TableLookup {
966 n_vars: 3,
967 lookers: vec![logup_star::LookerClaim {
968 eval_point: &looker.eval_point,
969 eval_claim: looker.eval_claim,
970 }],
971 },
972 ],
973 &mut verifier_transcript,
974 );
975 assert!(result.is_err(), "verifier must reject a lookup against the wrong table");
976 }
977
978 #[test]
979 #[should_panic(expected = "every table must have at least one variable")]
980 fn test_zero_variable_table_panics() {
981 let mut rng = StdRng::seed_from_u64(0);
982 let alloc = GlobalAllocator;
983
984 let table = random_field_buffer::<P>(&mut rng, 0);
986 let mut transcript = ProverTranscript::new(StdChallenger::default());
987 let gamma = IPProverChannel::<F>::sample(&mut transcript);
988 let _ = prove::<GlobalAllocator, F, P>(
989 &alloc,
990 gamma,
991 [TableLookup {
992 table: table.as_view(),
993 lookers: vec![Looker {
994 index: &[0],
995 eval_point: &[],
996 eval_claim: F::ZERO,
997 }],
998 }],
999 &mut transcript,
1000 );
1001 }
1002
1003 #[test]
1004 #[should_panic(expected = "every table must have at least one looker")]
1005 fn test_table_without_lookers_panics() {
1006 let mut rng = StdRng::seed_from_u64(0);
1007 let alloc = GlobalAllocator;
1008
1009 let table = random_field_buffer::<P>(&mut rng, 3);
1011 let mut transcript = ProverTranscript::new(StdChallenger::default());
1012 let gamma = IPProverChannel::<F>::sample(&mut transcript);
1013 let _ = prove::<GlobalAllocator, F, P>(
1014 &alloc,
1015 gamma,
1016 [TableLookup {
1017 table: table.as_view(),
1018 lookers: Vec::new(),
1019 }],
1020 &mut transcript,
1021 );
1022 }
1023
1024 #[test]
1025 #[should_panic(expected = "index column has 3 entries but 16 were expected for 4 variables")]
1026 fn test_rejects_index_length_mismatch() {
1027 let mut rng = StdRng::seed_from_u64(0);
1028 let alloc = GlobalAllocator;
1029 let table = random_field_buffer::<P>(&mut rng, 3);
1030 let eval_point = random_scalars::<F>(&mut rng, 4);
1031 let mut transcript = ProverTranscript::new(StdChallenger::default());
1032 let gamma = IPProverChannel::<F>::sample(&mut transcript);
1033
1034 let _ = prove::<GlobalAllocator, F, P>(
1036 &alloc,
1037 gamma,
1038 [TableLookup {
1039 table: table.as_view(),
1040 lookers: vec![Looker {
1041 index: &[0, 1, 2],
1042 eval_point: &eval_point,
1043 eval_claim: F::ZERO,
1044 }],
1045 }],
1046 &mut transcript,
1047 );
1048 }
1049
1050 #[test]
1051 #[should_panic(expected = "every index entry must be less than the size of the table")]
1052 fn test_out_of_range_index_panics() {
1053 let mut rng = StdRng::seed_from_u64(0);
1054 let alloc = GlobalAllocator;
1055 let table = random_field_buffer::<P>(&mut rng, 2);
1056 let eval_point = random_scalars::<F>(&mut rng, 1);
1057 let mut transcript = ProverTranscript::new(StdChallenger::default());
1058 let gamma = IPProverChannel::<F>::sample(&mut transcript);
1059
1060 let _ = prove::<GlobalAllocator, F, P>(
1071 &alloc,
1072 gamma,
1073 [TableLookup {
1074 table: table.as_view(),
1075 lookers: vec![Looker {
1076 index: &[0, 4],
1077 eval_point: &eval_point,
1078 eval_claim: F::ZERO,
1079 }],
1080 }],
1081 &mut transcript,
1082 );
1083 }
1084}