From 4f8e63155071e7b01f50504cff7b47c8f889c5e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alonso=20Gonz=C3=A1lez?= Date: Fri, 15 Mar 2024 12:43:45 +0100 Subject: [PATCH] Prove Starks without constraints (#1552) * Enable starks without constraints * Clippy * Add test stark without constraints * Missing file * Missing changes in the recursive side * Fix bug with recursion * Missing import * Clippy * Apply suggestions from code review Co-authored-by: Robin Salen <30937548+Nashtare@users.noreply.github.com> * Address reviews * Fix TODO * Apply suggestions from code review Co-authored-by: Linda Guiga <101227802+LindaGuiga@users.noreply.github.com> * More reviews * Fix bug in eval_helper_columns * Apply suggestions from code review Co-authored-by: Robin Salen <30937548+Nashtare@users.noreply.github.com> * Address reviews * Allow <= blowup_factor + 1 constraints + reviews * Add unconstrined Stark * Missing file * Remove asserts --------- Co-authored-by: Robin Salen <30937548+Nashtare@users.noreply.github.com> Co-authored-by: Linda Guiga <101227802+LindaGuiga@users.noreply.github.com> --- CHANGELOG.md | 1 + field/src/polynomial/mod.rs | 4 +- plonky2/src/recursion/recursive_verifier.rs | 10 +- starky/src/fibonacci_stark.rs | 193 ++-------------- starky/src/get_challenges.rs | 17 +- starky/src/lib.rs | 4 + starky/src/lookup.rs | 48 ++-- starky/src/permutation_stark.rs | 236 ++++++++++++++++++++ starky/src/proof.rs | 38 +++- starky/src/prover.rs | 95 ++++---- starky/src/recursive_verifier.rs | 41 ++-- starky/src/stark.rs | 37 ++- starky/src/stark_testing.rs | 2 +- starky/src/unconstrained_stark.rs | 201 +++++++++++++++++ starky/src/verifier.rs | 17 +- 15 files changed, 647 insertions(+), 297 deletions(-) create mode 100644 starky/src/permutation_stark.rs create mode 100644 starky/src/unconstrained_stark.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c363318f..dd901fda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased - Fix CTLs with exactly two looking tables ([#1555](https://github.com/0xPolygonZero/plonky2/pull/1555)) +- Make Starks without constraints provable ([#1552](https://github.com/0xPolygonZero/plonky2/pull/1552)) ## [0.2.1] - 2024-03-01 (`starky` crate only) diff --git a/field/src/polynomial/mod.rs b/field/src/polynomial/mod.rs index f61ad419..c13bbca2 100644 --- a/field/src/polynomial/mod.rs +++ b/field/src/polynomial/mod.rs @@ -88,9 +88,7 @@ impl PolynomialValues { } pub fn degree(&self) -> usize { - self.degree_plus_one() - .checked_sub(1) - .expect("deg(0) is undefined") + self.degree_plus_one().saturating_sub(1) } pub fn degree_plus_one(&self) -> usize { diff --git a/plonky2/src/recursion/recursive_verifier.rs b/plonky2/src/recursion/recursive_verifier.rs index 82d1e813..4635d56d 100644 --- a/plonky2/src/recursion/recursive_verifier.rs +++ b/plonky2/src/recursion/recursive_verifier.rs @@ -1,3 +1,6 @@ +#[cfg(not(feature = "std"))] +use alloc::vec; + use crate::field::extension::Extendable; use crate::hash::hash_types::{HashOutTarget, RichField}; use crate::plonk::circuit_builder::CircuitBuilder; @@ -149,13 +152,16 @@ impl, const D: usize> CircuitBuilder { let cap_height = fri_params.config.cap_height; let salt = salt_size(common_data.fri_params.hiding); - let num_leaves_per_oracle = &[ + let num_leaves_per_oracle = &mut vec![ common_data.num_preprocessed_polys(), config.num_wires + salt, common_data.num_zs_partial_products_polys() + common_data.num_all_lookup_polys() + salt, - common_data.num_quotient_polys() + salt, ]; + if common_data.num_quotient_polys() > 0 { + num_leaves_per_oracle.push(common_data.num_quotient_polys() + salt); + } + ProofTarget { wires_cap: self.add_virtual_cap(cap_height), plonk_zs_partial_products_cap: self.add_virtual_cap(cap_height), diff --git a/starky/src/fibonacci_stark.rs b/starky/src/fibonacci_stark.rs index 4bfbf404..7aa40b6e 100644 --- a/starky/src/fibonacci_stark.rs +++ b/starky/src/fibonacci_stark.rs @@ -15,7 +15,6 @@ use plonky2::plonk::circuit_builder::CircuitBuilder; use crate::constraint_consumer::{ConstraintConsumer, RecursiveConstraintConsumer}; use crate::evaluation_frame::{StarkEvaluationFrame, StarkFrame}; -use crate::lookup::{Column, Lookup}; use crate::stark::Stark; use crate::util::trace_rows_to_poly_values; @@ -132,135 +131,6 @@ impl, const D: usize> Stark for FibonacciStar } } -/// Similar system than above, but with extra columns to illustrate the permutation argument. -/// Computes a Fibonacci sequence with state `[x0, x1, i, j]` using the state transition -/// `x0' <- x1, x1' <- x0 + x1, i' <- i+1, j' <- j+1`. -/// Note: The `i, j` columns are the columns used to test the permutation argument. -#[derive(Copy, Clone)] -struct FibonacciWithPermutationStark, const D: usize> { - num_rows: usize, - _phantom: PhantomData, -} - -impl, const D: usize> FibonacciWithPermutationStark { - // The first public input is `x0`. - const PI_INDEX_X0: usize = 0; - // The second public input is `x1`. - const PI_INDEX_X1: usize = 1; - // The third public input is the second element of the last row, which should be equal to the - // `num_rows`-th Fibonacci number. - const PI_INDEX_RES: usize = 2; - - const fn new(num_rows: usize) -> Self { - Self { - num_rows, - _phantom: PhantomData, - } - } - - /// Generate the trace using `x0, x1, 0, 1, 1` as initial state values. - fn generate_trace(&self, x0: F, x1: F) -> Vec> { - let mut trace_rows = (0..self.num_rows) - .scan([x0, x1, F::ZERO, F::ONE, F::ONE], |acc, _| { - let tmp = *acc; - acc[0] = tmp[1]; - acc[1] = tmp[0] + tmp[1]; - acc[2] = tmp[2] + F::ONE; - acc[3] = tmp[3] + F::ONE; - // acc[4] (i.e. frequency column) remains unchanged, as we're permuting a strictly monotonous sequence. - Some(tmp) - }) - .collect::>(); - trace_rows[self.num_rows - 1][3] = F::ZERO; // So that column 2 and 3 are permutation of one another. - trace_rows_to_poly_values(trace_rows) - } -} - -const FIBONACCI_PERM_COLUMNS: usize = 5; -const FIBONACCI_PERM_PUBLIC_INPUTS: usize = 3; - -impl, const D: usize> Stark - for FibonacciWithPermutationStark -{ - type EvaluationFrame = StarkFrame - where - FE: FieldExtension, - P: PackedField; - - type EvaluationFrameTarget = StarkFrame< - ExtensionTarget, - ExtensionTarget, - FIBONACCI_PERM_COLUMNS, - FIBONACCI_PERM_PUBLIC_INPUTS, - >; - - fn eval_packed_generic( - &self, - vars: &Self::EvaluationFrame, - yield_constr: &mut ConstraintConsumer

, - ) where - FE: FieldExtension, - P: PackedField, - { - let local_values = vars.get_local_values(); - let next_values = vars.get_next_values(); - let public_inputs = vars.get_public_inputs(); - - // Check public inputs. - yield_constr.constraint_first_row(local_values[0] - public_inputs[Self::PI_INDEX_X0]); - yield_constr.constraint_first_row(local_values[1] - public_inputs[Self::PI_INDEX_X1]); - yield_constr.constraint_last_row(local_values[1] - public_inputs[Self::PI_INDEX_RES]); - - // x0' <- x1 - yield_constr.constraint_transition(next_values[0] - local_values[1]); - // x1' <- x0 + x1 - yield_constr.constraint_transition(next_values[1] - local_values[0] - local_values[1]); - } - - fn eval_ext_circuit( - &self, - builder: &mut CircuitBuilder, - vars: &Self::EvaluationFrameTarget, - yield_constr: &mut RecursiveConstraintConsumer, - ) { - let local_values = vars.get_local_values(); - let next_values = vars.get_next_values(); - let public_inputs = vars.get_public_inputs(); - // Check public inputs. - let pis_constraints = [ - builder.sub_extension(local_values[0], public_inputs[Self::PI_INDEX_X0]), - builder.sub_extension(local_values[1], public_inputs[Self::PI_INDEX_X1]), - builder.sub_extension(local_values[1], public_inputs[Self::PI_INDEX_RES]), - ]; - yield_constr.constraint_first_row(builder, pis_constraints[0]); - yield_constr.constraint_first_row(builder, pis_constraints[1]); - yield_constr.constraint_last_row(builder, pis_constraints[2]); - - // x0' <- x1 - let first_col_constraint = builder.sub_extension(next_values[0], local_values[1]); - yield_constr.constraint_transition(builder, first_col_constraint); - // x1' <- x0 + x1 - let second_col_constraint = { - let tmp = builder.sub_extension(next_values[1], local_values[0]); - builder.sub_extension(tmp, local_values[1]) - }; - yield_constr.constraint_transition(builder, second_col_constraint); - } - - fn constraint_degree(&self) -> usize { - 2 - } - - fn lookups(&self) -> Vec> { - vec![Lookup { - columns: vec![Column::single(2)], - table_column: Column::single(3), - frequencies_column: Column::single(4), - filter_columns: vec![None; 1], - }] - } -} - #[cfg(test)] mod tests { use anyhow::Result; @@ -274,7 +144,7 @@ mod tests { use plonky2::util::timing::TimingTree; use crate::config::StarkConfig; - use crate::fibonacci_stark::{FibonacciStark, FibonacciWithPermutationStark}; + use crate::fibonacci_stark::FibonacciStark; use crate::proof::StarkProofWithPublicInputs; use crate::prover::prove; use crate::recursive_verifier::{ @@ -294,30 +164,15 @@ mod tests { const D: usize = 2; type C = PoseidonGoldilocksConfig; type F = >::F; - type S1 = FibonacciStark; - type S2 = FibonacciWithPermutationStark; + type S = FibonacciStark; let config = StarkConfig::standard_fast_config(); let num_rows = 1 << 5; let public_inputs = [F::ZERO, F::ONE, fibonacci(num_rows - 1, F::ZERO, F::ONE)]; - // Test first STARK - let stark = S1::new(num_rows); + let stark = S::new(num_rows); let trace = stark.generate_trace(public_inputs[0], public_inputs[1]); - let proof = prove::( - stark, - &config, - trace, - &public_inputs, - &mut TimingTree::default(), - )?; - - verify_stark_proof(stark, proof, &config)?; - - // Test second STARK - let stark = S2::new(num_rows); - let trace = stark.generate_trace(public_inputs[0], public_inputs[1]); - let proof = prove::( + let proof = prove::( stark, &config, trace, @@ -333,14 +188,10 @@ mod tests { const D: usize = 2; type C = PoseidonGoldilocksConfig; type F = >::F; - type S1 = FibonacciStark; - type S2 = FibonacciWithPermutationStark; + type S = FibonacciStark; let num_rows = 1 << 5; - let stark = S1::new(num_rows); - test_stark_low_degree(stark)?; - - let stark = S2::new(num_rows); + let stark = S::new(num_rows); test_stark_low_degree(stark) } @@ -349,14 +200,11 @@ mod tests { const D: usize = 2; type C = PoseidonGoldilocksConfig; type F = >::F; - type S1 = FibonacciStark; - type S2 = FibonacciWithPermutationStark; + type S = FibonacciStark; let num_rows = 1 << 5; - let stark = S1::new(num_rows); - test_stark_circuit_constraints::(stark)?; - let stark = S2::new(num_rows); - test_stark_circuit_constraints::(stark) + let stark = S::new(num_rows); + test_stark_circuit_constraints::(stark) } #[test] @@ -365,17 +213,16 @@ mod tests { const D: usize = 2; type C = PoseidonGoldilocksConfig; type F = >::F; - type S1 = FibonacciStark; - type S2 = FibonacciWithPermutationStark; + type S = FibonacciStark; let config = StarkConfig::standard_fast_config(); let num_rows = 1 << 5; let public_inputs = [F::ZERO, F::ONE, fibonacci(num_rows - 1, F::ZERO, F::ONE)]; // Test first STARK - let stark = S1::new(num_rows); + let stark = S::new(num_rows); let trace = stark.generate_trace(public_inputs[0], public_inputs[1]); - let proof = prove::( + let proof = prove::( stark, &config, trace, @@ -384,21 +231,7 @@ mod tests { )?; verify_stark_proof(stark, proof.clone(), &config)?; - recursive_proof::(stark, proof, &config, true)?; - - // Test second STARK - let stark = S2::new(num_rows); - let trace = stark.generate_trace(public_inputs[0], public_inputs[1]); - let proof = prove::( - stark, - &config, - trace, - &public_inputs, - &mut TimingTree::default(), - )?; - verify_stark_proof(stark, proof.clone(), &config)?; - - recursive_proof::(stark, proof, &config, true) + recursive_proof::(stark, proof, &config, true) } fn recursive_proof< diff --git a/starky/src/get_challenges.rs b/starky/src/get_challenges.rs index be75b0e0..8000a9ef 100644 --- a/starky/src/get_challenges.rs +++ b/starky/src/get_challenges.rs @@ -28,7 +28,7 @@ fn get_challenges( challenges: Option<&GrandProductChallengeSet>, trace_cap: Option<&MerkleCap>, auxiliary_polys_cap: Option<&MerkleCap>, - quotient_polys_cap: &MerkleCap, + quotient_polys_cap: Option<&MerkleCap>, openings: &StarkOpeningSet, commit_phase_merkle_caps: &[MerkleCap], final_poly: &PolynomialCoeffs, @@ -60,7 +60,9 @@ where let stark_alphas = challenger.get_n_challenges(num_challenges); - challenger.observe_cap(quotient_polys_cap); + if let Some(quotient_polys_cap) = quotient_polys_cap { + challenger.observe_cap(quotient_polys_cap); + } let stark_zeta = challenger.get_extension_challenge::(); challenger.observe_openings(&openings.to_fri_openings()); @@ -125,7 +127,7 @@ where challenges, trace_cap, auxiliary_polys_cap.as_ref(), - quotient_polys_cap, + quotient_polys_cap.as_ref(), openings, commit_phase_merkle_caps, final_poly, @@ -168,7 +170,7 @@ fn get_challenges_target( challenges: Option<&GrandProductChallengeSet>, trace_cap: Option<&MerkleCapTarget>, auxiliary_polys_cap: Option<&MerkleCapTarget>, - quotient_polys_cap: &MerkleCapTarget, + quotient_polys_cap: Option<&MerkleCapTarget>, openings: &StarkOpeningSetTarget, commit_phase_merkle_caps: &[MerkleCapTarget], final_poly: &PolynomialCoeffsExtTarget, @@ -200,7 +202,10 @@ where let stark_alphas = challenger.get_n_challenges(builder, num_challenges); - challenger.observe_cap(quotient_polys_cap); + if let Some(cap) = quotient_polys_cap { + challenger.observe_cap(cap); + } + let stark_zeta = challenger.get_extension_challenge(builder); challenger.observe_openings(&openings.to_fri_openings(builder.zero())); @@ -266,7 +271,7 @@ impl StarkProofTarget { challenges, trace_cap, auxiliary_polys_cap.as_ref(), - quotient_polys_cap, + quotient_polys_cap.as_ref(), openings, commit_phase_merkle_caps, final_poly, diff --git a/starky/src/lib.rs b/starky/src/lib.rs index 63777fba..24bea760 100644 --- a/starky/src/lib.rs +++ b/starky/src/lib.rs @@ -340,3 +340,7 @@ pub mod verifier; #[cfg(test)] pub mod fibonacci_stark; +#[cfg(test)] +pub mod permutation_stark; +#[cfg(test)] +pub mod unconstrained_stark; diff --git a/starky/src/lookup.rs b/starky/src/lookup.rs index 80a01b08..16383cd6 100644 --- a/starky/src/lookup.rs +++ b/starky/src/lookup.rs @@ -431,7 +431,10 @@ impl Lookup { pub fn num_helper_columns(&self, constraint_degree: usize) -> usize { // One helper column for each column batch of size `constraint_degree-1`, // then one column for the inverse of `table + challenge` and one for the `Z` polynomial. - ceil_div_usize(self.columns.len(), constraint_degree - 1) + 1 + ceil_div_usize( + self.columns.len(), + constraint_degree.checked_sub(1).unwrap_or(1), + ) + 1 } } @@ -576,11 +579,6 @@ pub(crate) fn lookup_helper_columns( challenge: F, constraint_degree: usize, ) -> Vec> { - assert!( - constraint_degree == 2 || constraint_degree == 3, - "TODO: Allow other constraint degrees." - ); - assert_eq!(lookup.columns.len(), lookup.filter_columns.len()); let num_total_logup_entries = trace_poly_values[0].values.len() * lookup.columns.len(); @@ -666,11 +664,11 @@ pub(crate) fn eval_helper_columns( P: PackedField, { 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 chunk_size = constraint_degree.checked_sub(1).unwrap_or(1); + for (chunk, (fs, &h)) in columns + .chunks(chunk_size) + .zip(filter.chunks(chunk_size).zip(helper_columns)) + { match chunk.len() { 2 => { let combin0 = challenges.combine(&chunk[0]); @@ -719,11 +717,11 @@ pub(crate) fn eval_helper_columns_circuit, const D: consumer: &mut RecursiveConstraintConsumer, ) { 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 chunk_size = constraint_degree.checked_sub(1).unwrap_or(1); + for (chunk, (fs, &h)) in columns + .chunks(chunk_size) + .zip(filter.chunks(chunk_size).zip(helper_columns)) + { let one = builder.one_extension(); match chunk.len() { 2 => { @@ -774,11 +772,17 @@ pub(crate) fn get_helper_cols( challenge: GrandProductChallenge, constraint_degree: usize, ) -> Vec> { - let num_helper_columns = ceil_div_usize(columns_filters.len(), constraint_degree - 1); + let num_helper_columns = ceil_div_usize( + columns_filters.len(), + constraint_degree.checked_sub(1).unwrap_or(1), + ); let mut helper_columns = Vec::with_capacity(num_helper_columns); - for mut cols_filts in &columns_filters.iter().chunks(constraint_degree - 1) { + for mut cols_filts in &columns_filters + .iter() + .chunks(constraint_degree.checked_sub(1).unwrap_or(1)) + { let (first_col, first_filter) = cols_filts.next().unwrap(); let mut filter_col = Vec::with_capacity(degree); @@ -885,10 +889,6 @@ pub(crate) fn eval_packed_lookups_generic, const D: usize> { + num_rows: usize, + _phantom: PhantomData, +} + +impl, const D: usize> PermutationStark { + const fn new(num_rows: usize) -> Self { + Self { + num_rows, + _phantom: PhantomData, + } + } + + /// Generate the trace using `x0, x0+1, 1` as initial state values. + fn generate_trace(&self, x0: F) -> Vec> { + let mut trace_rows = (0..self.num_rows) + .scan([x0, x0 + F::ONE, F::ONE], |acc, _| { + let tmp = *acc; + acc[0] = tmp[0] + F::ONE; + acc[1] = tmp[1] + F::ONE; + // acc[2] (i.e. frequency column) remains unchanged, as we're permuting a strictly monotonous sequence. + Some(tmp) + }) + .collect::>(); + trace_rows[self.num_rows - 1][1] = x0; // So that column 0 and 1 are permutation of one another. + trace_rows_to_poly_values(trace_rows) + } +} + +const PERM_COLUMNS: usize = 3; +const PERM_PUBLIC_INPUTS: usize = 1; + +impl, const D: usize> Stark for PermutationStark { + type EvaluationFrame = StarkFrame + where + FE: FieldExtension, + P: PackedField; + + type EvaluationFrameTarget = + StarkFrame, ExtensionTarget, PERM_COLUMNS, PERM_PUBLIC_INPUTS>; + + fn constraint_degree(&self) -> usize { + 0 + } + + fn lookups(&self) -> Vec> { + vec![Lookup { + columns: vec![Column::single(0)], + table_column: Column::single(1), + frequencies_column: Column::single(2), + filter_columns: vec![None; 1], + }] + } + + // We don't constrain any register, for the sake of highlighting the permutation argument only. + fn eval_packed_generic( + &self, + _vars: &Self::EvaluationFrame, + _yield_constr: &mut ConstraintConsumer

, + ) where + FE: FieldExtension, + P: PackedField, + { + } + + // We don't constrain any register, for the sake of highlighting the permutation argument only. + fn eval_ext_circuit( + &self, + _builder: &mut CircuitBuilder, + _vars: &Self::EvaluationFrameTarget, + _yield_constr: &mut RecursiveConstraintConsumer, + ) { + } +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + use plonky2::field::extension::Extendable; + use plonky2::field::types::Field; + use plonky2::hash::hash_types::RichField; + use plonky2::iop::witness::PartialWitness; + use plonky2::plonk::circuit_builder::CircuitBuilder; + use plonky2::plonk::circuit_data::CircuitConfig; + use plonky2::plonk::config::{AlgebraicHasher, GenericConfig, PoseidonGoldilocksConfig}; + use plonky2::util::timing::TimingTree; + + use crate::config::StarkConfig; + use crate::permutation_stark::PermutationStark; + use crate::proof::StarkProofWithPublicInputs; + use crate::prover::prove; + use crate::recursive_verifier::{ + add_virtual_stark_proof_with_pis, set_stark_proof_with_pis_target, + verify_stark_proof_circuit, + }; + use crate::stark::Stark; + use crate::stark_testing::{test_stark_circuit_constraints, test_stark_low_degree}; + use crate::verifier::verify_stark_proof; + + #[test] + fn test_pemutations_stark() -> Result<()> { + const D: usize = 2; + type C = PoseidonGoldilocksConfig; + type F = >::F; + type S = PermutationStark; + + let config = StarkConfig::standard_fast_config(); + let num_rows = 1 << 5; + + let public_input = F::ZERO; + + let stark = S::new(num_rows); + let trace = stark.generate_trace(public_input); + let proof = prove::( + stark, + &config, + trace, + &[public_input], + &mut TimingTree::default(), + )?; + + verify_stark_proof(stark, proof, &config) + } + + #[test] + fn test_permutation_stark_degree() -> Result<()> { + const D: usize = 2; + type C = PoseidonGoldilocksConfig; + type F = >::F; + type S = PermutationStark; + + let num_rows = 1 << 5; + let stark = S::new(num_rows); + test_stark_low_degree(stark) + } + + #[test] + fn test_permutation_stark_circuit() -> Result<()> { + const D: usize = 2; + type C = PoseidonGoldilocksConfig; + type F = >::F; + type S = PermutationStark; + + let num_rows = 1 << 5; + let stark = S::new(num_rows); + test_stark_circuit_constraints::(stark) + } + + #[test] + fn test_recursive_stark_verifier() -> Result<()> { + init_logger(); + const D: usize = 2; + type C = PoseidonGoldilocksConfig; + type F = >::F; + type S = PermutationStark; + + let config = StarkConfig::standard_fast_config(); + let num_rows = 1 << 5; + let public_input = F::ZERO; + + let stark = S::new(num_rows); + let trace = stark.generate_trace(public_input); + let proof = prove::( + stark, + &config, + trace, + &[public_input], + &mut TimingTree::default(), + )?; + verify_stark_proof(stark, proof.clone(), &config)?; + + recursive_proof::(stark, proof, &config, true) + } + + fn recursive_proof< + F: RichField + Extendable, + C: GenericConfig, + S: Stark + Copy, + InnerC: GenericConfig, + const D: usize, + >( + stark: S, + inner_proof: StarkProofWithPublicInputs, + inner_config: &StarkConfig, + print_gate_counts: bool, + ) -> Result<()> + where + InnerC::Hasher: AlgebraicHasher, + { + let circuit_config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::::new(circuit_config); + let mut pw = PartialWitness::new(); + let degree_bits = inner_proof.proof.recover_degree_bits(inner_config); + let pt = + add_virtual_stark_proof_with_pis(&mut builder, &stark, inner_config, degree_bits, 0, 0); + set_stark_proof_with_pis_target(&mut pw, &pt, &inner_proof, builder.zero()); + + verify_stark_proof_circuit::(&mut builder, stark, pt, inner_config); + + if print_gate_counts { + builder.print_gate_counts(0); + } + + let data = builder.build::(); + let proof = data.prove(pw)?; + data.verify(proof) + } + + fn init_logger() { + let _ = env_logger::builder().format_timestamp(None).try_init(); + } +} diff --git a/starky/src/proof.rs b/starky/src/proof.rs index b6ea53ef..d521be02 100644 --- a/starky/src/proof.rs +++ b/starky/src/proof.rs @@ -33,7 +33,7 @@ pub struct StarkProof, C: GenericConfig, /// Optional merkle cap of LDEs of permutation Z values, if any. pub auxiliary_polys_cap: Option>, /// Merkle cap of LDEs of trace values. - pub quotient_polys_cap: MerkleCap, + pub quotient_polys_cap: Option>, /// Purported values of each polynomial at the challenge point. pub openings: StarkOpeningSet, /// A batch FRI argument for all openings. @@ -61,7 +61,7 @@ pub struct StarkProofTarget { /// Optional `Target` for the Merkle cap of lookup helper and CTL columns LDEs, if any. pub auxiliary_polys_cap: Option, /// `Target` for the Merkle cap of quotient polynomial evaluations LDEs. - pub quotient_polys_cap: MerkleCapTarget, + pub quotient_polys_cap: Option, /// `Target`s for the purported values of each polynomial at the challenge point. pub openings: StarkOpeningSetTarget, /// `Target`s for the batch FRI argument for all openings. @@ -76,7 +76,10 @@ impl StarkProofTarget { if let Some(poly) = &self.auxiliary_polys_cap { buffer.write_target_merkle_cap(poly)?; } - buffer.write_target_merkle_cap(&self.quotient_polys_cap)?; + buffer.write_bool(self.quotient_polys_cap.is_some())?; + if let Some(poly) = &self.quotient_polys_cap { + buffer.write_target_merkle_cap(poly)?; + } buffer.write_target_fri_proof(&self.opening_proof)?; self.openings.to_buffer(buffer)?; Ok(()) @@ -90,7 +93,11 @@ impl StarkProofTarget { } else { None }; - let quotient_polys_cap = buffer.read_target_merkle_cap()?; + let quotient_polys_cap = if buffer.read_bool()? { + Some(buffer.read_target_merkle_cap()?) + } else { + None + }; let opening_proof = buffer.read_target_fri_proof()?; let openings = StarkOpeningSetTarget::from_buffer(buffer)?; @@ -253,7 +260,7 @@ pub struct StarkOpeningSet, const D: usize> { /// Openings of cross-table lookups `Z` polynomials at `1`. pub ctl_zs_first: Option>, /// Openings of quotient polynomials at `zeta`. - pub quotient_polys: Vec, + pub quotient_polys: Option>, } impl, const D: usize> StarkOpeningSet { @@ -266,7 +273,7 @@ impl, const D: usize> StarkOpeningSet { g: F, trace_commitment: &PolynomialBatch, auxiliary_polys_commitment: Option<&PolynomialBatch>, - quotient_commitment: &PolynomialBatch, + quotient_commitment: Option<&PolynomialBatch>, num_lookup_columns: usize, requires_ctl: bool, num_ctl_polys: &[usize], @@ -298,7 +305,7 @@ impl, const D: usize> StarkOpeningSet { let total_num_helper_cols: usize = num_ctl_polys.iter().sum(); auxiliary_first.unwrap()[num_lookup_columns + total_num_helper_cols..].to_vec() }), - quotient_polys: eval_commitment(zeta, quotient_commitment), + quotient_polys: quotient_commitment.map(|c| eval_commitment(zeta, c)), } } @@ -310,7 +317,7 @@ impl, const D: usize> StarkOpeningSet { .local_values .iter() .chain(self.auxiliary_polys.iter().flatten()) - .chain(&self.quotient_polys) + .chain(self.quotient_polys.iter().flatten()) .copied() .collect_vec(), }; @@ -360,7 +367,7 @@ pub struct StarkOpeningSetTarget { /// `ExtensionTarget`s for the opening of lookups and cross-table lookups `Z` polynomials at 1. pub ctl_zs_first: Option>, /// `ExtensionTarget`s for the opening of quotient polynomials at `zeta`. - pub quotient_polys: Vec>, + pub quotient_polys: Option>>, } impl StarkOpeningSetTarget { @@ -386,7 +393,10 @@ impl StarkOpeningSetTarget { } else { buffer.write_bool(false)?; } - buffer.write_target_ext_vec(&self.quotient_polys)?; + buffer.write_bool(self.quotient_polys.is_some())?; + if let Some(quotient_polys) = &self.quotient_polys { + buffer.write_target_ext_vec(quotient_polys)?; + } Ok(()) } @@ -409,7 +419,11 @@ impl StarkOpeningSetTarget { } else { None }; - let quotient_polys = buffer.read_target_ext_vec::()?; + let quotient_polys = if buffer.read_bool()? { + Some(buffer.read_target_ext_vec::()?) + } else { + None + }; Ok(Self { local_values, @@ -428,7 +442,7 @@ impl StarkOpeningSetTarget { .local_values .iter() .chain(self.auxiliary_polys.iter().flatten()) - .chain(&self.quotient_polys) + .chain(self.quotient_polys.iter().flatten()) .copied() .collect_vec(), }; diff --git a/starky/src/prover.rs b/starky/src/prover.rs index 7014bdd3..c7b77b93 100644 --- a/starky/src/prover.rs +++ b/starky/src/prover.rs @@ -119,6 +119,12 @@ where "FRI total reduction arity is too large.", ); + let constraint_degree = stark.constraint_degree(); + assert!( + constraint_degree <= (1 << rate_bits) + 1, + "The degree of the Stark constraints must be <= blowup_factor + 1" + ); + // Permutation arguments. let constraint_degree = stark.constraint_degree(); @@ -238,38 +244,43 @@ where config, ) ); - let all_quotient_chunks = timed!( - timing, - "split quotient polys", - quotient_polys - .into_par_iter() - .flat_map(|mut quotient_poly| { - quotient_poly - .trim_to_len(degree * stark.quotient_degree_factor()) - .expect( - "Quotient has failed, the vanishing polynomial is not divisible by Z_H", - ); - // Split quotient into degree-n chunks. - quotient_poly.chunks(degree) - }) - .collect() - ); - // Commit to the quotient polynomials. - let quotient_commitment = timed!( - timing, - "compute quotient commitment", - PolynomialBatch::from_coeffs( - all_quotient_chunks, - rate_bits, - false, - config.fri_config.cap_height, + let (quotient_commitment, quotient_polys_cap) = if let Some(quotient_polys) = quotient_polys { + let all_quotient_chunks = timed!( timing, - None, - ) - ); - // Observe the quotient polynomials Merkle cap. - let quotient_polys_cap = quotient_commitment.merkle_tree.cap.clone(); - challenger.observe_cap("ient_polys_cap); + "split quotient polys", + quotient_polys + .into_par_iter() + .flat_map(|mut quotient_poly| { + quotient_poly + .trim_to_len(degree * stark.quotient_degree_factor()) + .expect( + "Quotient has failed, the vanishing polynomial is not divisible by Z_H", + ); + // Split quotient into degree-n chunks. + quotient_poly.chunks(degree) + }) + .collect() + ); + // Commit to the quotient polynomials. + let quotient_commitment = timed!( + timing, + "compute quotient commitment", + PolynomialBatch::from_coeffs( + all_quotient_chunks, + rate_bits, + false, + config.fri_config.cap_height, + timing, + None, + ) + ); + // Observe the quotient polynomials Merkle cap. + let quotient_polys_cap = quotient_commitment.merkle_tree.cap.clone(); + challenger.observe_cap("ient_polys_cap); + (Some(quotient_commitment), Some(quotient_polys_cap)) + } else { + (None, None) + }; let zeta = challenger.get_extension_challenge::(); @@ -288,7 +299,7 @@ where g, trace_commitment, auxiliary_polys_commitment.as_ref(), - "ient_commitment, + quotient_commitment.as_ref(), stark.num_lookup_helper_columns(config), stark.requires_ctls(), &num_ctl_polys, @@ -298,7 +309,7 @@ where let initial_merkle_trees = once(trace_commitment) .chain(&auxiliary_polys_commitment) - .chain(once("ient_commitment)) + .chain("ient_commitment) .collect_vec(); let opening_proof = timed!( @@ -342,13 +353,17 @@ fn compute_quotient_polys<'a, F, P, C, S, const D: usize>( num_lookup_columns: usize, num_ctl_columns: &[usize], config: &StarkConfig, -) -> Vec> +) -> Option>> where F: RichField + Extendable, P: PackedField, C: GenericConfig, S: Stark, { + if stark.quotient_degree_factor() == 0 { + return None; + } + let degree = 1 << degree_bits; let rate_bits = config.fri_config.rate_bits; let total_num_helper_cols: usize = num_ctl_columns.iter().sum(); @@ -501,11 +516,13 @@ where }) .collect::>(); - transpose("ient_values) - .into_par_iter() - .map(PolynomialValues::new) - .map(|values| values.coset_ifft(F::coset_shift())) - .collect() + Some( + transpose("ient_values) + .into_par_iter() + .map(PolynomialValues::new) + .map(|values| values.coset_ifft(F::coset_shift())) + .collect(), + ) } /// Check that all constraints evaluate to zero on `H`. diff --git a/starky/src/recursive_verifier.rs b/starky/src/recursive_verifier.rs index 9bc62e6b..83e39398 100644 --- a/starky/src/recursive_verifier.rs +++ b/starky/src/recursive_verifier.rs @@ -162,18 +162,20 @@ pub fn verify_stark_proof_with_challenges_circuit< // Check each polynomial identity, of the form `vanishing(x) = Z_H(x) quotient(x)`, at zeta. let mut scale = ReducingFactorTarget::new(zeta_pow_deg); - for (i, chunk) in quotient_polys - .chunks(stark.quotient_degree_factor()) - .enumerate() - { - let recombined_quotient = scale.reduce(chunk, builder); - let computed_vanishing_poly = builder.mul_extension(z_h_zeta, recombined_quotient); - builder.connect_extension(vanishing_polys_zeta[i], computed_vanishing_poly); + if let Some(quotient_polys) = quotient_polys { + for (i, chunk) in quotient_polys + .chunks(stark.quotient_degree_factor()) + .enumerate() + { + let recombined_quotient = scale.reduce(chunk, builder); + let computed_vanishing_poly = builder.mul_extension(z_h_zeta, recombined_quotient); + builder.connect_extension(vanishing_polys_zeta[i], computed_vanishing_poly); + } } let merkle_caps = once(proof.trace_cap.clone()) .chain(proof.auxiliary_polys_cap.clone()) - .chain(once(proof.quotient_polys_cap.clone())) + .chain(proof.quotient_polys_cap.clone()) .collect_vec(); let fri_instance = stark.fri_instance_target( @@ -258,16 +260,22 @@ pub fn add_virtual_stark_proof, S: Stark, con (stark.uses_lookups() || stark.requires_ctls()) .then(|| stark.num_lookup_helper_columns(config) + num_ctl_helper_zs), ) - .chain(once(stark.quotient_degree_factor() * config.num_challenges)) + .chain( + (stark.quotient_degree_factor() > 0) + .then(|| stark.quotient_degree_factor() * config.num_challenges), + ) .collect_vec(); let auxiliary_polys_cap = (stark.uses_lookups() || stark.requires_ctls()) .then(|| builder.add_virtual_cap(cap_height)); + let quotient_polys_cap = + (stark.constraint_degree() > 0).then(|| builder.add_virtual_cap(cap_height)); + StarkProofTarget { trace_cap: builder.add_virtual_cap(cap_height), auxiliary_polys_cap, - quotient_polys_cap: builder.add_virtual_cap(cap_height), + quotient_polys_cap, openings: add_virtual_stark_opening_set::( builder, stark, @@ -302,8 +310,11 @@ fn add_virtual_stark_opening_set, S: Stark, c ctl_zs_first: stark .requires_ctls() .then(|| builder.add_virtual_targets(num_ctl_zs)), - quotient_polys: builder - .add_virtual_extension_targets(stark.quotient_degree_factor() * config.num_challenges), + quotient_polys: (stark.constraint_degree() > 0).then(|| { + builder.add_virtual_extension_targets( + stark.quotient_degree_factor() * config.num_challenges, + ) + }), } } @@ -349,7 +360,11 @@ pub fn set_stark_proof_target, W, const D: usize>( W: Witness, { witness.set_cap_target(&proof_target.trace_cap, &proof.trace_cap); - witness.set_cap_target(&proof_target.quotient_polys_cap, &proof.quotient_polys_cap); + if let (Some(quotient_polys_cap_target), Some(quotient_polys_cap)) = + (&proof_target.quotient_polys_cap, &proof.quotient_polys_cap) + { + witness.set_cap_target(quotient_polys_cap_target, quotient_polys_cap); + } witness.set_fri_openings( &proof_target.openings.to_fri_openings(zero), diff --git a/starky/src/stark.rs b/starky/src/stark.rs index 0e2b3bd7..c47f9692 100644 --- a/starky/src/stark.rs +++ b/starky/src/stark.rs @@ -84,7 +84,10 @@ pub trait Stark, const D: usize>: Sync { /// Outputs the maximum quotient polynomial's degree factor of this [`Stark`]. fn quotient_degree_factor(&self) -> usize { - 1.max(self.constraint_degree() - 1) + match self.constraint_degree().checked_sub(1) { + Some(v) => 1.max(v), + None => 0, + } } /// Outputs the number of quotient polynomials this [`Stark`] would require with @@ -123,11 +126,17 @@ pub trait Stark, const D: usize>: Sync { }; let num_quotient_polys = self.num_quotient_polys(config); - let quotient_info = FriPolynomialInfo::from_range(oracles.len(), 0..num_quotient_polys); - oracles.push(FriOracleInfo { - num_polys: num_quotient_polys, - blinding: false, - }); + let quotient_info = if num_quotient_polys > 0 { + let quotient_polys = + FriPolynomialInfo::from_range(oracles.len(), 0..num_quotient_polys); + oracles.push(FriOracleInfo { + num_polys: num_quotient_polys, + blinding: false, + }); + quotient_polys + } else { + vec![] + }; let zeta_batch = FriBatchInfo { point: zeta, @@ -192,11 +201,17 @@ pub trait Stark, const D: usize>: Sync { }; let num_quotient_polys = self.num_quotient_polys(config); - let quotient_info = FriPolynomialInfo::from_range(oracles.len(), 0..num_quotient_polys); - oracles.push(FriOracleInfo { - num_polys: num_quotient_polys, - blinding: false, - }); + let quotient_info = if num_quotient_polys > 0 { + let quotient_polys = + FriPolynomialInfo::from_range(oracles.len(), 0..num_quotient_polys); + oracles.push(FriOracleInfo { + num_polys: num_quotient_polys, + blinding: false, + }); + quotient_polys + } else { + vec![] + }; let zeta_batch = FriBatchInfoTarget { point: zeta, diff --git a/starky/src/stark_testing.rs b/starky/src/stark_testing.rs index cc732844..bbe1c840 100644 --- a/starky/src/stark_testing.rs +++ b/starky/src/stark_testing.rs @@ -58,7 +58,7 @@ pub fn test_stark_low_degree, S: Stark, const .collect::>(); let constraint_eval_degree = PolynomialValues::new(constraint_evals).degree(); - let maximum_degree = WITNESS_SIZE * stark.constraint_degree() - 1; + let maximum_degree = (WITNESS_SIZE * stark.constraint_degree()).saturating_sub(1); ensure!( constraint_eval_degree <= maximum_degree, diff --git a/starky/src/unconstrained_stark.rs b/starky/src/unconstrained_stark.rs new file mode 100644 index 00000000..2f93c255 --- /dev/null +++ b/starky/src/unconstrained_stark.rs @@ -0,0 +1,201 @@ +//! An example of proving and verifying an empty STARK (that is, +//! a proof of knowledge of the trace) + +#[cfg(not(feature = "std"))] +use alloc::{vec, vec::Vec}; +use core::marker::PhantomData; + +use plonky2::field::extension::{Extendable, FieldExtension}; +use plonky2::field::packed::PackedField; +use plonky2::field::polynomial::PolynomialValues; +use plonky2::hash::hash_types::RichField; +use plonky2::iop::ext_target::ExtensionTarget; +use plonky2::plonk::circuit_builder::CircuitBuilder; + +use crate::constraint_consumer::{ConstraintConsumer, RecursiveConstraintConsumer}; +use crate::evaluation_frame::StarkFrame; +use crate::stark::Stark; +use crate::util::trace_rows_to_poly_values; + +/// A trace wirh arbitrary values +#[derive(Copy, Clone)] +struct UnconstrainedStark, const D: usize> { + num_rows: usize, + _phantom: PhantomData, +} + +impl, const D: usize> UnconstrainedStark { + const fn new(num_rows: usize) -> Self { + Self { + num_rows, + _phantom: PhantomData, + } + } + + /// Generate the trace using two columns of random values + fn generate_trace(&self) -> Vec> { + let trace_rows = (0..self.num_rows) + .map(|_| [F::rand(), F::rand()]) + .collect::>(); + trace_rows_to_poly_values(trace_rows) + } +} + +const COLUMNS: usize = 2; +const PUBLIC_INPUTS: usize = 0; + +impl, const D: usize> Stark for UnconstrainedStark { + type EvaluationFrame = StarkFrame + where + FE: FieldExtension, + P: PackedField; + + type EvaluationFrameTarget = + StarkFrame, ExtensionTarget, COLUMNS, PUBLIC_INPUTS>; + + fn constraint_degree(&self) -> usize { + 0 + } + + // We don't constrain any register. + fn eval_packed_generic( + &self, + _vars: &Self::EvaluationFrame, + _yield_constr: &mut ConstraintConsumer

