diff --git a/evm/src/config.rs b/evm/src/config.rs index 24ddb6a7..3f88d99f 100644 --- a/evm/src/config.rs +++ b/evm/src/config.rs @@ -1,16 +1,25 @@ use plonky2::fri::reduction_strategies::FriReductionStrategy; use plonky2::fri::{FriConfig, FriParams}; +/// A configuration containing the different parameters to be used by the STARK prover. pub struct StarkConfig { + /// The targeted security level for the proofs generated with this configuration. pub security_bits: usize, /// The number of challenge points to generate, for IOPs that have soundness errors of (roughly) /// `degree / |F|`. pub num_challenges: usize, + /// The configuration of the FRI sub-protocol. pub fri_config: FriConfig, } +impl Default for StarkConfig { + fn default() -> Self { + Self::standard_fast_config() + } +} + impl StarkConfig { /// A typical configuration with a rate of 2, resulting in fast but large proofs. /// Targets ~100 bit conjectured security. diff --git a/evm/src/fixed_recursive_verifier.rs b/evm/src/fixed_recursive_verifier.rs index 3f405e52..58db9987 100644 --- a/evm/src/fixed_recursive_verifier.rs +++ b/evm/src/fixed_recursive_verifier.rs @@ -69,8 +69,10 @@ where { /// The EVM root circuit, which aggregates the (shrunk) per-table recursive proofs. pub root: RootCircuitData, + /// The aggregation circuit, which verifies two proofs that can either be root or + /// aggregation proofs. pub aggregation: AggregationCircuitData, - /// The block circuit, which verifies an aggregation root proof and a previous block proof. + /// The block circuit, which verifies an aggregation root proof and an optional previous block proof. pub block: BlockCircuitData, /// Holds chains of circuits for each table and for each initial `degree_bits`. pub by_table: [RecursiveCircuitsForTable; NUM_TABLES], @@ -236,6 +238,8 @@ impl AggregationChildTarget { } } +/// Data for the block circuit, which is used to generate a final block proof, +/// and compress it with an optional parent proof if present. #[derive(Eq, PartialEq, Debug)] pub struct BlockCircuitData where @@ -298,6 +302,16 @@ where C: GenericConfig + 'static, C::Hasher: AlgebraicHasher, { + /// Serializes all these preprocessed circuits into a sequence of bytes. + /// + /// # Arguments + /// + /// - `skip_tables`: a boolean indicating whether to serialize only the upper circuits + /// or the entire prover state, including recursive circuits to shrink STARK proofs. + /// - `gate_serializer`: a custom gate serializer needed to serialize recursive circuits + /// common data. + /// - `generator_serializer`: a custom generator serializer needed to serialize recursive + /// circuits proving data. pub fn to_bytes( &self, skip_tables: bool, @@ -320,6 +334,17 @@ where Ok(buffer) } + /// Deserializes a sequence of bytes into an entire prover state containing all recursive circuits. + /// + /// # Arguments + /// + /// - `bytes`: a slice of bytes to deserialize this prover state from. + /// - `skip_tables`: a boolean indicating whether to deserialize only the upper circuits + /// or the entire prover state, including recursive circuits to shrink STARK proofs. + /// - `gate_serializer`: a custom gate serializer needed to serialize recursive circuits + /// common data. + /// - `generator_serializer`: a custom generator serializer needed to serialize recursive + /// circuits proving data. pub fn from_bytes( bytes: &[u8], skip_tables: bool, @@ -373,6 +398,19 @@ where } /// Preprocess all recursive circuits used by the system. + /// + /// # Arguments + /// + /// - `all_stark`: a structure defining the logic of all STARK modules and their associated + /// cross-table lookups. + /// - `degree_bits_ranges`: the logarithmic ranges to be supported for the recursive tables. + /// Transactions may yield arbitrary trace lengths for each STARK module (within some bounds), + /// unknown prior generating the witness to create a proof. Thus, for each STARK module, we + /// construct a map from `2^{degree_bits} = length` to a chain of shrinking recursion circuits, + /// starting from that length, for each `degree_bits` in the range specified for this STARK module. + /// Specifying a wide enough range allows a prover to cover all possible scenarios. + /// - `stark_config`: the configuration to be used for the STARK prover. It will usually be a fast + /// one yielding large proofs. pub fn new( all_stark: &AllStark, degree_bits_ranges: &[Range; NUM_TABLES], @@ -450,6 +488,19 @@ where /// Outputs the `VerifierCircuitData` needed to verify any block proof /// generated by an honest prover. + /// While the [`AllRecursiveCircuits`] prover state can also verify proofs, verifiers + /// only need a fraction of the state to verify proofs. This allows much less powerful + /// entities to behave as verifiers, by only loading the necessary data to verify block proofs. + /// + /// # Usage + /// + /// ```ignore + /// let prover_state = AllRecursiveCircuits { ... }; + /// let verifier_state = prover_state.final_verifier_data(); + /// + /// // Verify a provided block proof + /// assert!(verifier_state.verify(&block_proof).is_ok()); + /// ``` pub fn final_verifier_data(&self) -> VerifierCircuitData { self.block.circuit.verifier_data() } @@ -912,7 +963,29 @@ where } } - /// Create a proof for each STARK, then combine them, eventually culminating in a root proof. + /// For a given transaction payload passed as [`GenerationInputs`], create a proof + /// for each STARK module, then recursively shrink and combine them, eventually + /// culminating in a transaction proof, also called root proof. + /// + /// # Arguments + /// + /// - `all_stark`: a structure defining the logic of all STARK modules and their associated + /// cross-table lookups. + /// - `config`: the configuration to be used for the STARK prover. It will usually be a fast + /// one yielding large proofs. + /// - `generation_inputs`: a transaction and auxiliary data needed to generate a proof, provided + /// in Intermediary Representation. + /// - `timing`: a profiler defining a scope hierarchy and the time consumed by each one. + /// - `abort_signal`: an optional [`AtomicBool`] wrapped behind an [`Arc`], to send a kill signal + /// early. This is only necessary in a distributed setting where a worker may be blocking the entire + /// queue. + /// + /// # Outputs + /// + /// This method outputs a tuple of [`ProofWithPublicInputs`] and its [`PublicValues`]. Only + /// the proof with public inputs is necessary for a verifier to assert correctness of the computation, + /// but the public values are output for the prover convenience, as these are necessary during proof + /// aggregation. pub fn prove_root( &self, all_stark: &AllStark, @@ -981,6 +1054,50 @@ where /// From an initial set of STARK proofs passed with their associated recursive table circuits, /// generate a recursive transaction proof. /// It is aimed at being used when preprocessed table circuits have not been loaded to memory. + /// + /// **Note**: + /// The type of the `table_circuits` passed as arguments is + /// `&[(RecursiveCircuitsForTableSize, u8); NUM_TABLES]`. In particular, for each STARK + /// proof contained within the `AllProof` object provided to this method, we need to pass a tuple + /// of [`RecursiveCircuitsForTableSize`] and a [`u8`]. The former is the recursive chain + /// corresponding to the initial degree size of the associated STARK proof. The latter is the + /// index of this degree in the range that was originally passed when constructing the entire prover + /// state. + /// + /// # Usage + /// + /// ```ignore + /// // Load a prover state without its recursive table circuits. + /// let gate_serializer = DefaultGateSerializer; + /// let generator_serializer = DefaultGeneratorSerializer::::new(); + /// let initial_ranges = [16..25, 10..20, 12..25, 14..25, 9..20, 12..20, 17..30]; + /// let prover_state = AllRecursiveCircuits::::new( + /// &all_stark, + /// &initial_ranges, + /// &config, + /// ); + /// + /// // Generate a proof from the provided inputs. + /// let stark_proof = prove::(&all_stark, &config, inputs, &mut timing, abort_signal).unwrap(); + /// + /// // Read the degrees of the internal STARK proofs. + /// // Indices to be passed along the recursive tables + /// // can be easily recovered as `initial_ranges[i]` - `degrees[i]`. + /// let degrees = proof.degree_bits(&config); + /// + /// // Retrieve the corresponding recursive table circuits for each table with the corresponding degree. + /// let table_circuits = { ... }; + /// + /// // Finally shrink the STARK proof. + /// let (proof, public_values) = prove_root_after_initial_stark( + /// &all_stark, + /// &config, + /// &stark_proof, + /// &table_circuits, + /// &mut timing, + /// abort_signal, + /// ).unwrap(); + /// ``` pub fn prove_root_after_initial_stark( &self, all_stark: &AllStark, @@ -1031,6 +1148,31 @@ where self.root.circuit.verify(agg_proof) } + /// Create an aggregation proof, combining two contiguous proofs into a single one. The combined + /// proofs can either be transaction (aka root) proofs, or other aggregation proofs, as long as + /// their states are contiguous, meaning that the final state of the left child proof is the initial + /// state of the right child proof. + /// + /// While regular transaction proofs can only assert validity of a single transaction, aggregation + /// proofs can cover an arbitrary range, up to an entire block with all its transactions. + /// + /// # Arguments + /// + /// - `lhs_is_agg`: a boolean indicating whether the left child proof is an aggregation proof or + /// a regular transaction proof. + /// - `lhs_proof`: the left child proof. + /// - `lhs_public_values`: the public values associated to the right child proof. + /// - `rhs_is_agg`: a boolean indicating whether the right child proof is an aggregation proof or + /// a regular transaction proof. + /// - `rhs_proof`: the right child proof. + /// - `rhs_public_values`: the public values associated to the right child proof. + /// + /// # Outputs + /// + /// This method outputs a tuple of [`ProofWithPublicInputs`] and its [`PublicValues`]. Only + /// the proof with public inputs is necessary for a verifier to assert correctness of the computation, + /// but the public values are output for the prover convenience, as these are necessary during proof + /// aggregation. pub fn prove_aggregation( &self, lhs_is_agg: bool, @@ -1097,6 +1239,23 @@ where ) } + /// Create a final block proof, once all transactions of a given block have been combined into a + /// single aggregation proof. + /// + /// Block proofs can either be generated as standalone, or combined with a previous block proof + /// to assert validity of a range of blocks. + /// + /// # Arguments + /// + /// - `opt_parent_block_proof`: an optional parent block proof. Passing one will generate a proof of + /// validity for both the block range covered by the previous proof and the current block. + /// - `agg_root_proof`: the final aggregation proof containing all transactions within the current block. + /// - `public_values`: the public values associated to the aggregation proof. + /// + /// # Outputs + /// + /// This method outputs a tuple of [`ProofWithPublicInputs`] and its [`PublicValues`]. Only + /// the proof with public inputs is necessary for a verifier to assert correctness of the computation. pub fn prove_block( &self, opt_parent_block_proof: Option<&ProofWithPublicInputs>, @@ -1245,6 +1404,7 @@ where } } +/// A map between initial degree sizes and their associated shrinking recursion circuits. #[derive(Eq, PartialEq, Debug)] pub struct RecursiveCircuitsForTable where diff --git a/evm/src/generation/mod.rs b/evm/src/generation/mod.rs index 8ae487b0..515238ec 100644 --- a/evm/src/generation/mod.rs +++ b/evm/src/generation/mod.rs @@ -43,13 +43,17 @@ use crate::witness::util::mem_write_log; /// Inputs needed for trace generation. #[derive(Clone, Debug, Deserialize, Serialize, Default)] pub struct GenerationInputs { + /// The index of the transaction being proven within its block. pub txn_number_before: U256, + /// The cumulative gas used through the execution of all transactions prior the current one. pub gas_used_before: U256, + /// The cumulative gas used after the execution of the current transaction. The exact gas used + /// by the current transaction is `gas_used_after` - `gas_used_before`. pub gas_used_after: U256, - // A None would yield an empty proof, otherwise this contains the encoding of a transaction. + /// A None would yield an empty proof, otherwise this contains the encoding of a transaction. pub signed_txn: Option>, - // Withdrawal pairs `(addr, amount)`. At the end of the txs, `amount` is added to `addr`'s balance. See EIP-4895. + /// Withdrawal pairs `(addr, amount)`. At the end of the txs, `amount` is added to `addr`'s balance. See EIP-4895. pub withdrawals: Vec<(Address, U256)>, pub tries: TrieInputs, /// Expected trie roots after the transactions are executed. @@ -64,8 +68,10 @@ pub struct GenerationInputs { /// All account smart contracts that are invoked will have an entry present. pub contract_code: HashMap>, + /// Information contained in the block header. pub block_metadata: BlockMetadata, + /// The hash of the current block, and a list of the 256 previous block hashes. pub block_hashes: BlockHashes, } diff --git a/evm/src/lib.rs b/evm/src/lib.rs index 42547c3e..025fc8e6 100644 --- a/evm/src/lib.rs +++ b/evm/src/lib.rs @@ -1,3 +1,164 @@ +//! An implementation of a Type 1 zk-EVM by Polygon Zero. +//! +//! Following the [zk-EVM classification of V. Buterin](https://vitalik.eth.limo/general/2022/08/04/zkevm.html), +//! the plonky2_evm crate aims at providing an efficient solution for the problem of generating cryptographic +//! proofs of Ethereum-like transactions with *full Ethereum capability*. +//! +//! To this end, the plonky2 zk-EVM is tailored for an AIR-based STARK system satisfying degree 3 constraints, +//! with support for recursive aggregation leveraging plonky2 circuits with FRI-based plonkish arithmetization. +//! These circuits require a one-time, offline preprocessing phase. +//! See the [`fixed_recursive_verifier`] module for more details on how this works. +//! These preprocessed circuits are gathered within the [`AllRecursiveCircuits`] prover state, +//! and can be generated as such: +//! +//! ```ignore +//! // Specify the base field to use. +//! type F = GoldilocksField; +//! // Specify the extension degree to use. +//! const D: usize = 2; +//! // Specify the recursive configuration to use, here leveraging Poseidon hash +//! // over the Goldilocks field both natively and in-circuit. +//! type C = PoseidonGoldilocksConfig; +//! +//! let all_stark = AllStark::::default(); +//! let config = StarkConfig::standard_fast_config(); +//! +//! // Generate all the recursive circuits needed to generate succinct proofs for blocks. +//! // The ranges correspond to the supported table sizes for each individual STARK component. +//! let prover_state = AllRecursiveCircuits::::new( +//! &all_stark, +//! &[16..25, 10..20, 12..25, 14..25, 9..20, 12..20, 17..30], +//! &config, +//! ); +//! ``` +//! +//! # Inputs type +//! +//! Transactions need to be processed into an Intermediary Representation (IR) format for the prover +//! to be able to generate proofs of valid state transition. This involves passing the encoded transaction, +//! the header of the block in which it was included, some information on the state prior execution +//! of this transaction, etc. +//! This intermediary representation is called [`GenerationInputs`]. +//! +//! +//! # Generating succinct proofs +//! +//! ## Transaction proofs +//! +//! To generate a proof for a transaction, given its [`GenerationInputs`] and an [`AllRecursiveCircuits`] +//! prover state, one can simply call the [prove_root](AllRecursiveCircuits::prove_root) method. +//! +//! ```ignore +//! let mut timing = TimingTree::new("prove", log::Level::Debug); +//! let kill_signal = None; // Useful only with distributed proving to kill hanging jobs. +//! let (proof, public_values) = +//! prover_state.prove_root(all_stark, config, inputs, &mut timing, kill_signal); +//! ``` +//! +//! This outputs a transaction proof and its associated public values. These are necessary during the +//! aggregation levels (see below). If one were to miss the public values, they are also retrievable directly +//! from the proof's encoded public inputs, as such: +//! +//! ```ignore +//! let public_values = PublicValues::from_public_inputs(&proof.public_inputs); +//! ``` +//! +//! ## Aggregation proofs +//! +//! Because the plonky2 zkEVM generates proofs on a transaction basis, we then need to aggregate them for succinct +//! verification. This is done in a binary tree fashion, where each inner node proof verifies two children proofs, +//! through the [prove_aggregation](AllRecursiveCircuits::prove_aggregation) method. +//! Note that the tree does *not* need to be complete, as this aggregation process can take as inputs both regular +//! transaction proofs and aggregation proofs. We only need to specify for each child if it is an aggregation proof +//! or a regular one. +//! +//! ```ignore +//! let (proof_1, pv_1) = +//! prover_state.prove_root(all_stark, config, inputs_1, &mut timing, None); +//! let (proof_2, pv_2) = +//! prover_state.prove_root(all_stark, config, inputs_2, &mut timing, None); +//! let (proof_3, pv_3) = +//! prover_state.prove_root(all_stark, config, inputs_3, &mut timing, None); +//! +//! // Now aggregate proofs for txn 1 and 2. +//! let (agg_proof_1_2, pv_1_2) = +//! prover_state.prove_aggregation(false, proof_1, pv_1, false, proof_2, pv_2); +//! +//! // Now aggregate the newly generated aggregation proof with the last regular txn proof. +//! let (agg_proof_1_3, pv_1_3) = +//! prover_state.prove_aggregation(true, agg_proof_1_2, pv_1_2, false, proof_3, pv_3); +//! ``` +//! +//! **Note**: The proofs provided to the [prove_aggregation](AllRecursiveCircuits::prove_aggregation) method *MUST* have contiguous states. +//! Trying to combine `proof_1` and `proof_3` from the example above would fail. +//! +//! ## Block proofs +//! +//! Once all transactions of a block have been proven and we are left with a single aggregation proof and its public values, +//! we can then wrap it into a final block proof, attesting validity of the entire block. +//! This [prove_block](AllRecursiveCircuits::prove_block) method accepts an optional previous block proof as argument, +//! which will then try combining the previously proven block with the current one, generating a validity proof for both. +//! Applying this process from genesis would yield a single proof attesting correctness of the entire chain. +//! +//! ```ignore +//! let previous_block_proof = { ... }; +//! let (block_proof, block_public_values) = +//! prover_state.prove_block(Some(&previous_block_proof), &agg_proof, agg_pv)?; +//! ``` +//! +//! ### Checkpoint heights +//! +//! The process of always providing a previous block proof when generating a proof for the current block may yield some +//! undesirable issues. For this reason, the plonky2 zk-EVM supports checkpoint heights. At given block heights, +//! the prover does not have to pass a previous block proof. This would in practice correspond to block heights at which +//! a proof has been generated and sent to L1 for settlement. +//! +//! The only requirement when generating a block proof without passing a previous one as argument is to have the +//! `checkpoint_state_trie_root` metadata in the `PublicValues` of the final aggregation proof be matching the state +//! trie before applying all the included transactions. If this condition is not met, the prover will fail to generate +//! a valid proof. +//! +//! +//! ```ignore +//! let (block_proof, block_public_values) = +//! prover_state.prove_block(None, &agg_proof, agg_pv)?; +//! ``` +//! +//! # Prover state serialization +//! +//! Because the recursive circuits only need to be generated once, they can be saved to disk once the preprocessing phase +//! completed successfully, and deserialized on-demand. +//! The plonky2 zk-EVM provides serialization methods to convert the entire prover state to a vector of bytes, and vice-versa. +//! This requires the use of custom serializers for gates and generators for proper recursive circuit encoding. This crate provides +//! default serializers supporting all custom gates and associated generators defined within the [`plonky2`] crate. +//! +//! ```ignore +//! let prover_state = AllRecursiveCircuits::::new(...); +//! +//! // Default serializers +//! let gate_serializer = DefaultGateSerializer; +//! let generator_serializer = DefaultGeneratorSerializer:: { +//! _phantom: PhantomData::, +//! }; +//! +//! // Serialize the prover state to a sequence of bytes +//! let bytes = prover_state.to_bytes(false, &gate_serializer, &generator_serializer).unwrap(); +//! +//! // Deserialize the bytes into a prover state +//! let recovered_prover_state = AllRecursiveCircuits::::from_bytes( +//! &all_circuits_bytes, +//! false, +//! &gate_serializer, +//! &generator_serializer, +//! ).unwrap(); +//! +//! assert_eq!(prover_state, recovered_prover_state); +//! ``` +//! +//! Note that an entire prover state built with wide ranges may be particularly large (up to ~25 GB), hence serialization methods, +//! while faster than doing another preprocessing, may take some non-negligible time. + +#![cfg_attr(docsrs, feature(doc_cfg))] #![allow(clippy::needless_range_loop)] #![allow(clippy::too_many_arguments)] #![allow(clippy::field_reassign_with_default)] @@ -43,4 +204,11 @@ use jemallocator::Jemalloc; #[global_allocator] static GLOBAL: Jemalloc = Jemalloc; +// Public definitions and re-exports + pub type Node = eth_trie_utils::partial_trie::Node; + +pub use all_stark::AllStark; +pub use config::StarkConfig; +pub use fixed_recursive_verifier::AllRecursiveCircuits; +pub use generation::GenerationInputs; diff --git a/evm/tests/empty_txn_list.rs b/evm/tests/empty_txn_list.rs index 5904b8a9..ff4e7637 100644 --- a/evm/tests/empty_txn_list.rs +++ b/evm/tests/empty_txn_list.rs @@ -83,7 +83,7 @@ fn test_empty_txn_list() -> anyhow::Result<()> { { let gate_serializer = DefaultGateSerializer; - let generator_serializer = DefaultGeneratorSerializer { + let generator_serializer = DefaultGeneratorSerializer:: { _phantom: PhantomData::, }; diff --git a/field/src/goldilocks_field.rs b/field/src/goldilocks_field.rs index 36c6aad2..4e459c90 100644 --- a/field/src/goldilocks_field.rs +++ b/field/src/goldilocks_field.rs @@ -104,7 +104,7 @@ impl Field for GoldilocksField { /// Therefore $a^(p-2) = a^-1 (mod p)$ /// /// The following code has been adapted from winterfell/math/src/field/f64/mod.rs - /// located at https://github.com/facebook/winterfell. + /// located at . fn try_inverse(&self) -> Option { if self.is_zero() { return None; diff --git a/plonky2/src/fri/mod.rs b/plonky2/src/fri/mod.rs index 100ae851..da434d61 100644 --- a/plonky2/src/fri/mod.rs +++ b/plonky2/src/fri/mod.rs @@ -15,6 +15,7 @@ mod validate_shape; pub mod verifier; pub mod witness_util; +/// A configuration for the FRI protocol. #[derive(Debug, Clone, Eq, PartialEq, Serialize)] pub struct FriConfig { /// `rate = 2^{-rate_bits}`. @@ -23,8 +24,10 @@ pub struct FriConfig { /// Height of Merkle tree caps. pub cap_height: usize, + /// Number of bits used for grinding. pub proof_of_work_bits: u32, + /// The reduction strategy to be applied at each layer during the commit phase. pub reduction_strategy: FriReductionStrategy, /// Number of query rounds to perform.