2023-10-30 14:28:24 -04:00
//! This crate provides support for cross-table lookups.
//!
//! If a STARK S_1 calls an operation that is carried out by another STARK S_2,
//! S_1 provides the inputs to S_2 and reads the output from S_1. To ensure that
//! the operation was correctly carried out, we must check that the provided inputs
//! and outputs are correctly read. Cross-table lookups carry out that check.
//!
//! To achieve this, smaller CTL tables are created on both sides: looking and looked tables.
//! In our example, we create a table S_1' comprised of columns -- or linear combinations
//! of columns -- of S_1, and rows that call operations carried out in S_2. We also create a
//! table S_2' comprised of columns -- or linear combinations od columns -- of S_2 and rows
//! that carry out the operations needed by other STARKs. Then, S_1' is a looking table for
//! the looked S_2', since we want to check that the operation outputs in S_1' are indeeed in S_2'.
//! Furthermore, the concatenation of all tables looking into S_2' must be equal to S_2'.
//!
//! To achieve this, we construct, for each table, a permutation polynomial Z(x).
//! Z(x) is computed as the product of all its column combinations.
//! To check it was correctly constructed, we check:
//! - Z(gw) = Z(w) * combine(w) where combine(w) is the column combination at point w.
//! - Z(g^(n-1)) = combine(1).
//! - The verifier also checks that the product of looking table Z polynomials is equal
//! to the associated looked table Z polynomial.
//! Note that the first two checks are written that way because Z polynomials are computed
//! upside down for convenience.
//!
//! Additionally, we support cross-table lookups over two rows. The permutation principle
//! is similar, but we provide not only `local_values` but also `next_values` -- corresponding to
//! the current and next row values -- when computing the linear combinations.
2024-02-01 07:16:28 -05:00
use core ::borrow ::Borrow ;
use core ::cmp ::min ;
use core ::fmt ::Debug ;
use core ::iter ::repeat ;
2022-08-23 12:22:54 -07:00
2022-05-11 16:09:12 +02:00
use anyhow ::{ ensure , Result } ;
2024-01-10 08:54:13 +01:00
use hashbrown ::HashMap ;
2022-06-13 18:54:12 +02:00
use itertools ::Itertools ;
2024-01-10 08:54:13 +01:00
use plonky2 ::field ::batch_util ::{ batch_add_inplace , batch_multiply_inplace } ;
2022-06-27 07:18:21 -07:00
use plonky2 ::field ::extension ::{ Extendable , FieldExtension } ;
2022-06-27 15:07:52 -07:00
use plonky2 ::field ::packed ::PackedField ;
2022-05-05 22:21:09 +02:00
use plonky2 ::field ::polynomial ::PolynomialValues ;
2022-06-27 12:24:09 -07:00
use plonky2 ::field ::types ::Field ;
2022-05-05 22:21:09 +02:00
use plonky2 ::hash ::hash_types ::RichField ;
2023-02-13 15:58:26 +01:00
use plonky2 ::iop ::challenger ::{ Challenger , RecursiveChallenger } ;
2022-05-20 11:21:13 +02:00
use plonky2 ::iop ::ext_target ::ExtensionTarget ;
2022-05-24 16:24:52 +02:00
use plonky2 ::iop ::target ::Target ;
use plonky2 ::plonk ::circuit_builder ::CircuitBuilder ;
2023-02-13 15:58:26 +01:00
use plonky2 ::plonk ::config ::{ AlgebraicHasher , GenericConfig , Hasher } ;
use plonky2 ::plonk ::plonk_common ::{
reduce_with_powers , reduce_with_powers_circuit , reduce_with_powers_ext_circuit ,
} ;
2024-01-10 08:54:13 +01:00
use plonky2 ::util ::ceil_div_usize ;
2023-02-13 15:58:26 +01:00
use plonky2 ::util ::serialization ::{ Buffer , IoResult , Read , Write } ;
2022-05-05 22:21:09 +02:00
2022-08-26 10:12:45 +02:00
use crate ::all_stark ::{ Table , NUM_TABLES } ;
2022-05-05 22:21:09 +02:00
use crate ::config ::StarkConfig ;
2022-05-24 16:24:52 +02:00
use crate ::constraint_consumer ::{ ConstraintConsumer , RecursiveConstraintConsumer } ;
2023-09-22 09:19:13 -04:00
use crate ::evaluation_frame ::StarkEvaluationFrame ;
Shrink STARK proofs to a constant degree
The goal here is to end up with a single "root" circuit representing any EVM proof. I.e. it must verify each STARK, but be general enough to work with any combination of STARK sizes (within some range of sizes that we chose to support). This root circuit can then be plugged into our aggregation circuit.
In particular, for each STARK, and for each initial `degree_bits` (within a range that we choose to support), this adds a "shrinking chain" of circuits. Such a chain shrinks a STARK proof from that initial `degree_bits` down to a constant, `THRESHOLD_DEGREE_BITS`.
The root circuit then combines these shrunk-to-constant proofs for each table. It's similar to `RecursiveAllProof::verify_circuit`; I adapted the code from there and I think we can remove it after. The main difference is that now instead of having one verification key per STARK, we have several possible VKs, one per initial `degree_bits`. We bake the list of possible VKs into the root circuit, and have the prover indicate the index of the VK they're actually using.
This also partially removes the default feature of CTLs. So far we've used filters instead of defaults. Until now it was easy to keep supporting defaults just in case, but here maintaining support would require some more work. E.g. we couldn't use `exp_u64` any more, since the size delta is now dynamic, it can't be hardcoded. If there are no concerns, I'll fully remove the feature after.
2022-12-27 18:15:18 -08:00
use crate ::proof ::{ StarkProofTarget , StarkProofWithMetadata } ;
2022-05-06 14:55:54 +02:00
use crate ::stark ::Stark ;
2022-05-05 22:21:09 +02:00
2023-10-30 14:28:24 -04:00
/// Represent two linear combination of columns, corresponding to the current and next row values.
/// Each linear combination is represented as:
/// - a vector of `(usize, F)` corresponding to the column number and the associated multiplicand
/// - the constant of the linear combination.
2022-08-14 16:36:07 -07:00
#[ derive(Clone, Debug) ]
2023-11-13 09:26:56 -05:00
pub ( crate ) struct Column < F : Field > {
2022-08-23 12:22:54 -07:00
linear_combination : Vec < ( usize , F ) > ,
2023-09-15 18:14:07 -04:00
next_row_linear_combination : Vec < ( usize , F ) > ,
2022-06-14 19:09:03 +02:00
constant : F ,
2022-06-06 20:51:14 +02:00
}
2022-06-14 00:53:31 +02:00
impl < F : Field > Column < F > {
2023-10-30 14:28:24 -04:00
/// Returns the representation of a single column in the current row.
2023-11-13 09:26:56 -05:00
pub ( crate ) fn single ( c : usize ) -> Self {
2022-06-14 19:09:03 +02:00
Self {
2022-08-23 12:22:54 -07:00
linear_combination : vec ! [ ( c , F ::ONE ) ] ,
2023-09-15 18:14:07 -04:00
next_row_linear_combination : vec ! [ ] ,
2022-06-14 19:09:03 +02:00
constant : F ::ZERO ,
}
2022-06-14 00:53:31 +02:00
}
2023-10-30 14:28:24 -04:00
/// Returns multiple single columns in the current row.
2023-11-13 09:26:56 -05:00
pub ( crate ) fn singles < I : IntoIterator < Item = impl Borrow < usize > > > (
2022-08-29 18:10:51 -07:00
cs : I ,
) -> impl Iterator < Item = Self > {
cs . into_iter ( ) . map ( | c | Self ::single ( * c . borrow ( ) ) )
2022-06-14 00:53:31 +02:00
}
2023-10-30 14:28:24 -04:00
/// Returns the representation of a single column in the next row.
2023-11-13 09:26:56 -05:00
pub ( crate ) fn single_next_row ( c : usize ) -> Self {
2023-09-15 18:14:07 -04:00
Self {
linear_combination : vec ! [ ] ,
next_row_linear_combination : vec ! [ ( c , F ::ONE ) ] ,
constant : F ::ZERO ,
}
}
2023-10-30 14:28:24 -04:00
/// Returns multiple single columns for the next row.
2023-11-13 09:26:56 -05:00
pub ( crate ) fn singles_next_row < I : IntoIterator < Item = impl Borrow < usize > > > (
2023-09-15 18:14:07 -04:00
cs : I ,
) -> impl Iterator < Item = Self > {
cs . into_iter ( ) . map ( | c | Self ::single_next_row ( * c . borrow ( ) ) )
}
2023-10-30 14:28:24 -04:00
/// Returns a linear combination corresponding to a constant.
2023-11-13 09:26:56 -05:00
pub ( crate ) fn constant ( constant : F ) -> Self {
2022-08-14 16:36:07 -07:00
Self {
linear_combination : vec ! [ ] ,
2023-09-15 18:14:07 -04:00
next_row_linear_combination : vec ! [ ] ,
2022-08-14 16:36:07 -07:00
constant ,
}
}
2023-10-30 14:28:24 -04:00
/// Returns a linear combination corresponding to 0.
2023-11-13 09:26:56 -05:00
pub ( crate ) fn zero ( ) -> Self {
2022-08-14 16:36:07 -07:00
Self ::constant ( F ::ZERO )
}
2023-10-30 14:28:24 -04:00
/// Returns a linear combination corresponding to 1.
2023-11-13 09:26:56 -05:00
pub ( crate ) fn one ( ) -> Self {
2022-09-04 16:53:04 -07:00
Self ::constant ( F ::ONE )
}
2023-10-30 14:28:24 -04:00
/// Given an iterator of `(usize, F)` and a constant, returns the association linear combination of columns for the current row.
2023-11-13 09:26:56 -05:00
pub ( crate ) fn linear_combination_with_constant < I : IntoIterator < Item = ( usize , F ) > > (
2022-06-14 19:09:03 +02:00
iter : I ,
constant : F ,
) -> Self {
2022-06-14 00:53:31 +02:00
let v = iter . into_iter ( ) . collect ::< Vec < _ > > ( ) ;
assert! ( ! v . is_empty ( ) ) ;
2022-06-07 17:23:46 +02:00
debug_assert_eq! (
2022-08-23 12:22:54 -07:00
v . iter ( ) . map ( | ( c , _ ) | c ) . unique ( ) . count ( ) ,
2022-06-14 00:53:31 +02:00
v . len ( ) ,
2022-06-14 16:30:34 +02:00
" Duplicate columns. "
2022-06-07 17:23:46 +02:00
) ;
2022-06-14 19:09:03 +02:00
Self {
linear_combination : v ,
2023-09-15 18:14:07 -04:00
next_row_linear_combination : vec ! [ ] ,
constant ,
}
}
2023-10-30 14:28:24 -04:00
/// Given an iterator of `(usize, F)` and a constant, returns the associated linear combination of columns for the current and the next rows.
2023-11-13 09:26:56 -05:00
pub ( crate ) fn linear_combination_and_next_row_with_constant <
I : IntoIterator < Item = ( usize , F ) > ,
> (
2023-09-15 18:14:07 -04:00
iter : I ,
next_row_iter : I ,
constant : F ,
) -> Self {
let v = iter . into_iter ( ) . collect ::< Vec < _ > > ( ) ;
let next_row_v = next_row_iter . into_iter ( ) . collect ::< Vec < _ > > ( ) ;
assert! ( ! v . is_empty ( ) | | ! next_row_v . is_empty ( ) ) ;
debug_assert_eq! (
v . iter ( ) . map ( | ( c , _ ) | c ) . unique ( ) . count ( ) ,
v . len ( ) ,
" Duplicate columns. "
) ;
debug_assert_eq! (
next_row_v . iter ( ) . map ( | ( c , _ ) | c ) . unique ( ) . count ( ) ,
next_row_v . len ( ) ,
" Duplicate columns. "
) ;
Self {
linear_combination : v ,
next_row_linear_combination : next_row_v ,
2022-06-14 19:09:03 +02:00
constant ,
}
}
2023-10-30 14:28:24 -04:00
/// Returns a linear combination of columns, with no additional constant.
2023-11-13 09:26:56 -05:00
pub ( crate ) fn linear_combination < I : IntoIterator < Item = ( usize , F ) > > ( iter : I ) -> Self {
2022-06-14 19:09:03 +02:00
Self ::linear_combination_with_constant ( iter , F ::ZERO )
2022-06-14 00:53:31 +02:00
}
2023-10-30 14:28:24 -04:00
/// Given an iterator of columns (c_0, ..., c_n) containing bits in little endian order:
/// returns the representation of c_0 + 2 * c_1 + ... + 2^n * c_n.
2023-11-13 09:26:56 -05:00
pub ( crate ) fn le_bits < I : IntoIterator < Item = impl Borrow < usize > > > ( cs : I ) -> Self {
2022-08-29 18:10:51 -07:00
Self ::linear_combination ( cs . into_iter ( ) . map ( | c | * c . borrow ( ) ) . zip ( F ::TWO . powers ( ) ) )
2022-06-14 00:53:31 +02:00
}
2023-11-15 11:15:14 -05:00
/// Given an iterator of columns (c_0, ..., c_n) containing bits in little endian order:
/// returns the representation of c_0 + 2 * c_1 + ... + 2^n * c_n + k where `k` is an
/// additional constant.
pub ( crate ) fn le_bits_with_constant < I : IntoIterator < Item = impl Borrow < usize > > > (
cs : I ,
constant : F ,
) -> Self {
Self ::linear_combination_with_constant (
cs . into_iter ( ) . map ( | c | * c . borrow ( ) ) . zip ( F ::TWO . powers ( ) ) ,
constant ,
)
}
2023-10-30 14:28:24 -04:00
/// Given an iterator of columns (c_0, ..., c_n) containing bytes in little endian order:
/// returns the representation of c_0 + 256 * c_1 + ... + 256^n * c_n.
2023-11-13 09:26:56 -05:00
pub ( crate ) fn le_bytes < I : IntoIterator < Item = impl Borrow < usize > > > ( cs : I ) -> Self {
2022-08-29 18:10:51 -07:00
Self ::linear_combination (
cs . into_iter ( )
. map ( | c | * c . borrow ( ) )
. zip ( F ::from_canonical_u16 ( 256 ) . powers ( ) ) ,
)
2022-08-14 16:36:07 -07:00
}
2023-10-30 14:28:24 -04:00
/// Given an iterator of columns, returns the representation of their sum.
2023-11-13 09:26:56 -05:00
pub ( crate ) fn sum < I : IntoIterator < Item = impl Borrow < usize > > > ( cs : I ) -> Self {
2022-08-29 18:10:51 -07:00
Self ::linear_combination ( cs . into_iter ( ) . map ( | c | * c . borrow ( ) ) . zip ( repeat ( F ::ONE ) ) )
2022-06-14 00:53:31 +02:00
}
2023-10-30 14:28:24 -04:00
/// Given the column values for the current row, returns the evaluation of the linear combination.
2023-11-13 09:26:56 -05:00
pub ( crate ) fn eval < FE , P , const D : usize > ( & self , v : & [ P ] ) -> P
2022-06-14 00:53:31 +02:00
where
FE : FieldExtension < D , BaseField = F > ,
P : PackedField < Scalar = FE > ,
{
2022-06-14 19:09:03 +02:00
self . linear_combination
. iter ( )
2022-08-23 12:22:54 -07:00
. map ( | & ( c , f ) | v [ c ] * FE ::from_basefield ( f ) )
2022-06-14 19:09:03 +02:00
. sum ::< P > ( )
+ FE ::from_basefield ( self . constant )
2022-06-14 00:53:31 +02:00
}
2023-10-30 14:28:24 -04:00
/// Given the column values for the current and next rows, evaluates the current and next linear combinations and returns their sum.
2023-11-13 09:26:56 -05:00
pub ( crate ) fn eval_with_next < FE , P , const D : usize > ( & self , v : & [ P ] , next_v : & [ P ] ) -> P
2023-09-15 18:14:07 -04:00
where
FE : FieldExtension < D , BaseField = F > ,
P : PackedField < Scalar = FE > ,
{
self . linear_combination
. iter ( )
. map ( | & ( c , f ) | v [ c ] * FE ::from_basefield ( f ) )
. sum ::< P > ( )
+ self
. next_row_linear_combination
. iter ( )
. map ( | & ( c , f ) | next_v [ c ] * FE ::from_basefield ( f ) )
. sum ::< P > ( )
+ FE ::from_basefield ( self . constant )
}
2023-10-30 14:28:24 -04:00
/// Evaluate on a row of a table given in column-major form.
2023-11-13 09:26:56 -05:00
pub ( crate ) fn eval_table ( & self , table : & [ PolynomialValues < F > ] , row : usize ) -> F {
2023-09-15 18:14:07 -04:00
let mut res = self
. linear_combination
2022-06-14 19:09:03 +02:00
. iter ( )
2022-08-23 12:22:54 -07:00
. map ( | & ( c , f ) | table [ c ] . values [ row ] * f )
2022-06-14 19:09:03 +02:00
. sum ::< F > ( )
2023-09-15 18:14:07 -04:00
+ self . constant ;
// If we access the next row at the last row, for sanity, we consider the next row's values to be 0.
// If CTLs are correctly written, the filter should be 0 in that case anyway.
2023-09-20 12:45:14 -04:00
if ! self . next_row_linear_combination . is_empty ( ) & & row < table [ 0 ] . values . len ( ) - 1 {
2023-09-15 18:14:07 -04:00
res + = self
. next_row_linear_combination
. iter ( )
. map ( | & ( c , f ) | table [ c ] . values [ row + 1 ] * f )
. sum ::< F > ( ) ;
}
res
2022-06-14 00:53:31 +02:00
}
2023-12-18 23:27:12 +01:00
/// Evaluates the column on all rows.
pub ( crate ) fn eval_all_rows ( & self , table : & [ PolynomialValues < F > ] ) -> Vec < F > {
let length = table [ 0 ] . len ( ) ;
( 0 .. length )
. map ( | row | self . eval_table ( table , row ) )
. collect ::< Vec < F > > ( )
}
2023-10-30 14:28:24 -04:00
/// Circuit version of `eval`: Given a row's targets, returns their linear combination.
2023-11-13 09:26:56 -05:00
pub ( crate ) fn eval_circuit < const D : usize > (
2022-06-14 00:53:31 +02:00
& self ,
builder : & mut CircuitBuilder < F , D > ,
2022-08-23 12:22:54 -07:00
v : & [ ExtensionTarget < D > ] ,
2022-06-14 00:53:31 +02:00
) -> ExtensionTarget < D >
where
F : RichField + Extendable < D > ,
{
2022-06-14 19:09:03 +02:00
let pairs = self
. linear_combination
. iter ( )
2022-08-23 12:22:54 -07:00
. map ( | & ( c , f ) | {
2022-06-14 19:09:03 +02:00
(
2022-08-23 12:22:54 -07:00
v [ c ] ,
builder . constant_extension ( F ::Extension ::from_basefield ( f ) ) ,
2022-06-14 19:09:03 +02:00
)
} )
. collect ::< Vec < _ > > ( ) ;
let constant = builder . constant_extension ( F ::Extension ::from_basefield ( self . constant ) ) ;
builder . inner_product_extension ( F ::ONE , constant , pairs )
2022-06-14 00:53:31 +02:00
}
2023-09-15 18:14:07 -04:00
2023-10-30 14:28:24 -04:00
/// Circuit version of `eval_with_next`:
/// Given the targets of the current and next row, returns the sum of their linear combinations.
2023-11-13 09:26:56 -05:00
pub ( crate ) fn eval_with_next_circuit < const D : usize > (
2023-09-15 18:14:07 -04:00
& self ,
builder : & mut CircuitBuilder < F , D > ,
v : & [ ExtensionTarget < D > ] ,
next_v : & [ ExtensionTarget < D > ] ,
) -> ExtensionTarget < D >
where
F : RichField + Extendable < D > ,
{
let mut pairs = self
. linear_combination
. iter ( )
. map ( | & ( c , f ) | {
(
v [ c ] ,
builder . constant_extension ( F ::Extension ::from_basefield ( f ) ) ,
)
} )
. collect ::< Vec < _ > > ( ) ;
let next_row_pairs = self . next_row_linear_combination . iter ( ) . map ( | & ( c , f ) | {
(
next_v [ c ] ,
builder . constant_extension ( F ::Extension ::from_basefield ( f ) ) ,
)
} ) ;
pairs . extend ( next_row_pairs ) ;
let constant = builder . constant_extension ( F ::Extension ::from_basefield ( self . constant ) ) ;
builder . inner_product_extension ( F ::ONE , constant , pairs )
}
2022-06-14 00:53:31 +02:00
}
2023-12-05 17:02:37 -05:00
/// Represents a CTL filter, which evaluates to 1 if the row must be considered for the CTL and 0 otherwise.
/// It's an arbitrary degree 2 combination of columns: `products` are the degree 2 terms, and `constants` are
/// the degree 1 terms.
#[ derive(Clone, Debug) ]
pub ( crate ) struct Filter < F : Field > {
products : Vec < ( Column < F > , Column < F > ) > ,
constants : Vec < Column < F > > ,
}
impl < F : Field > Filter < F > {
pub ( crate ) fn new ( products : Vec < ( Column < F > , Column < F > ) > , constants : Vec < Column < F > > ) -> Self {
Self {
products ,
constants ,
}
}
/// Returns a filter made of a single column.
pub ( crate ) fn new_simple ( col : Column < F > ) -> Self {
Self {
products : vec ! [ ] ,
constants : vec ! [ col ] ,
}
}
/// Given the column values for the current and next rows, evaluates the filter.
pub ( crate ) fn eval_filter < FE , P , const D : usize > ( & self , v : & [ P ] , next_v : & [ P ] ) -> P
where
FE : FieldExtension < D , BaseField = F > ,
P : PackedField < Scalar = FE > ,
{
self . products
. iter ( )
. map ( | ( col1 , col2 ) | col1 . eval_with_next ( v , next_v ) * col2 . eval_with_next ( v , next_v ) )
. sum ::< P > ( )
+ self
. constants
. iter ( )
. map ( | col | col . eval_with_next ( v , next_v ) )
. sum ::< P > ( )
}
/// Circuit version of `eval_filter`:
/// Given the column values for the current and next rows, evaluates the filter.
pub ( crate ) fn eval_filter_circuit < const D : usize > (
& self ,
builder : & mut CircuitBuilder < F , D > ,
v : & [ ExtensionTarget < D > ] ,
next_v : & [ ExtensionTarget < D > ] ,
) -> ExtensionTarget < D >
where
F : RichField + Extendable < D > ,
{
let prods = self
. products
. iter ( )
. map ( | ( col1 , col2 ) | {
let col1_eval = col1 . eval_with_next_circuit ( builder , v , next_v ) ;
let col2_eval = col2 . eval_with_next_circuit ( builder , v , next_v ) ;
builder . mul_extension ( col1_eval , col2_eval )
} )
. collect ::< Vec < _ > > ( ) ;
let consts = self
. constants
. iter ( )
. map ( | col | col . eval_with_next_circuit ( builder , v , next_v ) )
. collect ::< Vec < _ > > ( ) ;
let prods = builder . add_many_extension ( prods ) ;
let consts = builder . add_many_extension ( consts ) ;
builder . add_extension ( prods , consts )
}
/// Evaluate on a row of a table given in column-major form.
pub ( crate ) fn eval_table ( & self , table : & [ PolynomialValues < F > ] , row : usize ) -> F {
self . products
. iter ( )
. map ( | ( col1 , col2 ) | col1 . eval_table ( table , row ) * col2 . eval_table ( table , row ) )
. sum ::< F > ( )
+ self
. constants
. iter ( )
. map ( | col | col . eval_table ( table , row ) )
. sum ( )
}
2023-12-18 23:27:12 +01:00
pub ( crate ) fn eval_all_rows ( & self , table : & [ PolynomialValues < F > ] ) -> Vec < F > {
let length = table [ 0 ] . len ( ) ;
( 0 .. length )
. map ( | row | self . eval_table ( table , row ) )
. collect ::< Vec < F > > ( )
}
2023-12-05 17:02:37 -05:00
}
2023-10-30 14:28:24 -04:00
/// A `Table` with a linear combination of columns and a filter.
2023-12-05 17:02:37 -05:00
/// `filter` is used to determine the rows to select in `Table`.
2023-10-30 14:28:24 -04:00
/// `columns` represents linear combinations of the columns of `Table`.
2022-08-14 16:36:07 -07:00
#[ derive(Clone, Debug) ]
2023-11-13 09:26:56 -05:00
pub ( crate ) struct TableWithColumns < F : Field > {
2022-06-14 00:53:31 +02:00
table : Table ,
columns : Vec < Column < F > > ,
2023-12-05 17:02:37 -05:00
pub ( crate ) filter : Option < Filter < F > > ,
2022-06-14 00:53:31 +02:00
}
impl < F : Field > TableWithColumns < F > {
2023-12-05 17:02:37 -05:00
/// Generates a new `TableWithColumns` given a `Table`, a linear combination of columns `columns` and a `filter`.
pub ( crate ) fn new ( table : Table , columns : Vec < Column < F > > , filter : Option < Filter < F > > ) -> Self {
2022-06-06 20:51:14 +02:00
Self {
table ,
columns ,
2023-12-05 17:02:37 -05:00
filter ,
2022-06-06 20:51:14 +02:00
}
}
}
2023-10-30 14:28:24 -04:00
/// Cross-table lookup data consisting in the lookup table (`looked_table`) and all the tables that look into `looked_table` (`looking_tables`).
/// Each `looking_table` corresponds to a STARK's table whose rows have been filtered out and whose columns have been through a linear combination (see `eval_table`). The concatenation of those smaller tables should result in the `looked_table`.
2022-08-23 12:22:54 -07:00
#[ derive(Clone) ]
2023-11-13 09:26:56 -05:00
pub ( crate ) struct CrossTableLookup < F : Field > {
2023-10-30 14:28:24 -04:00
/// Column linear combinations for all tables that are looking into the current table.
2022-12-02 13:56:52 -08:00
pub ( crate ) looking_tables : Vec < TableWithColumns < F > > ,
2023-10-30 14:28:24 -04:00
/// Column linear combination for the current table.
2022-12-02 13:56:52 -08:00
pub ( crate ) looked_table : TableWithColumns < F > ,
2022-05-06 17:22:30 +02:00
}
2022-05-17 09:24:22 +02:00
impl < F : Field > CrossTableLookup < F > {
2023-10-30 14:28:24 -04:00
/// Creates a new `CrossTableLookup` given some looking tables and a looked table.
/// All tables should have the same width.
2023-11-13 09:26:56 -05:00
pub ( crate ) fn new (
2022-06-14 00:53:31 +02:00
looking_tables : Vec < TableWithColumns < F > > ,
looked_table : TableWithColumns < F > ,
2022-05-06 17:22:30 +02:00
) -> Self {
2022-06-06 20:51:14 +02:00
assert! ( looking_tables
2022-06-01 18:53:19 +02:00
. iter ( )
2022-06-06 20:51:14 +02:00
. all ( | twc | twc . columns . len ( ) = = looked_table . columns . len ( ) ) ) ;
2022-05-06 17:22:30 +02:00
Self {
2022-06-01 18:53:19 +02:00
looking_tables ,
2022-05-06 17:22:30 +02:00
looked_table ,
}
}
2022-09-26 15:47:35 +02:00
2024-01-10 08:54:13 +01:00
/// Given a table, returns:
/// - the total number of helper columns for this table, over all Cross-table lookups,
/// - the total number of z polynomials for this table, over all Cross-table lookups,
/// - the number of helper columns for this table, for each Cross-table lookup.
pub ( crate ) fn num_ctl_helpers_zs_all (
ctls : & [ Self ] ,
table : Table ,
num_challenges : usize ,
constraint_degree : usize ,
) -> ( usize , usize , Vec < usize > ) {
let mut num_helpers = 0 ;
2022-09-29 16:32:41 +02:00
let mut num_ctls = 0 ;
2024-01-10 08:54:13 +01:00
let mut num_helpers_by_ctl = vec! [ 0 ; ctls . len ( ) ] ;
for ( i , ctl ) in ctls . iter ( ) . enumerate ( ) {
2022-09-29 16:32:41 +02:00
let all_tables = std ::iter ::once ( & ctl . looked_table ) . chain ( & ctl . looking_tables ) ;
2024-01-10 08:54:13 +01:00
let num_appearances = all_tables . filter ( | twc | twc . table = = table ) . count ( ) ;
let is_helpers = num_appearances > 2 ;
if is_helpers {
num_helpers_by_ctl [ i ] = ceil_div_usize ( num_appearances , constraint_degree - 1 ) ;
num_helpers + = num_helpers_by_ctl [ i ] ;
}
if num_appearances > 0 {
num_ctls + = 1 ;
}
2022-09-26 15:47:35 +02:00
}
2024-01-10 08:54:13 +01:00
(
num_helpers * num_challenges ,
num_ctls * num_challenges ,
num_helpers_by_ctl ,
)
2022-09-26 15:47:35 +02:00
}
2022-05-06 17:22:30 +02:00
}
2022-05-16 20:45:30 +02:00
/// Cross-table lookup data for one table.
2022-08-23 23:20:20 -07:00
#[ derive(Clone, Default) ]
2024-01-10 08:54:13 +01:00
pub ( crate ) struct CtlData < ' a , F : Field > {
2023-10-30 14:28:24 -04:00
/// Data associated with all Z(x) polynomials for one table.
2024-01-10 08:54:13 +01:00
pub ( crate ) zs_columns : Vec < CtlZData < ' a , F > > ,
2022-05-05 22:21:09 +02:00
}
2022-08-23 23:20:20 -07:00
/// Cross-table lookup data associated with one Z(x) polynomial.
2024-01-10 08:54:13 +01:00
/// One Z(x) polynomial can be associated to multiple tables,
/// built from the same STARK.
2022-08-23 23:20:20 -07:00
#[ derive(Clone) ]
2024-01-10 08:54:13 +01:00
pub ( crate ) struct CtlZData < ' a , F : Field > {
/// Helper columns to verify the Z polynomial values.
pub ( crate ) helper_columns : Vec < PolynomialValues < F > > ,
2023-10-30 14:28:24 -04:00
/// Z polynomial values.
2022-08-23 23:20:20 -07:00
pub ( crate ) z : PolynomialValues < F > ,
2023-10-30 14:28:24 -04:00
/// Cross-table lookup challenge.
2022-08-23 23:20:20 -07:00
pub ( crate ) challenge : GrandProductChallenge < F > ,
2024-01-10 08:54:13 +01:00
/// Vector of column linear combinations for the current tables.
pub ( crate ) columns : Vec < & ' a [ Column < F > ] > ,
/// Vector of filter columns for the current table.
/// Each filter evaluates to either 1 or 0.
pub ( crate ) filter : Vec < Option < Filter < F > > > ,
2022-08-23 23:20:20 -07:00
}
2022-05-05 22:21:09 +02:00
2024-01-10 08:54:13 +01:00
impl < ' a , F : Field > CtlData < ' a , F > {
2023-10-30 14:28:24 -04:00
/// Returns the number of cross-table lookup polynomials.
2023-11-13 09:26:56 -05:00
pub ( crate ) fn len ( & self ) -> usize {
2022-05-11 14:35:33 +02:00
self . zs_columns . len ( )
2022-05-10 15:08:08 +02:00
}
2023-10-30 14:28:24 -04:00
/// Returns whether there are no cross-table lookups.
2023-11-13 09:26:56 -05:00
pub ( crate ) fn is_empty ( & self ) -> bool {
2022-05-11 14:35:33 +02:00
self . zs_columns . is_empty ( )
2022-05-05 22:21:09 +02:00
}
2024-01-10 08:54:13 +01:00
/// Returns all the cross-table lookup helper polynomials.
pub ( crate ) fn ctl_helper_polys ( & self ) -> Vec < PolynomialValues < F > > {
let num_polys = self
. zs_columns
2022-08-23 23:20:20 -07:00
. iter ( )
2024-01-10 08:54:13 +01:00
. fold ( 0 , | acc , z | acc + z . helper_columns . len ( ) ) ;
let mut res = Vec ::with_capacity ( num_polys ) ;
for z in & self . zs_columns {
res . extend ( z . helper_columns . clone ( ) ) ;
}
res
}
/// Returns all the Z cross-table-lookup polynomials.
pub ( crate ) fn ctl_z_polys ( & self ) -> Vec < PolynomialValues < F > > {
let mut res = Vec ::with_capacity ( self . zs_columns . len ( ) ) ;
for z in & self . zs_columns {
res . push ( z . z . clone ( ) ) ;
}
res
}
/// Returns the number of helper columns for each STARK in each
/// `CtlZData`.
pub ( crate ) fn num_ctl_helper_polys ( & self ) -> Vec < usize > {
let mut res = Vec ::with_capacity ( self . zs_columns . len ( ) ) ;
for z in & self . zs_columns {
res . push ( z . helper_columns . len ( ) ) ;
}
res
2022-05-05 22:21:09 +02:00
}
}
2023-02-13 15:58:26 +01:00
/// Randomness for a single instance of a permutation check protocol.
#[ derive(Copy, Clone, Eq, PartialEq, Debug) ]
pub ( crate ) struct GrandProductChallenge < T : Copy + Eq + PartialEq + Debug > {
/// Randomness used to combine multiple columns into one.
pub ( crate ) beta : T ,
/// Random offset that's added to the beta-reduced column values.
pub ( crate ) gamma : T ,
}
impl < F : Field > GrandProductChallenge < F > {
pub ( crate ) fn combine < ' a , FE , P , T : IntoIterator < Item = & ' a P > , const D2 : usize > (
& self ,
terms : T ,
) -> P
where
FE : FieldExtension < D2 , BaseField = F > ,
P : PackedField < Scalar = FE > ,
T ::IntoIter : DoubleEndedIterator ,
{
reduce_with_powers ( terms , FE ::from_basefield ( self . beta ) ) + FE ::from_basefield ( self . gamma )
}
}
impl GrandProductChallenge < Target > {
pub ( crate ) fn combine_circuit < F : RichField + Extendable < D > , const D : usize > (
& self ,
builder : & mut CircuitBuilder < F , D > ,
terms : & [ ExtensionTarget < D > ] ,
) -> ExtensionTarget < D > {
let reduced = reduce_with_powers_ext_circuit ( builder , terms , self . beta ) ;
let gamma = builder . convert_to_ext ( self . gamma ) ;
builder . add_extension ( reduced , gamma )
}
}
impl GrandProductChallenge < Target > {
pub ( crate ) fn combine_base_circuit < F : RichField + Extendable < D > , const D : usize > (
& self ,
builder : & mut CircuitBuilder < F , D > ,
terms : & [ Target ] ,
) -> Target {
let reduced = reduce_with_powers_circuit ( builder , terms , self . beta ) ;
builder . add ( reduced , self . gamma )
}
}
/// Like `PermutationChallenge`, but with `num_challenges` copies to boost soundness.
#[ derive(Clone, Eq, PartialEq, Debug) ]
2023-12-22 17:23:22 +01:00
pub struct GrandProductChallengeSet < T : Copy + Eq + PartialEq + Debug > {
2023-02-13 15:58:26 +01:00
pub ( crate ) challenges : Vec < GrandProductChallenge < T > > ,
}
impl GrandProductChallengeSet < Target > {
2023-11-13 09:26:56 -05:00
pub ( crate ) fn to_buffer ( & self , buffer : & mut Vec < u8 > ) -> IoResult < ( ) > {
2023-02-13 15:58:26 +01:00
buffer . write_usize ( self . challenges . len ( ) ) ? ;
for challenge in & self . challenges {
buffer . write_target ( challenge . beta ) ? ;
buffer . write_target ( challenge . gamma ) ? ;
}
Ok ( ( ) )
}
2023-11-13 09:26:56 -05:00
pub ( crate ) fn from_buffer ( buffer : & mut Buffer ) -> IoResult < Self > {
2023-02-13 15:58:26 +01:00
let length = buffer . read_usize ( ) ? ;
let mut challenges = Vec ::with_capacity ( length ) ;
for _ in 0 .. length {
challenges . push ( GrandProductChallenge {
beta : buffer . read_target ( ) ? ,
gamma : buffer . read_target ( ) ? ,
} ) ;
}
Ok ( GrandProductChallengeSet { challenges } )
}
}
fn get_grand_product_challenge < F : RichField , H : Hasher < F > > (
challenger : & mut Challenger < F , H > ,
) -> GrandProductChallenge < F > {
let beta = challenger . get_challenge ( ) ;
let gamma = challenger . get_challenge ( ) ;
GrandProductChallenge { beta , gamma }
}
pub ( crate ) fn get_grand_product_challenge_set < F : RichField , H : Hasher < F > > (
challenger : & mut Challenger < F , H > ,
num_challenges : usize ,
) -> GrandProductChallengeSet < F > {
let challenges = ( 0 .. num_challenges )
. map ( | _ | get_grand_product_challenge ( challenger ) )
. collect ( ) ;
GrandProductChallengeSet { challenges }
}
fn get_grand_product_challenge_target <
F : RichField + Extendable < D > ,
H : AlgebraicHasher < F > ,
const D : usize ,
> (
builder : & mut CircuitBuilder < F , D > ,
challenger : & mut RecursiveChallenger < F , H , D > ,
) -> GrandProductChallenge < Target > {
let beta = challenger . get_challenge ( builder ) ;
let gamma = challenger . get_challenge ( builder ) ;
GrandProductChallenge { beta , gamma }
}
pub ( crate ) fn get_grand_product_challenge_set_target <
F : RichField + Extendable < D > ,
H : AlgebraicHasher < F > ,
const D : usize ,
> (
builder : & mut CircuitBuilder < F , D > ,
challenger : & mut RecursiveChallenger < F , H , D > ,
num_challenges : usize ,
) -> GrandProductChallengeSet < Target > {
let challenges = ( 0 .. num_challenges )
. map ( | _ | get_grand_product_challenge_target ( builder , challenger ) )
. collect ( ) ;
GrandProductChallengeSet { challenges }
}
2024-01-10 08:54:13 +01:00
/// Returns the number of helper columns for each `Table`.
pub ( crate ) fn num_ctl_helper_columns_by_table < F : Field > (
ctls : & [ CrossTableLookup < F > ] ,
constraint_degree : usize ,
) -> Vec < [ usize ; NUM_TABLES ] > {
let mut res = vec! [ [ 0 ; NUM_TABLES ] ; ctls . len ( ) ] ;
for ( i , ctl ) in ctls . iter ( ) . enumerate ( ) {
let CrossTableLookup {
looking_tables ,
looked_table ,
} = ctl ;
let mut num_by_table = [ 0 ; NUM_TABLES ] ;
let grouped_lookups = looking_tables . iter ( ) . group_by ( | & a | a . table ) ;
for ( table , group ) in grouped_lookups . into_iter ( ) {
let sum = group . count ( ) ;
if sum > 2 {
// We only need helper columns if there are more than 2 columns.
num_by_table [ table as usize ] = ceil_div_usize ( sum , constraint_degree - 1 ) ;
}
}
res [ i ] = num_by_table ;
}
res
}
2023-10-30 14:28:24 -04:00
/// Generates all the cross-table lookup data, for all tables.
/// - `trace_poly_values` corresponds to the trace values for all tables.
/// - `cross_table_lookups` corresponds to all the cross-table lookups, i.e. the looked and looking tables, as described in `CrossTableLookup`.
/// - `ctl_challenges` corresponds to the challenges used for CTLs.
2024-01-10 08:54:13 +01:00
/// - `constraint_degree` is the maximal constraint degree for the table.
2023-10-30 14:28:24 -04:00
/// For each `CrossTableLookup`, and each looking/looked table, the partial products for the CTL are computed, and added to the said table's `CtlZData`.
2024-01-10 08:54:13 +01:00
pub ( crate ) fn cross_table_lookup_data < ' a , F : RichField , const D : usize > (
2022-08-26 10:12:45 +02:00
trace_poly_values : & [ Vec < PolynomialValues < F > > ; NUM_TABLES ] ,
2024-01-10 08:54:13 +01:00
cross_table_lookups : & ' a [ CrossTableLookup < F > ] ,
Shrink STARK proofs to a constant degree
The goal here is to end up with a single "root" circuit representing any EVM proof. I.e. it must verify each STARK, but be general enough to work with any combination of STARK sizes (within some range of sizes that we chose to support). This root circuit can then be plugged into our aggregation circuit.
In particular, for each STARK, and for each initial `degree_bits` (within a range that we choose to support), this adds a "shrinking chain" of circuits. Such a chain shrinks a STARK proof from that initial `degree_bits` down to a constant, `THRESHOLD_DEGREE_BITS`.
The root circuit then combines these shrunk-to-constant proofs for each table. It's similar to `RecursiveAllProof::verify_circuit`; I adapted the code from there and I think we can remove it after. The main difference is that now instead of having one verification key per STARK, we have several possible VKs, one per initial `degree_bits`. We bake the list of possible VKs into the root circuit, and have the prover indicate the index of the VK they're actually using.
This also partially removes the default feature of CTLs. So far we've used filters instead of defaults. Until now it was easy to keep supporting defaults just in case, but here maintaining support would require some more work. E.g. we couldn't use `exp_u64` any more, since the size delta is now dynamic, it can't be hardcoded. If there are no concerns, I'll fully remove the feature after.
2022-12-27 18:15:18 -08:00
ctl_challenges : & GrandProductChallengeSet < F > ,
2024-01-10 08:54:13 +01:00
constraint_degree : usize ,
) -> [ CtlData < ' a , F > ; NUM_TABLES ] {
2022-08-26 10:12:45 +02:00
let mut ctl_data_per_table = [ 0 ; NUM_TABLES ] . map ( | _ | CtlData ::default ( ) ) ;
2022-05-18 09:22:58 +02:00
for CrossTableLookup {
2022-06-01 18:53:19 +02:00
looking_tables ,
2022-05-18 09:22:58 +02:00
looked_table ,
} in cross_table_lookups
{
2022-12-02 13:56:52 -08:00
log ::debug! ( " Processing CTL for {:?} " , looked_table . table ) ;
Shrink STARK proofs to a constant degree
The goal here is to end up with a single "root" circuit representing any EVM proof. I.e. it must verify each STARK, but be general enough to work with any combination of STARK sizes (within some range of sizes that we chose to support). This root circuit can then be plugged into our aggregation circuit.
In particular, for each STARK, and for each initial `degree_bits` (within a range that we choose to support), this adds a "shrinking chain" of circuits. Such a chain shrinks a STARK proof from that initial `degree_bits` down to a constant, `THRESHOLD_DEGREE_BITS`.
The root circuit then combines these shrunk-to-constant proofs for each table. It's similar to `RecursiveAllProof::verify_circuit`; I adapted the code from there and I think we can remove it after. The main difference is that now instead of having one verification key per STARK, we have several possible VKs, one per initial `degree_bits`. We bake the list of possible VKs into the root circuit, and have the prover indicate the index of the VK they're actually using.
This also partially removes the default feature of CTLs. So far we've used filters instead of defaults. Until now it was easy to keep supporting defaults just in case, but here maintaining support would require some more work. E.g. we couldn't use `exp_u64` any more, since the size delta is now dynamic, it can't be hardcoded. If there are no concerns, I'll fully remove the feature after.
2022-12-27 18:15:18 -08:00
for & challenge in & ctl_challenges . challenges {
2024-01-10 08:54:13 +01:00
let helper_zs_looking = ctl_helper_zs_cols (
trace_poly_values ,
looking_tables . clone ( ) ,
challenge ,
constraint_degree ,
) ;
let mut z_looked = partial_sums (
2022-06-06 20:51:14 +02:00
& trace_poly_values [ looked_table . table as usize ] ,
2024-01-10 08:54:13 +01:00
& [ ( & looked_table . columns , & looked_table . filter ) ] ,
2022-05-18 09:22:58 +02:00
challenge ,
2024-01-10 08:54:13 +01:00
constraint_degree ,
2022-05-18 09:22:58 +02:00
) ;
2024-01-10 08:54:13 +01:00
for ( table , helpers_zs ) in helper_zs_looking {
let num_helpers = helpers_zs . len ( ) - 1 ;
let count = looking_tables
. iter ( )
. filter ( | looking_table | looking_table . table as usize = = table )
. count ( ) ;
let cols_filts = looking_tables . iter ( ) . filter_map ( | looking_table | {
if looking_table . table as usize = = table {
Some ( ( & looking_table . columns , & looking_table . filter ) )
} else {
None
}
} ) ;
let mut columns = Vec ::with_capacity ( count ) ;
let mut filter = Vec ::with_capacity ( count ) ;
for ( col , filt ) in cols_filts {
columns . push ( & col [ .. ] ) ;
filter . push ( filt . clone ( ) ) ;
}
ctl_data_per_table [ table ] . zs_columns . push ( CtlZData {
helper_columns : helpers_zs [ .. num_helpers ] . to_vec ( ) ,
z : helpers_zs [ num_helpers ] . clone ( ) ,
challenge ,
columns ,
filter ,
} ) ;
2022-06-01 18:53:19 +02:00
}
2024-01-10 08:54:13 +01:00
// There is no helper column for the looking table.
let looked_poly = z_looked [ 0 ] . clone ( ) ;
2022-06-06 20:51:14 +02:00
ctl_data_per_table [ looked_table . table as usize ]
2022-05-18 09:22:58 +02:00
. zs_columns
2022-08-23 23:20:20 -07:00
. push ( CtlZData {
2024-01-10 08:54:13 +01:00
helper_columns : vec ! [ ] ,
z : looked_poly ,
2022-08-23 23:20:20 -07:00
challenge ,
2024-01-10 08:54:13 +01:00
columns : vec ! [ & looked_table . columns [ .. ] ] ,
filter : vec ! [ looked_table . filter . clone ( ) ] ,
2022-08-23 23:20:20 -07:00
} ) ;
2022-05-18 09:22:58 +02:00
}
}
ctl_data_per_table
2022-05-05 22:21:09 +02:00
}
2024-01-10 08:54:13 +01:00
type ColumnFilter < ' a , F > = ( & ' a [ Column < F > ] , & ' a Option < Filter < F > > ) ;
/// Given a STARK's trace, and the data associated to one lookup (either CTL or range check),
/// returns the associated helper polynomials.
pub ( crate ) fn get_helper_cols < F : Field > (
trace : & [ PolynomialValues < F > ] ,
degree : usize ,
columns_filters : & [ ColumnFilter < F > ] ,
challenge : GrandProductChallenge < F > ,
constraint_degree : usize ,
) -> Vec < PolynomialValues < F > > {
let num_helper_columns = ceil_div_usize ( columns_filters . len ( ) , constraint_degree - 1 ) ;
let mut helper_columns = Vec ::with_capacity ( num_helper_columns ) ;
let mut filter_index = 0 ;
for mut cols_filts in & columns_filters . iter ( ) . chunks ( constraint_degree - 1 ) {
let ( first_col , first_filter ) = cols_filts . next ( ) . unwrap ( ) ;
let mut filter_col = Vec ::with_capacity ( degree ) ;
let first_combined = ( 0 .. degree )
. map ( | d | {
let f = if let Some ( filter ) = first_filter {
let f = filter . eval_table ( trace , d ) ;
filter_col . push ( f ) ;
f
} else {
filter_col . push ( F ::ONE ) ;
F ::ONE
} ;
if f . is_one ( ) {
let evals = first_col
. iter ( )
. map ( | c | c . eval_table ( trace , d ) )
. collect ::< Vec < F > > ( ) ;
challenge . combine ( evals . iter ( ) )
} else {
assert_eq! ( f , F ::ZERO , " Non-binary filter? " ) ;
// Dummy value. Cannot be zero since it will be batch-inverted.
F ::ONE
}
} )
. collect ::< Vec < F > > ( ) ;
let mut acc = F ::batch_multiplicative_inverse ( & first_combined ) ;
for d in 0 .. degree {
if filter_col [ d ] . is_zero ( ) {
acc [ d ] = F ::ZERO ;
}
}
for ( col , filt ) in cols_filts {
let mut filter_col = Vec ::with_capacity ( degree ) ;
let mut combined = ( 0 .. degree )
. map ( | d | {
let f = if let Some ( filter ) = filt {
let f = filter . eval_table ( trace , d ) ;
filter_col . push ( f ) ;
f
} else {
filter_col . push ( F ::ONE ) ;
F ::ONE
} ;
if f . is_one ( ) {
let evals = col
. iter ( )
. map ( | c | c . eval_table ( trace , d ) )
. collect ::< Vec < F > > ( ) ;
challenge . combine ( evals . iter ( ) )
} else {
assert_eq! ( f , F ::ZERO , " Non-binary filter? " ) ;
// Dummy value. Cannot be zero since it will be batch-inverted.
F ::ONE
}
} )
. collect ::< Vec < F > > ( ) ;
combined = F ::batch_multiplicative_inverse ( & combined ) ;
for d in 0 .. degree {
if filter_col [ d ] . is_zero ( ) {
combined [ d ] = F ::ZERO ;
}
}
batch_add_inplace ( & mut acc , & combined ) ;
}
helper_columns . push ( acc . into ( ) ) ;
}
assert_eq! ( helper_columns . len ( ) , num_helper_columns ) ;
helper_columns
}
/// Computes helper columns and Z polynomials for all looking tables
/// of one cross-table lookup (i.e. for one looked table).
fn ctl_helper_zs_cols < F : Field > (
all_stark_traces : & [ Vec < PolynomialValues < F > > ; NUM_TABLES ] ,
looking_tables : Vec < TableWithColumns < F > > ,
challenge : GrandProductChallenge < F > ,
constraint_degree : usize ,
) -> Vec < ( usize , Vec < PolynomialValues < F > > ) > {
let grouped_lookups = looking_tables . iter ( ) . group_by ( | a | a . table ) ;
grouped_lookups
. into_iter ( )
. map ( | ( table , group ) | {
let degree = all_stark_traces [ table as usize ] [ 0 ] . len ( ) ;
let columns_filters = group
. map ( | table | ( & table . columns [ .. ] , & table . filter ) )
. collect ::< Vec < ( & [ Column < F > ] , & Option < Filter < F > > ) > > ( ) ;
(
table as usize ,
partial_sums (
& all_stark_traces [ table as usize ] ,
& columns_filters ,
challenge ,
constraint_degree ,
) ,
)
} )
. collect ::< Vec < ( usize , Vec < PolynomialValues < F > > ) > > ( )
}
2023-12-04 16:26:10 -05:00
/// Computes the cross-table lookup partial sums for one table and given column linear combinations.
2023-10-30 14:28:24 -04:00
/// `trace` represents the trace values for the given table.
2024-01-10 08:54:13 +01:00
/// `columns` is a vector of column linear combinations to evaluate. Each element in the vector represents columns that need to be combined.
/// `filter_cols` are column linear combinations used to determine whether a row should be selected.
2023-10-30 14:28:24 -04:00
/// `challenge` is a cross-table lookup challenge.
2023-12-04 16:26:10 -05:00
/// The initial sum `s` is 0.
2024-01-10 08:54:13 +01:00
/// For each row, if the `filter_column` evaluates to 1, then the row is selected. All the column linear combinations are evaluated at said row.
/// The evaluations of each elements of `columns` are then combined together to form a value `v`.
/// The values `v`` are grouped together, in groups of size `constraint_degree - 1` (2 in our case). For each group, we construct a helper
/// column: h = \sum_i 1/(v_i).
///
/// The sum is updated: `s += \sum h_i`, and is pushed to the vector of partial sums `z``.
/// Returns the helper columns and `z`.
2023-12-04 16:26:10 -05:00
fn partial_sums < F : Field > (
2022-05-05 22:21:09 +02:00
trace : & [ PolynomialValues < F > ] ,
2024-01-10 08:54:13 +01:00
columns_filters : & [ ColumnFilter < F > ] ,
2022-05-18 09:22:58 +02:00
challenge : GrandProductChallenge < F > ,
2024-01-10 08:54:13 +01:00
constraint_degree : usize ,
) -> Vec < PolynomialValues < F > > {
2022-05-12 20:38:11 +02:00
let degree = trace [ 0 ] . len ( ) ;
2024-01-10 08:54:13 +01:00
let mut z = Vec ::with_capacity ( degree ) ;
2023-12-04 16:26:10 -05:00
2024-01-10 08:54:13 +01:00
let mut helper_columns =
get_helper_cols ( trace , degree , columns_filters , challenge , constraint_degree ) ;
2023-12-04 16:26:10 -05:00
2024-01-10 08:54:13 +01:00
let x = helper_columns
. iter ( )
. map ( | col | col . values [ degree - 1 ] )
. sum ::< F > ( ) ;
z . push ( x ) ;
2023-12-04 16:26:10 -05:00
2024-01-10 08:54:13 +01:00
for i in ( 0 .. degree - 1 ) . rev ( ) {
let x = helper_columns . iter ( ) . map ( | col | col . values [ i ] ) . sum ::< F > ( ) ;
z . push ( z [ z . len ( ) - 1 ] + x ) ;
}
z . reverse ( ) ;
if columns_filters . len ( ) > 2 {
helper_columns . push ( z . into ( ) ) ;
} else {
helper_columns = vec! [ z . into ( ) ] ;
2022-05-05 22:21:09 +02:00
}
2023-12-04 16:26:10 -05:00
2024-01-10 08:54:13 +01:00
helper_columns
2022-05-05 22:21:09 +02:00
}
2023-10-30 14:28:24 -04:00
/// Data necessary to check the cross-table lookups of a given table.
2022-05-12 13:47:55 +02:00
#[ derive(Clone) ]
2023-11-13 09:26:56 -05:00
pub ( crate ) struct CtlCheckVars < ' a , F , FE , P , const D2 : usize >
2022-05-05 22:21:09 +02:00
where
F : Field ,
FE : FieldExtension < D2 , BaseField = F > ,
P : PackedField < Scalar = FE > ,
{
2024-01-10 08:54:13 +01:00
/// Helper columns to check that the Z polyomial
/// was constructed correctly.
pub ( crate ) helper_columns : Vec < P > ,
2023-10-30 14:28:24 -04:00
/// Evaluation of the trace polynomials at point `zeta`.
2022-05-06 14:55:54 +02:00
pub ( crate ) local_z : P ,
2023-10-30 14:28:24 -04:00
/// Evaluation of the trace polynomials at point `g * zeta`
2022-05-06 14:55:54 +02:00
pub ( crate ) next_z : P ,
2023-10-30 14:28:24 -04:00
/// Cross-table lookup challenges.
2022-05-12 13:51:02 +02:00
pub ( crate ) challenges : GrandProductChallenge < F > ,
2023-10-30 14:28:24 -04:00
/// Column linear combinations of the `CrossTableLookup`s.
2024-01-10 08:54:13 +01:00
pub ( crate ) columns : Vec < & ' a [ Column < F > ] > ,
2023-12-05 17:02:37 -05:00
/// Filter that evaluates to either 1 or 0.
2024-01-10 08:54:13 +01:00
pub ( crate ) filter : Vec < Option < Filter < F > > > ,
2022-05-12 13:47:55 +02:00
}
impl < ' a , F : RichField + Extendable < D > , const D : usize >
2022-05-16 20:45:30 +02:00
CtlCheckVars < ' a , F , F ::Extension , F ::Extension , D >
2022-05-12 13:47:55 +02:00
{
2023-10-30 14:28:24 -04:00
/// Extracts the `CtlCheckVars` for each STARK.
2023-04-01 09:34:13 -04:00
pub ( crate ) fn from_proofs < C : GenericConfig < D , F = F > > (
proofs : & [ StarkProofWithMetadata < F , C , D > ; NUM_TABLES ] ,
2022-05-17 09:24:22 +02:00
cross_table_lookups : & ' a [ CrossTableLookup < F > ] ,
2022-05-12 13:51:02 +02:00
ctl_challenges : & ' a GrandProductChallengeSet < F > ,
2023-02-13 15:58:26 +01:00
num_lookup_columns : & [ usize ; NUM_TABLES ] ,
2024-01-10 08:54:13 +01:00
num_helper_ctl_columns : & Vec < [ usize ; NUM_TABLES ] > ,
2023-05-11 02:59:02 +10:00
) -> [ Vec < Self > ; NUM_TABLES ] {
2024-01-10 08:54:13 +01:00
let mut total_num_helper_cols_by_table = [ 0 ; NUM_TABLES ] ;
for p_ctls in num_helper_ctl_columns {
for j in 0 .. NUM_TABLES {
total_num_helper_cols_by_table [ j ] + = p_ctls [ j ] * ctl_challenges . challenges . len ( ) ;
}
}
2023-10-30 14:28:24 -04:00
// Get all cross-table lookup polynomial openings for each STARK proof.
2022-05-12 13:47:55 +02:00
let mut ctl_zs = proofs
. iter ( )
2023-02-13 15:58:26 +01:00
. zip ( num_lookup_columns )
. map ( | ( p , & num_lookup ) | {
Shrink STARK proofs to a constant degree
The goal here is to end up with a single "root" circuit representing any EVM proof. I.e. it must verify each STARK, but be general enough to work with any combination of STARK sizes (within some range of sizes that we chose to support). This root circuit can then be plugged into our aggregation circuit.
In particular, for each STARK, and for each initial `degree_bits` (within a range that we choose to support), this adds a "shrinking chain" of circuits. Such a chain shrinks a STARK proof from that initial `degree_bits` down to a constant, `THRESHOLD_DEGREE_BITS`.
The root circuit then combines these shrunk-to-constant proofs for each table. It's similar to `RecursiveAllProof::verify_circuit`; I adapted the code from there and I think we can remove it after. The main difference is that now instead of having one verification key per STARK, we have several possible VKs, one per initial `degree_bits`. We bake the list of possible VKs into the root circuit, and have the prover indicate the index of the VK they're actually using.
This also partially removes the default feature of CTLs. So far we've used filters instead of defaults. Until now it was easy to keep supporting defaults just in case, but here maintaining support would require some more work. E.g. we couldn't use `exp_u64` any more, since the size delta is now dynamic, it can't be hardcoded. If there are no concerns, I'll fully remove the feature after.
2022-12-27 18:15:18 -08:00
let openings = & p . proof . openings ;
2024-01-10 08:54:13 +01:00
let ctl_zs = & openings . auxiliary_polys [ num_lookup .. ] ;
let ctl_zs_next = & openings . auxiliary_polys_next [ num_lookup .. ] ;
ctl_zs . iter ( ) . zip ( ctl_zs_next ) . collect ::< Vec < _ > > ( )
2022-05-12 13:47:55 +02:00
} )
. collect ::< Vec < _ > > ( ) ;
2023-10-30 14:28:24 -04:00
// Put each cross-table lookup polynomial into the correct table data: if a CTL polynomial is extracted from looking/looked table t, then we add it to the `CtlCheckVars` of table t.
2024-01-10 08:54:13 +01:00
let mut start_indices = [ 0 ; NUM_TABLES ] ;
let mut z_indices = [ 0 ; NUM_TABLES ] ;
2022-08-26 10:12:45 +02:00
let mut ctl_vars_per_table = [ 0 ; NUM_TABLES ] . map ( | _ | vec! [ ] ) ;
2024-01-10 08:54:13 +01:00
for (
CrossTableLookup {
looking_tables ,
looked_table ,
} ,
num_ctls ,
) in cross_table_lookups . iter ( ) . zip ( num_helper_ctl_columns )
2022-05-18 09:22:58 +02:00
{
for & challenges in & ctl_challenges . challenges {
2024-01-10 08:54:13 +01:00
// Group looking tables by `Table`, since we bundle the looking tables taken from the same `Table` together thanks to helper columns.
// We want to only iterate on each `Table` once.
let mut filtered_looking_tables =
Vec ::with_capacity ( min ( looking_tables . len ( ) , NUM_TABLES ) ) ;
2022-06-06 20:51:14 +02:00
for table in looking_tables {
2024-01-10 08:54:13 +01:00
if ! filtered_looking_tables . contains ( & ( table . table as usize ) ) {
filtered_looking_tables . push ( table . table as usize ) ;
}
}
for ( i , & table ) in filtered_looking_tables . iter ( ) . enumerate ( ) {
// We have first all the helper polynomials, then all the z polynomials.
let ( looking_z , looking_z_next ) =
ctl_zs [ table ] [ total_num_helper_cols_by_table [ table ] + z_indices [ table ] ] ;
let count = looking_tables
. iter ( )
. filter ( | looking_table | looking_table . table as usize = = table )
. count ( ) ;
let cols_filts = looking_tables . iter ( ) . filter_map ( | looking_table | {
if looking_table . table as usize = = table {
Some ( ( & looking_table . columns , & looking_table . filter ) )
} else {
None
}
} ) ;
let mut columns = Vec ::with_capacity ( count ) ;
let mut filter = Vec ::with_capacity ( count ) ;
for ( col , filt ) in cols_filts {
columns . push ( & col [ .. ] ) ;
filter . push ( filt . clone ( ) ) ;
}
let helper_columns = ctl_zs [ table ]
[ start_indices [ table ] .. start_indices [ table ] + num_ctls [ table ] ]
. iter ( )
. map ( | & ( h , _ ) | * h )
. collect ::< Vec < _ > > ( ) ;
start_indices [ table ] + = num_ctls [ table ] ;
z_indices [ table ] + = 1 ;
ctl_vars_per_table [ table ] . push ( Self {
helper_columns ,
2022-06-01 18:53:19 +02:00
local_z : * looking_z ,
next_z : * looking_z_next ,
challenges ,
2024-01-10 08:54:13 +01:00
columns ,
filter ,
2022-06-01 18:53:19 +02:00
} ) ;
}
2022-05-12 13:47:55 +02:00
2024-01-10 08:54:13 +01:00
let ( looked_z , looked_z_next ) = ctl_zs [ looked_table . table as usize ]
[ total_num_helper_cols_by_table [ looked_table . table as usize ]
+ z_indices [ looked_table . table as usize ] ] ;
z_indices [ looked_table . table as usize ] + = 1 ;
let columns = vec! [ & looked_table . columns [ .. ] ] ;
let filter = vec! [ looked_table . filter . clone ( ) ] ;
2022-06-06 20:51:14 +02:00
ctl_vars_per_table [ looked_table . table as usize ] . push ( Self {
2024-01-10 08:54:13 +01:00
helper_columns : vec ! [ ] ,
2022-05-18 09:22:58 +02:00
local_z : * looked_z ,
next_z : * looked_z_next ,
challenges ,
2024-01-10 08:54:13 +01:00
columns ,
filter ,
2022-05-18 09:22:58 +02:00
} ) ;
}
}
ctl_vars_per_table
2022-05-12 13:47:55 +02:00
}
2022-05-05 22:21:09 +02:00
}
2024-01-10 08:54:13 +01:00
/// Given data associated to a lookup (either a CTL or a range-check), check the associated helper polynomials.
pub ( crate ) fn eval_helper_columns < F , FE , P , const D : usize , const D2 : usize > (
filter : & [ Option < Filter < F > > ] ,
columns : & [ Vec < P > ] ,
local_values : & [ P ] ,
next_values : & [ P ] ,
helper_columns : & [ P ] ,
constraint_degree : usize ,
challenges : & GrandProductChallenge < F > ,
consumer : & mut ConstraintConsumer < P > ,
) where
F : RichField + Extendable < D > ,
FE : FieldExtension < D2 , BaseField = F > ,
P : PackedField < Scalar = FE > ,
{
if ! helper_columns . is_empty ( ) {
for ( j , chunk ) in columns . chunks ( constraint_degree - 1 ) . enumerate ( ) {
let fs =
& filter [ ( constraint_degree - 1 ) * j .. ( constraint_degree - 1 ) * j + chunk . len ( ) ] ;
let h = helper_columns [ j ] ;
match chunk . len ( ) {
2 = > {
let combin0 = challenges . combine ( & chunk [ 0 ] ) ;
let combin1 = challenges . combine ( chunk [ 1 ] . iter ( ) ) ;
let f0 = if let Some ( filter0 ) = & fs [ 0 ] {
filter0 . eval_filter ( local_values , next_values )
} else {
P ::ONES
} ;
let f1 = if let Some ( filter1 ) = & fs [ 1 ] {
filter1 . eval_filter ( local_values , next_values )
} else {
P ::ONES
} ;
consumer . constraint ( combin1 * combin0 * h - f0 * combin1 - f1 * combin0 ) ;
}
1 = > {
let combin = challenges . combine ( & chunk [ 0 ] ) ;
let f0 = if let Some ( filter1 ) = & fs [ 0 ] {
filter1 . eval_filter ( local_values , next_values )
} else {
P ::ONES
} ;
consumer . constraint ( combin * h - f0 ) ;
}
_ = > todo! ( " Allow other constraint degrees " ) ,
}
}
}
}
2023-10-30 14:28:24 -04:00
/// Checks the cross-table lookup Z polynomials for each table:
2023-12-04 16:26:10 -05:00
/// - Checks that the CTL `Z` partial sums are correctly updated.
/// - Checks that the final value of the CTL sum is the combination of all STARKs' CTL polynomials.
/// CTL `Z` partial sums are upside down: the complete sum is on the first row, and
2023-09-11 14:11:13 -04:00
/// the first term is on the last row. This allows the transition constraint to be:
2023-12-04 16:26:10 -05:00
/// `combine(w) * (Z(w) - Z(gw)) = filter` where combine is called on the local row
2023-09-11 14:11:13 -04:00
/// and not the next. This enables CTLs across two rows.
2023-02-10 21:47:51 -08:00
pub ( crate ) fn eval_cross_table_lookup_checks < F , FE , P , S , const D : usize , const D2 : usize > (
2023-09-22 09:19:13 -04:00
vars : & S ::EvaluationFrame < FE , P , D2 > ,
2022-05-16 20:45:30 +02:00
ctl_vars : & [ CtlCheckVars < F , FE , P , D2 > ] ,
2022-05-05 22:21:09 +02:00
consumer : & mut ConstraintConsumer < P > ,
2024-01-10 08:54:13 +01:00
constraint_degree : usize ,
2022-05-05 22:21:09 +02:00
) where
F : RichField + Extendable < D > ,
FE : FieldExtension < D2 , BaseField = F > ,
P : PackedField < Scalar = FE > ,
S : Stark < F , D > ,
{
2023-09-22 09:19:13 -04:00
let local_values = vars . get_local_values ( ) ;
let next_values = vars . get_next_values ( ) ;
2022-05-13 11:20:29 +02:00
for lookup_vars in ctl_vars {
2022-05-16 20:45:30 +02:00
let CtlCheckVars {
2024-01-10 08:54:13 +01:00
helper_columns ,
2022-05-06 14:55:54 +02:00
local_z ,
next_z ,
challenges ,
columns ,
2023-12-05 17:02:37 -05:00
filter ,
2022-05-13 11:20:29 +02:00
} = lookup_vars ;
2023-09-11 14:11:13 -04:00
2023-10-30 14:28:24 -04:00
// Compute all linear combinations on the current table, and combine them using the challenge.
2023-09-15 18:14:07 -04:00
let evals = columns
2023-09-11 14:11:13 -04:00
. iter ( )
2024-01-10 08:54:13 +01:00
. map ( | col | {
col . iter ( )
. map ( | c | c . eval_with_next ( local_values , next_values ) )
. collect ::< Vec < _ > > ( )
} )
2023-09-11 14:11:13 -04:00
. collect ::< Vec < _ > > ( ) ;
2024-01-10 08:54:13 +01:00
// Check helper columns.
eval_helper_columns (
filter ,
& evals ,
local_values ,
next_values ,
helper_columns ,
constraint_degree ,
challenges ,
consumer ,
) ;
if ! helper_columns . is_empty ( ) {
let h_sum = helper_columns . iter ( ) . fold ( P ::ZEROS , | acc , x | acc + * x ) ;
// Check value of `Z(g^(n-1))`
consumer . constraint_last_row ( * local_z - h_sum ) ;
// Check `Z(w) = Z(gw) + \sum h_i`
consumer . constraint_transition ( * local_z - * next_z - h_sum ) ;
} else if columns . len ( ) > 1 {
let combin0 = challenges . combine ( & evals [ 0 ] ) ;
let combin1 = challenges . combine ( & evals [ 1 ] ) ;
let f0 = if let Some ( filter0 ) = & filter [ 0 ] {
filter0 . eval_filter ( local_values , next_values )
} else {
P ::ONES
} ;
let f1 = if let Some ( filter1 ) = & filter [ 1 ] {
filter1 . eval_filter ( local_values , next_values )
} else {
P ::ONES
} ;
consumer
. constraint_last_row ( combin0 * combin1 * * local_z - f0 * combin1 - f1 * combin0 ) ;
consumer . constraint_transition (
combin0 * combin1 * ( * local_z - * next_z ) - f0 * combin1 - f1 * combin0 ,
) ;
} else {
let combin0 = challenges . combine ( & evals [ 0 ] ) ;
let f0 = if let Some ( filter0 ) = & filter [ 0 ] {
filter0 . eval_filter ( local_values , next_values )
} else {
P ::ONES
} ;
consumer . constraint_last_row ( combin0 * * local_z - f0 ) ;
consumer . constraint_transition ( combin0 * ( * local_z - * next_z ) - f0 ) ;
}
2022-05-05 22:21:09 +02:00
}
}
2022-05-11 16:09:12 +02:00
2023-10-30 14:28:24 -04:00
/// Circuit version of `CtlCheckVars`. Data necessary to check the cross-table lookups of a given table.
2022-05-23 17:49:04 +02:00
#[ derive(Clone) ]
2024-01-10 08:54:13 +01:00
pub ( crate ) struct CtlCheckVarsTarget < F : Field , const D : usize > {
///Evaluation of the helper columns to check that the Z polyomial
/// was constructed correctly.
pub ( crate ) helper_columns : Vec < ExtensionTarget < D > > ,
2023-10-30 14:28:24 -04:00
/// Evaluation of the trace polynomials at point `zeta`.
2022-05-23 17:49:04 +02:00
pub ( crate ) local_z : ExtensionTarget < D > ,
2023-10-30 14:28:24 -04:00
/// Evaluation of the trace polynomials at point `g * zeta`.
2022-05-23 17:49:04 +02:00
pub ( crate ) next_z : ExtensionTarget < D > ,
2023-10-30 14:28:24 -04:00
/// Cross-table lookup challenges.
2022-05-23 17:49:04 +02:00
pub ( crate ) challenges : GrandProductChallenge < Target > ,
2023-10-30 14:28:24 -04:00
/// Column linear combinations of the `CrossTableLookup`s.
2024-01-10 08:54:13 +01:00
pub ( crate ) columns : Vec < Vec < Column < F > > > ,
2023-12-05 17:02:37 -05:00
/// Filter that evaluates to either 1 or 0.
2024-01-10 08:54:13 +01:00
pub ( crate ) filter : Vec < Option < Filter < F > > > ,
2022-05-23 17:49:04 +02:00
}
2024-01-10 08:54:13 +01:00
impl < ' a , F : Field , const D : usize > CtlCheckVarsTarget < F , D > {
2023-10-30 14:28:24 -04:00
/// Circuit version of `from_proofs`. Extracts the `CtlCheckVarsTarget` for each STARK.
2022-08-25 22:04:28 +02:00
pub ( crate ) fn from_proof (
table : Table ,
2022-08-26 09:42:55 +02:00
proof : & StarkProofTarget < D > ,
2022-08-25 22:04:28 +02:00
cross_table_lookups : & ' a [ CrossTableLookup < F > ] ,
ctl_challenges : & ' a GrandProductChallengeSet < Target > ,
2023-02-13 15:58:26 +01:00
num_lookup_columns : usize ,
2024-01-10 08:54:13 +01:00
total_num_helper_columns : usize ,
num_helper_ctl_columns : & [ usize ] ,
2022-08-25 22:04:28 +02:00
) -> Vec < Self > {
2023-10-30 14:28:24 -04:00
// Get all cross-table lookup polynomial openings for each STARK proof.
2022-08-25 22:04:28 +02:00
let mut ctl_zs = {
2022-08-26 09:42:55 +02:00
let openings = & proof . openings ;
2023-02-13 15:58:26 +01:00
let ctl_zs = openings . auxiliary_polys . iter ( ) . skip ( num_lookup_columns ) ;
2022-08-25 22:04:28 +02:00
let ctl_zs_next = openings
2023-02-13 15:58:26 +01:00
. auxiliary_polys_next
2022-08-25 22:04:28 +02:00
. iter ( )
2023-02-13 15:58:26 +01:00
. skip ( num_lookup_columns ) ;
2024-01-10 08:54:13 +01:00
ctl_zs . zip ( ctl_zs_next ) . collect ::< Vec < _ > > ( )
2022-08-25 22:04:28 +02:00
} ;
2023-10-30 14:28:24 -04:00
// Put each cross-table lookup polynomial into the correct table data: if a CTL polynomial is extracted from looking/looked table t, then we add it to the `CtlCheckVars` of table t.
2024-01-10 08:54:13 +01:00
let mut z_index = 0 ;
let mut start_index = 0 ;
2022-08-25 22:04:28 +02:00
let mut ctl_vars = vec! [ ] ;
2024-01-10 08:54:13 +01:00
for (
i ,
CrossTableLookup {
looking_tables ,
looked_table ,
} ,
) in cross_table_lookups . iter ( ) . enumerate ( )
2022-08-25 22:04:28 +02:00
{
for & challenges in & ctl_challenges . challenges {
2024-01-10 08:54:13 +01:00
// Group looking tables by `Table`, since we bundle the looking tables taken from the same `Table` together thanks to helper columns.
let count = looking_tables
. iter ( )
. filter ( | looking_table | looking_table . table = = table )
. count ( ) ;
let cols_filts = looking_tables . iter ( ) . filter_map ( | looking_table | {
2022-08-25 22:04:28 +02:00
if looking_table . table = = table {
2024-01-10 08:54:13 +01:00
Some ( ( & looking_table . columns , & looking_table . filter ) )
} else {
None
2022-08-25 22:04:28 +02:00
}
2024-01-10 08:54:13 +01:00
} ) ;
if count > 0 {
let mut columns = Vec ::with_capacity ( count ) ;
let mut filter = Vec ::with_capacity ( count ) ;
for ( col , filt ) in cols_filts {
columns . push ( col . clone ( ) ) ;
filter . push ( filt . clone ( ) ) ;
}
let ( looking_z , looking_z_next ) = ctl_zs [ total_num_helper_columns + z_index ] ;
let helper_columns = ctl_zs
[ start_index .. start_index + num_helper_ctl_columns [ i ] ]
. iter ( )
. map ( | ( & h , _ ) | h )
. collect ::< Vec < _ > > ( ) ;
start_index + = num_helper_ctl_columns [ i ] ;
z_index + = 1 ;
// let columns = group.0.clone();
// let filter = group.1.clone();
ctl_vars . push ( Self {
helper_columns ,
local_z : * looking_z ,
next_z : * looking_z_next ,
challenges ,
columns ,
filter ,
} ) ;
2022-08-25 22:04:28 +02:00
}
if looked_table . table = = table {
2024-01-10 08:54:13 +01:00
let ( looked_z , looked_z_next ) = ctl_zs [ total_num_helper_columns + z_index ] ;
z_index + = 1 ;
let columns = vec! [ looked_table . columns . clone ( ) ] ;
let filter = vec! [ looked_table . filter . clone ( ) ] ;
2022-08-25 22:04:28 +02:00
ctl_vars . push ( Self {
2024-01-10 08:54:13 +01:00
helper_columns : vec ! [ ] ,
2022-08-25 22:04:28 +02:00
local_z : * looked_z ,
next_z : * looked_z_next ,
challenges ,
2024-01-10 08:54:13 +01:00
columns ,
filter ,
2022-08-25 22:04:28 +02:00
} ) ;
}
}
}
2024-01-10 08:54:13 +01:00
2022-08-25 22:04:28 +02:00
ctl_vars
}
2022-05-23 17:49:04 +02:00
}
2024-01-10 08:54:13 +01:00
/// Circuit version of `eval_helper_columns`.
/// Given data associated to a lookup (either a CTL or a range-check), check the associated helper polynomials.
pub ( crate ) fn eval_helper_columns_circuit < F : RichField + Extendable < D > , const D : usize > (
builder : & mut CircuitBuilder < F , D > ,
filter : & [ Option < Filter < F > > ] ,
columns : & [ Vec < ExtensionTarget < D > > ] ,
local_values : & [ ExtensionTarget < D > ] ,
next_values : & [ ExtensionTarget < D > ] ,
helper_columns : & [ ExtensionTarget < D > ] ,
constraint_degree : usize ,
challenges : & GrandProductChallenge < Target > ,
consumer : & mut RecursiveConstraintConsumer < F , D > ,
) {
if ! helper_columns . is_empty ( ) {
for ( j , chunk ) in columns . chunks ( constraint_degree - 1 ) . enumerate ( ) {
let fs =
& filter [ ( constraint_degree - 1 ) * j .. ( constraint_degree - 1 ) * j + chunk . len ( ) ] ;
let h = helper_columns [ j ] ;
let one = builder . one_extension ( ) ;
match chunk . len ( ) {
2 = > {
let combin0 = challenges . combine_circuit ( builder , & chunk [ 0 ] ) ;
let combin1 = challenges . combine_circuit ( builder , & chunk [ 1 ] ) ;
let f0 = if let Some ( filter0 ) = & fs [ 0 ] {
filter0 . eval_filter_circuit ( builder , local_values , next_values )
} else {
one
} ;
let f1 = if let Some ( filter1 ) = & fs [ 1 ] {
filter1 . eval_filter_circuit ( builder , local_values , next_values )
} else {
one
} ;
let constr = builder . mul_sub_extension ( combin0 , h , f0 ) ;
let constr = builder . mul_extension ( constr , combin1 ) ;
let f1_constr = builder . mul_extension ( f1 , combin0 ) ;
let constr = builder . sub_extension ( constr , f1_constr ) ;
consumer . constraint ( builder , constr ) ;
}
1 = > {
let combin = challenges . combine_circuit ( builder , & chunk [ 0 ] ) ;
let f0 = if let Some ( filter1 ) = & fs [ 0 ] {
filter1 . eval_filter_circuit ( builder , local_values , next_values )
} else {
one
} ;
let constr = builder . mul_sub_extension ( combin , h , f0 ) ;
consumer . constraint ( builder , constr ) ;
}
_ = > todo! ( " Allow other constraint degrees " ) ,
}
}
}
}
2023-12-04 16:26:10 -05:00
/// Circuit version of `eval_cross_table_lookup_checks`. Checks the cross-table lookup Z polynomials for each table:
/// - Checks that the CTL `Z` partial sums are correctly updated.
/// - Checks that the final value of the CTL sum is the combination of all STARKs' CTL polynomials.
/// CTL `Z` partial sums are upside down: the complete sum is on the first row, and
2023-10-30 14:28:24 -04:00
/// the first term is on the last row. This allows the transition constraint to be:
2023-12-04 16:26:10 -05:00
/// `combine(w) * (Z(w) - Z(gw)) = filter` where combine is called on the local row
2023-10-30 14:28:24 -04:00
/// and not the next. This enables CTLs across two rows.
2022-05-24 16:24:52 +02:00
pub ( crate ) fn eval_cross_table_lookup_checks_circuit <
S : Stark < F , D > ,
F : RichField + Extendable < D > ,
const D : usize ,
> (
builder : & mut CircuitBuilder < F , D > ,
2023-09-22 09:19:13 -04:00
vars : & S ::EvaluationFrameTarget ,
2022-06-14 00:53:31 +02:00
ctl_vars : & [ CtlCheckVarsTarget < F , D > ] ,
2022-05-24 16:24:52 +02:00
consumer : & mut RecursiveConstraintConsumer < F , D > ,
2024-01-10 08:54:13 +01:00
constraint_degree : usize ,
2022-05-24 16:24:52 +02:00
) {
2023-09-22 09:19:13 -04:00
let local_values = vars . get_local_values ( ) ;
let next_values = vars . get_next_values ( ) ;
2024-01-10 08:54:13 +01:00
let one = builder . one_extension ( ) ;
2022-05-24 16:24:52 +02:00
for lookup_vars in ctl_vars {
let CtlCheckVarsTarget {
2024-01-10 08:54:13 +01:00
helper_columns ,
2022-05-24 16:24:52 +02:00
local_z ,
next_z ,
challenges ,
columns ,
2023-12-05 17:02:37 -05:00
filter ,
2022-05-24 16:24:52 +02:00
} = lookup_vars ;
2023-10-30 14:28:24 -04:00
// Compute all linear combinations on the current table, and combine them using the challenge.
2023-09-15 18:14:07 -04:00
let evals = columns
2022-08-23 12:22:54 -07:00
. iter ( )
2024-01-10 08:54:13 +01:00
. map ( | col | {
col . iter ( )
. map ( | c | c . eval_with_next_circuit ( builder , local_values , next_values ) )
. collect ::< Vec < _ > > ( )
} )
2022-08-23 12:22:54 -07:00
. collect ::< Vec < _ > > ( ) ;
2023-09-11 14:11:13 -04:00
2024-01-10 08:54:13 +01:00
// Check helper columns.
eval_helper_columns_circuit (
builder ,
filter ,
& evals ,
local_values ,
next_values ,
helper_columns ,
constraint_degree ,
challenges ,
consumer ,
) ;
2023-09-11 14:11:13 -04:00
2023-12-04 16:26:10 -05:00
let z_diff = builder . sub_extension ( * local_z , * next_z ) ;
2024-01-10 08:54:13 +01:00
if ! helper_columns . is_empty ( ) {
// Check value of `Z(g^(n-1))`
let h_sum = builder . add_many_extension ( helper_columns ) ;
let last_row = builder . sub_extension ( * local_z , h_sum ) ;
consumer . constraint_last_row ( builder , last_row ) ;
// Check `Z(w) = Z(gw) * (filter / combination)`
let transition = builder . sub_extension ( z_diff , h_sum ) ;
consumer . constraint_transition ( builder , transition ) ;
} else if columns . len ( ) > 1 {
let combin0 = challenges . combine_circuit ( builder , & evals [ 0 ] ) ;
let combin1 = challenges . combine_circuit ( builder , & evals [ 1 ] ) ;
let f0 = if let Some ( filter0 ) = & filter [ 0 ] {
filter0 . eval_filter_circuit ( builder , local_values , next_values )
} else {
one
} ;
let f1 = if let Some ( filter1 ) = & filter [ 1 ] {
filter1 . eval_filter_circuit ( builder , local_values , next_values )
} else {
one
} ;
let combined = builder . mul_sub_extension ( combin1 , * local_z , f1 ) ;
let combined = builder . mul_extension ( combined , combin0 ) ;
let constr = builder . arithmetic_extension ( F ::NEG_ONE , F ::ONE , f0 , combin1 , combined ) ;
consumer . constraint_last_row ( builder , constr ) ;
let combined = builder . mul_sub_extension ( combin1 , z_diff , f1 ) ;
let combined = builder . mul_extension ( combined , combin0 ) ;
let constr = builder . arithmetic_extension ( F ::NEG_ONE , F ::ONE , f0 , combin1 , combined ) ;
consumer . constraint_last_row ( builder , constr ) ;
} else {
let combin0 = challenges . combine_circuit ( builder , & evals [ 0 ] ) ;
let f0 = if let Some ( filter0 ) = & filter [ 0 ] {
filter0 . eval_filter_circuit ( builder , local_values , next_values )
} else {
one
} ;
let constr = builder . mul_sub_extension ( combin0 , * local_z , f0 ) ;
consumer . constraint_last_row ( builder , constr ) ;
let constr = builder . mul_sub_extension ( combin0 , z_diff , f0 ) ;
consumer . constraint_transition ( builder , constr ) ;
}
2022-05-24 16:24:52 +02:00
}
}
2023-10-30 14:28:24 -04:00
/// Verifies all cross-table lookups.
2023-02-10 21:47:51 -08:00
pub ( crate ) fn verify_cross_table_lookups < F : RichField + Extendable < D > , const D : usize > (
Shrink STARK proofs to a constant degree
The goal here is to end up with a single "root" circuit representing any EVM proof. I.e. it must verify each STARK, but be general enough to work with any combination of STARK sizes (within some range of sizes that we chose to support). This root circuit can then be plugged into our aggregation circuit.
In particular, for each STARK, and for each initial `degree_bits` (within a range that we choose to support), this adds a "shrinking chain" of circuits. Such a chain shrinks a STARK proof from that initial `degree_bits` down to a constant, `THRESHOLD_DEGREE_BITS`.
The root circuit then combines these shrunk-to-constant proofs for each table. It's similar to `RecursiveAllProof::verify_circuit`; I adapted the code from there and I think we can remove it after. The main difference is that now instead of having one verification key per STARK, we have several possible VKs, one per initial `degree_bits`. We bake the list of possible VKs into the root circuit, and have the prover indicate the index of the VK they're actually using.
This also partially removes the default feature of CTLs. So far we've used filters instead of defaults. Until now it was easy to keep supporting defaults just in case, but here maintaining support would require some more work. E.g. we couldn't use `exp_u64` any more, since the size delta is now dynamic, it can't be hardcoded. If there are no concerns, I'll fully remove the feature after.
2022-12-27 18:15:18 -08:00
cross_table_lookups : & [ CrossTableLookup < F > ] ,
2023-09-11 14:11:13 -04:00
ctl_zs_first : [ Vec < F > ; NUM_TABLES ] ,
2023-12-04 16:26:10 -05:00
ctl_extra_looking_sums : Vec < Vec < F > > ,
2022-05-11 16:09:12 +02:00
config : & StarkConfig ,
) -> Result < ( ) > {
2023-09-11 14:11:13 -04:00
let mut ctl_zs_openings = ctl_zs_first . iter ( ) . map ( | v | v . iter ( ) ) . collect ::< Vec < _ > > ( ) ;
2023-09-12 14:45:37 -04:00
for (
index ,
CrossTableLookup {
looking_tables ,
looked_table ,
} ,
) in cross_table_lookups . iter ( ) . enumerate ( )
2022-05-11 16:09:12 +02:00
{
2023-10-30 14:28:24 -04:00
// Get elements looking into `looked_table` that are not associated to any STARK.
2023-12-04 16:26:10 -05:00
let extra_sum_vec = & ctl_extra_looking_sums [ looked_table . table as usize ] ;
2024-01-10 08:54:13 +01:00
// We want to iterate on each looking table only once.
let mut filtered_looking_tables = vec! [ ] ;
for table in looking_tables {
if ! filtered_looking_tables . contains ( & ( table . table as usize ) ) {
filtered_looking_tables . push ( table . table as usize ) ;
}
}
2023-04-14 21:55:44 +08:00
for c in 0 .. config . num_challenges {
2023-10-30 14:28:24 -04:00
// Compute the combination of all looking table CTL polynomial openings.
2024-01-10 08:54:13 +01:00
let looking_zs_sum = filtered_looking_tables
2022-06-16 02:06:15 +02:00
. iter ( )
2024-01-10 08:54:13 +01:00
. map ( | & table | * ctl_zs_openings [ table ] . next ( ) . unwrap ( ) )
2023-12-04 16:26:10 -05:00
. sum ::< F > ( )
+ extra_sum_vec [ c ] ;
2023-04-14 21:55:44 +08:00
2023-10-30 14:28:24 -04:00
// Get the looked table CTL polynomial opening.
2023-04-14 21:55:44 +08:00
let looked_z = * ctl_zs_openings [ looked_table . table as usize ] . next ( ) . unwrap ( ) ;
2023-10-30 14:28:24 -04:00
// Ensure that the combination of looking table openings is equal to the looked table opening.
2023-01-03 11:36:42 -08:00
ensure! (
2023-12-04 16:26:10 -05:00
looking_zs_sum = = looked_z ,
2023-09-12 14:45:37 -04:00
" Cross-table lookup {:?} verification failed. " ,
index
2023-01-03 11:36:42 -08:00
) ;
2022-06-16 02:06:15 +02:00
}
2022-05-11 16:09:12 +02:00
}
2022-06-16 02:15:40 +02:00
debug_assert! ( ctl_zs_openings . iter_mut ( ) . all ( | iter | iter . next ( ) . is_none ( ) ) ) ;
2022-05-11 16:09:12 +02:00
Ok ( ( ) )
}
2022-05-24 16:24:52 +02:00
2023-10-30 14:28:24 -04:00
/// Circuit version of `verify_cross_table_lookups`. Verifies all cross-table lookups.
2023-02-10 21:47:51 -08:00
pub ( crate ) fn verify_cross_table_lookups_circuit < F : RichField + Extendable < D > , const D : usize > (
2022-05-24 16:24:52 +02:00
builder : & mut CircuitBuilder < F , D > ,
cross_table_lookups : Vec < CrossTableLookup < F > > ,
2023-09-11 14:11:13 -04:00
ctl_zs_first : [ Vec < Target > ; NUM_TABLES ] ,
2023-12-04 16:26:10 -05:00
ctl_extra_looking_sums : Vec < Vec < Target > > ,
2022-05-24 16:24:52 +02:00
inner_config : & StarkConfig ,
) {
2023-09-11 14:11:13 -04:00
let mut ctl_zs_openings = ctl_zs_first . iter ( ) . map ( | v | v . iter ( ) ) . collect ::< Vec < _ > > ( ) ;
2023-05-10 15:37:05 -04:00
for CrossTableLookup {
looking_tables ,
looked_table ,
} in cross_table_lookups . into_iter ( )
2022-05-24 16:24:52 +02:00
{
2023-10-30 14:28:24 -04:00
// Get elements looking into `looked_table` that are not associated to any STARK.
2023-12-04 16:26:10 -05:00
let extra_sum_vec = & ctl_extra_looking_sums [ looked_table . table as usize ] ;
2024-01-10 08:54:13 +01:00
// We want to iterate on each looking table only once.
let mut filtered_looking_tables = vec! [ ] ;
for table in looking_tables {
if ! filtered_looking_tables . contains ( & ( table . table as usize ) ) {
filtered_looking_tables . push ( table . table as usize ) ;
}
}
2023-04-14 21:55:44 +08:00
for c in 0 .. inner_config . num_challenges {
2023-10-30 14:28:24 -04:00
// Compute the combination of all looking table CTL polynomial openings.
2023-12-04 16:26:10 -05:00
let mut looking_zs_sum = builder . add_many (
2024-01-10 08:54:13 +01:00
filtered_looking_tables
2022-06-16 02:06:15 +02:00
. iter ( )
2024-01-10 08:54:13 +01:00
. map ( | & table | * ctl_zs_openings [ table ] . next ( ) . unwrap ( ) ) ,
2022-06-16 02:06:15 +02:00
) ;
2023-04-14 21:55:44 +08:00
2023-12-04 16:26:10 -05:00
looking_zs_sum = builder . add ( looking_zs_sum , extra_sum_vec [ c ] ) ;
2023-04-14 21:55:44 +08:00
2023-10-30 14:28:24 -04:00
// Get the looked table CTL polynomial opening.
2022-06-16 02:06:15 +02:00
let looked_z = * ctl_zs_openings [ looked_table . table as usize ] . next ( ) . unwrap ( ) ;
2023-10-30 14:28:24 -04:00
// Verify that the combination of looking table openings is equal to the looked table opening.
2023-12-04 16:26:10 -05:00
builder . connect ( looked_z , looking_zs_sum ) ;
2022-06-07 22:19:36 +02:00
}
2022-05-24 16:24:52 +02:00
}
2022-06-16 02:15:40 +02:00
debug_assert! ( ctl_zs_openings . iter_mut ( ) . all ( | iter | iter . next ( ) . is_none ( ) ) ) ;
2022-05-24 16:24:52 +02:00
}
2022-07-11 11:07:16 +02:00
#[ cfg(test) ]
pub ( crate ) mod testutils {
use std ::collections ::HashMap ;
use plonky2 ::field ::polynomial ::PolynomialValues ;
use plonky2 ::field ::types ::Field ;
use crate ::all_stark ::Table ;
use crate ::cross_table_lookup ::{ CrossTableLookup , TableWithColumns } ;
type MultiSet < F > = HashMap < Vec < F > , Vec < ( Table , usize ) > > ;
2022-07-11 14:13:07 +02:00
/// Check that the provided traces and cross-table lookups are consistent.
pub ( crate ) fn check_ctls < F : Field > (
2022-07-11 11:07:16 +02:00
trace_poly_values : & [ Vec < PolynomialValues < F > > ] ,
2022-07-11 14:13:07 +02:00
cross_table_lookups : & [ CrossTableLookup < F > ] ,
2023-10-16 08:53:42 -04:00
extra_memory_looking_values : & [ Vec < F > ] ,
2022-07-11 11:07:16 +02:00
) {
2022-07-11 14:13:07 +02:00
for ( i , ctl ) in cross_table_lookups . iter ( ) . enumerate ( ) {
2023-10-16 08:53:42 -04:00
check_ctl ( trace_poly_values , ctl , i , extra_memory_looking_values ) ;
2022-07-11 11:07:16 +02:00
}
}
fn check_ctl < F : Field > (
trace_poly_values : & [ Vec < PolynomialValues < F > > ] ,
ctl : & CrossTableLookup < F > ,
ctl_index : usize ,
2023-10-16 08:53:42 -04:00
extra_memory_looking_values : & [ Vec < F > ] ,
2022-07-11 11:07:16 +02:00
) {
let CrossTableLookup {
looking_tables ,
looked_table ,
} = ctl ;
2022-07-11 14:24:12 +02:00
// Maps `m` with `(table, i) in m[row]` iff the `i`-th row of `table` is equal to `row` and
// the filter is 1. Without default values, the CTL check holds iff `looking_multiset == looked_multiset`.
2022-07-11 11:07:16 +02:00
let mut looking_multiset = MultiSet ::< F > ::new ( ) ;
let mut looked_multiset = MultiSet ::< F > ::new ( ) ;
for table in looking_tables {
process_table ( trace_poly_values , table , & mut looking_multiset ) ;
}
process_table ( trace_poly_values , looked_table , & mut looked_multiset ) ;
2023-10-16 08:53:42 -04:00
// Extra looking values for memory
if ctl_index = = Table ::Memory as usize {
for row in extra_memory_looking_values . iter ( ) {
// The table and the row index don't matter here, as we just want to enforce
// that the special extra values do appear when looking against the Memory table.
looking_multiset
. entry ( row . to_vec ( ) )
. or_default ( )
. push ( ( Table ::Cpu , 0 ) ) ;
}
}
2022-07-11 11:07:16 +02:00
let empty = & vec! [ ] ;
2023-01-03 12:43:05 -08:00
// Check that every row in the looking tables appears in the looked table the same number of times.
2022-07-11 11:07:16 +02:00
for ( row , looking_locations ) in & looking_multiset {
2022-07-11 14:56:27 +02:00
let looked_locations = looked_multiset . get ( row ) . unwrap_or ( empty ) ;
2022-07-11 11:07:16 +02:00
check_locations ( looking_locations , looked_locations , ctl_index , row ) ;
}
2022-07-11 14:13:07 +02:00
// Check that every row in the looked tables appears in the looked table the same number of times.
2022-07-11 11:07:16 +02:00
for ( row , looked_locations ) in & looked_multiset {
2022-07-11 14:56:27 +02:00
let looking_locations = looking_multiset . get ( row ) . unwrap_or ( empty ) ;
2022-07-11 11:07:16 +02:00
check_locations ( looking_locations , looked_locations , ctl_index , row ) ;
}
}
2022-07-11 14:13:07 +02:00
fn process_table < F : Field > (
trace_poly_values : & [ Vec < PolynomialValues < F > > ] ,
table : & TableWithColumns < F > ,
multiset : & mut MultiSet < F > ,
) {
let trace = & trace_poly_values [ table . table as usize ] ;
2022-08-23 12:22:54 -07:00
for i in 0 .. trace [ 0 ] . len ( ) {
2023-12-05 17:02:37 -05:00
let filter = if let Some ( combin ) = & table . filter {
combin . eval_table ( trace , i )
2022-07-11 14:13:07 +02:00
} else {
F ::ONE
} ;
if filter . is_one ( ) {
let row = table
. columns
. iter ( )
2022-08-23 12:22:54 -07:00
. map ( | c | c . eval_table ( trace , i ) )
2022-07-11 14:13:07 +02:00
. collect ::< Vec < _ > > ( ) ;
2022-08-23 12:22:54 -07:00
multiset . entry ( row ) . or_default ( ) . push ( ( table . table , i ) ) ;
2022-07-11 14:13:07 +02:00
} else {
assert_eq! ( filter , F ::ZERO , " Non-binary filter? " )
}
}
}
2022-07-11 11:07:16 +02:00
fn check_locations < F : Field > (
looking_locations : & [ ( Table , usize ) ] ,
looked_locations : & [ ( Table , usize ) ] ,
ctl_index : usize ,
row : & [ F ] ,
) {
if looking_locations . len ( ) ! = looked_locations . len ( ) {
panic! (
" CTL #{ctl_index}: \n \
2022-07-11 14:16:58 +02:00
Row { row :? } is present { l0 } times in the looking tables , but { l1 } times in the looked table . \ n \
Looking locations ( Table , Row index ) : { looking_locations :? } . \ n \
Looked locations ( Table , Row index ) : { looked_locations :? } . " ,
2022-07-11 11:07:16 +02:00
l0 = looking_locations . len ( ) ,
l1 = looked_locations . len ( ) ,
) ;
}
}
}