, + ) where + FE: FieldExtension, + P: PackedField, + { + } + + // We don't constrain any register. + fn eval_ext_circuit( + &self, + _builder: &mut CircuitBuilder, + _vars: &Self::EvaluationFrameTarget, + _yield_constr: &mut RecursiveConstraintConsumer, + ) { + } +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + use plonky2::field::extension::Extendable; + use plonky2::hash::hash_types::RichField; + use plonky2::iop::witness::PartialWitness; + use plonky2::plonk::circuit_builder::CircuitBuilder; + use plonky2::plonk::circuit_data::CircuitConfig; + use plonky2::plonk::config::{AlgebraicHasher, GenericConfig, PoseidonGoldilocksConfig}; + use plonky2::util::timing::TimingTree; + + use crate::config::StarkConfig; + use crate::proof::StarkProofWithPublicInputs; + use crate::prover::prove; + use crate::recursive_verifier::{ + add_virtual_stark_proof_with_pis, set_stark_proof_with_pis_target, + verify_stark_proof_circuit, + }; + use crate::stark::Stark; + use crate::stark_testing::{test_stark_circuit_constraints, test_stark_low_degree}; + use crate::unconstrained_stark::UnconstrainedStark; + use crate::verifier::verify_stark_proof; + + #[test] + fn test_unconstrained_stark() -> Result<()> { + const D: usize = 2; + type C = PoseidonGoldilocksConfig; + type F = >::F; + type S = UnconstrainedStark; + + let config = StarkConfig::standard_fast_config(); + let num_rows = 1 << 5; + + let stark = S::new(num_rows); + let trace = stark.generate_trace(); + let proof = prove::(stark, &config, trace, &[], &mut TimingTree::default())?; + + verify_stark_proof(stark, proof, &config) + } + + #[test] + fn test_unconstrained_stark_degree() -> Result<()> { + const D: usize = 2; + type C = PoseidonGoldilocksConfig; + type F = >::F; + type S = UnconstrainedStark; + + let num_rows = 1 << 5; + let stark = S::new(num_rows); + test_stark_low_degree(stark) + } + + #[test] + fn test_unconstrained_stark_circuit() -> Result<()> { + const D: usize = 2; + type C = PoseidonGoldilocksConfig; + type F = >::F; + type S = UnconstrainedStark; + + let num_rows = 1 << 5; + let stark = S::new(num_rows); + test_stark_circuit_constraints::(stark) + } + + #[test] + fn test_recursive_stark_verifier() -> Result<()> { + init_logger(); + const D: usize = 2; + type C = PoseidonGoldilocksConfig; + type F = >::F; + type S = UnconstrainedStark; + + let config = StarkConfig::standard_fast_config(); + let num_rows = 1 << 5; + + let stark = S::new(num_rows); + let trace = stark.generate_trace(); + let proof = prove::(stark, &config, trace, &[], &mut TimingTree::default())?; + verify_stark_proof(stark, proof.clone(), &config)?; + + recursive_proof::(stark, proof, &config, true) + } + + fn recursive_proof< + F: RichField + Extendable, + C: GenericConfig, + S: Stark + Copy, + InnerC: GenericConfig, + const D: usize, + >( + stark: S, + inner_proof: StarkProofWithPublicInputs, + inner_config: &StarkConfig, + print_gate_counts: bool, + ) -> Result<()> + where + InnerC::Hasher: AlgebraicHasher, + { + let circuit_config = CircuitConfig::standard_recursion_config(); + let mut builder = CircuitBuilder::::new(circuit_config); + let mut pw = PartialWitness::new(); + let degree_bits = inner_proof.proof.recover_degree_bits(inner_config); + let pt = + add_virtual_stark_proof_with_pis(&mut builder, &stark, inner_config, degree_bits, 0, 0); + set_stark_proof_with_pis_target(&mut pw, &pt, &inner_proof, builder.zero()); + + verify_stark_proof_circuit::(&mut builder, stark, pt, inner_config); + + if print_gate_counts { + builder.print_gate_counts(0); + } + + let data = builder.build::(); + let proof = data.prove(pw)?; + data.verify(proof) + } + + fn init_logger() { + let _ = env_logger::builder().format_timestamp(None).try_init(); + } +} diff --git a/starky/src/verifier.rs b/starky/src/verifier.rs index 7959ae0f..d56072ad 100644 --- a/starky/src/verifier.rs +++ b/starky/src/verifier.rs @@ -164,8 +164,10 @@ where // where the "real" quotient polynomial is `t(X) = t_0(X) + t_1(X)*X^n + t_2(X)*X^{2n} + ...`. // So to reconstruct `t(zeta)` we can compute `reduce_with_powers(chunk, zeta^n)` for each // `quotient_degree_factor`-sized chunk of the original evaluations. + for (i, chunk) in quotient_polys - .chunks(stark.quotient_degree_factor()) + .iter() + .flat_map(|x| x.chunks(stark.quotient_degree_factor())) .enumerate() { ensure!( @@ -176,7 +178,7 @@ where let merkle_caps = once(proof.trace_cap.clone()) .chain(proof.auxiliary_polys_cap.clone()) - .chain(once(proof.quotient_polys_cap.clone())) + .chain(proof.quotient_polys_cap.clone()) .collect_vec(); let num_ctl_zs = ctl_vars @@ -245,11 +247,18 @@ where let cap_height = fri_params.config.cap_height; ensure!(trace_cap.height() == cap_height); - ensure!(quotient_polys_cap.height() == cap_height); + ensure!( + quotient_polys_cap.is_none() + || quotient_polys_cap.as_ref().map(|q| q.height()) == Some(cap_height) + ); ensure!(local_values.len() == S::COLUMNS); ensure!(next_values.len() == S::COLUMNS); - ensure!(quotient_polys.len() == stark.num_quotient_polys(config)); + ensure!(if let Some(quotient_polys) = quotient_polys { + quotient_polys.len() == stark.num_quotient_polys(config) + } else { + stark.num_quotient_polys(config) == 0 + }); check_lookup_options::( stark,