diff --git a/.gitignore b/.gitignore index d0a4bbbc2..3ee1c729c 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,5 @@ jwt.hex tests_v3.0.0.tar.gz .env + +levm_ef_tests_report.txt diff --git a/cmd/ef_tests/levm/Cargo.toml b/cmd/ef_tests/levm/Cargo.toml index 51504dbc0..6a553046e 100644 --- a/cmd/ef_tests/levm/Cargo.toml +++ b/cmd/ef_tests/levm/Cargo.toml @@ -17,6 +17,17 @@ hex.workspace = true keccak-hash = "0.11.0" colored = "2.1.0" spinoff = "0.8.0" +thiserror = "2.0.3" +clap = { version = "4.3", features = ["derive"] } +clap_complete = "4.5.17" + +revm = { version = "14.0.3", features = [ + "serde", + "std", + "serde-json", + "optional_no_base_fee", + "optional_block_gas_limit", +], default-features = false } [dev-dependencies] hex = "0.4.3" @@ -25,5 +36,5 @@ hex = "0.4.3" path = "./ef_tests.rs" [[test]] -name = "test" +name = "ef_tests_levm" harness = false diff --git a/cmd/ef_tests/levm/deserialize.rs b/cmd/ef_tests/levm/deserialize.rs index b81f4208b..612e17045 100644 --- a/cmd/ef_tests/levm/deserialize.rs +++ b/cmd/ef_tests/levm/deserialize.rs @@ -1,4 +1,4 @@ -use crate::types::EFTest; +use crate::types::{EFTest, EFTests}; use bytes::Bytes; use ethrex_core::U256; use serde::Deserialize; @@ -122,103 +122,104 @@ where .collect() } -impl<'de> Deserialize<'de> for EFTest { +impl<'de> Deserialize<'de> for EFTests { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { + let mut ef_tests = Vec::new(); let aux: HashMap> = HashMap::deserialize(deserializer)?; - let test_name = aux - .keys() - .next() - .ok_or(serde::de::Error::missing_field("test name"))?; - let test_data = aux - .get(test_name) - .ok_or(serde::de::Error::missing_field("test data value"))?; - let raw_tx: EFTestRawTransaction = serde_json::from_value( - test_data - .get("transaction") - .ok_or(serde::de::Error::missing_field("transaction"))? - .clone(), - ) - .map_err(|err| { - serde::de::Error::custom(format!( - "error deserializing test \"{test_name}\", \"transaction\" field: {err}" - )) - })?; - - let mut transactions = Vec::new(); + for test_name in aux.keys() { + let test_data = aux + .get(test_name) + .ok_or(serde::de::Error::missing_field("test data value"))?; - // Note that inthis order of iteration, in an example tx with 2 datas, 2 gasLimit and 2 values, order would be - // 111, 112, 121, 122, 211, 212, 221, 222 - for (data_id, data) in raw_tx.data.iter().enumerate() { - for (gas_limit_id, gas_limit) in raw_tx.gas_limit.iter().enumerate() { - for (value_id, value) in raw_tx.value.iter().enumerate() { - let tx = EFTestTransaction { - data: data.clone(), - gas_limit: *gas_limit, - gas_price: raw_tx.gas_price, - nonce: raw_tx.nonce, - secret_key: raw_tx.secret_key, - sender: raw_tx.sender, - to: raw_tx.to.clone(), - value: *value, - }; - transactions.push(((data_id, gas_limit_id, value_id), tx)); - } - } - } - - let ef_test = Self { - name: test_name.to_owned().to_owned(), - _info: serde_json::from_value( - test_data - .get("_info") - .ok_or(serde::de::Error::missing_field("_info"))? - .clone(), - ) - .map_err(|err| { - serde::de::Error::custom(format!( - "error deserializing test \"{test_name}\", \"_info\" field: {err}" - )) - })?, - env: serde_json::from_value( - test_data - .get("env") - .ok_or(serde::de::Error::missing_field("env"))? - .clone(), - ) - .map_err(|err| { - serde::de::Error::custom(format!( - "error deserializing test \"{test_name}\", \"env\" field: {err}" - )) - })?, - post: serde_json::from_value( - test_data - .get("post") - .ok_or(serde::de::Error::missing_field("post"))? - .clone(), - ) - .map_err(|err| { - serde::de::Error::custom(format!( - "error deserializing test \"{test_name}\", \"post\" field: {err}" - )) - })?, - pre: serde_json::from_value( + let raw_tx: EFTestRawTransaction = serde_json::from_value( test_data - .get("pre") - .ok_or(serde::de::Error::missing_field("pre"))? + .get("transaction") + .ok_or(serde::de::Error::missing_field("transaction"))? .clone(), ) .map_err(|err| { serde::de::Error::custom(format!( - "error deserializing test \"{test_name}\", \"pre\" field: {err}" + "error deserializing test \"{test_name}\", \"transaction\" field: {err}" )) - })?, - transactions, - }; - Ok(ef_test) + })?; + + let mut transactions = HashMap::new(); + + // Note that inthis order of iteration, in an example tx with 2 datas, 2 gasLimit and 2 values, order would be + // 111, 112, 121, 122, 211, 212, 221, 222 + for (data_id, data) in raw_tx.data.iter().enumerate() { + for (gas_limit_id, gas_limit) in raw_tx.gas_limit.iter().enumerate() { + for (value_id, value) in raw_tx.value.iter().enumerate() { + let tx = EFTestTransaction { + data: data.clone(), + gas_limit: *gas_limit, + gas_price: raw_tx.gas_price, + nonce: raw_tx.nonce, + secret_key: raw_tx.secret_key, + sender: raw_tx.sender, + to: raw_tx.to.clone(), + value: *value, + }; + transactions.insert((data_id, gas_limit_id, value_id), tx); + } + } + } + + let ef_test = EFTest { + name: test_name.to_owned().to_owned(), + _info: serde_json::from_value( + test_data + .get("_info") + .ok_or(serde::de::Error::missing_field("_info"))? + .clone(), + ) + .map_err(|err| { + serde::de::Error::custom(format!( + "error deserializing test \"{test_name}\", \"_info\" field: {err}" + )) + })?, + env: serde_json::from_value( + test_data + .get("env") + .ok_or(serde::de::Error::missing_field("env"))? + .clone(), + ) + .map_err(|err| { + serde::de::Error::custom(format!( + "error deserializing test \"{test_name}\", \"env\" field: {err}" + )) + })?, + post: serde_json::from_value( + test_data + .get("post") + .ok_or(serde::de::Error::missing_field("post"))? + .clone(), + ) + .map_err(|err| { + serde::de::Error::custom(format!( + "error deserializing test \"{test_name}\", \"post\" field: {err}" + )) + })?, + pre: serde_json::from_value( + test_data + .get("pre") + .ok_or(serde::de::Error::missing_field("pre"))? + .clone(), + ) + .map_err(|err| { + serde::de::Error::custom(format!( + "error deserializing test \"{test_name}\", \"pre\" field: {err}" + )) + })?, + transactions, + }; + ef_tests.push(ef_test); + } + Ok(Self(ef_tests)) } } diff --git a/cmd/ef_tests/levm/ef_tests.rs b/cmd/ef_tests/levm/ef_tests.rs index 74e2ea7d5..27e09b709 100644 --- a/cmd/ef_tests/levm/ef_tests.rs +++ b/cmd/ef_tests/levm/ef_tests.rs @@ -1,5 +1,6 @@ mod deserialize; +pub mod parser; mod report; pub mod runner; -mod types; +pub mod types; mod utils; diff --git a/cmd/ef_tests/levm/parser.rs b/cmd/ef_tests/levm/parser.rs new file mode 100644 index 000000000..6cfb23f51 --- /dev/null +++ b/cmd/ef_tests/levm/parser.rs @@ -0,0 +1,112 @@ +use crate::{ + report::format_duration_as_mm_ss, + runner::EFTestRunnerOptions, + types::{EFTest, EFTests}, +}; +use colored::Colorize; +use spinoff::{spinners::Dots, Color, Spinner}; +use std::fs::DirEntry; + +#[derive(Debug, thiserror::Error)] +pub enum EFTestParseError { + #[error("Failed to read directory: {0}")] + FailedToReadDirectory(String), + #[error("Failed to read file: {0}")] + FailedToReadFile(String), + #[error("Failed to get file type: {0}")] + FailedToGetFileType(String), + #[error("Failed to parse test file: {0}")] + FailedToParseTestFile(String), +} + +pub fn parse_ef_tests(opts: &EFTestRunnerOptions) -> Result, EFTestParseError> { + let parsing_time = std::time::Instant::now(); + let cargo_manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let ef_general_state_tests_path = cargo_manifest_dir.join("vectors/GeneralStateTests"); + let mut spinner = Spinner::new(Dots, "Parsing EF Tests".bold().to_string(), Color::Cyan); + let mut tests = Vec::new(); + for test_dir in std::fs::read_dir(ef_general_state_tests_path.clone()) + .map_err(|err| { + EFTestParseError::FailedToReadDirectory(format!( + "{:?}: {err}", + ef_general_state_tests_path.file_name() + )) + })? + .flatten() + { + let directory_tests = parse_ef_test_dir(test_dir, opts, &mut spinner)?; + tests.extend(directory_tests); + } + spinner.success( + &format!( + "Parsed EF Tests in {}", + format_duration_as_mm_ss(parsing_time.elapsed()) + ) + .bold(), + ); + Ok(tests) +} + +pub fn parse_ef_test_dir( + test_dir: DirEntry, + opts: &EFTestRunnerOptions, + directory_parsing_spinner: &mut Spinner, +) -> Result, EFTestParseError> { + directory_parsing_spinner.update_text(format!("Parsing directory {:?}", test_dir.file_name())); + + let mut directory_tests = Vec::new(); + for test in std::fs::read_dir(test_dir.path()) + .map_err(|err| { + EFTestParseError::FailedToReadDirectory(format!("{:?}: {err}", test_dir.file_name())) + })? + .flatten() + { + if test + .file_type() + .map_err(|err| { + EFTestParseError::FailedToGetFileType(format!("{:?}: {err}", test.file_name())) + })? + .is_dir() + { + let sub_directory_tests = parse_ef_test_dir(test, opts, directory_parsing_spinner)?; + directory_tests.extend(sub_directory_tests); + continue; + } + // Skip non-JSON files. + if test.path().extension().is_some_and(|ext| ext != "json") + | test.path().extension().is_none() + { + continue; + } + // Skip the ValueOverflowParis.json file. + if test + .path() + .file_name() + .is_some_and(|name| name == "ValueOverflowParis.json") + { + continue; + } + + // Skip tests that are not in the list of tests to run. + if !opts.tests.is_empty() + && !opts + .tests + .contains(&test_dir.file_name().to_str().unwrap().to_owned()) + { + directory_parsing_spinner.update_text(format!( + "Skipping test {:?} as it is not in the list of tests to run", + test.path().file_name() + )); + return Ok(Vec::new()); + } + + let test_file = std::fs::File::open(test.path()).map_err(|err| { + EFTestParseError::FailedToReadFile(format!("{:?}: {err}", test.path())) + })?; + let test: EFTests = serde_json::from_reader(test_file).map_err(|err| { + EFTestParseError::FailedToParseTestFile(format!("{:?} parse error: {err}", test.path())) + })?; + directory_tests.extend(test.0); + } + Ok(directory_tests) +} diff --git a/cmd/ef_tests/levm/report.rs b/cmd/ef_tests/levm/report.rs index 012af8b01..9a90a1521 100644 --- a/cmd/ef_tests/levm/report.rs +++ b/cmd/ef_tests/levm/report.rs @@ -1,101 +1,573 @@ -// Note: I use this to do not affect the EF tests logic with this side effects -// The cost to add this would be to return a Result<(), InternalError> in EFTestsReport methods - +use crate::runner::{EFTestRunnerError, InternalError}; use colored::Colorize; -use std::{collections::HashMap, fmt}; - -#[derive(Debug, Default)] -pub struct EFTestsReport { - group_passed: u64, - group_failed: u64, - group_run: u64, - test_reports: HashMap, - passed_tests: Vec, - failed_tests: Vec<(String, (usize, usize, usize), String)>, +use ethrex_core::{Address, H256}; +use ethrex_levm::errors::{TransactionReport, TxResult, VMError}; +use ethrex_storage::{error::StoreError, AccountUpdate}; +use ethrex_vm::SpecId; +use revm::primitives::{EVMError, ExecutionResult as RevmExecutionResult}; +use serde::{Deserialize, Serialize}; +use spinoff::{spinners::Dots, Color, Spinner}; +use std::{ + collections::{HashMap, HashSet}, + fmt::{self, Display}, + path::PathBuf, + time::Duration, +}; + +pub type TestVector = (usize, usize, usize); + +pub fn progress(reports: &[EFTestReport], time: Duration) -> String { + format!( + "{}: {} {} {} - {}", + "Ethereum Foundation Tests".bold(), + format!( + "{} passed", + reports.iter().filter(|report| report.passed()).count() + ) + .green() + .bold(), + format!( + "{} failed", + reports.iter().filter(|report| !report.passed()).count() + ) + .red() + .bold(), + format!("{} total run", reports.len()).blue().bold(), + format_duration_as_mm_ss(time) + ) +} +pub fn summary(reports: &[EFTestReport]) -> String { + let total_passed = reports.iter().filter(|report| report.passed()).count(); + let total_run = reports.len(); + format!( + "{} {}/{total_run}\n\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n", + "Summary:".bold(), + if total_passed == total_run { + format!("{}", total_passed).green() + } else if total_passed > 0 { + format!("{}", total_passed).yellow() + } else { + format!("{}", total_passed).red() + }, + fork_summary(reports, SpecId::CANCUN), + fork_summary(reports, SpecId::SHANGHAI), + fork_summary(reports, SpecId::HOMESTEAD), + fork_summary(reports, SpecId::ISTANBUL), + fork_summary(reports, SpecId::LONDON), + fork_summary(reports, SpecId::BYZANTIUM), + fork_summary(reports, SpecId::BERLIN), + fork_summary(reports, SpecId::CONSTANTINOPLE), + fork_summary(reports, SpecId::MERGE), + fork_summary(reports, SpecId::FRONTIER), + ) +} + +pub fn write(reports: &[EFTestReport]) -> Result { + let report_file_path = PathBuf::from("./levm_ef_tests_report.txt"); + let failed_test_reports = EFTestsReport( + reports + .iter() + .filter(|&report| !report.passed()) + .cloned() + .collect(), + ); + std::fs::write( + "./levm_ef_tests_report.txt", + failed_test_reports.to_string(), + ) + .map_err(|err| { + EFTestRunnerError::Internal(InternalError::MainRunnerInternal(format!( + "Failed to write report to file: {err}" + ))) + })?; + Ok(report_file_path) +} + +pub const EF_TESTS_CACHE_FILE_PATH: &str = "./levm_ef_tests_cache.json"; + +pub fn cache(reports: &[EFTestReport]) -> Result { + let cache_file_path = PathBuf::from(EF_TESTS_CACHE_FILE_PATH); + let cache = serde_json::to_string_pretty(&reports).map_err(|err| { + EFTestRunnerError::Internal(InternalError::MainRunnerInternal(format!( + "Failed to serialize cache: {err}" + ))) + })?; + std::fs::write(&cache_file_path, cache).map_err(|err| { + EFTestRunnerError::Internal(InternalError::MainRunnerInternal(format!( + "Failed to write cache to file: {err}" + ))) + })?; + Ok(cache_file_path) +} + +pub fn load() -> Result, EFTestRunnerError> { + let mut reports_loading_spinner = + Spinner::new(Dots, "Loading reports...".to_owned(), Color::Cyan); + match std::fs::read_to_string(EF_TESTS_CACHE_FILE_PATH).ok() { + Some(cache) => { + reports_loading_spinner.success("Reports loaded"); + serde_json::from_str(&cache).map_err(|err| { + EFTestRunnerError::Internal(InternalError::MainRunnerInternal(format!( + "Cache exists but there was an error loading it: {err}" + ))) + }) + } + None => { + reports_loading_spinner.success("No cache found"); + Ok(Vec::default()) + } + } +} + +pub fn format_duration_as_mm_ss(duration: Duration) -> String { + let total_seconds = duration.as_secs(); + let minutes = total_seconds / 60; + let seconds = total_seconds % 60; + format!("{minutes:02}:{seconds:02}") } #[derive(Debug, Default, Clone)] +pub struct EFTestsReport(pub Vec); + +impl Display for EFTestsReport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let total_passed = self.0.iter().filter(|report| report.passed()).count(); + let total_run = self.0.len(); + writeln!( + f, + "{} {}/{total_run}", + "Summary:".bold(), + if total_passed == total_run { + format!("{}", total_passed).green() + } else if total_passed > 0 { + format!("{}", total_passed).yellow() + } else { + format!("{}", total_passed).red() + }, + )?; + writeln!(f)?; + writeln!(f, "{}", fork_summary(&self.0, SpecId::CANCUN))?; + writeln!(f, "{}", fork_summary(&self.0, SpecId::SHANGHAI))?; + writeln!(f, "{}", fork_summary(&self.0, SpecId::HOMESTEAD))?; + writeln!(f, "{}", fork_summary(&self.0, SpecId::ISTANBUL))?; + writeln!(f, "{}", fork_summary(&self.0, SpecId::LONDON))?; + writeln!(f, "{}", fork_summary(&self.0, SpecId::BYZANTIUM))?; + writeln!(f, "{}", fork_summary(&self.0, SpecId::BERLIN))?; + writeln!(f, "{}", fork_summary(&self.0, SpecId::CONSTANTINOPLE))?; + writeln!(f, "{}", fork_summary(&self.0, SpecId::MERGE))?; + writeln!(f, "{}", fork_summary(&self.0, SpecId::FRONTIER))?; + writeln!(f)?; + writeln!(f, "{}", "Failed tests:".bold())?; + writeln!(f)?; + for report in self.0.iter() { + if report.failed_vectors.is_empty() { + continue; + } + writeln!(f, "{}", format!("Test: {}", report.name).bold())?; + writeln!(f)?; + for (failed_vector, error) in &report.failed_vectors { + writeln!( + f, + "{} (data_index: {}, gas_limit_index: {}, value_index: {})", + "Vector:".bold(), + failed_vector.0, + failed_vector.1, + failed_vector.2 + )?; + writeln!(f, "{} {}", "Error:".bold(), error.to_string().red())?; + if let Some(re_run_report) = &report.re_run_report { + if let Some(execution_report) = + re_run_report.execution_report.get(failed_vector) + { + if let Some((levm_result, revm_result)) = + &execution_report.execution_result_mismatch + { + writeln!( + f, + "{}: LEVM: {levm_result:?}, REVM: {revm_result:?}", + "Execution result mismatch".bold() + )?; + } + if let Some((levm_gas_used, revm_gas_used)) = + &execution_report.gas_used_mismatch + { + writeln!( + f, + "{}: LEVM: {levm_gas_used}, REVM: {revm_gas_used} (diff: {})", + "Gas used mismatch".bold(), + levm_gas_used.abs_diff(*revm_gas_used) + )?; + } + if let Some((levm_gas_refunded, revm_gas_refunded)) = + &execution_report.gas_refunded_mismatch + { + writeln!( + f, + "{}: LEVM: {levm_gas_refunded}, REVM: {revm_gas_refunded} (diff: {})", + "Gas refunded mismatch".bold(), levm_gas_refunded.abs_diff(*revm_gas_refunded) + )?; + } + if let Some((levm_result, revm_error)) = &execution_report.re_runner_error { + writeln!( + f, + "{}: LEVM: {levm_result:?}, REVM: {revm_error}", + "Re-run error".bold() + )?; + } + } + + if let Some(account_update) = + re_run_report.account_updates_report.get(failed_vector) + { + writeln!(f, "{}", &account_update.to_string())?; + } else { + writeln!(f, "No account updates report found. Account update reports are only generated for tests that failed at the post-state validation stage.")?; + } + } else { + writeln!(f, "No re-run report found. Re-run reports are only generated for tests that failed at the post-state validation stage.")?; + } + writeln!(f)?; + } + } + Ok(()) + } +} + +fn fork_summary(reports: &[EFTestReport], fork: SpecId) -> String { + let fork_str: &str = fork.into(); + let fork_tests = reports.iter().filter(|report| report.fork == fork).count(); + let fork_passed_tests = reports + .iter() + .filter(|report| report.fork == fork && report.passed()) + .count(); + format!( + "{}: {}/{fork_tests}", + fork_str.bold(), + if fork_passed_tests == fork_tests { + format!("{}", fork_passed_tests).green() + } else if fork_passed_tests > 0 { + format!("{}", fork_passed_tests).yellow() + } else { + format!("{}", fork_passed_tests).red() + }, + ) +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct EFTestReport { - passed: u64, - failed: u64, - run: u64, - failed_tests: Vec<((usize, usize, usize), String)>, + pub name: String, + pub test_hash: H256, + pub fork: SpecId, + pub skipped: bool, + pub failed_vectors: HashMap, + pub re_run_report: Option, } -impl EFTestsReport { - pub fn register_pass(&mut self, test_name: &str) { - self.passed_tests.push(test_name.to_string()); +impl EFTestReport { + pub fn new(name: String, test_hash: H256, fork: SpecId) -> Self { + EFTestReport { + name, + test_hash, + fork, + ..Default::default() + } + } - let report = self.test_reports.entry(test_name.to_string()).or_default(); - report.passed += 1; - report.run += 1; + pub fn new_skipped() -> Self { + EFTestReport { + skipped: true, + ..Default::default() + } } - pub fn register_fail( + pub fn register_unexpected_execution_failure( &mut self, - tx_indexes: (usize, usize, usize), - test_name: &str, - reason: &str, + error: VMError, + failed_vector: TestVector, ) { - self.failed_tests - .push((test_name.to_owned(), tx_indexes, reason.to_owned())); + self.failed_vectors.insert( + failed_vector, + EFTestRunnerError::ExecutionFailedUnexpectedly(error), + ); + } - let report = self.test_reports.entry(test_name.to_string()).or_default(); - report.failed += 1; - report.failed_tests.push((tx_indexes, reason.to_owned())); - report.run += 1; + pub fn register_vm_initialization_failure( + &mut self, + reason: String, + failed_vector: TestVector, + ) { + self.failed_vectors.insert( + failed_vector, + EFTestRunnerError::VMInitializationFailed(reason), + ); } - pub fn register_group_pass(&mut self) { - self.group_passed += 1; - self.group_run += 1; + pub fn register_pre_state_validation_failure( + &mut self, + reason: String, + failed_vector: TestVector, + ) { + self.failed_vectors.insert( + failed_vector, + EFTestRunnerError::FailedToEnsurePreState(reason), + ); } - pub fn register_group_fail(&mut self) { - self.group_failed += 1; - self.group_run += 1; + pub fn register_post_state_validation_failure( + &mut self, + transaction_report: TransactionReport, + reason: String, + failed_vector: TestVector, + ) { + self.failed_vectors.insert( + failed_vector, + EFTestRunnerError::FailedToEnsurePostState(transaction_report, reason), + ); } - pub fn progress(&self) -> String { - format!( - "{}: {} {} {}", - "Ethereum Foundation Tests Run".bold(), - format!("{} passed", self.group_passed).green().bold(), - format!("{} failed", self.group_failed).red().bold(), - format!("{} total run", self.group_run).blue().bold(), - ) + pub fn register_re_run_report(&mut self, re_run_report: TestReRunReport) { + self.re_run_report = Some(re_run_report); + } + + pub fn iter_failed(&self) -> impl Iterator { + self.failed_vectors.iter() + } + + pub fn passed(&self) -> bool { + self.failed_vectors.is_empty() } } -impl fmt::Display for EFTestsReport { +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct AccountUpdatesReport { + pub levm_account_updates: Vec, + pub revm_account_updates: Vec, + pub levm_updated_accounts_only: HashSet
, + pub revm_updated_accounts_only: HashSet
, + pub shared_updated_accounts: HashSet
, +} + +impl fmt::Display for AccountUpdatesReport { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "Report:")?; - writeln!(f, "Total results: {}", self.progress())?; - for (test_name, report) in self.test_reports.clone() { - if report.failed == 0 { - continue; - } - writeln!(f)?; + writeln!(f, "Account Updates:")?; + for levm_updated_account_only in self.levm_updated_accounts_only.iter() { + writeln!(f, " {levm_updated_account_only:#x}:")?; + writeln!(f, "{}", " Was updated in LEVM but not in REVM".red())?; + } + for revm_updated_account_only in self.revm_updated_accounts_only.iter() { + writeln!(f, " {revm_updated_account_only:#x}:")?; + writeln!(f, "{}", " Was updated in REVM but not in LEVM".red())?; + } + for shared_updated_account in self.shared_updated_accounts.iter() { + writeln!(f, " {shared_updated_account:#x}:")?; + writeln!( f, - "Test results for {}: {} {} {}", - test_name, - format!("{} passed", report.passed).green().bold(), - format!("{} failed", report.failed).red().bold(), - format!("{} run", report.run).blue().bold(), + "{}", + " Was updated in both LEVM and REVM".to_string().green() )?; - for failing_test in report.failed_tests.clone() { - writeln!( - f, - "(data_index: {}, gas_limit_index: {}, value_index: {}). Err: {}", - failing_test.0 .0, - failing_test.0 .1, - failing_test.0 .2, - failing_test.1.bright_red().bold() - )?; + + let levm_updated_account = self + .levm_account_updates + .iter() + .find(|account_update| &account_update.address == shared_updated_account) + .unwrap(); + let revm_updated_account = self + .revm_account_updates + .iter() + .find(|account_update| &account_update.address == shared_updated_account) + .unwrap(); + + match (levm_updated_account.removed, revm_updated_account.removed) { + (true, false) => { + writeln!( + f, + "{}", + " Removed in LEVM but not in REVM".to_string().red() + )?; + } + (false, true) => { + writeln!( + f, + "{}", + " Removed in REVM but not in LEVM".to_string().red() + )?; + } + // Account was removed in both VMs. + (false, false) | (true, true) => {} + } + + match (&levm_updated_account.code, &revm_updated_account.code) { + (None, Some(_)) => { + writeln!( + f, + "{}", + " Has code in REVM but not in LEVM".to_string().red() + )?; + } + (Some(_), None) => { + writeln!( + f, + "{}", + " Has code in LEVM but not in REVM".to_string().red() + )?; + } + (Some(levm_account_code), Some(revm_account_code)) => { + if levm_account_code != revm_account_code { + writeln!(f, + "{}", + format!( + " Code mismatch: LEVM: {levm_account_code}, REVM: {revm_account_code}", + levm_account_code = hex::encode(levm_account_code), + revm_account_code = hex::encode(revm_account_code) + ) + .red() + )?; + } + } + (None, None) => {} } - } + match (&levm_updated_account.info, &revm_updated_account.info) { + (None, Some(_)) => { + writeln!( + f, + "{}", + format!(" Account {shared_updated_account:#x} has info in REVM but not in LEVM",) + .red() + .bold() + )?; + } + (Some(levm_account_info), Some(revm_account_info)) => { + if levm_account_info.balance != revm_account_info.balance { + writeln!(f, + "{}", + format!( + " Balance mismatch: LEVM: {levm_account_balance}, REVM: {revm_account_balance}", + levm_account_balance = levm_account_info.balance, + revm_account_balance = revm_account_info.balance + ) + .red() + .bold() + )?; + } + if levm_account_info.nonce != revm_account_info.nonce { + writeln!(f, + "{}", + format!( + " Nonce mismatch: LEVM: {levm_account_nonce}, REVM: {revm_account_nonce}", + levm_account_nonce = levm_account_info.nonce, + revm_account_nonce = revm_account_info.nonce + ) + .red() + .bold() + )?; + } + } + // We ignore the case (Some(_), None) because we always add the account info to the account update. + (Some(_), None) | (None, None) => {} + } + } Ok(()) } } + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct TestReRunExecutionReport { + pub execution_result_mismatch: Option<(TxResult, RevmExecutionResult)>, + pub gas_used_mismatch: Option<(u64, u64)>, + pub gas_refunded_mismatch: Option<(u64, u64)>, + pub re_runner_error: Option<(TxResult, String)>, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct TestReRunReport { + pub execution_report: HashMap, + pub account_updates_report: HashMap, +} + +impl TestReRunReport { + pub fn new() -> Self { + Self::default() + } + + pub fn register_execution_result_mismatch( + &mut self, + vector: TestVector, + levm_result: TxResult, + revm_result: RevmExecutionResult, + ) { + let value = Some((levm_result, revm_result)); + self.execution_report + .entry(vector) + .and_modify(|report| { + report.execution_result_mismatch = value.clone(); + }) + .or_insert(TestReRunExecutionReport { + execution_result_mismatch: value, + ..Default::default() + }); + } + + pub fn register_gas_used_mismatch( + &mut self, + vector: TestVector, + levm_gas_used: u64, + revm_gas_used: u64, + ) { + let value = Some((levm_gas_used, revm_gas_used)); + self.execution_report + .entry(vector) + .and_modify(|report| { + report.gas_used_mismatch = value; + }) + .or_insert(TestReRunExecutionReport { + gas_used_mismatch: value, + ..Default::default() + }); + } + + pub fn register_gas_refunded_mismatch( + &mut self, + vector: TestVector, + levm_gas_refunded: u64, + revm_gas_refunded: u64, + ) { + let value = Some((levm_gas_refunded, revm_gas_refunded)); + self.execution_report + .entry(vector) + .and_modify(|report| { + report.gas_refunded_mismatch = value; + }) + .or_insert(TestReRunExecutionReport { + gas_refunded_mismatch: value, + ..Default::default() + }); + } + + pub fn register_account_updates_report( + &mut self, + vector: TestVector, + report: AccountUpdatesReport, + ) { + self.account_updates_report.insert(vector, report); + } + + pub fn register_re_run_failure( + &mut self, + vector: TestVector, + levm_result: TxResult, + revm_error: EVMError, + ) { + let value = Some((levm_result, revm_error.to_string())); + self.execution_report + .entry(vector) + .and_modify(|report| { + report.re_runner_error = value.clone(); + }) + .or_insert(TestReRunExecutionReport { + re_runner_error: value, + ..Default::default() + }); + } +} diff --git a/cmd/ef_tests/levm/runner.rs b/cmd/ef_tests/levm/runner.rs deleted file mode 100644 index d3b4eaa95..000000000 --- a/cmd/ef_tests/levm/runner.rs +++ /dev/null @@ -1,289 +0,0 @@ -use crate::{report::EFTestsReport, types::EFTest, utils}; -use ethrex_core::{ - types::{code_hash, AccountInfo}, - H256, U256, -}; -use ethrex_levm::{ - db::Cache, - errors::{TransactionReport, VMError}, - vm::VM, - Environment, -}; -use ethrex_storage::AccountUpdate; -use ethrex_vm::db::StoreWrapper; -use keccak_hash::keccak; -use spinoff::{spinners::Dots, Color, Spinner}; -use std::{collections::HashMap, error::Error, sync::Arc}; - -pub fn run_ef_tests() -> Result> { - let mut report = EFTestsReport::default(); - let cargo_manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let ef_general_state_tests_path = cargo_manifest_dir.join("vectors/GeneralStateTests"); - let mut spinner = Spinner::new(Dots, report.progress(), Color::Cyan); - for test_dir in std::fs::read_dir(ef_general_state_tests_path)?.flatten() { - for test in std::fs::read_dir(test_dir.path())? - .flatten() - .filter(|entry| { - entry - .path() - .extension() - .map(|ext| ext == "json") - .unwrap_or(false) - }) - { - // TODO: Figure out what to do with overflowed value: 0x10000000000000000000000000000000000000000000000000000000000000001. - // Deserialization fails because the value is too big for U256. - if test - .path() - .file_name() - .is_some_and(|name| name == "ValueOverflowParis.json") - { - continue; - } - let test_result = run_ef_test( - serde_json::from_reader(std::fs::File::open(test.path())?)?, - &mut report, - ); - if test_result.is_err() { - continue; - } - } - spinner.update_text(report.progress()); - } - spinner.success(&report.progress()); - let mut spinner = Spinner::new(Dots, "Loading report...".to_owned(), Color::Cyan); - spinner.success(&report.to_string()); - Ok(report) -} - -pub fn run_ef_test_tx( - tx_id: usize, - test: &EFTest, - report: &mut EFTestsReport, -) -> Result<(), Box> { - let mut evm = prepare_vm_for_tx(tx_id, test)?; - ensure_pre_state(&evm, test)?; - let execution_result = evm.transact(); - ensure_post_state(execution_result, test, report)?; - Ok(()) -} - -pub fn run_ef_test(test: EFTest, report: &mut EFTestsReport) -> Result<(), Box> { - let mut failed = false; - for (tx_id, (tx_indexes, _tx)) in test.transactions.iter().enumerate() { - match run_ef_test_tx(tx_id, &test, report) { - Ok(_) => {} - Err(e) => { - failed = true; - let error_message: &str = &e.to_string(); - report.register_fail(tx_indexes.to_owned(), &test.name, error_message); - } - } - } - if failed { - report.register_group_fail(); - } else { - report.register_group_pass(); - } - Ok(()) -} - -pub fn prepare_vm_for_tx(tx_id: usize, test: &EFTest) -> Result> { - let (initial_state, block_hash) = utils::load_initial_state(test); - let db = Arc::new(StoreWrapper { - store: initial_state.database().unwrap().clone(), - block_hash, - }); - let vm_result = VM::new( - test.transactions.get(tx_id).unwrap().1.to.clone(), - Environment { - origin: test.transactions.get(tx_id).unwrap().1.sender, - consumed_gas: U256::default(), - refunded_gas: U256::default(), - gas_limit: test.env.current_gas_limit, - block_number: test.env.current_number, - coinbase: test.env.current_coinbase, - timestamp: test.env.current_timestamp, - prev_randao: Some(test.env.current_random), - chain_id: U256::from(1729), - base_fee_per_gas: test.env.current_base_fee, - gas_price: test - .transactions - .get(tx_id) - .unwrap() - .1 - .gas_price - .unwrap_or_default(), // or max_fee_per_gas? - block_excess_blob_gas: Some(test.env.current_excess_blob_gas), - block_blob_gas_used: None, - tx_blob_hashes: None, - }, - test.transactions.get(tx_id).unwrap().1.value, - test.transactions.get(tx_id).unwrap().1.data.clone(), - db, - Cache::default(), - ); - - match vm_result { - Ok(vm) => Ok(vm), - Err(err) => { - let error_reason = format!("VM initialization failed: {err:?}"); - Err(error_reason.into()) - } - } -} - -pub fn ensure_pre_state(evm: &VM, test: &EFTest) -> Result<(), Box> { - let world_state = &evm.db; - for (address, pre_value) in &test.pre.0 { - let account = world_state.get_account_info(*address); - ensure_pre_state_condition( - account.nonce == pre_value.nonce.as_u64(), - format!( - "Nonce mismatch for account {:#x}: expected {}, got {}", - address, pre_value.nonce, account.nonce - ), - )?; - ensure_pre_state_condition( - account.balance == pre_value.balance, - format!( - "Balance mismatch for account {:#x}: expected {}, got {}", - address, pre_value.balance, account.balance - ), - )?; - for (k, v) in &pre_value.storage { - let mut key_bytes = [0u8; 32]; - k.to_big_endian(&mut key_bytes); - let storage_slot = world_state.get_storage_slot(*address, H256::from_slice(&key_bytes)); - ensure_pre_state_condition( - &storage_slot == v, - format!( - "Storage slot mismatch for account {:#x} at key {:?}: expected {}, got {}", - address, k, v, storage_slot - ), - )?; - } - ensure_pre_state_condition( - keccak(account.bytecode.clone()) == keccak(pre_value.code.as_ref()), - format!( - "Code hash mismatch for account {:#x}: expected {}, got {}", - address, - keccak(pre_value.code.as_ref()), - keccak(account.bytecode) - ), - )?; - } - Ok(()) -} - -fn ensure_pre_state_condition(condition: bool, error_reason: String) -> Result<(), Box> { - if !condition { - let error_reason = format!("Pre-state condition failed: {error_reason}"); - return Err(error_reason.into()); - } - Ok(()) -} - -pub fn ensure_post_state( - execution_result: Result, - test: &EFTest, - report: &mut EFTestsReport, -) -> Result<(), Box> { - match execution_result { - Ok(execution_report) => { - match test - .post - .clone() - .values() - .first() - .map(|v| v.clone().expect_exception) - { - // Execution result was successful but an exception was expected. - Some(Some(expected_exception)) => { - let error_reason = format!("Expected exception: {expected_exception}"); - return Err(format!("Post-state condition failed: {error_reason}").into()); - } - // Execution result was successful and no exception was expected. - // TODO: Check that the post-state matches the expected post-state. - None | Some(None) => { - let pos_state_root = post_state_root(execution_report, test); - let expected_post_state_value = test.post.iter().next().cloned(); - if let Some(expected_post_state_root_hash) = expected_post_state_value { - let expected_post_state_root_hash = expected_post_state_root_hash.hash; - if expected_post_state_root_hash != pos_state_root { - let error_reason = format!( - "Post-state root mismatch: expected {expected_post_state_root_hash:#x}, got {pos_state_root:#x}", - ); - return Err( - format!("Post-state condition failed: {error_reason}").into() - ); - } - } else { - let error_reason = "No post-state root hash provided"; - return Err(format!("Post-state condition failed: {error_reason}").into()); - } - } - } - } - Err(err) => { - match test - .post - .clone() - .values() - .first() - .map(|v| v.clone().expect_exception) - { - // Execution result was unsuccessful and an exception was expected. - // TODO: Check that the exception matches the expected exception. - Some(Some(_expected_exception)) => {} - // Execution result was unsuccessful but no exception was expected. - None | Some(None) => { - let error_reason = format!("Unexpected exception: {err:?}"); - return Err(format!("Post-state condition failed: {error_reason}").into()); - } - } - } - }; - report.register_pass(&test.name); - Ok(()) -} - -pub fn post_state_root(execution_report: TransactionReport, test: &EFTest) -> H256 { - let (initial_state, block_hash) = utils::load_initial_state(test); - - let mut account_updates: Vec = vec![]; - for (address, account) in execution_report.new_state { - let mut added_storage = HashMap::new(); - - for (key, value) in account.storage { - added_storage.insert(key, value.current_value); - } - - let code = if account.info.bytecode.is_empty() { - None - } else { - Some(account.info.bytecode.clone()) - }; - - let account_update = AccountUpdate { - address, - removed: false, - info: Some(AccountInfo { - code_hash: code_hash(&account.info.bytecode), - balance: account.info.balance, - nonce: account.info.nonce, - }), - code, - added_storage, - }; - - account_updates.push(account_update); - } - - initial_state - .database() - .unwrap() - .apply_account_updates(block_hash, &account_updates) - .unwrap() - .unwrap() -} diff --git a/cmd/ef_tests/levm/runner/levm_runner.rs b/cmd/ef_tests/levm/runner/levm_runner.rs new file mode 100644 index 000000000..4cf9ee66b --- /dev/null +++ b/cmd/ef_tests/levm/runner/levm_runner.rs @@ -0,0 +1,247 @@ +use crate::{ + report::{EFTestReport, TestVector}, + runner::{EFTestRunnerError, InternalError}, + types::EFTest, + utils, +}; +use ethrex_core::{ + types::{code_hash, AccountInfo}, + H256, U256, +}; +use ethrex_levm::{ + db::Cache, + errors::{TransactionReport, VMError}, + vm::VM, + Environment, +}; +use ethrex_storage::AccountUpdate; +use ethrex_vm::db::StoreWrapper; +use keccak_hash::keccak; +use std::{collections::HashMap, sync::Arc}; + +pub fn run_ef_test(test: &EFTest) -> Result { + let mut ef_test_report = EFTestReport::new( + test.name.clone(), + test._info.generated_test_hash, + test.fork(), + ); + for (vector, _tx) in test.transactions.iter() { + match run_ef_test_tx(vector, test) { + Ok(_) => continue, + Err(EFTestRunnerError::VMInitializationFailed(reason)) => { + ef_test_report.register_vm_initialization_failure(reason, *vector); + } + Err(EFTestRunnerError::FailedToEnsurePreState(reason)) => { + ef_test_report.register_pre_state_validation_failure(reason, *vector); + } + Err(EFTestRunnerError::ExecutionFailedUnexpectedly(error)) => { + ef_test_report.register_unexpected_execution_failure(error, *vector); + } + Err(EFTestRunnerError::FailedToEnsurePostState(transaction_report, reason)) => { + ef_test_report.register_post_state_validation_failure( + transaction_report, + reason, + *vector, + ); + } + Err(EFTestRunnerError::VMExecutionMismatch(_)) => { + return Err(EFTestRunnerError::Internal(InternalError::FirstRunInternal( + "VM execution mismatch errors should only happen when running with revm. This failed during levm's execution." + .to_owned(), + ))); + } + Err(EFTestRunnerError::Internal(reason)) => { + return Err(EFTestRunnerError::Internal(reason)); + } + } + } + Ok(ef_test_report) +} + +pub fn run_ef_test_tx(vector: &TestVector, test: &EFTest) -> Result<(), EFTestRunnerError> { + let mut levm = prepare_vm_for_tx(vector, test)?; + ensure_pre_state(&levm, test)?; + let levm_execution_result = levm.transact(); + ensure_post_state(&levm_execution_result, vector, test)?; + Ok(()) +} + +pub fn prepare_vm_for_tx(vector: &TestVector, test: &EFTest) -> Result { + let (initial_state, block_hash) = utils::load_initial_state(test); + let db = Arc::new(StoreWrapper { + store: initial_state.database().unwrap().clone(), + block_hash, + }); + VM::new( + test.transactions.get(vector).unwrap().to.clone(), + Environment { + origin: test.transactions.get(vector).unwrap().sender, + consumed_gas: U256::default(), + refunded_gas: U256::default(), + gas_limit: test.env.current_gas_limit, + block_number: test.env.current_number, + coinbase: test.env.current_coinbase, + timestamp: test.env.current_timestamp, + prev_randao: test.env.current_random, + chain_id: U256::from(1729), + base_fee_per_gas: test.env.current_base_fee.unwrap_or_default(), + gas_price: test + .transactions + .get(vector) + .unwrap() + .gas_price + .unwrap_or_default(), // or max_fee_per_gas? + block_excess_blob_gas: test.env.current_excess_blob_gas, + block_blob_gas_used: None, + tx_blob_hashes: None, + }, + test.transactions.get(vector).unwrap().value, + test.transactions.get(vector).unwrap().data.clone(), + db, + Cache::default(), + ) + .map_err(|err| EFTestRunnerError::VMInitializationFailed(err.to_string())) +} + +pub fn ensure_pre_state(evm: &VM, test: &EFTest) -> Result<(), EFTestRunnerError> { + let world_state = &evm.db; + for (address, pre_value) in &test.pre.0 { + let account = world_state.get_account_info(*address); + ensure_pre_state_condition( + account.nonce == pre_value.nonce.as_u64(), + format!( + "Nonce mismatch for account {:#x}: expected {}, got {}", + address, pre_value.nonce, account.nonce + ), + )?; + ensure_pre_state_condition( + account.balance == pre_value.balance, + format!( + "Balance mismatch for account {:#x}: expected {}, got {}", + address, pre_value.balance, account.balance + ), + )?; + for (k, v) in &pre_value.storage { + let mut key_bytes = [0u8; 32]; + k.to_big_endian(&mut key_bytes); + let storage_slot = world_state.get_storage_slot(*address, H256::from_slice(&key_bytes)); + ensure_pre_state_condition( + &storage_slot == v, + format!( + "Storage slot mismatch for account {:#x} at key {:?}: expected {}, got {}", + address, k, v, storage_slot + ), + )?; + } + ensure_pre_state_condition( + keccak(account.bytecode.clone()) == keccak(pre_value.code.as_ref()), + format!( + "Code hash mismatch for account {:#x}: expected {}, got {}", + address, + keccak(pre_value.code.as_ref()), + keccak(account.bytecode) + ), + )?; + } + Ok(()) +} + +fn ensure_pre_state_condition( + condition: bool, + error_reason: String, +) -> Result<(), EFTestRunnerError> { + if !condition { + return Err(EFTestRunnerError::FailedToEnsurePreState(error_reason)); + } + Ok(()) +} + +pub fn ensure_post_state( + levm_execution_result: &Result, + vector: &TestVector, + test: &EFTest, +) -> Result<(), EFTestRunnerError> { + match levm_execution_result { + Ok(execution_report) => { + match test.post.vector_post_value(vector).expect_exception { + // Execution result was successful but an exception was expected. + Some(expected_exception) => { + let error_reason = format!("Expected exception: {expected_exception}"); + return Err(EFTestRunnerError::FailedToEnsurePostState( + execution_report.clone(), + error_reason, + )); + } + // Execution result was successful and no exception was expected. + None => { + let levm_account_updates = get_state_transitions(execution_report); + let pos_state_root = post_state_root(&levm_account_updates, test); + let expected_post_state_root_hash = test.post.vector_post_value(vector).hash; + if expected_post_state_root_hash != pos_state_root { + let error_reason = format!( + "Post-state root mismatch: expected {expected_post_state_root_hash:#x}, got {pos_state_root:#x}", + ); + return Err(EFTestRunnerError::FailedToEnsurePostState( + execution_report.clone(), + error_reason, + )); + } + } + } + } + Err(err) => { + match test.post.vector_post_value(vector).expect_exception { + // Execution result was unsuccessful and an exception was expected. + // TODO: Check that the exception matches the expected exception. + Some(_expected_exception) => {} + // Execution result was unsuccessful but no exception was expected. + None => { + return Err(EFTestRunnerError::ExecutionFailedUnexpectedly(err.clone())); + } + } + } + }; + Ok(()) +} + +pub fn get_state_transitions(execution_report: &TransactionReport) -> Vec { + let mut account_updates: Vec = vec![]; + for (address, account) in &execution_report.new_state { + let mut added_storage = HashMap::new(); + + for (key, value) in &account.storage { + added_storage.insert(*key, value.current_value); + } + + let code = if account.info.bytecode.is_empty() { + None + } else { + Some(account.info.bytecode.clone()) + }; + + let account_update = AccountUpdate { + address: *address, + removed: false, + info: Some(AccountInfo { + code_hash: code_hash(&account.info.bytecode), + balance: account.info.balance, + nonce: account.info.nonce, + }), + code, + added_storage, + }; + + account_updates.push(account_update); + } + account_updates +} + +pub fn post_state_root(account_updates: &[AccountUpdate], test: &EFTest) -> H256 { + let (initial_state, block_hash) = utils::load_initial_state(test); + initial_state + .database() + .unwrap() + .apply_account_updates(block_hash, account_updates) + .unwrap() + .unwrap() +} diff --git a/cmd/ef_tests/levm/runner/mod.rs b/cmd/ef_tests/levm/runner/mod.rs new file mode 100644 index 000000000..95957393f --- /dev/null +++ b/cmd/ef_tests/levm/runner/mod.rs @@ -0,0 +1,156 @@ +use crate::{ + report::{self, format_duration_as_mm_ss, EFTestReport, TestReRunReport}, + types::EFTest, +}; +use clap::Parser; +use colored::Colorize; +use ethrex_levm::errors::{TransactionReport, VMError}; +use ethrex_vm::SpecId; +use serde::{Deserialize, Serialize}; +use spinoff::{spinners::Dots, Color, Spinner}; + +pub mod levm_runner; +pub mod revm_runner; + +#[derive(Debug, thiserror::Error, Clone, Serialize, Deserialize)] +pub enum EFTestRunnerError { + #[error("VM initialization failed: {0}")] + VMInitializationFailed(String), + #[error("Transaction execution failed when it was not expected to fail: {0}")] + ExecutionFailedUnexpectedly(VMError), + #[error("Failed to ensure post-state: {0}")] + FailedToEnsurePreState(String), + #[error("Failed to ensure post-state: {1}")] + FailedToEnsurePostState(TransactionReport, String), + #[error("VM run mismatch: {0}")] + VMExecutionMismatch(String), + #[error("This is a bug: {0}")] + Internal(#[from] InternalError), +} + +#[derive(Debug, thiserror::Error, Clone, Serialize, Deserialize)] +pub enum InternalError { + #[error("First run failed unexpectedly: {0}")] + FirstRunInternal(String), + #[error("Re-runner failed unexpectedly: {0}")] + ReRunInternal(String, TestReRunReport), + #[error("Main runner failed unexpectedly: {0}")] + MainRunnerInternal(String), +} + +#[derive(Parser)] +pub struct EFTestRunnerOptions { + #[arg(short, long, value_name = "FORK", default_value = "Cancun")] + pub fork: Vec, + #[arg(short, long, value_name = "TESTS")] + pub tests: Vec, +} + +pub fn run_ef_tests( + ef_tests: Vec, + _opts: &EFTestRunnerOptions, +) -> Result<(), EFTestRunnerError> { + let mut reports = report::load()?; + if reports.is_empty() { + run_with_levm(&mut reports, &ef_tests)?; + } + re_run_with_revm(&mut reports, &ef_tests)?; + write_report(&reports) +} + +fn run_with_levm( + reports: &mut Vec, + ef_tests: &[EFTest], +) -> Result<(), EFTestRunnerError> { + let levm_run_time = std::time::Instant::now(); + let mut levm_run_spinner = Spinner::new( + Dots, + report::progress(reports, levm_run_time.elapsed()), + Color::Cyan, + ); + for test in ef_tests.iter() { + let ef_test_report = match levm_runner::run_ef_test(test) { + Ok(ef_test_report) => ef_test_report, + Err(EFTestRunnerError::Internal(err)) => return Err(EFTestRunnerError::Internal(err)), + non_internal_errors => { + return Err(EFTestRunnerError::Internal(InternalError::FirstRunInternal(format!( + "Non-internal error raised when executing levm. This should not happen: {non_internal_errors:?}", + )))) + } + }; + reports.push(ef_test_report); + levm_run_spinner.update_text(report::progress(reports, levm_run_time.elapsed())); + } + levm_run_spinner.success(&report::progress(reports, levm_run_time.elapsed())); + + let mut summary_spinner = Spinner::new(Dots, "Loading summary...".to_owned(), Color::Cyan); + summary_spinner.success(&report::summary(reports)); + Ok(()) +} + +fn re_run_with_revm( + reports: &mut [EFTestReport], + ef_tests: &[EFTest], +) -> Result<(), EFTestRunnerError> { + let revm_run_time = std::time::Instant::now(); + let mut revm_run_spinner = Spinner::new( + Dots, + "Running failed tests with REVM...".to_owned(), + Color::Cyan, + ); + let failed_tests = reports.iter().filter(|report| !report.passed()).count(); + for (idx, failed_test_report) in reports.iter_mut().enumerate() { + if failed_test_report.passed() { + continue; + } + revm_run_spinner.update_text(format!( + "{} {}/{failed_tests} - {}", + "Re-running failed tests with REVM".bold(), + idx + 1, + format_duration_as_mm_ss(revm_run_time.elapsed()) + )); + match revm_runner::re_run_failed_ef_test( + ef_tests + .iter() + .find(|test| test._info.generated_test_hash == failed_test_report.test_hash) + .unwrap(), + failed_test_report, + ) { + Ok(re_run_report) => { + failed_test_report.register_re_run_report(re_run_report.clone()); + } + Err(EFTestRunnerError::Internal(InternalError::ReRunInternal(reason, re_run_report))) => { + write_report(reports)?; + cache_re_run(reports)?; + return Err(EFTestRunnerError::Internal(InternalError::ReRunInternal( + reason, + re_run_report, + ))) + }, + non_re_run_internal_errors => { + return Err(EFTestRunnerError::Internal(InternalError::MainRunnerInternal(format!( + "Non-internal error raised when executing revm. This should not happen: {non_re_run_internal_errors:?}" + )))) + } + } + } + revm_run_spinner.success(&format!( + "Re-ran failed tests with REVM in {}", + format_duration_as_mm_ss(revm_run_time.elapsed()) + )); + Ok(()) +} + +fn write_report(reports: &[EFTestReport]) -> Result<(), EFTestRunnerError> { + let mut report_spinner = Spinner::new(Dots, "Loading report...".to_owned(), Color::Cyan); + let report_file_path = report::write(reports)?; + report_spinner.success(&format!("Report written to file {report_file_path:?}").bold()); + Ok(()) +} + +fn cache_re_run(reports: &[EFTestReport]) -> Result<(), EFTestRunnerError> { + let mut cache_spinner = Spinner::new(Dots, "Caching re-run...".to_owned(), Color::Cyan); + let cache_file_path = report::cache(reports)?; + cache_spinner.success(&format!("Re-run cached to file {cache_file_path:?}").bold()); + Ok(()) +} diff --git a/cmd/ef_tests/levm/runner/revm_runner.rs b/cmd/ef_tests/levm/runner/revm_runner.rs new file mode 100644 index 000000000..9594d6d9d --- /dev/null +++ b/cmd/ef_tests/levm/runner/revm_runner.rs @@ -0,0 +1,286 @@ +use crate::{ + report::{AccountUpdatesReport, EFTestReport, TestReRunReport, TestVector}, + runner::{levm_runner, EFTestRunnerError, InternalError}, + types::EFTest, + utils::load_initial_state, +}; +use ethrex_core::{types::TxKind, Address}; +use ethrex_levm::errors::{TransactionReport, TxResult}; +use ethrex_storage::{error::StoreError, AccountUpdate}; +use ethrex_vm::{db::StoreWrapper, spec_id, EvmState, RevmAddress, RevmU256}; +use revm::{ + db::State, + inspectors::TracerEip3155 as RevmTracerEip3155, + primitives::{ + BlobExcessGasAndPrice, BlockEnv as RevmBlockEnv, EVMError as REVMError, + ExecutionResult as RevmExecutionResult, TxEnv as RevmTxEnv, TxKind as RevmTxKind, + }, + Evm as Revm, +}; +use std::collections::HashSet; + +pub fn re_run_failed_ef_test( + test: &EFTest, + failed_test_report: &EFTestReport, +) -> Result { + assert_eq!(test.name, failed_test_report.name); + let mut re_run_report = TestReRunReport::new(); + for (vector, vector_failure) in failed_test_report.failed_vectors.iter() { + match vector_failure { + // We only want to re-run tests that failed in the post-state validation. + EFTestRunnerError::FailedToEnsurePostState(transaction_report, _) => { + match re_run_failed_ef_test_tx(vector, test, transaction_report, &mut re_run_report) { + Ok(_) => continue, + Err(EFTestRunnerError::VMInitializationFailed(reason)) => { + return Err(EFTestRunnerError::Internal(InternalError::ReRunInternal( + format!("REVM initialization failed when re-running failed test: {reason}"), re_run_report.clone() + ))); + } + Err(EFTestRunnerError::Internal(reason)) => { + return Err(EFTestRunnerError::Internal(reason)); + } + unexpected_error => { + return Err(EFTestRunnerError::Internal(InternalError::ReRunInternal(format!( + "Unexpected error when re-running failed test: {unexpected_error:?}" + ), re_run_report.clone()))); + } + } + }, + EFTestRunnerError::VMInitializationFailed(_) + | EFTestRunnerError::ExecutionFailedUnexpectedly(_) + | EFTestRunnerError::FailedToEnsurePreState(_) => continue, + EFTestRunnerError::VMExecutionMismatch(reason) => return Err(EFTestRunnerError::Internal(InternalError::ReRunInternal( + format!("VM execution mismatch errors should only happen when running with revm. This failed during levm's execution: {reason}"), re_run_report.clone()))), + EFTestRunnerError::Internal(reason) => return Err(EFTestRunnerError::Internal(reason.to_owned())), + } + } + Ok(re_run_report) +} + +pub fn re_run_failed_ef_test_tx( + vector: &TestVector, + test: &EFTest, + levm_execution_report: &TransactionReport, + re_run_report: &mut TestReRunReport, +) -> Result<(), EFTestRunnerError> { + let (mut state, _block_hash) = load_initial_state(test); + let mut revm = prepare_revm_for_tx(&mut state, vector, test)?; + let revm_execution_result = revm.transact_commit(); + drop(revm); // Need to drop the state mutable reference. + compare_levm_revm_execution_results( + vector, + levm_execution_report, + revm_execution_result, + re_run_report, + )?; + ensure_post_state( + levm_execution_report, + vector, + &mut state, + test, + re_run_report, + )?; + Ok(()) +} + +pub fn prepare_revm_for_tx<'state>( + initial_state: &'state mut EvmState, + vector: &TestVector, + test: &EFTest, +) -> Result>, EFTestRunnerError> { + let chain_spec = initial_state + .chain_config() + .map_err(|err| EFTestRunnerError::VMInitializationFailed(err.to_string()))?; + let block_env = RevmBlockEnv { + number: RevmU256::from_limbs(test.env.current_number.0), + coinbase: RevmAddress(test.env.current_coinbase.0.into()), + timestamp: RevmU256::from_limbs(test.env.current_timestamp.0), + gas_limit: RevmU256::from_limbs(test.env.current_gas_limit.0), + basefee: RevmU256::from_limbs(test.env.current_base_fee.unwrap_or_default().0), + difficulty: RevmU256::from_limbs(test.env.current_difficulty.0), + prevrandao: test.env.current_random.map(|v| v.0.into()), + blob_excess_gas_and_price: Some(BlobExcessGasAndPrice::new( + test.env + .current_excess_blob_gas + .unwrap_or_default() + .as_u64(), + )), + }; + let tx = &test + .transactions + .get(vector) + .ok_or(EFTestRunnerError::VMInitializationFailed(format!( + "Vector {vector:?} not found in test {}", + test.name + )))?; + let tx_env = RevmTxEnv { + caller: tx.sender.0.into(), + gas_limit: tx.gas_limit.as_u64(), + gas_price: RevmU256::from_limbs(tx.gas_price.unwrap_or_default().0), + transact_to: match tx.to { + TxKind::Call(to) => RevmTxKind::Call(to.0.into()), + TxKind::Create => RevmTxKind::Create, + }, + value: RevmU256::from_limbs(tx.value.0), + data: tx.data.to_vec().into(), + nonce: Some(tx.nonce.as_u64()), + chain_id: None, + access_list: Vec::default(), + gas_priority_fee: None, + blob_hashes: Vec::default(), + max_fee_per_blob_gas: None, + authorization_list: None, + }; + let evm_builder = Revm::builder() + .with_block_env(block_env) + .with_tx_env(tx_env) + .modify_cfg_env(|cfg| cfg.chain_id = chain_spec.chain_id) + .with_spec_id(spec_id(&chain_spec, test.env.current_timestamp.as_u64())) + .with_external_context( + RevmTracerEip3155::new(Box::new(std::io::stderr())).without_summary(), + ); + match initial_state { + EvmState::Store(db) => Ok(evm_builder.with_db(db).build()), + _ => Err(EFTestRunnerError::VMInitializationFailed( + "Expected LEVM state to be a Store".to_owned(), + )), + } +} + +pub fn compare_levm_revm_execution_results( + vector: &TestVector, + levm_execution_report: &TransactionReport, + revm_execution_result: Result>, + re_run_report: &mut TestReRunReport, +) -> Result<(), EFTestRunnerError> { + match (levm_execution_report, revm_execution_result) { + (levm_tx_report, Ok(revm_execution_result)) => { + match (&levm_tx_report.result, revm_execution_result.clone()) { + ( + TxResult::Success, + RevmExecutionResult::Success { + reason: _, + gas_used: revm_gas_used, + gas_refunded: revm_gas_refunded, + logs: _, + output: _, + }, + ) => { + if levm_tx_report.gas_used != revm_gas_used { + re_run_report.register_gas_used_mismatch( + *vector, + levm_tx_report.gas_used, + revm_gas_used, + ); + } + if levm_tx_report.gas_refunded != revm_gas_refunded { + re_run_report.register_gas_refunded_mismatch( + *vector, + levm_tx_report.gas_refunded, + revm_gas_refunded, + ); + } + } + ( + TxResult::Revert(_error), + RevmExecutionResult::Revert { + gas_used: revm_gas_used, + output: _, + }, + ) => { + if levm_tx_report.gas_used != revm_gas_used { + re_run_report.register_gas_used_mismatch( + *vector, + levm_tx_report.gas_used, + revm_gas_used, + ); + } + } + ( + TxResult::Revert(_error), + RevmExecutionResult::Halt { + reason: _, + gas_used: revm_gas_used, + }, + ) => { + // TODO: Register the revert reasons. + if levm_tx_report.gas_used != revm_gas_used { + re_run_report.register_gas_used_mismatch( + *vector, + levm_tx_report.gas_used, + revm_gas_used, + ); + } + } + _ => { + re_run_report.register_execution_result_mismatch( + *vector, + levm_tx_report.result.clone(), + revm_execution_result.clone(), + ); + } + } + } + (levm_transaction_report, Err(revm_error)) => { + re_run_report.register_re_run_failure( + *vector, + levm_transaction_report.result.clone(), + revm_error, + ); + } + } + Ok(()) +} + +pub fn ensure_post_state( + levm_execution_report: &TransactionReport, + vector: &TestVector, + revm_state: &mut EvmState, + test: &EFTest, + re_run_report: &mut TestReRunReport, +) -> Result<(), EFTestRunnerError> { + match test.post.vector_post_value(vector).expect_exception { + Some(_expected_exception) => {} + // We only want to compare account updates when no exception is expected. + None => { + let levm_account_updates = levm_runner::get_state_transitions(levm_execution_report); + let revm_account_updates = ethrex_vm::get_state_transitions(revm_state); + let account_updates_report = + compare_levm_revm_account_updates(&levm_account_updates, &revm_account_updates); + re_run_report.register_account_updates_report(*vector, account_updates_report); + } + } + + Ok(()) +} + +pub fn compare_levm_revm_account_updates( + levm_account_updates: &[AccountUpdate], + revm_account_updates: &[AccountUpdate], +) -> AccountUpdatesReport { + let levm_updated_accounts = levm_account_updates + .iter() + .map(|account_update| account_update.address) + .collect::>(); + let revm_updated_accounts = revm_account_updates + .iter() + .map(|account_update| account_update.address) + .collect::>(); + + AccountUpdatesReport { + levm_account_updates: levm_account_updates.to_vec(), + revm_account_updates: revm_account_updates.to_vec(), + levm_updated_accounts_only: levm_updated_accounts + .difference(&revm_updated_accounts) + .cloned() + .collect::>(), + revm_updated_accounts_only: revm_updated_accounts + .difference(&levm_updated_accounts) + .cloned() + .collect::>(), + shared_updated_accounts: levm_updated_accounts + .intersection(&revm_updated_accounts) + .cloned() + .collect::>(), + } +} diff --git a/cmd/ef_tests/levm/tests/ef_tests_levm.rs b/cmd/ef_tests/levm/tests/ef_tests_levm.rs new file mode 100644 index 000000000..bc7bd00cb --- /dev/null +++ b/cmd/ef_tests/levm/tests/ef_tests_levm.rs @@ -0,0 +1,13 @@ +use clap::Parser; +use ef_tests_levm::{ + parser, + runner::{self, EFTestRunnerOptions}, +}; +use std::error::Error; + +fn main() -> Result<(), Box> { + let opts = EFTestRunnerOptions::parse(); + let ef_tests = parser::parse_ef_tests(&opts)?; + runner::run_ef_tests(ef_tests, &opts)?; + Ok(()) +} diff --git a/cmd/ef_tests/levm/tests/test.rs b/cmd/ef_tests/levm/tests/test.rs deleted file mode 100644 index e726edb91..000000000 --- a/cmd/ef_tests/levm/tests/test.rs +++ /dev/null @@ -1,6 +0,0 @@ -use ef_tests_levm::runner; - -fn main() { - let report = runner::run_ef_tests().unwrap(); - println!("{report}"); -} diff --git a/cmd/ef_tests/levm/types.rs b/cmd/ef_tests/levm/types.rs index 9721b1e60..be399ca5e 100644 --- a/cmd/ef_tests/levm/types.rs +++ b/cmd/ef_tests/levm/types.rs @@ -1,16 +1,23 @@ -use crate::deserialize::{ - deserialize_ef_post_value_indexes, deserialize_hex_bytes, deserialize_hex_bytes_vec, - deserialize_u256_optional_safe, deserialize_u256_safe, deserialize_u256_valued_hashmap_safe, - deserialize_u256_vec_safe, +use crate::{ + deserialize::{ + deserialize_ef_post_value_indexes, deserialize_hex_bytes, deserialize_hex_bytes_vec, + deserialize_u256_optional_safe, deserialize_u256_safe, + deserialize_u256_valued_hashmap_safe, deserialize_u256_vec_safe, + }, + report::TestVector, }; use bytes::Bytes; use ethrex_core::{ types::{Genesis, GenesisAccount, TxKind}, Address, H256, U256, }; +use ethrex_vm::SpecId; use serde::Deserialize; use std::collections::HashMap; +#[derive(Debug)] +pub struct EFTests(pub Vec); + #[derive(Debug)] pub struct EFTest { pub name: String, @@ -18,7 +25,26 @@ pub struct EFTest { pub env: EFTestEnv, pub post: EFTestPost, pub pre: EFTestPre, - pub transactions: Vec<((usize, usize, usize), EFTestTransaction)>, + pub transactions: HashMap, +} + +impl EFTest { + pub fn fork(&self) -> SpecId { + match &self.post { + EFTestPost::Cancun(_) => SpecId::CANCUN, + EFTestPost::Shanghai(_) => SpecId::SHANGHAI, + EFTestPost::Homestead(_) => SpecId::HOMESTEAD, + EFTestPost::Istanbul(_) => SpecId::ISTANBUL, + EFTestPost::London(_) => SpecId::LONDON, + EFTestPost::Byzantium(_) => SpecId::BYZANTIUM, + EFTestPost::Berlin(_) => SpecId::BERLIN, + EFTestPost::Constantinople(_) | EFTestPost::ConstantinopleFix(_) => { + SpecId::CONSTANTINOPLE + } + EFTestPost::Paris(_) => SpecId::MERGE, + EFTestPost::Frontier(_) => SpecId::FRONTIER, + } + } } impl From<&EFTest> for Genesis { @@ -34,10 +60,10 @@ impl From<&EFTest> for Genesis { coinbase: test.env.current_coinbase, difficulty: test.env.current_difficulty, gas_limit: test.env.current_gas_limit.as_u64(), - mix_hash: test.env.current_random, + mix_hash: test.env.current_random.unwrap_or_default(), timestamp: test.env.current_timestamp.as_u64(), - base_fee_per_gas: Some(test.env.current_base_fee.as_u64()), - excess_blob_gas: Some(test.env.current_excess_blob_gas.as_u64()), + base_fee_per_gas: test.env.current_base_fee.map(|v| v.as_u64()), + excess_blob_gas: test.env.current_excess_blob_gas.map(|v| v.as_u64()), ..Default::default() } } @@ -64,18 +90,18 @@ pub struct EFTestInfo { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EFTestEnv { - #[serde(deserialize_with = "deserialize_u256_safe")] - pub current_base_fee: U256, + #[serde(default, deserialize_with = "deserialize_u256_optional_safe")] + pub current_base_fee: Option, pub current_coinbase: Address, #[serde(deserialize_with = "deserialize_u256_safe")] pub current_difficulty: U256, - #[serde(deserialize_with = "deserialize_u256_safe")] - pub current_excess_blob_gas: U256, + #[serde(default, deserialize_with = "deserialize_u256_optional_safe")] + pub current_excess_blob_gas: Option, #[serde(deserialize_with = "deserialize_u256_safe")] pub current_gas_limit: U256, #[serde(deserialize_with = "deserialize_u256_safe")] pub current_number: U256, - pub current_random: H256, + pub current_random: Option, #[serde(deserialize_with = "deserialize_u256_safe")] pub current_timestamp: U256, } @@ -83,18 +109,77 @@ pub struct EFTestEnv { #[derive(Debug, Deserialize, Clone)] pub enum EFTestPost { Cancun(Vec), + Shanghai(Vec), + Homestead(Vec), + Istanbul(Vec), + London(Vec), + Byzantium(Vec), + Berlin(Vec), + Constantinople(Vec), + Paris(Vec), + ConstantinopleFix(Vec), + Frontier(Vec), } impl EFTestPost { pub fn values(self) -> Vec { match self { EFTestPost::Cancun(v) => v, + EFTestPost::Shanghai(v) => v, + EFTestPost::Homestead(v) => v, + EFTestPost::Istanbul(v) => v, + EFTestPost::London(v) => v, + EFTestPost::Byzantium(v) => v, + EFTestPost::Berlin(v) => v, + EFTestPost::Constantinople(v) => v, + EFTestPost::Paris(v) => v, + EFTestPost::ConstantinopleFix(v) => v, + EFTestPost::Frontier(v) => v, + } + } + + pub fn vector_post_value(&self, vector: &TestVector) -> EFTestPostValue { + match self { + EFTestPost::Cancun(v) => Self::find_vector_post_value(v, vector), + EFTestPost::Shanghai(v) => Self::find_vector_post_value(v, vector), + EFTestPost::Homestead(v) => Self::find_vector_post_value(v, vector), + EFTestPost::Istanbul(v) => Self::find_vector_post_value(v, vector), + EFTestPost::London(v) => Self::find_vector_post_value(v, vector), + EFTestPost::Byzantium(v) => Self::find_vector_post_value(v, vector), + EFTestPost::Berlin(v) => Self::find_vector_post_value(v, vector), + EFTestPost::Constantinople(v) => Self::find_vector_post_value(v, vector), + EFTestPost::Paris(v) => Self::find_vector_post_value(v, vector), + EFTestPost::ConstantinopleFix(v) => Self::find_vector_post_value(v, vector), + EFTestPost::Frontier(v) => Self::find_vector_post_value(v, vector), } } + fn find_vector_post_value(values: &[EFTestPostValue], vector: &TestVector) -> EFTestPostValue { + values + .iter() + .find(|v| { + let data_index = v.indexes.get("data").unwrap().as_usize(); + let gas_limit_index = v.indexes.get("gas").unwrap().as_usize(); + let value_index = v.indexes.get("value").unwrap().as_usize(); + vector == &(data_index, gas_limit_index, value_index) + }) + .unwrap() + .clone() + } + pub fn iter(&self) -> impl Iterator { match self { EFTestPost::Cancun(v) => v.iter(), + EFTestPost::Shanghai(v) => v.iter(), + EFTestPost::Homestead(v) => v.iter(), + EFTestPost::Istanbul(v) => v.iter(), + EFTestPost::London(v) => v.iter(), + EFTestPost::Byzantium(v) => v.iter(), + EFTestPost::Berlin(v) => v.iter(), + EFTestPost::Constantinople(v) => v.iter(), + EFTestPost::Paris(v) => v.iter(), + EFTestPost::ConstantinopleFix(v) => v.iter(), + EFTestPost::Frontier(v) => v.iter(), } } } diff --git a/cmd/ef_tests/levm/utils.rs b/cmd/ef_tests/levm/utils.rs index dedb641c9..b09944b93 100644 --- a/cmd/ef_tests/levm/utils.rs +++ b/cmd/ef_tests/levm/utils.rs @@ -9,10 +9,11 @@ pub fn load_initial_state(test: &EFTest) -> (EvmState, H256) { let storage = Store::new("./temp", EngineType::InMemory).expect("Failed to create Store"); storage.add_initial_state(genesis.clone()).unwrap(); - let parent_hash = genesis.get_block().header.parent_hash; - ( - evm_state(storage.clone(), parent_hash), + evm_state( + storage.clone(), + genesis.get_block().header.compute_block_hash(), + ), genesis.get_block().header.compute_block_hash(), ) } diff --git a/crates/common/types/account.rs b/crates/common/types/account.rs index 378419199..28ac9b9bf 100644 --- a/crates/common/types/account.rs +++ b/crates/common/types/account.rs @@ -37,7 +37,7 @@ pub struct Account { pub storage: HashMap, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct AccountInfo { pub code_hash: H256, pub balance: U256, diff --git a/crates/storage/store/storage.rs b/crates/storage/store/storage.rs index 96073c485..c500093e4 100644 --- a/crates/storage/store/storage.rs +++ b/crates/storage/store/storage.rs @@ -13,6 +13,7 @@ use ethrex_core::types::{ use ethrex_rlp::decode::RLPDecode; use ethrex_rlp::encode::RLPEncode; use ethrex_trie::Trie; +use serde::{Deserialize, Serialize}; use sha3::{Digest as _, Keccak256}; use std::collections::HashMap; use std::fmt::Debug; @@ -39,7 +40,7 @@ pub enum EngineType { Libmdbx, } -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct AccountUpdate { pub address: Address, pub removed: bool, diff --git a/crates/vm/levm/src/account.rs b/crates/vm/levm/src/account.rs index da8872a92..a374ac463 100644 --- a/crates/vm/levm/src/account.rs +++ b/crates/vm/levm/src/account.rs @@ -5,9 +5,10 @@ use crate::{ use bytes::Bytes; use ethrex_core::{H256, U256}; use keccak_hash::keccak; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; -#[derive(Clone, Default, Debug, PartialEq, Eq)] +#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct AccountInfo { pub balance: U256, pub bytecode: Bytes, @@ -20,13 +21,13 @@ impl AccountInfo { } } -#[derive(Clone, Default, Debug, PartialEq, Eq)] +#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Account { pub info: AccountInfo, pub storage: HashMap, } -#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct StorageSlot { pub original_value: U256, pub current_value: U256, diff --git a/crates/vm/levm/src/errors.rs b/crates/vm/levm/src/errors.rs index b6bad188c..943f3c55b 100644 --- a/crates/vm/levm/src/errors.rs +++ b/crates/vm/levm/src/errors.rs @@ -1,11 +1,12 @@ use crate::account::Account; use bytes::Bytes; use ethrex_core::{types::Log, Address}; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use thiserror; /// Errors that halt the program -#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, Serialize, Deserialize)] pub enum VMError { #[error("Stack Underflow")] StackUnderflow, @@ -73,7 +74,7 @@ pub enum VMError { Internal(#[from] InternalError), } -#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error, Serialize, Deserialize)] pub enum OutOfGasError { #[error("Gas Cost Overflow")] GasCostOverflow, @@ -89,7 +90,7 @@ pub enum OutOfGasError { ArithmeticOperationDividedByZero, } -#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, Serialize, Deserialize)] pub enum InternalError { #[error("Overflowed when incrementing nonce")] NonceOverflowed, @@ -145,13 +146,13 @@ pub enum ResultReason { SelfDestruct, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum TxResult { Success, Revert(VMError), } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct TransactionReport { pub result: TxResult, pub new_state: HashMap, diff --git a/crates/vm/vm.rs b/crates/vm/vm.rs index 2c3c84b10..8b23ce8c0 100644 --- a/crates/vm/vm.rs +++ b/crates/vm/vm.rs @@ -23,7 +23,7 @@ use revm::{ inspector_handle_register, inspectors::TracerEip3155, precompile::{PrecompileSpecId, Precompiles}, - primitives::{BlobExcessGasAndPrice, BlockEnv, TxEnv, B256, U256 as RevmU256}, + primitives::{BlobExcessGasAndPrice, BlockEnv, TxEnv, B256}, Database, DatabaseCommit, Evm, }; use revm_inspectors::access_list::AccessListInspector; @@ -35,7 +35,7 @@ use revm_primitives::{ // Export needed types pub use errors::EvmError; pub use execution_result::*; -pub use revm::primitives::{Address as RevmAddress, SpecId}; +pub use revm::primitives::{Address as RevmAddress, SpecId, U256 as RevmU256}; type AccessList = Vec<(Address, Vec)>;