use core::mem::{self, MaybeUninit}; use std::collections::BTreeMap; use std::ops::Range; use eth_trie_utils::partial_trie::{HashedPartialTrie, Node, PartialTrie}; use hashbrown::HashMap; use itertools::{zip_eq, Itertools}; use plonky2::field::extension::Extendable; use plonky2::fri::FriParams; use plonky2::gates::constant::ConstantGate; use plonky2::gates::noop::NoopGate; use plonky2::hash::hash_types::RichField; use plonky2::iop::challenger::RecursiveChallenger; use plonky2::iop::target::{BoolTarget, Target}; use plonky2::iop::witness::{PartialWitness, WitnessWrite}; use plonky2::plonk::circuit_builder::CircuitBuilder; use plonky2::plonk::circuit_data::{ CircuitConfig, CircuitData, CommonCircuitData, VerifierCircuitData, VerifierCircuitTarget, }; use plonky2::plonk::config::{AlgebraicHasher, GenericConfig}; use plonky2::plonk::proof::{ProofWithPublicInputs, ProofWithPublicInputsTarget}; use plonky2::recursion::cyclic_recursion::check_cyclic_proof_verifier_data; use plonky2::recursion::dummy_circuit::cyclic_base_proof; use plonky2::util::serialization::{ Buffer, GateSerializer, IoResult, Read, WitnessGeneratorSerializer, Write, }; use plonky2::util::timing::TimingTree; use plonky2_util::log2_ceil; use crate::all_stark::{all_cross_table_lookups, AllStark, Table, NUM_TABLES}; use crate::config::StarkConfig; use crate::cross_table_lookup::{ get_grand_product_challenge_set_target, verify_cross_table_lookups_circuit, CrossTableLookup, GrandProductChallengeSet, }; use crate::generation::GenerationInputs; use crate::get_challenges::observe_public_values_target; use crate::proof::{ BlockHashesTarget, BlockMetadataTarget, ExtraBlockData, ExtraBlockDataTarget, PublicValues, PublicValuesTarget, StarkProofWithMetadata, TrieRootsTarget, }; use crate::prover::prove; use crate::recursive_verifier::{ add_common_recursion_gates, add_virtual_public_values, get_memory_extra_looking_products_circuit, recursive_stark_circuit, set_public_value_targets, PlonkWrapperCircuit, PublicInputs, StarkWrapperCircuit, }; use crate::stark::Stark; use crate::util::h256_limbs; /// The recursion threshold. We end a chain of recursive proofs once we reach this size. const THRESHOLD_DEGREE_BITS: usize = 13; /// Contains all recursive circuits used in the system. For each STARK and each initial /// `degree_bits`, this contains a chain of recursive circuits for shrinking that STARK from /// `degree_bits` to a constant `THRESHOLD_DEGREE_BITS`. It also contains a special root circuit /// for combining each STARK's shrunk wrapper proof into a single proof. #[derive(Eq, PartialEq, Debug)] pub struct AllRecursiveCircuits where F: RichField + Extendable, C: GenericConfig, C::Hasher: AlgebraicHasher, { /// The EVM root circuit, which aggregates the (shrunk) per-table recursive proofs. pub root: RootCircuitData, pub aggregation: AggregationCircuitData, /// The block circuit, which verifies an aggregation root proof and a previous block proof. pub block: BlockCircuitData, /// Holds chains of circuits for each table and for each initial `degree_bits`. by_table: [RecursiveCircuitsForTable; NUM_TABLES], } /// Data for the EVM root circuit, which is used to combine each STARK's shrunk wrapper proof /// into a single proof. #[derive(Eq, PartialEq, Debug)] pub struct RootCircuitData where F: RichField + Extendable, C: GenericConfig, { pub circuit: CircuitData, proof_with_pis: [ProofWithPublicInputsTarget; NUM_TABLES], /// For each table, various inner circuits may be used depending on the initial table size. /// This target holds the index of the circuit (within `final_circuits()`) that was used. index_verifier_data: [Target; NUM_TABLES], /// Public inputs containing public values. public_values: PublicValuesTarget, /// Public inputs used for cyclic verification. These aren't actually used for EVM root /// proofs; the circuit has them just to match the structure of aggregation proofs. cyclic_vk: VerifierCircuitTarget, } impl RootCircuitData where F: RichField + Extendable, C: GenericConfig, { fn to_buffer( &self, buffer: &mut Vec, gate_serializer: &dyn GateSerializer, generator_serializer: &dyn WitnessGeneratorSerializer, ) -> IoResult<()> { buffer.write_circuit_data(&self.circuit, gate_serializer, generator_serializer)?; for proof in &self.proof_with_pis { buffer.write_target_proof_with_public_inputs(proof)?; } for index in self.index_verifier_data { buffer.write_target(index)?; } self.public_values.to_buffer(buffer)?; buffer.write_target_verifier_circuit(&self.cyclic_vk)?; Ok(()) } fn from_buffer( buffer: &mut Buffer, gate_serializer: &dyn GateSerializer, generator_serializer: &dyn WitnessGeneratorSerializer, ) -> IoResult { let circuit = buffer.read_circuit_data(gate_serializer, generator_serializer)?; let mut proof_with_pis = Vec::with_capacity(NUM_TABLES); for _ in 0..NUM_TABLES { proof_with_pis.push(buffer.read_target_proof_with_public_inputs()?); } let mut index_verifier_data = Vec::with_capacity(NUM_TABLES); for _ in 0..NUM_TABLES { index_verifier_data.push(buffer.read_target()?); } let public_values = PublicValuesTarget::from_buffer(buffer)?; let cyclic_vk = buffer.read_target_verifier_circuit()?; Ok(Self { circuit, proof_with_pis: proof_with_pis.try_into().unwrap(), index_verifier_data: index_verifier_data.try_into().unwrap(), public_values, cyclic_vk, }) } } /// Data for the aggregation circuit, which is used to compress two proofs into one. Each inner /// proof can be either an EVM root proof or another aggregation proof. #[derive(Eq, PartialEq, Debug)] pub struct AggregationCircuitData where F: RichField + Extendable, C: GenericConfig, { pub circuit: CircuitData, lhs: AggregationChildTarget, rhs: AggregationChildTarget, public_values: PublicValuesTarget, cyclic_vk: VerifierCircuitTarget, } impl AggregationCircuitData where F: RichField + Extendable, C: GenericConfig, { fn to_buffer( &self, buffer: &mut Vec, gate_serializer: &dyn GateSerializer, generator_serializer: &dyn WitnessGeneratorSerializer, ) -> IoResult<()> { buffer.write_circuit_data(&self.circuit, gate_serializer, generator_serializer)?; buffer.write_target_verifier_circuit(&self.cyclic_vk)?; self.public_values.to_buffer(buffer)?; self.lhs.to_buffer(buffer)?; self.rhs.to_buffer(buffer)?; Ok(()) } fn from_buffer( buffer: &mut Buffer, gate_serializer: &dyn GateSerializer, generator_serializer: &dyn WitnessGeneratorSerializer, ) -> IoResult { let circuit = buffer.read_circuit_data(gate_serializer, generator_serializer)?; let cyclic_vk = buffer.read_target_verifier_circuit()?; let public_values = PublicValuesTarget::from_buffer(buffer)?; let lhs = AggregationChildTarget::from_buffer(buffer)?; let rhs = AggregationChildTarget::from_buffer(buffer)?; Ok(Self { circuit, lhs, rhs, public_values, cyclic_vk, }) } } #[derive(Eq, PartialEq, Debug)] struct AggregationChildTarget { is_agg: BoolTarget, agg_proof: ProofWithPublicInputsTarget, evm_proof: ProofWithPublicInputsTarget, } impl AggregationChildTarget { fn to_buffer(&self, buffer: &mut Vec) -> IoResult<()> { buffer.write_target_bool(self.is_agg)?; buffer.write_target_proof_with_public_inputs(&self.agg_proof)?; buffer.write_target_proof_with_public_inputs(&self.evm_proof)?; Ok(()) } fn from_buffer(buffer: &mut Buffer) -> IoResult { let is_agg = buffer.read_target_bool()?; let agg_proof = buffer.read_target_proof_with_public_inputs()?; let evm_proof = buffer.read_target_proof_with_public_inputs()?; Ok(Self { is_agg, agg_proof, evm_proof, }) } fn public_values>( &self, builder: &mut CircuitBuilder, ) -> PublicValuesTarget { let agg_pv = PublicValuesTarget::from_public_inputs(&self.agg_proof.public_inputs); let evm_pv = PublicValuesTarget::from_public_inputs(&self.evm_proof.public_inputs); PublicValuesTarget::select(builder, self.is_agg, agg_pv, evm_pv) } } #[derive(Eq, PartialEq, Debug)] pub struct BlockCircuitData where F: RichField + Extendable, C: GenericConfig, { pub circuit: CircuitData, has_parent_block: BoolTarget, parent_block_proof: ProofWithPublicInputsTarget, agg_root_proof: ProofWithPublicInputsTarget, public_values: PublicValuesTarget, cyclic_vk: VerifierCircuitTarget, } impl BlockCircuitData where F: RichField + Extendable, C: GenericConfig, { fn to_buffer( &self, buffer: &mut Vec, gate_serializer: &dyn GateSerializer, generator_serializer: &dyn WitnessGeneratorSerializer, ) -> IoResult<()> { buffer.write_circuit_data(&self.circuit, gate_serializer, generator_serializer)?; buffer.write_target_bool(self.has_parent_block)?; buffer.write_target_proof_with_public_inputs(&self.parent_block_proof)?; buffer.write_target_proof_with_public_inputs(&self.agg_root_proof)?; self.public_values.to_buffer(buffer)?; buffer.write_target_verifier_circuit(&self.cyclic_vk)?; Ok(()) } fn from_buffer( buffer: &mut Buffer, gate_serializer: &dyn GateSerializer, generator_serializer: &dyn WitnessGeneratorSerializer, ) -> IoResult { let circuit = buffer.read_circuit_data(gate_serializer, generator_serializer)?; let has_parent_block = buffer.read_target_bool()?; let parent_block_proof = buffer.read_target_proof_with_public_inputs()?; let agg_root_proof = buffer.read_target_proof_with_public_inputs()?; let public_values = PublicValuesTarget::from_buffer(buffer)?; let cyclic_vk = buffer.read_target_verifier_circuit()?; Ok(Self { circuit, has_parent_block, parent_block_proof, agg_root_proof, public_values, cyclic_vk, }) } } impl AllRecursiveCircuits where F: RichField + Extendable, C: GenericConfig + 'static, C::Hasher: AlgebraicHasher, { pub fn to_bytes( &self, gate_serializer: &dyn GateSerializer, generator_serializer: &dyn WitnessGeneratorSerializer, ) -> IoResult> { // TODO: would be better to initialize it dynamically based on the supported max degree. let mut buffer = Vec::with_capacity(1 << 34); self.root .to_buffer(&mut buffer, gate_serializer, generator_serializer)?; self.aggregation .to_buffer(&mut buffer, gate_serializer, generator_serializer)?; self.block .to_buffer(&mut buffer, gate_serializer, generator_serializer)?; for table in &self.by_table { table.to_buffer(&mut buffer, gate_serializer, generator_serializer)?; } Ok(buffer) } pub fn from_bytes( bytes: &[u8], gate_serializer: &dyn GateSerializer, generator_serializer: &dyn WitnessGeneratorSerializer, ) -> IoResult { let mut buffer = Buffer::new(bytes); let root = RootCircuitData::from_buffer(&mut buffer, gate_serializer, generator_serializer)?; let aggregation = AggregationCircuitData::from_buffer( &mut buffer, gate_serializer, generator_serializer, )?; let block = BlockCircuitData::from_buffer(&mut buffer, gate_serializer, generator_serializer)?; // Tricky use of MaybeUninit to remove the need for implementing Debug // for all underlying types, necessary to convert a by_table Vec to an array. let by_table = { let mut by_table: [MaybeUninit>; NUM_TABLES] = unsafe { MaybeUninit::uninit().assume_init() }; for table in &mut by_table[..] { let value = RecursiveCircuitsForTable::from_buffer( &mut buffer, gate_serializer, generator_serializer, )?; *table = MaybeUninit::new(value); } unsafe { mem::transmute::<_, [RecursiveCircuitsForTable; NUM_TABLES]>(by_table) } }; Ok(Self { root, aggregation, block, by_table, }) } /// Preprocess all recursive circuits used by the system. pub fn new( all_stark: &AllStark, degree_bits_ranges: &[Range; NUM_TABLES], stark_config: &StarkConfig, ) -> Self { let arithmetic = RecursiveCircuitsForTable::new( Table::Arithmetic, &all_stark.arithmetic_stark, degree_bits_ranges[Table::Arithmetic as usize].clone(), &all_stark.cross_table_lookups, stark_config, ); let byte_packing = RecursiveCircuitsForTable::new( Table::BytePacking, &all_stark.byte_packing_stark, degree_bits_ranges[Table::BytePacking as usize].clone(), &all_stark.cross_table_lookups, stark_config, ); let cpu = RecursiveCircuitsForTable::new( Table::Cpu, &all_stark.cpu_stark, degree_bits_ranges[Table::Cpu as usize].clone(), &all_stark.cross_table_lookups, stark_config, ); let keccak = RecursiveCircuitsForTable::new( Table::Keccak, &all_stark.keccak_stark, degree_bits_ranges[Table::Keccak as usize].clone(), &all_stark.cross_table_lookups, stark_config, ); let keccak_sponge = RecursiveCircuitsForTable::new( Table::KeccakSponge, &all_stark.keccak_sponge_stark, degree_bits_ranges[Table::KeccakSponge as usize].clone(), &all_stark.cross_table_lookups, stark_config, ); let logic = RecursiveCircuitsForTable::new( Table::Logic, &all_stark.logic_stark, degree_bits_ranges[Table::Logic as usize].clone(), &all_stark.cross_table_lookups, stark_config, ); let memory = RecursiveCircuitsForTable::new( Table::Memory, &all_stark.memory_stark, degree_bits_ranges[Table::Memory as usize].clone(), &all_stark.cross_table_lookups, stark_config, ); let by_table = [ arithmetic, byte_packing, cpu, keccak, keccak_sponge, logic, memory, ]; let root = Self::create_root_circuit(&by_table, stark_config); let aggregation = Self::create_aggregation_circuit(&root); let block = Self::create_block_circuit(&aggregation); Self { root, aggregation, block, by_table, } } /// Expand the preprocessed STARK table circuits with the provided ranges. /// /// If a range for a given table is contained within the current one, this will be a no-op. /// Otherwise, it will add the circuits for the missing table sizes, and regenerate the upper circuits. pub fn expand( &mut self, all_stark: &AllStark, degree_bits_ranges: &[Range; NUM_TABLES], stark_config: &StarkConfig, ) { self.by_table[Table::Arithmetic as usize].expand( Table::Arithmetic, &all_stark.arithmetic_stark, degree_bits_ranges[Table::Arithmetic as usize].clone(), &all_stark.cross_table_lookups, stark_config, ); self.by_table[Table::BytePacking as usize].expand( Table::BytePacking, &all_stark.byte_packing_stark, degree_bits_ranges[Table::BytePacking as usize].clone(), &all_stark.cross_table_lookups, stark_config, ); self.by_table[Table::Cpu as usize].expand( Table::Cpu, &all_stark.cpu_stark, degree_bits_ranges[Table::Cpu as usize].clone(), &all_stark.cross_table_lookups, stark_config, ); self.by_table[Table::Keccak as usize].expand( Table::Keccak, &all_stark.keccak_stark, degree_bits_ranges[Table::Keccak as usize].clone(), &all_stark.cross_table_lookups, stark_config, ); self.by_table[Table::KeccakSponge as usize].expand( Table::KeccakSponge, &all_stark.keccak_sponge_stark, degree_bits_ranges[Table::KeccakSponge as usize].clone(), &all_stark.cross_table_lookups, stark_config, ); self.by_table[Table::Logic as usize].expand( Table::Logic, &all_stark.logic_stark, degree_bits_ranges[Table::Logic as usize].clone(), &all_stark.cross_table_lookups, stark_config, ); self.by_table[Table::Memory as usize].expand( Table::Memory, &all_stark.memory_stark, degree_bits_ranges[Table::Memory as usize].clone(), &all_stark.cross_table_lookups, stark_config, ); // Regenerate the upper circuits. self.root = Self::create_root_circuit(&self.by_table, stark_config); self.aggregation = Self::create_aggregation_circuit(&self.root); self.block = Self::create_block_circuit(&self.aggregation); } /// Outputs the `VerifierCircuitData` needed to verify any block proof /// generated by an honest prover. pub fn final_verifier_data(&self) -> VerifierCircuitData { self.block.circuit.verifier_data() } fn create_root_circuit( by_table: &[RecursiveCircuitsForTable; NUM_TABLES], stark_config: &StarkConfig, ) -> RootCircuitData { let inner_common_data: [_; NUM_TABLES] = core::array::from_fn(|i| &by_table[i].final_circuits()[0].common); let mut builder = CircuitBuilder::new(CircuitConfig::standard_recursion_config()); let public_values = add_virtual_public_values(&mut builder); let recursive_proofs = core::array::from_fn(|i| builder.add_virtual_proof_with_pis(inner_common_data[i])); let pis: [_; NUM_TABLES] = core::array::from_fn(|i| { PublicInputs::>::AlgebraicPermutation>::from_vec( &recursive_proofs[i].public_inputs, stark_config, ) }); let index_verifier_data = core::array::from_fn(|_i| builder.add_virtual_target()); let mut challenger = RecursiveChallenger::::new(&mut builder); for pi in &pis { for h in &pi.trace_cap { challenger.observe_elements(h); } } observe_public_values_target::(&mut challenger, &public_values); let ctl_challenges = get_grand_product_challenge_set_target( &mut builder, &mut challenger, stark_config.num_challenges, ); // Check that the correct CTL challenges are used in every proof. for pi in &pis { for i in 0..stark_config.num_challenges { builder.connect( ctl_challenges.challenges[i].beta, pi.ctl_challenges.challenges[i].beta, ); builder.connect( ctl_challenges.challenges[i].gamma, pi.ctl_challenges.challenges[i].gamma, ); } } let state = challenger.compact(&mut builder); for (&before, &s) in zip_eq(state.as_ref(), pis[0].challenger_state_before.as_ref()) { builder.connect(before, s); } // Check that the challenger state is consistent between proofs. for i in 1..NUM_TABLES { for (&before, &after) in zip_eq( pis[i].challenger_state_before.as_ref(), pis[i - 1].challenger_state_after.as_ref(), ) { builder.connect(before, after); } } // Extra products to add to the looked last value. // Only necessary for the Memory values. let mut extra_looking_products = vec![vec![builder.one(); stark_config.num_challenges]; NUM_TABLES]; // Memory extra_looking_products[Table::Memory as usize] = (0..stark_config.num_challenges) .map(|c| { get_memory_extra_looking_products_circuit( &mut builder, &public_values, ctl_challenges.challenges[c], ) }) .collect_vec(); // Verify the CTL checks. verify_cross_table_lookups_circuit::( &mut builder, all_cross_table_lookups(), pis.map(|p| p.ctl_zs_first), extra_looking_products, stark_config, ); for (i, table_circuits) in by_table.iter().enumerate() { let final_circuits = table_circuits.final_circuits(); for final_circuit in &final_circuits { assert_eq!( &final_circuit.common, inner_common_data[i], "common_data mismatch" ); } let mut possible_vks = final_circuits .into_iter() .map(|c| builder.constant_verifier_data(&c.verifier_only)) .collect_vec(); // random_access_verifier_data expects a vector whose length is a power of two. // To satisfy this, we will just add some duplicates of the first VK. while !possible_vks.len().is_power_of_two() { possible_vks.push(possible_vks[0].clone()); } let inner_verifier_data = builder.random_access_verifier_data(index_verifier_data[i], possible_vks); builder.verify_proof::( &recursive_proofs[i], &inner_verifier_data, inner_common_data[i], ); } // We want EVM root proofs to have the exact same structure as aggregation proofs, so we add // public inputs for cyclic verification, even though they'll be ignored. let cyclic_vk = builder.add_verifier_data_public_inputs(); builder.add_gate( ConstantGate::new(inner_common_data[0].config.num_constants), vec![], ); RootCircuitData { circuit: builder.build::(), proof_with_pis: recursive_proofs, index_verifier_data, public_values, cyclic_vk, } } fn create_aggregation_circuit( root: &RootCircuitData, ) -> AggregationCircuitData { let mut builder = CircuitBuilder::::new(root.circuit.common.config.clone()); let public_values = add_virtual_public_values(&mut builder); let cyclic_vk = builder.add_verifier_data_public_inputs(); let lhs = Self::add_agg_child(&mut builder, root); let rhs = Self::add_agg_child(&mut builder, root); let lhs_public_values = lhs.public_values(&mut builder); let rhs_public_values = rhs.public_values(&mut builder); // Connect all block hash values BlockHashesTarget::connect( &mut builder, public_values.block_hashes, lhs_public_values.block_hashes, ); BlockHashesTarget::connect( &mut builder, public_values.block_hashes, rhs_public_values.block_hashes, ); // Connect all block metadata values. BlockMetadataTarget::connect( &mut builder, public_values.block_metadata, lhs_public_values.block_metadata, ); BlockMetadataTarget::connect( &mut builder, public_values.block_metadata, rhs_public_values.block_metadata, ); // Connect aggregation `trie_roots_before` with lhs `trie_roots_before`. TrieRootsTarget::connect( &mut builder, public_values.trie_roots_before, lhs_public_values.trie_roots_before, ); // Connect aggregation `trie_roots_after` with rhs `trie_roots_after`. TrieRootsTarget::connect( &mut builder, public_values.trie_roots_after, rhs_public_values.trie_roots_after, ); // Connect lhs `trie_roots_after` with rhs `trie_roots_before`. TrieRootsTarget::connect( &mut builder, lhs_public_values.trie_roots_after, rhs_public_values.trie_roots_before, ); Self::connect_extra_public_values( &mut builder, &public_values.extra_block_data, &lhs_public_values.extra_block_data, &rhs_public_values.extra_block_data, ); // Pad to match the root circuit's degree. while log2_ceil(builder.num_gates()) < root.circuit.common.degree_bits() { builder.add_gate(NoopGate, vec![]); } let circuit = builder.build::(); AggregationCircuitData { circuit, lhs, rhs, public_values, cyclic_vk, } } fn connect_extra_public_values( builder: &mut CircuitBuilder, pvs: &ExtraBlockDataTarget, lhs: &ExtraBlockDataTarget, rhs: &ExtraBlockDataTarget, ) { // Connect genesis state root values. for (&limb0, &limb1) in pvs .genesis_state_trie_root .iter() .zip(&rhs.genesis_state_trie_root) { builder.connect(limb0, limb1); } for (&limb0, &limb1) in pvs .genesis_state_trie_root .iter() .zip(&lhs.genesis_state_trie_root) { builder.connect(limb0, limb1); } // Connect the transaction number in public values to the lhs and rhs values correctly. builder.connect(pvs.txn_number_before, lhs.txn_number_before); builder.connect(pvs.txn_number_after, rhs.txn_number_after); // Connect lhs `txn_number_after` with rhs `txn_number_before`. builder.connect(lhs.txn_number_after, rhs.txn_number_before); // Connect the gas used in public values to the lhs and rhs values correctly. builder.connect(pvs.gas_used_before, lhs.gas_used_before); builder.connect(pvs.gas_used_after, rhs.gas_used_after); // Connect lhs `gas_used_after` with rhs `gas_used_before`. builder.connect(lhs.gas_used_after, rhs.gas_used_before); // Connect the `block_bloom` in public values to the lhs and rhs values correctly. for (&limb0, &limb1) in pvs.block_bloom_after.iter().zip(&rhs.block_bloom_after) { builder.connect(limb0, limb1); } for (&limb0, &limb1) in pvs.block_bloom_before.iter().zip(&lhs.block_bloom_before) { builder.connect(limb0, limb1); } // Connect lhs `block_bloom_after` with rhs `block_bloom_before`. for (&limb0, &limb1) in lhs.block_bloom_after.iter().zip(&rhs.block_bloom_before) { builder.connect(limb0, limb1); } } fn add_agg_child( builder: &mut CircuitBuilder, root: &RootCircuitData, ) -> AggregationChildTarget { let common = &root.circuit.common; let root_vk = builder.constant_verifier_data(&root.circuit.verifier_only); let is_agg = builder.add_virtual_bool_target_safe(); let agg_proof = builder.add_virtual_proof_with_pis(common); let evm_proof = builder.add_virtual_proof_with_pis(common); builder .conditionally_verify_cyclic_proof::( is_agg, &agg_proof, &evm_proof, &root_vk, common, ) .expect("Failed to build cyclic recursion circuit"); AggregationChildTarget { is_agg, agg_proof, evm_proof, } } fn create_block_circuit(agg: &AggregationCircuitData) -> BlockCircuitData { // The block circuit is similar to the agg circuit; both verify two inner proofs. // We need to adjust a few things, but it's easier than making a new CommonCircuitData. let expected_common_data = CommonCircuitData { fri_params: FriParams { degree_bits: 14, ..agg.circuit.common.fri_params.clone() }, ..agg.circuit.common.clone() }; let mut builder = CircuitBuilder::::new(CircuitConfig::standard_recursion_config()); let public_values = add_virtual_public_values(&mut builder); let has_parent_block = builder.add_virtual_bool_target_safe(); let parent_block_proof = builder.add_virtual_proof_with_pis(&expected_common_data); let agg_root_proof = builder.add_virtual_proof_with_pis(&agg.circuit.common); // Connect block hashes Self::connect_block_hashes(&mut builder, &parent_block_proof, &agg_root_proof); let parent_pv = PublicValuesTarget::from_public_inputs(&parent_block_proof.public_inputs); let agg_pv = PublicValuesTarget::from_public_inputs(&agg_root_proof.public_inputs); // Make connections between block proofs, and check initial and final block values. Self::connect_block_proof(&mut builder, has_parent_block, &parent_pv, &agg_pv); let cyclic_vk = builder.add_verifier_data_public_inputs(); builder .conditionally_verify_cyclic_proof_or_dummy::( has_parent_block, &parent_block_proof, &expected_common_data, ) .expect("Failed to build cyclic recursion circuit"); let agg_verifier_data = builder.constant_verifier_data(&agg.circuit.verifier_only); builder.verify_proof::(&agg_root_proof, &agg_verifier_data, &agg.circuit.common); let circuit = builder.build::(); BlockCircuitData { circuit, has_parent_block, parent_block_proof, agg_root_proof, public_values, cyclic_vk, } } /// Connect the 256 block hashes between two blocks fn connect_block_hashes( builder: &mut CircuitBuilder, lhs: &ProofWithPublicInputsTarget, rhs: &ProofWithPublicInputsTarget, ) { let lhs_public_values = PublicValuesTarget::from_public_inputs(&lhs.public_inputs); let rhs_public_values = PublicValuesTarget::from_public_inputs(&rhs.public_inputs); for i in 0..255 { for j in 0..8 { builder.connect( lhs_public_values.block_hashes.prev_hashes[8 * (i + 1) + j], rhs_public_values.block_hashes.prev_hashes[8 * i + j], ); } } let expected_hash = lhs_public_values.block_hashes.cur_hash; let prev_block_hash = &rhs_public_values.block_hashes.prev_hashes[255 * 8..256 * 8]; for i in 0..expected_hash.len() { builder.connect(expected_hash[i], prev_block_hash[i]); } } fn connect_block_proof( builder: &mut CircuitBuilder, has_parent_block: BoolTarget, lhs: &PublicValuesTarget, rhs: &PublicValuesTarget, ) { // Between blocks, we only connect state tries. for (&limb0, limb1) in lhs .trie_roots_after .state_root .iter() .zip(rhs.trie_roots_before.state_root) { builder.connect(limb0, limb1); } // Between blocks, the genesis state trie remains unchanged. for (&limb0, limb1) in lhs .extra_block_data .genesis_state_trie_root .iter() .zip(rhs.extra_block_data.genesis_state_trie_root) { builder.connect(limb0, limb1); } // Connect block numbers. let one = builder.one(); let prev_block_nb = builder.sub(rhs.block_metadata.block_number, one); builder.connect(lhs.block_metadata.block_number, prev_block_nb); // Check initial block values. Self::connect_initial_values_block(builder, rhs); // Connect intermediary values for gas_used and bloom filters to the block's final values. We only plug on the right, so there is no need to check the left-handside block. Self::connect_final_block_values_to_intermediary(builder, rhs); let has_not_parent_block = builder.sub(one, has_parent_block.target); // Check that the genesis block number is 0. let gen_block_constr = builder.mul(has_not_parent_block, lhs.block_metadata.block_number); builder.assert_zero(gen_block_constr); // Check that the genesis block has the predetermined state trie root in `ExtraBlockData`. Self::connect_genesis_block(builder, rhs, has_not_parent_block); } fn connect_genesis_block( builder: &mut CircuitBuilder, x: &PublicValuesTarget, has_not_parent_block: Target, ) where F: RichField + Extendable, { for (&limb0, limb1) in x .trie_roots_before .state_root .iter() .zip(x.extra_block_data.genesis_state_trie_root) { let mut constr = builder.sub(limb0, limb1); constr = builder.mul(has_not_parent_block, constr); builder.assert_zero(constr); } } fn connect_final_block_values_to_intermediary( builder: &mut CircuitBuilder, x: &PublicValuesTarget, ) where F: RichField + Extendable, { builder.connect( x.block_metadata.block_gas_used, x.extra_block_data.gas_used_after, ); for (&limb0, &limb1) in x .block_metadata .block_bloom .iter() .zip(&x.extra_block_data.block_bloom_after) { builder.connect(limb0, limb1); } } fn connect_initial_values_block(builder: &mut CircuitBuilder, x: &PublicValuesTarget) where F: RichField + Extendable, { // The initial number of transactions is 0. builder.assert_zero(x.extra_block_data.txn_number_before); // The initial gas used is 0. builder.assert_zero(x.extra_block_data.gas_used_before); // The initial bloom filter is all zeroes. for t in x.extra_block_data.block_bloom_before { builder.assert_zero(t); } // The transactions and receipts tries are empty at the beginning of the block. let initial_trie = HashedPartialTrie::from(Node::Empty).hash(); for (i, limb) in h256_limbs::(initial_trie).into_iter().enumerate() { let limb_target = builder.constant(limb); builder.connect(x.trie_roots_before.transactions_root[i], limb_target); builder.connect(x.trie_roots_before.receipts_root[i], limb_target); } } /// Create a proof for each STARK, then combine them, eventually culminating in a root proof. pub fn prove_root( &self, all_stark: &AllStark, config: &StarkConfig, generation_inputs: GenerationInputs, timing: &mut TimingTree, ) -> anyhow::Result<(ProofWithPublicInputs, PublicValues)> { let all_proof = prove::(all_stark, config, generation_inputs, timing)?; let mut root_inputs = PartialWitness::new(); for table in 0..NUM_TABLES { let stark_proof = &all_proof.stark_proofs[table]; let original_degree_bits = stark_proof.proof.recover_degree_bits(config); let table_circuits = &self.by_table[table]; let shrunk_proof = table_circuits .by_stark_size .get(&original_degree_bits) .ok_or_else(|| { anyhow::Error::msg(format!( "Missing preprocessed circuits for {:?} table with size {}.", Table::all()[table], original_degree_bits, )) })? .shrink(stark_proof, &all_proof.ctl_challenges)?; let index_verifier_data = table_circuits .by_stark_size .keys() .position(|&size| size == original_degree_bits) .unwrap(); root_inputs.set_target( self.root.index_verifier_data[table], F::from_canonical_usize(index_verifier_data), ); root_inputs.set_proof_with_pis_target(&self.root.proof_with_pis[table], &shrunk_proof); } root_inputs.set_verifier_data_target( &self.root.cyclic_vk, &self.aggregation.circuit.verifier_only, ); set_public_value_targets( &mut root_inputs, &self.root.public_values, &all_proof.public_values, ) .map_err(|_| { anyhow::Error::msg("Invalid conversion when setting public values targets.") })?; let root_proof = self.root.circuit.prove(root_inputs)?; Ok((root_proof, all_proof.public_values)) } pub fn verify_root(&self, agg_proof: ProofWithPublicInputs) -> anyhow::Result<()> { self.root.circuit.verify(agg_proof) } pub fn prove_aggregation( &self, lhs_is_agg: bool, lhs_proof: &ProofWithPublicInputs, lhs_public_values: PublicValues, rhs_is_agg: bool, rhs_proof: &ProofWithPublicInputs, rhs_public_values: PublicValues, ) -> anyhow::Result<(ProofWithPublicInputs, PublicValues)> { let mut agg_inputs = PartialWitness::new(); agg_inputs.set_bool_target(self.aggregation.lhs.is_agg, lhs_is_agg); agg_inputs.set_proof_with_pis_target(&self.aggregation.lhs.agg_proof, lhs_proof); agg_inputs.set_proof_with_pis_target(&self.aggregation.lhs.evm_proof, lhs_proof); agg_inputs.set_bool_target(self.aggregation.rhs.is_agg, rhs_is_agg); agg_inputs.set_proof_with_pis_target(&self.aggregation.rhs.agg_proof, rhs_proof); agg_inputs.set_proof_with_pis_target(&self.aggregation.rhs.evm_proof, rhs_proof); agg_inputs.set_verifier_data_target( &self.aggregation.cyclic_vk, &self.aggregation.circuit.verifier_only, ); // Aggregates both `PublicValues` from the provided proofs into a single one. let agg_public_values = PublicValues { trie_roots_before: lhs_public_values.trie_roots_before, trie_roots_after: rhs_public_values.trie_roots_after, extra_block_data: ExtraBlockData { genesis_state_trie_root: lhs_public_values.extra_block_data.genesis_state_trie_root, txn_number_before: lhs_public_values.extra_block_data.txn_number_before, txn_number_after: rhs_public_values.extra_block_data.txn_number_after, gas_used_before: lhs_public_values.extra_block_data.gas_used_before, gas_used_after: rhs_public_values.extra_block_data.gas_used_after, block_bloom_before: lhs_public_values.extra_block_data.block_bloom_before, block_bloom_after: rhs_public_values.extra_block_data.block_bloom_after, }, block_metadata: rhs_public_values.block_metadata, block_hashes: rhs_public_values.block_hashes, }; set_public_value_targets( &mut agg_inputs, &self.aggregation.public_values, &agg_public_values, ) .map_err(|_| { anyhow::Error::msg("Invalid conversion when setting public values targets.") })?; let aggregation_proof = self.aggregation.circuit.prove(agg_inputs)?; Ok((aggregation_proof, agg_public_values)) } pub fn verify_aggregation( &self, agg_proof: &ProofWithPublicInputs, ) -> anyhow::Result<()> { self.aggregation.circuit.verify(agg_proof.clone())?; check_cyclic_proof_verifier_data( agg_proof, &self.aggregation.circuit.verifier_only, &self.aggregation.circuit.common, ) } pub fn prove_block( &self, opt_parent_block_proof: Option<&ProofWithPublicInputs>, agg_root_proof: &ProofWithPublicInputs, public_values: PublicValues, ) -> anyhow::Result<(ProofWithPublicInputs, PublicValues)> { let mut block_inputs = PartialWitness::new(); block_inputs.set_bool_target( self.block.has_parent_block, opt_parent_block_proof.is_some(), ); if let Some(parent_block_proof) = opt_parent_block_proof { block_inputs .set_proof_with_pis_target(&self.block.parent_block_proof, parent_block_proof); } else { // Initialize genesis_state_trie, state_root_after, and the previous block hashes for correct connection between blocks. // Block number does not need to be initialized as genesis block is constrained to have number 0. if public_values.trie_roots_before.state_root != public_values.extra_block_data.genesis_state_trie_root { return Err(anyhow::Error::msg(format!( "Inconsistent pre-state for first block {:?} with genesis state {:?}.", public_values.trie_roots_before.state_root, public_values.extra_block_data.genesis_state_trie_root, ))); } // Initialize `state_root_after`. let state_trie_root_after_keys = TrieRootsTarget::SIZE..TrieRootsTarget::SIZE + TrieRootsTarget::HASH_SIZE; let mut nonzero_pis = HashMap::new(); for (key, &value) in state_trie_root_after_keys .zip_eq(&h256_limbs::(public_values.trie_roots_before.state_root)) { nonzero_pis.insert(key, value); } // Initialize the genesis state trie digest. let genesis_state_trie_keys = TrieRootsTarget::SIZE * 2 + BlockMetadataTarget::SIZE + BlockHashesTarget::BLOCK_HASHES_SIZE ..TrieRootsTarget::SIZE * 2 + BlockMetadataTarget::SIZE + BlockHashesTarget::BLOCK_HASHES_SIZE + 8; for (key, &value) in genesis_state_trie_keys.zip_eq(&h256_limbs::( public_values.extra_block_data.genesis_state_trie_root, )) { nonzero_pis.insert(key, value); } // Initialize block hashes. let block_hashes_keys = TrieRootsTarget::SIZE * 2 + BlockMetadataTarget::SIZE ..TrieRootsTarget::SIZE * 2 + BlockMetadataTarget::SIZE + BlockHashesTarget::BLOCK_HASHES_SIZE - 8; for i in 0..public_values.block_hashes.prev_hashes.len() - 1 { let targets = h256_limbs::(public_values.block_hashes.prev_hashes[i]); for j in 0..8 { nonzero_pis.insert(block_hashes_keys.start + 8 * (i + 1) + j, targets[j]); } } let block_hashes_current_start = TrieRootsTarget::SIZE * 2 + BlockMetadataTarget::SIZE + BlockHashesTarget::BLOCK_HASHES_SIZE - 8; let cur_targets = h256_limbs::(public_values.block_hashes.prev_hashes[255]); for i in 0..8 { nonzero_pis.insert(block_hashes_current_start + i, cur_targets[i]); } block_inputs.set_proof_with_pis_target( &self.block.parent_block_proof, &cyclic_base_proof( &self.block.circuit.common, &self.block.circuit.verifier_only, nonzero_pis, ), ); } block_inputs.set_proof_with_pis_target(&self.block.agg_root_proof, agg_root_proof); block_inputs .set_verifier_data_target(&self.block.cyclic_vk, &self.block.circuit.verifier_only); set_public_value_targets(&mut block_inputs, &self.block.public_values, &public_values) .map_err(|_| { anyhow::Error::msg("Invalid conversion when setting public values targets.") })?; let block_proof = self.block.circuit.prove(block_inputs)?; Ok((block_proof, public_values)) } pub fn verify_block(&self, block_proof: &ProofWithPublicInputs) -> anyhow::Result<()> { self.block.circuit.verify(block_proof.clone())?; check_cyclic_proof_verifier_data( block_proof, &self.block.circuit.verifier_only, &self.block.circuit.common, ) } } #[derive(Eq, PartialEq, Debug)] pub struct RecursiveCircuitsForTable where F: RichField + Extendable, C: GenericConfig, C::Hasher: AlgebraicHasher, { /// A map from `log_2(height)` to a chain of shrinking recursion circuits starting at that /// height. by_stark_size: BTreeMap>, } impl RecursiveCircuitsForTable where F: RichField + Extendable, C: GenericConfig, C::Hasher: AlgebraicHasher, { fn to_buffer( &self, buffer: &mut Vec, gate_serializer: &dyn GateSerializer, generator_serializer: &dyn WitnessGeneratorSerializer, ) -> IoResult<()> { buffer.write_usize(self.by_stark_size.len())?; for (&size, table) in &self.by_stark_size { buffer.write_usize(size)?; table.to_buffer(buffer, gate_serializer, generator_serializer)?; } Ok(()) } fn from_buffer( buffer: &mut Buffer, gate_serializer: &dyn GateSerializer, generator_serializer: &dyn WitnessGeneratorSerializer, ) -> IoResult { let length = buffer.read_usize()?; let mut by_stark_size = BTreeMap::new(); for _ in 0..length { let key = buffer.read_usize()?; let table = RecursiveCircuitsForTableSize::from_buffer( buffer, gate_serializer, generator_serializer, )?; by_stark_size.insert(key, table); } Ok(Self { by_stark_size }) } fn new>( table: Table, stark: &S, degree_bits_range: Range, all_ctls: &[CrossTableLookup], stark_config: &StarkConfig, ) -> Self { let by_stark_size = degree_bits_range .map(|degree_bits| { ( degree_bits, RecursiveCircuitsForTableSize::new::( table, stark, degree_bits, all_ctls, stark_config, ), ) }) .collect(); Self { by_stark_size } } fn expand>( &mut self, table: Table, stark: &S, degree_bits_range: Range, all_ctls: &[CrossTableLookup], stark_config: &StarkConfig, ) { let new_ranges = degree_bits_range .filter(|degree_bits| !self.by_stark_size.contains_key(degree_bits)) .collect_vec(); for degree_bits in new_ranges { self.by_stark_size.insert( degree_bits, RecursiveCircuitsForTableSize::new::( table, stark, degree_bits, all_ctls, stark_config, ), ); } } /// For each initial `degree_bits`, get the final circuit at the end of that shrinking chain. /// Each of these final circuits should have degree `THRESHOLD_DEGREE_BITS`. fn final_circuits(&self) -> Vec<&CircuitData> { self.by_stark_size .values() .map(|chain| { chain .shrinking_wrappers .last() .map(|wrapper| &wrapper.circuit) .unwrap_or(&chain.initial_wrapper.circuit) }) .collect() } } /// A chain of shrinking wrapper circuits, ending with a final circuit with `degree_bits` /// `THRESHOLD_DEGREE_BITS`. #[derive(Eq, PartialEq, Debug)] struct RecursiveCircuitsForTableSize where F: RichField + Extendable, C: GenericConfig, C::Hasher: AlgebraicHasher, { initial_wrapper: StarkWrapperCircuit, shrinking_wrappers: Vec>, } impl RecursiveCircuitsForTableSize where F: RichField + Extendable, C: GenericConfig, C::Hasher: AlgebraicHasher, { fn to_buffer( &self, buffer: &mut Vec, gate_serializer: &dyn GateSerializer, generator_serializer: &dyn WitnessGeneratorSerializer, ) -> IoResult<()> { buffer.write_usize(self.shrinking_wrappers.len())?; if !self.shrinking_wrappers.is_empty() { buffer.write_common_circuit_data( &self.shrinking_wrappers[0].circuit.common, gate_serializer, )?; } for wrapper in &self.shrinking_wrappers { buffer.write_prover_only_circuit_data( &wrapper.circuit.prover_only, generator_serializer, &wrapper.circuit.common, )?; buffer.write_verifier_only_circuit_data(&wrapper.circuit.verifier_only)?; buffer.write_target_proof_with_public_inputs(&wrapper.proof_with_pis_target)?; } self.initial_wrapper .to_buffer(buffer, gate_serializer, generator_serializer)?; Ok(()) } fn from_buffer( buffer: &mut Buffer, gate_serializer: &dyn GateSerializer, generator_serializer: &dyn WitnessGeneratorSerializer, ) -> IoResult { let length = buffer.read_usize()?; let mut shrinking_wrappers = Vec::with_capacity(length); if length != 0 { let common = buffer.read_common_circuit_data(gate_serializer)?; for _ in 0..length { let prover_only = buffer.read_prover_only_circuit_data(generator_serializer, &common)?; let verifier_only = buffer.read_verifier_only_circuit_data()?; let proof_with_pis_target = buffer.read_target_proof_with_public_inputs()?; shrinking_wrappers.push(PlonkWrapperCircuit { circuit: CircuitData { common: common.clone(), prover_only, verifier_only, }, proof_with_pis_target, }) } }; let initial_wrapper = StarkWrapperCircuit::from_buffer(buffer, gate_serializer, generator_serializer)?; Ok(Self { initial_wrapper, shrinking_wrappers, }) } fn new>( table: Table, stark: &S, degree_bits: usize, all_ctls: &[CrossTableLookup], stark_config: &StarkConfig, ) -> Self { let initial_wrapper = recursive_stark_circuit( table, stark, degree_bits, all_ctls, stark_config, &shrinking_config(), THRESHOLD_DEGREE_BITS, ); let mut shrinking_wrappers = vec![]; // Shrinking recursion loop. loop { let last = shrinking_wrappers .last() .map(|wrapper: &PlonkWrapperCircuit| &wrapper.circuit) .unwrap_or(&initial_wrapper.circuit); let last_degree_bits = last.common.degree_bits(); assert!(last_degree_bits >= THRESHOLD_DEGREE_BITS); if last_degree_bits == THRESHOLD_DEGREE_BITS { break; } let mut builder = CircuitBuilder::new(shrinking_config()); let proof_with_pis_target = builder.add_virtual_proof_with_pis(&last.common); let last_vk = builder.constant_verifier_data(&last.verifier_only); builder.verify_proof::(&proof_with_pis_target, &last_vk, &last.common); builder.register_public_inputs(&proof_with_pis_target.public_inputs); // carry PIs forward add_common_recursion_gates(&mut builder); let circuit = builder.build::(); assert!( circuit.common.degree_bits() < last_degree_bits, "Couldn't shrink to expected recursion threshold of 2^{}; stalled at 2^{}", THRESHOLD_DEGREE_BITS, circuit.common.degree_bits() ); shrinking_wrappers.push(PlonkWrapperCircuit { circuit, proof_with_pis_target, }); } Self { initial_wrapper, shrinking_wrappers, } } fn shrink( &self, stark_proof_with_metadata: &StarkProofWithMetadata, ctl_challenges: &GrandProductChallengeSet, ) -> anyhow::Result> { let mut proof = self .initial_wrapper .prove(stark_proof_with_metadata, ctl_challenges)?; for wrapper_circuit in &self.shrinking_wrappers { proof = wrapper_circuit.prove(&proof)?; } Ok(proof) } } /// Our usual recursion threshold is 2^12 gates, but for these shrinking circuits, we use a few more /// gates for a constant inner VK and for public inputs. This pushes us over the threshold to 2^13. /// As long as we're at 2^13 gates, we might as well use a narrower witness. fn shrinking_config() -> CircuitConfig { CircuitConfig { num_routed_wires: 40, ..CircuitConfig::standard_recursion_config() } }