From 9bb928186288f00d8b24aca0c7fe459f79c2ddb0 Mon Sep 17 00:00:00 2001 From: Robin Salen Date: Tue, 26 Sep 2023 11:23:29 -0400 Subject: [PATCH] Deserialize tests from BlockchainTests folder instead --- Cargo.lock | 18 +- README.md | 18 +- common/Cargo.toml | 4 +- common/src/config.rs | 1 + common/src/types.rs | 73 ++++---- eth_test_parser/Cargo.toml | 4 +- eth_test_parser/src/config.rs | 1 + eth_test_parser/src/deserialize.rs | 178 +++++++++++++----- eth_test_parser/src/eth_tests_fetching.rs | 8 +- eth_test_parser/src/fs_scaffolding.rs | 25 +-- eth_test_parser/src/main.rs | 35 ++-- eth_test_parser/src/revm_builder/env.rs | 173 ++++++------------ eth_test_parser/src/revm_builder/mod.rs | 16 +- eth_test_parser/src/trie_builder.rs | 193 +++++++++++++++----- evm_test_runner/Cargo.toml | 4 +- evm_test_runner/src/main.rs | 1 - evm_test_runner/src/persistent_run_state.rs | 4 +- evm_test_runner/src/plonky2_runner.rs | 107 +++-------- evm_test_runner/src/state_diff.rs | 157 ---------------- evm_test_runner/src/test_dir_reading.rs | 3 +- 20 files changed, 463 insertions(+), 560 deletions(-) delete mode 100644 evm_test_runner/src/state_diff.rs diff --git a/Cargo.lock b/Cargo.lock index bc37e26..5a33066 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -772,8 +772,7 @@ dependencies = [ [[package]] name = "eth_trie_utils" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3cdbe3cbef402dc29d3e93ff8d739165609dc740cbf905ebef6e1d96602492" +source = "git+https://github.com/mir-protocol/eth_trie_utils.git?rev=e9ec4ec2aa2ae976b7c699ef40c1ffc716d87ed5#e9ec4ec2aa2ae976b7c699ef40c1ffc716d87ed5" dependencies = [ "bytes", "enum-as-inner", @@ -1711,8 +1710,7 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "plonky2" version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c7acc7871fdaf000d3533116eab95d89ada3dfb82b7bb0231da981323c27d6" +source = "git+https://github.com/mir-protocol/plonky2.git?rev=1ff6d4a2839a0cd16598a5db263568885a47e7c9#1ff6d4a2839a0cd16598a5db263568885a47e7c9" dependencies = [ "ahash", "anyhow", @@ -1736,9 +1734,10 @@ dependencies = [ [[package]] name = "plonky2_evm" version = "0.1.1" -source = "git+https://github.com/mir-protocol/plonky2.git?rev=6f98fd762885e9b5343af5f0e3f0c9c90e8cf3ab#6f98fd762885e9b5343af5f0e3f0c9c90e8cf3ab" +source = "git+https://github.com/mir-protocol/plonky2.git?rev=1ff6d4a2839a0cd16598a5db263568885a47e7c9#1ff6d4a2839a0cd16598a5db263568885a47e7c9" dependencies = [ "anyhow", + "bytes", "env_logger", "eth_trie_utils", "ethereum-types", @@ -1769,8 +1768,7 @@ dependencies = [ [[package]] name = "plonky2_field" version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d33a655ab5d274f763c292fe7e14577f25e40d9d8607b70ef10b39f8619e60b4" +source = "git+https://github.com/mir-protocol/plonky2.git?rev=1ff6d4a2839a0cd16598a5db263568885a47e7c9#1ff6d4a2839a0cd16598a5db263568885a47e7c9" dependencies = [ "anyhow", "itertools", @@ -1785,8 +1783,7 @@ dependencies = [ [[package]] name = "plonky2_maybe_rayon" version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194db0cbdd974e92d897cd92b74adb3968dc1b967315eb280357c49a7637994e" +source = "git+https://github.com/mir-protocol/plonky2.git?rev=1ff6d4a2839a0cd16598a5db263568885a47e7c9#1ff6d4a2839a0cd16598a5db263568885a47e7c9" dependencies = [ "rayon", ] @@ -1794,8 +1791,7 @@ dependencies = [ [[package]] name = "plonky2_util" version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5696e2e2a6bb5c48a6e33fb0dd4d20d0a9472784b709964f337f224e99bd6d06" +source = "git+https://github.com/mir-protocol/plonky2.git?rev=1ff6d4a2839a0cd16598a5db263568885a47e7c9#1ff6d4a2839a0cd16598a5db263568885a47e7c9" [[package]] name = "portable-atomic" diff --git a/README.md b/README.md index 876e365..8e6ac15 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # EVM Test + Parses and runs compatible common Ethereum tests from [ethereum/tests](https://github.com/ethereum/tests) against Polygon Zero's EVM. > Note: This repo is currently very early in development and is not yet ready to evaluate the EVM completeness! @@ -6,43 +7,52 @@ Parses and runs compatible common Ethereum tests from [ethereum/tests](https://g ## Components ### Parser + Since the tests from the Ethereum test repo are meant for a full node, only certain tests are compatible with our EVM. Additionally, for the tests that are compatible, they need to be parsed (or converted) into a format that is usable by our EVM. The parser has two responsibilities: + - Query the upstream Ethereum tests repo and check if any tests have been added/updated/removed. - If there is a change, re-parse the tests. ### Runner + The runner feeds the parsed tests into the EVM. Successes are defined as no errors occurring (the tests themselves do not provide an expected final state). If the EVM returns an error or panics, then the test is considered to have failed. The runner also outputs a results file (likely as a `*.md`) which contains statistics on the last test run. ## Quick Start + *TODO: Add more details...* Run the parser to parse the Eth tests into a format usable by `plonky2`: + ```sh cd eth_test_parser cargo run ``` Then launch the runner pointing it at the parsed tests directory: + ```sh cd ../evm_test_runner -cargo run --release -- -r summary ../generation_inputs # For a high-level summary report -cargo run --release -- -r test ../generation_inputs # For detailed information per test (likely want to use a filter with `-f`) +cargo run --release -- -r summary ../generation_inputs/BlockchainTests # For a high-level summary report +cargo run --release -- -r test ../generation_inputs/BlockchainTests # For detailed information per test (likely want to use a filter with `-f`) ``` ## Other + [Polygon Hermez](https://github.com/0xPolygonHermez) is doing something similar [here](https://github.com/0xPolygonHermez/zkevm-testvectors). ## License + Licensed under either of -* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) -* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) +- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or ) at your option. ## Contribution + Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/common/Cargo.toml b/common/Cargo.toml index 2edc922..bcfa309 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -10,9 +10,9 @@ edition = "2021" [dependencies] anyhow = { version = "1.0.71", features = ["backtrace"] } ethereum-types = "0.14.1" -eth_trie_utils = "0.6.0" +eth_trie_utils = { git = "https://github.com/mir-protocol/eth_trie_utils.git", rev = "e9ec4ec2aa2ae976b7c699ef40c1ffc716d87ed5" } flexi_logger = { version = "0.25.4", features = ["async"] } -plonky2_evm = { git = "https://github.com/mir-protocol/plonky2.git", rev = "6f98fd762885e9b5343af5f0e3f0c9c90e8cf3ab" } +plonky2_evm = { git = "https://github.com/mir-protocol/plonky2.git", rev = "1ff6d4a2839a0cd16598a5db263568885a47e7c9" } serde = {version = "1.0.163", features = ["derive"] } revm = { version = "3.3.0", features = ["serde"] } ruint = { version = "1.8.0", features = ["primitive-types"] } diff --git a/common/src/config.rs b/common/src/config.rs index 16aedbf..9a66b2e 100644 --- a/common/src/config.rs +++ b/common/src/config.rs @@ -1,3 +1,4 @@ pub const GENERATION_INPUTS_DEFAULT_OUTPUT_DIR: &str = "generation_inputs"; +pub const MAIN_TEST_DIR: &str = "BlockchainTests"; pub const MATIC_CHAIN_ID: u64 = 137; pub const ETHEREUM_CHAIN_ID: u64 = 1; diff --git a/common/src/types.rs b/common/src/types.rs index 1dec0fc..743e777 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -5,9 +5,8 @@ use std::{ }; use anyhow::{anyhow, Context}; -use eth_trie_utils::partial_trie::{HashedPartialTrie, Node, PartialTrie}; -use ethereum_types::{Address, H256}; -use plonky2_evm::proof::TrieRoots; +use ethereum_types::{Address, H256, U256}; +use plonky2_evm::proof::{BlockHashes, TrieRoots}; use plonky2_evm::{ generation::{GenerationInputs, TrieInputs}, proof::BlockMetadata, @@ -18,7 +17,7 @@ use crate::revm::SerializableEVMInstance; #[derive(Debug, Deserialize, Serialize)] pub struct ParsedTestManifest { - pub plonky2_variants: Plonky2ParsedTest, + pub plonky2_variants: Vec, pub revm_variants: Option>, } @@ -40,17 +39,14 @@ impl ParsedTestManifest { let revm_variants: Vec> = match self.revm_variants { // `revm_variants` will be parallel to `plonky2_variants`, given they are both // generated from the same vec (`test.post.merge`). - None => (0..self.plonky2_variants.test_variants.len()) - .map(|_| None) - .collect(), + None => (0..self.plonky2_variants.len()).map(|_| None).collect(), Some(v) => v.into_iter().map(Some).collect(), }; - let tot_variants_without_filter = self.plonky2_variants.test_variants.len(); + let tot_variants_without_filter = self.plonky2_variants.len(); let variants = self .plonky2_variants - .test_variants .into_iter() .zip(revm_variants.into_iter()) .enumerate() @@ -61,30 +57,29 @@ impl ParsedTestManifest { }) .map(|(variant_idx, (t_var, revm_variant))| { let trie_roots_after = TrieRoots { - state_root: t_var.common.expected_final_account_state_root_hash, - transactions_root: HashedPartialTrie::from(Node::Empty).hash(), // TODO: Fix this when we have transactions trie. - receipts_root: HashedPartialTrie::from(Node::Empty).hash(), // TODO: Fix this when we have receipts trie. + state_root: t_var.final_roots.state_root_hash, + transactions_root: t_var.final_roots.txn_trie_root_hash, + receipts_root: t_var.final_roots.receipts_trie_root_hash, }; let gen_inputs = GenerationInputs { signed_txns: vec![t_var.txn_bytes], - tries: self.plonky2_variants.const_plonky2_inputs.tries.clone(), + tries: t_var.plonky2_metadata.tries.clone(), trie_roots_after, - contract_code: self - .plonky2_variants - .const_plonky2_inputs - .contract_code - .clone(), - block_metadata: self - .plonky2_variants - .const_plonky2_inputs - .block_metadata - .clone(), - addresses: self.plonky2_variants.const_plonky2_inputs.addresses.clone(), + genesis_state_trie_root: t_var.plonky2_metadata.genesis_state_root, + contract_code: t_var.plonky2_metadata.contract_code.clone(), + block_metadata: t_var.plonky2_metadata.block_metadata.clone(), + addresses: t_var.plonky2_metadata.addresses.clone(), + txn_number_before: U256::zero(), + gas_used_before: U256::zero(), + block_bloom_before: [U256::zero(); 8], + gas_used_after: t_var.plonky2_metadata.block_metadata.block_gas_used, + block_bloom_after: t_var.plonky2_metadata.block_metadata.block_bloom, + block_hashes: BlockHashes::default(), }; TestVariantRunInfo { gen_inputs, - common: t_var.common, + final_roots: t_var.final_roots, revm_variant, variant_idx, } @@ -103,37 +98,35 @@ impl ParsedTestManifest { /// Note that for our runner we break any txn "variants" (see `indexes` under https://ethereum-tests.readthedocs.io/en/latest/test_types/gstate_tests.html#post-section) into separate sub-tests when running. This is because we don't want a single sub-test variant to cause the entire test to fail (we just want the variant to fail). #[derive(Debug, Deserialize, Serialize)] pub struct Plonky2ParsedTest { - pub test_variants: Vec, - - /// State that is constant between tests. - pub const_plonky2_inputs: ConstGenerationInputs, -} - -/// A single test that -#[derive(Debug, Deserialize, Serialize)] -pub struct TestVariant { - /// The txn bytes for each txn in the test. pub txn_bytes: Vec, - pub common: TestVariantCommon, + pub final_roots: ExpectedFinalRoots, + + /// All the metadata needed to prove the transaction in the `test_variant`. + pub plonky2_metadata: TestMetadata, } #[derive(Debug)] pub struct TestVariantRunInfo { pub gen_inputs: GenerationInputs, - pub common: TestVariantCommon, + pub final_roots: ExpectedFinalRoots, pub revm_variant: Option, pub variant_idx: usize, } #[derive(Debug, Deserialize, Serialize)] -pub struct TestVariantCommon { +pub struct ExpectedFinalRoots { /// The root hash of the expected final state trie. - pub expected_final_account_state_root_hash: H256, + pub state_root_hash: H256, + /// The root hash of the expected final transactions trie. + pub txn_trie_root_hash: H256, + /// The root hash of the expected final receipts trie. + pub receipts_trie_root_hash: H256, } #[derive(Debug, Deserialize, Serialize)] -pub struct ConstGenerationInputs { +pub struct TestMetadata { pub tries: TrieInputs, + pub genesis_state_root: H256, pub contract_code: HashMap>, pub block_metadata: BlockMetadata, pub addresses: Vec
, diff --git a/eth_test_parser/Cargo.toml b/eth_test_parser/Cargo.toml index 3dde377..6fb35b9 100644 --- a/eth_test_parser/Cargo.toml +++ b/eth_test_parser/Cargo.toml @@ -9,8 +9,8 @@ edition = "2021" [dependencies] common = { path = "../common" } -eth_trie_utils = "0.6.0" -plonky2_evm = { git = "https://github.com/mir-protocol/plonky2.git", rev = "6f98fd762885e9b5343af5f0e3f0c9c90e8cf3ab" } +eth_trie_utils = { git = "https://github.com/mir-protocol/eth_trie_utils.git", rev = "e9ec4ec2aa2ae976b7c699ef40c1ffc716d87ed5" } +plonky2_evm = { git = "https://github.com/mir-protocol/plonky2.git", rev = "1ff6d4a2839a0cd16598a5db263568885a47e7c9" } anyhow = { version = "1.0.71", features = ["backtrace"] } clap = {version = "4.2.7", features = ["derive"] } diff --git a/eth_test_parser/src/config.rs b/eth_test_parser/src/config.rs index 1277886..4085cd2 100644 --- a/eth_test_parser/src/config.rs +++ b/eth_test_parser/src/config.rs @@ -1,5 +1,6 @@ pub(crate) const ETH_TESTS_REPO_URL: &str = "https://github.com/ethereum/tests.git"; pub(crate) const ETH_TESTS_REPO_LOCAL_PATH: &str = "eth_tests"; +pub(crate) const GENERAL_GROUP: &str = "BlockchainTests"; pub(crate) const TEST_GROUPS: [&str; 1] = ["GeneralStateTests"]; // The following subgroups contain subfolders unlike the other test folders. pub(crate) const SPECIAL_TEST_SUBGROUPS: [&str; 2] = ["Shanghai", "VMTests"]; diff --git a/eth_test_parser/src/deserialize.rs b/eth_test_parser/src/deserialize.rs index 5bd9a60..6a4b0fa 100644 --- a/eth_test_parser/src/deserialize.rs +++ b/eth_test_parser/src/deserialize.rs @@ -1,9 +1,11 @@ #![allow(dead_code)] -use std::collections::HashMap; use std::str::FromStr; +use std::{collections::HashMap, marker::PhantomData}; +use anyhow::Result; use ethereum_types::{Address, H160, H256, U256, U512}; use hex::FromHex; +use serde::de::MapAccess; use serde::{ de::{Error, Visitor}, Deserialize, Deserializer, @@ -17,24 +19,15 @@ use serde_with::{serde_as, DefaultOnNull, NoneAsEmptyString}; /// 2. The value will be greater than 256 bits. /// /// This helper takes care of stripping that prefix, if it exists, and -/// additionally pads the value with a U512 to catch overflow. Note that this -/// implementation is specific to a Vec<_>; in the event that this syntax is -/// found to occur more often than this particular instance -/// (`transaction.value`), this logic should be broken out to be modular. -/// -/// See [this test](https://github.com/ethereum/tests/blob/develop/GeneralStateTests/stTransactionTest/ValueOverflow.json#L197) for a concrete example. -fn vec_eth_big_int_u512<'de, D>(deserializer: D) -> Result, D::Error> +/// additionally pads the value with a U512 to catch overflow. +fn eth_big_int_u512<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { - let s: Vec = Deserialize::deserialize(deserializer)?; + let s: String = Deserialize::deserialize(deserializer)?; const BIG_INT_PREFIX: &str = "0x:bigint "; - s.into_iter() - .map(|s| { - U512::from_str(s.strip_prefix(BIG_INT_PREFIX).unwrap_or(&s)).map_err(D::Error::custom) - }) - .collect() + U512::from_str(s.strip_prefix(BIG_INT_PREFIX).unwrap_or(&s)).map_err(D::Error::custom) } #[derive(Deserialize, Debug)] @@ -95,39 +88,58 @@ where .collect::, D::Error>>() } -#[derive(Deserialize, Debug)] +fn bloom_array_from_hex<'de, D>(deserializer: D) -> Result<[U256; 8], D::Error> +where + D: Deserializer<'de>, +{ + let s: String = Deserialize::deserialize(deserializer)?; + let mut bloom = [U256::zero(); 8]; + + for (b, c) in bloom + .iter_mut() + .zip(s[2..].chars().collect::>().chunks(64)) + { + *b = U256::from_str_radix(&c.iter().collect::(), 16).map_err(D::Error::custom)?; + } + + Ok(bloom) +} + +#[derive(Deserialize, Clone, Debug, Default)] #[serde(rename_all = "camelCase")] -pub(crate) struct Env { - pub(crate) current_base_fee: U256, - pub(crate) current_coinbase: H160, - pub(crate) current_difficulty: U256, - pub(crate) current_gas_limit: U256, - pub(crate) current_number: U256, - pub(crate) current_random: U256, - pub(crate) current_timestamp: U256, - pub(crate) previous_hash: H256, +pub(crate) struct BlockHeader { + #[serde(default)] + pub(crate) base_fee_per_gas: Option, + #[serde(deserialize_with = "bloom_array_from_hex")] + pub(crate) bloom: [U256; 8], + pub(crate) coinbase: H160, + pub(crate) difficulty: U256, + pub(crate) gas_limit: U256, + pub(crate) gas_used: U256, + pub(crate) number: U256, + pub(crate) mix_hash: H256, + pub(crate) receipt_trie: H256, + pub(crate) state_root: H256, + pub(crate) transactions_trie: H256, + pub(crate) timestamp: U256, } -#[derive(Deserialize, Debug)] -pub(crate) struct PostStateIndexes { - pub(crate) data: usize, - pub(crate) gas: usize, - pub(crate) value: usize, +#[derive(Deserialize, Clone, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub(crate) struct BlockHeaderOnlyRoot { + pub(crate) state_root: H256, } #[derive(Deserialize, Debug)] -pub(crate) struct PostState { - pub(crate) hash: H256, - pub(crate) indexes: PostStateIndexes, - pub(crate) logs: H256, - pub(crate) txbytes: ByteString, +#[serde(rename_all = "camelCase")] +pub(crate) struct Block { + pub(crate) block_header: Option, + pub(crate) rlp: Rlp, + pub(crate) transactions: Option>, } #[derive(Deserialize, Debug)] -#[serde(rename_all = "PascalCase")] -pub(crate) struct Post { - pub(crate) shanghai: Vec, -} +pub(crate) struct Rlp(pub(crate) ByteString); #[derive(Deserialize, Debug)] pub(crate) struct PreAccount { @@ -147,7 +159,7 @@ pub(crate) struct AccessList { } #[serde_as] -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Default)] /// This is a wrapper around a `Vec` that is used to deserialize a /// `null` into an empty vec. pub(crate) struct AccessListsInner( @@ -160,28 +172,98 @@ pub(crate) struct AccessListsInner( pub(crate) struct Transaction { #[serde(default)] pub(crate) access_lists: Vec, - pub(crate) data: Vec, - #[serde(deserialize_with = "vec_u64_from_hex")] - pub(crate) gas_limit: Vec, + pub(crate) data: ByteString, + pub(crate) max_fee_per_gas: Option, + pub(crate) max_priority_fee_per_gas: Option, + #[serde(deserialize_with = "u64_from_hex")] + pub(crate) gas_limit: u64, pub(crate) gas_price: Option, pub(crate) nonce: U256, - pub(crate) secret_key: H256, pub(crate) sender: H160, #[serde_as(as = "NoneAsEmptyString")] pub(crate) to: Option, + pub(crate) r: U256, + pub(crate) s: U256, + pub(crate) v: U256, // Protect against overflow. - #[serde(deserialize_with = "vec_eth_big_int_u512")] - pub(crate) value: Vec, + #[serde(deserialize_with = "eth_big_int_u512")] + pub(crate) value: U512, } +#[derive(Debug)] +pub(crate) struct TestBodyCompact(pub(crate) Vec); + +#[serde_as] #[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] pub(crate) struct TestBody { - pub(crate) env: Env, - pub(crate) post: Post, - pub(crate) transaction: Transaction, + pub(crate) blocks: Vec, + pub(crate) genesis_block_header: BlockHeaderOnlyRoot, pub(crate) pre: HashMap, } +// Wrapper around a regular `HashMap` used to conveniently skip +// non-Shanghai related tests when deserializing. +#[derive(Default, Debug)] +pub(crate) struct TestFile(pub(crate) HashMap); + +impl<'de> Deserialize<'de> for TestFile { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct TestFileVisitor { + marker: PhantomData TestFile>, + } + + impl TestFileVisitor { + fn new() -> Self { + TestFileVisitor { + marker: PhantomData, + } + } + } + + impl<'de> Visitor<'de> for TestFileVisitor { + // The type that our Visitor is going to produce. + type Value = TestFile; + + // Format a message stating what data this Visitor expects to receive. + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + formatter.write_str("a very special map") + } + + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'de>, + { + let mut map = TestFile(HashMap::with_capacity(access.size_hint().unwrap_or(0))); + + // While there are entries remaining in the input, add them + // into our map if they contain `Shanghai` in their key name. + while let Some(key) = access.next_key::()? { + if key.contains("Shanghai") { + // Remove the needless suffix. + let value = access.next_value::()?; + // Some tests have no transactions in the clear, only the txn RLP. + // TODO: handle those tests through txn RLP decoding + sender recovery from + // signature fields. + if value.blocks[0].transactions.is_some() { + map.0.insert(key, value); + } + } else { + let _ = access.next_value::()?; + } + } + + Ok(map) + } + } + + deserializer.deserialize_map(TestFileVisitor::new()) + } +} + #[cfg(test)] mod tests { use super::ByteString; diff --git a/eth_test_parser/src/eth_tests_fetching.rs b/eth_test_parser/src/eth_tests_fetching.rs index e5bbb72..fde72c1 100644 --- a/eth_test_parser/src/eth_tests_fetching.rs +++ b/eth_test_parser/src/eth_tests_fetching.rs @@ -3,7 +3,9 @@ use std::{fs, path::Path, process::Command}; use crate::{ - config::{ETH_TESTS_REPO_LOCAL_PATH, ETH_TESTS_REPO_URL, SPECIAL_TEST_SUBGROUPS, TEST_GROUPS}, + config::{ + ETH_TESTS_REPO_LOCAL_PATH, ETH_TESTS_REPO_URL, GENERAL_GROUP, SPECIAL_TEST_SUBGROUPS, + }, fs_scaffolding::get_test_group_dirs, utils::run_cmd, }; @@ -94,7 +96,7 @@ fn download_remote_tests() { println!( "Setting sparse checkout for test groups... ({})", - TEST_GROUPS.join(", ") + GENERAL_GROUP ); // sparse-checkout out the relevant test group folders. run_cmd(Command::new("git").args([ @@ -102,7 +104,7 @@ fn download_remote_tests() { ETH_TESTS_REPO_LOCAL_PATH, "sparse-checkout", "set", - &TEST_GROUPS.join(" "), + GENERAL_GROUP, ])) .unwrap(); } diff --git a/eth_test_parser/src/fs_scaffolding.rs b/eth_test_parser/src/fs_scaffolding.rs index 35e4de8..2bf5ae2 100644 --- a/eth_test_parser/src/fs_scaffolding.rs +++ b/eth_test_parser/src/fs_scaffolding.rs @@ -1,7 +1,6 @@ //! Filesystem helpers. A set of convenience functions for interacting with test //! input and output directories. use std::{ - collections::HashMap, fs::{self, DirEntry, File}, io::BufReader, path::{Path, PathBuf}, @@ -10,8 +9,10 @@ use std::{ use anyhow::{anyhow, Result}; use common::config::GENERATION_INPUTS_DEFAULT_OUTPUT_DIR; -use crate::config::{ETH_TESTS_REPO_LOCAL_PATH, TEST_GROUPS}; -use crate::deserialize::TestBody; +use crate::{ + config::{ETH_TESTS_REPO_LOCAL_PATH, GENERAL_GROUP, TEST_GROUPS}, + deserialize::{TestBody, TestFile}, +}; /// Get the default parsed test output directory. /// We first check if the flat file, `ETH_TEST_PARSER_DEV`, exists @@ -49,7 +50,7 @@ pub(crate) fn get_default_out_dir() -> anyhow::Result { /// // │   └── {test_case_n}.json /// ``` pub(crate) fn get_test_group_dirs() -> Result> { - let dirs = fs::read_dir(ETH_TESTS_REPO_LOCAL_PATH)? + let dirs = fs::read_dir(ETH_TESTS_REPO_LOCAL_PATH.to_owned() + "/" + GENERAL_GROUP)? .flatten() .filter(|entry| match entry.file_name().to_str() { Some(file_name) => TEST_GROUPS.contains(&file_name), @@ -112,7 +113,7 @@ pub(crate) fn prepare_output_dir(out_path: &Path) -> Result<()> { /// Generate an iterator containing the deserialized test bodies (`TestBody`) /// and their `DirEntry`s. pub(crate) fn get_deserialized_test_bodies( -) -> Result>> { +) -> Result), (String, String)>>> { Ok(get_test_files()?.map(|entry| { let test_body = get_deserialized_test_body(&entry) .map_err(|err| (err.to_string(), entry.path().to_string_lossy().to_string()))?; @@ -120,17 +121,9 @@ pub(crate) fn get_deserialized_test_bodies( })) } -fn get_deserialized_test_body(entry: &DirEntry) -> Result { +fn get_deserialized_test_body(entry: &DirEntry) -> Result> { let buf = BufReader::new(File::open(entry.path())?); - let file_json: HashMap = serde_json::from_reader(buf)?; - - // Each test JSON always contains a single outer key containing the test name. - // The test name is irrelevant for deserialization purposes, so we always drop - // it. - let test_body = file_json - .into_values() - .next() - .ok_or_else(|| anyhow!("Empty test found: {:?}", entry))?; + let test_file: TestFile = serde_json::from_reader(buf)?; - anyhow::Ok(test_body) + anyhow::Ok(test_file.0.into_values().collect()) } diff --git a/eth_test_parser/src/main.rs b/eth_test_parser/src/main.rs index 4e8d171..eb8735e 100644 --- a/eth_test_parser/src/main.rs +++ b/eth_test_parser/src/main.rs @@ -45,23 +45,30 @@ async fn run(ProgArgs { no_fetch, out_path }: ProgArgs) -> anyhow::Result<()> { let generation_input_handles = get_deserialized_test_bodies()?.filter_map(|res| { match res { - Ok((test_dir_entry, test_body)) => Some(tokio::task::spawn_blocking(move || { - let parsed_test = test_body.as_plonky2_test_input(); - let revm_variants = match test_body.as_serializable_evm_instances() { - Ok(revm_variants) => Some(revm_variants), - Err(err) => { - warn!( - "Unable to generate evm instance for test {} due to error: {}. Skipping!", - test_dir_entry.path().display(), - err - ); + Ok((test_dir_entry, test_bodies)) => Some(tokio::task::spawn_blocking(move || { + let mut plonky2_variants = Vec::with_capacity(test_bodies.len()); + let mut revm_variants = Vec::with_capacity(test_bodies.len()); + for test_body in test_bodies.iter() { + let plonky2_variant = test_body.as_plonky2_test_inputs(); + let revm_variant = match test_body.as_serializable_evm_instance() { + Ok(revm_variant) => Some(revm_variant), + Err(err) => { + warn!( + "Unable to generate evm instance for test {} due to error: {}. Skipping!", + test_dir_entry.path().display(), + err + ); None - } - }; + } + }; + + plonky2_variants.push(plonky2_variant); + revm_variants.push(revm_variant); + } let test_manifest = ParsedTestManifest { - plonky2_variants: parsed_test, - revm_variants, + plonky2_variants, + revm_variants: revm_variants.into_iter().collect(), }; (test_dir_entry, serde_cbor::to_vec(&test_manifest).unwrap()) diff --git a/eth_test_parser/src/revm_builder/env.rs b/eth_test_parser/src/revm_builder/env.rs index 6bd587a..1d89ffa 100644 --- a/eth_test_parser/src/revm_builder/env.rs +++ b/eth_test_parser/src/revm_builder/env.rs @@ -1,144 +1,91 @@ //! Convert a `TestBody` into a `Vec` of [`Env`](revm::primitives::Env). -use anyhow::Result; +use anyhow::{anyhow, Result}; use common::config::MATIC_CHAIN_ID; -use revm::primitives::{BlockEnv, Bytes, CfgEnv, Env, TransactTo, TxEnv}; +use revm::primitives::{BlockEnv, CfgEnv, Env, TransactTo, TxEnv, B160}; +use ruint::Uint; -use crate::deserialize::TestBody; - -struct TxSharedData { - data: Vec, - access_list: Vec)>>, - gas_limit: Vec, - value: Vec, -} +use crate::deserialize::{AccessListsInner, TestBody, Transaction}; impl TestBody { - fn try_as_tx_shared_data(&self) -> Result { - let data = self - .transaction - .data - .iter() - .map(|byte_string| byte_string.0.clone().into()) - .collect(); + fn to_txn_env(transaction: &Transaction) -> Result { + let to = match transaction.to { + Some(to) => TransactTo::Call(to.to_fixed_bytes().into()), + None => TransactTo::Call(Default::default()), + }; + let v = transaction.value; + let value: ethereum_types::U256 = v + .try_into() + .expect("Unable to convert transaction.value to U256"); - let access_list = self - .transaction + let access_list = transaction .access_lists + .get(0) + .unwrap_or(&AccessListsInner::default()) + .0 .iter() - .map(|access_list| { - access_list - .0 - .iter() - .map(|x| { - ( - x.address.to_fixed_bytes().into(), - x.storage_keys.iter().map(|x| (*x).into()).collect(), - ) - }) - .collect() + .map(|x| { + ( + B160::from(x.address.to_fixed_bytes()), + x.storage_keys + .iter() + .map(|x| (*x).into()) + .collect::>>(), + ) }) .collect(); - let gas_limit = self.transaction.gas_limit.to_vec(); - - let value = self - .transaction - .value - .iter() - .enumerate() - .map(|(i, v)| { - let u256: ethereum_types::U256 = v.try_into().map_err(|_| { - anyhow::Error::msg("Overflow").context(format!( - "Unable to convert transaction.value[{i}] to U256. Got {v}" - )) - })?; - Ok(u256.into()) - }) - .collect::>>()?; - - Ok(TxSharedData { - data, + Ok(TxEnv { + caller: transaction.sender.to_fixed_bytes().into(), + gas_limit: transaction.gas_limit, + gas_price: transaction.gas_price.map(|p| p.into()).unwrap_or_default(), + gas_priority_fee: None, + transact_to: to, + value: value.into(), + data: transaction.data.0.clone().into(), + chain_id: Some(MATIC_CHAIN_ID), + nonce: transaction.nonce.try_into().ok(), + // `access_list` is defined parallel to `transaction.data` in the test + // filler definitions. + // https://ethereum-tests.readthedocs.io/en/latest/test_filler/test_transaction_state.html?highlight=access#fields access_list, - gas_limit, - value, }) } - pub(crate) fn as_revm_env(&self) -> Result> { + pub(crate) fn as_revm_env(&self) -> Result { let cfg = CfgEnv { chain_id: MATIC_CHAIN_ID.try_into()?, ..Default::default() }; - let block = BlockEnv { - number: self.env.current_number.into(), - coinbase: self.env.current_coinbase.to_fixed_bytes().into(), - timestamp: self.env.current_timestamp.into(), - difficulty: self.env.current_difficulty.into(), - prevrandao: Some( - >::into( - self.env.current_difficulty, - ) - .to_be_bytes() - .into(), - ), - basefee: self.env.current_base_fee.into(), - gas_limit: self.env.current_gas_limit.into(), - }; - - let gas_price = self - .transaction - .gas_price - .map(|p| p.into()) - .unwrap_or_default(); + if self.blocks.is_empty() { + return Err(anyhow!("No block")); + } - let transact_to = match self.transaction.to { - Some(to) => TransactTo::Call(to.to_fixed_bytes().into()), - None => TransactTo::Call(Default::default()), - }; + let block = &self.blocks[0]; - let tx_shared_data: TxSharedData = self.try_into()?; + let transaction = &block.transactions.as_ref().unwrap()[0]; - Ok(self - .post - .shanghai - .iter() - .map(|m| Env { - cfg: cfg.clone(), - block: block.clone(), - tx: TxEnv { - caller: self.transaction.sender.to_fixed_bytes().into(), - gas_limit: tx_shared_data.gas_limit[m.indexes.gas], - gas_price, - gas_priority_fee: None, - transact_to: transact_to.clone(), - value: tx_shared_data.value[m.indexes.value], - data: tx_shared_data.data[m.indexes.data].clone(), - chain_id: Some(MATIC_CHAIN_ID), - nonce: self.transaction.nonce.try_into().ok(), - // `access_list` is defined parallel to `transaction.data` in the test filler - // definitions. - // https://ethereum-tests.readthedocs.io/en/latest/test_filler/test_transaction_state.html?highlight=access#fields - access_list: tx_shared_data - .access_list - .get(m.indexes.data) - .cloned() - .unwrap_or_default(), - }, - }) - .collect()) - } -} + let block_header = &block.block_header.clone().unwrap_or_default(); -impl TryFrom<&TestBody> for TxSharedData { - type Error = anyhow::Error; + let block = BlockEnv { + number: block_header.number.into(), + coinbase: block_header.coinbase.to_fixed_bytes().into(), + timestamp: block_header.timestamp.into(), + difficulty: block_header.difficulty.into(), + prevrandao: Some(block_header.mix_hash.into()), + basefee: block_header.base_fee_per_gas.unwrap_or_default().into(), + gas_limit: block_header.gas_limit.into(), + }; - fn try_from(body: &TestBody) -> Result { - body.try_as_tx_shared_data() + Ok(Env { + cfg, + block, + tx: TestBody::to_txn_env(transaction).unwrap(), + }) } } -impl TryFrom<&TestBody> for Vec { +impl TryFrom<&TestBody> for Env { type Error = anyhow::Error; fn try_from(body: &TestBody) -> Result { diff --git a/eth_test_parser/src/revm_builder/mod.rs b/eth_test_parser/src/revm_builder/mod.rs index f80cf59..f19c651 100644 --- a/eth_test_parser/src/revm_builder/mod.rs +++ b/eth_test_parser/src/revm_builder/mod.rs @@ -20,24 +20,18 @@ mod cache_db; mod env; impl TestBody { - pub(crate) fn as_serializable_evm_instances(&self) -> Result> { - let envs = self.as_revm_env()?; + pub(crate) fn as_serializable_evm_instance(&self) -> Result { + let env = self.as_revm_env()?; let db = self.as_revm_cache_db()?; - Ok(envs - .into_iter() - .map(|env| SerializableEVMInstance { - env, - db: db.clone(), - }) - .collect()) + Ok(SerializableEVMInstance { env, db }) } } -impl TryFrom for Vec { +impl TryFrom for SerializableEVMInstance { type Error = anyhow::Error; fn try_from(body: TestBody) -> Result { - body.as_serializable_evm_instances() + body.as_serializable_evm_instance() } } diff --git a/eth_test_parser/src/trie_builder.rs b/eth_test_parser/src/trie_builder.rs index f88a1fb..17b4bae 100644 --- a/eth_test_parser/src/trie_builder.rs +++ b/eth_test_parser/src/trie_builder.rs @@ -9,8 +9,8 @@ use std::collections::HashMap; use anyhow::Result; use common::{ - config, - types::{ConstGenerationInputs, Plonky2ParsedTest, TestVariant, TestVariantCommon}, + config::ETHEREUM_CHAIN_ID, + types::{ExpectedFinalRoots, Plonky2ParsedTest, TestMetadata}, }; use eth_trie_utils::{ nibbles::Nibbles, @@ -18,11 +18,20 @@ use eth_trie_utils::{ }; use ethereum_types::{Address, H256, U256}; use keccak_hash::keccak; -use plonky2_evm::{generation::TrieInputs, proof::BlockMetadata}; +use plonky2_evm::{ + generation::{ + mpt::{ + AccessListItemRlp, AccessListTransactionRlp, AddressOption, FeeMarketTransactionRlp, + LegacyTransactionRlp, + }, + TrieInputs, + }, + proof::BlockMetadata, +}; use rlp::Encodable; use rlp_derive::{RlpDecodable, RlpEncodable}; -use crate::deserialize::{Env, TestBody}; +use crate::deserialize::{AccessListsInner, Block, TestBody}; #[derive(RlpDecodable, RlpEncodable)] pub(crate) struct AccountRlp { @@ -32,34 +41,35 @@ pub(crate) struct AccountRlp { code_hash: H256, } -impl Env { +impl Block { fn block_metadata(&self) -> BlockMetadata { + let header = self.block_header.clone().unwrap_or_default(); BlockMetadata { - block_beneficiary: self.current_coinbase, - block_timestamp: self.current_timestamp, - block_number: self.current_number, - block_difficulty: self.current_difficulty, - block_gaslimit: self.current_gas_limit, - block_chain_id: config::ETHEREUM_CHAIN_ID.into(), - block_base_fee: self.current_base_fee, + block_beneficiary: header.coinbase, + block_timestamp: header.timestamp, + block_number: header.number, + block_difficulty: header.difficulty, + block_gaslimit: header.gas_limit, + block_chain_id: ETHEREUM_CHAIN_ID.into(), + block_base_fee: header.base_fee_per_gas.unwrap_or_default(), + block_random: header.mix_hash, + block_gas_used: header.gas_used, + block_bloom: header.bloom, } } } impl TestBody { - pub fn as_plonky2_test_input(&self) -> Plonky2ParsedTest { + pub fn as_plonky2_test_inputs(&self) -> Plonky2ParsedTest { + let block = &self.blocks[0]; + let storage_tries = self.get_storage_tries(); let state_trie = self.get_state_trie(&storage_tries); let tries = TrieInputs { state_trie, - transactions_trie: HashedPartialTrie::default(), /* TODO: Change to - * self.get_txn_trie() - * once - * zkEVM supports it */ - receipts_trie: HashedPartialTrie::default(), /* TODO: Fill in once we know what we - * are - * doing... */ + transactions_trie: HashedPartialTrie::default(), + receipts_trie: HashedPartialTrie::default(), storage_tries, }; @@ -69,30 +79,26 @@ impl TestBody { .map(|pre| (hash(&pre.code.0), pre.code.0.clone())) .collect(); - let test_variants = self - .post - .shanghai - .iter() - .map(|x| TestVariant { - txn_bytes: x.txbytes.0.clone(), - common: TestVariantCommon { - expected_final_account_state_root_hash: x.hash, - }, - }) - .collect(); + let header = block.block_header.clone().unwrap_or_default(); let addresses = self.pre.keys().copied().collect::>(); - let const_plonky2_inputs = ConstGenerationInputs { + let plonky2_metadata = TestMetadata { tries, contract_code, - block_metadata: self.env.block_metadata(), + genesis_state_root: self.genesis_block_header.state_root, + block_metadata: self.blocks[0].block_metadata(), addresses, }; Plonky2ParsedTest { - test_variants, - const_plonky2_inputs, + txn_bytes: self.get_txn_bytes(), + final_roots: ExpectedFinalRoots { + state_root_hash: header.state_root, + txn_trie_root_hash: header.transactions_trie, + receipts_trie_root_hash: header.receipt_trie, + }, + plonky2_metadata, } } @@ -138,25 +144,114 @@ impl TestBody { .collect() } - #[allow(unused)] // TODO: Will be used later. - fn get_txn_trie(&self) -> HashedPartialTrie { - self.post - .shanghai - .iter() - .enumerate() - .map(|(txn_idx, post)| { - ( - Nibbles::from_bytes_be(&txn_idx.to_be_bytes()).unwrap(), - post.txbytes.0.clone(), - ) - }) - .collect() + // #[allow(unused)] // TODO: Will be used later. + // fn get_txn_trie(&self) -> HashedPartialTrie { + // self.post + // .0 + // .iter() + // .enumerate() + // .map(|(txn_idx, post)| { + // ( + // Nibbles::from_bytes_be(&txn_idx.to_be_bytes()).unwrap(), + // post.txbytes.0.clone(), + // ) + // }) + // .collect() + // } + + fn get_txn_bytes(&self) -> Vec { + let transaction = &self.blocks[0].transactions.as_ref().unwrap()[0]; + match transaction.max_priority_fee_per_gas { + None => { + if transaction.access_lists.is_empty() { + // Try legacy transaction, and check + let txn = LegacyTransactionRlp { + nonce: transaction.nonce, + gas_price: transaction.gas_price.unwrap_or_default(), + gas: transaction.gas_limit.into(), + to: AddressOption(transaction.to), + value: transaction.value.try_into().unwrap(), + data: transaction.data.0.clone().into(), + v: transaction.v, + r: transaction.r, + s: transaction.s, + }; + + rlp::encode(&txn).to_vec() + } else { + let txn = AccessListTransactionRlp { + access_list: transaction + .access_lists + .get(0) + .unwrap_or(&AccessListsInner::default()) + .0 + .iter() + .map(|l| AccessListItemRlp { + address: l.address, + storage_keys: l.storage_keys.clone(), + }) + .collect(), + chain_id: ETHEREUM_CHAIN_ID, + nonce: transaction.nonce, + gas_price: transaction.gas_price.unwrap_or_default(), + gas: transaction.gas_limit.into(), + to: AddressOption(transaction.to), + value: transaction.value.try_into().unwrap(), + data: transaction.data.0.clone().into(), + y_parity: transaction.v, + r: transaction.r, + s: transaction.s, + }; + + let rlp = rlp::encode(&txn).to_vec(); + let mut output = Vec::with_capacity(rlp.len() + 1); + output.push(0x01); + output.extend(&rlp); + + output + } + } + Some(max_priority_fee_per_gas) => { + // Type 2 (FeeMarket) transaction + let txn = FeeMarketTransactionRlp { + access_list: transaction + .access_lists + .get(0) + .unwrap_or(&AccessListsInner::default()) + .0 + .iter() + .map(|l| AccessListItemRlp { + address: l.address, + storage_keys: l.storage_keys.clone(), + }) + .collect(), + chain_id: ETHEREUM_CHAIN_ID, + nonce: transaction.nonce, + max_priority_fee_per_gas, + max_fee_per_gas: transaction.max_fee_per_gas.unwrap(), + gas: transaction.gas_limit.into(), + to: AddressOption(transaction.to), + value: transaction.value.try_into().unwrap(), + data: transaction.data.0.clone().into(), + y_parity: transaction.v, + r: transaction.r, + s: transaction.s, + }; + + let rlp = rlp::encode(&txn).to_vec(); + let mut output = Vec::with_capacity(rlp.len() + 1); + output.push(0x02); + output.extend(&rlp); + + output + } + } } } impl From for Plonky2ParsedTest { fn from(test_body: TestBody) -> Self { - test_body.as_plonky2_test_input() + test_body.as_plonky2_test_inputs() } } diff --git a/evm_test_runner/Cargo.toml b/evm_test_runner/Cargo.toml index d7751d6..6ac6999 100644 --- a/evm_test_runner/Cargo.toml +++ b/evm_test_runner/Cargo.toml @@ -9,8 +9,8 @@ edition = "2021" [dependencies] common = { path = "../common" } -plonky2 = "0.1.4" # NOTE: Make sure that this version matches the one used by plonky2_evm when bumping to newer versions. -plonky2_evm = { git = "https://github.com/mir-protocol/plonky2.git", rev = "6f98fd762885e9b5343af5f0e3f0c9c90e8cf3ab" } +plonky2 = { git = "https://github.com/mir-protocol/plonky2.git", rev = "1ff6d4a2839a0cd16598a5db263568885a47e7c9" } +plonky2_evm = { git = "https://github.com/mir-protocol/plonky2.git", rev = "1ff6d4a2839a0cd16598a5db263568885a47e7c9" } anyhow = { version = "1.0", features = ["backtrace"] } askama = "0.12.0" diff --git a/evm_test_runner/src/main.rs b/evm_test_runner/src/main.rs index 3f3f4b4..5685171 100644 --- a/evm_test_runner/src/main.rs +++ b/evm_test_runner/src/main.rs @@ -22,7 +22,6 @@ mod arg_parsing; mod persistent_run_state; mod plonky2_runner; mod report_generation; -mod state_diff; mod test_dir_reading; // Oneshot is ideal here, but I can't get it to the abort handler. diff --git a/evm_test_runner/src/persistent_run_state.rs b/evm_test_runner/src/persistent_run_state.rs index 0f4be5b..22fd881 100644 --- a/evm_test_runner/src/persistent_run_state.rs +++ b/evm_test_runner/src/persistent_run_state.rs @@ -102,9 +102,7 @@ impl From for PassState { match v { TestStatus::Passed => PassState::Passed, TestStatus::Ignored => PassState::Ignored, - TestStatus::EvmErr(_) - | TestStatus::IncorrectAccountFinalState(_) - | TestStatus::TimedOut => PassState::Failed, + TestStatus::EvmErr(_) | TestStatus::TimedOut => PassState::Failed, } } } diff --git a/evm_test_runner/src/plonky2_runner.rs b/evm_test_runner/src/plonky2_runner.rs index ba6dafb..ae72a36 100644 --- a/evm_test_runner/src/plonky2_runner.rs +++ b/evm_test_runner/src/plonky2_runner.rs @@ -7,7 +7,7 @@ use std::{ }; use common::types::TestVariantRunInfo; -use ethereum_types::H256; +use ethereum_types::U256; use futures::executor::block_on; use indicatif::{ProgressBar, ProgressStyle}; use log::trace; @@ -16,13 +16,12 @@ use plonky2::{ util::timing::TimingTree, }; use plonky2_evm::{ - all_stark::AllStark, config::StarkConfig, prover::prove_with_outputs, verifier::verify_proof, + all_stark::AllStark, config::StarkConfig, prover::prove, verifier::verify_proof, }; use tokio::{select, time::timeout}; use crate::{ persistent_run_state::TestRunEntries, - state_diff::StateDiff, test_dir_reading::{ParsedTestGroup, ParsedTestSubGroup, Test}, ProcessAbortedRecv, }; @@ -76,7 +75,6 @@ pub(crate) enum TestStatus { Passed, Ignored, EvmErr(String), - IncorrectAccountFinalState(TrieFinalStateDiff), TimedOut, } @@ -86,52 +84,11 @@ impl Display for TestStatus { TestStatus::Passed => write!(f, "Passed"), TestStatus::Ignored => write!(f, "Ignored"), TestStatus::EvmErr(err) => write!(f, "Evm error: {}", err), - TestStatus::IncorrectAccountFinalState(diff) => { - write!(f, "Expected trie hash mismatch: {}", diff) - } TestStatus::TimedOut => write!(f, "Test timed out"), } } } -/// If one or more trie hashes are different from the expected, then we return a -/// diff showing which tries where different. -#[derive(Clone, Debug)] -pub(crate) struct TrieFinalStateDiff { - state: TrieComparisonResult, - receipt: TrieComparisonResult, - transaction: TrieComparisonResult, -} - -/// A result of comparing the actual outputted `plonky2` trie to the one -/// expected by the test. -#[derive(Clone, Debug)] -enum TrieComparisonResult { - Correct, - Difference(H256, H256), -} - -impl Display for TrieComparisonResult { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Correct => write!(f, "Correct"), - Self::Difference(actual, expected) => { - write!(f, "Difference (Actual: {}, Expected: {})", actual, expected) - } - } - } -} - -impl Display for TrieFinalStateDiff { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "State: {}, Receipt: {}, Transaction: {}", - self.state, self.receipt, self.transaction - ) - } -} - impl TestStatus { pub(crate) fn passed(&self) -> bool { matches!(self, TestStatus::Passed) @@ -298,31 +255,41 @@ fn run_test_or_fail_on_timeout( fn run_test_and_get_test_result(test: TestVariantRunInfo) -> TestStatus { let timing = TimingTree::new("prove", log::Level::Debug); - let proof_run_res = prove_with_outputs::( + // Because many tests use a gas limit that isn't supported by plonky2 zkEVM, + // we manually reduce the gas limit for those to `u32::MAX`, and ignore the ones + // for which the gas used would be greater. + let mut gen_inputs = test.gen_inputs.clone(); + if TryInto::::try_into(gen_inputs.block_metadata.block_gas_used).is_err() { + // This test cannot be proven by plonky2 zkEVM. + return TestStatus::Ignored; + } + + if TryInto::::try_into(test.gen_inputs.block_metadata.block_gaslimit).is_err() { + // Set gaslimit to the largest integer supported by plonky2 zkEVM for this. + gen_inputs.block_metadata.block_gaslimit = U256::from(u32::MAX); + } + + let proof_run_res = prove::( &AllStark::default(), &StarkConfig::standard_fast_config(), - test.gen_inputs.clone(), + gen_inputs, &mut TimingTree::default(), ); timing.filter(Duration::from_millis(100)).print(); - let (proof_run_output, generation_outputs) = match proof_run_res { + let proof_run_output = match proof_run_res { Ok(v) => v, Err(evm_err) => { - if evm_err.to_string().contains("GasLimitError") - && TryInto::::try_into(test.gen_inputs.block_metadata.block_gaslimit).is_err() - { - // Gas limit of more than 32 bits is not supported by the zkEVM. + if TryInto::::try_into(test.gen_inputs.block_metadata.block_gaslimit).is_err() { + // We manually altered the gaslimit to try running this test, so we ignore it as + // failure cannot be 100% attributed to the zkEVM behavior. return TestStatus::Ignored; - } else { - return TestStatus::EvmErr(evm_err.to_string()); - }; + } + return TestStatus::EvmErr(evm_err.to_string()); } }; - let actual_state_trie_hash = proof_run_output.public_values.trie_roots_after.state_root; - if verify_proof( &AllStark::default(), proof_run_output, @@ -333,31 +300,5 @@ fn run_test_and_get_test_result(test: TestVariantRunInfo) -> TestStatus { return TestStatus::EvmErr("Proof verification failed.".to_string()); } - if actual_state_trie_hash != test.common.expected_final_account_state_root_hash { - if let Some(serialized_revm_variant) = test.revm_variant { - let instance = serialized_revm_variant.into_hydrated(); - let expected_state = instance.transact_ref().map(|result| result.state); - if let Ok(state) = expected_state { - let state_diff = StateDiff::new(state, generation_outputs.accounts); - // TODO: Make this optional / configurable - println!("{}", state_diff); - } - } - - let trie_diff = TrieFinalStateDiff { - state: TrieComparisonResult::Difference( - actual_state_trie_hash, - test.common.expected_final_account_state_root_hash, - ), - receipt: TrieComparisonResult::Correct, // TODO... - transaction: TrieComparisonResult::Correct, // TODO... - }; - - return TestStatus::IncorrectAccountFinalState(trie_diff); - } - - // TODO: Also check receipt and txn hashes once these are provided by the - // parser... - TestStatus::Passed } diff --git a/evm_test_runner/src/state_diff.rs b/evm_test_runner/src/state_diff.rs deleted file mode 100644 index 1bd2a8e..0000000 --- a/evm_test_runner/src/state_diff.rs +++ /dev/null @@ -1,157 +0,0 @@ -use std::{collections::HashMap, fmt::Display}; - -use console::{style, Style}; -use ethereum_types::{Address, H160, H256, U256}; -use keccak_hash::keccak; -use plonky2_evm::generation::outputs::{AccountOutput, AddressOrStateKey}; -use revm::primitives::{Account, B160}; -use similar::{ChangeTag, TextDiff}; - -#[derive(Debug, Clone)] -pub(crate) struct StateDiff { - revm_state: HashMap, - plonky2_state: HashMap, -} - -#[derive(Eq, PartialEq, PartialOrd, Ord, Hash, Debug, Clone)] -/// Normalized representation of an account. -struct AccountCompare { - balance: U256, - nonce: u64, - storage: Vec<(U256, U256)>, -} - -impl StateDiff { - /// Construct a new [`StateDiff`](crate::state_diff::StateDiff). - /// - /// We normalize both the revm and plonky2 states into `AccountCompare` to - /// make them comparable. - pub(crate) fn new( - revm_state: revm::primitives::HashMap, - plonky2_state: HashMap, - ) -> Self { - // Store a lookup table from keccak hashes to addresses in the event - // that plonky2 is missing an address from its output. - let keccak_address_lookup: HashMap = revm_state - .keys() - .map(|k| (keccak(k.to_fixed_bytes()), H160::from(k.to_fixed_bytes()))) - .collect(); - - let revm_state = revm_state - .into_iter() - .map(|(k, v)| { - let mut storage = v - .storage - .into_iter() - .map(|(k, v)| { - ( - keccak(k.to_be_bytes::<32>()).0.into(), - v.present_value.into(), - ) - }) - .collect::>(); - storage.sort_by(|a, b| b.0.cmp(&a.0)); - - ( - k.to_fixed_bytes().into(), - AccountCompare { - balance: v.info.balance.into(), - nonce: v.info.nonce, - storage, - }, - ) - }) - .collect(); - - let plonky2_state = plonky2_state - .into_iter() - .map(|(k, v)| { - let address = match k { - AddressOrStateKey::Address(a) => a, - // If the address is missing from the plonky2 output, we can look it up in the - // keccak table. - AddressOrStateKey::StateKey(k) => keccak_address_lookup[&k], - }; - - let mut storage = v - .storage - .into_iter() - .map(|(k, v)| (k, v)) - .collect::>(); - storage.sort_by(|a, b| b.0.cmp(&a.0)); - - ( - address, - AccountCompare { - balance: v.balance, - nonce: v.nonce, - storage, - }, - ) - }) - .collect(); - - Self { - revm_state, - plonky2_state, - } - } -} - -struct Line(Option); - -impl Display for Line { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self.0 { - None => write!(f, " "), - Some(idx) => write!(f, "{:<4}", idx + 1), - } - } -} - -impl Display for StateDiff { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut fst = self.revm_state.iter().collect::>(); - fst.sort_by_key(|(address, _)| (self.plonky2_state.contains_key(address), *address)); - let mut snd = self.plonky2_state.iter().collect::>(); - snd.sort_by_key(|(address, _)| (self.revm_state.contains_key(address), *address)); - - let text1 = format!("{:#?}", fst); - let text2 = format!("{:#?}", snd); - let diff = TextDiff::from_lines(text1.as_str(), text2.as_str()); - - for (idx, group) in diff.grouped_ops(10).iter().enumerate() { - if idx > 0 { - writeln!(f, "{:-^1$}", "-", 80)?; - } - for op in group { - for change in diff.iter_inline_changes(op) { - let (sign, s) = match change.tag() { - ChangeTag::Delete => ("-", Style::new().red()), - ChangeTag::Insert => ("+", Style::new().green()), - ChangeTag::Equal => (" ", Style::new().dim()), - }; - write!( - f, - "{}{} |{}", - style(Line(change.old_index())).dim(), - style(Line(change.new_index())).dim(), - s.apply_to(sign).bold(), - )?; - for (emphasized, value) in change.iter_strings_lossy() { - if emphasized { - write!(f, "{}", s.apply_to(value).underlined().on_black())?; - } else { - write!(f, "{}", s.apply_to(value))?; - } - } - if change.missing_newline() { - writeln!(f)?; - } - } - } - } - - Ok(()) - } -} diff --git a/evm_test_runner/src/test_dir_reading.rs b/evm_test_runner/src/test_dir_reading.rs index 0d65c5a..ed74b67 100644 --- a/evm_test_runner/src/test_dir_reading.rs +++ b/evm_test_runner/src/test_dir_reading.rs @@ -16,7 +16,7 @@ use std::{ use anyhow::{anyhow, Context}; use common::{ - config::GENERATION_INPUTS_DEFAULT_OUTPUT_DIR, + config::{GENERATION_INPUTS_DEFAULT_OUTPUT_DIR, MAIN_TEST_DIR}, types::{ParsedTestManifest, TestVariantRunInfo, VariantFilterType}, }; use log::{info, trace}; @@ -50,6 +50,7 @@ pub(crate) fn get_default_parsed_tests_path() -> anyhow::Result { .map(|ancestor| { let mut buf = ancestor.to_path_buf(); buf.push(GENERATION_INPUTS_DEFAULT_OUTPUT_DIR); + buf.push(MAIN_TEST_DIR); buf }) .find(|path| path.exists())