diff --git a/Cargo.toml b/Cargo.toml index 875f037f..f7c9dac5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "evm", "evm-tests" ] +exclude = ["zk-evm"] [workspace.package] authors = ["Aurora Labs "] diff --git a/evm-tests/Cargo.toml b/evm-tests/Cargo.toml index eada2868..e60fdce5 100644 --- a/evm-tests/Cargo.toml +++ b/evm-tests/Cargo.toml @@ -19,7 +19,7 @@ aurora-engine-precompiles = "2.1.0" aurora-evm.workspace = true bytecount = "0.6" clap = { version = "4.5", features = ["cargo"] } -c-kzg = "1.0" +#c-kzg = { version = "1.0", default-features = false, features = ["generate-bindings", "portable", "serde"] } derive_more = { version = "2", features = ["full"] } ethereum = "0.18" hex = "0.4" diff --git a/evm-tests/src/assertions.rs b/evm-tests/src/assertions.rs index 76c66601..502f3f17 100644 --- a/evm-tests/src/assertions.rs +++ b/evm-tests/src/assertions.rs @@ -194,6 +194,7 @@ pub fn assert_vicinity_validation( /// Check Exit Reason of EVM execution #[allow(clippy::too_many_lines)] +#[must_use] pub fn check_validate_exit_reason( reason: &InvalidTxReason, expect_exception: Option<&String>, @@ -361,6 +362,7 @@ pub fn assert_call_exit_exception(expect_exception: Option<&String>, name: &str) } /// Check Exit Reason of EVM execution +#[must_use] pub fn check_create_exit_reason( reason: &ExitReason, expect_exception: Option<&String>, diff --git a/evm-tests/src/lib.rs b/evm-tests/src/lib.rs new file mode 100644 index 00000000..10299cc8 --- /dev/null +++ b/evm-tests/src/lib.rs @@ -0,0 +1,5 @@ +#![allow(clippy::too_long_first_doc_paragraph, clippy::missing_panics_doc)] +pub mod assertions; +pub mod config; +pub mod precompiles; +pub mod types; diff --git a/evm-tests/src/precompiles.rs b/evm-tests/src/precompiles.rs index 6b948985..577affe9 100644 --- a/evm-tests/src/precompiles.rs +++ b/evm-tests/src/precompiles.rs @@ -1,6 +1,6 @@ -mod kzg; +// mod kzg; -use crate::precompiles::kzg::Kzg; +// use crate::precompiles::kzg::Kzg; use crate::types::Spec; use aurora_engine_precompiles::modexp::AuroraModExp; use aurora_engine_precompiles::{ @@ -40,6 +40,7 @@ impl PrecompileSet for Precompiles { } impl Precompiles { + #[must_use] pub fn new(spec: &Spec) -> Self { match *spec { Spec::Frontier @@ -57,6 +58,7 @@ impl Precompiles { } } + #[must_use] pub fn new_istanbul() -> Self { let mut map = BTreeMap::new(); map.insert( @@ -86,6 +88,7 @@ impl Precompiles { Self(map) } + #[must_use] pub fn new_berlin() -> Self { let mut map = BTreeMap::new(); map.insert( @@ -115,12 +118,14 @@ impl Precompiles { Self(map) } + #[must_use] pub fn new_cancun() -> Self { - let mut map = Self::new_berlin().0; - map.insert(Kzg::ADDRESS, Box::new(Kzg)); + let map = Self::new_berlin().0; + // map.insert(Kzg::ADDRESS, Box::new(Kzg)); Self(map) } + #[must_use] pub fn new_prague() -> Self { let mut map = Self::new_cancun().0; map.insert(BlsG1Add::ADDRESS.raw(), Box::new(BlsG1Add)); @@ -133,6 +138,7 @@ impl Precompiles { Self(map) } + #[must_use] pub fn new_osaka() -> Self { let mut map = BTreeMap::new(); map.insert( @@ -160,7 +166,7 @@ impl Precompiles { ); map.insert(Blake2F::ADDRESS.raw(), Box::new(Blake2F)); - map.insert(Kzg::ADDRESS, Box::new(Kzg)); + // map.insert(Kzg::ADDRESS, Box::new(Kzg)); map.insert(BlsG1Add::ADDRESS.raw(), Box::new(BlsG1Add)); map.insert(BlsG1Msm::ADDRESS.raw(), Box::new(BlsG1Msm)); map.insert(BlsG2Add::ADDRESS.raw(), Box::new(BlsG2Add)); diff --git a/evm-tests/src/types/account_state.rs b/evm-tests/src/types/account_state.rs index d8711e7c..b0876e42 100644 --- a/evm-tests/src/types/account_state.rs +++ b/evm-tests/src/types/account_state.rs @@ -52,10 +52,10 @@ impl From for MemoryAccount { /// corresponding state (`StateAccount`). /// It uses a `BTreeMap` to ensure a consistent order for serialization. #[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone)] -pub struct AccountsState(BTreeMap); +pub struct AccountsState(pub BTreeMap); impl AccountsState { - /// Converts the `AccountsState` into a `BTreeMap` of `H160` addresses to `MemoryAccount`. + /// Converts the `AccountsState` into a `BTreeMap` of `H160` addresses to `MemoryAccount`. #[must_use] pub fn to_memory_accounts_state(&self) -> MemoryAccountsState { MemoryAccountsState( diff --git a/evm-tests/src/types/mod.rs b/evm-tests/src/types/mod.rs index 87fd2257..dcb18ea9 100644 --- a/evm-tests/src/types/mod.rs +++ b/evm-tests/src/types/mod.rs @@ -16,7 +16,7 @@ pub mod blob; pub mod eip_4844; pub mod eip_7623; pub mod eip_7702; -mod info; +pub mod info; mod json_utils; pub mod spec; pub mod transaction; @@ -208,7 +208,7 @@ impl From for MemoryVicinity { /// corresponding state (`StateAccount`). /// Represents vis `AccountsState`. #[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, Deserialize)] -pub struct PreState(AccountsState); +pub struct PreState(pub AccountsState); impl AsRef for PreState { fn as_ref(&self) -> &AccountsState { diff --git a/evm-tests/src/types/spec.rs b/evm-tests/src/types/spec.rs index 22140aa2..dbedf892 100644 --- a/evm-tests/src/types/spec.rs +++ b/evm-tests/src/types/spec.rs @@ -70,7 +70,7 @@ impl Spec { } #[must_use] - pub(crate) const fn get_gasometer_config(&self) -> Option { + pub const fn get_gasometer_config(&self) -> Option { match self { Self::Istanbul => Some(Config::istanbul()), Self::Berlin => Some(Config::berlin()), diff --git a/evm/src/runtime/mod.rs b/evm/src/runtime/mod.rs index 75e7e330..443863a9 100644 --- a/evm/src/runtime/mod.rs +++ b/evm/src/runtime/mod.rs @@ -85,7 +85,7 @@ impl Runtime { pub fn run( &mut self, handler: &mut H, - ) -> Capture> { + ) -> Capture> { loop { let result = self.machine.step(handler, &self.context.address); match result { diff --git a/zk-evm/.gitignore b/zk-evm/.gitignore new file mode 100644 index 00000000..f4247e18 --- /dev/null +++ b/zk-evm/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +Cargo.lock +methods/guest/Cargo.lock +target/ diff --git a/zk-evm/Cargo.toml b/zk-evm/Cargo.toml new file mode 100644 index 00000000..38873ea2 --- /dev/null +++ b/zk-evm/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "aurora-zk-evm" +version = "0.1.0" +edition = "2021" + +[dependencies] +aurora-evm-jsontests = { path = "../evm-tests" } +clap = { version = "4.5", features = ["cargo"] } +methods = { path = "methods" } +risc0-zkvm = { version = "^3.0.4" } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +serde = "1.0" +serde_json = "1.0" diff --git a/zk-evm/Makefile b/zk-evm/Makefile new file mode 100644 index 00000000..11eebf9d --- /dev/null +++ b/zk-evm/Makefile @@ -0,0 +1,3 @@ +run: + @cargo run --release -- ../../../ethereum/fixtures_static-v4.5.0/fixtures/state_tests/static/state_tests/stStaticCall/static_log_Caller.json +# @cargo run --release -- ../../../ethereum/ethtests-17.0/GeneralStateTests/stInitCodeTest/StackUnderFlowContractCreation.json diff --git a/zk-evm/methods/Cargo.toml b/zk-evm/methods/Cargo.toml new file mode 100644 index 00000000..2b64b4a3 --- /dev/null +++ b/zk-evm/methods/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "methods" +version = "0.1.0" +edition = "2021" + +[build-dependencies] +risc0-build = { version = "^3.0.4" } + +[package.metadata.risc0] +methods = ["guest"] diff --git a/zk-evm/methods/build.rs b/zk-evm/methods/build.rs new file mode 100644 index 00000000..08a8a4eb --- /dev/null +++ b/zk-evm/methods/build.rs @@ -0,0 +1,3 @@ +fn main() { + risc0_build::embed_methods(); +} diff --git a/zk-evm/methods/guest/Cargo.toml b/zk-evm/methods/guest/Cargo.toml new file mode 100644 index 00000000..343f3aa1 --- /dev/null +++ b/zk-evm/methods/guest/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "zk_evm" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +aurora-evm = { path = "../../../evm" } +aurora-evm-jsontests = { path = "../../../evm-tests" } +risc0-zkvm = { version = "^3.0.4", default-features = false, features = ['std'] } +serde_json = "1" +serde = { version = "1.0.228", features = ["derive"] } diff --git a/zk-evm/methods/guest/src/main.rs b/zk-evm/methods/guest/src/main.rs new file mode 100644 index 00000000..69e04780 --- /dev/null +++ b/zk-evm/methods/guest/src/main.rs @@ -0,0 +1,258 @@ +#![allow(unused_imports, dead_code)] +use aurora_evm::backend::{ApplyBackend, MemoryBackend}; +use aurora_evm::executor::stack::{MemoryStackState, StackExecutor, StackSubstateMetadata}; +use aurora_evm::utils::U256_ZERO; +use aurora_evm_jsontests::assertions; +use aurora_evm_jsontests::assertions::{ + assert_call_exit_exception, assert_empty_create_caller, assert_vicinity_validation, + check_create_exit_reason, +}; +use aurora_evm_jsontests::config::TestConfig; +use aurora_evm_jsontests::precompiles::Precompiles; +use aurora_evm_jsontests::types::account_state::{AccountsState, MemoryAccountsState}; +use aurora_evm_jsontests::types::blob::{calc_data_fee, calc_max_data_fee, BlobExcessGasAndPrice}; +use aurora_evm_jsontests::types::info::Info; +use aurora_evm_jsontests::types::transaction::{Transaction, TxType}; +use aurora_evm_jsontests::types::{PreState, StateEnv}; +use aurora_evm_jsontests::types::{Spec, StateTestCase}; +use risc0_zkvm::guest::env; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, HashMap}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct ExecutionResult { + pub used_gas: u64, + pub is_valid_hash: bool, + pub actual_hash: String, +} + +pub type ExecutionResults = Vec; + +fn main() { + // read the input + let input: String = env::read(); + let test_suite = serde_json::from_str::>(&input) + .expect("Parse test cases failed"); + + let test_config = TestConfig::default(); + + let mut results: ExecutionResults = vec![]; + + for (_, test) in test_suite { + for (spec, states) in &test.post_states { + // Geet gasometer config for the current spec + let Some(gasometer_config) = spec.get_gasometer_config() else { + // If the spec is not supported, skip the test + continue; + }; + + // EIP-4844 + let blob_gas_price = BlobExcessGasAndPrice::from_env(&test.env); + // EIP-4844 + let data_max_fee = calc_max_data_fee(&gasometer_config, &test.transaction); + let data_fee = calc_data_fee( + &gasometer_config, + &test.transaction, + blob_gas_price.as_ref(), + ); + + let original_state = test.pre_state.as_ref().to_memory_accounts_state(); + let vicinity = test.get_memory_vicinity(spec, blob_gas_price); + + if let Err(tx_err) = vicinity { + let h = states.first().unwrap().hash; + // if vicinity could not be computed, then the transaction was invalid, so we simply + // check the original state and move on + let (is_valid_hash, actual_hash) = original_state.check_valid_hash(&h); + if !is_valid_hash { + results.push(ExecutionResult { + used_gas: 0, + is_valid_hash, + actual_hash: actual_hash.to_string(), + }); + + continue; + } + assert_vicinity_validation(&tx_err, states, spec, &test_config); + // As it's an expected validation error-skip the test run + continue; + } + + let vicinity = vicinity.unwrap(); + let caller = test.transaction.get_caller_from_secret_key(); + + let caller_balance = original_state.caller_balance(caller); + // EIP-3607 + let caller_code = original_state.caller_code(caller); + // EIP-7702 - check if it's delegated designation. If it's a delegation designation, then, + // even if `caller_code` is non-empty, the transaction should be executed. + let is_delegated = original_state.is_delegated(caller); + + for state in states.iter() { + let mut backend = MemoryBackend::new(&vicinity, original_state.0.clone()); + + // Test case may be expected to fail with an unsupported tx type if the current fork is + // older than Berlin (see EIP-2718). However, this is not implemented in sputnik itself and rather + // in the code hosting sputnik. https://github.com/rust-blockchain/evm/pull/40 + if spec.is_filtered_spec_for_skip() + && TxType::from_tx_bytes(&state.tx_bytes) != TxType::Legacy + && state.expect_exception.as_deref() == Some("TR_TypeNotSupported") + { + continue; + } + + let gas_limit: u64 = test.transaction.get_gas_limit(state).as_u64(); + let data: Vec = test.transaction.get_data(state); + + let valid_tx = test.transaction.validate( + test.env.block_gas_limit, + caller_balance, + &gasometer_config, + &vicinity, + blob_gas_price, + data_max_fee, + spec, + state, + ); + // Only execute valid transactions + let authorization_list = match valid_tx { + Ok(list) => list, + Err(err) + if assertions::check_validate_exit_reason( + &err, + state.expect_exception.as_ref(), + test_config.name.as_str(), + spec, + ) => + { + continue + } + Err(err) => panic!("transaction validation error: {err:?}"), + }; + + // We do not check overflow after TX validation + let total_fee = if let Some(data_fee) = data_fee { + vicinity.effective_gas_price * gas_limit + data_fee + } else { + vicinity.effective_gas_price * gas_limit + }; + + let metadata = StackSubstateMetadata::new(gas_limit, &gasometer_config); + let executor_state = MemoryStackState::new(metadata, &backend); + // let precompile = JsonPrecompile::precompile(spec).unwrap(); + let precompile = Precompiles::new(spec); + let mut executor = StackExecutor::new_with_precompiles( + executor_state, + &gasometer_config, + &precompile, + ); + executor.state_mut().withdraw(caller, total_fee).unwrap(); + + let access_list = test.transaction.get_access_list(state); + + // EIP-3607: Reject transactions from senders with deployed code + // EIP-7702: Accept transaction even if the caller has code. + if caller_code.is_empty() || is_delegated { + let value = test.transaction.get_value(state); + if let Some(to) = test.transaction.to { + // Exit reason for the call is not analyzed as it mostly does not expect exceptions + let _reason = executor.transact_call( + caller, + to, + value, + data, + gas_limit, + access_list.clone(), + authorization_list.clone(), + ); + assert_call_exit_exception( + state.expect_exception.as_ref(), + &test_config.name, + ); + } else { + let code = data; + + let reason = + executor.transact_create(caller, value, code, gas_limit, access_list); + if check_create_exit_reason(&reason.0, state.expect_exception.as_ref(), "") + { + continue; + } + } + } else { + // According to EIP7702 - https://eips.ethereum.org/EIPS/eip-7702#transaction-origination: + // allow EOAs whose code is a valid delegation designation, i.e. `0xef0100 || address`, + // to continue to originate transactions. + #[allow(clippy::collapsible_if)] + if !(*spec >= Spec::Prague + && TxType::from_tx_bytes(&state.tx_bytes) == TxType::EOAAccountCode) + { + assert_empty_create_caller( + state.expect_exception.as_ref(), + &test_config.name, + ); + } + } + + let used_gas = executor.used_gas(); + let actual_fee = executor.fee(vicinity.effective_gas_price); + // Forks after London burn miner rewards and thus have different gas fee + // calculation (see EIP-1559) + let miner_reward = if *spec > Spec::Berlin { + let coinbase_gas_price = vicinity + .effective_gas_price + .saturating_sub(vicinity.block_base_fee_per_gas); + executor.fee(coinbase_gas_price) + } else { + actual_fee + }; + + executor + .state_mut() + .deposit(vicinity.block_coinbase, miner_reward); + + let amount_to_return_for_caller = data_fee.map_or_else( + || total_fee - actual_fee, + |data_fee| total_fee - actual_fee - data_fee, + ); + executor + .state_mut() + .deposit(caller, amount_to_return_for_caller); + + let (values, logs) = executor.into_state().deconstruct(); + + backend.apply(values, logs, true); + // It's a special case for hard forks: London and before, + // According to EIP-160, an empty account should be removed. But in that particular test - original test state + // contains account 0x03 (it's a precompile), and when precompile 0x03 was called it exit with + // OutOfGas result. And after exit of the substate, the account is not marked as touched, as exit reason + // is not a success. And it means that it doesn't appear in Apply::Modify, then as untouched it + // can't be removed by the backend.apply event. In that particular case we should manage it manually. + // NOTE: it's not realistic situation for real life flow. + if *spec <= Spec::London && test_config.name == "failed_tx_xcf416c53" { + let state = backend.state_mut(); + state.retain(|addr, account| { + // Check if the account is empty for the precompile `0x03` + !(addr.to_low_u64_be() == 3 + && account.balance == U256_ZERO + && account.nonce == U256_ZERO + && account.code.is_empty()) + }); + } + + let backend_state = MemoryAccountsState(backend.state().clone()); + let (is_valid_hash, actual_hash) = backend_state.check_valid_hash(&state.hash); + results.push(ExecutionResult { + used_gas, + is_valid_hash, + actual_hash: actual_hash.to_string(), + }); + } + } + } + + let output = serde_json::to_string_pretty(&results).unwrap(); + + // write public output to the journal + env::commit(&output); +} diff --git a/zk-evm/methods/src/lib.rs b/zk-evm/methods/src/lib.rs new file mode 100644 index 00000000..1bdb3085 --- /dev/null +++ b/zk-evm/methods/src/lib.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/methods.rs")); diff --git a/zk-evm/rust-toolchain.toml b/zk-evm/rust-toolchain.toml new file mode 100644 index 00000000..36614c30 --- /dev/null +++ b/zk-evm/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "stable" +components = ["rustfmt", "rust-src"] +profile = "minimal" diff --git a/zk-evm/src/main.rs b/zk-evm/src/main.rs new file mode 100644 index 00000000..5ac934e4 --- /dev/null +++ b/zk-evm/src/main.rs @@ -0,0 +1,76 @@ +use aurora_evm_jsontests::types::StateTestCase; +use clap::{arg, command, value_parser}; +use risc0_zkvm::{default_prover, ExecutorEnv, Receipt}; + +use std::collections::HashMap; +use std::fs::File; +use std::io::BufReader; +use std::path::{Path, PathBuf}; + +// These constants represent the RISC-V ELF and the image ID generated by risc0-build. +// The ELF is used for proving and the ID is used for verification. +use methods::{ZK_EVM_ELF, ZK_EVM_ID}; + +fn main() { + // Initialize tracing. In order to view logs, run `RUST_LOG=info cargo run` + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env()) + .init(); + + let input = run_cli(); + let (receipt, _) = execute_evm(input); + + // The receipt was verified at the end of proving, but the below code is an + // example of how someone else could verify this receipt. + receipt.verify(ZK_EVM_ID).unwrap(); +} + +fn execute_evm(tests_suit: String) -> (Receipt, String) { + let env = ExecutorEnv::builder() + .write(&tests_suit) + .unwrap() + .build() + .unwrap(); + + // Obtain the default prover. + let prover = default_prover(); + + // Produce a receipt by proving the specified ELF binary. + let receipt = prover.prove(env, ZK_EVM_ELF).unwrap().receipt; + let output = receipt.journal.decode().unwrap(); + (receipt, output) +} + +fn get_tests_suite(file_name: &Path) -> String { + let file = File::open(file_name).expect("Open file failed"); + let reader = BufReader::new(file); + + // We parse the JSON test cases to verify is it deserialized correctly + let _ = serde_json::from_reader::<_, HashMap>(reader) + .expect("Parse test cases failed"); + std::fs::read_to_string(file_name).unwrap() +} + +fn run_cli() -> String { + let matches = command!() + .version(env!("CARGO_PKG_VERSION")) + .arg( + arg!( "JSON file for tests run") + .value_parser(value_parser!(PathBuf)) + .required(true), + ) + .get_matches(); + + let src_name = matches.get_one::("PATH").expect("PATH required"); + + let path = Path::new(src_name); + + assert!( + path.exists(), + "data source does not exist: {}", + path.display() + ); + assert!(path.is_file(), "Path is not a file: {}", path.display()); + + get_tests_suite(path) +}