1use binius_compute::Allocator;
21use binius_field::{BinaryField, Divisible, PackedField};
22use binius_ip_prover::logup_star::{self as reduction, LogupTableOutput, witness};
23pub use binius_ip_prover::logup_star::{Looker, TableLookup};
24use binius_math::{FieldBuffer, multilinear::eq::eq_ind_partial_eval_in};
25use itertools::izip;
26
27use crate::channel::IOPProverChannel;
28
29pub struct LogupProof<F> {
34 pub table_eval_point: Vec<F>,
38 pub index_eval_point: Vec<F>,
42 pub tables: Vec<LogupTableOutput<F>>,
44}
45
46#[tracing::instrument(skip_all, level = "debug", name = "logup* (committed)")]
71pub fn prove<'a, F, P, Channel, A>(
72 tables: impl IntoIterator<Item = reduction::TableLookup<'a, P>>,
73 channel: &mut Channel,
74 alloc: &A,
75) -> LogupProof<F>
76where
77 F: BinaryField<Underlier: Divisible<u64>>,
78 P: PackedField<Scalar = F> + 'a,
79 Channel: IOPProverChannel<P, A>,
80 A: Allocator,
81{
82 let tables = tables.into_iter().collect::<Vec<_>>();
85
86 let gamma = channel.sample();
92 let (numerators, pushforwards) = witness::combined_lookers::<A, F, P>(alloc, gamma, &tables);
93
94 let oracles = tracing::debug_span!("Commit pushforwards").in_scope(|| {
96 pushforwards
97 .iter()
98 .map(|pushforward| channel.send_oracle(pushforward.as_view()))
99 .collect::<Vec<_>>()
100 });
101
102 let pushforward_slices = pushforwards
105 .iter()
106 .map(FieldBuffer::as_view)
107 .collect::<Vec<_>>();
108 let output =
109 reduction::prove_reduction(alloc, gamma, &tables, numerators, &pushforward_slices, channel);
110
111 let _open_guard = tracing::debug_span!("Open pushforward relations").entered();
118 for (oracle, pushforward, table, table_output) in
119 izip!(oracles, pushforwards, &tables, &output.tables)
120 {
121 let m = table.table.log_len();
122 let transparent = eq_ind_partial_eval_in::<A, P>(alloc, &output.table_eval_point[..m]);
123 channel.prove_oracle_relation(
124 oracle.clone(),
125 transparent.into(),
126 table_output.pushforward_claim,
127 );
128 channel.finalize_oracle(oracle, pushforward);
129 }
130
131 LogupProof {
132 table_eval_point: output.table_eval_point,
133 index_eval_point: output.index_eval_point,
134 tables: output.tables,
135 }
136}
137
138pub struct LogupTransparentProof<F> {
143 pub index_eval_point: Vec<F>,
147 pub index_eval_claims: Vec<Vec<F>>,
150}
151
152#[tracing::instrument(skip_all, level = "debug", name = "logup* transparent (committed)")]
175pub fn prove_transparent<'a, F, P, Channel, A>(
176 tables: impl IntoIterator<Item = reduction::TableLookup<'a, P>>,
177 channel: &mut Channel,
178 alloc: &A,
179) -> LogupTransparentProof<F>
180where
181 F: BinaryField<Underlier: Divisible<u64>>,
182 P: PackedField<Scalar = F> + 'a,
183 Channel: IOPProverChannel<P, A>,
184 A: Allocator,
185{
186 let tables = tables.into_iter().collect::<Vec<_>>();
187
188 let gamma = channel.sample();
191 let (numerators, pushforwards) = witness::combined_lookers::<A, F, P>(alloc, gamma, &tables);
192 let oracles = tracing::debug_span!("Commit pushforwards").in_scope(|| {
193 pushforwards
194 .iter()
195 .map(|pushforward| channel.send_oracle(pushforward.as_view()))
196 .collect::<Vec<_>>()
197 });
198
199 let pushforward_slices = pushforwards
200 .iter()
201 .map(FieldBuffer::as_view)
202 .collect::<Vec<_>>();
203 let output = reduction::prove_reduction_transparent(
204 alloc,
205 gamma,
206 &tables,
207 numerators,
208 &pushforward_slices,
209 channel,
210 );
211
212 let _open_guard = tracing::debug_span!("Open pushforward relations").entered();
216 for (oracle, pushforward, table, open) in izip!(oracles, pushforwards, &tables, &output.tables)
217 {
218 let leaf_eq = eq_ind_partial_eval_in::<A, P>(alloc, &open.pushforward_eval_point);
219 channel.prove_oracle_relation(oracle.clone(), leaf_eq.into(), open.pushforward_eval_claim);
220 channel.prove_oracle_relation(
221 oracle.clone(),
222 FieldBuffer::from_view_in(alloc, table.table).into(),
223 open.product_claim,
224 );
225 channel.finalize_oracle(oracle, pushforward);
226 }
227
228 LogupTransparentProof {
229 index_eval_point: output.index_eval_point,
230 index_eval_claims: output
231 .tables
232 .into_iter()
233 .map(|table| table.index_eval_claims)
234 .collect(),
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use std::iter;
241
242 use binius_compute::GlobalAllocator;
243 use binius_field::{
244 BinaryField1b, ExtensionField, Field, PackedGhash1x128b,
245 arch::{OptimalB128, OptimalPackedB128},
246 };
247 use binius_hash::{StdDigest, StdHashSuite};
248 use binius_iop::{
249 basefold::compiler::BaseFoldVerifierCompiler,
250 channel::{OracleSpec, naive::NaiveVerifierChannel},
251 fri::MinProofSizeStrategy,
252 logup_star::{self as verify_logup, TransparentTableLookup},
253 merkle_tree::BinaryMerkleTreeScheme,
254 };
255 use binius_ip::logup_star::LookerClaim;
256 use binius_math::{
257 FieldBuffer,
258 multilinear::{
259 eq::eq_ind_partial_eval_scalars,
260 evaluate::{evaluate, evaluate_inplace_scalars},
261 },
262 ntt::{NeighborsLastSingleThread, domain_context::GaoMateerOnTheFly},
263 test_utils::{random_field_buffer, random_scalars},
264 };
265 use binius_transcript::{ProverTranscript, fiat_shamir::HasherChallenger};
266 use rand::prelude::*;
267
268 use super::*;
269 use crate::{basefold::compiler::BaseFoldProverCompiler, channel::naive::NaiveProverChannel};
270
271 type F = OptimalB128;
272 type P = OptimalPackedB128;
273 type StdChallenger = HasherChallenger<sha2::Sha256>;
274 type BP = PackedGhash1x128b;
276 type Chal = HasherChallenger<StdDigest>;
277
278 fn iota<E: Field + ExtensionField<BinaryField1b>>(j: usize, m: usize) -> E {
282 (0..m)
283 .filter(|t| (j >> t) & 1 == 1)
284 .map(E::basis)
285 .fold(E::ZERO, |acc, b| acc + b)
286 }
287
288 struct TestLooker<E> {
290 index: Vec<usize>,
291 eval_point: Vec<E>,
292 eval_claim: E,
293 }
294
295 struct TestTable<E, Q: PackedField> {
297 values: FieldBuffer<Q>,
298 lookers: Vec<TestLooker<E>>,
299 }
300
301 fn random_looker<E, Q>(
303 rng: &mut StdRng,
304 n: usize,
305 table_values: &FieldBuffer<Q>,
306 ) -> TestLooker<E>
307 where
308 E: Field,
309 Q: PackedField<Scalar = E>,
310 {
311 let m = table_values.log_len();
312 let index = (0..(1usize << n))
313 .map(|_| rng.random_range(0..(1usize << m)))
314 .collect::<Vec<_>>();
315 let eval_point = random_scalars::<E>(&mut *rng, n);
316
317 let eq_r = eq_ind_partial_eval_scalars(&eval_point);
319 let eval_claim = index
320 .iter()
321 .zip(&eq_r)
322 .map(|(&j, &eq)| eq * table_values.get(j))
323 .fold(E::ZERO, |acc, t| acc + t);
324
325 TestLooker {
326 index,
327 eval_point,
328 eval_claim,
329 }
330 }
331
332 fn random_instance<E, Q>(spec: &[(usize, Vec<usize>)], seed: u64) -> Vec<TestTable<E, Q>>
335 where
336 E: Field,
337 Q: PackedField<Scalar = E>,
338 {
339 let mut rng = StdRng::seed_from_u64(seed);
340 spec.iter()
341 .map(|(m, looker_n_vars)| {
342 let values = random_field_buffer::<Q>(&mut rng, *m);
343 let lookers = looker_n_vars
344 .iter()
345 .map(|&n| random_looker::<E, Q>(&mut rng, n, &values))
346 .collect::<Vec<_>>();
347 TestTable { values, lookers }
348 })
349 .collect()
350 }
351
352 fn check_proofs<Q>(
355 prover_proof: &LogupProof<F>,
356 verifier_proof: &verify_logup::LogupProof<F>,
357 tables: &[TestTable<F, Q>],
358 shape: &str,
359 ) where
360 Q: PackedField<Scalar = F>,
361 {
362 assert_eq!(
363 prover_proof.table_eval_point, verifier_proof.table_eval_point,
364 "table point ({shape})"
365 );
366 assert_eq!(
367 prover_proof.index_eval_point, verifier_proof.index_eval_point,
368 "index point ({shape})"
369 );
370 assert_eq!(prover_proof.tables, verifier_proof.tables, "per-table claims ({shape})");
371
372 let table_point = &prover_proof.table_eval_point;
374 for (table_index, table) in tables.iter().enumerate() {
375 let m = table.values.log_len();
376 assert_eq!(
377 prover_proof.tables[table_index].eval_claim,
378 evaluate(&table.values, &table_point[..m]),
379 "table claim wrong for table {table_index} ({shape})"
380 );
381 }
382
383 let claims_by_table = prover_proof
384 .tables
385 .iter()
386 .map(|table| table.index_eval_claims.clone())
387 .collect::<Vec<_>>();
388 check_index_claims(&prover_proof.index_eval_point, &claims_by_table, tables, shape);
389 }
390
391 fn check_index_claims<Q>(
395 index_point: &[F],
396 claims_by_table: &[Vec<F>],
397 tables: &[TestTable<F, Q>],
398 shape: &str,
399 ) where
400 Q: PackedField<Scalar = F>,
401 {
402 for (table_index, (table, claims)) in iter::zip(tables, claims_by_table).enumerate() {
403 let m = table.values.log_len();
404 assert_eq!(claims.len(), table.lookers.len(), "claim count ({shape})");
405 for (looker, claim) in iter::zip(&table.lookers, claims) {
406 let embedded = looker
407 .index
408 .iter()
409 .map(|&j| iota::<F>(j, m))
410 .collect::<Vec<_>>();
411 let embedded = FieldBuffer::<P>::from_values(&embedded);
412 let own_point = &index_point[index_point.len() - looker.eval_point.len()..];
413 assert_eq!(
414 *claim,
415 evaluate(&embedded, own_point),
416 "index claim wrong for table {table_index}, n={} ({shape})",
417 looker.eval_point.len()
418 );
419 }
420 }
421 }
422
423 fn prover_tables<'a, Q>(tables: &'a [TestTable<F, Q>]) -> Vec<TableLookup<'a, Q>>
425 where
426 Q: PackedField<Scalar = F>,
427 {
428 tables
429 .iter()
430 .map(|table| TableLookup {
431 table: table.values.as_view(),
432 lookers: table
433 .lookers
434 .iter()
435 .map(|looker| Looker {
436 index: &looker.index,
437 eval_point: &looker.eval_point,
438 eval_claim: looker.eval_claim,
439 })
440 .collect(),
441 })
442 .collect()
443 }
444
445 fn verifier_tables<'a, Q>(
447 tables: &'a [TestTable<F, Q>],
448 ) -> Vec<binius_ip::logup_star::TableLookup<'a, F>>
449 where
450 Q: PackedField<Scalar = F>,
451 {
452 tables
453 .iter()
454 .map(|table| binius_ip::logup_star::TableLookup {
455 n_vars: table.values.log_len(),
456 lookers: table
457 .lookers
458 .iter()
459 .map(|looker| LookerClaim {
460 eval_point: &looker.eval_point,
461 eval_claim: looker.eval_claim,
462 })
463 .collect(),
464 })
465 .collect()
466 }
467
468 fn transparent_verifier_tables<'a, Q>(
472 tables: &'a [TestTable<F, Q>],
473 ) -> Vec<TransparentTableLookup<'a, F>>
474 where
475 Q: PackedField<Scalar = F>,
476 {
477 iter::zip(verifier_tables(tables), tables)
478 .map(|(lookup, table)| {
479 let values = table.values.iter_scalars().collect::<Vec<_>>();
480 TransparentTableLookup {
481 lookup,
482 table_eval: Box::new(move |point: &[F]| {
483 evaluate_inplace_scalars(values.clone(), point)
484 }),
485 }
486 })
487 .collect()
488 }
489
490 fn check_prove_verify(spec: &[(usize, Vec<usize>)], seed: u64) {
492 let tables = random_instance::<F, P>(spec, seed);
493 let shape = format!("{spec:?}");
494
495 let specs = tables
497 .iter()
498 .map(|table| OracleSpec::new(table.values.log_len()))
499 .collect::<Vec<_>>();
500
501 let mut prover_transcript = ProverTranscript::new(StdChallenger::default());
503 let mut prover_channel =
504 NaiveProverChannel::<F, _>::new(&mut prover_transcript, specs.clone());
505 let prover_proof =
506 prove::<F, P, _, _>(prover_tables(&tables), &mut prover_channel, &GlobalAllocator);
507 prover_channel.finish();
508
509 let mut verifier_transcript = prover_transcript.into_verifier();
511 let mut verifier_channel =
512 NaiveVerifierChannel::<F, _>::new(&mut verifier_transcript, &specs);
513 let verifier_proof = verify_logup::verify(verifier_tables(&tables), &mut verifier_channel)
514 .expect("verification succeeds");
515 verifier_channel.finish();
516
517 check_proofs(&prover_proof, &verifier_proof, &tables, &shape);
518 }
519
520 fn check_prove_verify_transparent(spec: &[(usize, Vec<usize>)], seed: u64) {
525 let tables = random_instance::<F, P>(spec, seed);
526 let shape = format!("{spec:?}");
527
528 let specs = tables
530 .iter()
531 .map(|table| OracleSpec::new(table.values.log_len()))
532 .collect::<Vec<_>>();
533
534 let mut prover_transcript = ProverTranscript::new(StdChallenger::default());
535 let mut prover_channel =
536 NaiveProverChannel::<F, _>::new(&mut prover_transcript, specs.clone());
537 let prover_proof = prove_transparent::<F, P, _, _>(
538 prover_tables(&tables),
539 &mut prover_channel,
540 &GlobalAllocator,
541 );
542 prover_channel.finish();
543
544 let mut verifier_transcript = prover_transcript.into_verifier();
545 let mut verifier_channel =
546 NaiveVerifierChannel::<F, _>::new(&mut verifier_transcript, &specs);
547 let verifier_proof = verify_logup::verify_transparent(
548 transparent_verifier_tables(&tables),
549 &mut verifier_channel,
550 )
551 .expect("verification succeeds");
552 verifier_channel.finish();
553
554 assert_eq!(
555 prover_proof.index_eval_point, verifier_proof.index_eval_point,
556 "index point ({shape})"
557 );
558 assert_eq!(
559 prover_proof.index_eval_claims, verifier_proof.index_eval_claims,
560 "index claims ({shape})"
561 );
562 check_index_claims(
563 &prover_proof.index_eval_point,
564 &prover_proof.index_eval_claims,
565 &tables,
566 &shape,
567 );
568 }
569
570 #[test]
571 fn test_prove_verify_round_trip() {
572 for (n, m) in [(6, 2), (5, 3), (4, 4), (3, 5), (7, 1)] {
574 check_prove_verify(&[(m, vec![n])], 0);
575 }
576 }
577
578 #[test]
579 fn test_multi_looker_committed_round_trip() {
580 check_prove_verify(&[(3, vec![5, 5])], 13);
582 }
583
584 #[test]
585 fn test_prove_verify_single_table_variable() {
586 check_prove_verify(&[(1, vec![4])], 1);
588 }
589
590 #[test]
591 fn test_multi_table_committed_round_trip() {
592 for spec in [
596 vec![(3usize, vec![5usize, 3usize]), (2, vec![2, 6])],
597 vec![(4, vec![1]), (2, vec![3]), (5, vec![2])],
598 vec![(2, vec![4, 4, 2]), (3, vec![5])],
599 vec![(1, vec![0]), (4, vec![3])],
600 ] {
601 check_prove_verify(&spec, 23);
602 }
603 }
604
605 #[test]
606 fn test_prove_verify_transparent_round_trip() {
607 for spec in [
611 vec![(2usize, vec![6usize])],
612 vec![(4, vec![4])],
613 vec![(5, vec![3])],
614 vec![(1, vec![4])],
615 vec![(3, vec![5, 5])],
616 vec![(3, vec![5, 3]), (2, vec![2, 6])],
617 vec![(4, vec![1]), (2, vec![3]), (5, vec![2])],
618 ] {
619 check_prove_verify_transparent(&spec, 31);
620 }
621 }
622
623 #[test]
624 fn test_verifier_rejects_wrong_eval_claim() {
625 let mut tables = random_instance::<F, P>(&[(3, vec![5])], 3);
626 let specs = vec![OracleSpec::new(3)];
627
628 tables[0].lookers[0].eval_claim += F::ONE;
630
631 let mut prover_transcript = ProverTranscript::new(StdChallenger::default());
632 let mut prover_channel =
633 NaiveProverChannel::<F, _>::new(&mut prover_transcript, specs.clone());
634 let _prover_proof =
635 prove::<F, P, _, _>(prover_tables(&tables), &mut prover_channel, &GlobalAllocator);
636 prover_channel.finish();
637
638 let mut verifier_transcript = prover_transcript.into_verifier();
640 let mut verifier_channel =
641 NaiveVerifierChannel::<F, _>::new(&mut verifier_transcript, &specs);
642 let result = verify_logup::verify(verifier_tables(&tables), &mut verifier_channel);
643 assert!(result.is_err(), "verifier must reject a wrong eval claim");
644 }
645
646 fn run_basefold_transparent(
653 tables: &[TestTable<F, BP>],
654 ) -> Result<
655 (LogupTransparentProof<F>, verify_logup::LogupTransparentProof<F>),
656 verify_logup::Error,
657 > {
658 const LOG_INV_RATE: usize = 1;
659 const SECURITY_BITS: usize = 32;
660 let n_test_queries = SECURITY_BITS.div_ceil(LOG_INV_RATE);
661 let oracle_specs = tables
662 .iter()
663 .map(|table| OracleSpec::new_zk(table.values.log_len()))
664 .collect::<Vec<_>>();
665
666 let verifier_compiler = BaseFoldVerifierCompiler::new(
667 &BinaryMerkleTreeScheme::<F, StdHashSuite>::new(),
668 oracle_specs,
669 LOG_INV_RATE,
670 n_test_queries,
671 &MinProofSizeStrategy,
672 );
673
674 let domain_context = GaoMateerOnTheFly::generate(verifier_compiler.max_log_domain_size());
676 let ntt = NeighborsLastSingleThread::new(domain_context);
677 let prover_compiler =
678 BaseFoldProverCompiler::<BP, _>::from_verifier_compiler(&verifier_compiler, ntt);
679
680 let mut prover_transcript = ProverTranscript::new(Chal::default());
681 let mut prover_channel = prover_compiler
682 .create_channel_from_transcript::<StdHashSuite, Chal, _, _>(
683 &mut prover_transcript,
684 StdRng::seed_from_u64(8),
685 GlobalAllocator,
686 );
687
688 let alloc = GlobalAllocator;
689 let prover_proof =
690 prove_transparent::<F, BP, _, _>(prover_tables(tables), &mut prover_channel, &alloc);
691 prover_channel.finish();
692
693 let mut verifier_transcript = prover_transcript.into_verifier();
695 let mut verifier_channel = verifier_compiler
696 .create_channel_from_transcript::<StdHashSuite, Chal, _>(&mut verifier_transcript);
697 let verifier_proof = verify_logup::verify_transparent(
698 transparent_verifier_tables(tables),
699 &mut verifier_channel,
700 )?;
701 verifier_channel.finish()?;
702
703 Ok((prover_proof, verifier_proof))
704 }
705
706 #[test]
707 fn test_basefold_round_trip() {
708 let spec = [(2usize, vec![6usize, 2usize]), (4, vec![3])];
711 let tables = random_instance::<F, BP>(&spec, 7);
712
713 const LOG_INV_RATE: usize = 1;
714 const SECURITY_BITS: usize = 32;
715 let n_test_queries = SECURITY_BITS.div_ceil(LOG_INV_RATE);
716 let oracle_specs = tables
717 .iter()
718 .map(|table| OracleSpec::new_zk(table.values.log_len()))
719 .collect::<Vec<_>>();
720
721 let verifier_compiler = BaseFoldVerifierCompiler::new(
722 &BinaryMerkleTreeScheme::<F, StdHashSuite>::new(),
723 oracle_specs,
724 LOG_INV_RATE,
725 n_test_queries,
726 &MinProofSizeStrategy,
727 );
728
729 let domain_context = GaoMateerOnTheFly::generate(verifier_compiler.max_log_domain_size());
731 let ntt = NeighborsLastSingleThread::new(domain_context);
732 let prover_compiler =
733 BaseFoldProverCompiler::<BP, _>::from_verifier_compiler(&verifier_compiler, ntt);
734
735 let mut prover_transcript = ProverTranscript::new(Chal::default());
736 let prover_channel_rng = StdRng::seed_from_u64(8);
737 let mut prover_channel = prover_compiler
738 .create_channel_from_transcript::<StdHashSuite, Chal, _, _>(
739 &mut prover_transcript,
740 prover_channel_rng,
741 GlobalAllocator,
742 );
743
744 let alloc = GlobalAllocator;
745 let prover_proof =
746 prove::<F, BP, _, _>(prover_tables(&tables), &mut prover_channel, &alloc);
747 prover_channel.finish();
748
749 let mut verifier_transcript = prover_transcript.into_verifier();
752 let mut verifier_channel = verifier_compiler
753 .create_channel_from_transcript::<StdHashSuite, Chal, _>(&mut verifier_transcript);
754 let verifier_proof = verify_logup::verify(verifier_tables(&tables), &mut verifier_channel)
755 .expect("verification succeeds");
756 verifier_channel
757 .finish()
758 .expect("the batched FRI openings verify");
759
760 check_proofs(&prover_proof, &verifier_proof, &tables, "basefold");
763 }
764
765 #[test]
766 fn test_basefold_transparent_round_trip() {
767 let tables = random_instance::<F, BP>(&[(2usize, vec![6usize, 2usize]), (4, vec![3])], 7);
770
771 let (prover_proof, verifier_proof) =
772 run_basefold_transparent(&tables).expect("the batched FRI openings verify");
773
774 assert_eq!(prover_proof.index_eval_point, verifier_proof.index_eval_point, "index point");
775 assert_eq!(
776 prover_proof.index_eval_claims, verifier_proof.index_eval_claims,
777 "index claims"
778 );
779 check_index_claims(
780 &prover_proof.index_eval_point,
781 &prover_proof.index_eval_claims,
782 &tables,
783 "basefold transparent",
784 );
785 }
786
787 #[test]
788 fn test_basefold_transparent_rejects_wrong_eval_claim() {
789 let mut tables = random_instance::<F, BP>(&[(3usize, vec![5usize])], 3);
792 tables[0].lookers[0].eval_claim += F::ONE;
793
794 assert!(
795 run_basefold_transparent(&tables).is_err(),
796 "the opening must reject a wrong eval claim"
797 );
798 }
799}